CustomField Resource Policies closes #386 #387
47 changed files with 1178 additions and 883 deletions
|
|
@ -683,6 +683,13 @@ end
|
||||||
- Falls back to admin user from seeds if system user doesn't exist
|
- Falls back to admin user from seeds if system user doesn't exist
|
||||||
- Should NEVER be used for user-initiated actions (only systemic operations)
|
- Should NEVER be used for user-initiated actions (only systemic operations)
|
||||||
|
|
||||||
|
**DO NOT use system actor as a fallback:**
|
||||||
|
|
||||||
|
- **Never** fall back to `Mv.Helpers.SystemActor.get_system_actor()` when an actor is missing or nil (e.g. in validations, changes, or when reading from context).
|
||||||
|
- Fallbacks hide bugs (callers forget to pass actor) and can cause privilege escalation (unauthenticated or low-privilege paths run with system rights).
|
||||||
|
- If no actor is available, fail explicitly (validation error, Forbidden, or clear error message). Fix the caller to pass the correct actor instead of adding a fallback.
|
||||||
|
- Use system actor only where the operation is **explicitly** a systemic operation (see list above); never as a "safety net" when actor is absent.
|
||||||
|
|
||||||
**User Mode vs System Mode:**
|
**User Mode vs System Mode:**
|
||||||
|
|
||||||
- **User Mode**: User-initiated actions use the actual user actor, policies are enforced
|
- **User Mode**: User-initiated actions use the actual user actor, policies are enforced
|
||||||
|
|
@ -1711,6 +1718,30 @@ This organization ensures fast feedback in standard CI while maintaining compreh
|
||||||
|
|
||||||
## 5. Security Guidelines
|
## 5. Security Guidelines
|
||||||
|
|
||||||
|
### 5.0 No system-actor fallbacks (mandatory)
|
||||||
|
|
||||||
|
**Do not use the system actor as a fallback when an actor is missing.**
|
||||||
|
|
||||||
|
Examples of forbidden patterns:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# ❌ FORBIDDEN - Fallback to system actor when actor is nil
|
||||||
|
actor = Map.get(changeset.context, :actor) || Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# ❌ FORBIDDEN - "Safety" fallback in validations, changes, or helpers
|
||||||
|
actor = opts[:actor] || Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# ❌ FORBIDDEN - Default actor in function options
|
||||||
|
def list_something(opts \\ []) do
|
||||||
|
actor = Keyword.get(opts, :actor) || Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Fallbacks hide missing-actor bugs and can lead to privilege escalation (e.g. a request without actor would run with system privileges). Always require the caller to pass the actor for user-facing or context-dependent operations; if none is available, return an error or fail validation instead of using the system actor.
|
||||||
|
|
||||||
|
**Allowed:** Use the system actor only where the operation is **by design** a systemic operation (e.g. email sync, seeds, test fixtures, background jobs) and you explicitly call `SystemActor.get_system_actor()` at that call site—never as a fallback when `actor` is nil or absent.
|
||||||
|
|
||||||
### 5.1 Authentication & Authorization
|
### 5.1 Authentication & Authorization
|
||||||
|
|
||||||
**Use AshAuthentication:**
|
**Use AshAuthentication:**
|
||||||
|
|
|
||||||
|
|
@ -1101,28 +1101,23 @@ end
|
||||||
|
|
||||||
### CustomField Resource Policies
|
### CustomField Resource Policies
|
||||||
|
|
||||||
**Location:** `lib/mv/membership/custom_field.ex`
|
**Location:** `lib/membership/custom_field.ex`
|
||||||
|
|
||||||
**No Special Cases:** All users can read, only admin can write.
|
**No Special Cases:** All users can read, only admin can write.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule Mv.Membership.CustomField do
|
defmodule Mv.Membership.CustomField do
|
||||||
use Ash.Resource, ...
|
use Ash.Resource,
|
||||||
|
domain: Mv.Membership,
|
||||||
|
data_layer: AshPostgres.DataLayer,
|
||||||
|
authorizers: [Ash.Policy.Authorizer]
|
||||||
|
|
||||||
policies do
|
policies do
|
||||||
# All authenticated users can read custom fields (needed for forms)
|
|
||||||
# Write operations are admin-only
|
|
||||||
policy action_type([:read, :create, :update, :destroy]) do
|
policy action_type([:read, :create, :update, :destroy]) do
|
||||||
description "Check permissions from user's role"
|
description "Check permissions from user's role"
|
||||||
authorize_if Mv.Authorization.Checks.HasPermission
|
authorize_if Mv.Authorization.Checks.HasPermission
|
||||||
end
|
end
|
||||||
|
|
||||||
# DEFAULT: Forbid
|
|
||||||
policy action_type([:read, :create, :update, :destroy]) do
|
|
||||||
forbid_if always()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ defmodule Mv.Membership.CustomField do
|
||||||
"""
|
"""
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: Mv.Membership,
|
domain: Mv.Membership,
|
||||||
data_layer: AshPostgres.DataLayer
|
data_layer: AshPostgres.DataLayer,
|
||||||
|
authorizers: [Ash.Policy.Authorizer]
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
table "custom_fields"
|
table "custom_fields"
|
||||||
|
|
@ -79,6 +80,13 @@ defmodule Mv.Membership.CustomField do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
policies do
|
||||||
|
policy action_type([:read, :create, :update, :destroy]) do
|
||||||
|
description "Check permissions from user's role"
|
||||||
|
authorize_if Mv.Authorization.Checks.HasPermission
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -471,11 +471,12 @@ defmodule Mv.Membership.Member do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Validate required custom fields
|
# Validate required custom fields (actor from validation context only; no fallback)
|
||||||
validate fn changeset, _ ->
|
validate fn changeset, context ->
|
||||||
provided_values = provided_custom_field_values(changeset)
|
provided_values = provided_custom_field_values(changeset)
|
||||||
|
actor = context.actor
|
||||||
|
|
||||||
case Mv.Membership.list_required_custom_fields() do
|
case Mv.Membership.list_required_custom_fields(actor: actor) do
|
||||||
{:ok, required_custom_fields} ->
|
{:ok, required_custom_fields} ->
|
||||||
missing_fields = missing_required_fields(required_custom_fields, provided_values)
|
missing_fields = missing_required_fields(required_custom_fields, provided_values)
|
||||||
|
|
||||||
|
|
@ -485,6 +486,24 @@ defmodule Mv.Membership.Member do
|
||||||
build_custom_field_validation_error(missing_fields)
|
build_custom_field_validation_error(missing_fields)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
{:error, %Ash.Error.Forbidden{}} ->
|
||||||
|
Logger.warning(
|
||||||
|
"Required custom fields validation: actor not authorized to read CustomField"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
field: :custom_field_values,
|
||||||
|
message:
|
||||||
|
"You are not authorized to perform this action. Please sign in again or contact support."}
|
||||||
|
|
||||||
|
{:error, :missing_actor} ->
|
||||||
|
Logger.warning("Required custom fields validation: no actor in context")
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
field: :custom_field_values,
|
||||||
|
message:
|
||||||
|
"You are not authorized to perform this action. Please sign in again or contact support."}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
Logger.error(
|
Logger.error(
|
||||||
"Failed to load custom fields for validation: #{inspect(error)}. Required field validation cannot be performed."
|
"Failed to load custom fields for validation: #{inspect(error)}. Required field validation cannot be performed."
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule Mv.Membership do
|
||||||
The domain exposes these main actions:
|
The domain exposes these main actions:
|
||||||
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
|
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
|
||||||
- Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
|
- Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
|
||||||
- Custom field management: `create_custom_field/1`, `list_custom_fields/0`, `list_required_custom_fields/0`, etc.
|
- Custom field management: `create_custom_field/1`, `list_custom_fields/0`, `list_required_custom_fields/1`, etc.
|
||||||
- Settings management: `get_settings/0`, `update_settings/2`, `update_member_field_visibility/2`, `update_single_member_field_visibility/3`
|
- Settings management: `get_settings/0`, `update_settings/2`, `update_member_field_visibility/2`, `update_single_member_field_visibility/3`
|
||||||
- Group management: `create_group/1`, `list_groups/0`, `update_group/2`, `destroy_group/1`
|
- Group management: `create_group/1`, `list_groups/0`, `update_group/2`, `destroy_group/1`
|
||||||
- Member-group associations: `create_member_group/1`, `list_member_groups/0`, `destroy_member_group/1`
|
- Member-group associations: `create_member_group/1`, `list_member_groups/0`, `destroy_member_group/1`
|
||||||
|
|
@ -155,25 +155,37 @@ defmodule Mv.Membership do
|
||||||
Lists only required custom fields.
|
Lists only required custom fields.
|
||||||
|
|
||||||
This is an optimized version that filters at the database level instead of
|
This is an optimized version that filters at the database level instead of
|
||||||
loading all custom fields and filtering in memory.
|
loading all custom fields and filtering in memory. Requires an actor for
|
||||||
|
authorization (CustomField read policy). Callers must pass `actor:`; no default.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
- `:actor` - Required. The actor for authorization (e.g. current user).
|
||||||
|
All roles can read CustomField; actor must have a valid role.
|
||||||
|
|
||||||
## Returns
|
## Returns
|
||||||
|
|
||||||
- `{:ok, required_custom_fields}` - List of required custom fields
|
- `{:ok, required_custom_fields}` - List of required custom fields
|
||||||
- `{:error, error}` - Error reading custom fields
|
- `{:error, :missing_actor}` - When actor is nil (caller must pass actor)
|
||||||
|
- `{:error, error}` - Error reading custom fields (e.g. Forbidden)
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields()
|
iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields(actor: actor)
|
||||||
iex> Enum.all?(required_fields, & &1.required)
|
iex> Enum.all?(required_fields, & &1.required)
|
||||||
true
|
true
|
||||||
|
|
||||||
|
iex> Mv.Membership.list_required_custom_fields(actor: nil)
|
||||||
|
{:error, :missing_actor}
|
||||||
"""
|
"""
|
||||||
def list_required_custom_fields do
|
def list_required_custom_fields(actor: actor) when not is_nil(actor) do
|
||||||
Mv.Membership.CustomField
|
Mv.Membership.CustomField
|
||||||
|> Ash.Query.filter(expr(required == true))
|
|> Ash.Query.filter(expr(required == true))
|
||||||
|> Ash.read(domain: __MODULE__)
|
|> Ash.read(domain: __MODULE__, actor: actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_required_custom_fields(actor: nil), do: {:error, :missing_actor}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates the member field visibility configuration.
|
Updates the member field visibility configuration.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,8 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("save", %{"custom_field" => custom_field_params}, socket) do
|
def handle_event("save", %{"custom_field" => custom_field_params}, socket) do
|
||||||
actor = MvWeb.LiveHelpers.current_actor(socket)
|
# Actor must be passed from parent (IndexComponent); component socket has no current_user
|
||||||
|
actor = socket.assigns[:actor]
|
||||||
|
|
||||||
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, custom_field_params, actor) do
|
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, custom_field_params, actor) do
|
||||||
{:ok, custom_field} ->
|
{:ok, custom_field} ->
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_component
|
use MvWeb, :live_component
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
assigns = assign(assigns, :field_type_label, &MvWeb.Translations.FieldTypes.label/1)
|
assigns = assign(assigns, :field_type_label, &MvWeb.Translations.FieldTypes.label/1)
|
||||||
|
|
@ -38,6 +40,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MvWeb.CustomFieldLive.FormComponent}
|
module={MvWeb.CustomFieldLive.FormComponent}
|
||||||
id={@form_id}
|
id={@form_id}
|
||||||
|
actor={@actor}
|
||||||
custom_field={@editing_custom_field}
|
custom_field={@editing_custom_field}
|
||||||
on_save={
|
on_save={
|
||||||
fn custom_field, action -> send(self(), {:custom_field_saved, custom_field, action}) end
|
fn custom_field, action -> send(self(), {:custom_field_saved, custom_field, action}) end
|
||||||
|
|
@ -176,6 +179,21 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp stream_custom_fields(actor, parent_pid) do
|
||||||
|
case Ash.read(Mv.Membership.CustomField, actor: actor) do
|
||||||
|
{:ok, custom_fields} ->
|
||||||
|
custom_fields
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
Logger.warning(
|
||||||
|
"CustomFieldLive.IndexComponent: failed to load custom fields: #{inspect(error)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
send(parent_pid, {:custom_fields_load_error, error})
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
# Track previous show_form state to detect when form is closed
|
# Track previous show_form state to detect when form is closed
|
||||||
|
|
@ -207,7 +225,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
|> assign_new(:show_delete_modal, fn -> false end)
|
|> assign_new(:show_delete_modal, fn -> false end)
|
||||||
|> assign_new(:custom_field_to_delete, fn -> nil end)
|
|> assign_new(:custom_field_to_delete, fn -> nil end)
|
||||||
|> assign_new(:slug_confirmation, fn -> "" end)
|
|> assign_new(:slug_confirmation, fn -> "" end)
|
||||||
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField), reset: true)}
|
|> stream(:custom_fields, stream_custom_fields(assigns[:actor], self()), reset: true)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -226,7 +244,8 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("edit_custom_field", %{"id" => id}, socket) do
|
def handle_event("edit_custom_field", %{"id" => id}, socket) do
|
||||||
custom_field = Ash.get!(Mv.Membership.CustomField, id)
|
actor = socket.assigns[:actor]
|
||||||
|
custom_field = Ash.get!(Mv.Membership.CustomField, id, actor: actor)
|
||||||
|
|
||||||
# Only send event if form was not already open
|
# Only send event if form was not already open
|
||||||
if not socket.assigns[:show_form] do
|
if not socket.assigns[:show_form] do
|
||||||
|
|
@ -242,7 +261,13 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("prepare_delete", %{"id" => id}, socket) do
|
def handle_event("prepare_delete", %{"id" => id}, socket) do
|
||||||
custom_field = Ash.get!(Mv.Membership.CustomField, id, load: [:assigned_members_count])
|
actor = socket.assigns[:actor]
|
||||||
|
|
||||||
|
custom_field =
|
||||||
|
Ash.get!(Mv.Membership.CustomField, id,
|
||||||
|
load: [:assigned_members_count],
|
||||||
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|
|
@ -259,9 +284,10 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("confirm_delete", _params, socket) do
|
def handle_event("confirm_delete", _params, socket) do
|
||||||
custom_field = socket.assigns.custom_field_to_delete
|
custom_field = socket.assigns.custom_field_to_delete
|
||||||
|
actor = socket.assigns[:actor]
|
||||||
|
|
||||||
if socket.assigns.slug_confirmation == custom_field.slug do
|
if socket.assigns.slug_confirmation == custom_field.slug do
|
||||||
case Ash.destroy(custom_field) do
|
case Ash.destroy(custom_field, actor: actor) do
|
||||||
:ok ->
|
:ok ->
|
||||||
send(self(), {:custom_field_deleted, custom_field})
|
send(self(), {:custom_field_deleted, custom_field})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
:if={@active_editing_section != :member_fields}
|
:if={@active_editing_section != :member_fields}
|
||||||
module={MvWeb.CustomFieldLive.IndexComponent}
|
module={MvWeb.CustomFieldLive.IndexComponent}
|
||||||
id="custom-fields-component"
|
id="custom-fields-component"
|
||||||
|
actor={@current_user}
|
||||||
/>
|
/>
|
||||||
</.form_section>
|
</.form_section>
|
||||||
|
|
||||||
|
|
@ -503,6 +504,15 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
{:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))}
|
{:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({:custom_fields_load_error, _error}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
put_flash(
|
||||||
|
socket,
|
||||||
|
:error,
|
||||||
|
gettext("Could not load data fields. Please check your permissions.")
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:editing_section_changed, section}, socket) do
|
def handle_info({:editing_section_changed, section}, socket) do
|
||||||
{:noreply, assign(socket, :active_editing_section, section)}
|
{:noreply, assign(socket, :active_editing_section, section)}
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
def mount(params, _session, socket) do
|
def mount(params, _session, socket) do
|
||||||
# current_user should be set by on_mount hooks (LiveUserAuth + LiveHelpers)
|
# current_user should be set by on_mount hooks (LiveUserAuth + LiveHelpers)
|
||||||
actor = current_actor(socket)
|
actor = current_actor(socket)
|
||||||
{:ok, custom_fields} = Mv.Membership.list_custom_fields()
|
{:ok, custom_fields} = Mv.Membership.list_custom_fields(actor: actor)
|
||||||
|
|
||||||
initial_custom_field_values =
|
initial_custom_field_values =
|
||||||
Enum.map(custom_fields, fn cf ->
|
Enum.map(custom_fields, fn cf ->
|
||||||
|
|
|
||||||
|
|
@ -2262,3 +2262,13 @@ msgstr "Dieser Benutzer kann nicht bearbeitet werden."
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "This user cannot be viewed."
|
msgid "This user cannot be viewed."
|
||||||
msgstr "Dieser Benutzer kann nicht angezeigt werden."
|
msgstr "Dieser Benutzer kann nicht angezeigt werden."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/group_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not authorized."
|
||||||
|
msgstr "Nicht berechtigt."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not load data fields. Please check your permissions."
|
||||||
|
msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfen Sie Ihre Berechtigungen."
|
||||||
|
|
|
||||||
|
|
@ -2268,3 +2268,8 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Not authorized."
|
msgid "Not authorized."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not load data fields. Please check your permissions."
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -2263,3 +2263,13 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "This user cannot be viewed."
|
msgid "This user cannot be viewed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/group_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not authorized."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not load data fields. Please check your permissions."
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -118,10 +118,12 @@ for attrs <- [
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
] do
|
] do
|
||||||
|
# Bootstrap: no admin user yet; CustomField create requires admin, so skip authorization
|
||||||
Membership.create_custom_field!(
|
Membership.create_custom_field!(
|
||||||
attrs,
|
attrs,
|
||||||
upsert?: true,
|
upsert?: true,
|
||||||
upsert_identity: :unique_name
|
upsert_identity: :unique_name,
|
||||||
|
authorize?: false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -379,12 +381,16 @@ Enum.each(member_attrs_list, fn member_attrs ->
|
||||||
final_member =
|
final_member =
|
||||||
if is_nil(member.membership_fee_type_id) and
|
if is_nil(member.membership_fee_type_id) and
|
||||||
Map.has_key?(member_attrs_without_status, :membership_fee_type_id) do
|
Map.has_key?(member_attrs_without_status, :membership_fee_type_id) do
|
||||||
member
|
{:ok, updated} =
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
Membership.update_member(
|
||||||
membership_fee_type_id: member_attrs_without_status.membership_fee_type_id
|
member,
|
||||||
})
|
%{
|
||||||
|> Ash.Changeset.put_context(:actor, admin_user_with_role)
|
membership_fee_type_id: member_attrs_without_status.membership_fee_type_id
|
||||||
|> Ash.update!(actor: admin_user_with_role)
|
},
|
||||||
|
actor: admin_user_with_role
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
else
|
else
|
||||||
member
|
member
|
||||||
end
|
end
|
||||||
|
|
@ -544,9 +550,12 @@ Enum.with_index(linked_members)
|
||||||
fee_type_index = rem(3 + index, length(all_fee_types))
|
fee_type_index = rem(3 + index, length(all_fee_types))
|
||||||
fee_type = Enum.at(all_fee_types, fee_type_index)
|
fee_type = Enum.at(all_fee_types, fee_type_index)
|
||||||
|
|
||||||
member
|
{:ok, updated} =
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
Membership.update_member(member, %{membership_fee_type_id: fee_type.id},
|
||||||
|> Ash.update!(actor: admin_user_with_role)
|
actor: admin_user_with_role
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
else
|
else
|
||||||
member
|
member
|
||||||
end
|
end
|
||||||
|
|
@ -594,7 +603,7 @@ end)
|
||||||
|
|
||||||
# Create sample custom field values for some members
|
# Create sample custom field values for some members
|
||||||
all_members = Ash.read!(Membership.Member, actor: admin_user_with_role)
|
all_members = Ash.read!(Membership.Member, actor: admin_user_with_role)
|
||||||
all_custom_fields = Ash.read!(Membership.CustomField)
|
all_custom_fields = Ash.read!(Membership.CustomField, actor: admin_user_with_role)
|
||||||
|
|
||||||
# Helper function to find custom field by name
|
# Helper function to find custom field by name
|
||||||
find_field = fn name -> Enum.find(all_custom_fields, &(&1.name == name)) end
|
find_field = fn name -> Enum.find(all_custom_fields, &(&1.name == name)) end
|
||||||
|
|
|
||||||
|
|
@ -239,13 +239,14 @@ defmodule Mv.Membership.CustomFieldDeletionTest do
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
defp create_member(actor) do
|
defp create_member(actor) do
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User#{System.unique_integer([:positive])}",
|
last_name: "User#{System.unique_integer([:positive])}",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
email: "test#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_custom_field(name, value_type, actor) do
|
defp create_custom_field(name, value_type, actor) do
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do
|
||||||
|
|
||||||
# Create a test member
|
# Create a test member
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test.validation@example.com"
|
email: "test.validation@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Create custom fields for different types
|
# Create custom fields for different types
|
||||||
{:ok, string_field} =
|
{:ok, string_field} =
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,8 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
|
|
|
||||||
|
|
@ -14,31 +14,22 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Create test members
|
# Create test members
|
||||||
{:ok, member1} =
|
{:ok, member1} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Anderson",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member2} =
|
{:ok, member2} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
|
||||||
first_name: "Bob",
|
actor: system_actor
|
||||||
last_name: "Brown",
|
)
|
||||||
email: "bob@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member3} =
|
{:ok, member3} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"},
|
||||||
first_name: "Charlie",
|
actor: system_actor
|
||||||
last_name: "Clark",
|
)
|
||||||
email: "charlie@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create custom fields for different types
|
# Create custom fields for different types
|
||||||
{:ok, string_field} =
|
{:ok, string_field} =
|
||||||
|
|
@ -112,9 +103,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update by reloading member
|
# Force search_vector update by reloading member
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Search for the custom field value
|
# Search for the custom field value
|
||||||
results =
|
results =
|
||||||
|
|
@ -143,9 +132,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Search for the custom field value
|
# Search for the custom field value
|
||||||
results =
|
results =
|
||||||
|
|
@ -174,9 +161,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Search for partial custom field value (should work via FTS or custom field filter)
|
# Search for partial custom field value (should work via FTS or custom field filter)
|
||||||
results =
|
results =
|
||||||
|
|
@ -225,9 +210,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Search for the custom field value (date is stored as text in search_vector)
|
# Search for the custom field value (date is stored as text in search_vector)
|
||||||
results =
|
results =
|
||||||
|
|
@ -256,9 +239,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Search for the custom field value (boolean is stored as "true" or "false" text)
|
# Search for the custom field value (boolean is stored as "true" or "false" text)
|
||||||
results =
|
results =
|
||||||
|
|
@ -287,9 +268,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Update custom field value
|
# Update custom field value
|
||||||
{:ok, _updated_cfv} =
|
{:ok, _updated_cfv} =
|
||||||
|
|
@ -334,9 +313,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Verify it's searchable
|
# Verify it's searchable
|
||||||
results =
|
results =
|
||||||
|
|
@ -401,9 +378,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Update member (should trigger search_vector update including custom fields)
|
# Update member (should trigger search_vector update including custom fields)
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{notes: "Updated notes"}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{notes: "Updated notes"})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Search should find the custom field value
|
# Search should find the custom field value
|
||||||
results =
|
results =
|
||||||
|
|
@ -452,9 +427,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# All values should be searchable
|
# All values should be searchable
|
||||||
results1 =
|
results1 =
|
||||||
|
|
@ -496,9 +469,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Search for full value (should work via search_vector)
|
# Search for full value (should work via search_vector)
|
||||||
results_full =
|
results_full =
|
||||||
|
|
@ -541,9 +512,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||||
|
|
||||||
# Force search_vector update
|
# Force search_vector update
|
||||||
{:ok, _updated_member} =
|
{:ok, _updated_member} =
|
||||||
member1
|
Mv.Membership.update_member(member1, %{}, actor: system_actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{})
|
|
||||||
|> Ash.update(actor: system_actor)
|
|
||||||
|
|
||||||
# Search for full phone number (should work via search_vector)
|
# Search for full phone number (should work via search_vector)
|
||||||
results_full =
|
results_full =
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,8 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
|
|
@ -76,10 +74,14 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
# Manually assign fee type (this will trigger cycle generation)
|
# Manually assign fee type (this will trigger cycle generation)
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> then(fn m ->
|
||||||
membership_fee_type_id: yearly_type1.id
|
{:ok, updated} =
|
||||||
})
|
Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
|
||||||
|> Ash.update!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Cycle generation runs synchronously in the same transaction
|
# Cycle generation runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -140,11 +142,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
|
|
||||||
# Change membership fee type (same interval, different amount)
|
# Change membership fee type (same interval, different amount)
|
||||||
assert {:ok, _updated_member} =
|
assert {:ok, _updated_member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
actor: actor
|
||||||
membership_fee_type_id: yearly_type2.id
|
)
|
||||||
})
|
|
||||||
|> Ash.update(actor: actor)
|
|
||||||
|
|
||||||
# Cycle regeneration runs synchronously in the same transaction
|
# Cycle regeneration runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -194,10 +194,14 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
# Manually assign fee type (this will trigger cycle generation)
|
# Manually assign fee type (this will trigger cycle generation)
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> then(fn m ->
|
||||||
membership_fee_type_id: yearly_type1.id
|
{:ok, updated} =
|
||||||
})
|
Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
|
||||||
|> Ash.update!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Cycle generation runs synchronously in the same transaction
|
# Cycle generation runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -215,11 +219,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
|
|
||||||
# Change membership fee type
|
# Change membership fee type
|
||||||
assert {:ok, _updated_member} =
|
assert {:ok, _updated_member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
actor: actor
|
||||||
membership_fee_type_id: yearly_type2.id
|
)
|
||||||
})
|
|
||||||
|> Ash.update(actor: actor)
|
|
||||||
|
|
||||||
# Cycle regeneration runs synchronously in the same transaction
|
# Cycle regeneration runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -242,10 +244,14 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
# Manually assign fee type (this will trigger cycle generation)
|
# Manually assign fee type (this will trigger cycle generation)
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> then(fn m ->
|
||||||
membership_fee_type_id: yearly_type1.id
|
{:ok, updated} =
|
||||||
})
|
Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
|
||||||
|> Ash.update!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Cycle generation runs synchronously in the same transaction
|
# Cycle generation runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -263,11 +269,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
|
|
||||||
# Change membership fee type
|
# Change membership fee type
|
||||||
assert {:ok, _updated_member} =
|
assert {:ok, _updated_member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
actor: actor
|
||||||
membership_fee_type_id: yearly_type2.id
|
)
|
||||||
})
|
|
||||||
|> Ash.update(actor: actor)
|
|
||||||
|
|
||||||
# Cycle regeneration runs synchronously in the same transaction
|
# Cycle regeneration runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -290,10 +294,14 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
# Manually assign fee type (this will trigger cycle generation)
|
# Manually assign fee type (this will trigger cycle generation)
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> then(fn m ->
|
||||||
membership_fee_type_id: yearly_type1.id
|
{:ok, updated} =
|
||||||
})
|
Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
|
||||||
|> Ash.update!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Cycle generation runs synchronously in the same transaction
|
# Cycle generation runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -357,11 +365,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
|
|
||||||
# Change membership fee type
|
# Change membership fee type
|
||||||
assert {:ok, _updated_member} =
|
assert {:ok, _updated_member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
actor: actor
|
||||||
membership_fee_type_id: yearly_type2.id
|
)
|
||||||
})
|
|
||||||
|> Ash.update(actor: actor)
|
|
||||||
|
|
||||||
# Cycle regeneration runs synchronously in the same transaction
|
# Cycle regeneration runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -406,10 +412,14 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
# Manually assign fee type (this will trigger cycle generation)
|
# Manually assign fee type (this will trigger cycle generation)
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> then(fn m ->
|
||||||
membership_fee_type_id: yearly_type1.id
|
{:ok, updated} =
|
||||||
})
|
Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
|
||||||
|> Ash.update!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Cycle generation runs synchronously in the same transaction
|
# Cycle generation runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
@ -463,11 +473,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
|
||||||
|
|
||||||
# Change membership fee type
|
# Change membership fee type
|
||||||
assert {:ok, updated_member} =
|
assert {:ok, updated_member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
actor: actor
|
||||||
membership_fee_type_id: yearly_type2.id
|
)
|
||||||
})
|
|
||||||
|> Ash.update(actor: actor)
|
|
||||||
|
|
||||||
# Cycle regeneration runs synchronously in the same transaction
|
# Cycle regeneration runs synchronously in the same transaction
|
||||||
# No need to wait for async completion
|
# No need to wait for async completion
|
||||||
|
|
|
||||||
|
|
@ -146,16 +146,17 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
||||||
|> Ash.create!(actor: actor)
|
|> Ash.create!(actor: actor)
|
||||||
|
|
||||||
# Create member with join_date and fee type but no explicit start date
|
# Create member with join_date and fee type but no explicit start date
|
||||||
member =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2024-03-15],
|
join_date: ~D[2024-03-15],
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Should have auto-calculated start date (2024-01-01 for yearly with include_joining_cycle=true)
|
# Should have auto-calculated start date (2024-01-01 for yearly with include_joining_cycle=true)
|
||||||
assert member.membership_fee_start_date == ~D[2024-01-01]
|
assert member.membership_fee_start_date == ~D[2024-01-01]
|
||||||
|
|
@ -177,17 +178,18 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
||||||
# Create member with explicit start date
|
# Create member with explicit start date
|
||||||
manual_start_date = ~D[2024-07-01]
|
manual_start_date = ~D[2024-07-01]
|
||||||
|
|
||||||
member =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2024-03-15],
|
join_date: ~D[2024-03-15],
|
||||||
membership_fee_type_id: fee_type.id,
|
membership_fee_type_id: fee_type.id,
|
||||||
membership_fee_start_date: manual_start_date
|
membership_fee_start_date: manual_start_date
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Should keep the manually set date
|
# Should keep the manually set date
|
||||||
assert member.membership_fee_start_date == manual_start_date
|
assert member.membership_fee_start_date == manual_start_date
|
||||||
|
|
@ -207,16 +209,17 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
||||||
|> Ash.create!(actor: actor)
|
|> Ash.create!(actor: actor)
|
||||||
|
|
||||||
# Create member
|
# Create member
|
||||||
member =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2024-03-15],
|
join_date: ~D[2024-03-15],
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Should have next cycle start date (2025-01-01 for yearly with include_joining_cycle=false)
|
# Should have next cycle start date (2025-01-01 for yearly with include_joining_cycle=false)
|
||||||
assert member.membership_fee_start_date == ~D[2025-01-01]
|
assert member.membership_fee_start_date == ~D[2025-01-01]
|
||||||
|
|
@ -236,16 +239,17 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
||||||
|> Ash.create!(actor: actor)
|
|> Ash.create!(actor: actor)
|
||||||
|
|
||||||
# Create member without join_date
|
# Create member without join_date
|
||||||
member =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
# No join_date
|
# No join_date
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Should not have auto-calculated start date
|
# Should not have auto-calculated start date
|
||||||
assert is_nil(member.membership_fee_start_date)
|
assert is_nil(member.membership_fee_start_date)
|
||||||
|
|
@ -255,16 +259,17 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do
|
||||||
setup_settings(true, actor)
|
setup_settings(true, actor)
|
||||||
|
|
||||||
# Create member without fee type
|
# Create member without fee type
|
||||||
member =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2024-03-15]
|
join_date: ~D[2024-03-15]
|
||||||
# No membership_fee_type_id
|
# No membership_fee_type_id
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Should not have auto-calculated start date
|
# Should not have auto-calculated start date
|
||||||
assert is_nil(member.membership_fee_start_date)
|
assert is_nil(member.membership_fee_start_date)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
"""
|
"""
|
||||||
use Mv.DataCase, async: true
|
use Mv.DataCase, async: true
|
||||||
|
|
||||||
alias Mv.Membership.Member
|
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
alias Mv.MembershipFees.Changes.ValidateSameInterval
|
alias Mv.MembershipFees.Changes.ValidateSameInterval
|
||||||
|
|
||||||
|
|
@ -37,10 +36,8 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "validate_interval_match/1" do
|
describe "validate_interval_match/1" do
|
||||||
|
|
@ -52,9 +49,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: yearly_type2.id},
|
||||||
membership_fee_type_id: yearly_type2.id
|
actor: actor
|
||||||
})
|
)
|
||||||
|> ValidateSameInterval.change(%{}, %{})
|
|> ValidateSameInterval.change(%{}, %{})
|
||||||
|
|
||||||
assert changeset.valid?
|
assert changeset.valid?
|
||||||
|
|
@ -68,9 +65,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: monthly_type.id},
|
||||||
membership_fee_type_id: monthly_type.id
|
actor: actor
|
||||||
})
|
)
|
||||||
|> ValidateSameInterval.change(%{}, %{})
|
|> ValidateSameInterval.change(%{}, %{})
|
||||||
|
|
||||||
refute changeset.valid?
|
refute changeset.valid?
|
||||||
|
|
@ -90,9 +87,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: yearly_type.id},
|
||||||
membership_fee_type_id: yearly_type.id
|
actor: actor
|
||||||
})
|
)
|
||||||
|> ValidateSameInterval.change(%{}, %{})
|
|> ValidateSameInterval.change(%{}, %{})
|
||||||
|
|
||||||
assert changeset.valid?
|
assert changeset.valid?
|
||||||
|
|
@ -104,9 +101,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: nil}, actor: actor)
|
||||||
membership_fee_type_id: nil
|
|
||||||
})
|
|
||||||
|> ValidateSameInterval.change(%{}, %{})
|
|> ValidateSameInterval.change(%{}, %{})
|
||||||
|
|
||||||
refute changeset.valid?
|
refute changeset.valid?
|
||||||
|
|
@ -124,9 +119,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> Ash.Changeset.for_update(:update_member, %{first_name: "New Name"}, actor: actor)
|
||||||
first_name: "New Name"
|
|
||||||
})
|
|
||||||
|> ValidateSameInterval.change(%{}, %{})
|
|> ValidateSameInterval.change(%{}, %{})
|
||||||
|
|
||||||
assert changeset.valid?
|
assert changeset.valid?
|
||||||
|
|
@ -140,9 +133,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: quarterly_type.id},
|
||||||
membership_fee_type_id: quarterly_type.id
|
actor: actor
|
||||||
})
|
)
|
||||||
|> ValidateSameInterval.change(%{}, %{})
|
|> ValidateSameInterval.change(%{}, %{})
|
||||||
|
|
||||||
error = Enum.find(changeset.errors, &(&1.field == :membership_fee_type_id))
|
error = Enum.find(changeset.errors, &(&1.field == :membership_fee_type_id))
|
||||||
|
|
@ -179,9 +172,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: type2.id},
|
||||||
membership_fee_type_id: type2.id
|
actor: actor
|
||||||
})
|
)
|
||||||
|> ValidateSameInterval.change(%{}, %{})
|
|> ValidateSameInterval.change(%{}, %{})
|
||||||
|
|
||||||
refute changeset.valid?,
|
refute changeset.valid?,
|
||||||
|
|
@ -199,11 +192,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
# Try to update member with different interval type
|
# Try to update member with different interval type
|
||||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
assert {:error, %Ash.Error.Invalid{} = error} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: monthly_type.id},
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
actor: actor
|
||||||
membership_fee_type_id: monthly_type.id
|
)
|
||||||
})
|
|
||||||
|> Ash.update(actor: actor)
|
|
||||||
|
|
||||||
# Check that error is about interval mismatch
|
# Check that error is about interval mismatch
|
||||||
error_message = extract_error_message(error)
|
error_message = extract_error_message(error)
|
||||||
|
|
@ -220,11 +211,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
||||||
|
|
||||||
# Update member with same-interval type
|
# Update member with same-interval type
|
||||||
assert {:ok, updated_member} =
|
assert {:ok, updated_member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
actor: actor
|
||||||
membership_fee_type_id: yearly_type2.id
|
)
|
||||||
})
|
|
||||||
|> Ash.update(actor: actor)
|
|
||||||
|
|
||||||
assert updated_member.membership_fee_type_id == yearly_type2.id
|
assert updated_member.membership_fee_type_id == yearly_type2.id
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -52,16 +52,17 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
||||||
setup_settings(true, actor)
|
setup_settings(true, actor)
|
||||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||||
|
|
||||||
member =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2023-03-15],
|
join_date: ~D[2023-03-15],
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
cycles = get_member_cycles(member.id, actor)
|
cycles = get_member_cycles(member.id, actor)
|
||||||
|
|
||||||
|
|
@ -80,16 +81,16 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
||||||
test "does not create cycles when member has no fee type", %{actor: actor} do
|
test "does not create cycles when member has no fee type", %{actor: actor} do
|
||||||
setup_settings(true, actor)
|
setup_settings(true, actor)
|
||||||
|
|
||||||
member =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2023-03-15]
|
join_date: ~D[2023-03-15]
|
||||||
# No membership_fee_type_id
|
},
|
||||||
})
|
actor: actor
|
||||||
|> Ash.create!(actor: actor)
|
)
|
||||||
|
|
||||||
cycles = get_member_cycles(member.id, actor)
|
cycles = get_member_cycles(member.id, actor)
|
||||||
|
|
||||||
|
|
@ -100,16 +101,16 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
||||||
setup_settings(true, actor)
|
setup_settings(true, actor)
|
||||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||||
|
|
||||||
member =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
# No join_date
|
},
|
||||||
})
|
actor: actor
|
||||||
|> Ash.create!(actor: actor)
|
)
|
||||||
|
|
||||||
cycles = get_member_cycles(member.id, actor)
|
cycles = get_member_cycles(member.id, actor)
|
||||||
|
|
||||||
|
|
@ -123,23 +124,23 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
||||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||||
|
|
||||||
# Create member without fee type
|
# Create member without fee type
|
||||||
member =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2023-03-15]
|
join_date: ~D[2023-03-15]
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Verify no cycles yet
|
# Verify no cycles yet
|
||||||
assert get_member_cycles(member.id, actor) == []
|
assert get_member_cycles(member.id, actor) == []
|
||||||
|
|
||||||
# Update to assign fee type
|
# Update to assign fee type
|
||||||
member
|
{:ok, member} =
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|> Ash.update!(actor: actor)
|
|
||||||
|
|
||||||
cycles = get_member_cycles(member.id, actor)
|
cycles = get_member_cycles(member.id, actor)
|
||||||
|
|
||||||
|
|
@ -157,15 +158,19 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
||||||
tasks =
|
tasks =
|
||||||
Enum.map(1..5, fn i ->
|
Enum.map(1..5, fn i ->
|
||||||
Task.async(fn ->
|
Task.async(fn ->
|
||||||
Member
|
{:ok, member} =
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
Mv.Membership.create_member(
|
||||||
first_name: "Test#{i}",
|
%{
|
||||||
last_name: "User#{i}",
|
first_name: "Test#{i}",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
last_name: "User#{i}",
|
||||||
join_date: ~D[2023-03-15],
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
membership_fee_type_id: fee_type.id
|
join_date: ~D[2023-03-15],
|
||||||
})
|
membership_fee_type_id: fee_type.id
|
||||||
|> Ash.create!(actor: actor)
|
},
|
||||||
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
|
member
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -184,16 +189,17 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do
|
||||||
setup_settings(true, actor)
|
setup_settings(true, actor)
|
||||||
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
fee_type = create_fee_type(%{interval: :yearly}, actor)
|
||||||
|
|
||||||
member =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2023-03-15],
|
join_date: ~D[2023-03-15],
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
initial_cycles = get_member_cycles(member.id, actor)
|
initial_cycles = get_member_cycles(member.id, actor)
|
||||||
initial_count = length(initial_cycles)
|
initial_count = length(initial_cycles)
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,8 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
|
|
|
||||||
188
test/mv/membership/custom_field_policies_test.exs
Normal file
188
test/mv/membership/custom_field_policies_test.exs
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
defmodule Mv.Membership.CustomFieldPoliciesTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for CustomField resource authorization policies.
|
||||||
|
|
||||||
|
Verifies that all authenticated users with a valid role can read custom fields,
|
||||||
|
and only admin can create/update/destroy custom fields.
|
||||||
|
"""
|
||||||
|
use Mv.DataCase, async: false
|
||||||
|
|
||||||
|
alias Mv.Membership.CustomField
|
||||||
|
alias Mv.Accounts
|
||||||
|
alias Mv.Authorization
|
||||||
|
|
||||||
|
setup do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
%{actor: system_actor}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_role_with_permission_set(permission_set_name, actor) do
|
||||||
|
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
||||||
|
|
||||||
|
case Authorization.create_role(
|
||||||
|
%{
|
||||||
|
name: role_name,
|
||||||
|
description: "Test role for #{permission_set_name}",
|
||||||
|
permission_set_name: permission_set_name
|
||||||
|
},
|
||||||
|
actor: actor
|
||||||
|
) do
|
||||||
|
{:ok, role} -> role
|
||||||
|
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_user_with_permission_set(permission_set_name, actor) do
|
||||||
|
role = create_role_with_permission_set(permission_set_name, actor)
|
||||||
|
|
||||||
|
{:ok, user} =
|
||||||
|
Accounts.User
|
||||||
|
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||||
|
email: "user#{System.unique_integer([:positive])}@example.com",
|
||||||
|
password: "testpassword123"
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: actor)
|
||||||
|
|
||||||
|
{:ok, user} =
|
||||||
|
user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||||
|
|> Ash.update(actor: actor)
|
||||||
|
|
||||||
|
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
|
||||||
|
user_with_role
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_custom_field(actor) do
|
||||||
|
{:ok, field} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "test_field_#{System.unique_integer([:positive])}",
|
||||||
|
value_type: :string
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: actor, domain: Mv.Membership)
|
||||||
|
|
||||||
|
field
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "read access (all roles)" do
|
||||||
|
test "user with own_data can read all custom fields", %{actor: actor} do
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
user = create_user_with_permission_set("own_data", actor)
|
||||||
|
|
||||||
|
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
|
||||||
|
ids = Enum.map(fields, & &1.id)
|
||||||
|
assert custom_field.id in ids
|
||||||
|
|
||||||
|
{:ok, fetched} = Ash.get(CustomField, custom_field.id, actor: user, domain: Mv.Membership)
|
||||||
|
assert fetched.id == custom_field.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user with read_only can read all custom fields", %{actor: actor} do
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
user = create_user_with_permission_set("read_only", actor)
|
||||||
|
|
||||||
|
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
|
||||||
|
ids = Enum.map(fields, & &1.id)
|
||||||
|
assert custom_field.id in ids
|
||||||
|
|
||||||
|
{:ok, fetched} = Ash.get(CustomField, custom_field.id, actor: user, domain: Mv.Membership)
|
||||||
|
assert fetched.id == custom_field.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user with normal_user can read all custom fields", %{actor: actor} do
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
user = create_user_with_permission_set("normal_user", actor)
|
||||||
|
|
||||||
|
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
|
||||||
|
ids = Enum.map(fields, & &1.id)
|
||||||
|
assert custom_field.id in ids
|
||||||
|
|
||||||
|
{:ok, fetched} = Ash.get(CustomField, custom_field.id, actor: user, domain: Mv.Membership)
|
||||||
|
assert fetched.id == custom_field.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user with admin can read all custom fields", %{actor: actor} do
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
user = create_user_with_permission_set("admin", actor)
|
||||||
|
|
||||||
|
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
|
||||||
|
ids = Enum.map(fields, & &1.id)
|
||||||
|
assert custom_field.id in ids
|
||||||
|
|
||||||
|
{:ok, fetched} = Ash.get(CustomField, custom_field.id, actor: user, domain: Mv.Membership)
|
||||||
|
assert fetched.id == custom_field.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "write access - non-admin cannot create/update/destroy" do
|
||||||
|
setup %{actor: actor} do
|
||||||
|
user = create_user_with_permission_set("normal_user", actor)
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
%{user: user, custom_field: custom_field}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot create custom field (forbidden)", %{user: user} do
|
||||||
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "forbidden_field_#{System.unique_integer([:positive])}",
|
||||||
|
value_type: :string
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: user, domain: Mv.Membership)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot update custom field (forbidden)", %{
|
||||||
|
user: user,
|
||||||
|
custom_field: custom_field
|
||||||
|
} do
|
||||||
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
|
custom_field
|
||||||
|
|> Ash.Changeset.for_update(:update, %{description: "Updated"})
|
||||||
|
|> Ash.update(actor: user, domain: Mv.Membership)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot destroy custom field (forbidden)", %{
|
||||||
|
user: user,
|
||||||
|
custom_field: custom_field
|
||||||
|
} do
|
||||||
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
|
Ash.destroy(custom_field, actor: user, domain: Mv.Membership)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "write access - admin can create/update/destroy" do
|
||||||
|
setup %{actor: actor} do
|
||||||
|
user = create_user_with_permission_set("admin", actor)
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
%{user: user, custom_field: custom_field}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can create custom field", %{user: user} do
|
||||||
|
name = "admin_field_#{System.unique_integer([:positive])}"
|
||||||
|
|
||||||
|
assert {:ok, %CustomField{} = field} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: name, value_type: :string})
|
||||||
|
|> Ash.create(actor: user, domain: Mv.Membership)
|
||||||
|
|
||||||
|
assert field.name == name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can update custom field", %{user: user, custom_field: custom_field} do
|
||||||
|
assert {:ok, updated} =
|
||||||
|
custom_field
|
||||||
|
|> Ash.Changeset.for_update(:update, %{description: "Admin updated"})
|
||||||
|
|> Ash.update(actor: user, domain: Mv.Membership)
|
||||||
|
|
||||||
|
assert updated.description == "Admin updated"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can destroy custom field", %{user: user, custom_field: custom_field} do
|
||||||
|
assert :ok = Ash.destroy(custom_field, actor: user, domain: Mv.Membership)
|
||||||
|
|
||||||
|
assert {:error, _} =
|
||||||
|
Ash.get(CustomField, custom_field.id, domain: Mv.Membership, actor: user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -9,7 +9,7 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
|
||||||
# async: false because we need database commits to be visible across queries
|
# async: false because we need database commits to be visible across queries
|
||||||
use Mv.DataCase, async: false
|
use Mv.DataCase, async: false
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
alias Mv.Membership.{CustomField, CustomFieldValue}
|
||||||
alias Mv.Accounts
|
alias Mv.Accounts
|
||||||
alias Mv.Authorization
|
alias Mv.Authorization
|
||||||
|
|
||||||
|
|
@ -62,13 +62,14 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
|
||||||
|
|
||||||
defp create_linked_member_for_user(user, actor) do
|
defp create_linked_member_for_user(user, actor) do
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Linked",
|
first_name: "Linked",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "linked#{System.unique_integer([:positive])}@example.com"
|
email: "linked#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: actor, return_notifications?: false)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
user
|
user
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|
@ -80,13 +81,14 @@ defmodule Mv.Membership.CustomFieldValuePoliciesTest do
|
||||||
|
|
||||||
defp create_unlinked_member(actor) do
|
defp create_unlinked_member(actor) do
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Unlinked",
|
first_name: "Unlinked",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
member
|
member
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,14 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
# NOTE: We need to ensure the member is actually persisted to the database
|
# NOTE: We need to ensure the member is actually persisted to the database
|
||||||
# before we try to link it. Ash may delay writes, so we explicitly return the struct.
|
# before we try to link it. Ash may delay writes, so we explicitly return the struct.
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Membership.Member
|
Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Linked",
|
first_name: "Linked",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "linked#{System.unique_integer([:positive])}@example.com"
|
email: "linked#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: admin, return_notifications?: false)
|
actor: admin
|
||||||
|
)
|
||||||
|
|
||||||
# Link member to user (User.member_id = member.id)
|
# Link member to user (User.member_id = member.id)
|
||||||
# We use force_change_attribute because the member already exists and we just
|
# We use force_change_attribute because the member already exists and we just
|
||||||
|
|
@ -108,13 +109,14 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
admin = create_admin_user(actor)
|
admin = create_admin_user(actor)
|
||||||
|
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Membership.Member
|
Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Unlinked",
|
first_name: "Unlinked",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: admin)
|
actor: admin
|
||||||
|
)
|
||||||
|
|
||||||
member
|
member
|
||||||
end
|
end
|
||||||
|
|
@ -145,9 +147,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
# Update is allowed via HasPermission check with :linked scope (not via special case)
|
# Update is allowed via HasPermission check with :linked scope (not via special case)
|
||||||
# The special case policy only applies to :read actions
|
# The special case policy only applies to :read actions
|
||||||
{:ok, updated_member} =
|
{:ok, updated_member} =
|
||||||
linked_member
|
Membership.update_member(linked_member, %{first_name: "Updated"}, actor: user)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
|
||||||
|> Ash.update(actor: user)
|
|
||||||
|
|
||||||
assert updated_member.first_name == "Updated"
|
assert updated_member.first_name == "Updated"
|
||||||
end
|
end
|
||||||
|
|
@ -168,11 +168,8 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
user: user,
|
user: user,
|
||||||
unlinked_member: unlinked_member
|
unlinked_member: unlinked_member
|
||||||
} do
|
} do
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
unlinked_member
|
Membership.update_member(unlinked_member, %{first_name: "Updated"}, actor: user)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
|
||||||
|> Ash.update!(actor: user)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "list members returns only linked member", %{
|
test "list members returns only linked member", %{
|
||||||
|
|
@ -187,15 +184,15 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cannot create member (returns forbidden)", %{user: user} do
|
test "cannot create member (returns forbidden)", %{user: user} do
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
Membership.Member
|
Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "New",
|
first_name: "New",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: user)
|
actor: user
|
||||||
end
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cannot destroy member (returns forbidden)", %{
|
test "cannot destroy member (returns forbidden)", %{
|
||||||
|
|
@ -245,26 +242,23 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cannot create member (returns forbidden)", %{user: user} do
|
test "cannot create member (returns forbidden)", %{user: user} do
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
Membership.Member
|
Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "New",
|
first_name: "New",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: user)
|
actor: user
|
||||||
end
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cannot update any member (returns forbidden)", %{
|
test "cannot update any member (returns forbidden)", %{
|
||||||
user: user,
|
user: user,
|
||||||
linked_member: linked_member
|
linked_member: linked_member
|
||||||
} do
|
} do
|
||||||
assert_raise Ash.Error.Forbidden, fn ->
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
linked_member
|
Membership.update_member(linked_member, %{first_name: "Updated"}, actor: user)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
|
||||||
|> Ash.update!(actor: user)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cannot destroy any member (returns forbidden)", %{
|
test "cannot destroy any member (returns forbidden)", %{
|
||||||
|
|
@ -305,22 +299,21 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
|
|
||||||
test "can create member", %{user: user} do
|
test "can create member", %{user: user} do
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Membership.Member
|
Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "New",
|
first_name: "New",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: user)
|
actor: user
|
||||||
|
)
|
||||||
|
|
||||||
assert member.first_name == "New"
|
assert member.first_name == "New"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can update any member", %{user: user, unlinked_member: unlinked_member} do
|
test "can update any member", %{user: user, unlinked_member: unlinked_member} do
|
||||||
{:ok, updated_member} =
|
{:ok, updated_member} =
|
||||||
unlinked_member
|
Membership.update_member(unlinked_member, %{first_name: "Updated"}, actor: user)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
|
||||||
|> Ash.update(actor: user)
|
|
||||||
|
|
||||||
assert updated_member.first_name == "Updated"
|
assert updated_member.first_name == "Updated"
|
||||||
end
|
end
|
||||||
|
|
@ -363,22 +356,21 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
|
|
||||||
test "can create member", %{user: user} do
|
test "can create member", %{user: user} do
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Membership.Member
|
Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "New",
|
first_name: "New",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: user)
|
actor: user
|
||||||
|
)
|
||||||
|
|
||||||
assert member.first_name == "New"
|
assert member.first_name == "New"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can update any member", %{user: user, unlinked_member: unlinked_member} do
|
test "can update any member", %{user: user, unlinked_member: unlinked_member} do
|
||||||
{:ok, updated_member} =
|
{:ok, updated_member} =
|
||||||
unlinked_member
|
Membership.update_member(unlinked_member, %{first_name: "Updated"}, actor: user)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
|
||||||
|> Ash.update(actor: user)
|
|
||||||
|
|
||||||
assert updated_member.first_name == "Updated"
|
assert updated_member.first_name == "Updated"
|
||||||
end
|
end
|
||||||
|
|
@ -456,9 +448,7 @@ defmodule Mv.Membership.MemberPoliciesTest do
|
||||||
|
|
||||||
# Should succeed via HasPermission check (not special case)
|
# Should succeed via HasPermission check (not special case)
|
||||||
{:ok, updated_member} =
|
{:ok, updated_member} =
|
||||||
linked_member
|
Membership.update_member(linked_member, %{first_name: "Updated"}, actor: user)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
|
||||||
|> Ash.update(actor: user)
|
|
||||||
|
|
||||||
assert updated_member.first_name == "Updated"
|
assert updated_member.first_name == "Updated"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,8 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a member and explicitly generate cycles with a fixed "today" date.
|
# Helper to create a member and explicitly generate cycles with a fixed "today" date.
|
||||||
|
|
@ -74,9 +72,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
||||||
# Assign fee type if provided (this will trigger auto-generation with real today)
|
# Assign fee type if provided (this will trigger auto-generation with real today)
|
||||||
member =
|
member =
|
||||||
if fee_type_id do
|
if fee_type_id do
|
||||||
member
|
{:ok, updated} =
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type_id})
|
Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type_id},
|
||||||
|> Ash.update!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
else
|
else
|
||||||
member
|
member
|
||||||
end
|
end
|
||||||
|
|
@ -130,10 +131,8 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
# Assign fee type
|
# Assign fee type
|
||||||
member =
|
{:ok, member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|
||||||
|> Ash.update!(actor: actor)
|
|
||||||
|
|
||||||
# Explicitly generate cycles with fixed "today" date
|
# Explicitly generate cycles with fixed "today" date
|
||||||
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
||||||
|
|
@ -163,10 +162,8 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
# Assign fee type
|
# Assign fee type
|
||||||
member =
|
{:ok, member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|
||||||
|> Ash.update!(actor: actor)
|
|
||||||
|
|
||||||
# Explicitly generate cycles with fixed "today" date
|
# Explicitly generate cycles with fixed "today" date
|
||||||
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
|
||||||
|
|
@ -349,10 +346,8 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
||||||
|> Ash.create!(actor: actor)
|
|> Ash.create!(actor: actor)
|
||||||
|
|
||||||
# Now assign fee type
|
# Now assign fee type
|
||||||
member =
|
{:ok, member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|
||||||
|> Ash.update!(actor: actor)
|
|
||||||
|
|
||||||
# Explicitly generate cycles with fixed "today" date
|
# Explicitly generate cycles with fixed "today" date
|
||||||
today = ~D[2024-06-15]
|
today = ~D[2024-06-15]
|
||||||
|
|
@ -616,13 +611,15 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
# Now assign fee type (simulating a retroactive assignment)
|
# Now assign fee type (simulating a retroactive assignment)
|
||||||
member =
|
{:ok, member} =
|
||||||
member
|
Mv.Membership.update_member(
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
member,
|
||||||
membership_fee_type_id: fee_type.id,
|
%{
|
||||||
membership_fee_start_date: ~D[2021-01-01]
|
membership_fee_type_id: fee_type.id,
|
||||||
})
|
membership_fee_start_date: ~D[2021-01-01]
|
||||||
|> Ash.update!(actor: actor)
|
},
|
||||||
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Run batch generation with a "today" date after the member left
|
# Run batch generation with a "today" date after the member left
|
||||||
today = ~D[2024-06-15]
|
today = ~D[2024-06-15]
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,8 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to set up settings with specific include_joining_cycle value
|
# Helper to set up settings with specific include_joining_cycle value
|
||||||
|
|
@ -81,8 +79,12 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
||||||
# Assign fee type
|
# Assign fee type
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|> then(fn m ->
|
||||||
|> Ash.update!(actor: actor)
|
{:ok, updated} =
|
||||||
|
Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Explicitly generate cycles with fixed "today" date to avoid date dependency
|
# Explicitly generate cycles with fixed "today" date to avoid date dependency
|
||||||
today = ~D[2024-06-15]
|
today = ~D[2024-06-15]
|
||||||
|
|
@ -128,8 +130,12 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
||||||
# Now assign fee type to member
|
# Now assign fee type to member
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|> then(fn m ->
|
||||||
|> Ash.update!(actor: actor)
|
{:ok, updated} =
|
||||||
|
Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Generate cycles with specific "today" date
|
# Generate cycles with specific "today" date
|
||||||
today = ~D[2024-06-15]
|
today = ~D[2024-06-15]
|
||||||
|
|
@ -237,8 +243,12 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
|
||||||
# start from the last existing cycle (2023) and go forward
|
# start from the last existing cycle (2023) and go forward
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|> then(fn m ->
|
||||||
|> Ash.update!(actor: actor)
|
{:ok, updated} =
|
||||||
|
Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Verify gap was NOT filled and new cycles were generated from last existing
|
# Verify gap was NOT filled and new cycles were generated from last existing
|
||||||
all_cycles = get_member_cycles(member.id, actor)
|
all_cycles = get_member_cycles(member.id, actor)
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ defmodule MvWeb.Components.MemberFilterComponentTest do
|
||||||
|
|
||||||
alias Mv.Membership.CustomField
|
alias Mv.Membership.CustomField
|
||||||
|
|
||||||
# Helper to create a boolean custom field
|
# Helper to create a boolean custom field (uses system_actor - only admin can create)
|
||||||
defp create_boolean_custom_field(attrs \\ %{}) do
|
defp create_boolean_custom_field(attrs \\ %{}) do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
default_attrs = %{
|
default_attrs = %{
|
||||||
name: "test_boolean_#{System.unique_integer([:positive])}",
|
name: "test_boolean_#{System.unique_integer([:positive])}",
|
||||||
value_type: :boolean
|
value_type: :boolean
|
||||||
|
|
@ -27,11 +29,13 @@ defmodule MvWeb.Components.MemberFilterComponentTest do
|
||||||
|
|
||||||
CustomField
|
CustomField
|
||||||
|> Ash.Changeset.for_create(:create, attrs)
|
|> Ash.Changeset.for_create(:create, attrs)
|
||||||
|> Ash.create!()
|
|> Ash.create!(actor: system_actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a non-boolean custom field
|
# Helper to create a non-boolean custom field (uses system_actor - only admin can create)
|
||||||
defp create_string_custom_field(attrs \\ %{}) do
|
defp create_string_custom_field(attrs \\ %{}) do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
default_attrs = %{
|
default_attrs = %{
|
||||||
name: "test_string_#{System.unique_integer([:positive])}",
|
name: "test_string_#{System.unique_integer([:positive])}",
|
||||||
value_type: :string
|
value_type: :string
|
||||||
|
|
@ -41,7 +45,7 @@ defmodule MvWeb.Components.MemberFilterComponentTest do
|
||||||
|
|
||||||
CustomField
|
CustomField
|
||||||
|> Ash.Changeset.for_create(:create, attrs)
|
|> Ash.Changeset.for_create(:create, attrs)
|
||||||
|> Ash.create!()
|
|> Ash.create!(actor: system_actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "rendering" do
|
describe "rendering" do
|
||||||
|
|
|
||||||
|
|
@ -80,21 +80,20 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
||||||
|> Ash.create!(actor: actor)
|
|> Ash.create!(actor: actor)
|
||||||
|
|
||||||
# Create member without fee type first to avoid auto-generation
|
# Create member without fee type first to avoid auto-generation
|
||||||
member =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2022-01-01]
|
join_date: ~D[2022-01-01]
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Assign fee type after member creation (this may generate cycles, but we'll create our own)
|
# Assign fee type after member creation (this may generate cycles, but we'll create our own)
|
||||||
member =
|
{:ok, member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|
||||||
|> Ash.update!(actor: actor)
|
|
||||||
|
|
||||||
# Delete any auto-generated cycles first
|
# Delete any auto-generated cycles first
|
||||||
cycles =
|
cycles =
|
||||||
|
|
@ -151,20 +150,19 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
||||||
|> Ash.create!(actor: actor)
|
|> Ash.create!(actor: actor)
|
||||||
|
|
||||||
# Create member without fee type first
|
# Create member without fee type first
|
||||||
member =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
email: "test#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Assign fee type
|
# Assign fee type
|
||||||
member =
|
{:ok, member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|
||||||
|> Ash.update!(actor: actor)
|
|
||||||
|
|
||||||
# Delete any auto-generated cycles
|
# Delete any auto-generated cycles
|
||||||
cycles =
|
cycles =
|
||||||
|
|
@ -197,21 +195,20 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
||||||
|> Ash.create!(actor: actor)
|
|> Ash.create!(actor: actor)
|
||||||
|
|
||||||
# Create member without fee type first
|
# Create member without fee type first
|
||||||
member =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
join_date: ~D[2023-01-01]
|
join_date: ~D[2023-01-01]
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: actor)
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Assign fee type
|
# Assign fee type
|
||||||
member =
|
{:ok, member} =
|
||||||
member
|
Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|
||||||
|> Ash.update!(actor: actor)
|
|
||||||
|
|
||||||
# Delete any auto-generated cycles
|
# Delete any auto-generated cycles
|
||||||
cycles =
|
cycles =
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,15 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
||||||
use MvWeb.ConnCase, async: true
|
use MvWeb.ConnCase, async: true
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
admin_role = Mv.Fixtures.role_fixture("admin")
|
||||||
|
|
||||||
# Create admin user for testing
|
# Create admin user for testing (must have admin role to read/manage CustomField)
|
||||||
{:ok, user} =
|
{:ok, user} =
|
||||||
Mv.Accounts.User
|
Mv.Accounts.User
|
||||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||||
|
|
@ -30,8 +32,18 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
||||||
})
|
})
|
||||||
|> Ash.create(actor: system_actor)
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
conn = log_in_user(build_conn(), user)
|
{:ok, user} =
|
||||||
%{conn: conn, user: user}
|
user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||||
|
|> Ash.update(actor: system_actor)
|
||||||
|
|
||||||
|
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts, actor: system_actor)
|
||||||
|
conn = log_in_user(build_conn(), user_with_role)
|
||||||
|
# Use English locale so "Delete" link text matches in assertions
|
||||||
|
session = conn.private[:plug_session] || %{}
|
||||||
|
conn = Plug.Test.init_test_session(conn, Map.put(session, "locale", "en"))
|
||||||
|
%{conn: conn, user: user_with_role}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete button and modal" do
|
describe "delete button and modal" do
|
||||||
|
|
@ -107,9 +119,8 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
||||||
|> element("#delete-custom-field-modal form")
|
|> element("#delete-custom-field-modal form")
|
||||||
|> render_change(%{"slug" => custom_field.slug})
|
|> render_change(%{"slug" => custom_field.slug})
|
||||||
|
|
||||||
# Confirm button should be enabled now (no disabled attribute)
|
# Confirm button should be enabled now (no disabled attribute on the confirm button)
|
||||||
html = render(view)
|
refute has_element?(view, "#delete-custom-field-modal button.btn-error[disabled]")
|
||||||
refute html =~ ~r/disabled(?:=""|(?!\w))/
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "delete button is disabled when slug doesn't match", %{conn: conn} do
|
test "delete button is disabled when slug doesn't match", %{conn: conn} do
|
||||||
|
|
@ -126,9 +137,8 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
||||||
|> element("#delete-custom-field-modal form")
|
|> element("#delete-custom-field-modal form")
|
||||||
|> render_change(%{"slug" => "wrong-slug"})
|
|> render_change(%{"slug" => "wrong-slug"})
|
||||||
|
|
||||||
# Button should be disabled
|
# Confirm button should be disabled
|
||||||
html = render(view)
|
assert has_element?(view, "#delete-custom-field-modal button.btn-error[disabled]")
|
||||||
assert html =~ ~r/disabled(?:=""|(?!\w))/
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -186,10 +196,8 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
||||||
|> element("#delete-custom-field-modal form")
|
|> element("#delete-custom-field-modal form")
|
||||||
|> render_change(%{"slug" => "wrong-slug"})
|
|> render_change(%{"slug" => "wrong-slug"})
|
||||||
|
|
||||||
# Button should be disabled and we cannot click it
|
# Confirm button should be disabled and we cannot click it
|
||||||
# The test verifies that the button is properly disabled in the UI
|
assert has_element?(view, "#delete-custom-field-modal button.btn-error[disabled]")
|
||||||
html = render(view)
|
|
||||||
assert html =~ ~r/disabled(?:=""|(?!\w))/
|
|
||||||
|
|
||||||
# Custom field should still exist since deletion couldn't proceed
|
# Custom field should still exist since deletion couldn't proceed
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
@ -224,17 +232,58 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper functions
|
describe "create custom field" do
|
||||||
|
test "submitting new data field form creates custom field and shows success", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||||
|
|
||||||
|
# Open "New Data Field" form
|
||||||
|
view
|
||||||
|
|> element("#custom-fields-component button", "New Data Field")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Form is visible; submit with valid data
|
||||||
|
form_params = %{
|
||||||
|
"custom_field" => %{
|
||||||
|
"name" => "Created via Form",
|
||||||
|
"value_type" => "string",
|
||||||
|
"description" => "",
|
||||||
|
"required" => "false",
|
||||||
|
"show_in_overview" => "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view
|
||||||
|
|> form("#custom-field-form-new-form", form_params)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
# Success flash (FormComponent needs actor from parent; without it KeyError would occur)
|
||||||
|
assert render(view) =~ "successfully"
|
||||||
|
|
||||||
|
# Custom field was created in DB
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
search_name = "Created via Form"
|
||||||
|
|
||||||
|
[custom_field] =
|
||||||
|
Mv.Membership.CustomField
|
||||||
|
|> Ash.Query.filter(name == ^search_name)
|
||||||
|
|> Ash.read!(actor: system_actor)
|
||||||
|
|
||||||
|
assert custom_field.value_type == :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper functions (use code interface so actor is in validation context)
|
||||||
defp create_member do
|
defp create_member do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "User#{System.unique_integer([:positive])}",
|
last_name: "User#{System.unique_integer([:positive])}",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
email: "test#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_custom_field(name, value_type) do
|
defp create_custom_field(name, value_type) do
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
alias Mv.Membership.Member
|
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
|
@ -55,10 +54,8 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: system_actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: system_actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "create form" do
|
describe "create form" do
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
alias Mv.Membership.Member
|
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
|
@ -31,7 +30,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a member
|
# Helper to create a member
|
||||||
# Uses admin actor from global setup to ensure authorization
|
# Uses admin actor from global setup to ensure authorization; falls back to system_actor when nil
|
||||||
defp create_member(attrs, actor) do
|
defp create_member(attrs, actor) do
|
||||||
default_attrs = %{
|
default_attrs = %{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
|
|
@ -40,12 +39,9 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
effective_actor = actor || Mv.Helpers.SystemActor.get_system_actor()
|
||||||
opts = if actor, do: [actor: actor], else: []
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: effective_actor)
|
||||||
|
member
|
||||||
Member
|
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(opts)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "list display" do
|
describe "list display" do
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,10 @@ defmodule MvWeb.UserLive.ShowTest do
|
||||||
|
|
||||||
# Create member
|
# Create member
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Smith", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Smith",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create user and link to member
|
# Create user and link to member
|
||||||
user = create_test_user(%{email: "user@example.com"})
|
user = create_test_user(%{email: "user@example.com"})
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Mv.Membership.Member
|
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
describe "error handling - flash messages" do
|
describe "error handling - flash messages" do
|
||||||
|
|
@ -16,13 +14,14 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
|
||||||
|
|
||||||
# Create a member with the same email to trigger uniqueness error
|
# Create a member with the same email to trigger uniqueness error
|
||||||
{:ok, _existing_member} =
|
{:ok, _existing_member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Existing",
|
first_name: "Existing",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "duplicate@example.com"
|
email: "duplicate@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members/new")
|
{:ok, view, _html} = live(conn, "/members/new")
|
||||||
|
|
@ -79,23 +78,21 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
|
||||||
|
|
||||||
# Create a member to edit
|
# Create a member to edit
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Original",
|
first_name: "Original",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "original@example.com"
|
email: "original@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Create another member with different email
|
# Create another member with different email
|
||||||
{:ok, _other_member} =
|
{:ok, _other_member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Other", last_name: "Member", email: "other@example.com"},
|
||||||
first_name: "Other",
|
actor: system_actor
|
||||||
last_name: "Member",
|
)
|
||||||
email: "other@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
|
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Mv.Membership.Member
|
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
@ -37,10 +36,8 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: admin_user)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: admin_user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "membership fee type dropdown" do
|
describe "membership fee type dropdown" do
|
||||||
|
|
@ -129,7 +126,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
|
|
||||||
# Verify member was created with fee type - use admin_user to test permissions
|
# Verify member was created with fee type - use admin_user to test permissions
|
||||||
member =
|
member =
|
||||||
Member
|
Mv.Membership.Member
|
||||||
|> Ash.Query.filter(email == ^form_data["member[email]"])
|
|> Ash.Query.filter(email == ^form_data["member[email]"])
|
||||||
|> Ash.read_one!(actor: admin_user)
|
|> Ash.read_one!(actor: admin_user)
|
||||||
|
|
||||||
|
|
@ -177,14 +174,15 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
|
|
||||||
# Create member with fee type 1 and custom field value
|
# Create member with fee type 1 and custom field value
|
||||||
member =
|
member =
|
||||||
Member
|
create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
membership_fee_type_id: fee_type1.id
|
membership_fee_type_id: fee_type1.id
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: admin_user)
|
admin_user
|
||||||
|
)
|
||||||
|
|
||||||
# Add custom field value
|
# Add custom field value
|
||||||
Mv.Membership.CustomFieldValue
|
Mv.Membership.CustomFieldValue
|
||||||
|
|
@ -222,14 +220,15 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
|
|
||||||
# Create member with date custom field value
|
# Create member with date custom field value
|
||||||
member =
|
member =
|
||||||
Member
|
create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: admin_user)
|
admin_user
|
||||||
|
)
|
||||||
|
|
||||||
test_date = ~D[2024-01-15]
|
test_date = ~D[2024-01-15]
|
||||||
|
|
||||||
|
|
@ -269,14 +268,15 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
|
|
||||||
# Create member with custom field value
|
# Create member with custom field value
|
||||||
member =
|
member =
|
||||||
Member
|
create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
})
|
},
|
||||||
|> Ash.create!(actor: admin_user)
|
admin_user
|
||||||
|
)
|
||||||
|
|
||||||
# Add custom field value
|
# Add custom field value
|
||||||
_cfv =
|
_cfv =
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,8 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: system_actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: system_actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
|
|
@ -106,8 +104,14 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
||||||
# Assign fee type
|
# Assign fee type
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|> then(fn m ->
|
||||||
|> Ash.update!(actor: system_actor)
|
{:ok, updated} =
|
||||||
|
Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Delete any auto-generated cycles
|
# Delete any auto-generated cycles
|
||||||
cycles =
|
cycles =
|
||||||
|
|
@ -151,8 +155,14 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
||||||
# Assign fee type
|
# Assign fee type
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|> then(fn m ->
|
||||||
|> Ash.update!(actor: system_actor)
|
{:ok, updated} =
|
||||||
|
Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Delete any auto-generated cycles
|
# Delete any auto-generated cycles
|
||||||
cycles =
|
cycles =
|
||||||
|
|
@ -192,8 +202,14 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
||||||
# Assign fee type
|
# Assign fee type
|
||||||
member =
|
member =
|
||||||
member
|
member
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|> then(fn m ->
|
||||||
|> Ash.update!(actor: system_actor)
|
{:ok, updated} =
|
||||||
|
Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
|
end)
|
||||||
|
|
||||||
# Delete any auto-generated cycles
|
# Delete any auto-generated cycles
|
||||||
cycles =
|
cycles =
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,17 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
alias Mv.Membership.{CustomField, CustomFieldValue}
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
# Create test member
|
# Create test member
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Anderson",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create custom field with show_in_overview: true
|
# Create custom field with show_in_overview: true
|
||||||
{:ok, field} =
|
{:ok, field} =
|
||||||
|
|
|
||||||
|
|
@ -14,29 +14,23 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
alias Mv.Membership.{CustomField, CustomFieldValue}
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
# Create test members
|
# Create test members
|
||||||
{:ok, member1} =
|
{:ok, member1} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Anderson",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member2} =
|
{:ok, member2} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
|
||||||
first_name: "Bob",
|
actor: system_actor
|
||||||
last_name: "Brown",
|
)
|
||||||
email: "bob@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create custom fields
|
# Create custom fields
|
||||||
{:ok, field_show_string} =
|
{:ok, field_show_string} =
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, Member}
|
alias Mv.Membership.CustomField
|
||||||
|
|
||||||
@tag :slow
|
@tag :slow
|
||||||
test "displays custom field column even when no members have values", %{conn: conn} do
|
test "displays custom field column even when no members have values", %{conn: conn} do
|
||||||
|
|
@ -18,22 +18,16 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
||||||
|
|
||||||
# Create test members without custom field values
|
# Create test members without custom field values
|
||||||
{:ok, _member1} =
|
{:ok, _member1} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Anderson",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, _member2} =
|
{:ok, _member2} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
|
||||||
first_name: "Bob",
|
actor: system_actor
|
||||||
last_name: "Brown",
|
)
|
||||||
email: "bob@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create custom field with show_in_overview: true but no values
|
# Create custom field with show_in_overview: true but no values
|
||||||
{:ok, field} =
|
{:ok, field} =
|
||||||
|
|
@ -58,13 +52,10 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
||||||
|
|
||||||
# Create test member
|
# Create test member
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Anderson",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create custom field
|
# Create custom field
|
||||||
{:ok, field} =
|
{:ok, field} =
|
||||||
|
|
@ -102,13 +93,10 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
||||||
|
|
||||||
# Create test member
|
# Create test member
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Anderson",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create multiple custom fields with show_in_overview: true
|
# Create multiple custom fields with show_in_overview: true
|
||||||
{:ok, field1} =
|
{:ok, field1} =
|
||||||
|
|
|
||||||
|
|
@ -13,38 +13,29 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
alias Mv.Membership.{CustomField, CustomFieldValue}
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
# Create test members
|
# Create test members
|
||||||
{:ok, member1} =
|
{:ok, member1} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Anderson",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member2} =
|
{:ok, member2} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
|
||||||
first_name: "Bob",
|
actor: system_actor
|
||||||
last_name: "Brown",
|
)
|
||||||
email: "bob@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member3} =
|
{:ok, member3} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"},
|
||||||
first_name: "Charlie",
|
actor: system_actor
|
||||||
last_name: "Clark",
|
)
|
||||||
email: "charlie@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create custom field with show_in_overview: true
|
# Create custom field with show_in_overview: true
|
||||||
{:ok, field_string} =
|
{:ok, field_string} =
|
||||||
|
|
@ -242,40 +233,28 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
||||||
|
|
||||||
# Create additional members with NULL and empty string values
|
# Create additional members with NULL and empty string values
|
||||||
{:ok, member_with_value} =
|
{:ok, member_with_value} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "WithValue", last_name: "Test", email: "withvalue@example.com"},
|
||||||
first_name: "WithValue",
|
actor: system_actor
|
||||||
last_name: "Test",
|
)
|
||||||
email: "withvalue@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member_with_empty} =
|
{:ok, member_with_empty} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "WithEmpty", last_name: "Test", email: "withempty@example.com"},
|
||||||
first_name: "WithEmpty",
|
actor: system_actor
|
||||||
last_name: "Test",
|
)
|
||||||
email: "withempty@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member_with_null} =
|
{:ok, member_with_null} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "WithNull", last_name: "Test", email: "withnull@example.com"},
|
||||||
first_name: "WithNull",
|
actor: system_actor
|
||||||
last_name: "Test",
|
)
|
||||||
email: "withnull@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member_with_another_value} =
|
{:ok, member_with_another_value} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "AnotherValue", last_name: "Test", email: "another@example.com"},
|
||||||
first_name: "AnotherValue",
|
actor: system_actor
|
||||||
last_name: "Test",
|
)
|
||||||
email: "another@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create custom field
|
# Create custom field
|
||||||
{:ok, field} =
|
{:ok, field} =
|
||||||
|
|
@ -355,40 +334,28 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
||||||
|
|
||||||
# Create additional members with NULL and empty string values
|
# Create additional members with NULL and empty string values
|
||||||
{:ok, member_with_value} =
|
{:ok, member_with_value} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "WithValue", last_name: "Test", email: "withvalue@example.com"},
|
||||||
first_name: "WithValue",
|
actor: system_actor
|
||||||
last_name: "Test",
|
)
|
||||||
email: "withvalue@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member_with_empty} =
|
{:ok, member_with_empty} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "WithEmpty", last_name: "Test", email: "withempty@example.com"},
|
||||||
first_name: "WithEmpty",
|
actor: system_actor
|
||||||
last_name: "Test",
|
)
|
||||||
email: "withempty@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member_with_null} =
|
{:ok, member_with_null} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "WithNull", last_name: "Test", email: "withnull@example.com"},
|
||||||
first_name: "WithNull",
|
actor: system_actor
|
||||||
last_name: "Test",
|
)
|
||||||
email: "withnull@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
{:ok, member_with_another_value} =
|
{:ok, member_with_another_value} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "AnotherValue", last_name: "Test", email: "another@example.com"},
|
||||||
first_name: "AnotherValue",
|
actor: system_actor
|
||||||
last_name: "Test",
|
)
|
||||||
email: "another@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
# Create custom field
|
# Create custom field
|
||||||
{:ok, field} =
|
{:ok, field} =
|
||||||
|
|
|
||||||
|
|
@ -16,33 +16,35 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
alias Mv.Membership.{CustomField, CustomFieldValue}
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
# Create test members
|
# Create test members
|
||||||
{:ok, member1} =
|
{:ok, member1} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Alice",
|
first_name: "Alice",
|
||||||
last_name: "Anderson",
|
last_name: "Anderson",
|
||||||
email: "alice@example.com",
|
email: "alice@example.com",
|
||||||
street: "Main St",
|
street: "Main St",
|
||||||
city: "Berlin"
|
city: "Berlin"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, member2} =
|
{:ok, member2} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Bob",
|
first_name: "Bob",
|
||||||
last_name: "Brown",
|
last_name: "Brown",
|
||||||
email: "bob@example.com",
|
email: "bob@example.com",
|
||||||
street: "Second St",
|
street: "Second St",
|
||||||
city: "Hamburg"
|
city: "Hamburg"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Create custom field
|
# Create custom field
|
||||||
{:ok, custom_field} =
|
{:ok, custom_field} =
|
||||||
|
|
|
||||||
|
|
@ -3,33 +3,29 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership.Member
|
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
{:ok, member1} =
|
{:ok, member1} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Alice",
|
first_name: "Alice",
|
||||||
last_name: "Anderson",
|
last_name: "Anderson",
|
||||||
email: "alice@example.com",
|
email: "alice@example.com",
|
||||||
street: "Main Street",
|
street: "Main Street",
|
||||||
house_number: "123",
|
house_number: "123",
|
||||||
postal_code: "12345",
|
postal_code: "12345",
|
||||||
city: "Berlin",
|
city: "Berlin",
|
||||||
join_date: ~D[2020-01-15]
|
join_date: ~D[2020-01-15]
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, member2} =
|
{:ok, member2} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
|
||||||
first_name: "Bob",
|
actor: system_actor
|
||||||
last_name: "Brown",
|
)
|
||||||
email: "bob@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
%{
|
%{
|
||||||
member1: member1,
|
member1: member1,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Mv.Membership.Member
|
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
alias Mv.MembershipFees.MembershipFeeCycle
|
alias Mv.MembershipFees.MembershipFeeCycle
|
||||||
|
|
||||||
|
|
@ -40,10 +39,8 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: system_actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: system_actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
|
|
|
||||||
|
|
@ -531,10 +531,8 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
||||||
Mv.Membership.Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "filter shows only members with paid status in last cycle", %{conn: conn} do
|
test "filter shows only members with paid status in last cycle", %{conn: conn} do
|
||||||
|
|
@ -750,8 +748,10 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
describe "boolean custom field filters" do
|
describe "boolean custom field filters" do
|
||||||
alias Mv.Membership.CustomField
|
alias Mv.Membership.CustomField
|
||||||
|
|
||||||
# Helper to create a boolean custom field
|
# Helper to create a boolean custom field (uses system actor for authorization)
|
||||||
defp create_boolean_custom_field(attrs \\ %{}) do
|
defp create_boolean_custom_field(attrs \\ %{}) do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
default_attrs = %{
|
default_attrs = %{
|
||||||
name: "test_boolean_#{System.unique_integer([:positive])}",
|
name: "test_boolean_#{System.unique_integer([:positive])}",
|
||||||
value_type: :boolean
|
value_type: :boolean
|
||||||
|
|
@ -761,11 +761,13 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
CustomField
|
CustomField
|
||||||
|> Ash.Changeset.for_create(:create, attrs)
|
|> Ash.Changeset.for_create(:create, attrs)
|
||||||
|> Ash.create!()
|
|> Ash.create!(actor: system_actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a non-boolean custom field
|
# Helper to create a non-boolean custom field (uses system actor for authorization)
|
||||||
defp create_string_custom_field(attrs \\ %{}) do
|
defp create_string_custom_field(attrs \\ %{}) do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
default_attrs = %{
|
default_attrs = %{
|
||||||
name: "test_string_#{System.unique_integer([:positive])}",
|
name: "test_string_#{System.unique_integer([:positive])}",
|
||||||
value_type: :string
|
value_type: :string
|
||||||
|
|
@ -775,7 +777,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
CustomField
|
CustomField
|
||||||
|> Ash.Changeset.for_create(:create, attrs)
|
|> Ash.Changeset.for_create(:create, attrs)
|
||||||
|> Ash.create!()
|
|> Ash.create!(actor: system_actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "mount initializes boolean_custom_field_filters as empty map", %{conn: conn} do
|
test "mount initializes boolean_custom_field_filters as empty map", %{conn: conn} do
|
||||||
|
|
@ -1016,6 +1018,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
test "handle_params removes filter when custom field is deleted", %{conn: conn} do
|
test "handle_params removes filter when custom field is deleted", %{conn: conn} do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
boolean_field = create_boolean_custom_field()
|
boolean_field = create_boolean_custom_field()
|
||||||
|
|
||||||
# Set up filter via URL
|
# Set up filter via URL
|
||||||
|
|
@ -1026,8 +1029,8 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
filters_before = state_before.socket.assigns.boolean_custom_field_filters
|
filters_before = state_before.socket.assigns.boolean_custom_field_filters
|
||||||
assert filters_before[boolean_field.id] == true
|
assert filters_before[boolean_field.id] == true
|
||||||
|
|
||||||
# Delete the custom field
|
# Delete the custom field (requires actor with destroy permission)
|
||||||
Ash.destroy!(boolean_field)
|
Ash.destroy!(boolean_field, actor: system_actor)
|
||||||
|
|
||||||
# Navigate again - filter should be removed since custom field no longer exists
|
# Navigate again - filter should be removed since custom field no longer exists
|
||||||
{:ok, view2, _html} =
|
{:ok, view2, _html} =
|
||||||
|
|
@ -1122,18 +1125,15 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
# Helper to create a member with a boolean custom field value
|
# Helper to create a member with a boolean custom field value
|
||||||
defp create_member_with_boolean_value(member_attrs, custom_field, value, actor) do
|
defp create_member_with_boolean_value(member_attrs, custom_field, value, actor) do
|
||||||
{:ok, member} =
|
attrs =
|
||||||
Mv.Membership.Member
|
%{
|
||||||
|> Ash.Changeset.for_create(
|
first_name: "Test",
|
||||||
:create_member,
|
last_name: "Member",
|
||||||
%{
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
first_name: "Test",
|
}
|
||||||
last_name: "Member",
|
|> Map.merge(member_attrs)
|
||||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
|
||||||
}
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
||||||
|> Map.merge(member_attrs)
|
|
||||||
)
|
|
||||||
|> Ash.create(actor: actor)
|
|
||||||
|
|
||||||
{:ok, _cfv} =
|
{:ok, _cfv} =
|
||||||
Mv.Membership.CustomFieldValue
|
Mv.Membership.CustomFieldValue
|
||||||
|
|
@ -1177,13 +1177,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
boolean_field = create_boolean_custom_field()
|
boolean_field = create_boolean_custom_field()
|
||||||
|
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Create CustomFieldValue with map format (Ash expects _union_type and _union_value)
|
# Create CustomFieldValue with map format (Ash expects _union_type and _union_value)
|
||||||
{:ok, _cfv} =
|
{:ok, _cfv} =
|
||||||
|
|
@ -1210,13 +1211,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
boolean_field = create_boolean_custom_field()
|
boolean_field = create_boolean_custom_field()
|
||||||
|
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Member has no custom field value for this field
|
# Member has no custom field value for this field
|
||||||
member = member |> Ash.load!(:custom_field_values, actor: system_actor)
|
member = member |> Ash.load!(:custom_field_values, actor: system_actor)
|
||||||
|
|
@ -1233,13 +1235,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
boolean_field = create_boolean_custom_field()
|
boolean_field = create_boolean_custom_field()
|
||||||
|
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Create CustomFieldValue with nil value (edge case)
|
# Create CustomFieldValue with nil value (edge case)
|
||||||
{:ok, _cfv} =
|
{:ok, _cfv} =
|
||||||
|
|
@ -1266,13 +1269,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
boolean_field = create_boolean_custom_field()
|
boolean_field = create_boolean_custom_field()
|
||||||
|
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Create string custom field value (not boolean)
|
# Create string custom field value (not boolean)
|
||||||
{:ok, _cfv} =
|
{:ok, _cfv} =
|
||||||
|
|
@ -1315,20 +1319,21 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, member_without_value} =
|
{:ok, member_without_value} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "NoValue",
|
first_name: "NoValue",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
member_without_value =
|
member_without_value =
|
||||||
member_without_value |> Ash.load!(:custom_field_values, actor: system_actor)
|
member_without_value |> Ash.load!(:custom_field_values, actor: system_actor)
|
||||||
|
|
||||||
members = [member_with_true, member_with_false, member_without_value]
|
members = [member_with_true, member_with_false, member_without_value]
|
||||||
filters = %{to_string(boolean_field.id) => true}
|
filters = %{to_string(boolean_field.id) => true}
|
||||||
all_custom_fields = Mv.Membership.CustomField |> Ash.read!()
|
all_custom_fields = Mv.Membership.CustomField |> Ash.read!(actor: system_actor)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
||||||
|
|
@ -1365,20 +1370,21 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, member_without_value} =
|
{:ok, member_without_value} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "NoValue",
|
first_name: "NoValue",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
member_without_value =
|
member_without_value =
|
||||||
member_without_value |> Ash.load!(:custom_field_values, actor: system_actor)
|
member_without_value |> Ash.load!(:custom_field_values, actor: system_actor)
|
||||||
|
|
||||||
members = [member_with_true, member_with_false, member_without_value]
|
members = [member_with_true, member_with_false, member_without_value]
|
||||||
filters = %{to_string(boolean_field.id) => false}
|
filters = %{to_string(boolean_field.id) => false}
|
||||||
all_custom_fields = Mv.Membership.CustomField |> Ash.read!()
|
all_custom_fields = Mv.Membership.CustomField |> Ash.read!(actor: system_actor)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
||||||
|
|
@ -1417,7 +1423,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
members = [member1, member2]
|
members = [member1, member2]
|
||||||
filters = %{}
|
filters = %{}
|
||||||
all_custom_fields = Mv.Membership.CustomField |> Ash.read!()
|
all_custom_fields = Mv.Membership.CustomField |> Ash.read!(actor: system_actor)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
||||||
|
|
@ -1442,13 +1448,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
# Member with both fields = true
|
# Member with both fields = true
|
||||||
{:ok, member_both_true} =
|
{:ok, member_both_true} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "BothTrue",
|
first_name: "BothTrue",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "bothtrue.member.#{System.unique_integer([:positive])}@example.com"
|
email: "bothtrue.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, _cfv1} =
|
{:ok, _cfv1} =
|
||||||
Mv.Membership.CustomFieldValue
|
Mv.Membership.CustomFieldValue
|
||||||
|
|
@ -1472,13 +1479,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
# Member with field1 = true, field2 = false
|
# Member with field1 = true, field2 = false
|
||||||
{:ok, member_mixed} =
|
{:ok, member_mixed} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "Mixed",
|
first_name: "Mixed",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "mixed.member.#{System.unique_integer([:positive])}@example.com"
|
email: "mixed.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, _cfv3} =
|
{:ok, _cfv3} =
|
||||||
Mv.Membership.CustomFieldValue
|
Mv.Membership.CustomFieldValue
|
||||||
|
|
@ -1507,7 +1515,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
to_string(boolean_field2.id) => true
|
to_string(boolean_field2.id) => true
|
||||||
}
|
}
|
||||||
|
|
||||||
all_custom_fields = Mv.Membership.CustomField |> Ash.read!()
|
all_custom_fields = Mv.Membership.CustomField |> Ash.read!(actor: system_actor)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
||||||
|
|
@ -1538,7 +1546,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
members = [member]
|
members = [member]
|
||||||
filters = %{fake_id => true}
|
filters = %{fake_id => true}
|
||||||
all_custom_fields = Mv.Membership.CustomField |> Ash.read!()
|
all_custom_fields = Mv.Membership.CustomField |> Ash.read!(actor: system_actor)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
MvWeb.MemberLive.Index.apply_boolean_custom_field_filters(
|
||||||
|
|
@ -1575,13 +1583,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, _member_without_value} =
|
{:ok, _member_without_value} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "NoValue",
|
first_name: "NoValue",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Test true filter
|
# Test true filter
|
||||||
{:ok, _view, html_true} =
|
{:ok, _view, html_true} =
|
||||||
|
|
@ -1610,14 +1619,15 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
# Member with true boolean value and paid status
|
# Member with true boolean value and paid status
|
||||||
{:ok, member_paid_true} =
|
{:ok, member_paid_true} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "PaidTrue",
|
first_name: "PaidTrue",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "paidtrue.member.#{System.unique_integer([:positive])}@example.com",
|
email: "paidtrue.member.#{System.unique_integer([:positive])}@example.com",
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, _cfv} =
|
{:ok, _cfv} =
|
||||||
Mv.Membership.CustomFieldValue
|
Mv.Membership.CustomFieldValue
|
||||||
|
|
@ -1637,14 +1647,15 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
# Member with true boolean value but unpaid status
|
# Member with true boolean value but unpaid status
|
||||||
{:ok, member_unpaid_true} =
|
{:ok, member_unpaid_true} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "UnpaidTrue",
|
first_name: "UnpaidTrue",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "unpaidtrue.member.#{System.unique_integer([:positive])}@example.com",
|
email: "unpaidtrue.member.#{System.unique_integer([:positive])}@example.com",
|
||||||
membership_fee_type_id: fee_type.id
|
membership_fee_type_id: fee_type.id
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, _cfv2} =
|
{:ok, _cfv2} =
|
||||||
Mv.Membership.CustomFieldValue
|
Mv.Membership.CustomFieldValue
|
||||||
|
|
@ -1725,13 +1736,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, _member_without_value} =
|
{:ok, _member_without_value} =
|
||||||
Mv.Membership.Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{
|
||||||
first_name: "NoValue",
|
first_name: "NoValue",
|
||||||
last_name: "Member",
|
last_name: "Member",
|
||||||
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
email: "novalue.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
},
|
||||||
|> Ash.create(actor: system_actor)
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
# Test that filter works even though field is not visible in overview
|
# Test that filter works even though field is not visible in overview
|
||||||
{:ok, _view, html_true} =
|
{:ok, _view, html_true} =
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Mv.Membership.Member
|
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
alias Mv.MembershipFees.MembershipFeeCycle
|
alias Mv.MembershipFees.MembershipFeeCycle
|
||||||
|
|
||||||
|
|
@ -40,10 +39,8 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: system_actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: system_actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "end-to-end workflows" do
|
describe "end-to-end workflows" do
|
||||||
|
|
@ -152,7 +149,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
member =
|
member =
|
||||||
Member
|
Mv.Membership.Member
|
||||||
|> Ash.Query.filter(email == ^form_data["member[email]"])
|
|> Ash.Query.filter(email == ^form_data["member[email]"])
|
||||||
|> Ash.read_one!(actor: system_actor)
|
|> Ash.read_one!(actor: system_actor)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Mv.Membership.Member
|
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
alias Mv.MembershipFees.MembershipFeeCycle
|
alias Mv.MembershipFees.MembershipFeeCycle
|
||||||
|
|
||||||
|
|
@ -40,10 +39,8 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = Map.merge(default_attrs, attrs)
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: system_actor)
|
||||||
Member
|
member
|
||||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
||||||
|> Ash.create!(actor: system_actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,17 @@ defmodule MvWeb.MemberLive.ShowTest do
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
use Gettext, backend: MvWeb.Gettext
|
use Gettext, backend: MvWeb.Gettext
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
alias Mv.Membership.{CustomField, CustomFieldValue}
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
# Create test member
|
# Create test member
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Member
|
Mv.Membership.create_member(
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
first_name: "Alice",
|
actor: system_actor
|
||||||
last_name: "Anderson",
|
)
|
||||||
email: "alice@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create(actor: system_actor)
|
|
||||||
|
|
||||||
%{member: member, actor: system_actor}
|
%{member: member, actor: system_actor}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue