All checks were successful
continuous-integration/drone/push Build is passing
- Add CycleGenerator module with advisory lock mechanism - Add SetMembershipFeeStartDate change for auto-calculation - Extend Settings with include_joining_cycle and default_membership_fee_type_id - Add scheduled job skeleton for future Oban integration
224 lines
6.4 KiB
Elixir
224 lines
6.4 KiB
Elixir
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!()
|
|
|
|
# Wait for async cycle generation
|
|
Process.sleep(300)
|
|
|
|
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!()
|
|
|
|
# Wait for potential async cycle generation
|
|
Process.sleep(200)
|
|
|
|
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!()
|
|
|
|
# Wait for potential async cycle generation
|
|
Process.sleep(200)
|
|
|
|
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!()
|
|
|
|
# Wait for async cycle generation
|
|
Process.sleep(300)
|
|
|
|
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)
|
|
|
|
# Wait for all async cycle generations
|
|
Process.sleep(500)
|
|
|
|
# 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!()
|
|
|
|
# Wait for async cycle generation
|
|
Process.sleep(300)
|
|
|
|
initial_cycles = get_member_cycles(member.id)
|
|
initial_count = length(initial_cycles)
|
|
|
|
# Manually trigger generation again
|
|
{:ok, _} = Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(member.id)
|
|
|
|
final_cycles = get_member_cycles(member.id)
|
|
final_count = length(final_cycles)
|
|
|
|
# Should have same number of cycles
|
|
assert final_count == initial_count
|
|
end
|
|
end
|
|
end
|