diff --git a/.tool-versions b/.tool-versions index 489262a..98239f3 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ elixir 1.18.3-otp-27 erlang 27.3.4 -just 1.45.0 +just 1.43.1 diff --git a/config/config.exs b/config/config.exs index 053fc19..17891e0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -49,7 +49,7 @@ config :spark, config :mv, ecto_repos: [Mv.Repo], generators: [timestamp_type: :utc_datetime], - ash_domains: [Mv.Membership, Mv.Accounts, Mv.MembershipFees] + ash_domains: [Mv.Membership, Mv.Accounts] # Configures the endpoint config :mv, MvWeb.Endpoint, diff --git a/docker-compose.yml b/docker-compose.yml index feff34c..b10ab22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,7 @@ services: rauthy: container_name: rauthy-dev - image: ghcr.io/sebadob/rauthy:0.33.1 + image: ghcr.io/sebadob/rauthy:0.32.0 environment: - LOCAL_TEST=true - SMTP_URL=mailcrab diff --git a/docs/database_schema.dbml b/docs/database_schema.dbml index f97463e..b620830 100644 --- a/docs/database_schema.dbml +++ b/docs/database_schema.dbml @@ -6,8 +6,8 @@ // - https://dbdocs.io // - VS Code Extensions: "DBML Language" or "dbdiagram.io" // -// Version: 1.3 -// Last Updated: 2025-12-11 +// Version: 1.2 +// Last Updated: 2025-11-13 Project mila_membership_management { database_type: 'PostgreSQL' @@ -27,7 +27,6 @@ Project mila_membership_management { ## Domains: - **Accounts**: User authentication and session management - **Membership**: Club member data and custom fields - - **MembershipFees**: Membership fee types and billing cycles ## Required PostgreSQL Extensions: - uuid-ossp (UUID generation) @@ -133,8 +132,6 @@ Table members { house_number text [null, note: 'House number'] postal_code text [null, note: '5-digit German postal code'] search_vector tsvector [null, note: 'Full-text search index (auto-generated)'] - membership_fee_type_id uuid [null, note: 'FK to membership_fee_types - assigned fee type'] - membership_fee_start_date date [null, note: 'Date from which membership fees should be calculated'] indexes { email [unique, name: 'members_unique_email_index'] @@ -149,7 +146,6 @@ Table members { last_name [name: 'members_last_name_idx', note: 'B-tree index for name sorting'] join_date [name: 'members_join_date_idx', note: 'B-tree index for date filters'] (paid) [name: 'members_paid_idx', type: btree, note: 'Partial index WHERE paid IS NOT NULL'] - membership_fee_type_id [name: 'members_membership_fee_type_id_index', note: 'B-tree index for fee type lookups'] } Note: ''' @@ -182,8 +178,6 @@ Table members { **Relationships:** - Optional 1:1 with users (0..1 ↔ 0..1) - authentication account - 1:N with custom_field_values (custom dynamic fields) - - Optional N:1 with membership_fee_types - assigned fee type - - 1:N with membership_fee_cycles - billing history **Validation Rules:** - first_name, last_name: min 1 character @@ -287,98 +281,6 @@ Table custom_fields { ''' } -// ============================================ -// MEMBERSHIP_FEES DOMAIN -// ============================================ - -Table membership_fee_types { - id uuid [pk, not null, default: `uuid_generate_v7()`, note: 'UUIDv7 primary key'] - name text [not null, unique, note: 'Unique name for the fee type (e.g., "Standard", "Reduced")'] - amount numeric(10,2) [not null, note: 'Fee amount in default currency (CHECK: >= 0)'] - interval text [not null, note: 'Billing interval (CHECK: IN monthly, quarterly, half_yearly, yearly) - immutable'] - description text [null, note: 'Optional description for the fee type'] - - indexes { - name [unique, name: 'membership_fee_types_unique_name_index'] - } - - Note: ''' - **Membership Fee Type Definitions** - - Defines the different types of membership fees with fixed billing intervals. - - **Attributes:** - - `name`: Unique identifier for the fee type - - `amount`: Default fee amount (stored per cycle for audit trail) - - `interval`: Billing cycle - immutable after creation - - `description`: Optional documentation - - **Interval Values:** - - `monthly`: 1st to last day of month - - `quarterly`: 1st of Jan/Apr/Jul/Oct to last day of quarter - - `half_yearly`: 1st of Jan/Jul to last day of half - - `yearly`: Jan 1 to Dec 31 - - **Immutability:** - The `interval` field cannot be changed after creation to prevent - complex migration scenarios. Create a new fee type to change intervals. - - **Relationships:** - - 1:N with members - members assigned to this fee type - - 1:N with membership_fee_cycles - all cycles using this fee type - - **Deletion Behavior:** - - ON DELETE RESTRICT: Cannot delete if members or cycles reference it - ''' -} - -Table membership_fee_cycles { - id uuid [pk, not null, default: `uuid_generate_v7()`, note: 'UUIDv7 primary key'] - cycle_start date [not null, note: 'Start date of the billing cycle'] - amount numeric(10,2) [not null, note: 'Fee amount for this cycle (CHECK: >= 0)'] - status text [not null, default: 'unpaid', note: 'Payment status (CHECK: IN unpaid, paid, suspended)'] - notes text [null, note: 'Optional notes for this cycle'] - member_id uuid [not null, note: 'FK to members - the member this cycle belongs to'] - membership_fee_type_id uuid [not null, note: 'FK to membership_fee_types - fee type for this cycle'] - - indexes { - member_id [name: 'membership_fee_cycles_member_id_index'] - membership_fee_type_id [name: 'membership_fee_cycles_membership_fee_type_id_index'] - status [name: 'membership_fee_cycles_status_index'] - cycle_start [name: 'membership_fee_cycles_cycle_start_index'] - (member_id, cycle_start) [unique, name: 'membership_fee_cycles_unique_cycle_per_member_index', note: 'One cycle per member per cycle_start'] - } - - Note: ''' - **Individual Membership Fee Cycles** - - Represents a single billing cycle for a member with payment tracking. - - **Design Decisions:** - - `cycle_end` is NOT stored - calculated from cycle_start + interval - - `amount` is stored per cycle to preserve historical values when fee type amount changes - - Cycles are aligned to calendar boundaries - - **Status Values:** - - `unpaid`: Payment pending (default) - - `paid`: Payment received - - `suspended`: Payment suspended (e.g., hardship case) - - **Constraints:** - - Unique: One cycle per member per cycle_start date - - member_id: Required (belongs_to) - - membership_fee_type_id: Required (belongs_to) - - **Relationships:** - - N:1 with members - the member this cycle belongs to - - N:1 with membership_fee_types - the fee type for this cycle - - **Deletion Behavior:** - - ON DELETE CASCADE (member_id): Cycles deleted when member deleted - - ON DELETE RESTRICT (membership_fee_type_id): Cannot delete fee type if cycles exist - ''' -} - // ============================================ // RELATIONSHIPS // ============================================ @@ -404,22 +306,6 @@ Ref: custom_field_values.member_id > members.id [delete: cascade] // - ON DELETE RESTRICT: Cannot delete type if custom_field_values exist Ref: custom_field_values.custom_field_id > custom_fields.id [delete: restrict] -// Member → MembershipFeeType (N:1) -// - Many members can be assigned to one fee type -// - Optional relationship (member can have no fee type) -// - ON DELETE RESTRICT: Cannot delete fee type if members are assigned -Ref: members.membership_fee_type_id > membership_fee_types.id [delete: restrict] - -// MembershipFeeCycle → Member (N:1) -// - Many cycles belong to one member -// - ON DELETE CASCADE: Cycles deleted when member deleted -Ref: membership_fee_cycles.member_id > members.id [delete: cascade] - -// MembershipFeeCycle → MembershipFeeType (N:1) -// - Many cycles reference one fee type -// - ON DELETE RESTRICT: Cannot delete fee type if cycles reference it -Ref: membership_fee_cycles.membership_fee_type_id > membership_fee_types.id [delete: restrict] - // ============================================ // ENUMS // ============================================ @@ -442,21 +328,6 @@ Enum token_purpose { email_confirmation [note: 'Email verification tokens'] } -// Billing interval for membership fee types -Enum membership_fee_interval { - monthly [note: '1st to last day of month'] - quarterly [note: '1st of Jan/Apr/Jul/Oct to last day of quarter'] - half_yearly [note: '1st of Jan/Jul to last day of half'] - yearly [note: 'Jan 1 to Dec 31'] -} - -// Payment status for membership fee cycles -Enum membership_fee_status { - unpaid [note: 'Payment pending (default)'] - paid [note: 'Payment received'] - suspended [note: 'Payment suspended'] -} - // ============================================ // TABLE GROUPS // ============================================ @@ -486,17 +357,3 @@ TableGroup membership_domain { ''' } -TableGroup membership_fees_domain { - membership_fee_types - membership_fee_cycles - - Note: ''' - **Membership Fees Domain** - - Handles membership fee management including: - - Fee type definitions with intervals - - Individual billing cycles per member - - Payment status tracking - ''' -} - diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 5816d19..d29a759 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -79,8 +79,7 @@ defmodule Mv.Membership.Member do # user_id is NOT in accept list to prevent direct foreign key manipulation argument :user, :map, allow_nil?: true - # Accept member fields plus membership_fee_type_id (belongs_to FK) - accept @member_fields ++ [:membership_fee_type_id] + accept @member_fields change manage_relationship(:custom_field_values, type: :create) @@ -113,8 +112,7 @@ defmodule Mv.Membership.Member do # user_id is NOT in accept list to prevent direct foreign key manipulation argument :user, :map, allow_nil?: true - # Accept member fields plus membership_fee_type_id (belongs_to FK) - accept @member_fields ++ [:membership_fee_type_id] + accept @member_fields change manage_relationship(:custom_field_values, on_match: :update, on_no_match: :create) @@ -396,15 +394,6 @@ defmodule Mv.Membership.Member do writable?: false, public?: false, select_by_default?: false - - # Membership fee fields - # membership_fee_start_date: Date from which membership fees should be calculated - # If nil, calculated from join_date + global setting - attribute :membership_fee_start_date, :date do - allow_nil? true - public? true - description "Date from which membership fees should be calculated" - end end relationships do @@ -413,16 +402,6 @@ defmodule Mv.Membership.Member do # This references the User's member_id attribute # The relationship is optional (allow_nil? true by default) has_one :user, Mv.Accounts.User - - # Membership fee relationships - # belongs_to: The fee type assigned to this member - # Optional for MVP - can be nil if no fee type assigned yet - belongs_to :membership_fee_type, Mv.MembershipFees.MembershipFeeType do - allow_nil? true - end - - # has_many: All fee cycles for this member - has_many :membership_fee_cycles, Mv.MembershipFees.MembershipFeeCycle end # Define identities for upsert operations diff --git a/lib/membership_fees/membership_fee_cycle.ex b/lib/membership_fees/membership_fee_cycle.ex deleted file mode 100644 index 4c47623..0000000 --- a/lib/membership_fees/membership_fee_cycle.ex +++ /dev/null @@ -1,102 +0,0 @@ -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 diff --git a/lib/membership_fees/membership_fee_type.ex b/lib/membership_fees/membership_fee_type.ex deleted file mode 100644 index 877a385..0000000 --- a/lib/membership_fees/membership_fee_type.ex +++ /dev/null @@ -1,92 +0,0 @@ -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 (non-negative, max 2 decimal places)" - constraints min: 0, scale: 2 - 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 diff --git a/lib/membership_fees/membership_fees.ex b/lib/membership_fees/membership_fees.ex deleted file mode 100644 index 7a2833a..0000000 --- a/lib/membership_fees/membership_fees.ex +++ /dev/null @@ -1,42 +0,0 @@ -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 diff --git a/lib/mv/constants.ex b/lib/mv/constants.ex index 843ad2b..7bfb07b 100644 --- a/lib/mv/constants.ex +++ b/lib/mv/constants.ex @@ -15,8 +15,7 @@ defmodule Mv.Constants do :city, :street, :house_number, - :postal_code, - :membership_fee_start_date + :postal_code ] @custom_field_prefix "custom_field_" diff --git a/priv/repo/migrations/20251211151449_add_membership_fees_tables.exs b/priv/repo/migrations/20251211151449_add_membership_fees_tables.exs deleted file mode 100644 index e050521..0000000 --- a/priv/repo/migrations/20251211151449_add_membership_fees_tables.exs +++ /dev/null @@ -1,142 +0,0 @@ -defmodule Mv.Repo.Migrations.AddMembershipFeesTables do - @moduledoc """ - Updates resources based on their most recent snapshots. - - This file was autogenerated with `mix ash_postgres.generate_migrations` - """ - - use Ecto.Migration - - def up do - create table(:membership_fee_types, primary_key: false) do - add :id, :uuid, null: false, default: fragment("uuid_generate_v7()"), primary_key: true - add :name, :text, null: false - # Precision: 10 digits total, 2 decimal places (max 99,999,999.99) - add :amount, :numeric, null: false, precision: 10, scale: 2 - add :interval, :text, null: false - add :description, :text - end - - create unique_index(:membership_fee_types, [:name], - name: "membership_fee_types_unique_name_index" - ) - - # CHECK constraint for interval values (enforced at DB level) - create constraint(:membership_fee_types, :membership_fee_types_interval_check, - check: "interval IN ('monthly', 'quarterly', 'half_yearly', 'yearly')" - ) - - # CHECK constraint for non-negative amount - create constraint(:membership_fee_types, :membership_fee_types_amount_check, - check: "amount >= 0" - ) - - create table(:membership_fee_cycles, primary_key: false) do - add :id, :uuid, null: false, default: fragment("uuid_generate_v7()"), primary_key: true - add :cycle_start, :date, null: false - # Precision: 10 digits total, 2 decimal places (max 99,999,999.99) - add :amount, :numeric, null: false, precision: 10, scale: 2 - add :status, :text, null: false, default: "unpaid" - add :notes, :text - - # CASCADE: Delete cycles when member is deleted - add :member_id, - references(:members, - column: :id, - name: "membership_fee_cycles_member_id_fkey", - type: :uuid, - prefix: "public", - on_delete: :delete_all - ), - null: false - - # RESTRICT: Cannot delete fee type if cycles reference it - add :membership_fee_type_id, - references(:membership_fee_types, - column: :id, - name: "membership_fee_cycles_membership_fee_type_id_fkey", - type: :uuid, - prefix: "public", - on_delete: :restrict - ), - null: false - end - - # CHECK constraint for status values (enforced at DB level) - create constraint(:membership_fee_cycles, :membership_fee_cycles_status_check, - check: "status IN ('unpaid', 'paid', 'suspended')" - ) - - # CHECK constraint for non-negative amount - create constraint(:membership_fee_cycles, :membership_fee_cycles_amount_check, - check: "amount >= 0" - ) - - # Indexes as specified in architecture document - create index(:membership_fee_cycles, [:member_id]) - create index(:membership_fee_cycles, [:membership_fee_type_id]) - create index(:membership_fee_cycles, [:status]) - create index(:membership_fee_cycles, [:cycle_start]) - - # Composite unique index: one cycle per member per cycle_start - create unique_index(:membership_fee_cycles, [:member_id, :cycle_start], - name: "membership_fee_cycles_unique_cycle_per_member_index" - ) - - # Extend members table with membership fee fields - alter table(:members) do - add :membership_fee_start_date, :date - - # RESTRICT: Cannot delete fee type if members are assigned to it - add :membership_fee_type_id, - references(:membership_fee_types, - column: :id, - name: "members_membership_fee_type_id_fkey", - type: :uuid, - prefix: "public", - on_delete: :restrict - ) - end - - # Index for efficient lookup of members by fee type - create index(:members, [:membership_fee_type_id]) - end - - def down do - # First: Remove members extension (depends on membership_fee_types) - drop_if_exists index(:members, [:membership_fee_type_id]) - drop constraint(:members, "members_membership_fee_type_id_fkey") - - alter table(:members) do - remove :membership_fee_type_id - remove :membership_fee_start_date - end - - # Second: Drop cycles table (depends on membership_fee_types) - drop_if_exists unique_index(:membership_fee_cycles, [:member_id, :cycle_start], - name: "membership_fee_cycles_unique_cycle_per_member_index" - ) - - drop_if_exists index(:membership_fee_cycles, [:cycle_start]) - drop_if_exists index(:membership_fee_cycles, [:status]) - drop_if_exists index(:membership_fee_cycles, [:membership_fee_type_id]) - drop_if_exists index(:membership_fee_cycles, [:member_id]) - - drop constraint(:membership_fee_cycles, "membership_fee_cycles_member_id_fkey") - drop constraint(:membership_fee_cycles, "membership_fee_cycles_membership_fee_type_id_fkey") - drop_if_exists constraint(:membership_fee_cycles, :membership_fee_cycles_status_check) - drop_if_exists constraint(:membership_fee_cycles, :membership_fee_cycles_amount_check) - - drop table(:membership_fee_cycles) - - # Third: Drop fee types table - drop_if_exists unique_index(:membership_fee_types, [:name], - name: "membership_fee_types_unique_name_index" - ) - - drop_if_exists constraint(:membership_fee_types, :membership_fee_types_interval_check) - drop_if_exists constraint(:membership_fee_types, :membership_fee_types_amount_check) - - drop table(:membership_fee_types) - end -end diff --git a/priv/resource_snapshots/repo/membership_fee_cycles/20251211151449.json b/priv/resource_snapshots/repo/membership_fee_cycles/20251211151449.json deleted file mode 100644 index d0dd8fa..0000000 --- a/priv/resource_snapshots/repo/membership_fee_cycles/20251211151449.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "attributes": [ - { - "allow_nil?": false, - "default": "fragment(\"uuid_generate_v7()\")", - "generated?": false, - "precision": null, - "primary_key?": true, - "references": null, - "scale": null, - "size": null, - "source": "id", - "type": "uuid" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "cycle_start", - "type": "date" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "amount", - "type": "decimal" - }, - { - "allow_nil?": false, - "default": "\"unpaid\"", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "status", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "notes", - "type": "text" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": { - "deferrable": false, - "destination_attribute": "id", - "destination_attribute_default": null, - "destination_attribute_generated": null, - "index?": false, - "match_type": null, - "match_with": null, - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "name": "membership_fee_cycles_member_id_fkey", - "on_delete": null, - "on_update": null, - "primary_key?": true, - "schema": "public", - "table": "members" - }, - "scale": null, - "size": null, - "source": "member_id", - "type": "uuid" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": { - "deferrable": false, - "destination_attribute": "id", - "destination_attribute_default": null, - "destination_attribute_generated": null, - "index?": false, - "match_type": null, - "match_with": null, - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "name": "membership_fee_cycles_membership_fee_type_id_fkey", - "on_delete": null, - "on_update": null, - "primary_key?": true, - "schema": "public", - "table": "membership_fee_types" - }, - "scale": null, - "size": null, - "source": "membership_fee_type_id", - "type": "uuid" - } - ], - "base_filter": null, - "check_constraints": [], - "custom_indexes": [], - "custom_statements": [], - "has_create_action": true, - "hash": "43EA9EA365C09D423249AC4B6757A9AC07788C6C1E4BC7C50F8EF2CE01DE5684", - "identities": [ - { - "all_tenants?": false, - "base_filter": null, - "index_name": "membership_fee_cycles_unique_cycle_per_member_index", - "keys": [ - { - "type": "atom", - "value": "member_id" - }, - { - "type": "atom", - "value": "cycle_start" - } - ], - "name": "unique_cycle_per_member", - "nils_distinct?": true, - "where": null - } - ], - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "repo": "Elixir.Mv.Repo", - "schema": null, - "table": "membership_fee_cycles" -} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/membership_fee_types/20251211151449.json b/priv/resource_snapshots/repo/membership_fee_types/20251211151449.json deleted file mode 100644 index 037b02d..0000000 --- a/priv/resource_snapshots/repo/membership_fee_types/20251211151449.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "attributes": [ - { - "allow_nil?": false, - "default": "fragment(\"uuid_generate_v7()\")", - "generated?": false, - "precision": null, - "primary_key?": true, - "references": null, - "scale": null, - "size": null, - "source": "id", - "type": "uuid" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "name", - "type": "text" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "amount", - "type": "decimal" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "interval", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "description", - "type": "text" - } - ], - "base_filter": null, - "check_constraints": [], - "custom_indexes": [], - "custom_statements": [], - "has_create_action": true, - "hash": "E93A7A1EE90E5CEAC98CEA57C99C6330465716248642D5E2949EF578DE514E99", - "identities": [ - { - "all_tenants?": false, - "base_filter": null, - "index_name": "membership_fee_types_unique_name_index", - "keys": [ - { - "type": "atom", - "value": "name" - } - ], - "name": "unique_name", - "nils_distinct?": true, - "where": null - } - ], - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "repo": "Elixir.Mv.Repo", - "schema": null, - "table": "membership_fee_types" -} \ No newline at end of file diff --git a/test/membership_fees/foreign_key_test.exs b/test/membership_fees/foreign_key_test.exs deleted file mode 100644 index dd164a7..0000000 --- a/test/membership_fees/foreign_key_test.exs +++ /dev/null @@ -1,220 +0,0 @@ -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 diff --git a/test/membership_fees/membership_fee_cycle_test.exs b/test/membership_fees/membership_fee_cycle_test.exs deleted file mode 100644 index ca59e26..0000000 --- a/test/membership_fees/membership_fee_cycle_test.exs +++ /dev/null @@ -1,282 +0,0 @@ -defmodule Mv.MembershipFees.MembershipFeeCycleTest do - @moduledoc """ - Tests for MembershipFeeCycle resource. - """ - use Mv.DataCase, async: true - - alias Mv.MembershipFees.MembershipFeeCycle - alias Mv.MembershipFees.MembershipFeeType - alias Mv.Membership.Member - - setup do - # Create a member for testing - {:ok, member} = - Ash.create(Member, %{ - first_name: "Test", - last_name: "Member", - email: "test.member.#{System.unique_integer([:positive])}@example.com" - }) - - # Create a fee type for testing - {: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} - end - - describe "create MembershipFeeCycle" do - test "can create cycle with valid attributes", %{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, %MembershipFeeCycle{} = cycle} = Ash.create(MembershipFeeCycle, 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 - attrs = %{ - cycle_start: ~D[2025-01-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id, - notes: "First payment cycle" - } - - assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) - assert cycle.notes == "First payment cycle" - end - - test "requires cycle_start", %{member: member, fee_type: fee_type} do - attrs = %{ - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id - } - - assert {:error, error} = Ash.create(MembershipFeeCycle, attrs) - assert error_on_field?(error, :cycle_start) - end - - test "requires amount", %{member: member, fee_type: fee_type} do - attrs = %{ - cycle_start: ~D[2025-01-01], - 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 "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 - end - - test "validates status enum values - 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, - status: :unpaid - } - - assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) - assert cycle.status == :unpaid - end - - test "validates status enum values - paid", %{member: member, fee_type: fee_type} do - attrs = %{ - cycle_start: ~D[2025-02-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id, - status: :paid - } - - assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) - assert cycle.status == :paid - end - - test "validates status enum values - suspended", %{member: member, fee_type: fee_type} do - attrs = %{ - cycle_start: ~D[2025-03-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id, - status: :suspended - } - - assert {:ok, cycle} = Ash.create(MembershipFeeCycle, attrs) - assert cycle.status == :suspended - 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 - - describe "uniqueness constraint" do - test "cannot create duplicate cycle for same member and cycle_start", %{ - 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, _cycle1} = Ash.create(MembershipFeeCycle, attrs) - assert {:error, error} = Ash.create(MembershipFeeCycle, attrs) - - # Should fail due to uniqueness constraint - assert is_struct(error, Ash.Error.Invalid) - end - - test "can create cycles for same member with different cycle_start", %{ - member: member, - fee_type: fee_type - } do - attrs1 = %{ - cycle_start: ~D[2025-01-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id - } - - attrs2 = %{ - cycle_start: ~D[2025-02-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id - } - - assert {:ok, _cycle1} = Ash.create(MembershipFeeCycle, attrs1) - assert {:ok, _cycle2} = Ash.create(MembershipFeeCycle, attrs2) - end - - test "can create cycles for different members with same cycle_start", %{fee_type: fee_type} do - {:ok, member1} = - Ash.create(Member, %{ - first_name: "Member", - last_name: "One", - email: "member.one.#{System.unique_integer([:positive])}@example.com" - }) - - {:ok, member2} = - Ash.create(Member, %{ - 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 - - # Helper to check if an error occurred on a specific field - defp error_on_field?(%Ash.Error.Invalid{} = error, field) do - Enum.any?(error.errors, fn e -> - case e do - %{field: ^field} -> true - %{fields: fields} when is_list(fields) -> field in fields - _ -> false - end - end) - end - - defp error_on_field?(_, _), do: false -end diff --git a/test/membership_fees/membership_fee_type_test.exs b/test/membership_fees/membership_fee_type_test.exs deleted file mode 100644 index 9ca6f0a..0000000 --- a/test/membership_fees/membership_fee_type_test.exs +++ /dev/null @@ -1,172 +0,0 @@ -defmodule Mv.MembershipFees.MembershipFeeTypeTest do - @moduledoc """ - Tests for MembershipFeeType resource. - """ - use Mv.DataCase, async: true - - alias Mv.MembershipFees.MembershipFeeType - - describe "create MembershipFeeType" do - test "can create membership fee type with valid attributes" do - attrs = %{ - name: "Standard Membership", - amount: Decimal.new("120.00"), - interval: :yearly, - description: "Standard yearly membership fee" - } - - assert {:ok, %MembershipFeeType{} = fee_type} = - Ash.create(MembershipFeeType, attrs) - - assert fee_type.name == "Standard Membership" - assert Decimal.equal?(fee_type.amount, Decimal.new("120.00")) - assert fee_type.interval == :yearly - assert fee_type.description == "Standard yearly membership fee" - end - - test "can create membership fee type without description" do - attrs = %{ - name: "Basic", - amount: Decimal.new("60.00"), - interval: :monthly - } - - assert {:ok, %MembershipFeeType{}} = Ash.create(MembershipFeeType, attrs) - end - - test "requires name" do - attrs = %{ - amount: Decimal.new("100.00"), - interval: :yearly - } - - assert {:error, error} = Ash.create(MembershipFeeType, attrs) - assert error_on_field?(error, :name) - end - - test "requires amount" do - attrs = %{ - name: "Test Fee", - interval: :yearly - } - - assert {:error, error} = Ash.create(MembershipFeeType, attrs) - assert error_on_field?(error, :amount) - end - - test "requires interval" do - attrs = %{ - name: "Test Fee", - amount: Decimal.new("100.00") - } - - assert {:error, error} = Ash.create(MembershipFeeType, attrs) - assert error_on_field?(error, :interval) - end - - test "validates interval enum values - monthly" do - attrs = %{name: "Monthly", amount: Decimal.new("10.00"), interval: :monthly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) - assert fee_type.interval == :monthly - end - - test "validates interval enum values - quarterly" do - attrs = %{name: "Quarterly", amount: Decimal.new("30.00"), interval: :quarterly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) - assert fee_type.interval == :quarterly - end - - test "validates interval enum values - half_yearly" do - attrs = %{name: "Half Yearly", amount: Decimal.new("60.00"), interval: :half_yearly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) - assert fee_type.interval == :half_yearly - end - - test "validates interval enum values - yearly" do - attrs = %{name: "Yearly", amount: Decimal.new("120.00"), interval: :yearly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) - assert fee_type.interval == :yearly - end - - test "rejects invalid interval values" do - attrs = %{name: "Invalid", amount: Decimal.new("100.00"), interval: :weekly} - assert {:error, error} = Ash.create(MembershipFeeType, attrs) - assert error_on_field?(error, :interval) - end - - test "name must be unique" do - attrs = %{name: "Unique Name", amount: Decimal.new("100.00"), interval: :yearly} - - assert {:ok, _} = Ash.create(MembershipFeeType, attrs) - assert {:error, error} = Ash.create(MembershipFeeType, attrs) - - # Check for uniqueness error - assert error_on_field?(error, :name) - end - - test "rejects negative amount" do - attrs = %{name: "Negative Test", amount: Decimal.new("-10.00"), interval: :yearly} - assert {:error, error} = Ash.create(MembershipFeeType, attrs) - assert error_on_field?(error, :amount) - end - - test "accepts zero amount" do - attrs = %{name: "Zero Amount", amount: Decimal.new("0.00"), interval: :yearly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) - assert Decimal.equal?(fee_type.amount, Decimal.new("0.00")) - end - - test "amount respects scale of 2 decimal places" do - attrs = %{name: "Scale Test", amount: Decimal.new("100.50"), interval: :yearly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) - assert Decimal.equal?(fee_type.amount, Decimal.new("100.50")) - end - end - - describe "update MembershipFeeType" do - setup do - {:ok, fee_type} = - Ash.create(MembershipFeeType, %{ - name: "Original Name", - amount: Decimal.new("100.00"), - interval: :yearly, - description: "Original description" - }) - - %{fee_type: fee_type} - end - - test "can update name", %{fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"}) - assert updated.name == "Updated Name" - end - - test "can update amount", %{fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")}) - assert Decimal.equal?(updated.amount, Decimal.new("150.00")) - end - - test "can update description", %{fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{description: "Updated description"}) - assert updated.description == "Updated description" - end - - test "can clear description", %{fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{description: nil}) - assert updated.description == nil - end - end - - # Helper to check if an error occurred on a specific field - defp error_on_field?(%Ash.Error.Invalid{} = error, field) do - Enum.any?(error.errors, fn e -> - case e do - %{field: ^field} -> true - %{fields: fields} when is_list(fields) -> field in fields - _ -> false - end - end) - end - - defp error_on_field?(_, _), do: false -end