feat: add status management actions to MembershipFeeCycle
This commit is contained in:
parent
f39fd49af3
commit
48d98b97b2
2 changed files with 186 additions and 234 deletions
|
|
@ -51,6 +51,33 @@ defmodule Mv.MembershipFees.MembershipFeeCycle do
|
||||||
primary? true
|
primary? true
|
||||||
accept [:status, :notes]
|
accept [:status, :notes]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
update :mark_as_paid do
|
||||||
|
description "Mark cycle as paid"
|
||||||
|
require_atomic? false
|
||||||
|
accept [:notes]
|
||||||
|
change fn changeset, _context ->
|
||||||
|
Ash.Changeset.force_change_attribute(changeset, :status, :paid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
update :mark_as_suspended do
|
||||||
|
description "Mark cycle as suspended"
|
||||||
|
require_atomic? false
|
||||||
|
accept [:notes]
|
||||||
|
change fn changeset, _context ->
|
||||||
|
Ash.Changeset.force_change_attribute(changeset, :status, :suspended)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
update :mark_as_unpaid do
|
||||||
|
description "Mark cycle as unpaid (for error correction)"
|
||||||
|
require_atomic? false
|
||||||
|
accept [:notes]
|
||||||
|
change fn changeset, _context ->
|
||||||
|
Ash.Changeset.force_change_attribute(changeset, :status, :unpaid)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Tests for MembershipFeeCycle resource.
|
Tests for MembershipFeeCycle resource, focusing on status management actions.
|
||||||
"""
|
"""
|
||||||
use Mv.DataCase, async: true
|
use Mv.DataCase, async: true
|
||||||
|
|
||||||
|
|
@ -8,275 +8,200 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
alias Mv.Membership.Member
|
alias Mv.Membership.Member
|
||||||
|
|
||||||
setup do
|
# Helper to create a membership fee type
|
||||||
# Create a member for testing
|
defp create_fee_type(attrs) do
|
||||||
{:ok, member} =
|
default_attrs = %{
|
||||||
Ash.create(Member, %{
|
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||||
first_name: "Test",
|
amount: Decimal.new("50.00"),
|
||||||
last_name: "Member",
|
interval: :yearly
|
||||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
}
|
||||||
})
|
|
||||||
|
|
||||||
# Create a fee type for testing
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
{: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}
|
MembershipFeeType
|
||||||
|
|> Ash.Changeset.for_create(:create, attrs)
|
||||||
|
|> Ash.create!()
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "create MembershipFeeCycle" do
|
# Helper to create a member
|
||||||
test "can create cycle with valid attributes", %{member: member, fee_type: fee_type} do
|
defp create_member(attrs) do
|
||||||
attrs = %{
|
default_attrs = %{
|
||||||
cycle_start: ~D[2025-01-01],
|
first_name: "Test",
|
||||||
amount: Decimal.new("100.00"),
|
last_name: "Member",
|
||||||
member_id: member.id,
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
membership_fee_type_id: fee_type.id
|
}
|
||||||
}
|
|
||||||
|
|
||||||
assert {:ok, %MembershipFeeCycle{} = cycle} = Ash.create(MembershipFeeCycle, attrs)
|
attrs = Map.merge(default_attrs, 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
|
Member
|
||||||
attrs = %{
|
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||||
cycle_start: ~D[2025-01-01],
|
|> Ash.create!()
|
||||||
amount: Decimal.new("100.00"),
|
end
|
||||||
member_id: member.id,
|
|
||||||
membership_fee_type_id: fee_type.id,
|
|
||||||
notes: "First payment cycle"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs)
|
# Helper to create a cycle
|
||||||
assert cycle.notes == "First payment cycle"
|
defp create_cycle(member, fee_type, attrs) do
|
||||||
end
|
default_attrs = %{
|
||||||
|
cycle_start: ~D[2024-01-01],
|
||||||
|
amount: Decimal.new("50.00"),
|
||||||
|
member_id: member.id,
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
}
|
||||||
|
|
||||||
test "requires cycle_start", %{member: member, fee_type: fee_type} do
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
attrs = %{
|
|
||||||
amount: Decimal.new("100.00"),
|
|
||||||
member_id: member.id,
|
|
||||||
membership_fee_type_id: fee_type.id
|
|
||||||
}
|
|
||||||
|
|
||||||
assert {:error, error} = Ash.create(MembershipFeeCycle, attrs)
|
MembershipFeeCycle
|
||||||
assert error_on_field?(error, :cycle_start)
|
|> Ash.Changeset.for_create(:create, attrs)
|
||||||
end
|
|> Ash.create!()
|
||||||
|
end
|
||||||
|
|
||||||
test "requires amount", %{member: member, fee_type: fee_type} do
|
describe "status defaults" do
|
||||||
attrs = %{
|
test "status defaults to :unpaid when creating a cycle" do
|
||||||
cycle_start: ~D[2025-01-01],
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
member_id: member.id,
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
membership_fee_type_id: fee_type.id
|
|
||||||
}
|
|
||||||
|
|
||||||
assert {:error, error} = Ash.create(MembershipFeeCycle, attrs)
|
cycle =
|
||||||
assert error_on_field?(error, :amount)
|
MembershipFeeCycle
|
||||||
end
|
|> 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!()
|
||||||
|
|
||||||
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
|
assert cycle.status == :unpaid
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "validates status enum values - unpaid", %{member: member, fee_type: fee_type} do
|
describe "mark_as_paid" do
|
||||||
attrs = %{
|
test "sets status to :paid" do
|
||||||
cycle_start: ~D[2025-01-01],
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
amount: Decimal.new("100.00"),
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
member_id: member.id,
|
cycle = create_cycle(member, fee_type, %{status: :unpaid})
|
||||||
membership_fee_type_id: fee_type.id,
|
|
||||||
status: :unpaid
|
|
||||||
}
|
|
||||||
|
|
||||||
assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs)
|
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_paid)
|
||||||
assert cycle.status == :unpaid
|
assert updated.status == :paid
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validates status enum values - paid", %{member: member, fee_type: fee_type} do
|
test "can set notes when marking as paid" do
|
||||||
attrs = %{
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
cycle_start: ~D[2025-02-01],
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
amount: Decimal.new("100.00"),
|
cycle = create_cycle(member, fee_type, %{status: :unpaid})
|
||||||
member_id: member.id,
|
|
||||||
membership_fee_type_id: fee_type.id,
|
|
||||||
status: :paid
|
|
||||||
}
|
|
||||||
|
|
||||||
assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs)
|
assert {:ok, updated} =
|
||||||
assert cycle.status == :paid
|
Ash.update(cycle, %{notes: "Payment received via bank transfer"},
|
||||||
|
action: :mark_as_paid
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated.status == :paid
|
||||||
|
assert updated.notes == "Payment received via bank transfer"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validates status enum values - suspended", %{member: member, fee_type: fee_type} do
|
test "can change from suspended to paid" do
|
||||||
attrs = %{
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
cycle_start: ~D[2025-03-01],
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
amount: Decimal.new("100.00"),
|
cycle = create_cycle(member, fee_type, %{status: :suspended})
|
||||||
member_id: member.id,
|
|
||||||
membership_fee_type_id: fee_type.id,
|
|
||||||
status: :suspended
|
|
||||||
}
|
|
||||||
|
|
||||||
assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs)
|
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_paid)
|
||||||
assert cycle.status == :suspended
|
assert updated.status == :paid
|
||||||
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
|
|
||||||
|
|
||||||
test "rejects negative amount", %{member: member, fee_type: fee_type} do
|
|
||||||
attrs = %{
|
|
||||||
cycle_start: ~D[2025-04-01],
|
|
||||||
amount: Decimal.new("-50.00"),
|
|
||||||
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 "accepts zero amount", %{member: member, fee_type: fee_type} do
|
|
||||||
attrs = %{
|
|
||||||
cycle_start: ~D[2025-05-01],
|
|
||||||
amount: Decimal.new("0.00"),
|
|
||||||
member_id: member.id,
|
|
||||||
membership_fee_type_id: fee_type.id
|
|
||||||
}
|
|
||||||
|
|
||||||
assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs)
|
|
||||||
assert Decimal.equal?(cycle.amount, Decimal.new("0.00"))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "uniqueness constraint" do
|
describe "mark_as_suspended" do
|
||||||
test "cannot create duplicate cycle for same member and cycle_start", %{
|
test "sets status to :suspended" do
|
||||||
member: member,
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
fee_type: fee_type
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
} do
|
cycle = create_cycle(member, fee_type, %{status: :unpaid})
|
||||||
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 {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_suspended)
|
||||||
assert {:error, error} = Ash.create(MembershipFeeCycle, attrs)
|
assert updated.status == :suspended
|
||||||
|
|
||||||
# Should fail due to uniqueness constraint
|
|
||||||
assert is_struct(error, Ash.Error.Invalid)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can create cycles for same member with different cycle_start", %{
|
test "can set notes when marking as suspended" do
|
||||||
member: member,
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
fee_type: fee_type
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
} do
|
cycle = create_cycle(member, fee_type, %{status: :unpaid})
|
||||||
attrs1 = %{
|
|
||||||
cycle_start: ~D[2025-01-01],
|
|
||||||
amount: Decimal.new("100.00"),
|
|
||||||
member_id: member.id,
|
|
||||||
membership_fee_type_id: fee_type.id
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs2 = %{
|
assert {:ok, updated} =
|
||||||
cycle_start: ~D[2025-02-01],
|
Ash.update(cycle, %{notes: "Waived due to special circumstances"},
|
||||||
amount: Decimal.new("100.00"),
|
action: :mark_as_suspended
|
||||||
member_id: member.id,
|
)
|
||||||
membership_fee_type_id: fee_type.id
|
|
||||||
}
|
|
||||||
|
|
||||||
assert {:ok, _cycle1} = Ash.create(MembershipFeeCycle, attrs1)
|
assert updated.status == :suspended
|
||||||
assert {:ok, _cycle2} = Ash.create(MembershipFeeCycle, attrs2)
|
assert updated.notes == "Waived due to special circumstances"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can create cycles for different members with same cycle_start", %{fee_type: fee_type} do
|
test "can change from paid to suspended" do
|
||||||
{:ok, member1} =
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
Ash.create(Member, %{
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
first_name: "Member",
|
cycle = create_cycle(member, fee_type, %{status: :paid})
|
||||||
last_name: "One",
|
|
||||||
email: "member.one.#{System.unique_integer([:positive])}@example.com"
|
|
||||||
})
|
|
||||||
|
|
||||||
{:ok, member2} =
|
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_suspended)
|
||||||
Ash.create(Member, %{
|
assert updated.status == :suspended
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to check if an error occurred on a specific field
|
describe "mark_as_unpaid" do
|
||||||
defp error_on_field?(%Ash.Error.Invalid{} = error, field) do
|
test "sets status to :unpaid" do
|
||||||
Enum.any?(error.errors, fn e ->
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
case e do
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
%{field: ^field} -> true
|
cycle = create_cycle(member, fee_type, %{status: :paid})
|
||||||
%{fields: fields} when is_list(fields) -> field in fields
|
|
||||||
_ -> false
|
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid)
|
||||||
end
|
assert updated.status == :unpaid
|
||||||
end)
|
end
|
||||||
|
|
||||||
|
test "can set notes when marking as unpaid" do
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
cycle = create_cycle(member, fee_type, %{status: :paid})
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
Ash.update(cycle, %{notes: "Payment was reversed"}, action: :mark_as_unpaid)
|
||||||
|
|
||||||
|
assert updated.status == :unpaid
|
||||||
|
assert updated.notes == "Payment was reversed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can change from suspended to unpaid" do
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
cycle = create_cycle(member, fee_type, %{status: :suspended})
|
||||||
|
|
||||||
|
assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid)
|
||||||
|
assert updated.status == :unpaid
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp error_on_field?(_, _), do: false
|
describe "status transitions" do
|
||||||
|
test "all status transitions are allowed" do
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
|
||||||
|
# unpaid -> paid
|
||||||
|
cycle1 = create_cycle(member, fee_type, %{status: :unpaid})
|
||||||
|
assert {:ok, c1} = Ash.update(cycle1, %{}, action: :mark_as_paid)
|
||||||
|
assert c1.status == :paid
|
||||||
|
|
||||||
|
# paid -> suspended
|
||||||
|
assert {:ok, c2} = Ash.update(c1, %{}, action: :mark_as_suspended)
|
||||||
|
assert c2.status == :suspended
|
||||||
|
|
||||||
|
# suspended -> unpaid
|
||||||
|
assert {:ok, c3} = Ash.update(c2, %{}, action: :mark_as_unpaid)
|
||||||
|
assert c3.status == :unpaid
|
||||||
|
|
||||||
|
# unpaid -> suspended
|
||||||
|
assert {:ok, c4} = Ash.update(c3, %{}, action: :mark_as_suspended)
|
||||||
|
assert c4.status == :suspended
|
||||||
|
|
||||||
|
# suspended -> paid
|
||||||
|
assert {:ok, c5} = Ash.update(c4, %{}, action: :mark_as_paid)
|
||||||
|
assert c5.status == :paid
|
||||||
|
|
||||||
|
# paid -> unpaid
|
||||||
|
assert {:ok, c6} = Ash.update(c5, %{}, action: :mark_as_unpaid)
|
||||||
|
assert c6.status == :unpaid
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue