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