- Application: create SyncFlash ETS table on start - Vereinfacht: Client, SyncFlash, sync_member, format_error, sync_members_without_contact - SyncContact change on Member create_member and update_member - Member: attribute vereinfacht_contact_id, internal action set_vereinfacht_contact_id
222 lines
6.5 KiB
Elixir
222 lines
6.5 KiB
Elixir
defmodule Mv.Vereinfacht.Client do
|
|
@moduledoc """
|
|
HTTP client for the Vereinfacht accounting software JSON:API.
|
|
|
|
Creates and updates finance contacts. Uses Bearer token authentication and
|
|
requires club ID for multi-tenancy. Configuration via ENV or Settings
|
|
(see Mv.Config).
|
|
"""
|
|
require Logger
|
|
|
|
@content_type "application/vnd.api+json"
|
|
|
|
@doc """
|
|
Creates a finance contact in Vereinfacht for the given member.
|
|
|
|
Returns the contact ID on success. Does not update the member record;
|
|
the caller (e.g. SyncContact change) must persist `vereinfacht_contact_id`.
|
|
|
|
## Options
|
|
- None; URL, API key, and club ID are read from Mv.Config.
|
|
|
|
## Examples
|
|
|
|
iex> create_contact(member)
|
|
{:ok, "242"}
|
|
|
|
iex> create_contact(member)
|
|
{:error, {:http, 401, "Unauthenticated."}}
|
|
"""
|
|
@spec create_contact(struct()) :: {:ok, String.t()} | {:error, term()}
|
|
def create_contact(member) do
|
|
base_url = base_url()
|
|
api_key = api_key()
|
|
club_id = club_id()
|
|
|
|
if is_nil(base_url) or is_nil(api_key) or is_nil(club_id) do
|
|
{:error, :not_configured}
|
|
else
|
|
body = build_create_body(member, club_id)
|
|
url = base_url |> String.trim_trailing("/") |> then(&"#{&1}/finance-contacts")
|
|
post_and_parse_contact(url, body, api_key)
|
|
end
|
|
end
|
|
|
|
defp post_and_parse_contact(url, body, api_key) do
|
|
# Req expects body to be iodata (e.g. string); a raw map causes ArgumentError.
|
|
encoded_body = Jason.encode!(body)
|
|
|
|
case Req.post(url,
|
|
body: encoded_body,
|
|
headers: headers(api_key),
|
|
receive_timeout: 15_000
|
|
) do
|
|
{:ok, %{status: 201, body: resp_body}} ->
|
|
case get_contact_id_from_response(resp_body) do
|
|
nil -> {:error, {:invalid_response, resp_body}}
|
|
id -> {:ok, id}
|
|
end
|
|
|
|
{:ok, %{status: status, body: resp_body}} ->
|
|
{:error, {:http, status, extract_error_message(resp_body)}}
|
|
|
|
{:error, reason} ->
|
|
{:error, {:request_failed, reason}}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates an existing finance contact in Vereinfacht.
|
|
|
|
Only sends attributes that are typically synced from the member (name, email,
|
|
address fields). Returns the same contact_id on success.
|
|
|
|
## Examples
|
|
|
|
iex> update_contact("242", member)
|
|
{:ok, "242"}
|
|
|
|
iex> update_contact("242", member)
|
|
{:error, {:http, 404, "Not Found"}}
|
|
"""
|
|
@spec update_contact(String.t(), struct()) :: {:ok, String.t()} | {:error, term()}
|
|
def update_contact(contact_id, member) when is_binary(contact_id) do
|
|
base_url = base_url()
|
|
api_key = api_key()
|
|
|
|
if is_nil(base_url) or is_nil(api_key) do
|
|
{:error, :not_configured}
|
|
else
|
|
body = build_update_body(contact_id, member)
|
|
encoded_body = Jason.encode!(body)
|
|
|
|
url =
|
|
base_url
|
|
|> String.trim_trailing("/")
|
|
|> then(&"#{&1}/finance-contacts/#{contact_id}")
|
|
|
|
case Req.patch(url,
|
|
body: encoded_body,
|
|
headers: headers(api_key),
|
|
receive_timeout: 15_000
|
|
) do
|
|
{:ok, %{status: 200, body: _resp_body}} ->
|
|
{:ok, contact_id}
|
|
|
|
{:ok, %{status: status, body: body}} ->
|
|
{:error, {:http, status, extract_error_message(body)}}
|
|
|
|
{:error, reason} ->
|
|
{:error, {:request_failed, reason}}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Fetches a single finance contact from Vereinfacht (GET /finance-contacts/:id).
|
|
|
|
Returns the full response body (decoded JSON) for debugging/display.
|
|
"""
|
|
@spec get_contact(String.t()) :: {:ok, map()} | {:error, term()}
|
|
def get_contact(contact_id) when is_binary(contact_id) do
|
|
base_url = base_url()
|
|
api_key = api_key()
|
|
|
|
if is_nil(base_url) or is_nil(api_key) do
|
|
{:error, :not_configured}
|
|
else
|
|
url =
|
|
base_url
|
|
|> String.trim_trailing("/")
|
|
|> then(&"#{&1}/finance-contacts/#{contact_id}")
|
|
|
|
case Req.get(url,
|
|
headers: headers(api_key),
|
|
receive_timeout: 15_000
|
|
) do
|
|
{:ok, %{status: 200, body: body}} when is_map(body) ->
|
|
{:ok, body}
|
|
|
|
{:ok, %{status: status, body: body}} ->
|
|
{:error, {:http, status, extract_error_message(body)}}
|
|
|
|
{:error, reason} ->
|
|
{:error, {:request_failed, reason}}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp base_url, do: Mv.Config.vereinfacht_api_url()
|
|
defp api_key, do: Mv.Config.vereinfacht_api_key()
|
|
defp club_id, do: Mv.Config.vereinfacht_club_id()
|
|
|
|
defp headers(api_key) do
|
|
[
|
|
{"Accept", @content_type},
|
|
{"Content-Type", @content_type},
|
|
{"Authorization", "Bearer #{api_key}"}
|
|
]
|
|
end
|
|
|
|
defp build_create_body(member, club_id) do
|
|
attributes = member_to_attributes(member)
|
|
|
|
%{
|
|
"data" => %{
|
|
"type" => "finance-contacts",
|
|
"attributes" => attributes,
|
|
"relationships" => %{
|
|
"club" => %{
|
|
"data" => %{"type" => "clubs", "id" => club_id}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
end
|
|
|
|
defp build_update_body(contact_id, member) do
|
|
attributes = member_to_attributes(member)
|
|
|
|
%{
|
|
"data" => %{
|
|
"type" => "finance-contacts",
|
|
"id" => contact_id,
|
|
"attributes" => attributes
|
|
}
|
|
}
|
|
end
|
|
|
|
defp member_to_attributes(member) do
|
|
address =
|
|
[member |> Map.get(:street), member |> Map.get(:house_number)]
|
|
|> Enum.reject(&is_nil/1)
|
|
|> Enum.map_join(" ", &to_string/1)
|
|
|> then(fn s -> if s == "", do: nil, else: s end)
|
|
|
|
%{}
|
|
|> put_attr("lastName", member |> Map.get(:last_name))
|
|
|> put_attr("firstName", member |> Map.get(:first_name))
|
|
|> put_attr("email", member |> Map.get(:email))
|
|
|> put_attr("address", address)
|
|
|> put_attr("zipCode", member |> Map.get(:postal_code))
|
|
|> put_attr("city", member |> Map.get(:city))
|
|
|> Map.put("contactType", "person")
|
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
|
|> Map.new()
|
|
end
|
|
|
|
defp put_attr(acc, _key, nil), do: acc
|
|
defp put_attr(acc, key, value), do: Map.put(acc, key, to_string(value))
|
|
|
|
defp get_contact_id_from_response(%{"data" => %{"id" => id}}) when is_binary(id), do: id
|
|
|
|
defp get_contact_id_from_response(%{"data" => %{"id" => id}}) when is_integer(id),
|
|
do: to_string(id)
|
|
|
|
defp get_contact_id_from_response(_), do: nil
|
|
|
|
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(other), do: inspect(other)
|
|
end
|