Compare commits
4 commits
9f169b9835
...
e879958e53
| Author | SHA1 | Date | |
|---|---|---|---|
| e879958e53 | |||
| face48531f | |||
| e899cc738f | |||
| dc2cff8ec4 |
14 changed files with 114 additions and 153 deletions
|
|
@ -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
|
||||
<!-- Mark required fields (value from settings or always true for email) -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
47
docs/vereinfacht-api.md
Normal file
47
docs/vereinfacht-api.md
Normal file
|
|
@ -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]=<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, 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.
|
||||
|
||||
### 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`.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -389,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)
|
||||
|
|
|
|||
|
|
@ -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 --%>
|
||||
<div class="mt-4">
|
||||
<%!-- Required: disabled for email (always required) or Vereinfacht-required fields when integration is active --%>
|
||||
<%!-- Required: disabled for email (always required); else configurable in settings --%>
|
||||
<div
|
||||
:if={@is_email_field? or @vereinfacht_required_field?}
|
||||
:if={@is_email_field?}
|
||||
class="tooltip tooltip-right"
|
||||
data-tip={
|
||||
if(@is_email_field?,
|
||||
do: gettext("This is a technical field and cannot be changed"),
|
||||
else: gettext("Required for Vereinfacht integration and cannot be disabled.")
|
||||
)
|
||||
}
|
||||
aria-label={
|
||||
if(@is_email_field?,
|
||||
do: gettext("This is a technical field and cannot be changed"),
|
||||
else: gettext("Required for Vereinfacht integration and cannot be disabled.")
|
||||
)
|
||||
}
|
||||
data-tip={gettext("This is a technical field and cannot be changed")}
|
||||
aria-label={gettext("This is a technical field and cannot be changed")}
|
||||
>
|
||||
<fieldset class="mb-2 fieldset">
|
||||
<label>
|
||||
|
|
@ -164,7 +153,7 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
|||
</fieldset>
|
||||
</div>
|
||||
<.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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue