Vereinfacht API: use filter for contact lookup, drop extra required fields
- find_contact_by_email uses GET with filter[isExternal]=true and filter[email] - vereinfacht_required_member_fields is now empty (API accepts minimal payload)
This commit is contained in:
parent
23e1afa994
commit
96ca857e06
3 changed files with 27 additions and 62 deletions
|
|
@ -28,15 +28,17 @@ defmodule Mv.Constants do
|
||||||
|
|
||||||
@email_validator_checks [:html_input, :pow]
|
@email_validator_checks [:html_input, :pow]
|
||||||
|
|
||||||
# Member fields that are required when Vereinfacht integration is active (contact sync)
|
# No member fields are required solely for Vereinfacht; API accepts minimal payload
|
||||||
@vereinfacht_required_member_fields [:first_name, :last_name, :street, :postal_code, :city]
|
# (contactType + isExternal) when creating external contacts and supports filter by email for lookup.
|
||||||
|
@vereinfacht_required_member_fields []
|
||||||
|
|
||||||
def member_fields, do: @member_fields
|
def member_fields, do: @member_fields
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns member fields that are always required when Vereinfacht integration is configured.
|
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
|
def vereinfacht_required_member_fields, do: @vereinfacht_required_member_fields
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,12 +166,12 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@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
|
Uses GET /finance-contacts?filter[isExternal]=true&filter[email]=... so the API
|
||||||
fetch the first page and find the contact client-side. Returns {:ok, contact_id}
|
returns only matching external contacts. Returns {:ok, contact_id} if a contact
|
||||||
if a contact with that email exists, {:error, :not_found} if none, or
|
exists, {:error, :not_found} if none, or {:error, reason} on API/network failure.
|
||||||
{:error, reason} on API/network failure. Used before create for idempotency.
|
Used before create for idempotency.
|
||||||
"""
|
"""
|
||||||
@spec find_contact_by_email(String.t()) :: {:ok, String.t()} | {:error, :not_found | term()}
|
@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
|
def find_contact_by_email(email) when is_binary(email) do
|
||||||
|
|
@ -182,25 +182,17 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@find_contact_page_size 100
|
|
||||||
@find_contact_max_pages 100
|
|
||||||
|
|
||||||
defp do_find_contact_by_email(email) do
|
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")
|
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
|
case Req.get(url, [headers: headers(api_key())] ++ req_http_options()) do
|
||||||
{:ok, %{status: 200, body: body}} when is_map(body) ->
|
{: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}} ->
|
{:ok, %{status: status, body: body}} ->
|
||||||
{:error, {:http, status, extract_error_message(body)}}
|
{:error, {:http, status, extract_error_message(body)}}
|
||||||
|
|
@ -210,48 +202,12 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_find_contact_page_response(body, page, normalized) do
|
defp get_first_contact_id_from_list(%{"data" => [%{"id" => id} | _]}) do
|
||||||
case find_contact_id_by_email_in_list(body, normalized) do
|
normalize_contact_id(id)
|
||||||
id when is_binary(id) -> {:ok, id}
|
|
||||||
nil -> maybe_find_contact_next_page(body, page, normalized)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_find_contact_next_page(body, page, normalized) do
|
defp get_first_contact_id_from_list(%{"data" => []}), do: nil
|
||||||
data = Map.get(body, "data") || []
|
defp get_first_contact_id_from_list(_), do: nil
|
||||||
|
|
||||||
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
|
|
||||||
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 normalize_contact_id(id) when is_binary(id), do: id
|
defp normalize_contact_id(id) when is_binary(id), do: id
|
||||||
defp normalize_contact_id(id) when is_integer(id), do: to_string(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
|
||||||
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
|
defp build_member_struct do
|
||||||
%{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue