From babe26a33a33167542a671e1d6cda297189fad15 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 10 Feb 2026 22:31:45 +0100 Subject: [PATCH] Pass actor through CycleGenerator so seeds can use admin - get_actor(opts): use opts[:actor] or system actor - load_member, do_generate_cycles, create_cycles pass opts - Seeds pass admin_user_with_role for Ash.load! and cycle updates Co-authored-by: Cursor --- lib/mv/membership_fees/cycle_generator.ex | 48 +++++++++++++---------- priv/repo/seeds.exs | 14 +++---- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/lib/mv/membership_fees/cycle_generator.ex b/lib/mv/membership_fees/cycle_generator.ex index ec33914..1a33ca8 100644 --- a/lib/mv/membership_fees/cycle_generator.ex +++ b/lib/mv/membership_fees/cycle_generator.ex @@ -87,7 +87,7 @@ defmodule Mv.MembershipFees.CycleGenerator do def generate_cycles_for_member(member_or_id, opts \\ []) def generate_cycles_for_member(member_id, opts) when is_binary(member_id) do - case load_member(member_id) do + case load_member(member_id, opts) do {:ok, member} -> generate_cycles_for_member(member, opts) {:error, reason} -> {:error, reason} end @@ -97,25 +97,25 @@ defmodule Mv.MembershipFees.CycleGenerator do today = Keyword.get(opts, :today, Date.utc_today()) skip_lock? = Keyword.get(opts, :skip_lock?, false) - do_generate_cycles_with_lock(member, today, skip_lock?) + do_generate_cycles_with_lock(member, today, skip_lock?, opts) end # Generate cycles with lock handling # Returns {:ok, cycles, notifications} - notifications are never sent here, # they should be returned to the caller (e.g., via after_action hook) - defp do_generate_cycles_with_lock(member, today, true = _skip_lock?) do - # Lock already set by caller (e.g., regenerate_cycles_on_type_change) + defp do_generate_cycles_with_lock(member, today, true = _skip_lock?, opts) do + # Lock already set by caller (e.g., regenerate_cycles_on_type_change or seeds) # Just generate cycles without additional locking - do_generate_cycles(member, today) + do_generate_cycles(member, today, opts) end - defp do_generate_cycles_with_lock(member, today, false) do + defp do_generate_cycles_with_lock(member, today, false, opts) do lock_key = :erlang.phash2(member.id) Repo.transaction(fn -> Ecto.Adapters.SQL.query!(Repo, "SELECT pg_advisory_xact_lock($1)", [lock_key]) - case do_generate_cycles(member, today) do + case do_generate_cycles(member, today, opts) do {:ok, cycles, notifications} -> # Return cycles and notifications - do NOT send notifications here # They will be sent by the caller (e.g., via after_action hook) @@ -235,25 +235,33 @@ defmodule Mv.MembershipFees.CycleGenerator do # Private functions - defp load_member(member_id) do - system_actor = SystemActor.get_system_actor() - opts = Helpers.ash_actor_opts(system_actor) + # Use actor from opts when provided (e.g. seeds pass admin); otherwise system actor + defp get_actor(opts) do + case Keyword.get(opts, :actor) do + nil -> SystemActor.get_system_actor() + actor -> actor + end + end + + defp load_member(member_id, opts) do + actor = get_actor(opts) + read_opts = Helpers.ash_actor_opts(actor) query = Member |> Ash.Query.filter(id == ^member_id) |> Ash.Query.load([:membership_fee_type, :membership_fee_cycles]) - case Ash.read_one(query, opts) do + case Ash.read_one(query, read_opts) do {:ok, nil} -> {:error, :member_not_found} {:ok, member} -> {:ok, member} {:error, reason} -> {:error, reason} end end - defp do_generate_cycles(member, today) do + defp do_generate_cycles(member, today, opts) do # Reload member with relationships to ensure fresh data - case load_member(member.id) do + case load_member(member.id, opts) do {:ok, member} -> cond do is_nil(member.membership_fee_type_id) -> @@ -263,7 +271,7 @@ defmodule Mv.MembershipFees.CycleGenerator do {:error, :no_join_date} true -> - generate_missing_cycles(member, today) + generate_missing_cycles(member, today, opts) end {:error, reason} -> @@ -271,7 +279,7 @@ defmodule Mv.MembershipFees.CycleGenerator do end end - defp generate_missing_cycles(member, today) do + defp generate_missing_cycles(member, today, opts) do fee_type = member.membership_fee_type interval = fee_type.interval amount = fee_type.amount @@ -287,7 +295,7 @@ defmodule Mv.MembershipFees.CycleGenerator do # Only generate if start_date <= end_date if start_date && Date.compare(start_date, end_date) != :gt do cycle_starts = generate_cycle_starts(start_date, end_date, interval) - create_cycles(cycle_starts, member.id, fee_type.id, amount) + create_cycles(cycle_starts, member.id, fee_type.id, amount, opts) else {:ok, [], []} end @@ -382,9 +390,9 @@ defmodule Mv.MembershipFees.CycleGenerator do end end - defp create_cycles(cycle_starts, member_id, fee_type_id, amount) do - system_actor = SystemActor.get_system_actor() - opts = Helpers.ash_actor_opts(system_actor) + defp create_cycles(cycle_starts, member_id, fee_type_id, amount, opts) do + actor = get_actor(opts) + create_opts = Helpers.ash_actor_opts(actor) # Always use return_notifications?: true to collect notifications # Notifications will be returned to the caller, who is responsible for @@ -400,7 +408,7 @@ defmodule Mv.MembershipFees.CycleGenerator do } handle_cycle_creation_result( - Ash.create(MembershipFeeCycle, attrs, [return_notifications?: true] ++ opts), + Ash.create(MembershipFeeCycle, attrs, [return_notifications?: true] ++ create_opts), cycle_start ) end) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index f686c73..e96ca6e 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -379,10 +379,9 @@ Enum.each(member_attrs_list, fn member_attrs -> # Generate cycles if member has a fee type if final_member.membership_fee_type_id do - # Load member with cycles to check if they already exist + # Load member with cycles to check if they already exist (actor required for auth) member_with_cycles = - final_member - |> Ash.load!(:membership_fee_cycles) + Ash.load!(final_member, :membership_fee_cycles, actor: admin_user_with_role) # Only generate if no cycles exist yet (to avoid duplicates on re-run) cycles = @@ -427,7 +426,7 @@ Enum.each(member_attrs_list, fn member_attrs -> if cycle.status != status do cycle |> Ash.Changeset.for_update(:update, %{status: status}) - |> Ash.update!(actor: admin_user_with_role) + |> Ash.update!(actor: admin_user_with_role, domain: Mv.MembershipFees) end end) end @@ -542,10 +541,9 @@ Enum.with_index(linked_members) # Generate cycles for linked members if final_member.membership_fee_type_id do - # Load member with cycles to check if they already exist + # Load member with cycles to check if they already exist (actor required for auth) member_with_cycles = - final_member - |> Ash.load!(:membership_fee_cycles) + Ash.load!(final_member, :membership_fee_cycles, actor: admin_user_with_role) # Only generate if no cycles exist yet (to avoid duplicates on re-run) cycles = @@ -575,7 +573,7 @@ Enum.with_index(linked_members) if cycle.status != status do cycle |> Ash.Changeset.for_update(:update, %{status: status}) - |> Ash.update!() + |> Ash.update!(actor: admin_user_with_role, domain: Mv.MembershipFees) end end) end