mitgliederverwaltung/lib/membership_fees/membership_fee_cycle.ex

129 lines
3.5 KiB
Elixir

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
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
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, non-negative, max 2 decimal places)"
constraints min: 0, scale: 2
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