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,99 @@
defmodule Mv.MembershipFees.MembershipFeeCycle do
@moduledoc """
Ash resource representing an individual membership fee cycle for a member.
## Overview
MembershipFeeCycle represents a single billing cycle for a member. Each cycle
tracks the payment status and amount for a specific time period.
## Attributes
- `cycle_start` - Start date of the billing cycle (aligned to calendar boundaries)
- `amount` - The fee amount for this cycle (stored for audit trail)
- `status` - Payment status: unpaid, paid, or suspended
- `notes` - Optional notes for this cycle
## Design Decisions
- **No cycle_end field**: Calculated from cycle_start + interval (from fee type)
- **Amount stored per cycle**: Preserves historical amounts when fee type changes
- **Calendar-aligned cycles**: All cycles start on calendar boundaries
## Relationships
- `belongs_to :member` - The member this cycle belongs to
- `belongs_to :membership_fee_type` - The fee type for this cycle
## Constraints
- Unique constraint on (member_id, cycle_start) - one cycle per period per member
- CASCADE delete when member is deleted
- RESTRICT delete on membership_fee_type if cycles exist
"""
use Ash.Resource,
domain: Mv.MembershipFees,
data_layer: AshPostgres.DataLayer
postgres do
table "membership_fee_cycles"
repo Mv.Repo
end
resource do
description "Individual membership fee cycle for a member"
end
actions do
defaults [:read, :destroy]
create :create do
primary? true
accept [:cycle_start, :amount, :status, :notes, :member_id, :membership_fee_type_id]
end
update :update do
primary? true
accept [:status, :notes]
end
end
attributes do
uuid_v7_primary_key :id
attribute :cycle_start, :date do
allow_nil? false
public? true
description "Start date of the billing cycle"
end
attribute :amount, :decimal do
allow_nil? false
public? true
description "Fee amount for this cycle (stored for audit trail)"
end
attribute :status, :atom do
allow_nil? false
public? true
default :unpaid
description "Payment status of this cycle"
constraints one_of: [:unpaid, :paid, :suspended]
end
attribute :notes, :string do
allow_nil? true
public? true
description "Optional notes for this cycle"
end
end
relationships do
belongs_to :member, Mv.Membership.Member do
allow_nil? false
end
belongs_to :membership_fee_type, Mv.MembershipFees.MembershipFeeType do
allow_nil? false
end
end
identities do
identity :unique_cycle_per_member, [:member_id, :cycle_start]
end
end

View file

@ -0,0 +1,91 @@
defmodule Mv.MembershipFees.MembershipFeeType do
@moduledoc """
Ash resource representing a membership fee type definition.
## Overview
MembershipFeeType defines the different types of membership fees that can be
assigned to members. Each type has a fixed interval (billing cycle) and a
default amount.
## Attributes
- `name` - Unique name for the fee type (e.g., "Standard", "Reduced", "Family")
- `amount` - The fee amount in the default currency (decimal)
- `interval` - Billing interval: monthly, quarterly, half_yearly, or yearly
- `description` - Optional description for the fee type
## Immutability
The `interval` field is immutable after creation. This prevents complex
migration scenarios when changing billing cycles. To change intervals,
create a new fee type and migrate members.
## Relationships
- `has_many :members` - Members assigned to this fee type
- `has_many :membership_fee_cycles` - All cycles using this fee type
"""
use Ash.Resource,
domain: Mv.MembershipFees,
data_layer: AshPostgres.DataLayer
postgres do
table "membership_fee_types"
repo Mv.Repo
end
resource do
description "Membership fee type definition with interval and amount"
end
actions do
defaults [:read, :destroy]
create :create do
primary? true
accept [:name, :amount, :interval, :description]
end
update :update do
primary? true
# Note: interval is NOT in accept list - it's immutable after creation
# Immutability validation will be added in a future issue
accept [:name, :amount, :description]
end
end
attributes do
uuid_v7_primary_key :id
attribute :name, :string do
allow_nil? false
public? true
description "Unique name for the membership fee type"
end
attribute :amount, :decimal do
allow_nil? false
public? true
description "Fee amount in default currency"
end
attribute :interval, :atom do
allow_nil? false
public? true
description "Billing interval (immutable after creation)"
constraints one_of: [:monthly, :quarterly, :half_yearly, :yearly]
end
attribute :description, :string do
allow_nil? true
public? true
description "Optional description for the fee type"
end
end
relationships do
has_many :membership_fee_cycles, Mv.MembershipFees.MembershipFeeCycle
has_many :members, Mv.Membership.Member
end
identities do
identity :unique_name, [:name]
end
end

View file

@ -0,0 +1,42 @@
defmodule Mv.MembershipFees do
@moduledoc """
Ash Domain for membership fee management.
## Resources
- `MembershipFeeType` - Defines membership fee types with intervals and amounts
- `MembershipFeeCycle` - Individual membership fee cycles per member
## Overview
This domain handles the complete membership fee lifecycle including:
- Fee type definitions (monthly, quarterly, half-yearly, yearly)
- Individual fee cycles for each member
- Payment status tracking (unpaid, paid, suspended)
## Architecture Decisions
- `interval` field on MembershipFeeType is immutable after creation
- `cycle_end` is calculated, not stored (from cycle_start + interval)
- `amount` is stored per cycle for audit trail when prices change
"""
use Ash.Domain,
extensions: [AshAdmin.Domain, AshPhoenix]
admin do
show? true
end
resources do
resource Mv.MembershipFees.MembershipFeeType do
define :create_membership_fee_type, action: :create
define :list_membership_fee_types, action: :read
define :update_membership_fee_type, action: :update
define :destroy_membership_fee_type, action: :destroy
end
resource Mv.MembershipFees.MembershipFeeCycle do
define :create_membership_fee_cycle, action: :create
define :list_membership_fee_cycles, action: :read
define :update_membership_fee_cycle, action: :update
define :destroy_membership_fee_cycle, action: :destroy
end
end
end