Replace the create_fee_type/create_cycle helpers duplicated across 18/8 membership-fee test files with a single shared definition in Mv.Fixtures, reconciling the divergent local signatures (including the reversed argument order) into one superset so behavior is unchanged.
213 lines
7.2 KiB
Elixir
213 lines
7.2 KiB
Elixir
defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
|
|
@moduledoc """
|
|
Tests for ValidateSameInterval change module.
|
|
"""
|
|
use Mv.DataCase, async: true
|
|
|
|
import Mv.Fixtures, only: [create_fee_type: 2]
|
|
|
|
alias Mv.MembershipFees.Changes.ValidateSameInterval
|
|
|
|
setup do
|
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
|
%{actor: system_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
|