feat(vereinfacht): add DB schema, config and setting attributes
- Migrations: vereinfacht_contact_id on members, vereinfacht_* on settings - Mv.Config: Vereinfacht ENV/Settings helpers, vereinfacht_configured?, contact_view_url - Setting: vereinfacht_api_url, api_key, club_id
This commit is contained in:
parent
adea380d86
commit
b775f5f5c4
6 changed files with 542 additions and 2 deletions
|
|
@ -69,7 +69,10 @@ defmodule Mv.Membership.Setting do
|
||||||
:club_name,
|
:club_name,
|
||||||
:member_field_visibility,
|
:member_field_visibility,
|
||||||
:include_joining_cycle,
|
:include_joining_cycle,
|
||||||
:default_membership_fee_type_id
|
:default_membership_fee_type_id,
|
||||||
|
:vereinfacht_api_url,
|
||||||
|
:vereinfacht_api_key,
|
||||||
|
:vereinfacht_club_id
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -81,7 +84,10 @@ defmodule Mv.Membership.Setting do
|
||||||
:club_name,
|
:club_name,
|
||||||
:member_field_visibility,
|
:member_field_visibility,
|
||||||
:include_joining_cycle,
|
:include_joining_cycle,
|
||||||
:default_membership_fee_type_id
|
:default_membership_fee_type_id,
|
||||||
|
:vereinfacht_api_url,
|
||||||
|
:vereinfacht_api_key,
|
||||||
|
:vereinfacht_club_id
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -225,6 +231,26 @@ defmodule Mv.Membership.Setting do
|
||||||
description "Default membership fee type ID for new members"
|
description "Default membership fee type ID for new members"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Vereinfacht accounting software integration (can be overridden by ENV)
|
||||||
|
attribute :vereinfacht_api_url, :string do
|
||||||
|
allow_nil? true
|
||||||
|
public? true
|
||||||
|
description "Vereinfacht API base URL (e.g. https://api.verein.visuel.dev/api/v1)"
|
||||||
|
end
|
||||||
|
|
||||||
|
attribute :vereinfacht_api_key, :string do
|
||||||
|
allow_nil? true
|
||||||
|
public? true
|
||||||
|
description "Vereinfacht API key (Bearer token)"
|
||||||
|
sensitive? true
|
||||||
|
end
|
||||||
|
|
||||||
|
attribute :vereinfacht_club_id, :string do
|
||||||
|
allow_nil? true
|
||||||
|
public? true
|
||||||
|
description "Vereinfacht club ID for multi-tenancy"
|
||||||
|
end
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,4 +142,98 @@ defmodule Mv.Config do
|
||||||
|> Keyword.get(key, default)
|
|> Keyword.get(key, default)
|
||||||
|> parse_and_validate_integer(default)
|
|> parse_and_validate_integer(default)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Vereinfacht accounting software integration
|
||||||
|
# ENV variables take priority; fallback to Settings from database.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the Vereinfacht API base URL.
|
||||||
|
|
||||||
|
Reads from `VEREINFACHT_API_URL` env first, then from Settings.
|
||||||
|
"""
|
||||||
|
@spec vereinfacht_api_url() :: String.t() | nil
|
||||||
|
def vereinfacht_api_url do
|
||||||
|
env_or_setting("VEREINFACHT_API_URL", :vereinfacht_api_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the Vereinfacht API key (Bearer token).
|
||||||
|
|
||||||
|
Reads from `VEREINFACHT_API_KEY` env first, then from Settings.
|
||||||
|
"""
|
||||||
|
@spec vereinfacht_api_key() :: String.t() | nil
|
||||||
|
def vereinfacht_api_key do
|
||||||
|
env_or_setting("VEREINFACHT_API_KEY", :vereinfacht_api_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the Vereinfacht club ID for multi-tenancy.
|
||||||
|
|
||||||
|
Reads from `VEREINFACHT_CLUB_ID` env first, then from Settings.
|
||||||
|
"""
|
||||||
|
@spec vereinfacht_club_id() :: String.t() | nil
|
||||||
|
def vereinfacht_club_id do
|
||||||
|
env_or_setting("VEREINFACHT_CLUB_ID", :vereinfacht_club_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns true if Vereinfacht is fully configured (URL, API key, and club ID all set).
|
||||||
|
"""
|
||||||
|
@spec vereinfacht_configured?() :: boolean()
|
||||||
|
def vereinfacht_configured? do
|
||||||
|
present?(vereinfacht_api_url()) and present?(vereinfacht_api_key()) and
|
||||||
|
present?(vereinfacht_club_id())
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns true if any Vereinfacht ENV variable is set (used to gray out Settings UI).
|
||||||
|
"""
|
||||||
|
@spec vereinfacht_env_configured?() :: boolean()
|
||||||
|
def vereinfacht_env_configured? do
|
||||||
|
System.get_env("VEREINFACHT_API_URL") != nil or
|
||||||
|
System.get_env("VEREINFACHT_API_KEY") != nil or
|
||||||
|
System.get_env("VEREINFACHT_CLUB_ID") != nil
|
||||||
|
end
|
||||||
|
|
||||||
|
defp env_or_setting(env_key, setting_key) do
|
||||||
|
case System.get_env(env_key) do
|
||||||
|
nil -> get_vereinfacht_from_settings(setting_key)
|
||||||
|
value -> trim_nil(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_vereinfacht_from_settings(key) do
|
||||||
|
case Mv.Membership.get_settings() do
|
||||||
|
{:ok, settings} -> settings |> Map.get(key) |> trim_nil()
|
||||||
|
{:error, _} -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp trim_nil(nil), do: nil
|
||||||
|
|
||||||
|
defp trim_nil(s) when is_binary(s) do
|
||||||
|
t = String.trim(s)
|
||||||
|
if t == "", do: nil, else: t
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the URL to view a finance contact (e.g. in Vereinfacht frontend or API).
|
||||||
|
|
||||||
|
Uses the configured API base URL and appends /finance-contacts/{id}.
|
||||||
|
Can be extended later with a dedicated frontend URL setting.
|
||||||
|
"""
|
||||||
|
@spec vereinfacht_contact_view_url(String.t()) :: String.t() | nil
|
||||||
|
def vereinfacht_contact_view_url(contact_id) when is_binary(contact_id) do
|
||||||
|
base = vereinfacht_api_url()
|
||||||
|
|
||||||
|
if present?(base),
|
||||||
|
do: base |> String.trim_trailing("/") |> then(&"#{&1}/finance-contacts/#{contact_id}"),
|
||||||
|
else: nil
|
||||||
|
end
|
||||||
|
|
||||||
|
defp present?(nil), do: false
|
||||||
|
defp present?(s) when is_binary(s), do: String.trim(s) != ""
|
||||||
|
defp present?(_), do: false
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Mv.Repo.Migrations.AddVereinfachtContactIdToMembers 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
|
||||||
|
alter table(:members) do
|
||||||
|
add :vereinfacht_contact_id, :text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:members) do
|
||||||
|
remove :vereinfacht_contact_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
defmodule Mv.Repo.Migrations.AddVereinfachtSettings 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
|
||||||
|
alter table(:settings) do
|
||||||
|
add :vereinfacht_api_url, :text
|
||||||
|
add :vereinfacht_api_key, :text
|
||||||
|
add :vereinfacht_club_id, :text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:settings) do
|
||||||
|
remove :vereinfacht_club_id
|
||||||
|
remove :vereinfacht_api_key
|
||||||
|
remove :vereinfacht_api_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
234
priv/resource_snapshots/repo/members/20260218185510.json
Normal file
234
priv/resource_snapshots/repo/members/20260218185510.json
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
{
|
||||||
|
"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?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "first_name",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "last_name",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "email",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "join_date",
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "exit_date",
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "notes",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "city",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "street",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "house_number",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "postal_code",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "search_vector",
|
||||||
|
"type": "tsvector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "membership_fee_start_date",
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "vereinfacht_contact_id",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"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": "members_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": [],
|
||||||
|
"create_table_options": null,
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "4DF7F20D4C8D91E229906D6ADF87A4B5EB410672799753012DE4F0F49B470A51",
|
||||||
|
"identities": [
|
||||||
|
{
|
||||||
|
"all_tenants?": false,
|
||||||
|
"base_filter": null,
|
||||||
|
"index_name": "members_unique_email_index",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"type": "atom",
|
||||||
|
"value": "email"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "unique_email",
|
||||||
|
"nils_distinct?": true,
|
||||||
|
"where": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Mv.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "members"
|
||||||
|
}
|
||||||
140
priv/resource_snapshots/repo/settings/20260218185541.json
Normal file
140
priv/resource_snapshots/repo/settings/20260218185541.json
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"gen_random_uuid()\")",
|
||||||
|
"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": "club_name",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "member_field_visibility",
|
||||||
|
"type": "map"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "true",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "include_joining_cycle",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "default_membership_fee_type_id",
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "vereinfacht_api_url",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "vereinfacht_api_key",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "vereinfacht_club_id",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "inserted_at",
|
||||||
|
"type": "utc_datetime_usec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "updated_at",
|
||||||
|
"type": "utc_datetime_usec"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"create_table_options": null,
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "1038A37F021DFC347E325042D613B0359FEB7DAFAE3286CBCEAA940A52B71217",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Mv.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "settings"
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue