Vereinfacht API: filter-based contact lookup, no extra required fields, country sync, and docs #459
3 changed files with 27 additions and 62 deletions
|
|
@ -28,15 +28,17 @@ defmodule Mv.Constants do
|
|||
|
||||
@email_validator_checks [:html_input, :pow]
|
||||
|
||||
# Member fields that are required when Vereinfacht integration is active (contact sync)
|
||||
@vereinfacht_required_member_fields [:first_name, :last_name, :street, :postal_code, :city]
|
||||
# No member fields are required solely for Vereinfacht; API accepts minimal payload
|
||||
# (contactType + isExternal) when creating external contacts and supports filter by email for lookup.
|
||||
@vereinfacht_required_member_fields []
|
||||
|
||||
def member_fields, do: @member_fields
|
||||
|
||||
@doc """
|
||||
Returns member fields that are always required when Vereinfacht integration is configured.
|
||||
|
||||
Used for validation, member form required indicators, and settings UI (checkbox disabled).
|
||||
Currently empty: the Vereinfacht API only requires contactType (e.g. "person") when creating
|
||||
external contacts; lookup uses filter[email] so no extra required fields in the app.
|
||||
"""
|
||||
def vereinfacht_required_member_fields, do: @vereinfacht_required_member_fields
|
||||
|
||||
|
|
|
|||
|
|
@ -166,12 +166,12 @@ defmodule Mv.Vereinfacht.Client do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Finds a finance contact by email (GET /finance-contacts, then match in response).
|
||||
Finds a finance contact by email using the API filter.
|
||||
|
||||
The Vereinfacht API does not allow filter by email on this endpoint, so we
|
||||
fetch the first page and find the contact client-side. Returns {:ok, contact_id}
|
||||
if a contact with that email exists, {:error, :not_found} if none, or
|
||||
{:error, reason} on API/network failure. Used before create for idempotency.
|
||||
Uses GET /finance-contacts?filter[isExternal]=true&filter[email]=... so the API
|
||||
returns only matching external contacts. Returns {:ok, contact_id} if a contact
|
||||
exists, {:error, :not_found} if none, or {:error, reason} on API/network failure.
|
||||
Used before create for idempotency.
|
||||
"""
|
||||
@spec find_contact_by_email(String.t()) :: {:ok, String.t()} | {:error, :not_found | term()}
|
||||
def find_contact_by_email(email) when is_binary(email) do
|
||||
|
|
@ -182,25 +182,17 @@ defmodule Mv.Vereinfacht.Client do
|
|||
end
|
||||
end
|
||||
|
||||
@find_contact_page_size 100
|
||||
@find_contact_max_pages 100
|
||||
|
||||
defp do_find_contact_by_email(email) do
|
||||
normalized = String.trim(email) |> String.downcase()
|
||||
do_find_contact_by_email_page(1, normalized)
|
||||
end
|
||||
|
||||
defp do_find_contact_by_email_page(page, _normalized) when page > @find_contact_max_pages do
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
defp do_find_contact_by_email_page(page, normalized) do
|
||||
base = base_url() |> String.trim_trailing("/") |> then(&"#{&1}/finance-contacts")
|
||||
url = base <> "?page[size]=#{@find_contact_page_size}&page[number]=#{page}"
|
||||
encoded_email = URI.encode_www_form(email |> String.trim())
|
||||
url = "#{base}?filter[isExternal]=true&filter[email]=#{encoded_email}"
|
||||
|
||||
case Req.get(url, [headers: headers(api_key())] ++ req_http_options()) do
|
||||
{:ok, %{status: 200, body: body}} when is_map(body) ->
|
||||
handle_find_contact_page_response(body, page, normalized)
|
||||
case get_first_contact_id_from_list(body) do
|
||||
nil -> {:error, :not_found}
|
||||
id -> {:ok, id}
|
||||
end
|
||||
|
||||
{:ok, %{status: status, body: body}} ->
|
||||
{:error, {:http, status, extract_error_message(body)}}
|
||||
|
|
@ -210,48 +202,12 @@ defmodule Mv.Vereinfacht.Client do
|
|||
end
|
||||
end
|
||||
|
||||
defp handle_find_contact_page_response(body, page, normalized) do
|
||||
case find_contact_id_by_email_in_list(body, normalized) do
|
||||
id when is_binary(id) -> {:ok, id}
|
||||
nil -> maybe_find_contact_next_page(body, page, normalized)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_find_contact_next_page(body, page, normalized) do
|
||||
data = Map.get(body, "data") || []
|
||||
|
||||
if length(data) < @find_contact_page_size,
|
||||
do: {:error, :not_found},
|
||||
else: do_find_contact_by_email_page(page + 1, normalized)
|
||||
end
|
||||
|
||||
defp find_contact_id_by_email_in_list(%{"data" => list}, normalized) when is_list(list) do
|
||||
Enum.find_value(list, fn
|
||||
%{"id" => id, "attributes" => %{"email" => att_email, "isExternal" => true}}
|
||||
when is_binary(att_email) ->
|
||||
if att_email |> String.trim() |> String.downcase() == normalized do
|
||||
defp get_first_contact_id_from_list(%{"data" => [%{"id" => id} | _]}) do
|
||||
normalize_contact_id(id)
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
%{"id" => id, "attributes" => %{"email" => att_email, "isExternal" => "true"}}
|
||||
when is_binary(att_email) ->
|
||||
if att_email |> String.trim() |> String.downcase() == normalized do
|
||||
normalize_contact_id(id)
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
%{"id" => _id, "attributes" => _} ->
|
||||
nil
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_contact_id_by_email_in_list(_, _), do: nil
|
||||
defp get_first_contact_id_from_list(%{"data" => []}), do: nil
|
||||
defp get_first_contact_id_from_list(_), do: nil
|
||||
|
||||
defp normalize_contact_id(id) when is_binary(id), do: id
|
||||
defp normalize_contact_id(id) when is_integer(id), do: to_string(id)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,13 @@ defmodule Mv.Vereinfacht.ClientTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "find_contact_by_email/1" do
|
||||
test "returns {:error, :not_configured} when Vereinfacht is not configured" do
|
||||
assert Client.find_contact_by_email("kayley.becker@example.com") ==
|
||||
{:error, :not_configured}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_member_struct do
|
||||
%{
|
||||
first_name: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue