fix: resolve notification handling and maintain after_action for cycle regeneration

This commit is contained in:
Moritz 2025-12-15 15:26:05 +01:00
parent 6a91f7c711
commit f7c33bfc7d
Signed by: moritz
GPG key ID: 1020A035E5DD0824
3 changed files with 108 additions and 57 deletions

View file

@ -38,10 +38,10 @@ defmodule Mv.MembershipFees.CycleGenerator do
"""
alias Mv.MembershipFees.CalendarCycles
alias Mv.MembershipFees.MembershipFeeCycle
alias Mv.MembershipFees.Changes.SetMembershipFeeStartDate
alias Mv.Membership.Member
alias Mv.MembershipFees.CalendarCycles
alias Mv.MembershipFees.Changes.SetMembershipFeeStartDate
alias Mv.MembershipFees.MembershipFeeCycle
alias Mv.Repo
require Ash.Query
@ -84,12 +84,20 @@ defmodule Mv.MembershipFees.CycleGenerator do
def generate_cycles_for_member(%Member{} = member, opts) do
today = Keyword.get(opts, :today, Date.utc_today())
skip_lock? = Keyword.get(opts, :skip_lock?, false)
# Use advisory lock to prevent concurrent generation
# Notifications are handled inside with_advisory_lock after transaction commits
with_advisory_lock(member.id, fn ->
if skip_lock? do
# Lock already set by caller (e.g., regenerate_cycles_on_type_change)
# Just generate cycles without additional locking
# When in transaction, notifications are returned and must be sent after commit
do_generate_cycles(member, today)
end)
else
# Use advisory lock to prevent concurrent generation
# Notifications are handled inside with_advisory_lock after transaction commits
with_advisory_lock(member.id, fn ->
do_generate_cycles(member, today)
end)
end
end
@doc """
@ -198,6 +206,7 @@ defmodule Mv.MembershipFees.CycleGenerator do
# Already in transaction: use advisory lock directly without starting new transaction
# This prevents nested transactions which can cause deadlocks
# Returns {:ok, cycles, notifications} where notifications should be sent after commit
defp with_advisory_lock_in_transaction(lock_key, fun) do
Ecto.Adapters.SQL.query!(Repo, "SELECT pg_advisory_xact_lock($1)", [lock_key])
normalize_fun_result(fun.())
@ -216,16 +225,16 @@ defmodule Mv.MembershipFees.CycleGenerator do
end
# Execute function within transaction and return normalized result
# When in transaction, create_cycles returns {:ok, cycles, notifications}
# When not in transaction, create_cycles returns {:ok, cycles}
# execute_within_transaction is always called within a Repo.transaction
# create_cycles returns {:ok, cycles, notifications} when in transaction
defp execute_within_transaction(fun) do
case fun.() do
{:ok, result, notifications} when is_list(notifications) ->
# In transaction case: return result and notifications separately
# Return result and notifications separately
{result, notifications}
{:ok, result} ->
# Not in transaction case: notifications handled by Ash automatically
# Fallback case: no notifications returned
{result, []}
{:error, reason} ->
@ -234,15 +243,15 @@ defmodule Mv.MembershipFees.CycleGenerator do
end
# Normalize function result to consistent format
# When in transaction, create_cycles returns {:ok, cycles, notifications}
# When not in transaction, create_cycles returns {:ok, cycles}
defp normalize_fun_result({:ok, result, _notifications}) do
# In transaction case: notifications will be sent after outer transaction commits
# normalize_fun_result is called when already in a transaction (skip_lock? case)
# create_cycles returns {:ok, cycles, notifications} when in transaction
defp normalize_fun_result({:ok, result, notifications}) when is_list(notifications) do
# Notifications will be sent after outer transaction commits
# Return in same format as non-transaction case for consistency
{:ok, result}
{:ok, result, notifications}
end
defp normalize_fun_result({:ok, result}), do: {:ok, result}
defp normalize_fun_result({:ok, result}), do: {:ok, result, []}
defp normalize_fun_result({:error, reason}), do: {:error, reason}
# Handle transaction result and send notifications if needed