357 lines
11 KiB
Elixir
357 lines
11 KiB
Elixir
defmodule Mv.Membership.MemberCycleCalculationsTest do
|
|
@moduledoc """
|
|
Tests for Member cycle status calculations.
|
|
"""
|
|
use Mv.DataCase, async: true
|
|
|
|
alias Mv.Membership.Member
|
|
alias Mv.MembershipFees.MembershipFeeType
|
|
alias Mv.MembershipFees.MembershipFeeCycle
|
|
alias Mv.MembershipFees.CalendarCycles
|
|
|
|
# Helper to create a membership fee type
|
|
defp create_fee_type(attrs) do
|
|
default_attrs = %{
|
|
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
|
amount: Decimal.new("50.00"),
|
|
interval: :yearly
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
|
|
MembershipFeeType
|
|
|> Ash.Changeset.for_create(:create, attrs)
|
|
|> Ash.create!()
|
|
end
|
|
|
|
# Helper to create a member
|
|
defp create_member(attrs) do
|
|
default_attrs = %{
|
|
first_name: "Test",
|
|
last_name: "Member",
|
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
|
|
Member
|
|
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
|> Ash.create!()
|
|
end
|
|
|
|
# Helper to create a cycle
|
|
defp create_cycle(member, fee_type, attrs) do
|
|
default_attrs = %{
|
|
cycle_start: ~D[2024-01-01],
|
|
amount: Decimal.new("50.00"),
|
|
member_id: member.id,
|
|
membership_fee_type_id: fee_type.id,
|
|
status: :unpaid
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
|
|
MembershipFeeCycle
|
|
|> Ash.Changeset.for_create(:create, attrs)
|
|
|> Ash.create!()
|
|
end
|
|
|
|
describe "current_cycle_status" do
|
|
test "returns status of current cycle for member with active cycle" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
# 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
|
|
})
|
|
|
|
member = Ash.load!(member, :current_cycle_status)
|
|
assert member.current_cycle_status == :paid
|
|
end
|
|
|
|
test "returns nil for member without current cycle" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
# Create a cycle in the past (not current)
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2020-01-01],
|
|
status: :paid
|
|
})
|
|
|
|
member = Ash.load!(member, :current_cycle_status)
|
|
assert member.current_cycle_status == nil
|
|
end
|
|
|
|
test "returns nil for member without cycles" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
member = Ash.load!(member, :current_cycle_status)
|
|
assert member.current_cycle_status == nil
|
|
end
|
|
|
|
test "returns status of current cycle for monthly interval" do
|
|
fee_type = create_fee_type(%{interval: :monthly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
# 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
|
|
})
|
|
|
|
member = Ash.load!(member, :current_cycle_status)
|
|
assert member.current_cycle_status == :unpaid
|
|
end
|
|
end
|
|
|
|
describe "last_cycle_status" do
|
|
test "returns status of last completed cycle" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
# 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
|
|
})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2023-01-01],
|
|
status: :unpaid
|
|
})
|
|
|
|
# Current cycle
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: cycle_start,
|
|
status: :paid
|
|
})
|
|
|
|
member = Ash.load!(member, :last_cycle_status)
|
|
# Should return status of 2023 (last completed)
|
|
assert member.last_cycle_status == :unpaid
|
|
end
|
|
|
|
test "returns nil for member without completed cycles" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
# 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
|
|
})
|
|
|
|
member = Ash.load!(member, :last_cycle_status)
|
|
assert member.last_cycle_status == nil
|
|
end
|
|
|
|
test "returns nil for member without cycles" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
member = Ash.load!(member, :last_cycle_status)
|
|
assert member.last_cycle_status == nil
|
|
end
|
|
|
|
test "returns status of last completed cycle for monthly interval" do
|
|
fee_type = create_fee_type(%{interval: :monthly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
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
|
|
})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: current_month_start,
|
|
status: :unpaid
|
|
})
|
|
|
|
member = Ash.load!(member, :last_cycle_status)
|
|
# 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" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
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
|
|
})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2023-01-01],
|
|
status: :paid
|
|
})
|
|
|
|
# Current cycle
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: cycle_start,
|
|
status: :unpaid
|
|
})
|
|
|
|
# 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
|
|
})
|
|
end
|
|
|
|
member = Ash.load!(member, :overdue_count)
|
|
# Should only count 2022 (unpaid and ended)
|
|
assert member.overdue_count == 1
|
|
end
|
|
|
|
test "returns 0 when no overdue cycles" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
# Create only paid cycles
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2022-01-01],
|
|
status: :paid
|
|
})
|
|
|
|
member = Ash.load!(member, :overdue_count)
|
|
assert member.overdue_count == 0
|
|
end
|
|
|
|
test "returns 0 for member without cycles" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
member = Ash.load!(member, :overdue_count)
|
|
assert member.overdue_count == 0
|
|
end
|
|
|
|
test "counts overdue cycles for monthly interval" do
|
|
fee_type = create_fee_type(%{interval: :monthly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
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
|
|
})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: last_month_start,
|
|
status: :paid
|
|
})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: current_month_start,
|
|
status: :unpaid
|
|
})
|
|
|
|
member = Ash.load!(member, :overdue_count)
|
|
# Should only count two_months_ago (unpaid and ended)
|
|
assert member.overdue_count == 1
|
|
end
|
|
|
|
test "counts multiple overdue cycles" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
# Create multiple unpaid, ended cycles
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2020-01-01],
|
|
status: :unpaid
|
|
})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2021-01-01],
|
|
status: :unpaid
|
|
})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2022-01-01],
|
|
status: :unpaid
|
|
})
|
|
|
|
member = Ash.load!(member, :overdue_count)
|
|
assert member.overdue_count == 3
|
|
end
|
|
end
|
|
|
|
describe "calculations with multiple cycles" do
|
|
test "all calculations work correctly with multiple cycles" do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
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
|
|
})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2023-01-01],
|
|
status: :paid
|
|
})
|
|
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: cycle_start,
|
|
status: :unpaid
|
|
})
|
|
|
|
member =
|
|
Ash.load!(member, [:current_cycle_status, :last_cycle_status, :overdue_count])
|
|
|
|
assert member.current_cycle_status == :unpaid
|
|
assert member.last_cycle_status == :paid
|
|
assert member.overdue_count == 1
|
|
end
|
|
end
|
|
end
|