defmodule Mv.MembershipFees.MemberCycleIntegrationTest do @moduledoc """ Integration tests for membership fee cycle generation triggered by member actions. """ use Mv.DataCase, async: false alias Mv.MembershipFees.MembershipFeeCycle alias Mv.MembershipFees.MembershipFeeType require Ash.Query setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() %{actor: system_actor} end # Helper to create a membership fee type defp create_fee_type(attrs, actor) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), interval: :yearly } attrs = Map.merge(default_attrs, attrs) MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) |> Ash.create!(actor: actor) end # Helper to set up settings defp setup_settings(include_joining_cycle, actor) do {:ok, settings} = Mv.Membership.get_settings() settings |> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle}) |> Ash.update!(actor: actor) end # Helper to get cycles for a member defp get_member_cycles(member_id, actor) do MembershipFeeCycle |> Ash.Query.filter(member_id == ^member_id) |> Ash.Query.sort(cycle_start: :asc) |> Ash.read!(actor: actor) end describe "member creation triggers cycle generation" do test "creates cycles when member is created with fee type and join_date", %{actor: actor} do setup_settings(true, actor) fee_type = create_fee_type(%{interval: :yearly}, actor) {:ok, member} = Mv.Membership.create_member( %{ first_name: "Test", last_name: "User", email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-03-15], membership_fee_type_id: fee_type.id }, actor: actor ) cycles = get_member_cycles(member.id, actor) # Should have cycles for 2023 and 2024 (and possibly current year) assert length(cycles) >= 2 # Verify cycles have correct data Enum.each(cycles, fn cycle -> assert cycle.member_id == member.id assert cycle.membership_fee_type_id == fee_type.id assert Decimal.equal?(cycle.amount, fee_type.amount) assert cycle.status == :unpaid end) end test "does not create cycles when member has no fee type", %{actor: actor} do setup_settings(true, actor) {:ok, member} = Mv.Membership.create_member( %{ first_name: "Test", last_name: "User", email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-03-15] }, actor: actor ) cycles = get_member_cycles(member.id, actor) assert cycles == [] end test "does not create cycles when member has no join_date", %{actor: actor} do setup_settings(true, actor) fee_type = create_fee_type(%{interval: :yearly}, actor) {:ok, member} = Mv.Membership.create_member( %{ first_name: "Test", last_name: "User", email: "test#{System.unique_integer([:positive])}@example.com", membership_fee_type_id: fee_type.id }, actor: actor ) cycles = get_member_cycles(member.id, actor) assert cycles == [] end end describe "member update triggers cycle generation" do test "generates cycles when fee type is assigned to existing member", %{actor: actor} do setup_settings(true, actor) fee_type = create_fee_type(%{interval: :yearly}, actor) # Create member without fee type {:ok, member} = Mv.Membership.create_member( %{ first_name: "Test", last_name: "User", email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-03-15] }, actor: actor ) # Verify no cycles yet assert get_member_cycles(member.id, actor) == [] # Update to assign fee type {:ok, member} = Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor) cycles = get_member_cycles(member.id, actor) # Should have generated cycles assert length(cycles) >= 2 end end describe "concurrent cycle generation" do test "handles multiple members being created concurrently", %{actor: actor} do setup_settings(true, actor) fee_type = create_fee_type(%{interval: :yearly}, actor) # Create multiple members concurrently tasks = Enum.map(1..5, fn i -> Task.async(fn -> {:ok, member} = Mv.Membership.create_member( %{ first_name: "Test#{i}", last_name: "User#{i}", email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-03-15], membership_fee_type_id: fee_type.id }, actor: actor ) member end) end) members = Enum.map(tasks, &Task.await/1) # Each member should have cycles Enum.each(members, fn member -> cycles = get_member_cycles(member.id, actor) assert length(cycles) >= 2, "Member #{member.id} should have at least 2 cycles" end) end end describe "idempotent cycle generation" do test "running generation multiple times does not create duplicate cycles", %{actor: actor} do setup_settings(true, actor) fee_type = create_fee_type(%{interval: :yearly}, actor) {:ok, member} = Mv.Membership.create_member( %{ first_name: "Test", last_name: "User", email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-03-15], membership_fee_type_id: fee_type.id }, actor: actor ) initial_cycles = get_member_cycles(member.id, actor) initial_count = length(initial_cycles) # Use a fixed "today" date to avoid date dependency # Use a date far enough in the future to ensure all cycles are generated today = ~D[2025-12-31] # Manually trigger generation again with fixed "today" date {:ok, _, _} = Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(member.id, today: today) final_cycles = get_member_cycles(member.id, actor) final_count = length(final_cycles) # Should have same number of cycles (idempotent) assert final_count == initial_count end end end