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
186 lines
6.3 KiB
Elixir
186 lines
6.3 KiB
Elixir
defmodule Mv.Membership.Setting do
|
|
@moduledoc """
|
|
Ash resource representing global application settings.
|
|
|
|
## Overview
|
|
Settings is a singleton resource that stores global configuration for the association,
|
|
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.
|
|
The resource is designed to be read and updated, but not created or destroyed
|
|
through normal CRUD operations. Initial settings should be seeded.
|
|
|
|
## Environment Variable Support
|
|
The `club_name` can be set via the `ASSOCIATION_NAME` environment variable.
|
|
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
|
|
{:ok, settings} = Mv.Membership.get_settings()
|
|
settings.club_name # => "My Club"
|
|
|
|
# Update club name
|
|
{:ok, updated} = Mv.Membership.update_settings(settings, %{club_name: "New Name"})
|
|
|
|
# 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,
|
|
data_layer: AshPostgres.DataLayer
|
|
|
|
postgres do
|
|
table "settings"
|
|
repo Mv.Repo
|
|
end
|
|
|
|
resource do
|
|
description "Global application settings (singleton resource)"
|
|
end
|
|
|
|
actions do
|
|
defaults [:read]
|
|
|
|
# Internal create action - not exposed via code interface
|
|
# 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,
|
|
:include_joining_cycle,
|
|
:default_membership_fee_type_id
|
|
]
|
|
end
|
|
|
|
update :update do
|
|
primary? true
|
|
require_atomic? false
|
|
|
|
accept [
|
|
:club_name,
|
|
:member_field_visibility,
|
|
:include_joining_cycle,
|
|
:default_membership_fee_type_id
|
|
]
|
|
end
|
|
|
|
update :update_member_field_visibility do
|
|
description "Updates the visibility configuration for member fields in the overview"
|
|
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
|
|
validate present(:club_name), on: [:create, :update]
|
|
validate string_length(:club_name, min: 1), on: [:create, :update]
|
|
|
|
# Validate member_field_visibility map structure and content
|
|
validate fn changeset, _context ->
|
|
visibility = Ash.Changeset.get_attribute(changeset, :member_field_visibility)
|
|
|
|
if visibility && is_map(visibility) do
|
|
# Validate all values are booleans
|
|
invalid_values =
|
|
Enum.filter(visibility, fn {_key, value} ->
|
|
not is_boolean(value)
|
|
end)
|
|
|
|
# Validate all keys are valid member fields
|
|
valid_field_strings = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
|
|
|
|
invalid_keys =
|
|
Enum.filter(visibility, fn {key, _value} ->
|
|
key not in valid_field_strings
|
|
end)
|
|
|> Enum.map(fn {key, _value} -> key end)
|
|
|
|
cond do
|
|
not Enum.empty?(invalid_values) ->
|
|
{:error,
|
|
field: :member_field_visibility,
|
|
message: "All values in member_field_visibility must be booleans"}
|
|
|
|
not Enum.empty?(invalid_keys) ->
|
|
{:error,
|
|
field: :member_field_visibility,
|
|
message: "Invalid member field keys: #{inspect(invalid_keys)}"}
|
|
|
|
true ->
|
|
:ok
|
|
end
|
|
else
|
|
:ok
|
|
end
|
|
end,
|
|
on: [:create, :update]
|
|
end
|
|
|
|
attributes do
|
|
uuid_primary_key :id
|
|
|
|
attribute :club_name, :string,
|
|
allow_nil?: false,
|
|
public?: true,
|
|
description: "The name of the association/club",
|
|
constraints: [
|
|
trim?: true,
|
|
min_length: 1
|
|
]
|
|
|
|
attribute :member_field_visibility, :map,
|
|
allow_nil?: true,
|
|
public?: true,
|
|
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
|