Replace the create_fee_type/create_cycle helpers duplicated across 18/8 membership-fee test files with a single shared definition in Mv.Fixtures, reconciling the divergent local signatures (including the reversed argument order) into one superset so behavior is unchanged.
207 lines
6.2 KiB
Elixir
207 lines
6.2 KiB
Elixir
defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
|
@moduledoc """
|
|
Integration tests for membership fee cycle generation triggered by member actions.
|
|
"""
|
|
use Mv.DataCase, async: false
|
|
|
|
import Mv.Fixtures, only: [create_fee_type: 2]
|
|
|
|
alias Mv.MembershipFees.MembershipFeeCycle
|
|
|
|
require Ash.Query
|
|
|
|
setup do
|
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
%{actor: system_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
|