feat(membership-fees): denormalize cycle_end so fee status filters run in the database

Store the cycle's end date alongside its start so the payment-status
filter can be a plain date comparison in SQL instead of loading every
cycle and classifying it in memory. The value is fixed when the cycle
is created (the fee type's interval is immutable and the update action
never changes the start date), so the denormalized column can never
drift from the computed end and needs no write-time reconciliation.
This commit is contained in:
Simon 2026-07-03 11:04:43 +02:00
parent a629bfb617
commit b745b13ca5
7 changed files with 448 additions and 4 deletions

View file

@ -305,7 +305,7 @@ defmodule Mv.MembershipFees.CycleGenerator do
# 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, opts)
create_cycles(cycle_starts, member.id, fee_type.id, amount, interval, opts)
else
{:ok, [], []}
end
@ -400,7 +400,7 @@ defmodule Mv.MembershipFees.CycleGenerator do
end
end
defp create_cycles(cycle_starts, member_id, fee_type_id, amount, opts) do
defp create_cycles(cycle_starts, member_id, fee_type_id, amount, interval, opts) do
actor = get_actor(opts)
create_opts = Helpers.ash_actor_opts(actor)
@ -414,7 +414,10 @@ defmodule Mv.MembershipFees.CycleGenerator do
member_id: member_id,
membership_fee_type_id: fee_type_id,
amount: amount,
status: :unpaid
status: :unpaid,
# Pass the already-resolved interval so SetCycleEnd does not re-fetch the
# fee type for every cycle in the batch (avoids an N+1 lookup).
interval: interval
}
handle_cycle_creation_result(