Add actor parameter to all tests requiring authorization
All checks were successful
continuous-integration/drone/push Build is passing

This commit adds actor: system_actor to all Ash operations in tests that
require authorization.
This commit is contained in:
Moritz 2026-01-23 20:00:24 +01:00
parent 4c846f8bba
commit a6cdeaa18d
Signed by: moritz
GPG key ID: 1020A035E5DD0824
75 changed files with 4649 additions and 2865 deletions

View file

@ -19,8 +19,13 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
require Ash.Query
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) do
defp create_fee_type(attrs, actor) do
default_attrs = %{
name: "Test Fee Type #{System.unique_integer([:positive])}",
amount: Decimal.new("50.00"),
@ -31,12 +36,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: actor)
end
# Helper to create a member. Note: If membership_fee_type_id is provided,
# cycles will be auto-generated during creation in test environment.
defp create_member(attrs) do
defp create_member(attrs, actor) do
default_attrs = %{
first_name: "Test",
last_name: "User",
@ -47,7 +52,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: actor)
end
# Helper to create a member and explicitly generate cycles with a fixed "today" date.
@ -56,7 +61,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
# Note: We first create the member without fee_type_id, then assign it via update,
# which triggers the after_action hook. However, we then explicitly regenerate
# cycles with the fixed "today" date to ensure consistency.
defp create_member_with_cycles(attrs, today) do
defp create_member_with_cycles(attrs, today, actor) do
# Extract membership_fee_type_id if present
fee_type_id = Map.get(attrs, :membership_fee_type_id)
@ -64,14 +69,14 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
attrs_without_fee_type = Map.delete(attrs, :membership_fee_type_id)
member =
create_member(attrs_without_fee_type)
create_member(attrs_without_fee_type, actor)
# Assign fee type if provided (this will trigger auto-generation with real today)
member =
if fee_type_id do
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type_id})
|> Ash.update!()
|> Ash.update!(actor: actor)
else
member
end
@ -80,8 +85,8 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
# This ensures the test uses the fixed date, not the real current date
if fee_type_id && member.join_date do
# Delete any existing cycles first to ensure clean state
existing_cycles = get_member_cycles(member.id)
Enum.each(existing_cycles, &Ash.destroy!(&1))
existing_cycles = get_member_cycles(member.id, actor)
Enum.each(existing_cycles, &Ash.destroy!(&1, actor: actor))
# Generate cycles with fixed "today" date
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
@ -91,85 +96,91 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
end
# Helper to get cycles for a member
defp get_member_cycles(member_id) do
defp get_member_cycles(member_id, actor) do
MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member_id)
|> Ash.Query.sort(cycle_start: :asc)
|> Ash.read!()
|> Ash.read!(actor: actor)
end
# Helper to set up settings
defp setup_settings(include_joining_cycle) do
defp setup_settings(include_joining_cycle, actor) do
{:ok, settings} = Mv.Membership.get_settings()
settings
|> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle})
|> Ash.update!()
|> Ash.update!(actor: actor)
end
describe "member joins today" do
test "current cycle is generated (yearly)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "current cycle is generated (yearly)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
today = ~D[2024-06-15]
# Create member WITHOUT fee type first to avoid auto-generation with real today
member =
create_member(%{
join_date: today,
membership_fee_start_date: ~D[2024-01-01]
})
create_member(
%{
join_date: today,
membership_fee_start_date: ~D[2024-01-01]
},
actor
)
# Assign fee type
member =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Explicitly generate cycles with fixed "today" date
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
# Should have the current year's cycle
cycle_years = Enum.map(cycles, & &1.cycle_start.year)
assert 2024 in cycle_years
end
test "current cycle is generated (monthly)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :monthly})
test "current cycle is generated (monthly)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :monthly}, actor)
today = ~D[2024-06-15]
# Create member WITHOUT fee type first to avoid auto-generation with real today
member =
create_member(%{
join_date: today,
membership_fee_start_date: ~D[2024-06-01]
})
create_member(
%{
join_date: today,
membership_fee_start_date: ~D[2024-06-01]
},
actor
)
# Assign fee type
member =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Explicitly generate cycles with fixed "today" date
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
# Should have June 2024 cycle
assert Enum.any?(cycles, fn c -> c.cycle_start == ~D[2024-06-01] end)
end
test "current cycle is generated (quarterly)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :quarterly})
test "current cycle is generated (quarterly)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :quarterly}, actor)
today = ~D[2024-05-15]
@ -181,11 +192,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-04-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
# Should have Q2 2024 cycle
assert Enum.any?(cycles, fn c -> c.cycle_start == ~D[2024-04-01] end)
@ -193,9 +205,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
end
describe "member left yesterday" do
test "no future cycles are generated" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "no future cycles are generated", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
today = ~D[2024-06-15]
yesterday = Date.add(today, -1)
@ -209,11 +221,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2022-01-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
# 2024 should be included because the member was still active during that cycle
@ -225,21 +238,24 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
refute 2025 in cycle_years
end
test "exit during first month of year stops at that year (monthly)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :monthly})
test "exit during first month of year stops at that year (monthly)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :monthly}, actor)
# Create member - cycles will be auto-generated
member =
create_member(%{
join_date: ~D[2024-01-15],
exit_date: ~D[2024-03-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
})
create_member(
%{
join_date: ~D[2024-01-15],
exit_date: ~D[2024-03-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
},
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_months = Enum.map(cycles, & &1.cycle_start.month) |> Enum.sort()
assert 1 in cycle_months
@ -253,18 +269,21 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
end
describe "member has no cycles initially" do
test "returns error when fee type is not assigned" do
setup_settings(true)
test "returns error when fee type is not assigned", %{actor: actor} do
setup_settings(true, actor)
# Create member WITHOUT fee type (no auto-generation)
member =
create_member(%{
join_date: ~D[2022-03-15],
membership_fee_start_date: ~D[2022-01-01]
})
create_member(
%{
join_date: ~D[2022-03-15],
membership_fee_start_date: ~D[2022-01-01]
},
actor
)
# Verify no cycles exist initially
initial_cycles = get_member_cycles(member.id)
initial_cycles = get_member_cycles(member.id, actor)
assert initial_cycles == []
# Trying to generate cycles without fee type should return error
@ -272,9 +291,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
assert result == {:error, :no_membership_fee_type}
end
test "generates all cycles when member is created with fee type" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "generates all cycles when member is created with fee type", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
today = ~D[2024-06-15]
@ -286,11 +305,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2022-01-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
# Should have generated all cycles from 2022 to 2024 (3 cycles)
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
@ -303,16 +323,19 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
end
describe "member has existing cycles" do
test "generates from last cycle (not duplicating existing)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "generates from last cycle (not duplicating existing)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
# Create member WITHOUT fee type first
member =
create_member(%{
join_date: ~D[2022-03-15],
membership_fee_start_date: ~D[2022-01-01]
})
create_member(
%{
join_date: ~D[2022-03-15],
membership_fee_start_date: ~D[2022-01-01]
},
actor
)
# Manually create an existing cycle for 2022
MembershipFeeCycle
@ -323,20 +346,20 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
amount: fee_type.amount,
status: :paid
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Now assign fee type
member =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Explicitly generate cycles with fixed "today" date
today = ~D[2024-06-15]
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
# Check all cycles
all_cycles = get_member_cycles(member.id)
all_cycles = get_member_cycles(member.id, actor)
all_cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()
# Should have 2022 (manually created), 2023 and 2024 (auto-generated)
@ -350,9 +373,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
end
describe "year boundary handling" do
test "cycles span across year boundaries correctly (yearly)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "cycles span across year boundaries correctly (yearly)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
today = ~D[2024-06-15]
@ -364,11 +387,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2023-01-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
# Should have 2023 and 2024
@ -376,9 +400,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
assert 2024 in cycle_years
end
test "cycles span across year boundaries correctly (quarterly)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :quarterly})
test "cycles span across year boundaries correctly (quarterly)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :quarterly}, actor)
today = ~D[2024-12-15]
@ -390,20 +414,21 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-10-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_starts = Enum.map(cycles, & &1.cycle_start) |> Enum.sort(Date)
# Should have Q4 2024
assert ~D[2024-10-01] in cycle_starts
end
test "December to January transition (monthly)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :monthly})
test "December to January transition (monthly)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :monthly}, actor)
today = ~D[2024-12-31]
@ -415,11 +440,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-12-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_starts = Enum.map(cycles, & &1.cycle_start) |> Enum.sort(Date)
# Should have Dec 2024
@ -428,9 +454,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
end
describe "leap year handling" do
test "February cycles in leap year" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :monthly})
test "February cycles in leap year", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :monthly}, actor)
today = ~D[2024-03-15]
@ -443,11 +469,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-02-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
# Should have February 2024 cycle
feb_cycle = Enum.find(cycles, fn c -> c.cycle_start == ~D[2024-02-01] end)
@ -455,9 +482,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
assert feb_cycle != nil
end
test "February cycles in non-leap year" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :monthly})
test "February cycles in non-leap year", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :monthly}, actor)
today = ~D[2023-03-15]
@ -470,11 +497,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2023-02-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
# Should have February 2023 cycle
feb_cycle = Enum.find(cycles, fn c -> c.cycle_start == ~D[2023-02-01] end)
@ -482,9 +510,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
assert feb_cycle != nil
end
test "yearly cycle in leap year" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "yearly cycle in leap year", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
today = ~D[2024-12-31]
@ -496,11 +524,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
# Should have 2024 cycle
cycle_2024 = Enum.find(cycles, fn c -> c.cycle_start == ~D[2024-01-01] end)
@ -510,9 +539,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
end
describe "include_joining_cycle variations" do
test "include_joining_cycle = true starts from joining cycle" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "include_joining_cycle = true starts from joining cycle", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
today = ~D[2024-06-15]
@ -525,20 +554,21 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id
# membership_fee_start_date will be auto-calculated
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
# Should include 2023 (joining year)
assert 2023 in cycle_years
end
test "include_joining_cycle = false starts from next cycle" do
setup_settings(false)
fee_type = create_fee_type(%{interval: :yearly})
test "include_joining_cycle = false starts from next cycle", %{actor: actor} do
setup_settings(false, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
today = ~D[2024-06-15]
@ -551,11 +581,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id
# membership_fee_start_date will be auto-calculated
},
today
today,
actor
)
# Check all cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
# Should NOT include 2023 (joining year)
@ -567,17 +598,22 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
end
describe "inactive member processing" do
test "inactive members receive cycles up to exit_date via generate_cycles_for_all_members" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "inactive members receive cycles up to exit_date via generate_cycles_for_all_members", %{
actor: actor
} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
# Create an inactive member (left in 2023) WITHOUT fee type initially
# This simulates a member that was created before the fee system existed
member =
create_member(%{
join_date: ~D[2021-03-15],
exit_date: ~D[2023-06-15]
})
create_member(
%{
join_date: ~D[2021-03-15],
exit_date: ~D[2023-06-15]
},
actor
)
# Now assign fee type (simulating a retroactive assignment)
member =
@ -586,7 +622,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2021-01-01]
})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Run batch generation with a "today" date after the member left
today = ~D[2024-06-15]
@ -596,7 +632,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
assert results.total >= 1
# Check the member's cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()
# Should have 2021, 2022, 2023 (exit year included)
@ -608,9 +644,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
refute 2024 in cycle_years
end
test "exit_date on cycle_start still generates that cycle" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "exit_date on cycle_start still generates that cycle", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
today = ~D[2024-12-31]
@ -624,11 +660,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2022-01-01]
},
today
today,
actor
)
# Check cycles
cycles = get_member_cycles(member.id)
cycles = get_member_cycles(member.id, actor)
cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
# 2024 should be included because exit_date == cycle_start means

View file

@ -11,8 +11,13 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
require Ash.Query
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) do
defp create_fee_type(attrs, actor) do
default_attrs = %{
name: "Test Fee Type #{System.unique_integer([:positive])}",
amount: Decimal.new("50.00"),
@ -23,11 +28,11 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: actor)
end
# Helper to create a member without triggering cycle generation
defp create_member_without_cycles(attrs) do
defp create_member_without_cycles(attrs, actor) do
default_attrs = %{
first_name: "Test",
last_name: "User",
@ -38,50 +43,53 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: actor)
end
# Helper to set up settings with specific include_joining_cycle value
defp setup_settings(include_joining_cycle) do
defp setup_settings(include_joining_cycle, actor) do
{:ok, settings} = Mv.Membership.get_settings()
settings
|> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle})
|> Ash.update!()
|> Ash.update!(actor: actor)
end
# Helper to get cycles for a member
defp get_member_cycles(member_id) do
defp get_member_cycles(member_id, actor) do
MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member_id)
|> Ash.Query.sort(cycle_start: :asc)
|> Ash.read!()
|> Ash.read!(actor: actor)
end
describe "generate_cycles_for_member/2" do
test "generates cycles from start date to today" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "generates cycles from start date to today", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
# Create member WITHOUT fee type first to avoid auto-generation
member =
create_member_without_cycles(%{
join_date: ~D[2022-03-15],
membership_fee_start_date: ~D[2022-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2022-03-15],
membership_fee_start_date: ~D[2022-01-01]
},
actor
)
# Assign fee type
member =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Explicitly generate cycles with fixed "today" date to avoid date dependency
today = ~D[2024-06-15]
{:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
# Verify cycles were generated
all_cycles = get_member_cycles(member.id)
all_cycles = get_member_cycles(member.id, actor)
cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()
# With include_joining_cycle=true and join_date=2022-03-15,
@ -92,16 +100,19 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
assert 2024 in cycle_years
end
test "generates cycles from last existing cycle" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "generates cycles from last existing cycle", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
# Create member without fee type first to avoid auto-generation
member =
create_member_without_cycles(%{
join_date: ~D[2022-03-15],
membership_fee_start_date: ~D[2022-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2022-03-15],
membership_fee_start_date: ~D[2022-01-01]
},
actor
)
# Manually create a cycle for 2022
MembershipFeeCycle
@ -112,13 +123,13 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
amount: fee_type.amount,
status: :paid
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Now assign fee type to member
member =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Generate cycles with specific "today" date
today = ~D[2024-06-15]
@ -130,17 +141,20 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
assert 2022 not in new_cycle_years
end
test "respects left_at boundary (stops generation)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "respects left_at boundary (stops generation)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
member =
create_member_without_cycles(%{
join_date: ~D[2022-03-15],
exit_date: ~D[2023-06-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2022-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2022-03-15],
exit_date: ~D[2023-06-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2022-01-01]
},
actor
)
# Generate cycles with specific "today" date far in the future
today = ~D[2025-06-15]
@ -154,16 +168,19 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
assert 2025 not in cycle_years
end
test "skips existing cycles (idempotent)" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "skips existing cycles (idempotent)", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
member =
create_member_without_cycles(%{
join_date: ~D[2023-03-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2023-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2023-03-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2023-01-01]
},
actor
)
today = ~D[2024-06-15]
@ -177,37 +194,43 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
assert second_cycles == []
end
test "does not fill gaps when cycles were deleted" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "does not fill gaps when cycles were deleted", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
# Create member without fee type first to control which cycles exist
member =
create_member_without_cycles(%{
join_date: ~D[2020-03-15],
membership_fee_start_date: ~D[2020-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2020-03-15],
membership_fee_start_date: ~D[2020-01-01]
},
actor
)
# Manually create cycles for 2020, 2021, 2022, 2023
for year <- [2020, 2021, 2022, 2023] do
MembershipFeeCycle
|> Ash.Changeset.for_create(:create, %{
cycle_start: Date.new!(year, 1, 1),
member_id: member.id,
membership_fee_type_id: fee_type.id,
amount: fee_type.amount,
status: :unpaid
})
|> Ash.create!()
|> Ash.Changeset.for_create(
:create,
%{
cycle_start: Date.new!(year, 1, 1),
member_id: member.id,
membership_fee_type_id: fee_type.id,
amount: fee_type.amount,
status: :unpaid
}
)
|> Ash.create!(actor: actor)
end
# Delete the 2021 cycle (create a gap)
cycle_2021 =
MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id and cycle_start == ^~D[2021-01-01])
|> Ash.read_one!()
|> Ash.read_one!(actor: actor)
Ash.destroy!(cycle_2021)
Ash.destroy!(cycle_2021, actor: actor)
# Now assign fee type to member (this triggers generation)
# Since cycles already exist (2020, 2022, 2023), the generator will
@ -215,10 +238,10 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
member =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Verify gap was NOT filled and new cycles were generated from last existing
all_cycles = get_member_cycles(member.id)
all_cycles = get_member_cycles(member.id, actor)
all_cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort()
# 2021 should NOT exist (gap was not filled)
@ -234,20 +257,23 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
assert 2025 in all_cycle_years
end
test "sets correct amount from membership fee type" do
setup_settings(true)
test "sets correct amount from membership fee type", %{actor: actor} do
setup_settings(true, actor)
amount = Decimal.new("75.50")
fee_type = create_fee_type(%{interval: :yearly, amount: amount})
fee_type = create_fee_type(%{interval: :yearly, amount: amount}, actor)
member =
create_member_without_cycles(%{
join_date: ~D[2024-03-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2024-03-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
},
actor
)
# Verify cycles were generated with correct amount
all_cycles = get_member_cycles(member.id)
all_cycles = get_member_cycles(member.id, actor)
refute Enum.empty?(all_cycles), "Expected cycles to be generated"
# All cycles should have the correct amount
@ -256,21 +282,24 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
end)
end
test "handles NULL membership_fee_start_date by calculating from join_date" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :quarterly})
test "handles NULL membership_fee_start_date by calculating from join_date", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :quarterly}, actor)
# Create member without membership_fee_start_date - it will be auto-calculated
# and cycles will be auto-generated
member =
create_member_without_cycles(%{
join_date: ~D[2024-02-15],
membership_fee_type_id: fee_type.id
# No membership_fee_start_date - should be calculated
})
create_member_without_cycles(
%{
join_date: ~D[2024-02-15],
membership_fee_type_id: fee_type.id
# No membership_fee_start_date - should be calculated
},
actor
)
# Verify cycles were auto-generated
all_cycles = get_member_cycles(member.id)
all_cycles = get_member_cycles(member.id, actor)
# With include_joining_cycle=true and join_date=2024-02-15 (quarterly),
# start_date should be 2024-01-01 (Q1 start)
@ -284,28 +313,34 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
assert first_cycle_start == ~D[2024-01-01]
end
test "returns error when member has no membership_fee_type" do
test "returns error when member has no membership_fee_type", %{actor: actor} do
# Create member without fee type - no auto-generation will occur
member =
create_member_without_cycles(%{
join_date: ~D[2024-03-15]
# No membership_fee_type_id
})
create_member_without_cycles(
%{
join_date: ~D[2024-03-15]
# No membership_fee_type_id
},
actor
)
{:error, reason} = CycleGenerator.generate_cycles_for_member(member.id)
assert reason == :no_membership_fee_type
end
test "returns error when member has no join_date" do
fee_type = create_fee_type(%{interval: :yearly})
test "returns error when member has no join_date", %{actor: actor} do
fee_type = create_fee_type(%{interval: :yearly}, actor)
# Create member without join_date - no auto-generation will occur
# (after_action hook checks for join_date)
member =
create_member_without_cycles(%{
membership_fee_type_id: fee_type.id
# No join_date
})
create_member_without_cycles(
%{
membership_fee_type_id: fee_type.id
# No join_date
},
actor
)
{:error, reason} = CycleGenerator.generate_cycles_for_member(member.id)
assert reason == :no_join_date
@ -357,24 +392,30 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
end
describe "generate_cycles_for_all_members/1" do
test "generates cycles for multiple members" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "generates cycles for multiple members", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
# Create multiple members
_member1 =
create_member_without_cycles(%{
join_date: ~D[2024-01-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2024-01-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
},
actor
)
_member2 =
create_member_without_cycles(%{
join_date: ~D[2024-02-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2024-02-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2024-01-01]
},
actor
)
today = ~D[2024-06-15]
{:ok, results} = CycleGenerator.generate_cycles_for_all_members(today: today)
@ -387,16 +428,19 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do
end
describe "lock mechanism" do
test "prevents concurrent generation for same member" do
setup_settings(true)
fee_type = create_fee_type(%{interval: :yearly})
test "prevents concurrent generation for same member", %{actor: actor} do
setup_settings(true, actor)
fee_type = create_fee_type(%{interval: :yearly}, actor)
member =
create_member_without_cycles(%{
join_date: ~D[2022-03-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2022-01-01]
})
create_member_without_cycles(
%{
join_date: ~D[2022-03-15],
membership_fee_type_id: fee_type.id,
membership_fee_start_date: ~D[2022-01-01]
},
actor
)
today = ~D[2024-06-15]