From 96ca857e064d5f0476be40956954c31ec82cf3db Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 4 Mar 2026 19:22:27 +0100 Subject: [PATCH 1/4] Vereinfacht API: use filter for contact lookup, drop extra required fields - find_contact_by_email uses GET with filter[isExternal]=true and filter[email] - vereinfacht_required_member_fields is now empty (API accepts minimal payload) --- lib/mv/constants.ex | 8 ++-- lib/mv/vereinfacht/client.ex | 74 ++++++----------------------- test/mv/vereinfacht/client_test.exs | 7 +++ 3 files changed, 27 insertions(+), 62 deletions(-) diff --git a/lib/mv/constants.ex b/lib/mv/constants.ex index 3a01fa9..7bb6274 100644 --- a/lib/mv/constants.ex +++ b/lib/mv/constants.ex @@ -28,15 +28,17 @@ defmodule Mv.Constants do @email_validator_checks [:html_input, :pow] - # Member fields that are required when Vereinfacht integration is active (contact sync) - @vereinfacht_required_member_fields [:first_name, :last_name, :street, :postal_code, :city] + # No member fields are required solely for Vereinfacht; API accepts minimal payload + # (contactType + isExternal) when creating external contacts and supports filter by email for lookup. + @vereinfacht_required_member_fields [] def member_fields, do: @member_fields @doc """ Returns member fields that are always required when Vereinfacht integration is configured. - Used for validation, member form required indicators, and settings UI (checkbox disabled). + Currently empty: the Vereinfacht API only requires contactType (e.g. "person") when creating + external contacts; lookup uses filter[email] so no extra required fields in the app. """ def vereinfacht_required_member_fields, do: @vereinfacht_required_member_fields diff --git a/lib/mv/vereinfacht/client.ex b/lib/mv/vereinfacht/client.ex index 6ec8c8c..f41e962 100644 --- a/lib/mv/vereinfacht/client.ex +++ b/lib/mv/vereinfacht/client.ex @@ -166,12 +166,12 @@ defmodule Mv.Vereinfacht.Client do end @doc """ - Finds a finance contact by email (GET /finance-contacts, then match in response). + Finds a finance contact by email using the API filter. - The Vereinfacht API does not allow filter by email on this endpoint, so we - fetch the first page and find the contact client-side. Returns {:ok, contact_id} - if a contact with that email exists, {:error, :not_found} if none, or - {:error, reason} on API/network failure. Used before create for idempotency. + Uses GET /finance-contacts?filter[isExternal]=true&filter[email]=... so the API + returns only matching external contacts. Returns {:ok, contact_id} if a contact + exists, {:error, :not_found} if none, or {:error, reason} on API/network failure. + Used before create for idempotency. """ @spec find_contact_by_email(String.t()) :: {:ok, String.t()} | {:error, :not_found | term()} def find_contact_by_email(email) when is_binary(email) do @@ -182,25 +182,17 @@ defmodule Mv.Vereinfacht.Client do end end - @find_contact_page_size 100 - @find_contact_max_pages 100 - defp do_find_contact_by_email(email) do - normalized = String.trim(email) |> String.downcase() - do_find_contact_by_email_page(1, normalized) - end - - 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}" + encoded_email = URI.encode_www_form(email |> String.trim()) + url = "#{base}?filter[isExternal]=true&filter[email]=#{encoded_email}" case Req.get(url, [headers: headers(api_key())] ++ req_http_options()) do {:ok, %{status: 200, body: body}} when is_map(body) -> - handle_find_contact_page_response(body, page, normalized) + case get_first_contact_id_from_list(body) do + nil -> {:error, :not_found} + id -> {:ok, id} + end {:ok, %{status: status, body: body}} -> {:error, {:http, status, extract_error_message(body)}} @@ -210,48 +202,12 @@ defmodule Mv.Vereinfacht.Client do end end - defp handle_find_contact_page_response(body, page, normalized) do - case find_contact_id_by_email_in_list(body, normalized) do - id when is_binary(id) -> {:ok, id} - nil -> maybe_find_contact_next_page(body, page, normalized) - end + defp get_first_contact_id_from_list(%{"data" => [%{"id" => id} | _]}) do + normalize_contact_id(id) 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 - Enum.find_value(list, fn - %{"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 - nil - end - - %{"id" => _id, "attributes" => _} -> - nil - - _ -> - nil - end) - end - - defp find_contact_id_by_email_in_list(_, _), do: nil + defp get_first_contact_id_from_list(%{"data" => []}), do: nil + defp get_first_contact_id_from_list(_), do: nil defp normalize_contact_id(id) when is_binary(id), do: id defp normalize_contact_id(id) when is_integer(id), do: to_string(id) diff --git a/test/mv/vereinfacht/client_test.exs b/test/mv/vereinfacht/client_test.exs index d936adc..d326879 100644 --- a/test/mv/vereinfacht/client_test.exs +++ b/test/mv/vereinfacht/client_test.exs @@ -30,6 +30,13 @@ defmodule Mv.Vereinfacht.ClientTest do end end + describe "find_contact_by_email/1" do + test "returns {:error, :not_configured} when Vereinfacht is not configured" do + assert Client.find_contact_by_email("kayley.becker@example.com") == + {:error, :not_configured} + end + end + defp build_member_struct do %{ first_name: "Test", From 0ac39c646f42781bdfc143dd511a7fd59b3be40b Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 4 Mar 2026 19:22:41 +0100 Subject: [PATCH 2/4] Remove Vereinfacht-required logic from settings and member validation - Member field settings: required only from email + settings (no API override) - Member resource validation: required fields from settings only - Gettext: remove obsolete 'Required for Vereinfacht integration' string --- lib/membership/member.ex | 15 ++--- .../live/member_field_live/form_component.ex | 62 +++++-------------- .../live/member_field_live/index_component.ex | 8 +-- lib/mv_web/live/member_live/form.ex | 13 +--- priv/gettext/de/LC_MESSAGES/default.po | 5 -- priv/gettext/default.pot | 5 -- priv/gettext/en/LC_MESSAGES/default.po | 5 -- 7 files changed, 27 insertions(+), 86 deletions(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index be99b7f..4e85fa8 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -550,11 +550,9 @@ defmodule Mv.Membership.Member do end, where: [action_is([:create_member, :update_member])] - # Validate member fields that are marked as required in settings or by Vereinfacht. - # When settings cannot be loaded, we still enforce email + Vereinfacht-required fields. + # Validate member fields that are marked as required in settings. + # When settings cannot be loaded, enforce only email. validate fn changeset, _context -> - vereinfacht_required? = Mv.Config.vereinfacht_configured?() - required_fields = case Mv.Membership.get_settings() do {:ok, settings} -> @@ -562,20 +560,17 @@ defmodule Mv.Membership.Member do normalized = VisibilityConfig.normalize(required_config) Enum.filter(Mv.Constants.member_fields(), fn field -> - field == :email || - (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field)) || - Map.get(normalized, field, false) + field == :email || Map.get(normalized, field, false) end) {:error, reason} -> Logger.warning( "Member required-fields validation: could not load settings (#{inspect(reason)}). " <> - "Enforcing only email and Vereinfacht-required fields." + "Enforcing only email." ) Enum.filter(Mv.Constants.member_fields(), fn field -> - field == :email || - (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field)) + field == :email end) end diff --git a/lib/mv_web/live/member_field_live/form_component.ex b/lib/mv_web/live/member_field_live/form_component.ex index 5085b8b..84889e5 100644 --- a/lib/mv_web/live/member_field_live/form_component.ex +++ b/lib/mv_web/live/member_field_live/form_component.ex @@ -33,7 +33,6 @@ defmodule MvWeb.MemberFieldLive.FormComponent do assigns |> assign(:field_attributes, get_field_attributes(assigns.member_field)) |> assign(:is_email_field?, assigns.member_field == :email) - |> assign(:vereinfacht_required_field?, vereinfacht_required_field?(assigns)) |> assign(:field_label, MemberFields.label(assigns.member_field)) ~H""" @@ -120,22 +119,12 @@ defmodule MvWeb.MemberFieldLive.FormComponent do <%!-- Line break before Required / Show in overview block --%>
- <%!-- Required: disabled for email (always required) or Vereinfacht-required fields when integration is active --%> + <%!-- Required: disabled for email (always required); else configurable in settings --%>
<.input - :if={not @is_email_field? and not @vereinfacht_required_field?} + :if={not @is_email_field?} field={@form[:required]} type="checkbox" label={gettext("Required")} @@ -211,12 +200,11 @@ defmodule MvWeb.MemberFieldLive.FormComponent do end required = - socket.assigns.vereinfacht_required_field? || - if Map.has_key?(member_field_params, "required") do - TypeParsers.parse_boolean(member_field_params["required"]) - else - form.source["required"] - end + if Map.has_key?(member_field_params, "required") do + TypeParsers.parse_boolean(member_field_params["required"]) + else + form.source["required"] + end # Merge so we keep name/value_type and have current checkbox state; use as new form source merged_source = @@ -247,12 +235,11 @@ defmodule MvWeb.MemberFieldLive.FormComponent do end required = - socket.assigns.vereinfacht_required_field? || - if Map.has_key?(member_field_params, "required") do - TypeParsers.parse_boolean(member_field_params["required"]) - else - form.source["required"] - end + if Map.has_key?(member_field_params, "required") do + TypeParsers.parse_boolean(member_field_params["required"]) + else + form.source["required"] + end field_string = Atom.to_string(socket.assigns.member_field) @@ -292,20 +279,10 @@ defmodule MvWeb.MemberFieldLive.FormComponent do normalized_visibility = VisibilityConfig.normalize(visibility_config) normalized_required = VisibilityConfig.normalize(required_config) show_in_overview = Map.get(normalized_visibility, member_field, true) - vereinfacht_required? = Mv.Config.vereinfacht_configured?() - # Persist in socket so validate/save can enforce server-side without relying on render assigns - socket = - assign( - socket, - :vereinfacht_required_field?, - vereinfacht_required_field?(%{member_field: member_field}) - ) - # Email always required; Vereinfacht-required fields when integration active; else from settings + # Email always required; else from settings required = - member_field == :email || - (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(member_field)) || - Map.get(normalized_required, member_field, false) + member_field == :email || Map.get(normalized_required, member_field, false) form_data = %{ "name" => MemberFields.label(member_field), @@ -338,9 +315,4 @@ defmodule MvWeb.MemberFieldLive.FormComponent do defp format_error(error) do inspect(error) end - - defp vereinfacht_required_field?(assigns) do - Mv.Config.vereinfacht_configured?() && - Mv.Constants.vereinfacht_required_field?(assigns.member_field) - end end diff --git a/lib/mv_web/live/member_field_live/index_component.ex b/lib/mv_web/live/member_field_live/index_component.ex index 97dc9ff..2285d90 100644 --- a/lib/mv_web/live/member_field_live/index_component.ex +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -172,19 +172,15 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do member_fields = Mv.Constants.member_fields() visibility_config = settings.member_field_visibility || %{} required_config = settings.member_field_required || %{} - vereinfacht_required? = Mv.Config.vereinfacht_configured?() - normalized_visibility = VisibilityConfig.normalize(visibility_config) normalized_required = VisibilityConfig.normalize(required_config) Enum.map(member_fields, fn field -> show_in_overview = Map.get(normalized_visibility, field, true) - # Email always required; Vereinfacht-required fields when integration active; else from settings + # Email always required; else from settings required = - field == :email || - (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field)) || - Map.get(normalized_required, field, false) + field == :email || Map.get(normalized_required, field, false) attribute = Info.attribute(Mv.Membership.Member, field) diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index abb29e3..6d187fa 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -398,8 +398,6 @@ defmodule MvWeb.MemberLive.Form do end defp get_member_field_required_map do - vereinfacht_required? = Mv.Config.vereinfacht_configured?() - case Membership.get_settings() do {:ok, settings} -> required_config = settings.member_field_required || %{} @@ -407,20 +405,15 @@ defmodule MvWeb.MemberLive.Form do Mv.Constants.member_fields() |> Enum.map(fn field -> - required = - field == :email || - (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field)) || - Map.get(normalized, field, false) - + required = field == :email || Map.get(normalized, field, false) {field, required} end) |> Map.new() {:error, _} -> - # Email always required; Vereinfacht fields when integration active + # When settings cannot be loaded, only email is required Map.new(Mv.Constants.member_fields(), fn f -> - {f, - f == :email || (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(f))} + {f, f == :email} end) end end diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 904386a..982798d 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -2820,11 +2820,6 @@ msgstr "Okt." msgid "Sep." msgstr "Sep." -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Required for Vereinfacht integration and cannot be disabled." -msgstr "Für die Vereinfacht-Integration erforderlich und kann nicht deaktiviert werden." - #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 8f66f55..5b6ef4c 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -2820,11 +2820,6 @@ msgstr "" msgid "Sep." msgstr "" -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Required for Vereinfacht integration and cannot be disabled." -msgstr "" - #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index b8186ff..a566be0 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2820,11 +2820,6 @@ msgstr "" msgid "Sep." msgstr "" -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Required for Vereinfacht integration and cannot be disabled." -msgstr "Required for Vereinfacht integration and cannot be disabled." - #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format From fbc3fc2a4d73e80392f25908ae80d7e48fc2e551 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 4 Mar 2026 19:22:45 +0100 Subject: [PATCH 3/4] Docs: Vereinfacht API integration and guidelines - CODE_GUIDELINES: add vereinfacht/ to project structure, required-fields note, link to vereinfacht-api - docs/vereinfacht-api.md: filter API, minimal create payload, no extra required fields - feature-roadmap: member-contact sync implemented, link to doc --- CODE_GUIDELINES.md | 10 +++++++-- docs/feature-roadmap.md | 2 +- docs/vereinfacht-api.md | 47 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 docs/vereinfacht-api.md diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md index bb127f1..c3de14b 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -62,6 +62,7 @@ We are building a membership management system (Mila) using the following techno **Related documents:** - **UI / UX:** [`DESIGN_DUIDELINES.md`](../DESIGN_DUIDELINES.md) defines visual and interaction consistency: use of CoreComponents (no raw DaisyUI in views), page skeleton (`<.header>`, `mt-6 space-y-6`), **Back button left in header for edit/new forms** (§2.2), typography, buttons, forms, tables, flash/toast, and microcopy (e.g. German "du" and glossary). Follow "components first" and semantic variants instead of hard-coded colors. +- **Vereinfacht API:** [`docs/vereinfacht-api.md`](docs/vereinfacht-api.md) describes the finance-contact sync (find by email filter, minimal create payload, no extra required member fields). --- @@ -115,8 +116,13 @@ lib/ │ ├── membership_fees/ # Membership fee business logic │ │ ├── cycle_generator.ex # Cycle generation algorithm │ │ └── calendar_cycles.ex # Calendar cycle calculations +│ ├── vereinfacht/ # Vereinfacht accounting API integration +│ │ ├── client.ex # HTTP client (finance-contacts: create, update, find by email) +│ │ ├── vereinfacht.ex # Business logic (sync_member, sync_members_without_contact) +│ │ ├── sync_flash.ex # Flash message helpers for sync results +│ │ └── changes/ # Ash changes (SyncContact, sync linked member) │ ├── helpers.ex # Shared helper functions (ash_actor_opts) -│ ├── constants.ex # Application constants (member_fields, custom_field_prefix) +│ ├── constants.ex # Application constants (member_fields, custom_field_prefix, vereinfacht_required_member_fields) │ ├── application.ex # OTP application │ ├── mailer.ex # Email mailer │ ├── release.ex # Release tasks @@ -2874,7 +2880,7 @@ Building accessible applications ensures that all users, including those with di **Required Fields:** -Which member fields are required (asterisk, tooltip, validation) is configured in **Settings** (Memberdata section: edit a member field and set "Required"). The member create/edit form and Member resource validation both read `settings.member_field_required`. Email is always required; other fields default to optional. +Which member fields are required (asterisk, tooltip, validation) is configured in **Settings** (Memberdata section: edit a member field and set "Required"). The member create/edit form and Member resource validation both read `settings.member_field_required`. Email is always required; other fields default to optional. The Vereinfacht integration does not add extra required member fields (the external API accepts a minimal payload when creating contacts and supports filter-by-email for lookup). ```heex diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md index 23f19b7..89c2f39 100644 --- a/docs/feature-roadmap.md +++ b/docs/feature-roadmap.md @@ -247,7 +247,7 @@ - ❌ Payment records/transactions (external payment tracking) - ❌ Payment reminders - ❌ Invoice generation -- ❌ vereinfacht.digital API integration +- ✅ Member–finance-contact sync with vereinfacht.digital API (see `docs/vereinfacht-api.md`); ❌ transaction import / full API integration - ❌ SEPA direct debit support - ❌ Payment reports diff --git a/docs/vereinfacht-api.md b/docs/vereinfacht-api.md new file mode 100644 index 0000000..e0480f4 --- /dev/null +++ b/docs/vereinfacht-api.md @@ -0,0 +1,47 @@ +# Vereinfacht API Integration + +This document describes the current integration with the Vereinfacht (verein.visuel.dev) accounting API for syncing members as finance contacts. + +## Overview + +- **Purpose:** Create and update external finance contacts in Vereinfacht when members are created or updated; support bulk sync for members without a contact ID. +- **Configuration:** ENV or Settings: `VEREINFACHT_API_URL`, `VEREINFACHT_API_KEY`, `VEREINFACHT_CLUB_ID`, optional `VEREINFACHT_APP_URL` for contact view links. +- **Modules:** `Mv.Vereinfacht` (business logic), `Mv.Vereinfacht.Client` (HTTP client), `Mv.Vereinfacht.Changes.SyncContact` (Ash after_transaction change). + +## API Usage + +### Finding an existing contact by email + +The API supports filtered list requests. Use a single GET instead of paginating: + +- **Endpoint:** `GET /api/v1/finance-contacts?filter[isExternal]=true&filter[email]=` +- **Client:** `Mv.Vereinfacht.Client.find_contact_by_email/1` builds this URL (with encoded email) and returns `{:ok, contact_id}` if the first match exists, `{:error, :not_found}` otherwise. +- No member fields are required in the app solely for this lookup. + +### Creating a contact + +When creating an external finance contact, the API only requires: + +- **Attributes:** `contactType` (e.g. `"person"`), `isExternal: true` +- **Relationship:** `club` (club ID from config) + +Additional attributes (firstName, lastName, email, address, zipCode, city) are optional and are sent when present on the member so the contact is filled in. The app does **not** enforce extra required member fields for Vereinfacht; only Settings-based required fields and email apply. + +- **Client:** `Mv.Vereinfacht.Client.create_contact/1` builds the JSON:API body from the member; `Mv.Constants.vereinfacht_required_member_fields/0` is an empty list. + +### Updating a contact + +- **Endpoint:** `PATCH /api/v1/finance-contacts/:id` +- **Client:** `Mv.Vereinfacht.Client.update_contact/2` sends current member attributes. The API may still validate presence/format of fields on update. + +## Flow + +1. **Member create/update:** `SyncContact` runs after the transaction. If the member has no `vereinfacht_contact_id`, the client tries `find_contact_by_email(email)`; if found, it updates that contact and stores the ID on the member; otherwise it creates a contact and stores the new ID. If the member already has a contact ID, the client updates the contact. +2. **Bulk sync:** “Sync all members without Vereinfacht contact” calls `Vereinfacht.sync_members_without_contact/0`, which loads members with nil/blank `vereinfacht_contact_id` and runs the same create/update flow per member. + +## References + +- **Config:** `Mv.Config` (`vereinfacht_api_url`, `vereinfacht_api_key`, `vereinfacht_club_id`, `vereinfacht_app_url`, `vereinfacht_configured?/0`). +- **Constants:** `Mv.Constants.vereinfacht_required_member_fields/0` (empty), `vereinfacht_required_field?/1`. +- **Tests:** `test/mv/vereinfacht/`, `test/mv/config_vereinfacht_test.exs`; see `test/mv/vereinfacht/vereinfacht_test_README.md` for scope. +- **Roadmap:** Payment/transaction import and deeper integration are tracked in `docs/feature-roadmap.md` and `docs/membership-fee-architecture.md`. From 9f169b9835aa0342140f13edbf84e51dc859ab9e Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 4 Mar 2026 19:37:19 +0100 Subject: [PATCH 4/4] Vereinfacht: sync country with finance contact API --- docs/vereinfacht-api.md | 2 +- lib/mv/vereinfacht/changes/sync_contact.ex | 5 +++-- lib/mv/vereinfacht/client.ex | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/vereinfacht-api.md b/docs/vereinfacht-api.md index e0480f4..e50274f 100644 --- a/docs/vereinfacht-api.md +++ b/docs/vereinfacht-api.md @@ -25,7 +25,7 @@ When creating an external finance contact, the API only requires: - **Attributes:** `contactType` (e.g. `"person"`), `isExternal: true` - **Relationship:** `club` (club ID from config) -Additional attributes (firstName, lastName, email, address, zipCode, city) are optional and are sent when present on the member so the contact is filled in. The app does **not** enforce extra required member fields for Vereinfacht; only Settings-based required fields and email apply. +Additional attributes (firstName, lastName, email, address, zipCode, city, country) are optional and are sent when present on the member so the contact is filled in. The app does **not** enforce extra required member fields for Vereinfacht; only Settings-based required fields and email apply. - **Client:** `Mv.Vereinfacht.Client.create_contact/1` builds the JSON:API body from the member; `Mv.Constants.vereinfacht_required_member_fields/0` is an empty list. diff --git a/lib/mv/vereinfacht/changes/sync_contact.ex b/lib/mv/vereinfacht/changes/sync_contact.ex index f3679d4..014557b 100644 --- a/lib/mv/vereinfacht/changes/sync_contact.ex +++ b/lib/mv/vereinfacht/changes/sync_contact.ex @@ -9,7 +9,7 @@ defmodule Mv.Vereinfacht.Changes.SyncContact do (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, + first_name, last_name, email, street, house_number, postal_code, city, or country changed, or when the member has no vereinfacht_contact_id yet (to avoid unnecessary API calls). """ use Ash.Resource.Change @@ -26,7 +26,8 @@ defmodule Mv.Vereinfacht.Changes.SyncContact do :street, :house_number, :postal_code, - :city + :city, + :country ] @impl true diff --git a/lib/mv/vereinfacht/client.ex b/lib/mv/vereinfacht/client.ex index f41e962..3ed70b8 100644 --- a/lib/mv/vereinfacht/client.ex +++ b/lib/mv/vereinfacht/client.ex @@ -345,6 +345,7 @@ defmodule Mv.Vereinfacht.Client do |> put_attr("address", address) |> put_attr("zipCode", member |> Map.get(:postal_code)) |> put_attr("city", member |> Map.get(:city)) + |> put_attr("country", member |> Map.get(:country)) |> Map.put("contactType", "person") |> Map.put("isExternal", true) |> Enum.reject(fn {_k, v} -> is_nil(v) end)