feat(membership-fees): add database schema and Ash domain structure

This commit is contained in:
Moritz 2025-12-11 16:27:06 +01:00 committed by moritz
parent e563d12be3
commit 4d1b33357e
14 changed files with 1405 additions and 7 deletions

View file

@ -0,0 +1,220 @@
defmodule Mv.MembershipFees.ForeignKeyTest do
@moduledoc """
Tests for foreign key behaviors (CASCADE and RESTRICT).
"""
use Mv.DataCase, async: true
alias Mv.MembershipFees.MembershipFeeCycle
alias Mv.MembershipFees.MembershipFeeType
alias Mv.Membership.Member
describe "CASCADE behavior" do
test "deleting member deletes associated membership_fee_cycles" do
# Create member
{:ok, member} =
Ash.create(Member, %{
first_name: "Cascade",
last_name: "Test",
email: "cascade.test.#{System.unique_integer([:positive])}@example.com"
})
# Create fee type
{:ok, fee_type} =
Ash.create(MembershipFeeType, %{
name: "Cascade Test Fee #{System.unique_integer([:positive])}",
amount: Decimal.new("100.00"),
interval: :monthly
})
# Create multiple cycles for this member
{:ok, cycle1} =
Ash.create(MembershipFeeCycle, %{
cycle_start: ~D[2025-01-01],
amount: Decimal.new("100.00"),
member_id: member.id,
membership_fee_type_id: fee_type.id
})
{:ok, cycle2} =
Ash.create(MembershipFeeCycle, %{
cycle_start: ~D[2025-02-01],
amount: Decimal.new("100.00"),
member_id: member.id,
membership_fee_type_id: fee_type.id
})
# Verify cycles exist
assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle1.id)
assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle2.id)
# Delete member
assert :ok = Ash.destroy(member)
# Verify cycles are also deleted (CASCADE)
# NotFound is wrapped in Ash.Error.Invalid
assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle1.id)
assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle2.id)
end
end
describe "RESTRICT behavior" do
test "cannot delete membership_fee_type if cycles reference it" do
# Create member
{:ok, member} =
Ash.create(Member, %{
first_name: "Restrict",
last_name: "Test",
email: "restrict.test.#{System.unique_integer([:positive])}@example.com"
})
# Create fee type
{:ok, fee_type} =
Ash.create(MembershipFeeType, %{
name: "Restrict Test Fee #{System.unique_integer([:positive])}",
amount: Decimal.new("100.00"),
interval: :monthly
})
# Create a cycle referencing this fee type
{:ok, _cycle} =
Ash.create(MembershipFeeCycle, %{
cycle_start: ~D[2025-01-01],
amount: Decimal.new("100.00"),
member_id: member.id,
membership_fee_type_id: fee_type.id
})
# Try to delete fee type - should fail due to RESTRICT
assert {:error, error} = Ash.destroy(fee_type)
# Check that it's a foreign key violation error
assert is_struct(error, Ash.Error.Invalid) or is_struct(error, Ash.Error.Unknown)
end
test "can delete membership_fee_type if no cycles reference it" do
# Create fee type without any cycles
{:ok, fee_type} =
Ash.create(MembershipFeeType, %{
name: "Deletable Fee #{System.unique_integer([:positive])}",
amount: Decimal.new("100.00"),
interval: :monthly
})
# Should be able to delete
assert :ok = Ash.destroy(fee_type)
# Verify it's gone (NotFound is wrapped in Ash.Error.Invalid)
assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeType, fee_type.id)
end
test "cannot delete membership_fee_type if members reference it" do
# Create fee type
{:ok, fee_type} =
Ash.create(MembershipFeeType, %{
name: "Member Ref Fee #{System.unique_integer([:positive])}",
amount: Decimal.new("100.00"),
interval: :monthly
})
# Create member with this fee type
{:ok, _member} =
Ash.create(Member, %{
first_name: "FeeType",
last_name: "Reference",
email: "feetype.ref.#{System.unique_integer([:positive])}@example.com",
membership_fee_type_id: fee_type.id
})
# Try to delete fee type - should fail due to RESTRICT
assert {:error, error} = Ash.destroy(fee_type)
assert is_struct(error, Ash.Error.Invalid) or is_struct(error, Ash.Error.Unknown)
end
end
describe "member extensions" do
test "member can be created with membership_fee_type_id" do
# Create fee type first
{:ok, fee_type} =
Ash.create(MembershipFeeType, %{
name: "Create Test Fee #{System.unique_integer([:positive])}",
amount: Decimal.new("100.00"),
interval: :yearly
})
# Create member with fee type
{:ok, member} =
Ash.create(Member, %{
first_name: "With",
last_name: "FeeType",
email: "with.feetype.#{System.unique_integer([:positive])}@example.com",
membership_fee_type_id: fee_type.id
})
assert member.membership_fee_type_id == fee_type.id
end
test "member can be created with membership_fee_start_date" do
{:ok, member} =
Ash.create(Member, %{
first_name: "With",
last_name: "StartDate",
email: "with.startdate.#{System.unique_integer([:positive])}@example.com",
membership_fee_start_date: ~D[2025-01-01]
})
assert member.membership_fee_start_date == ~D[2025-01-01]
end
test "member can be created without membership fee fields" do
{:ok, member} =
Ash.create(Member, %{
first_name: "No",
last_name: "FeeFields",
email: "no.feefields.#{System.unique_integer([:positive])}@example.com"
})
assert member.membership_fee_type_id == nil
assert member.membership_fee_start_date == nil
end
test "member can be updated with membership_fee_type_id" do
# Create fee type
{:ok, fee_type} =
Ash.create(MembershipFeeType, %{
name: "Update Test Fee #{System.unique_integer([:positive])}",
amount: Decimal.new("100.00"),
interval: :yearly
})
# Create member without fee type
{:ok, member} =
Ash.create(Member, %{
first_name: "Update",
last_name: "Test",
email: "update.test.#{System.unique_integer([:positive])}@example.com"
})
assert member.membership_fee_type_id == nil
# Update member with fee type
{:ok, updated_member} = Ash.update(member, %{membership_fee_type_id: fee_type.id})
assert updated_member.membership_fee_type_id == fee_type.id
end
test "member can be updated with membership_fee_start_date" do
{:ok, member} =
Ash.create(Member, %{
first_name: "Start",
last_name: "Date",
email: "start.date.#{System.unique_integer([:positive])}@example.com"
})
assert member.membership_fee_start_date == nil
{:ok, updated_member} = Ash.update(member, %{membership_fee_start_date: ~D[2025-06-01]})
assert updated_member.membership_fee_start_date == ~D[2025-06-01]
end
end
end