Counts each member's unpaid billing cycles that fall inside the selected period. Cycle boundaries are member-relative — derived from each member's fee-type interval — so "the current cycle" spans different calendar dates per member.
79 lines
3.3 KiB
Elixir
79 lines
3.3 KiB
Elixir
defmodule MvWeb.MemberLive.IndexPaymentAgingTest do
|
|
@moduledoc """
|
|
§1.13 / §3.3 — the period-scoped `unpaid_cycle_count` calculation on Member.
|
|
|
|
Counts a member's cycles with `status == :unpaid` whose denormalized
|
|
`cycle_end` falls inside the active period; paid and suspended cycles are
|
|
never counted. A nil period bound means "unbounded on that side" (the
|
|
all-outstanding default counts every unpaid cycle).
|
|
"""
|
|
use Mv.DataCase, async: false
|
|
|
|
import Mv.Fixtures, only: [create_fee_type: 2, member_fixture_with_actor: 2, create_cycle: 4]
|
|
|
|
alias Mv.Membership.Member
|
|
|
|
require Ash.Query
|
|
|
|
setup do
|
|
actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
# Monthly so several cycles can share one period without cycle_start collisions.
|
|
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
|
%{actor: actor, fee_type: fee_type}
|
|
end
|
|
|
|
defp count(member, from, to, actor) do
|
|
member
|
|
|> Ash.load!([unpaid_cycle_count: %{period_from: from, period_to: to}], actor: actor)
|
|
|> Map.fetch!(:unpaid_cycle_count)
|
|
end
|
|
|
|
test "counts only unpaid cycles with cycle_end in the period; suspended and paid excluded",
|
|
%{actor: actor, fee_type: ft} do
|
|
# No membership_fee_type_id on the member itself → no auto-generated cycles.
|
|
member = member_fixture_with_actor(%{}, actor)
|
|
|
|
# In-period unpaid (counted)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2024-01-01], status: :unpaid}, actor)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2024-02-01], status: :unpaid}, actor)
|
|
# In-period but not unpaid (excluded)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2024-03-01], status: :paid}, actor)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2024-04-01], status: :suspended}, actor)
|
|
# Unpaid but out of period (excluded)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2023-01-01], status: :unpaid}, actor)
|
|
|
|
assert count(member, ~D[2024-01-01], ~D[2024-12-31], actor) == 2
|
|
|
|
# Fully-paid member (only paid/suspended in period) → 0
|
|
paid_member = member_fixture_with_actor(%{}, actor)
|
|
create_cycle(paid_member, ft, %{cycle_start: ~D[2024-01-01], status: :paid}, actor)
|
|
create_cycle(paid_member, ft, %{cycle_start: ~D[2024-02-01], status: :suspended}, actor)
|
|
assert count(paid_member, ~D[2024-01-01], ~D[2024-12-31], actor) == 0
|
|
end
|
|
|
|
test "nil period bounds count every unpaid cycle (all-outstanding default)",
|
|
%{actor: actor, fee_type: ft} do
|
|
member = member_fixture_with_actor(%{}, actor)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2020-01-01], status: :unpaid}, actor)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2024-06-01], status: :unpaid}, actor)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2026-01-01], status: :unpaid}, actor)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2025-01-01], status: :paid}, actor)
|
|
|
|
assert count(member, nil, nil, actor) == 3
|
|
end
|
|
|
|
test "loads for a whole read query (DB-pushable aggregate)", %{actor: actor, fee_type: ft} do
|
|
member = member_fixture_with_actor(%{}, actor)
|
|
create_cycle(member, ft, %{cycle_start: ~D[2024-01-01], status: :unpaid}, actor)
|
|
|
|
[loaded] =
|
|
Member
|
|
|> Ash.Query.filter(id == ^member.id)
|
|
|> Ash.Query.load(
|
|
unpaid_cycle_count: %{period_from: ~D[2024-01-01], period_to: ~D[2024-12-31]}
|
|
)
|
|
|> Ash.read!(actor: actor)
|
|
|
|
assert loaded.unpaid_cycle_count == 1
|
|
end
|
|
end
|