- setting.ex: domain/authorize for default_membership_fee_type_id check - validate_same_interval: require membership_fee_type (no None) - set_membership_fee_start_date: domain/actor for fee type lookup - Validations: domain/authorize for cross-resource checks - helpers.ex, email_sync change, seeds.exs actor/authorize fixes - Update related tests
227 lines
7.5 KiB
Elixir
227 lines
7.5 KiB
Elixir
defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|
@moduledoc """
|
|
Tests for ValidateSameInterval change module.
|
|
"""
|
|
use Mv.DataCase, async: true
|
|
|
|
alias Mv.MembershipFees.MembershipFeeType
|
|
alias Mv.MembershipFees.Changes.ValidateSameInterval
|
|
|
|
setup do
|
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
%{actor: system_actor}
|
|
end
|
|
|
|
# Helper to create a membership fee type
|
|
defp create_fee_type(attrs, actor) do
|
|
default_attrs = %{
|
|
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
|
amount: Decimal.new("50.00"),
|
|
interval: :yearly
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
|
|
MembershipFeeType
|
|
|> Ash.Changeset.for_create(:create, attrs)
|
|
|> Ash.create!(actor: actor)
|
|
end
|
|
|
|
# Helper to create a member
|
|
defp create_member(attrs, actor) do
|
|
default_attrs = %{
|
|
first_name: "Test",
|
|
last_name: "Member",
|
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
{:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
|
|
member
|
|
end
|
|
|
|
describe "validate_interval_match/1" do
|
|
test "allows change to type with same interval", %{actor: actor} do
|
|
yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"}, actor)
|
|
yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"}, actor)
|
|
|
|
member = create_member(%{membership_fee_type_id: yearly_type1.id}, actor)
|
|
|
|
changeset =
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: yearly_type2.id},
|
|
actor: actor
|
|
)
|
|
|> ValidateSameInterval.change(%{}, %{actor: actor})
|
|
|
|
assert changeset.valid?
|
|
end
|
|
|
|
test "prevents change to type with different interval", %{actor: actor} do
|
|
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
|
monthly_type = create_fee_type(%{interval: :monthly}, actor)
|
|
|
|
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
|
|
|
changeset =
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: monthly_type.id},
|
|
actor: actor
|
|
)
|
|
|> ValidateSameInterval.change(%{}, %{actor: actor})
|
|
|
|
refute changeset.valid?
|
|
assert %{errors: errors} = changeset
|
|
|
|
assert Enum.any?(errors, fn error ->
|
|
error.field == :membership_fee_type_id and
|
|
error.message =~ "yearly" and
|
|
error.message =~ "monthly"
|
|
end)
|
|
end
|
|
|
|
test "allows first assignment of membership fee type", %{actor: actor} do
|
|
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
|
# No fee type assigned
|
|
member = create_member(%{}, actor)
|
|
|
|
changeset =
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: yearly_type.id},
|
|
actor: actor
|
|
)
|
|
|> ValidateSameInterval.change(%{}, %{actor: actor})
|
|
|
|
assert changeset.valid?
|
|
end
|
|
|
|
test "prevents removal of membership fee type", %{actor: actor} do
|
|
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
|
|
|
changeset =
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: nil}, actor: actor)
|
|
|> ValidateSameInterval.change(%{}, %{actor: actor})
|
|
|
|
refute changeset.valid?
|
|
assert %{errors: errors} = changeset
|
|
|
|
assert Enum.any?(errors, fn error ->
|
|
error.field == :membership_fee_type_id and
|
|
error.message =~ "Cannot remove membership fee type"
|
|
end)
|
|
end
|
|
|
|
test "does nothing when membership_fee_type_id is not changed", %{actor: actor} do
|
|
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
|
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
|
|
|
changeset =
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{first_name: "New Name"}, actor: actor)
|
|
|> ValidateSameInterval.change(%{}, %{actor: actor})
|
|
|
|
assert changeset.valid?
|
|
end
|
|
|
|
test "error message is clear and helpful", %{actor: actor} do
|
|
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
|
quarterly_type = create_fee_type(%{interval: :quarterly}, actor)
|
|
|
|
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
|
|
|
changeset =
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: quarterly_type.id},
|
|
actor: actor
|
|
)
|
|
|> ValidateSameInterval.change(%{}, %{actor: actor})
|
|
|
|
error = Enum.find(changeset.errors, &(&1.field == :membership_fee_type_id))
|
|
assert error.message =~ "yearly"
|
|
assert error.message =~ "quarterly"
|
|
assert error.message =~ "same-interval"
|
|
end
|
|
|
|
test "handles all interval types correctly", %{actor: actor} do
|
|
intervals = [:monthly, :quarterly, :half_yearly, :yearly]
|
|
|
|
for interval1 <- intervals,
|
|
interval2 <- intervals,
|
|
interval1 != interval2 do
|
|
type1 =
|
|
create_fee_type(
|
|
%{
|
|
interval: interval1,
|
|
name: "Type #{interval1} #{System.unique_integer([:positive])}"
|
|
},
|
|
actor
|
|
)
|
|
|
|
type2 =
|
|
create_fee_type(
|
|
%{
|
|
interval: interval2,
|
|
name: "Type #{interval2} #{System.unique_integer([:positive])}"
|
|
},
|
|
actor
|
|
)
|
|
|
|
member = create_member(%{membership_fee_type_id: type1.id}, actor)
|
|
|
|
changeset =
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: type2.id},
|
|
actor: actor
|
|
)
|
|
|> ValidateSameInterval.change(%{}, %{actor: actor})
|
|
|
|
refute changeset.valid?,
|
|
"Should prevent change from #{interval1} to #{interval2}"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "integration with update_member action" do
|
|
test "validation works when updating member via update_member action", %{actor: actor} do
|
|
yearly_type = create_fee_type(%{interval: :yearly}, actor)
|
|
monthly_type = create_fee_type(%{interval: :monthly}, actor)
|
|
|
|
member = create_member(%{membership_fee_type_id: yearly_type.id}, actor)
|
|
|
|
# Try to update member with different interval type
|
|
assert {:error, %Ash.Error.Invalid{} = error} =
|
|
Mv.Membership.update_member(member, %{membership_fee_type_id: monthly_type.id},
|
|
actor: actor
|
|
)
|
|
|
|
# Check that error is about interval mismatch
|
|
error_message = extract_error_message(error)
|
|
assert error_message =~ "yearly"
|
|
assert error_message =~ "monthly"
|
|
assert error_message =~ "same-interval"
|
|
end
|
|
|
|
test "allows update when interval matches", %{actor: actor} do
|
|
yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"}, actor)
|
|
yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"}, actor)
|
|
|
|
member = create_member(%{membership_fee_type_id: yearly_type1.id}, actor)
|
|
|
|
# Update member with same-interval type
|
|
assert {:ok, updated_member} =
|
|
Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
|
|
actor: actor
|
|
)
|
|
|
|
assert updated_member.membership_fee_type_id == yearly_type2.id
|
|
end
|
|
|
|
defp extract_error_message(%Ash.Error.Invalid{errors: errors}) do
|
|
errors
|
|
|> Enum.filter(&(&1.field == :membership_fee_type_id))
|
|
|> Enum.map_join(" ", & &1.message)
|
|
end
|
|
end
|
|
end
|