diff --git a/lib/mv/vereinfacht/client.ex b/lib/mv/vereinfacht/client.ex
index 58e06a9..6ec8c8c 100644
--- a/lib/mv/vereinfacht/client.ex
+++ b/lib/mv/vereinfacht/client.ex
@@ -10,6 +10,54 @@ defmodule Mv.Vereinfacht.Client do
@content_type "application/vnd.api+json"
+ @doc """
+ Tests the connection to the Vereinfacht API with the given credentials.
+
+ Makes a lightweight `GET /finance-contacts?page[size]=1` request to verify
+ that the API URL, API key, and club ID are valid and reachable.
+
+ ## Returns
+ - `{:ok, :connected}` – credentials are valid (HTTP 200)
+ - `{:error, :not_configured}` – any parameter is nil or blank
+ - `{:error, {:http, status, message}}` – API returned an error (e.g. 401, 403)
+ - `{:error, {:request_failed, reason}}` – network/transport error
+
+ ## Examples
+
+ iex> test_connection("https://api.example.com/api/v1", "token", "2")
+ {:ok, :connected}
+
+ iex> test_connection(nil, "token", "2")
+ {:error, :not_configured}
+ """
+ @spec test_connection(String.t() | nil, String.t() | nil, String.t() | nil) ::
+ {:ok, :connected} | {:error, term()}
+ def test_connection(api_url, api_key, club_id) do
+ if blank?(api_url) or blank?(api_key) or blank?(club_id) do
+ {:error, :not_configured}
+ else
+ url =
+ api_url
+ |> String.trim_trailing("/")
+ |> then(&"#{&1}/finance-contacts?page[size]=1")
+
+ case Req.get(url, [headers: headers(api_key)] ++ req_http_options()) do
+ {:ok, %{status: 200}} ->
+ {:ok, :connected}
+
+ {:ok, %{status: status, body: body}} ->
+ {:error, {:http, status, extract_error_message(body)}}
+
+ {:error, reason} ->
+ {:error, {:request_failed, reason}}
+ end
+ end
+ end
+
+ defp blank?(nil), do: true
+ defp blank?(s) when is_binary(s), do: String.trim(s) == ""
+ defp blank?(_), do: true
+
@doc """
Creates a finance contact in Vereinfacht for the given member.
@@ -360,5 +408,16 @@ defmodule Mv.Vereinfacht.Client do
defp extract_error_message(%{"errors" => [%{"detail" => d} | _]}) when is_binary(d), do: d
defp extract_error_message(%{"errors" => [%{"title" => t} | _]}) when is_binary(t), do: t
defp extract_error_message(body) when is_map(body), do: inspect(body)
+
+ defp extract_error_message(body) when is_binary(body) do
+ trimmed = String.trim(body)
+
+ if String.starts_with?(trimmed, "<") do
+ :html_response
+ else
+ trimmed
+ end
+ end
+
defp extract_error_message(other), do: inspect(other)
end
diff --git a/lib/mv/vereinfacht/vereinfacht.ex b/lib/mv/vereinfacht/vereinfacht.ex
index ce8005d..6520b64 100644
--- a/lib/mv/vereinfacht/vereinfacht.ex
+++ b/lib/mv/vereinfacht/vereinfacht.ex
@@ -14,6 +14,27 @@ defmodule Mv.Vereinfacht do
alias Mv.Helpers.SystemActor
alias Mv.Helpers
+ @doc """
+ Tests the connection to the Vereinfacht API using the current configuration.
+
+ Delegates to `Mv.Vereinfacht.Client.test_connection/3` with the values from
+ `Mv.Config` (ENV variables take priority over database settings).
+
+ ## Returns
+ - `{:ok, :connected}` – credentials are valid and API is reachable
+ - `{:error, :not_configured}` – URL, API key or club ID is missing
+ - `{:error, {:http, status, message}}` – API returned an error (e.g. 401, 403)
+ - `{:error, {:request_failed, reason}}` – network/transport error
+ """
+ @spec test_connection() :: {:ok, :connected} | {:error, term()}
+ def test_connection do
+ Client.test_connection(
+ Mv.Config.vereinfacht_api_url(),
+ Mv.Config.vereinfacht_api_key(),
+ Mv.Config.vereinfacht_club_id()
+ )
+ end
+
@doc """
Syncs a single member to Vereinfacht (create or update finance contact).
diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex
index b841931..c710f79 100644
--- a/lib/mv_web/live/global_settings_live.ex
+++ b/lib/mv_web/live/global_settings_live.ex
@@ -51,6 +51,7 @@ defmodule MvWeb.GlobalSettingsLive do
|> assign(:vereinfacht_app_url_env_set, Mv.Config.vereinfacht_app_url_env_set?())
|> assign(:vereinfacht_api_key_set, present?(settings.vereinfacht_api_key))
|> assign(:last_vereinfacht_sync_result, nil)
+ |> assign(:vereinfacht_test_result, nil)
|> assign_form()
{:ok, socket}
@@ -167,15 +168,29 @@ defmodule MvWeb.GlobalSettingsLive do
>
{gettext("Save Vereinfacht Settings")}
- <.button
- :if={Mv.Config.vereinfacht_configured?()}
- type="button"
- phx-click="sync_vereinfacht_contacts"
- phx-disable-with={gettext("Syncing...")}
- class="mt-4 btn-outline"
- >
- {gettext("Sync all members without Vereinfacht contact")}
-
+
+ <.button
+ :if={Mv.Config.vereinfacht_configured?()}
+ type="button"
+ phx-click="test_vereinfacht_connection"
+ phx-disable-with={gettext("Testing...")}
+ class="btn-outline"
+ >
+ {gettext("Test Integration")}
+
+ <.button
+ :if={Mv.Config.vereinfacht_configured?()}
+ type="button"
+ phx-click="sync_vereinfacht_contacts"
+ phx-disable-with={gettext("Syncing...")}
+ class="btn-outline"
+ >
+ {gettext("Sync all members without Vereinfacht contact")}
+
+
+ <%= if @vereinfacht_test_result do %>
+ <.vereinfacht_test_result result={@vereinfacht_test_result} />
+ <% end %>
<%= if @last_vereinfacht_sync_result do %>
<.vereinfacht_sync_result result={@last_vereinfacht_sync_result} />
<% end %>
@@ -207,6 +222,12 @@ defmodule MvWeb.GlobalSettingsLive do
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, setting_params))}
end
+ @impl true
+ def handle_event("test_vereinfacht_connection", _params, socket) do
+ result = Mv.Vereinfacht.test_connection()
+ {:noreply, assign(socket, :vereinfacht_test_result, result)}
+ end
+
@impl true
def handle_event("sync_vereinfacht_contacts", _params, socket) do
case Mv.Vereinfacht.sync_members_without_contact() do
@@ -246,15 +267,20 @@ defmodule MvWeb.GlobalSettingsLive do
actor = MvWeb.LiveHelpers.current_actor(socket)
# Never send blank API key so we do not overwrite the stored secret (security)
setting_params_clean = drop_blank_vereinfacht_api_key(setting_params)
+ saves_vereinfacht = vereinfacht_params?(setting_params_clean)
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, setting_params_clean, actor) do
{:ok, _updated_settings} ->
{:ok, fresh_settings} = Membership.get_settings()
+ test_result =
+ if saves_vereinfacht, do: Mv.Vereinfacht.test_connection(), else: nil
+
socket =
socket
|> assign(:settings, fresh_settings)
|> assign(:vereinfacht_api_key_set, present?(fresh_settings.vereinfacht_api_key))
+ |> assign(:vereinfacht_test_result, test_result)
|> put_flash(:info, gettext("Settings updated successfully"))
|> assign_form()
@@ -265,6 +291,12 @@ defmodule MvWeb.GlobalSettingsLive do
end
end
+ @vereinfacht_param_keys ~w[vereinfacht_api_url vereinfacht_api_key vereinfacht_club_id vereinfacht_app_url]
+
+ defp vereinfacht_params?(params) when is_map(params) do
+ Enum.any?(@vereinfacht_param_keys, &Map.has_key?(params, &1))
+ end
+
defp drop_blank_vereinfacht_api_key(params) when is_map(params) do
case params do
%{"vereinfacht_api_key" => v} when v in [nil, ""] ->
@@ -412,6 +444,109 @@ defmodule MvWeb.GlobalSettingsLive do
Gettext.dgettext(MvWeb.Gettext, "default", message)
end
+ attr :result, :any, required: true
+
+ defp vereinfacht_test_result(%{result: {:ok, :connected}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-check-circle" class="size-5 shrink-0" />
+ {gettext("Connection successful. API URL, API Key and Club ID are valid.")}
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, :not_configured}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
+ {gettext("Not configured. Please set API URL, API Key and Club ID.")}
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, _status, :html_response}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+
+ {gettext(
+ "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
+ )}
+
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, 401, _}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+ {gettext("Connection failed (HTTP 401): API key is invalid or missing.")}
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, 403, _}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+
+ {gettext(
+ "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
+ )}
+
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, 404, _}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+
+ {gettext(
+ "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
+ )}
+
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, status, message}}} = assigns) do
+ assigns = assign(assigns, :status, status)
+ assigns = assign(assigns, :message, message)
+
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+
+ {gettext("Connection failed (HTTP %{status}):", status: @status)}
+ {@message}
+
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:request_failed, _reason}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0" />
+
+ {gettext("Connection failed. Could not reach the API (network error or wrong URL).")}
+
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, _}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0" />
+ {gettext("Connection failed. Unknown error.")}
+
+ """
+ end
+
attr :result, :map, required: true
defp vereinfacht_sync_result(assigns) do
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 2f4c1b8..c0672c5 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -2682,6 +2682,61 @@ msgstr "Vereinfacht-Integration"
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr "Vereinfacht ist nicht konfiguriert. Bitte setze API-URL, API-Schlüssel und Vereins-ID."
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Test Integration"
+msgstr "Integration testen"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Testing..."
+msgstr "Wird getestet..."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection successful. API URL, API Key and Club ID are valid."
+msgstr "Verbindung erfolgreich. API-URL, API-Schlüssel und Vereins-ID sind korrekt."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Not configured. Please set API URL, API Key and Club ID."
+msgstr "Nicht konfiguriert. Bitte API-URL, API-Schlüssel und Vereins-ID setzen."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP %{status}):"
+msgstr "Verbindung fehlgeschlagen (HTTP %{status}):"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 401): API key is invalid or missing."
+msgstr "Verbindung fehlgeschlagen (HTTP 401): API-Schlüssel ist ungültig oder fehlt."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
+msgstr "Verbindung fehlgeschlagen (HTTP 403): Zugriff verweigert. Bitte Vereins-ID und Berechtigungen des API-Schlüssels prüfen."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
+msgstr "Verbindung fehlgeschlagen (HTTP 404): API-Endpunkt nicht gefunden. Bitte die API-URL prüfen (z. B. korrekter Versionspfad)."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
+msgstr "Verbindung fehlgeschlagen. Die URL zeigt nicht auf eine Vereinfacht-API (HTML statt JSON erhalten)."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Could not reach the API (network error or wrong URL)."
+msgstr "Verbindung fehlgeschlagen. API nicht erreichbar (Netzwerkfehler oder falsche URL)."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Unknown error."
+msgstr "Verbindung fehlgeschlagen. Unbekannter Fehler."
+
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index 98c2b91..e26b874 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -2683,6 +2683,61 @@ msgstr ""
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr ""
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Test Integration"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Testing..."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection successful. API URL, API Key and Club ID are valid."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Not configured. Please set API URL, API Key and Club ID."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP %{status}):"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 401): API key is invalid or missing."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Could not reach the API (network error or wrong URL)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Unknown error."
+msgstr ""
+
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht"
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index a76c9f6..839a43b 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -2683,6 +2683,61 @@ msgstr ""
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr ""
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Test Integration"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Testing..."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection successful. API URL, API Key and Club ID are valid."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Not configured. Please set API URL, API Key and Club ID."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP %{status}):"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 401): API key is invalid or missing."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Could not reach the API (network error or wrong URL)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Unknown error."
+msgstr ""
+
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht"