Add Vereinfacht app URL setting and contact view URL

- Setting attribute vereinfacht_app_url, migration, .env.example
- Config: vereinfacht_app_url() from env/setting or derived from API URL
- Contact view URL uses app URL with /en/admin/finances/contacts/{id}
- Global settings: App URL field, read-only when VEREINFACHT_APP_URL set
- Tests: update contact view URL expectations
This commit is contained in:
Moritz 2026-02-23 19:21:09 +01:00
parent 7db609deec
commit 9d3c72acff
Signed by: moritz
GPG key ID: 1020A035E5DD0824
6 changed files with 98 additions and 11 deletions

View file

@ -36,3 +36,4 @@ ASSOCIATION_NAME="Sportsclub XYZ"
# VEREINFACHT_API_URL=https://api.verein.visuel.dev/api/v1 # VEREINFACHT_API_URL=https://api.verein.visuel.dev/api/v1
# VEREINFACHT_API_KEY=your-api-key # VEREINFACHT_API_KEY=your-api-key
# VEREINFACHT_CLUB_ID=2 # VEREINFACHT_CLUB_ID=2
# VEREINFACHT_APP_URL=https://app.verein.visuel.dev

View file

@ -72,7 +72,8 @@ defmodule Mv.Membership.Setting do
:default_membership_fee_type_id, :default_membership_fee_type_id,
:vereinfacht_api_url, :vereinfacht_api_url,
:vereinfacht_api_key, :vereinfacht_api_key,
:vereinfacht_club_id :vereinfacht_club_id,
:vereinfacht_app_url
] ]
end end
@ -87,7 +88,8 @@ defmodule Mv.Membership.Setting do
:default_membership_fee_type_id, :default_membership_fee_type_id,
:vereinfacht_api_url, :vereinfacht_api_url,
:vereinfacht_api_key, :vereinfacht_api_key,
:vereinfacht_club_id :vereinfacht_club_id,
:vereinfacht_app_url
] ]
end end
@ -251,6 +253,13 @@ defmodule Mv.Membership.Setting do
description "Vereinfacht club ID for multi-tenancy" description "Vereinfacht club ID for multi-tenancy"
end end
attribute :vereinfacht_app_url, :string do
allow_nil? true
public? true
description "Vereinfacht app base URL for contact view links (e.g. https://app.verein.visuel.dev)"
end
timestamps() timestamps()
end end

View file

@ -178,6 +178,37 @@ defmodule Mv.Config do
env_or_setting("VEREINFACHT_CLUB_ID", :vereinfacht_club_id) env_or_setting("VEREINFACHT_CLUB_ID", :vereinfacht_club_id)
end end
@doc """
Returns the Vereinfacht app base URL for contact view links (frontend, not API).
Reads from `VEREINFACHT_APP_URL` env first, then from Settings.
Used to build links like https://app.verein.visuel.dev/en/admin/finances/contacts/{id}.
If not set, derived from API URL by replacing host \"api.\" with \"app.\" when possible.
"""
@spec vereinfacht_app_url() :: String.t() | nil
def vereinfacht_app_url do
env_or_setting("VEREINFACHT_APP_URL", :vereinfacht_app_url) ||
derive_app_url_from_api_url(vereinfacht_api_url())
end
defp derive_app_url_from_api_url(nil), do: nil
defp derive_app_url_from_api_url(api_url) when is_binary(api_url) do
api_url = String.trim(api_url)
uri = URI.parse(api_url)
host = uri.host || ""
if String.starts_with?(host, "api.") do
app_host = "app." <> String.slice(host, 4..-1//1)
scheme = uri.scheme || "https"
"#{scheme}://#{app_host}"
else
nil
end
end
defp derive_app_url_from_api_url(_), do: nil
@doc """ @doc """
Returns true if Vereinfacht is fully configured (URL, API key, and club ID all set). Returns true if Vereinfacht is fully configured (URL, API key, and club ID all set).
""" """
@ -211,6 +242,11 @@ defmodule Mv.Config do
""" """
def vereinfacht_club_id_env_set?, do: env_set?("VEREINFACHT_CLUB_ID") def vereinfacht_club_id_env_set?, do: env_set?("VEREINFACHT_CLUB_ID")
@doc """
Returns true if VEREINFACHT_APP_URL is set (field is read-only in Settings).
"""
def vereinfacht_app_url_env_set?, do: env_set?("VEREINFACHT_APP_URL")
defp env_set?(key) do defp env_set?(key) do
case System.get_env(key) do case System.get_env(key) do
nil -> false nil -> false
@ -241,18 +277,22 @@ defmodule Mv.Config do
end end
@doc """ @doc """
Returns the URL to view a finance contact (e.g. in Vereinfacht frontend or API). Returns the URL to view a finance contact in the Vereinfacht app (frontend).
Uses the configured API base URL and appends /finance-contacts/{id}. Uses the configured app base URL (or derived from API URL) and appends
Can be extended later with a dedicated frontend URL setting. /en/admin/finances/contacts/{id}. Returns nil if no app URL can be determined.
""" """
@spec vereinfacht_contact_view_url(String.t()) :: String.t() | nil @spec vereinfacht_contact_view_url(String.t()) :: String.t() | nil
def vereinfacht_contact_view_url(contact_id) when is_binary(contact_id) do def vereinfacht_contact_view_url(contact_id) when is_binary(contact_id) do
base = vereinfacht_api_url() base = vereinfacht_app_url()
if present?(base), if present?(base) do
do: base |> String.trim_trailing("/") |> then(&"#{&1}/finance-contacts/#{contact_id}"), base
else: nil |> String.trim_trailing("/")
|> then(&"#{&1}/en/admin/finances/contacts/#{contact_id}")
else
nil
end
end end
defp present?(nil), do: false defp present?(nil), do: false

View file

@ -48,6 +48,7 @@ defmodule MvWeb.GlobalSettingsLive do
|> assign(:vereinfacht_api_url_env_set, Mv.Config.vereinfacht_api_url_env_set?()) |> assign(:vereinfacht_api_url_env_set, Mv.Config.vereinfacht_api_url_env_set?())
|> assign(:vereinfacht_api_key_env_set, Mv.Config.vereinfacht_api_key_env_set?()) |> assign(:vereinfacht_api_key_env_set, Mv.Config.vereinfacht_api_key_env_set?())
|> assign(:vereinfacht_club_id_env_set, Mv.Config.vereinfacht_club_id_env_set?()) |> assign(:vereinfacht_club_id_env_set, Mv.Config.vereinfacht_club_id_env_set?())
|> assign(:vereinfacht_app_url_env_set, Mv.Config.vereinfacht_app_url_env_set?())
|> assign(:vereinfacht_api_key_set, present?(settings.vereinfacht_api_key)) |> assign(:vereinfacht_api_key_set, present?(settings.vereinfacht_api_key))
|> assign(:last_vereinfacht_sync_result, nil) |> assign(:last_vereinfacht_sync_result, nil)
|> assign_form() |> assign_form()
@ -142,6 +143,18 @@ defmodule MvWeb.GlobalSettingsLive do
if(@vereinfacht_club_id_env_set, do: gettext("From VEREINFACHT_CLUB_ID"), else: "2") if(@vereinfacht_club_id_env_set, do: gettext("From VEREINFACHT_CLUB_ID"), else: "2")
} }
/> />
<.input
field={@form[:vereinfacht_app_url]}
type="text"
label={gettext("App URL (contact view link)")}
disabled={@vereinfacht_app_url_env_set}
placeholder={
if(@vereinfacht_app_url_env_set,
do: gettext("From VEREINFACHT_APP_URL"),
else: "https://app.verein.visuel.dev"
)
}
/>
</div> </div>
<.button <.button
:if={ :if={

View file

@ -0,0 +1,15 @@
defmodule Mv.Repo.Migrations.AddVereinfachtAppUrl do
use Ecto.Migration
def up do
alter table(:settings) do
add :vereinfacht_app_url, :text
end
end
def down do
alter table(:settings) do
remove :vereinfacht_app_url
end
end
end

View file

@ -39,14 +39,23 @@ defmodule Mv.ConfigVereinfachtTest do
assert Mv.Config.vereinfacht_contact_view_url("123") == nil assert Mv.Config.vereinfacht_contact_view_url("123") == nil
end end
test "returns URL when API URL is set" do test "returns app contact view URL when API URL is set (derived app URL)" do
set_vereinfacht_env("VEREINFACHT_API_URL", "https://api.example.com/api/v1") set_vereinfacht_env("VEREINFACHT_API_URL", "https://api.example.com/api/v1")
assert Mv.Config.vereinfacht_contact_view_url("42") == assert Mv.Config.vereinfacht_contact_view_url("42") ==
"https://api.example.com/api/v1/finance-contacts/42" "https://app.example.com/en/admin/finances/contacts/42"
after after
clear_vereinfacht_env() clear_vereinfacht_env()
end end
test "returns app contact view URL when VEREINFACHT_APP_URL is set" do
set_vereinfacht_env("VEREINFACHT_APP_URL", "https://app.verein.visuel.dev")
assert Mv.Config.vereinfacht_contact_view_url("abc") ==
"https://app.verein.visuel.dev/en/admin/finances/contacts/abc"
after
System.delete_env("VEREINFACHT_APP_URL")
end
end end
defp set_vereinfacht_env(key, value) do defp set_vereinfacht_env(key, value) do