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
|
|
@ -117,6 +117,9 @@ defmodule Mv.Membership.Member do
|
|||
# Requires both join_date and membership_fee_type_id to be present
|
||||
change Mv.MembershipFees.Changes.SetMembershipFeeStartDate
|
||||
|
||||
# Sync member to Vereinfacht as finance contact (if configured)
|
||||
change Mv.Vereinfacht.Changes.SyncContact
|
||||
|
||||
# Trigger cycle generation after member creation
|
||||
# Only runs if membership_fee_type_id is set
|
||||
# Note: Cycle generation runs asynchronously to not block the action,
|
||||
|
|
@ -190,6 +193,9 @@ defmodule Mv.Membership.Member do
|
|||
where [changing(:membership_fee_type_id)]
|
||||
end
|
||||
|
||||
# Sync member to Vereinfacht as finance contact (if configured)
|
||||
change Mv.Vereinfacht.Changes.SyncContact
|
||||
|
||||
# Trigger cycle regeneration when membership_fee_type_id changes
|
||||
# This deletes future unpaid cycles and regenerates them with the new type/amount
|
||||
# Note: Cycle regeneration runs synchronously in the same transaction to ensure atomicity
|
||||
|
|
@ -243,6 +249,13 @@ defmodule Mv.Membership.Member do
|
|||
end)
|
||||
end
|
||||
|
||||
# Internal: set vereinfacht_contact_id after syncing with Vereinfacht API.
|
||||
# Not exposed via code interface; used only by Mv.Vereinfacht.Changes.SyncContact.
|
||||
update :set_vereinfacht_contact_id do
|
||||
require_atomic? false
|
||||
accept [:vereinfacht_contact_id]
|
||||
end
|
||||
|
||||
# Action to handle fuzzy search on specific fields
|
||||
read :search do
|
||||
argument :query, :string, allow_nil?: true
|
||||
|
|
@ -320,6 +333,12 @@ defmodule Mv.Membership.Member do
|
|||
authorize_if Mv.Authorization.Checks.HasPermission
|
||||
end
|
||||
|
||||
# Internal sync action: allow setting vereinfacht_contact_id (used only by SyncContact change).
|
||||
policy action(:set_vereinfacht_contact_id) do
|
||||
description "Allow internal sync to set Vereinfacht contact ID"
|
||||
authorize_if always()
|
||||
end
|
||||
|
||||
# CREATE/UPDATE: Forbid member–user link unless admin, then check permissions
|
||||
# ForbidMemberUserLinkUnlessAdmin: only admins may pass :user (link or unlink via nil/empty).
|
||||
# HasPermission: :own_data → update linked; :read_only → no update; :normal_user/admin → update all.
|
||||
|
|
@ -593,6 +612,14 @@ defmodule Mv.Membership.Member do
|
|||
public? true
|
||||
description "Date from which membership fees should be calculated"
|
||||
end
|
||||
|
||||
# Vereinfacht accounting software integration: ID of the finance contact synced via API.
|
||||
# Set by Mv.Vereinfacht.Changes.SyncContact; not accepted in create/update actions.
|
||||
attribute :vereinfacht_contact_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
description "ID of the finance contact in Vereinfacht (set by sync)"
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
|
|
@ -1275,7 +1302,10 @@ defmodule Mv.Membership.Member do
|
|||
|
||||
# Extracts custom field values from existing member data (update scenario)
|
||||
defp extract_existing_values(member_data, changeset) do
|
||||
actor = Map.get(changeset.context, :actor)
|
||||
actor =
|
||||
Map.get(changeset.context, :actor) ||
|
||||
Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
opts = Helpers.ash_actor_opts(actor)
|
||||
|
||||
case Ash.load(member_data, :custom_field_values, opts) do
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ defmodule Mv.Application do
|
|||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
Mv.Vereinfacht.SyncFlash.create_table!()
|
||||
|
||||
children = [
|
||||
MvWeb.Telemetry,
|
||||
Mv.Repo,
|
||||
|
|
|
|||
54
lib/mv/vereinfacht/changes/sync_contact.ex
Normal file
54
lib/mv/vereinfacht/changes/sync_contact.ex
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
defmodule Mv.Vereinfacht.Changes.SyncContact do
|
||||
@moduledoc """
|
||||
Syncs a member to Vereinfacht as a finance contact after create/update.
|
||||
|
||||
- If the member has no `vereinfacht_contact_id`, creates a contact via API and saves the ID.
|
||||
- If the member already has an ID, updates the contact via API.
|
||||
Runs in `after_transaction` so the member is persisted first. API failures are logged
|
||||
but do not block the member operation. Requires Vereinfacht to be configured
|
||||
(Mv.Config.vereinfacht_configured?/0).
|
||||
"""
|
||||
use Ash.Resource.Change
|
||||
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def change(changeset, _opts, _context) do
|
||||
if Mv.Config.vereinfacht_configured?() do
|
||||
Ash.Changeset.after_transaction(changeset, &sync_after_transaction/2)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
# Ash calls after_transaction with (changeset, result) only - 2 args.
|
||||
defp sync_after_transaction(_changeset, {:ok, member}) do
|
||||
case Mv.Vereinfacht.sync_member(member) do
|
||||
:ok ->
|
||||
Mv.Vereinfacht.SyncFlash.store(to_string(member.id), :ok, "Synced to Vereinfacht.")
|
||||
{:ok, member}
|
||||
|
||||
{:ok, member_updated} ->
|
||||
Mv.Vereinfacht.SyncFlash.store(
|
||||
to_string(member_updated.id),
|
||||
:ok,
|
||||
"Synced to Vereinfacht."
|
||||
)
|
||||
|
||||
{:ok, member_updated}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Vereinfacht sync failed for member #{member.id}: #{inspect(reason)}")
|
||||
|
||||
Mv.Vereinfacht.SyncFlash.store(
|
||||
to_string(member.id),
|
||||
:warning,
|
||||
Mv.Vereinfacht.format_error(reason)
|
||||
)
|
||||
|
||||
{:ok, member}
|
||||
end
|
||||
end
|
||||
|
||||
defp sync_after_transaction(_changeset, error), do: error
|
||||
end
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
defmodule Mv.Vereinfacht.Changes.SyncLinkedMemberAfterUserChange do
|
||||
@moduledoc """
|
||||
Syncs the linked Member to Vereinfacht after a User action that may have updated
|
||||
the member's email via Ecto (e.g. User email change → SyncUserEmailToMember).
|
||||
|
||||
Attach to any User action that uses SyncUserEmailToMember. After the transaction
|
||||
commits, if the user has a linked member and Vereinfacht is configured, syncs
|
||||
that member to the API. Failures are logged but do not affect the User result.
|
||||
"""
|
||||
use Ash.Resource.Change
|
||||
|
||||
require Logger
|
||||
alias Mv.Membership.Member
|
||||
alias Mv.Membership
|
||||
alias Mv.Helpers.SystemActor
|
||||
alias Mv.Helpers
|
||||
|
||||
@impl true
|
||||
def change(changeset, _opts, _context) do
|
||||
if Mv.Config.vereinfacht_configured?() do
|
||||
Ash.Changeset.after_transaction(changeset, &sync_linked_member_after_transaction/2)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp sync_linked_member_after_transaction(_changeset, {:ok, user}) do
|
||||
case load_linked_member(user) do
|
||||
nil ->
|
||||
{:ok, user}
|
||||
|
||||
member ->
|
||||
case Mv.Vereinfacht.sync_member(member) do
|
||||
:ok ->
|
||||
{:ok, user}
|
||||
|
||||
{:ok, _} ->
|
||||
{:ok, user}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(
|
||||
"Vereinfacht sync failed for member #{member.id} (linked to user #{user.id}): #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp sync_linked_member_after_transaction(_changeset, result), do: result
|
||||
|
||||
defp load_linked_member(%{member_id: nil}), do: nil
|
||||
defp load_linked_member(%{member_id: ""}), do: nil
|
||||
|
||||
defp load_linked_member(user) do
|
||||
actor = SystemActor.get_system_actor()
|
||||
opts = Helpers.ash_actor_opts(actor)
|
||||
|
||||
case Ash.get(Member, user.member_id, [domain: Membership] ++ opts) do
|
||||
{:ok, %Member{} = member} -> member
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
222
lib/mv/vereinfacht/client.ex
Normal file
222
lib/mv/vereinfacht/client.ex
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
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
|
||||
44
lib/mv/vereinfacht/sync_flash.ex
Normal file
44
lib/mv/vereinfacht/sync_flash.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
defmodule Mv.Vereinfacht.SyncFlash do
|
||||
@moduledoc """
|
||||
Short-lived store for Vereinfacht sync results so the UI can show them after save.
|
||||
|
||||
The SyncContact change runs in after_transaction and cannot access the LiveView
|
||||
socket. This module stores a message keyed by member_id; the form LiveView
|
||||
calls `take/1` after a successful save and displays the message in flash.
|
||||
"""
|
||||
@table :vereinfacht_sync_flash
|
||||
|
||||
@doc """
|
||||
Stores a sync result for the given member. Overwrites any previous message.
|
||||
|
||||
- `:ok` - Sync succeeded (optional user message).
|
||||
- `:warning` - Sync failed; message should be shown as a warning.
|
||||
"""
|
||||
@spec store(String.t(), :ok | :warning, String.t()) :: :ok
|
||||
def store(member_id, kind, message) when is_binary(member_id) do
|
||||
:ets.insert(@table, {member_id, {kind, message}})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Takes and removes the stored sync message for the given member.
|
||||
|
||||
Returns `{kind, message}` if present, otherwise `nil`.
|
||||
"""
|
||||
@spec take(String.t()) :: {:ok | :warning, String.t()} | nil
|
||||
def take(member_id) when is_binary(member_id) do
|
||||
case :ets.take(@table, member_id) do
|
||||
[{^member_id, value}] -> value
|
||||
[] -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def create_table! do
|
||||
if :ets.whereis(@table) == :undefined do
|
||||
:ets.new(@table, [:set, :public, :named_table])
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
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