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