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, 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