145 lines
4.5 KiB
Elixir
145 lines
4.5 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: type not found"
|
|
|
|
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
|