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 alias Mv.Membership.Member require Ash.Query # Helper to create a membership fee type defp create_fee_type(attrs) 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!() end # Helper to set up settings defp setup_settings(include_joining_cycle) do {:ok, settings} = Mv.Membership.get_settings() settings |> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle}) |> Ash.update!() end # Helper to get cycles for a member defp get_member_cycles(member_id) do MembershipFeeCycle |> Ash.Query.filter(member_id == ^member_id) |> Ash.Query.sort(cycle_start: :asc) |> Ash.read!() end describe "member creation triggers cycle generation" do test "creates cycles when member is created with fee type and join_date" do setup_settings(true) fee_type = create_fee_type(%{interval: :yearly}) member = Member |> Ash.Changeset.for_create(: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 }) |> Ash.create!() cycles = get_member_cycles(member.id) # 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" do setup_settings(true) member = Member |> Ash.Changeset.for_create(:create_member, %{ first_name: "Test", last_name: "User", email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-03-15] # No membership_fee_type_id }) |> Ash.create!() cycles = get_member_cycles(member.id) assert cycles == [] end test "does not create cycles when member has no join_date" do setup_settings(true) fee_type = create_fee_type(%{interval: :yearly}) member = Member |> Ash.Changeset.for_create(:create_member, %{ first_name: "Test", last_name: "User", email: "test#{System.unique_integer([:positive])}@example.com", membership_fee_type_id: fee_type.id # No join_date }) |> Ash.create!() cycles = get_member_cycles(member.id) assert cycles == [] end end describe "member update triggers cycle generation" do test "generates cycles when fee type is assigned to existing member" do setup_settings(true) fee_type = create_fee_type(%{interval: :yearly}) # Create member without fee type member = Member |> Ash.Changeset.for_create(:create_member, %{ first_name: "Test", last_name: "User", email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-03-15] }) |> Ash.create!() # Verify no cycles yet assert get_member_cycles(member.id) == [] # Update to assign fee type member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) |> Ash.update!() cycles = get_member_cycles(member.id) # Should have generated cycles assert length(cycles) >= 2 end end describe "concurrent cycle generation" do test "handles multiple members being created concurrently" do setup_settings(true) fee_type = create_fee_type(%{interval: :yearly}) # Create multiple members concurrently tasks = Enum.map(1..5, fn i -> Task.async(fn -> Member |> Ash.Changeset.for_create(: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 }) |> Ash.create!() 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) 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" do setup_settings(true) fee_type = create_fee_type(%{interval: :yearly}) member = Member |> Ash.Changeset.for_create(: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 }) |> Ash.create!() initial_cycles = get_member_cycles(member.id) 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) final_count = length(final_cycles) # Should have same number of cycles (idempotent) assert final_count == initial_count end end end