feat(vereinfacht): add client, sync flash and SyncContact change
- 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
This commit is contained in:
parent
a5a4d66655
commit
a008cf381a
7 changed files with 551 additions and 1 deletions
134
lib/mv/vereinfacht/vereinfacht.ex
Normal file
134
lib/mv/vereinfacht/vereinfacht.ex
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
defmodule Mv.Vereinfacht do
|
||||
@moduledoc """
|
||||
Business logic for Vereinfacht accounting software integration.
|
||||
|
||||
- `sync_member/1` – Sync a single member to the API (create or update contact).
|
||||
Used by Member create/update (SyncContact) and by User actions that update
|
||||
the linked member's email via Ecto (e.g. user email change).
|
||||
- `sync_members_without_contact/0` – Bulk sync of members without a contact ID.
|
||||
"""
|
||||
require Ash.Query
|
||||
alias Mv.Vereinfacht.Client
|
||||
alias Mv.Membership.Member
|
||||
alias Mv.Helpers.SystemActor
|
||||
alias Mv.Helpers
|
||||
|
||||
@doc """
|
||||
Syncs a single member to Vereinfacht (create or update finance contact).
|
||||
|
||||
If the member has no `vereinfacht_contact_id`, creates a contact and updates
|
||||
the member with the new ID. If they already have an ID, updates the contact.
|
||||
Uses system actor for any Ash update. Does nothing if Vereinfacht is not configured.
|
||||
|
||||
Returns:
|
||||
- `:ok` – Contact was updated.
|
||||
- `{:ok, member}` – Contact was created and member was updated with the new ID.
|
||||
- `{:error, reason}` – API or update failed.
|
||||
"""
|
||||
@spec sync_member(struct()) :: :ok | {:ok, struct()} | {:error, term()}
|
||||
def sync_member(member) do
|
||||
if Mv.Config.vereinfacht_configured?() do
|
||||
do_sync_member(member)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp do_sync_member(member) do
|
||||
if present_contact_id?(member.vereinfacht_contact_id) do
|
||||
case Client.update_contact(member.vereinfacht_contact_id, member) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
else
|
||||
case Client.create_contact(member) do
|
||||
{:ok, contact_id} ->
|
||||
save_contact_id(member, contact_id)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp save_contact_id(member, contact_id) do
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
opts = Helpers.ash_actor_opts(system_actor)
|
||||
|
||||
case Ash.update(member, %{vereinfacht_contact_id: contact_id}, [
|
||||
{:action, :set_vereinfacht_contact_id} | opts
|
||||
]) do
|
||||
{:ok, updated} -> {:ok, updated}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp present_contact_id?(nil), do: false
|
||||
defp present_contact_id?(""), do: false
|
||||
defp present_contact_id?(s) when is_binary(s), do: String.trim(s) != ""
|
||||
defp present_contact_id?(_), do: false
|
||||
|
||||
@doc """
|
||||
Formats an API/request error reason into a short user-facing message.
|
||||
|
||||
Used by SyncContact (flash) and GlobalSettingsLive (sync result list).
|
||||
"""
|
||||
@spec format_error(term()) :: String.t()
|
||||
def format_error({:http, _status, detail}) when is_binary(detail), do: "Vereinfacht: " <> detail
|
||||
def format_error({:http, status, _}), do: "Vereinfacht: API error (HTTP #{status})."
|
||||
|
||||
def format_error({:request_failed, _}),
|
||||
do: "Vereinfacht: Request failed (e.g. connection error)."
|
||||
|
||||
def format_error({:invalid_response, _}), do: "Vereinfacht: Invalid API response."
|
||||
def format_error(other), do: "Vereinfacht: " <> inspect(other)
|
||||
|
||||
@doc """
|
||||
Creates Vereinfacht contacts for all members that do not yet have a
|
||||
`vereinfacht_contact_id`. Uses system actor for reads and updates.
|
||||
|
||||
Returns `{:ok, %{synced: count, errors: list}}` where errors is a list of
|
||||
`{member_id, reason}`. Does nothing if Vereinfacht is not configured.
|
||||
"""
|
||||
@spec sync_members_without_contact() ::
|
||||
{:ok, %{synced: non_neg_integer(), errors: [{String.t(), term()}]}}
|
||||
| {:error, :not_configured}
|
||||
def sync_members_without_contact do
|
||||
if Mv.Config.vereinfacht_configured?() do
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
opts = Helpers.ash_actor_opts(system_actor)
|
||||
|
||||
query =
|
||||
Member
|
||||
|> Ash.Query.filter(is_nil(vereinfacht_contact_id))
|
||||
|
||||
case Ash.read(query, opts) do
|
||||
{:ok, members} ->
|
||||
do_sync_members(members, opts)
|
||||
|
||||
{:error, _} = err ->
|
||||
err
|
||||
end
|
||||
else
|
||||
{:error, :not_configured}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_sync_members(members, opts) do
|
||||
{synced, errors} =
|
||||
Enum.reduce(members, {0, []}, fn member, {acc_synced, acc_errors} ->
|
||||
{inc, new_errors} = sync_one_member(member, opts)
|
||||
{acc_synced + inc, acc_errors ++ new_errors}
|
||||
end)
|
||||
|
||||
{:ok, %{synced: synced, errors: errors}}
|
||||
end
|
||||
|
||||
defp sync_one_member(member, _opts) do
|
||||
case sync_member(member) do
|
||||
:ok -> {1, []}
|
||||
{:ok, _} -> {1, []}
|
||||
{:error, reason} -> {0, [{member.id, reason}]}
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue