Membership Fee Type Resource & Settings closes #278 #291
2 changed files with 87 additions and 6 deletions
|
|
@ -83,12 +83,18 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generates membership fee cycles for all active members.
|
Generates membership fee cycles for all members with a fee type assigned.
|
||||||
|
|
||||||
Active members are those who:
|
This includes both active and inactive (left) members. Inactive members
|
||||||
|
will have cycles generated up to their exit_date if they don't have cycles
|
||||||
|
for that period yet. This allows for catch-up generation of missing cycles.
|
||||||
|
|
||||||
|
Members processed are those who:
|
||||||
- Have a membership_fee_type assigned
|
- Have a membership_fee_type assigned
|
||||||
- Have a join_date set
|
- Have a join_date set
|
||||||
- Either have no exit_date or exit_date >= today
|
|
||||||
|
The exit_date boundary is respected during generation (not in the query),
|
||||||
|
so inactive members will get cycles up to their exit date.
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|
||||||
|
|
@ -107,12 +113,13 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
||||||
today = Keyword.get(opts, :today, Date.utc_today())
|
today = Keyword.get(opts, :today, Date.utc_today())
|
||||||
batch_size = Keyword.get(opts, :batch_size, 10)
|
batch_size = Keyword.get(opts, :batch_size, 10)
|
||||||
|
|
||||||
# Query active members with fee type assigned
|
# Query ALL members with fee type assigned (including inactive/left members)
|
||||||
|
# The exit_date boundary is applied during cycle generation, not here.
|
||||||
|
# This allows catch-up generation for members who left but are missing cycles.
|
||||||
query =
|
query =
|
||||||
Member
|
Member
|
||||||
|> Ash.Query.filter(not is_nil(membership_fee_type_id))
|
|> Ash.Query.filter(not is_nil(membership_fee_type_id))
|
||||||
|> Ash.Query.filter(not is_nil(join_date))
|
|> Ash.Query.filter(not is_nil(join_date))
|
||||||
|> Ash.Query.filter(is_nil(exit_date) or exit_date >= ^today)
|
|
||||||
|
|
||||||
case Ash.read(query) do
|
case Ash.read(query) do
|
||||||
{:ok, members} ->
|
{:ok, members} ->
|
||||||
|
|
@ -234,7 +241,11 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
||||||
|
|
||||||
defp determine_end_date(member, today) do
|
defp determine_end_date(member, today) do
|
||||||
if member.exit_date && Date.compare(member.exit_date, today) == :lt do
|
if member.exit_date && Date.compare(member.exit_date, today) == :lt do
|
||||||
# Member has left - use the cycle that contains the exit date
|
# Member has left - use the exit date as boundary
|
||||||
|
# Note: If exit_date == cycle_start, the cycle IS still generated.
|
||||||
|
# This means the member is considered a member on the first day of that cycle.
|
||||||
|
# Example: exit_date = 2025-01-01, yearly interval
|
||||||
|
# -> The 2025 cycle (starting 2025-01-01) WILL be generated
|
||||||
member.exit_date
|
member.exit_date
|
||||||
else
|
else
|
||||||
today
|
today
|
||||||
|
|
|
||||||
|
|
@ -454,4 +454,74 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
||||||
assert 2024 in cycle_years
|
assert 2024 in cycle_years
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "inactive member processing" do
|
||||||
|
test "inactive members receive cycles up to exit_date via generate_cycles_for_all_members" do
|
||||||
|
setup_settings(true)
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
|
||||||
|
# Create an inactive member (left in 2023) WITHOUT fee type initially
|
||||||
|
# This simulates a member that was created before the fee system existed
|
||||||
|
member =
|
||||||
|
create_member(%{
|
||||||
|
join_date: ~D[2021-03-15],
|
||||||
|
exit_date: ~D[2023-06-15]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Now assign fee type (simulating a retroactive assignment)
|
||||||
|
member =
|
||||||
|
member
|
||||||
|
|> Ash.Changeset.for_update(:update_member, %{
|
||||||
|
membership_fee_type_id: fee_type.id,
|
||||||
|
membership_fee_start_date: ~D[2021-01-01]
|
||||||
|
})
|
||||||
|
|> Ash.update!()
|
||||||
|
|
||||||
|
# Run batch generation with a "today" date after the member left
|
||||||
|
today = ~D[2024-06-15]
|
||||||
|
{:ok, results} = CycleGenerator.generate_cycles_for_all_members(today: today)
|
||||||
|
|
||||||
|
# The inactive member should have been processed
|
||||||
|
assert results.total >= 1
|
||||||
|
|
||||||
|
# Check the member's cycles
|
||||||
|
cycles = get_member_cycles(member.id)
|
||||||
|
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()
|
||||||
|
|
||||||
|
# Should have 2021, 2022, 2023 (exit year included)
|
||||||
|
assert 2021 in cycle_years
|
||||||
|
assert 2022 in cycle_years
|
||||||
|
assert 2023 in cycle_years
|
||||||
|
|
||||||
|
# Should NOT have 2024 (after exit)
|
||||||
|
refute 2024 in cycle_years
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exit_date on cycle_start still generates that cycle" do
|
||||||
|
setup_settings(true)
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
|
||||||
|
# Member exits exactly on cycle start (2024-01-01)
|
||||||
|
member =
|
||||||
|
create_member(%{
|
||||||
|
join_date: ~D[2022-03-15],
|
||||||
|
exit_date: ~D[2024-01-01],
|
||||||
|
membership_fee_type_id: fee_type.id,
|
||||||
|
membership_fee_start_date: ~D[2022-01-01]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check cycles
|
||||||
|
cycles = get_member_cycles(member.id)
|
||||||
|
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
|
||||||
|
|
||||||
|
# 2024 should be included because exit_date == cycle_start means
|
||||||
|
# the member was still a member on that day
|
||||||
|
assert 2022 in cycle_years
|
||||||
|
assert 2023 in cycle_years
|
||||||
|
assert 2024 in cycle_years
|
||||||
|
|
||||||
|
# 2025 should NOT be included
|
||||||
|
refute 2025 in cycle_years
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue