defmodule Mv.MembershipFees.MembershipFeeCycleTest do @moduledoc """ Tests for MembershipFeeCycle resource. """ use Mv.DataCase, async: true alias Mv.MembershipFees.MembershipFeeCycle alias Mv.MembershipFees.MembershipFeeType alias Mv.Membership.Member setup do # Create a member for testing {:ok, member} = Ash.create(Member, %{ first_name: "Test", last_name: "Member", email: "test.member.#{System.unique_integer([:positive])}@example.com" }) # Create a fee type for testing {:ok, fee_type} = Ash.create(MembershipFeeType, %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("100.00"), interval: :monthly }) %{member: member, fee_type: fee_type} end describe "create MembershipFeeCycle" do test "can create cycle with valid attributes", %{member: member, fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id } assert {:ok, %MembershipFeeCycle{} = cycle} = Ash.create(MembershipFeeCycle, attrs) assert cycle.cycle_start == ~D[2025-01-01] assert Decimal.equal?(cycle.amount, Decimal.new("100.00")) assert cycle.member_id == member.id assert cycle.membership_fee_type_id == fee_type.id end test "can create cycle with notes", %{member: member, fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id, notes: "First payment cycle" } assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) assert cycle.notes == "First payment cycle" end test "requires cycle_start", %{member: member, fee_type: fee_type} do attrs = %{ amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id } assert {:error, error} = Ash.create(MembershipFeeCycle, attrs) assert error_on_field?(error, :cycle_start) end test "requires amount", %{member: member, fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-01-01], member_id: member.id, membership_fee_type_id: fee_type.id } assert {:error, error} = Ash.create(MembershipFeeCycle, attrs) assert error_on_field?(error, :amount) end test "requires member_id", %{fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), membership_fee_type_id: fee_type.id } assert {:error, error} = Ash.create(MembershipFeeCycle, attrs) assert error_on_field?(error, :member_id) or error_on_field?(error, :member) end test "requires membership_fee_type_id", %{member: member} do attrs = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member.id } assert {:error, error} = Ash.create(MembershipFeeCycle, attrs) assert error_on_field?(error, :membership_fee_type_id) or error_on_field?(error, :membership_fee_type) end test "status defaults to :unpaid", %{member: member, fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id } assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) assert cycle.status == :unpaid end test "validates status enum values - unpaid", %{member: member, fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id, status: :unpaid } assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) assert cycle.status == :unpaid end test "validates status enum values - paid", %{member: member, fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-02-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id, status: :paid } assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) assert cycle.status == :paid end test "validates status enum values - suspended", %{member: member, fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-03-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id, status: :suspended } assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) assert cycle.status == :suspended end test "rejects invalid status values", %{member: member, fee_type: fee_type} do attrs = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id, status: :cancelled } assert {:error, error} = Ash.create(MembershipFeeCycle, attrs) assert error_on_field?(error, :status) end end describe "uniqueness constraint" do test "cannot create duplicate cycle for same member and cycle_start", %{ member: member, fee_type: fee_type } do attrs = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id } assert {:ok, _cycle1} = Ash.create(MembershipFeeCycle, attrs) assert {:error, error} = Ash.create(MembershipFeeCycle, attrs) # Should fail due to uniqueness constraint assert is_struct(error, Ash.Error.Invalid) end test "can create cycles for same member with different cycle_start", %{ member: member, fee_type: fee_type } do attrs1 = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id } attrs2 = %{ cycle_start: ~D[2025-02-01], amount: Decimal.new("100.00"), member_id: member.id, membership_fee_type_id: fee_type.id } assert {:ok, _cycle1} = Ash.create(MembershipFeeCycle, attrs1) assert {:ok, _cycle2} = Ash.create(MembershipFeeCycle, attrs2) end test "can create cycles for different members with same cycle_start", %{fee_type: fee_type} do {:ok, member1} = Ash.create(Member, %{ first_name: "Member", last_name: "One", email: "member.one.#{System.unique_integer([:positive])}@example.com" }) {:ok, member2} = Ash.create(Member, %{ first_name: "Member", last_name: "Two", email: "member.two.#{System.unique_integer([:positive])}@example.com" }) attrs1 = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member1.id, membership_fee_type_id: fee_type.id } attrs2 = %{ cycle_start: ~D[2025-01-01], amount: Decimal.new("100.00"), member_id: member2.id, membership_fee_type_id: fee_type.id } assert {:ok, _cycle1} = Ash.create(MembershipFeeCycle, attrs1) assert {:ok, _cycle2} = Ash.create(MembershipFeeCycle, attrs2) end end # Helper to check if an error occurred on a specific field defp error_on_field?(%Ash.Error.Invalid{} = error, field) do Enum.any?(error.errors, fn e -> case e do %{field: ^field} -> true %{fields: fields} when is_list(fields) -> field in fields _ -> false end end) end defp error_on_field?(_, _), do: false end