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). Only runs when relevant data changed: on create always; on update only when first_name, last_name, email, street, house_number, postal_code, or city changed, or when the member has no vereinfacht_contact_id yet (to avoid unnecessary API calls). """ use Ash.Resource.Change require Logger @synced_attributes [ :first_name, :last_name, :email, :street, :house_number, :postal_code, :city ] @impl true def change(changeset, _opts, _context) do if Mv.Config.vereinfacht_configured?() and sync_relevant?(changeset) do Ash.Changeset.after_transaction(changeset, &sync_after_transaction/2) else changeset end end defp sync_relevant?(changeset) do case changeset.action_type do :create -> true :update -> relevant_update?(changeset) _ -> false end end defp relevant_update?(changeset) do any_synced_attr_changed? = Enum.any?(@synced_attributes, &Ash.Changeset.changing_attribute?(changeset, &1)) record = changeset.data no_contact_id_yet? = record && blank_contact_id?(record.vereinfacht_contact_id) any_synced_attr_changed? or no_contact_id_yet? end defp blank_contact_id?(nil), do: true defp blank_contact_id?(""), do: true defp blank_contact_id?(s) when is_binary(s), do: String.trim(s) == "" defp blank_contact_id?(_), do: false # 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