Use system actor for cycle generation
Update cycle generator, member hooks, and job to use system actor. Remove actor parameters as cycle generation is a mandatory side effect.
This commit is contained in:
parent
f0169c95b7
commit
c64b74588f
3 changed files with 65 additions and 87 deletions
|
|
@ -30,12 +30,11 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
|
||||
## Authorization
|
||||
|
||||
This module runs systemically and accepts optional actor parameters.
|
||||
When called from hooks/changes, actor is extracted from changeset context.
|
||||
When called directly, actor should be provided for proper authorization.
|
||||
This module runs systemically and uses the system actor for all operations.
|
||||
This ensures that cycle generation always works, regardless of user permissions.
|
||||
|
||||
All functions accept an optional `actor` parameter in the `opts` keyword list
|
||||
that is passed to Ash operations to ensure proper authorization checks are performed.
|
||||
All functions use `Mv.Helpers.SystemActor.get_system_actor/0` to bypass
|
||||
user permission checks, as cycle generation is a mandatory side effect.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -47,6 +46,8 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
|
||||
"""
|
||||
|
||||
alias Mv.Helpers
|
||||
alias Mv.Helpers.SystemActor
|
||||
alias Mv.Membership.Member
|
||||
alias Mv.MembershipFees.CalendarCycles
|
||||
alias Mv.MembershipFees.Changes.SetMembershipFeeStartDate
|
||||
|
|
@ -86,9 +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
|
||||
actor = Keyword.get(opts, :actor)
|
||||
|
||||
case load_member(member_id, actor: actor) do
|
||||
case load_member(member_id) do
|
||||
{:ok, member} -> generate_cycles_for_member(member, opts)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
|
|
@ -98,27 +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?, opts)
|
||||
do_generate_cycles_with_lock(member, today, skip_lock?)
|
||||
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?, opts) do
|
||||
defp do_generate_cycles_with_lock(member, today, true = _skip_lock?) do
|
||||
# Lock already set by caller (e.g., regenerate_cycles_on_type_change)
|
||||
# Just generate cycles without additional locking
|
||||
actor = Keyword.get(opts, :actor)
|
||||
do_generate_cycles(member, today, actor: actor)
|
||||
do_generate_cycles(member, today)
|
||||
end
|
||||
|
||||
defp do_generate_cycles_with_lock(member, today, false, opts) do
|
||||
defp do_generate_cycles_with_lock(member, today, false) do
|
||||
lock_key = :erlang.phash2(member.id)
|
||||
actor = Keyword.get(opts, :actor)
|
||||
|
||||
Repo.transaction(fn ->
|
||||
Ecto.Adapters.SQL.query!(Repo, "SELECT pg_advisory_xact_lock($1)", [lock_key])
|
||||
|
||||
case do_generate_cycles(member, today, actor: actor) do
|
||||
case do_generate_cycles(member, today) 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)
|
||||
|
|
@ -168,12 +165,15 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
# Query ALL members with fee type assigned (including inactive/left members)
|
||||
# The exit_date boundary is applied during cycle generation, not here.
|
||||
# This allows catch-up generation for members who left but are missing cycles.
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
opts = Helpers.ash_actor_opts(system_actor)
|
||||
|
||||
query =
|
||||
Member
|
||||
|> Ash.Query.filter(not is_nil(membership_fee_type_id))
|
||||
|> Ash.Query.filter(not is_nil(join_date))
|
||||
|
||||
case Ash.read(query) do
|
||||
case Ash.read(query, opts) do
|
||||
{:ok, members} ->
|
||||
results = process_members_in_batches(members, batch_size, today)
|
||||
{:ok, build_results_summary(results)}
|
||||
|
|
@ -235,33 +235,25 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
|
||||
# Private functions
|
||||
|
||||
defp load_member(member_id, opts) do
|
||||
actor = Keyword.get(opts, :actor)
|
||||
defp load_member(member_id) do
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
opts = Helpers.ash_actor_opts(system_actor)
|
||||
|
||||
query =
|
||||
Member
|
||||
|> Ash.Query.filter(id == ^member_id)
|
||||
|> Ash.Query.load([:membership_fee_type, :membership_fee_cycles])
|
||||
|
||||
result =
|
||||
if actor do
|
||||
Ash.read_one(query, actor: actor)
|
||||
else
|
||||
Ash.read_one(query)
|
||||
end
|
||||
|
||||
case result do
|
||||
case Ash.read_one(query, opts) do
|
||||
{:ok, nil} -> {:error, :member_not_found}
|
||||
{:ok, member} -> {:ok, member}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_generate_cycles(member, today, opts) do
|
||||
actor = Keyword.get(opts, :actor)
|
||||
|
||||
defp do_generate_cycles(member, today) do
|
||||
# Reload member with relationships to ensure fresh data
|
||||
case load_member(member.id, actor: actor) do
|
||||
case load_member(member.id) do
|
||||
{:ok, member} ->
|
||||
cond do
|
||||
is_nil(member.membership_fee_type_id) ->
|
||||
|
|
@ -271,7 +263,7 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
{:error, :no_join_date}
|
||||
|
||||
true ->
|
||||
generate_missing_cycles(member, today, actor: actor)
|
||||
generate_missing_cycles(member, today)
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
|
|
@ -279,8 +271,7 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
end
|
||||
end
|
||||
|
||||
defp generate_missing_cycles(member, today, opts) do
|
||||
actor = Keyword.get(opts, :actor)
|
||||
defp generate_missing_cycles(member, today) do
|
||||
fee_type = member.membership_fee_type
|
||||
interval = fee_type.interval
|
||||
amount = fee_type.amount
|
||||
|
|
@ -296,7 +287,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, actor: actor)
|
||||
create_cycles(cycle_starts, member.id, fee_type.id, amount)
|
||||
else
|
||||
{:ok, [], []}
|
||||
end
|
||||
|
|
@ -391,8 +382,10 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
end
|
||||
end
|
||||
|
||||
defp create_cycles(cycle_starts, member_id, fee_type_id, amount, opts) do
|
||||
actor = Keyword.get(opts, :actor)
|
||||
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)
|
||||
|
||||
# Always use return_notifications?: true to collect notifications
|
||||
# Notifications will be returned to the caller, who is responsible for
|
||||
# sending them (e.g., via after_action hook returning {:ok, result, notifications})
|
||||
|
|
@ -407,7 +400,7 @@ defmodule Mv.MembershipFees.CycleGenerator do
|
|||
}
|
||||
|
||||
handle_cycle_creation_result(
|
||||
Ash.create(MembershipFeeCycle, attrs, return_notifications?: true, actor: actor),
|
||||
Ash.create(MembershipFeeCycle, attrs, [return_notifications?: true] ++ opts),
|
||||
cycle_start
|
||||
)
|
||||
end)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue