fix: CycleGenerator generates from last cycle, not filling gaps
- Change algorithm to start from last existing cycle instead of start_date - Deleted cycles (gaps) are no longer automatically filled - Add test to verify gaps remain unfilled - Update documentation to clarify gap handling behavior
This commit is contained in:
parent
272a8a8afc
commit
0b986db635
2 changed files with 95 additions and 14 deletions
|
|
@ -8,11 +8,20 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
## Algorithm
|
||||
|
||||
1. Load member with relationships (membership_fee_type, membership_fee_cycles)
|
||||
2. Determine membership_fee_start_date (calculate if nil)
|
||||
3. Find the last existing cycle start date (or use membership_fee_start_date)
|
||||
4. Generate all cycle starts from last to today (or left_at)
|
||||
5. Filter out existing cycles (idempotency)
|
||||
6. Create new cycles with the current amount from membership_fee_type
|
||||
2. Determine the generation start point:
|
||||
- If NO cycles exist: Start from `membership_fee_start_date` (or calculated from `join_date`)
|
||||
- If cycles exist: Start from the cycle AFTER the last existing one
|
||||
3. Generate all cycle starts from the determined start point to today (or `exit_date`)
|
||||
4. Create new cycles with the current amount from `membership_fee_type`
|
||||
|
||||
## Important: Gap Handling
|
||||
|
||||
**Gaps are NOT filled.** If a cycle was explicitly deleted (e.g., 2023 was deleted
|
||||
but 2022 and 2024 exist), the generator will NOT recreate the deleted cycle.
|
||||
It always continues from the LAST existing cycle, regardless of any gaps.
|
||||
|
||||
This behavior ensures that manually deleted cycles remain deleted and prevents
|
||||
unwanted automatic recreation of intentionally removed cycles.
|
||||
|
||||
## Concurrency
|
||||
|
||||
|
|
@ -215,21 +224,36 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
amount = fee_type.amount
|
||||
existing_cycles = member.membership_fee_cycles || []
|
||||
|
||||
# Determine start date
|
||||
start_date = determine_start_date(member, interval)
|
||||
# Determine start point based on existing cycles
|
||||
# Note: We do NOT fill gaps - only generate from the last existing cycle onwards
|
||||
start_date = determine_generation_start(member, existing_cycles, interval)
|
||||
|
||||
# Determine end date (today or exit_date, whichever is earlier)
|
||||
end_date = determine_end_date(member, today)
|
||||
|
||||
# Generate all cycle starts from start_date to end_date
|
||||
all_cycle_starts = generate_cycle_starts(start_date, end_date, interval)
|
||||
# Only generate if start_date <= end_date
|
||||
if start_date && Date.compare(start_date, end_date) != :gt do
|
||||
cycle_starts = generate_cycle_starts(start_date, end_date, interval)
|
||||
create_cycles(cycle_starts, member.id, fee_type.id, amount)
|
||||
else
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
# Filter out existing cycles
|
||||
existing_starts = MapSet.new(existing_cycles, & &1.cycle_start)
|
||||
missing_starts = Enum.reject(all_cycle_starts, &MapSet.member?(existing_starts, &1))
|
||||
# No existing cycles: start from membership_fee_start_date
|
||||
defp determine_generation_start(member, [], interval) do
|
||||
determine_start_date(member, interval)
|
||||
end
|
||||
|
||||
# Create missing cycles
|
||||
create_cycles(missing_starts, member.id, fee_type.id, amount)
|
||||
# Has existing cycles: start from the cycle AFTER the last one
|
||||
# This ensures gaps (deleted cycles) are NOT filled
|
||||
defp determine_generation_start(_member, existing_cycles, interval) do
|
||||
last_cycle_start =
|
||||
existing_cycles
|
||||
|> Enum.map(& &1.cycle_start)
|
||||
|> Enum.max(Date)
|
||||
|
||||
CalendarCycles.next_cycle_start(last_cycle_start, interval)
|
||||
end
|
||||
|
||||
defp determine_start_date(member, interval) do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue