1. Fix critical notifications bug 2. Fix today inconsistency 3. Add advisory lock around deletion 4. Improve helper function documentation 5. Improve error message UX
148 lines
4.6 KiB
Elixir
148 lines
4.6 KiB
Elixir
defmodule Mv.MembershipFees.Changes.ValidateSameInterval do
|
|
@moduledoc """
|
|
Validates that membership fee type changes only allow same-interval types.
|
|
|
|
Prevents changing from yearly to monthly, etc. (MVP constraint).
|
|
|
|
## Usage
|
|
|
|
In a Member action:
|
|
|
|
update :update_member do
|
|
# ...
|
|
change Mv.MembershipFees.Changes.ValidateSameInterval
|
|
end
|
|
|
|
The change module only executes when `membership_fee_type_id` is being changed.
|
|
If the new type has a different interval than the current type, a validation error is returned.
|
|
"""
|
|
use Ash.Resource.Change
|
|
|
|
@impl true
|
|
def change(changeset, _opts, _context) do
|
|
if changing_membership_fee_type?(changeset) do
|
|
validate_interval_match(changeset)
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
|
|
# Check if membership_fee_type_id is being changed
|
|
defp changing_membership_fee_type?(changeset) do
|
|
Ash.Changeset.changing_attribute?(changeset, :membership_fee_type_id)
|
|
end
|
|
|
|
# Validate that the new type has the same interval as the current type
|
|
defp validate_interval_match(changeset) do
|
|
current_type_id = get_current_type_id(changeset)
|
|
new_type_id = get_new_type_id(changeset)
|
|
|
|
cond do
|
|
# If no current type, allow any change (first assignment)
|
|
is_nil(current_type_id) ->
|
|
changeset
|
|
|
|
# If new type is nil, reject the change (membership_fee_type_id is required)
|
|
is_nil(new_type_id) ->
|
|
add_nil_type_error(changeset)
|
|
|
|
# Both types exist - validate intervals match
|
|
true ->
|
|
validate_intervals_match(changeset, current_type_id, new_type_id)
|
|
end
|
|
end
|
|
|
|
# Validates that intervals match when both types exist
|
|
defp validate_intervals_match(changeset, current_type_id, new_type_id) do
|
|
case get_intervals(current_type_id, new_type_id) do
|
|
{:ok, current_interval, new_interval} ->
|
|
if current_interval == new_interval do
|
|
changeset
|
|
else
|
|
add_interval_mismatch_error(changeset, current_interval, new_interval)
|
|
end
|
|
|
|
{:error, reason} ->
|
|
# Fail closed: If we can't load the types, reject the change
|
|
# This prevents inconsistent data states
|
|
add_type_validation_error(changeset, reason)
|
|
end
|
|
end
|
|
|
|
# Get current type ID from changeset data
|
|
defp get_current_type_id(changeset) do
|
|
case changeset.data do
|
|
%{membership_fee_type_id: type_id} -> type_id
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
# Get new type ID from changeset
|
|
defp get_new_type_id(changeset) do
|
|
case Ash.Changeset.fetch_change(changeset, :membership_fee_type_id) do
|
|
{:ok, type_id} -> type_id
|
|
:error -> nil
|
|
end
|
|
end
|
|
|
|
# Get intervals for both types
|
|
defp get_intervals(current_type_id, new_type_id) do
|
|
alias Mv.MembershipFees.MembershipFeeType
|
|
|
|
case {Ash.get(MembershipFeeType, current_type_id), Ash.get(MembershipFeeType, new_type_id)} do
|
|
{{:ok, current_type}, {:ok, new_type}} ->
|
|
{:ok, current_type.interval, new_type.interval}
|
|
|
|
_ ->
|
|
{:error, :type_not_found}
|
|
end
|
|
end
|
|
|
|
# Add validation error for interval mismatch
|
|
defp add_interval_mismatch_error(changeset, current_interval, new_interval) do
|
|
current_interval_name = format_interval(current_interval)
|
|
new_interval_name = format_interval(new_interval)
|
|
|
|
message =
|
|
"Cannot change membership fee type: current type uses #{current_interval_name} interval, " <>
|
|
"new type uses #{new_interval_name} interval. Only same-interval changes are allowed."
|
|
|
|
Ash.Changeset.add_error(
|
|
changeset,
|
|
field: :membership_fee_type_id,
|
|
message: message
|
|
)
|
|
end
|
|
|
|
# Add validation error when types cannot be loaded
|
|
defp add_type_validation_error(changeset, _reason) do
|
|
message =
|
|
"Could not validate membership fee type intervals. " <>
|
|
"The current or new membership fee type no longer exists. " <>
|
|
"This may indicate a data consistency issue."
|
|
|
|
Ash.Changeset.add_error(
|
|
changeset,
|
|
field: :membership_fee_type_id,
|
|
message: message
|
|
)
|
|
end
|
|
|
|
# Add validation error when trying to set membership_fee_type_id to nil
|
|
defp add_nil_type_error(changeset) do
|
|
message = "Cannot remove membership fee type. A membership fee type is required."
|
|
|
|
Ash.Changeset.add_error(
|
|
changeset,
|
|
field: :membership_fee_type_id,
|
|
message: message
|
|
)
|
|
end
|
|
|
|
# Format interval atom to human-readable string
|
|
defp format_interval(:monthly), do: "monthly"
|
|
defp format_interval(:quarterly), do: "quarterly"
|
|
defp format_interval(:half_yearly), do: "half-yearly"
|
|
defp format_interval(:yearly), do: "yearly"
|
|
defp format_interval(interval), do: to_string(interval)
|
|
end
|