Vereinfacht API: filter-based contact lookup, no extra required fields, country sync, and docs #459
16 changed files with 208 additions and 154 deletions
|
|
@ -62,6 +62,7 @@ We are building a membership management system (Mila) using the following techno
|
||||||
|
|
||||||
**Related documents:**
|
**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.
|
- **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
|
│ ├── membership_fees/ # Membership fee business logic
|
||||||
│ │ ├── cycle_generator.ex # Cycle generation algorithm
|
│ │ ├── cycle_generator.ex # Cycle generation algorithm
|
||||||
│ │ └── calendar_cycles.ex # Calendar cycle calculations
|
│ │ └── 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)
|
│ ├── 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
|
│ ├── application.ex # OTP application
|
||||||
│ ├── mailer.ex # Email mailer
|
│ ├── mailer.ex # Email mailer
|
||||||
│ ├── release.ex # Release tasks
|
│ ├── release.ex # Release tasks
|
||||||
|
|
@ -2874,7 +2880,7 @@ Building accessible applications ensures that all users, including those with di
|
||||||
|
|
||||||
**Required Fields:**
|
**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
|
```heex
|
||||||
<!-- Mark required fields (value from settings or always true for email) -->
|
<!-- Mark required fields (value from settings or always true for email) -->
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@
|
||||||
- ❌ Payment records/transactions (external payment tracking)
|
- ❌ Payment records/transactions (external payment tracking)
|
||||||
- ❌ Payment reminders
|
- ❌ Payment reminders
|
||||||
- ❌ Invoice generation
|
- ❌ 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
|
- ❌ SEPA direct debit support
|
||||||
- ❌ Payment reports
|
- ❌ 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` (legacy; currently unused in UI or validation).
|
||||||
|
- **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,
|
end,
|
||||||
where: [action_is([:create_member, :update_member])]
|
where: [action_is([:create_member, :update_member])]
|
||||||
|
|
||||||
# Validate member fields that are marked as required in settings or by Vereinfacht.
|
# Validate member fields that are marked as required in settings.
|
||||||
# When settings cannot be loaded, we still enforce email + Vereinfacht-required fields.
|
# When settings cannot be loaded, enforce only email.
|
||||||
validate fn changeset, _context ->
|
validate fn changeset, _context ->
|
||||||
vereinfacht_required? = Mv.Config.vereinfacht_configured?()
|
|
||||||
|
|
||||||
required_fields =
|
required_fields =
|
||||||
case Mv.Membership.get_settings() do
|
case Mv.Membership.get_settings() do
|
||||||
{:ok, settings} ->
|
{:ok, settings} ->
|
||||||
|
|
@ -562,20 +560,17 @@ defmodule Mv.Membership.Member do
|
||||||
normalized = VisibilityConfig.normalize(required_config)
|
normalized = VisibilityConfig.normalize(required_config)
|
||||||
|
|
||||||
Enum.filter(Mv.Constants.member_fields(), fn field ->
|
Enum.filter(Mv.Constants.member_fields(), fn field ->
|
||||||
field == :email ||
|
field == :email || Map.get(normalized, field, false)
|
||||||
(vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field)) ||
|
|
||||||
Map.get(normalized, field, false)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
Logger.warning(
|
Logger.warning(
|
||||||
"Member required-fields validation: could not load settings (#{inspect(reason)}). " <>
|
"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 ->
|
Enum.filter(Mv.Constants.member_fields(), fn field ->
|
||||||
field == :email ||
|
field == :email
|
||||||
(vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field))
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,17 @@ defmodule Mv.Constants do
|
||||||
|
|
||||||
@email_validator_checks [:html_input, :pow]
|
@email_validator_checks [:html_input, :pow]
|
||||||
|
|
||||||
# Member fields that are required when Vereinfacht integration is active (contact sync)
|
# No member fields are required solely for Vereinfacht; API accepts minimal payload
|
||||||
@vereinfacht_required_member_fields [:first_name, :last_name, :street, :postal_code, :city]
|
# (contactType + isExternal) when creating external contacts and supports filter by email for lookup.
|
||||||
|
@vereinfacht_required_member_fields []
|
||||||
|
|
||||||
def member_fields, do: @member_fields
|
def member_fields, do: @member_fields
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns member fields that are always required when Vereinfacht integration is configured.
|
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
|
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).
|
(Mv.Config.vereinfacht_configured?/0).
|
||||||
|
|
||||||
Only runs when relevant data changed: on create always; on update only when
|
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).
|
or when the member has no vereinfacht_contact_id yet (to avoid unnecessary API calls).
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Change
|
use Ash.Resource.Change
|
||||||
|
|
@ -26,7 +26,8 @@ defmodule Mv.Vereinfacht.Changes.SyncContact do
|
||||||
:street,
|
:street,
|
||||||
:house_number,
|
:house_number,
|
||||||
:postal_code,
|
:postal_code,
|
||||||
:city
|
:city,
|
||||||
|
:country
|
||||||
]
|
]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -166,12 +166,12 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@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
|
Uses GET /finance-contacts?filter[isExternal]=true&filter[email]=... so the API
|
||||||
fetch the first page and find the contact client-side. Returns {:ok, contact_id}
|
returns only matching external contacts. Returns {:ok, contact_id} if a contact
|
||||||
if a contact with that email exists, {:error, :not_found} if none, or
|
exists, {:error, :not_found} if none, or {:error, reason} on API/network failure.
|
||||||
{:error, reason} on API/network failure. Used before create for idempotency.
|
Used before create for idempotency.
|
||||||
"""
|
"""
|
||||||
@spec find_contact_by_email(String.t()) :: {:ok, String.t()} | {:error, :not_found | term()}
|
@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
|
def find_contact_by_email(email) when is_binary(email) do
|
||||||
|
|
@ -182,25 +182,18 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@find_contact_page_size 100
|
|
||||||
@find_contact_max_pages 100
|
|
||||||
|
|
||||||
defp do_find_contact_by_email(email) do
|
defp do_find_contact_by_email(email) do
|
||||||
normalized = String.trim(email) |> String.downcase()
|
normalized_email = email |> String.trim() |> 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")
|
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(normalized_email)
|
||||||
|
url = "#{base}?filter[isExternal]=true&filter[email]=#{encoded_email}"
|
||||||
|
|
||||||
case Req.get(url, [headers: headers(api_key())] ++ req_http_options()) do
|
case Req.get(url, [headers: headers(api_key())] ++ req_http_options()) do
|
||||||
{:ok, %{status: 200, body: body}} when is_map(body) ->
|
{: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}} ->
|
{:ok, %{status: status, body: body}} ->
|
||||||
{:error, {:http, status, extract_error_message(body)}}
|
{:error, {:http, status, extract_error_message(body)}}
|
||||||
|
|
@ -210,48 +203,20 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_find_contact_page_response(body, page, normalized) do
|
defp get_first_contact_id_from_list(%{"data" => data} = _body) when is_list(data) do
|
||||||
case find_contact_id_by_email_in_list(body, normalized) do
|
if length(data) > 1 do
|
||||||
id when is_binary(id) -> {:ok, id}
|
Logger.warning(
|
||||||
nil -> maybe_find_contact_next_page(body, page, normalized)
|
"Vereinfacht find_contact_by_email: API returned multiple contacts for same email (count: #{length(data)}), using first. Check for duplicate or inconsistent data."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
case data do
|
||||||
|
[%{"id" => id} | _] -> normalize_contact_id(id)
|
||||||
|
[] -> nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_find_contact_next_page(body, page, normalized) do
|
defp get_first_contact_id_from_list(_), do: nil
|
||||||
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 normalize_contact_id(id) when is_binary(id), do: id
|
defp normalize_contact_id(id) when is_binary(id), do: id
|
||||||
defp normalize_contact_id(id) when is_integer(id), do: to_string(id)
|
defp normalize_contact_id(id) when is_integer(id), do: to_string(id)
|
||||||
|
|
@ -389,6 +354,7 @@ defmodule Mv.Vereinfacht.Client do
|
||||||
|> put_attr("address", address)
|
|> put_attr("address", address)
|
||||||
|> put_attr("zipCode", member |> Map.get(:postal_code))
|
|> put_attr("zipCode", member |> Map.get(:postal_code))
|
||||||
|> put_attr("city", member |> Map.get(:city))
|
|> put_attr("city", member |> Map.get(:city))
|
||||||
|
|> put_attr("country", member |> Map.get(:country))
|
||||||
|> Map.put("contactType", "person")
|
|> Map.put("contactType", "person")
|
||||||
|> Map.put("isExternal", true)
|
|> Map.put("isExternal", true)
|
||||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
||||||
assigns
|
assigns
|
||||||
|> assign(:field_attributes, get_field_attributes(assigns.member_field))
|
|> assign(:field_attributes, get_field_attributes(assigns.member_field))
|
||||||
|> assign(:is_email_field?, assigns.member_field == :email)
|
|> assign(:is_email_field?, assigns.member_field == :email)
|
||||||
|> assign(:vereinfacht_required_field?, vereinfacht_required_field?(assigns))
|
|
||||||
|> assign(:field_label, MemberFields.label(assigns.member_field))
|
|> assign(:field_label, MemberFields.label(assigns.member_field))
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -120,22 +119,12 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
||||||
|
|
||||||
<%!-- Line break before Required / Show in overview block --%>
|
<%!-- Line break before Required / Show in overview block --%>
|
||||||
<div class="mt-4">
|
<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
|
<div
|
||||||
:if={@is_email_field? or @vereinfacht_required_field?}
|
:if={@is_email_field?}
|
||||||
class="tooltip tooltip-right"
|
class="tooltip tooltip-right"
|
||||||
data-tip={
|
data-tip={gettext("This is a technical field and cannot be changed")}
|
||||||
if(@is_email_field?,
|
aria-label={gettext("This is a technical field and cannot be changed")}
|
||||||
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.")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<fieldset class="mb-2 fieldset">
|
<fieldset class="mb-2 fieldset">
|
||||||
<label>
|
<label>
|
||||||
|
|
@ -164,7 +153,7 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<.input
|
<.input
|
||||||
:if={not @is_email_field? and not @vereinfacht_required_field?}
|
:if={not @is_email_field?}
|
||||||
field={@form[:required]}
|
field={@form[:required]}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
label={gettext("Required")}
|
label={gettext("Required")}
|
||||||
|
|
@ -211,7 +200,6 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
||||||
end
|
end
|
||||||
|
|
||||||
required =
|
required =
|
||||||
socket.assigns.vereinfacht_required_field? ||
|
|
||||||
if Map.has_key?(member_field_params, "required") do
|
if Map.has_key?(member_field_params, "required") do
|
||||||
TypeParsers.parse_boolean(member_field_params["required"])
|
TypeParsers.parse_boolean(member_field_params["required"])
|
||||||
else
|
else
|
||||||
|
|
@ -247,7 +235,6 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
||||||
end
|
end
|
||||||
|
|
||||||
required =
|
required =
|
||||||
socket.assigns.vereinfacht_required_field? ||
|
|
||||||
if Map.has_key?(member_field_params, "required") do
|
if Map.has_key?(member_field_params, "required") do
|
||||||
TypeParsers.parse_boolean(member_field_params["required"])
|
TypeParsers.parse_boolean(member_field_params["required"])
|
||||||
else
|
else
|
||||||
|
|
@ -292,20 +279,10 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
||||||
normalized_visibility = VisibilityConfig.normalize(visibility_config)
|
normalized_visibility = VisibilityConfig.normalize(visibility_config)
|
||||||
normalized_required = VisibilityConfig.normalize(required_config)
|
normalized_required = VisibilityConfig.normalize(required_config)
|
||||||
show_in_overview = Map.get(normalized_visibility, member_field, true)
|
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 =
|
required =
|
||||||
member_field == :email ||
|
member_field == :email || Map.get(normalized_required, member_field, false)
|
||||||
(vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(member_field)) ||
|
|
||||||
Map.get(normalized_required, member_field, false)
|
|
||||||
|
|
||||||
form_data = %{
|
form_data = %{
|
||||||
"name" => MemberFields.label(member_field),
|
"name" => MemberFields.label(member_field),
|
||||||
|
|
@ -338,9 +315,4 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
||||||
defp format_error(error) do
|
defp format_error(error) do
|
||||||
inspect(error)
|
inspect(error)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp vereinfacht_required_field?(assigns) do
|
|
||||||
Mv.Config.vereinfacht_configured?() &&
|
|
||||||
Mv.Constants.vereinfacht_required_field?(assigns.member_field)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -172,19 +172,15 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
|
||||||
member_fields = Mv.Constants.member_fields()
|
member_fields = Mv.Constants.member_fields()
|
||||||
visibility_config = settings.member_field_visibility || %{}
|
visibility_config = settings.member_field_visibility || %{}
|
||||||
required_config = settings.member_field_required || %{}
|
required_config = settings.member_field_required || %{}
|
||||||
vereinfacht_required? = Mv.Config.vereinfacht_configured?()
|
|
||||||
|
|
||||||
normalized_visibility = VisibilityConfig.normalize(visibility_config)
|
normalized_visibility = VisibilityConfig.normalize(visibility_config)
|
||||||
normalized_required = VisibilityConfig.normalize(required_config)
|
normalized_required = VisibilityConfig.normalize(required_config)
|
||||||
|
|
||||||
Enum.map(member_fields, fn field ->
|
Enum.map(member_fields, fn field ->
|
||||||
show_in_overview = Map.get(normalized_visibility, field, true)
|
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 =
|
required =
|
||||||
field == :email ||
|
field == :email || Map.get(normalized_required, field, false)
|
||||||
(vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field)) ||
|
|
||||||
Map.get(normalized_required, field, false)
|
|
||||||
|
|
||||||
attribute = Info.attribute(Mv.Membership.Member, field)
|
attribute = Info.attribute(Mv.Membership.Member, field)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -398,8 +398,6 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_member_field_required_map do
|
defp get_member_field_required_map do
|
||||||
vereinfacht_required? = Mv.Config.vereinfacht_configured?()
|
|
||||||
|
|
||||||
case Membership.get_settings() do
|
case Membership.get_settings() do
|
||||||
{:ok, settings} ->
|
{:ok, settings} ->
|
||||||
required_config = settings.member_field_required || %{}
|
required_config = settings.member_field_required || %{}
|
||||||
|
|
@ -407,20 +405,15 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
|
|
||||||
Mv.Constants.member_fields()
|
Mv.Constants.member_fields()
|
||||||
|> Enum.map(fn field ->
|
|> Enum.map(fn field ->
|
||||||
required =
|
required = field == :email || Map.get(normalized, field, false)
|
||||||
field == :email ||
|
|
||||||
(vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field)) ||
|
|
||||||
Map.get(normalized, field, false)
|
|
||||||
|
|
||||||
{field, required}
|
{field, required}
|
||||||
end)
|
end)
|
||||||
|> Map.new()
|
|> Map.new()
|
||||||
|
|
||||||
{:error, _} ->
|
{: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 ->
|
Map.new(Mv.Constants.member_fields(), fn f ->
|
||||||
{f,
|
{f, f == :email}
|
||||||
f == :email || (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(f))}
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
1
mix.exs
1
mix.exs
|
|
@ -75,6 +75,7 @@ defmodule Mv.MixProject do
|
||||||
{:bandit, "~> 1.5"},
|
{:bandit, "~> 1.5"},
|
||||||
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
|
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
|
||||||
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
|
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
|
||||||
|
{:bypass, "~> 2.1", only: [:dev, :test]},
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:picosat_elixir, "~> 0.1"},
|
{:picosat_elixir, "~> 0.1"},
|
||||||
{:ecto_commons, "~> 0.3"},
|
{:ecto_commons, "~> 0.3"},
|
||||||
|
|
|
||||||
6
mix.lock
6
mix.lock
|
|
@ -10,11 +10,15 @@
|
||||||
"bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"},
|
"bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"},
|
||||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
||||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
|
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
|
||||||
"castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"},
|
"castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"},
|
||||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||||
"cinder": {:hex, :cinder, "0.12.1", "02ae4988e025fb32c37e4e7f2e491586b952918c0dd99d856da13271cd680e16", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.3", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "a48b5677c1f57619d9d7564fb2bd7928f93750a2e8c0b1b145852a30ecf2aa20"},
|
"cinder": {:hex, :cinder, "0.12.1", "02ae4988e025fb32c37e4e7f2e491586b952918c0dd99d856da13271cd680e16", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.3", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "a48b5677c1f57619d9d7564fb2bd7928f93750a2e8c0b1b145852a30ecf2aa20"},
|
||||||
"circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"},
|
"circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"},
|
||||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||||
|
"cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"},
|
||||||
|
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||||
|
"cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
|
||||||
"credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"},
|
"credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"},
|
||||||
"crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"},
|
"crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"},
|
||||||
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
|
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
|
||||||
|
|
@ -65,8 +69,10 @@
|
||||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||||
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||||
|
"plug_cowboy": {:hex, :plug_cowboy, "2.8.0", "07789e9c03539ee51bb14a07839cc95aa96999fd8846ebfd28c97f0b50c7b612", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9cbfaaf17463334ca31aed38ea7e08a68ee37cabc077b1e9be6d2fb68e0171d0"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||||
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
|
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
|
||||||
|
"ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"},
|
||||||
"reactor": {:hex, :reactor, "1.0.0", "024bd13df910bcb8c01cebed4f10bd778269a141a1c8a234e4f67796ac4883cf", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "ae8eb507fffc517f5aa5947db9d2ede2db8bae63b66c94ccb5a2027d30f830a0"},
|
"reactor": {:hex, :reactor, "1.0.0", "024bd13df910bcb8c01cebed4f10bd778269a141a1c8a234e4f67796ac4883cf", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "ae8eb507fffc517f5aa5947db9d2ede2db8bae63b66c94ccb5a2027d30f830a0"},
|
||||||
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
||||||
"rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"},
|
"rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"},
|
||||||
|
|
|
||||||
|
|
@ -2820,11 +2820,6 @@ msgstr "Okt."
|
||||||
msgid "Sep."
|
msgid "Sep."
|
||||||
msgstr "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/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
#: lib/mv_web/translations/member_fields.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
|
||||||
|
|
@ -2820,11 +2820,6 @@ msgstr ""
|
||||||
msgid "Sep."
|
msgid "Sep."
|
||||||
msgstr ""
|
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/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
#: lib/mv_web/translations/member_fields.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
|
||||||
|
|
@ -2820,11 +2820,6 @@ msgstr ""
|
||||||
msgid "Sep."
|
msgid "Sep."
|
||||||
msgstr ""
|
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/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
#: lib/mv_web/translations/member_fields.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ defmodule Mv.Vereinfacht.ClientTest do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Tests for Mv.Vereinfacht.Client.
|
Tests for Mv.Vereinfacht.Client.
|
||||||
|
|
||||||
Only tests the "not configured" path; no real HTTP calls. Config reads from
|
"Not configured" path: no HTTP. When configured we use Bypass to stub the API
|
||||||
ENV first, then from Settings (DB), so we use DataCase so get_settings() is available.
|
and assert on request (query params) and response parsing.
|
||||||
"""
|
"""
|
||||||
use Mv.DataCase, async: false
|
use Mv.DataCase, async: false
|
||||||
|
|
||||||
|
|
@ -30,6 +30,84 @@ defmodule Mv.Vereinfacht.ClientTest do
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|
||||||
|
@tag :bypass
|
||||||
|
test "sends filter[isExternal]=true and filter[email]=<encoded> and returns :not_found when data is empty" do
|
||||||
|
bypass = Bypass.open()
|
||||||
|
base = "http://127.0.0.1:#{bypass.port}"
|
||||||
|
set_vereinfacht_env(base)
|
||||||
|
|
||||||
|
Bypass.expect_once(bypass, "GET", "/finance-contacts", fn conn ->
|
||||||
|
qs = conn.query_string || ""
|
||||||
|
|
||||||
|
assert qs =~ "filter[isExternal]=true",
|
||||||
|
"expected query to contain filter[isExternal]=true, got: #{inspect(qs)}"
|
||||||
|
|
||||||
|
assert qs =~ "filter[email]=",
|
||||||
|
"expected query to contain filter[email]=..., got: #{inspect(qs)}"
|
||||||
|
|
||||||
|
# Email should be encoded (e.g. @ as %40)
|
||||||
|
assert qs =~ "filter[email]=test%40example.com",
|
||||||
|
"expected filter[email] to be URL-encoded (downcased), got: #{inspect(qs)}"
|
||||||
|
|
||||||
|
body = Jason.encode!(%{"jsonapi" => %{"version" => "1.0"}, "data" => []})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> Plug.Conn.put_resp_content_type("application/vnd.api+json")
|
||||||
|
|> Plug.Conn.send_resp(200, body)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert Client.find_contact_by_email(" Test@Example.com ") == {:error, :not_found}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :bypass
|
||||||
|
test "returns {:ok, id} when API returns one contact (string id)" do
|
||||||
|
bypass = Bypass.open()
|
||||||
|
base = "http://127.0.0.1:#{bypass.port}"
|
||||||
|
set_vereinfacht_env(base)
|
||||||
|
|
||||||
|
Bypass.expect_once(bypass, "GET", "/finance-contacts", fn conn ->
|
||||||
|
body =
|
||||||
|
Jason.encode!(%{
|
||||||
|
"jsonapi" => %{"version" => "1.0"},
|
||||||
|
"data" => [%{"type" => "finance-contacts", "id" => "123", "attributes" => %{}}]
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> Plug.Conn.put_resp_content_type("application/vnd.api+json")
|
||||||
|
|> Plug.Conn.send_resp(200, body)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert Client.find_contact_by_email("user@example.com") == {:ok, "123"}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :bypass
|
||||||
|
test "returns {:ok, id} when API returns one contact (integer id)" do
|
||||||
|
bypass = Bypass.open()
|
||||||
|
base = "http://127.0.0.1:#{bypass.port}"
|
||||||
|
set_vereinfacht_env(base)
|
||||||
|
|
||||||
|
Bypass.expect_once(bypass, "GET", "/finance-contacts", fn conn ->
|
||||||
|
body =
|
||||||
|
Jason.encode!(%{
|
||||||
|
"jsonapi" => %{"version" => "1.0"},
|
||||||
|
"data" => [%{"type" => "finance-contacts", "id" => 456, "attributes" => %{}}]
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> Plug.Conn.put_resp_content_type("application/vnd.api+json")
|
||||||
|
|> Plug.Conn.send_resp(200, body)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert Client.find_contact_by_email("other@example.com") == {:ok, "456"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp build_member_struct do
|
defp build_member_struct do
|
||||||
%{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
|
|
@ -42,6 +120,12 @@ defmodule Mv.Vereinfacht.ClientTest do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp set_vereinfacht_env(base_url) do
|
||||||
|
System.put_env("VEREINFACHT_API_URL", base_url)
|
||||||
|
System.put_env("VEREINFACHT_API_KEY", "test-key")
|
||||||
|
System.put_env("VEREINFACHT_CLUB_ID", "2")
|
||||||
|
end
|
||||||
|
|
||||||
defp clear_vereinfacht_env do
|
defp clear_vereinfacht_env do
|
||||||
System.delete_env("VEREINFACHT_API_URL")
|
System.delete_env("VEREINFACHT_API_URL")
|
||||||
System.delete_env("VEREINFACHT_API_KEY")
|
System.delete_env("VEREINFACHT_API_KEY")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue