Vereinfacht accounting software API closes #431 #432
1 changed files with 37 additions and 24 deletions
|
|
@ -134,15 +134,25 @@ 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
|
||||||
url =
|
normalized = String.trim(email) |> String.downcase()
|
||||||
base_url()
|
do_find_contact_by_email_page(1, normalized)
|
||||||
|> String.trim_trailing("/")
|
end
|
||||||
|> then(&"#{&1}/finance-contacts")
|
|
||||||
|
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}"
|
||||||
|
|
||||||
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) ->
|
||||||
parse_find_by_email_response(body, email)
|
handle_find_contact_page_response(body, page, normalized)
|
||||||
|
|
||||||
{:ok, %{status: status, body: body}} ->
|
{:ok, %{status: status, body: body}} ->
|
||||||
{:error, {:http, status, extract_error_message(body)}}
|
{:error, {:http, status, extract_error_message(body)}}
|
||||||
|
|
@ -152,15 +162,21 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_find_by_email_response(body, email) do
|
defp handle_find_contact_page_response(body, page, normalized) do
|
||||||
normalized = String.trim(email) |> String.downcase()
|
|
||||||
|
|
||||||
case find_contact_id_by_email_in_list(body, normalized) do
|
case find_contact_id_by_email_in_list(body, normalized) do
|
||||||
nil -> {:error, :not_found}
|
id when is_binary(id) -> {:ok, id}
|
||||||
id -> {:ok, id}
|
nil -> maybe_find_contact_next_page(body, page, normalized)
|
||||||
end
|
end
|
||||||
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
|
defp find_contact_id_by_email_in_list(%{"data" => list}, normalized) when is_list(list) do
|
||||||
Enum.find_value(list, fn
|
Enum.find_value(list, fn
|
||||||
%{"id" => id, "attributes" => %{"email" => att_email, "isExternal" => true}}
|
%{"id" => id, "attributes" => %{"email" => att_email, "isExternal" => true}}
|
||||||
|
|
@ -249,31 +265,28 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
base <> sep <> "include=" <> URI.encode(value, &URI.char_unreserved?/1)
|
base <> sep <> "include=" <> URI.encode(value, &URI.char_unreserved?/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Allowlist of receipt attribute keys we expose (avoids String.to_atom on arbitrary API input / DoS).
|
||||||
|
@receipt_attr_allowlist ~w[amount bookingDate createdAt receiptType referenceNumber status updatedAt]a
|
||||||
|
|
||||||
defp extract_receipts_from_response(%{"included" => included}) when is_list(included) do
|
defp extract_receipts_from_response(%{"included" => included}) when is_list(included) do
|
||||||
included
|
included
|
||||||
|> Enum.filter(&match?(%{"type" => "receipts"}, &1))
|
|> Enum.filter(&match?(%{"type" => "receipts"}, &1))
|
||||||
|> Enum.map(fn %{"id" => id, "attributes" => attrs} = r ->
|
|> Enum.map(fn %{"id" => id, "attributes" => attrs} = r ->
|
||||||
Map.merge(%{id: id, type: r["type"]}, string_keys_to_atoms(attrs || %{}))
|
Map.merge(%{id: id, type: r["type"]}, receipt_attrs_allowlist(attrs || %{}))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp extract_receipts_from_response(_), do: []
|
defp extract_receipts_from_response(_), do: []
|
||||||
|
|
||||||
defp string_keys_to_atoms(map) when is_map(map) do
|
defp receipt_attrs_allowlist(attrs) when is_map(attrs) do
|
||||||
Map.new(map, fn {k, v} -> {to_atom_key(k), v} end)
|
Map.new(@receipt_attr_allowlist, fn key ->
|
||||||
|
str_key = to_string(key)
|
||||||
|
{key, Map.get(attrs, str_key)}
|
||||||
|
end)
|
||||||
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||||
|
|> Map.new()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp to_atom_key(k) when is_binary(k) do
|
|
||||||
try do
|
|
||||||
String.to_existing_atom(k)
|
|
||||||
rescue
|
|
||||||
ArgumentError -> String.to_atom(k)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_atom_key(k) when is_atom(k), do: k
|
|
||||||
defp to_atom_key(k), do: to_atom_key(to_string(k))
|
|
||||||
|
|
||||||
defp base_url, do: Mv.Config.vereinfacht_api_url()
|
defp base_url, do: Mv.Config.vereinfacht_api_url()
|
||||||
defp api_key, do: Mv.Config.vereinfacht_api_key()
|
defp api_key, do: Mv.Config.vereinfacht_api_key()
|
||||||
defp club_id, do: Mv.Config.vereinfacht_club_id()
|
defp club_id, do: Mv.Config.vereinfacht_club_id()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue