Vereinfacht accounting software API closes #431 #432

Merged
moritz merged 31 commits from feature/vereinfacht_api into main 2026-02-23 21:18:46 +01:00
Showing only changes of commit 6c22d889a1 - Show all commits

View file

@ -163,7 +163,16 @@ defmodule Mv.Vereinfacht.Client do
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}} when is_binary(att_email) ->
%{"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
@ -191,16 +200,34 @@ defmodule Mv.Vereinfacht.Client do
"""
@spec get_contact(String.t()) :: {:ok, map()} | {:error, term()}
def get_contact(contact_id) when is_binary(contact_id) do
fetch_contact(contact_id, [])
end
@doc """
Fetches a finance contact with receipts (GET /finance-contacts/:id?include=receipts).
Returns {:ok, receipts} where receipts is a list of maps with :id and :attributes
(and optional :type) for each receipt, or {:error, reason}.
"""
@spec get_contact_with_receipts(String.t()) :: {:ok, [map()]} | {:error, term()}
def get_contact_with_receipts(contact_id) when is_binary(contact_id) do
case fetch_contact(contact_id, include: "receipts") do
{:ok, body} -> {:ok, extract_receipts_from_response(body)}
{:error, _} = err -> err
end
end
defp fetch_contact(contact_id, query_params) 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}")
path =
base_url |> String.trim_trailing("/") |> then(&"#{&1}/finance-contacts/#{contact_id}")
url = build_url_with_params(path, query_params)
case Req.get(url, [headers: headers(api_key)] ++ req_http_options()) do
{:ok, %{status: 200, body: body}} when is_map(body) ->
@ -215,6 +242,38 @@ defmodule Mv.Vereinfacht.Client do
end
end
defp build_url_with_params(base, []), do: base
defp build_url_with_params(base, include: value) do
sep = if String.contains?(base, "?"), do: "&", else: "?"
base <> sep <> "include=" <> URI.encode(value, &URI.char_unreserved?/1)
end
defp extract_receipts_from_response(%{"included" => included}) when is_list(included) do
included
|> Enum.filter(&match?(%{"type" => "receipts"}, &1))
|> Enum.map(fn %{"id" => id, "attributes" => attrs} = r ->
Map.merge(%{id: id, type: r["type"]}, string_keys_to_atoms(attrs || %{}))
end)
end
defp extract_receipts_from_response(_), do: []
defp string_keys_to_atoms(map) when is_map(map) do
Map.new(map, fn {k, v} -> {to_atom_key(k), v} 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 api_key, do: Mv.Config.vereinfacht_api_key()
defp club_id, do: Mv.Config.vereinfacht_club_id()
@ -270,6 +329,7 @@ defmodule Mv.Vereinfacht.Client do
|> put_attr("zipCode", member |> Map.get(:postal_code))
|> put_attr("city", member |> Map.get(:city))
|> Map.put("contactType", "person")
|> Map.put("isExternal", true)
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|> Map.new()
end