defmodule Mv.MembershipFees.MembershipFeeCycleTest do @moduledoc """ Tests for MembershipFeeCycle resource, focusing on status management actions. """ use Mv.DataCase, async: true alias Mv.MembershipFees.MembershipFeeCycle alias Mv.MembershipFees.MembershipFeeType setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() %{actor: system_actor} end # Helper to create a membership fee type defp create_fee_type(attrs, actor) 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!(actor: 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 # Helper to create a cycle defp create_cycle(member, fee_type, attrs, actor) 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 } attrs = Map.merge(default_attrs, attrs) MembershipFeeCycle |> Ash.Changeset.for_create(:create, attrs) |> Ash.create!(actor: actor) end describe "status defaults" do test "status defaults to :unpaid when creating a cycle", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = MembershipFeeCycle |> Ash.Changeset.for_create(:create, %{ cycle_start: ~D[2024-01-01], amount: Decimal.new("50.00"), member_id: member.id, membership_fee_type_id: fee_type.id }) |> Ash.create!(actor: actor) assert cycle.status == :unpaid end end describe "mark_as_paid" do test "sets status to :paid", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor) assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_paid) assert updated.status == :paid end test "can set notes when marking as paid", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor) assert {:ok, updated} = Ash.update(cycle, %{notes: "Payment received via bank transfer"}, actor: actor, action: :mark_as_paid ) assert updated.status == :paid assert updated.notes == "Payment received via bank transfer" end test "can change from suspended to paid", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :suspended}, actor) assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_paid) assert updated.status == :paid end end describe "mark_as_suspended" do test "sets status to :suspended", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor) assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_suspended) assert updated.status == :suspended end test "can set notes when marking as suspended", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor) assert {:ok, updated} = Ash.update(cycle, %{notes: "Waived due to special circumstances"}, actor: actor, action: :mark_as_suspended ) assert updated.status == :suspended assert updated.notes == "Waived due to special circumstances" end test "can change from paid to suspended", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :paid}, actor) assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_suspended) assert updated.status == :suspended end end describe "mark_as_unpaid" do test "sets status to :unpaid", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :paid}, actor) assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid) assert updated.status == :unpaid end test "can set notes when marking as unpaid", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :paid}, actor) assert {:ok, updated} = Ash.update(cycle, %{notes: "Payment was reversed"}, actor: actor, action: :mark_as_unpaid ) assert updated.status == :unpaid assert updated.notes == "Payment was reversed" end test "can change from suspended to unpaid", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) cycle = create_cycle(member, fee_type, %{status: :suspended}, actor) assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid) assert updated.status == :unpaid end end describe "status transitions" do test "all status transitions are allowed", %{actor: actor} do fee_type = create_fee_type(%{interval: :yearly}, actor) member = create_member(%{membership_fee_type_id: fee_type.id}, actor) # unpaid -> paid cycle1 = create_cycle(member, fee_type, %{status: :unpaid}, actor) assert {:ok, c1} = Ash.update(cycle1, %{}, actor: actor, action: :mark_as_paid) assert c1.status == :paid # paid -> suspended assert {:ok, c2} = Ash.update(c1, %{}, actor: actor, action: :mark_as_suspended) assert c2.status == :suspended # suspended -> unpaid assert {:ok, c3} = Ash.update(c2, %{}, actor: actor, action: :mark_as_unpaid) assert c3.status == :unpaid # unpaid -> suspended assert {:ok, c4} = Ash.update(c3, %{}, actor: actor, action: :mark_as_suspended) assert c4.status == :suspended # suspended -> paid assert {:ok, c5} = Ash.update(c4, %{}, actor: actor, action: :mark_as_paid) assert c5.status == :paid # paid -> unpaid assert {:ok, c6} = Ash.update(c5, %{}, actor: actor, action: :mark_as_unpaid) assert c6.status == :unpaid end end end