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:
parent
a629bfb617
commit
b745b13ca5
7 changed files with 448 additions and 4 deletions
53
lib/membership_fees/cycle_end_backfill.ex
Normal file
53
lib/membership_fees/cycle_end_backfill.ex
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
defmodule Mv.MembershipFees.CycleEndBackfill do
|
||||
@moduledoc """
|
||||
One-time backfill for the denormalized `cycle_end` column on
|
||||
`membership_fee_cycles`.
|
||||
|
||||
For every cycle whose `cycle_end` is still `NULL`, sets it to
|
||||
`CalendarCycles.calculate_cycle_end(cycle_start, interval)` using the cycle's
|
||||
own fee-type interval. Idempotent: rows that already have a `cycle_end` are
|
||||
left untouched, so it is safe to re-run.
|
||||
|
||||
Invoked from the `cycle_end` migration. Kept as a module function (rather than
|
||||
inline migration SQL) so the exact calendar arithmetic is reused from
|
||||
`CalendarCycles` and can be tested directly.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Mv.MembershipFees.CalendarCycles
|
||||
alias Mv.Repo
|
||||
|
||||
@doc """
|
||||
Backfills `cycle_end` for all cycles missing it. Returns `{:ok, updated_count}`.
|
||||
"""
|
||||
@spec run() :: {:ok, non_neg_integer()}
|
||||
def run do
|
||||
rows =
|
||||
from(c in "membership_fee_cycles",
|
||||
join: t in "membership_fee_types",
|
||||
on: c.membership_fee_type_id == t.id,
|
||||
where: is_nil(c.cycle_end),
|
||||
select: {c.id, c.cycle_start, t.interval}
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
count =
|
||||
Enum.reduce(rows, 0, fn {id, cycle_start, interval}, acc ->
|
||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, normalize_interval(interval))
|
||||
|
||||
from(c in "membership_fee_cycles", where: c.id == ^id)
|
||||
|> Repo.update_all(set: [cycle_end: cycle_end])
|
||||
|
||||
acc + 1
|
||||
end)
|
||||
|
||||
{:ok, count}
|
||||
end
|
||||
|
||||
# The raw query returns the enum column as a string.
|
||||
defp normalize_interval(interval) when is_atom(interval), do: interval
|
||||
|
||||
defp normalize_interval(interval) when is_binary(interval),
|
||||
do: String.to_existing_atom(interval)
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue