mitgliederverwaltung/test/membership_fees/changes/validate_same_interval_test.exs
Moritz 06324d77c5
Some checks failed
continuous-integration/drone/push Build is failing
feat: regenerate cycles when membership fee type changes (same interval)
- Implemented regenerate_cycles_on_type_change helper in Member resource
- Cycles that haven't ended yet (cycle_end >= today) are deleted and regenerated
- Paid and suspended cycles remain unchanged (not deleted)
- CycleGenerator reloads member with new membership_fee_type_id
- Adjusted tests to work with current cycles only (no future cycles)
- All integration tests passing

Phase 4 completed: Cycle regeneration on type change
2025-12-15 11:39:26 +01:00

222 lines
6.8 KiB
Elixir

defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
@moduledoc """
Tests for ValidateSameInterval change module.
"""
use Mv.DataCase, async: true
alias Mv.Membership.Member
alias Mv.MembershipFees.MembershipFeeType
alias Mv.MembershipFees.Changes.ValidateSameInterval
# Helper to create a membership fee type
defp create_fee_type(attrs) 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!()
end
# Helper to create a member
defp create_member(attrs) do
default_attrs = %{
first_name: "Test",
last_name: "Member",
email: "test.member.#{System.unique_integer([:positive])}@example.com"
}
attrs = Map.merge(default_attrs, attrs)
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
end
describe "validate_interval_match/1" do
test "allows change to type with same interval" do
yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"})
yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"})
member = create_member(%{membership_fee_type_id: yearly_type1.id})
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{
membership_fee_type_id: yearly_type2.id
})
|> ValidateSameInterval.change(%{}, %{})
assert changeset.valid?
end
test "prevents change to type with different interval" do
yearly_type = create_fee_type(%{interval: :yearly})
monthly_type = create_fee_type(%{interval: :monthly})
member = create_member(%{membership_fee_type_id: yearly_type.id})
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{
membership_fee_type_id: monthly_type.id
})
|> ValidateSameInterval.change(%{}, %{})
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" do
yearly_type = create_fee_type(%{interval: :yearly})
# No fee type assigned
member = create_member(%{})
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{
membership_fee_type_id: yearly_type.id
})
|> ValidateSameInterval.change(%{}, %{})
assert changeset.valid?
end
test "allows removal of membership fee type" do
yearly_type = create_fee_type(%{interval: :yearly})
member = create_member(%{membership_fee_type_id: yearly_type.id})
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{
membership_fee_type_id: nil
})
|> ValidateSameInterval.change(%{}, %{})
assert changeset.valid?
end
test "does nothing when membership_fee_type_id is not changed" do
yearly_type = create_fee_type(%{interval: :yearly})
member = create_member(%{membership_fee_type_id: yearly_type.id})
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{
first_name: "New Name"
})
|> ValidateSameInterval.change(%{}, %{})
assert changeset.valid?
end
test "error message is clear and helpful" do
yearly_type = create_fee_type(%{interval: :yearly})
quarterly_type = create_fee_type(%{interval: :quarterly})
member = create_member(%{membership_fee_type_id: yearly_type.id})
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{
membership_fee_type_id: quarterly_type.id
})
|> ValidateSameInterval.change(%{}, %{})
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" 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])}"
})
type2 =
create_fee_type(%{
interval: interval2,
name: "Type #{interval2} #{System.unique_integer([:positive])}"
})
member = create_member(%{membership_fee_type_id: type1.id})
changeset =
member
|> Ash.Changeset.for_update(:update_member, %{
membership_fee_type_id: type2.id
})
|> ValidateSameInterval.change(%{}, %{})
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" do
yearly_type = create_fee_type(%{interval: :yearly})
monthly_type = create_fee_type(%{interval: :monthly})
member = create_member(%{membership_fee_type_id: yearly_type.id})
# Try to update member with different interval type
assert {:error, %Ash.Error.Invalid{} = error} =
member
|> Ash.Changeset.for_update(:update_member, %{
membership_fee_type_id: monthly_type.id
})
|> Ash.update()
# 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" do
yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"})
yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"})
member = create_member(%{membership_fee_type_id: yearly_type1.id})
# Update member with same-interval type
assert {:ok, updated_member} =
member
|> Ash.Changeset.for_update(:update_member, %{
membership_fee_type_id: yearly_type2.id
})
|> Ash.update()
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(& &1.message)
|> Enum.join(" ")
end
end
end