Replace the create_fee_type/create_cycle helpers duplicated across 18/8 membership-fee test files with a single shared definition in Mv.Fixtures, reconciling the divergent local signatures (including the reversed argument order) into one superset so behavior is unchanged.
447 lines
12 KiB
Elixir
447 lines
12 KiB
Elixir
defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|
@moduledoc """
|
|
Tests for Member cycle status calculations.
|
|
"""
|
|
use Mv.DataCase, async: true
|
|
|
|
import Mv.Fixtures, only: [create_fee_type: 2, create_cycle: 4]
|
|
|
|
alias Mv.MembershipFees.CalendarCycles
|
|
|
|
setup do
|
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
%{actor: system_actor}
|
|
end
|
|
|
|
# Helper to create a member
|
|
defp create_member(attrs, actor) do
|
|
default_attrs = %{
|
|
first_name: "Test",
|
|
last_name: "Member",
|
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
|
member
|
|
end
|
|
|
|
describe "current_cycle_status" do
|
|
test "returns status of current cycle for member with active cycle", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
# Create a cycle that is active today (2024-01-01 to 2024-12-31)
|
|
# Assuming today is in 2024
|
|
today = Date.utc_today()
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: cycle_start,
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :current_cycle_status, actor: actor)
|
|
assert member.current_cycle_status == :paid
|
|
end
|
|
|
|
test "returns nil for member without current cycle", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
# Create a cycle in the past (not current)
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2020-01-01],
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :current_cycle_status, actor: actor)
|
|
assert member.current_cycle_status == nil
|
|
end
|
|
|
|
test "returns nil for member without cycles", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
member = Ash.load!(member, :current_cycle_status, actor: actor)
|
|
assert member.current_cycle_status == nil
|
|
end
|
|
|
|
test "returns status of current cycle for monthly interval", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
# Create a cycle that is active today (current month)
|
|
today = Date.utc_today()
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: cycle_start,
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :current_cycle_status, actor: actor)
|
|
assert member.current_cycle_status == :unpaid
|
|
end
|
|
end
|
|
|
|
describe "last_cycle_status" do
|
|
test "returns status of last completed cycle", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
# Create cycles: 2022 (completed), 2023 (completed), 2024 (current)
|
|
today = Date.utc_today()
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2022-01-01],
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2023-01-01],
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
# Current cycle
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: cycle_start,
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :last_cycle_status, actor: actor)
|
|
# Should return status of 2023 (last completed)
|
|
assert member.last_cycle_status == :unpaid
|
|
end
|
|
|
|
test "returns nil for member without completed cycles", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
# Only create current cycle (not completed yet)
|
|
today = Date.utc_today()
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: cycle_start,
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :last_cycle_status, actor: actor)
|
|
assert member.last_cycle_status == nil
|
|
end
|
|
|
|
test "returns nil for member without cycles", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
member = Ash.load!(member, :last_cycle_status, actor: actor)
|
|
assert member.last_cycle_status == nil
|
|
end
|
|
|
|
test "returns status of last completed cycle for monthly interval", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
today = Date.utc_today()
|
|
# Create cycles: last month (completed), current month (not completed)
|
|
last_month_start = Date.add(today, -32) |> CalendarCycles.calculate_cycle_start(:monthly)
|
|
current_month_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: last_month_start,
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: current_month_start,
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :last_cycle_status, actor: actor)
|
|
# Should return status of last month (last completed)
|
|
assert member.last_cycle_status == :paid
|
|
end
|
|
end
|
|
|
|
describe "overdue_count" do
|
|
test "counts only unpaid cycles that have ended", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
today = Date.utc_today()
|
|
|
|
# Create cycles:
|
|
# 2022: unpaid, ended (overdue)
|
|
# 2023: paid, ended (not overdue)
|
|
# 2024: unpaid, current (not overdue)
|
|
# 2025: unpaid, future (not overdue)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2022-01-01],
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2023-01-01],
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
# Current cycle
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: cycle_start,
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
# Future cycle (if we're not at the end of the year)
|
|
next_year = today.year + 1
|
|
|
|
if today.month < 12 or today.day < 31 do
|
|
next_year_start = Date.new!(next_year, 1, 1)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: next_year_start,
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
end
|
|
|
|
member = Ash.load!(member, :overdue_count, actor: actor)
|
|
# Should only count 2022 (unpaid and ended)
|
|
assert member.overdue_count == 1
|
|
end
|
|
|
|
test "returns 0 when no overdue cycles", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
# Create only paid cycles
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2022-01-01],
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :overdue_count, actor: actor)
|
|
assert member.overdue_count == 0
|
|
end
|
|
|
|
test "returns 0 for member without cycles", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
member = Ash.load!(member, :overdue_count, actor: actor)
|
|
assert member.overdue_count == 0
|
|
end
|
|
|
|
test "counts overdue cycles for monthly interval", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :monthly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
today = Date.utc_today()
|
|
|
|
# Create cycles: two months ago (unpaid, ended), last month (paid, ended), current month (unpaid, not ended)
|
|
two_months_ago_start =
|
|
Date.add(today, -65) |> CalendarCycles.calculate_cycle_start(:monthly)
|
|
|
|
last_month_start = Date.add(today, -32) |> CalendarCycles.calculate_cycle_start(:monthly)
|
|
current_month_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: two_months_ago_start,
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: last_month_start,
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: current_month_start,
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :overdue_count, actor: actor)
|
|
# Should only count two_months_ago (unpaid and ended)
|
|
assert member.overdue_count == 1
|
|
end
|
|
|
|
test "counts multiple overdue cycles", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
# Create multiple unpaid, ended cycles
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2020-01-01],
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2021-01-01],
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2022-01-01],
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = Ash.load!(member, :overdue_count, actor: actor)
|
|
assert member.overdue_count == 3
|
|
end
|
|
end
|
|
|
|
describe "calculations with multiple cycles" do
|
|
test "all calculations work correctly with multiple cycles", %{actor: actor} do
|
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: fee_type.id}, actor)
|
|
|
|
today = Date.utc_today()
|
|
|
|
# Create cycles: 2022 (unpaid, ended), 2023 (paid, ended), 2024 (unpaid, current)
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2022-01-01],
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: ~D[2023-01-01],
|
|
status: :paid
|
|
},
|
|
actor
|
|
)
|
|
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
|
|
|
create_cycle(
|
|
member,
|
|
fee_type,
|
|
%{
|
|
cycle_start: cycle_start,
|
|
status: :unpaid
|
|
},
|
|
actor
|
|
)
|
|
|
|
member =
|
|
Ash.load!(member, [:current_cycle_status, :last_cycle_status, :overdue_count],
|
|
actor: actor
|
|
)
|
|
|
|
assert member.current_cycle_status == :unpaid
|
|
assert member.last_cycle_status == :paid
|
|
assert member.overdue_count == 1
|
|
end
|
|
end
|
|
end
|