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.
211 lines
7.7 KiB
Elixir
211 lines
7.7 KiB
Elixir
defmodule Mv.StatisticsTest do
|
|
@moduledoc """
|
|
Tests for Mv.Statistics module (member and membership fee cycle statistics).
|
|
"""
|
|
use Mv.DataCase, async: true
|
|
|
|
require Ash.Query
|
|
import Ash.Expr
|
|
import Mv.Fixtures, only: [create_fee_type: 2]
|
|
|
|
alias Mv.Membership.Member
|
|
alias Mv.MembershipFees
|
|
alias Mv.MembershipFees.MembershipFeeCycle
|
|
alias Mv.Statistics
|
|
|
|
setup do
|
|
actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
%{actor: actor}
|
|
end
|
|
|
|
describe "first_join_year/1" do
|
|
test "returns the year of the earliest join_date", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2019-03-15]})
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2022-01-01]})
|
|
assert Statistics.first_join_year(actor: actor) == 2019
|
|
end
|
|
|
|
test "returns the only member's join year when one member exists", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2021-06-01]})
|
|
assert Statistics.first_join_year(actor: actor) == 2021
|
|
end
|
|
|
|
test "returns nil when no members exist", %{actor: actor} do
|
|
# Guarantee empty member table so the assertion is deterministic
|
|
Member
|
|
|> Ash.read!(actor: actor)
|
|
|> Enum.each(&Ash.destroy!(&1, actor: actor))
|
|
|
|
result = Statistics.first_join_year(actor: actor)
|
|
assert is_nil(result)
|
|
end
|
|
end
|
|
|
|
describe "active_member_count/1" do
|
|
test "returns 0 when there are no members", %{actor: actor} do
|
|
assert Statistics.active_member_count(actor: actor) == 0
|
|
end
|
|
|
|
test "returns 1 when one member has no exit_date", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-15]})
|
|
assert Statistics.active_member_count(actor: actor) == 1
|
|
end
|
|
|
|
test "returns 0 for that member when exit_date is set", %{actor: actor} do
|
|
_member =
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-15], exit_date: ~D[2024-06-01]})
|
|
|
|
assert Statistics.active_member_count(actor: actor) == 0
|
|
end
|
|
|
|
test "counts only active members when mix of active and inactive", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2022-01-01]})
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-01], exit_date: ~D[2024-01-01]})
|
|
assert Statistics.active_member_count(actor: actor) == 1
|
|
end
|
|
end
|
|
|
|
describe "inactive_member_count/1" do
|
|
test "returns 0 when all members are active", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-01]})
|
|
assert Statistics.inactive_member_count(actor: actor) == 0
|
|
end
|
|
|
|
test "returns 1 when one member has exit_date set", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2022-01-01], exit_date: ~D[2024-06-01]})
|
|
assert Statistics.inactive_member_count(actor: actor) == 1
|
|
end
|
|
end
|
|
|
|
describe "joins_by_year/2" do
|
|
test "returns 0 for year with no joins", %{actor: actor} do
|
|
assert Statistics.joins_by_year(1999, actor: actor) == 0
|
|
end
|
|
|
|
test "returns 1 when one member has join_date in that year", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2023-06-15]})
|
|
assert Statistics.joins_by_year(2023, actor: actor) == 1
|
|
end
|
|
|
|
test "returns 2 when two members joined in that year", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-01]})
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2023-12-31]})
|
|
assert Statistics.joins_by_year(2023, actor: actor) == 2
|
|
end
|
|
end
|
|
|
|
describe "exits_by_year/2" do
|
|
test "returns 0 for year with no exits", %{actor: actor} do
|
|
assert Statistics.exits_by_year(1999, actor: actor) == 0
|
|
end
|
|
|
|
test "returns 1 when one member has exit_date in that year", %{actor: actor} do
|
|
Mv.Fixtures.member_fixture(%{join_date: ~D[2020-01-01], exit_date: ~D[2023-06-15]})
|
|
assert Statistics.exits_by_year(2023, actor: actor) == 1
|
|
end
|
|
end
|
|
|
|
describe "cycle_totals_by_year/2" do
|
|
test "returns zero totals for year with no cycles", %{actor: actor} do
|
|
result = Statistics.cycle_totals_by_year(1999, actor: actor)
|
|
assert result.total == Decimal.new(0)
|
|
assert result.paid == Decimal.new(0)
|
|
assert result.unpaid == Decimal.new(0)
|
|
assert result.suspended == Decimal.new(0)
|
|
end
|
|
|
|
test "returns totals by status for cycles in that year", %{actor: actor} do
|
|
fee_type = create_fee_type(%{amount: Decimal.new("50.00")}, actor)
|
|
|
|
# Creating members with fee type triggers cycle generation (2020..today). We use 2024 cycles.
|
|
_member1 =
|
|
Mv.Fixtures.member_fixture(%{
|
|
join_date: ~D[2020-01-01],
|
|
membership_fee_type_id: fee_type.id
|
|
})
|
|
|
|
_member2 =
|
|
Mv.Fixtures.member_fixture(%{
|
|
join_date: ~D[2020-01-01],
|
|
membership_fee_type_id: fee_type.id
|
|
})
|
|
|
|
# Get 2024 cycles and set status (each member has one 2024 yearly cycle from generator)
|
|
cycles_2024 =
|
|
MembershipFeeCycle
|
|
|> Ash.Query.filter(
|
|
expr(cycle_start >= ^~D[2024-01-01] and cycle_start < ^~D[2025-01-01])
|
|
)
|
|
|> Ash.read!(actor: actor)
|
|
|> Enum.sort_by(& &1.member_id)
|
|
|
|
[c1, c2] = cycles_2024
|
|
assert {:ok, _} = Ash.update(c1, %{status: :paid}, domain: MembershipFees, actor: actor)
|
|
|
|
assert {:ok, _} =
|
|
Ash.update(c2, %{status: :suspended}, domain: MembershipFees, actor: actor)
|
|
|
|
result = Statistics.cycle_totals_by_year(2024, actor: actor)
|
|
assert Decimal.equal?(result.total, Decimal.new("100.00"))
|
|
assert Decimal.equal?(result.paid, Decimal.new("50.00"))
|
|
assert Decimal.equal?(result.unpaid, Decimal.new(0))
|
|
assert Decimal.equal?(result.suspended, Decimal.new("50.00"))
|
|
end
|
|
|
|
test "when fee_type_id is passed in opts, returns only cycles of that fee type", %{
|
|
actor: actor
|
|
} do
|
|
fee_type_a = create_fee_type(%{amount: Decimal.new("30.00")}, actor)
|
|
fee_type_b = create_fee_type(%{amount: Decimal.new("70.00")}, actor)
|
|
|
|
_m1 =
|
|
Mv.Fixtures.member_fixture(%{
|
|
join_date: ~D[2020-01-01],
|
|
membership_fee_type_id: fee_type_a.id
|
|
})
|
|
|
|
_m2 =
|
|
Mv.Fixtures.member_fixture(%{
|
|
join_date: ~D[2020-01-01],
|
|
membership_fee_type_id: fee_type_b.id
|
|
})
|
|
|
|
# Without filter: both fee types' cycles (2024)
|
|
all_result = Statistics.cycle_totals_by_year(2024, actor: actor)
|
|
assert Decimal.equal?(all_result.total, Decimal.new("100.00"))
|
|
|
|
# With fee_type_id as string (as from form/URL): only that type's cycles
|
|
opts_a = [actor: actor, fee_type_id: to_string(fee_type_a.id)]
|
|
result_a = Statistics.cycle_totals_by_year(2024, opts_a)
|
|
assert Decimal.equal?(result_a.total, Decimal.new("30.00"))
|
|
|
|
opts_b = [actor: actor, fee_type_id: to_string(fee_type_b.id)]
|
|
result_b = Statistics.cycle_totals_by_year(2024, opts_b)
|
|
assert Decimal.equal?(result_b.total, Decimal.new("70.00"))
|
|
end
|
|
end
|
|
|
|
describe "open_amount_total/1" do
|
|
test "returns 0 when there are no unpaid cycles", %{actor: actor} do
|
|
assert Statistics.open_amount_total(actor: actor) == Decimal.new(0)
|
|
end
|
|
|
|
test "returns sum of amount for all unpaid cycles", %{actor: actor} do
|
|
fee_type = create_fee_type(%{amount: Decimal.new("50.00")}, actor)
|
|
|
|
_member =
|
|
Mv.Fixtures.member_fixture(%{
|
|
join_date: ~D[2020-01-01],
|
|
membership_fee_type_id: fee_type.id
|
|
})
|
|
|
|
# Cycle generator creates yearly cycles (2020..today), all unpaid by default
|
|
unpaid_sum = Statistics.open_amount_total(actor: actor)
|
|
assert Decimal.compare(unpaid_sum, Decimal.new(0)) == :gt
|
|
# Should be 50 * number of years from 2020 to current year
|
|
current_year = Date.utc_today().year
|
|
expected_count = current_year - 2020 + 1
|
|
assert Decimal.equal?(unpaid_sum, Decimal.new(50 * expected_count))
|
|
end
|
|
end
|
|
end
|