feat: implement automatic cycle generation for members
All checks were successful
continuous-integration/drone/push Build is passing

- Add CycleGenerator module with advisory lock mechanism
- Add SetMembershipFeeStartDate change for auto-calculation
- Extend Settings with include_joining_cycle and default_membership_fee_type_id
- Add scheduled job skeleton for future Oban integration
This commit is contained in:
Moritz 2025-12-11 21:16:47 +01:00
parent ecddf55331
commit 162d06da21
Signed by: moritz
GPG key ID: 1020A035E5DD0824
15 changed files with 2698 additions and 6 deletions

View file

@ -4,13 +4,15 @@ defmodule Mv.Membership.Setting do
## Overview
Settings is a singleton resource that stores global configuration for the association,
such as the club name and branding information. There should only ever be one settings
record in the database.
such as the club name, branding information, and membership fee settings. There should
only ever be one settings record in the database.
## Attributes
- `club_name` - The name of the association/club (required, cannot be empty)
- `member_field_visibility` - JSONB map storing visibility configuration for member fields
(e.g., `%{"street" => false, "house_number" => false}`). Fields not in the map default to `true`.
- `include_joining_cycle` - Whether to include the joining cycle in membership fee generation (default: true)
- `default_membership_fee_type_id` - Default membership fee type for new members (optional)
## Singleton Pattern
This resource uses a singleton pattern - there should only be one settings record.
@ -22,6 +24,12 @@ defmodule Mv.Membership.Setting do
If set, the environment variable value is used as a fallback when no database
value exists. Database values always take precedence over environment variables.
## Membership Fee Settings
- `include_joining_cycle`: When true, members pay from their joining cycle. When false,
they pay from the next full cycle after joining.
- `default_membership_fee_type_id`: The membership fee type automatically assigned to
new members. Can be nil if no default is set.
## Examples
# Get current settings
@ -33,6 +41,9 @@ defmodule Mv.Membership.Setting do
# Update member field visibility
{:ok, updated} = Mv.Membership.update_member_field_visibility(settings, %{"street" => false, "house_number" => false})
# Update membership fee settings
{:ok, updated} = Mv.Membership.update_settings(settings, %{include_joining_cycle: false})
"""
use Ash.Resource,
domain: Mv.Membership,
@ -54,13 +65,24 @@ defmodule Mv.Membership.Setting do
# Used only as fallback in get_settings/0 if settings don't exist
# Settings should normally be created via seed script
create :create do
accept [:club_name, :member_field_visibility]
accept [
:club_name,
:member_field_visibility,
:include_joining_cycle,
:default_membership_fee_type_id
]
end
update :update do
primary? true
require_atomic? false
accept [:club_name, :member_field_visibility]
accept [
:club_name,
:member_field_visibility,
:include_joining_cycle,
:default_membership_fee_type_id
]
end
update :update_member_field_visibility do
@ -68,6 +90,12 @@ defmodule Mv.Membership.Setting do
require_atomic? false
accept [:member_field_visibility]
end
update :update_membership_fee_settings do
description "Updates the membership fee configuration"
require_atomic? false
accept [:include_joining_cycle, :default_membership_fee_type_id]
end
end
validations do
@ -133,6 +161,26 @@ defmodule Mv.Membership.Setting do
description:
"Configuration for member field visibility in overview (JSONB map). Keys are member field names (atoms), values are booleans."
# Membership fee settings
attribute :include_joining_cycle, :boolean do
allow_nil? false
default true
public? true
description "Whether to include the joining cycle in membership fee generation"
end
attribute :default_membership_fee_type_id, :uuid do
allow_nil? true
public? true
description "Default membership fee type ID for new members"
end
timestamps()
end
relationships do
# Optional relationship to the default membership fee type
# Note: We use manual FK (default_membership_fee_type_id attribute) instead of belongs_to
# to avoid circular dependency between Membership and MembershipFees domains
end
end