Membership Fee Type Resource & Settings closes #278 #291

Open
moritz wants to merge 27 commits from feature/278_membership_fee_settings into main
Showing only changes of commit e6ac5d1ab1 - Show all commits

View file

@ -86,6 +86,7 @@ defmodule Mv.MembershipFees.CycleGenerator do
today = Keyword.get(opts, :today, Date.utc_today()) today = Keyword.get(opts, :today, Date.utc_today())
# Use advisory lock to prevent concurrent generation # Use advisory lock to prevent concurrent generation
# Notifications are handled inside with_advisory_lock after transaction commits
with_advisory_lock(member.id, fn -> with_advisory_lock(member.id, fn ->
do_generate_cycles(member, today) do_generate_cycles(member, today)
end) end)
@ -187,15 +188,37 @@ defmodule Mv.MembershipFees.CycleGenerator do
# Convert UUID to integer for advisory lock (use hash) # Convert UUID to integer for advisory lock (use hash)
lock_key = :erlang.phash2(member_id) lock_key = :erlang.phash2(member_id)
Repo.transaction(fn -> result =
# Acquire advisory lock for this transaction Repo.transaction(fn ->
Ecto.Adapters.SQL.query!(Repo, "SELECT pg_advisory_xact_lock($1)", [lock_key]) # Acquire advisory lock for this transaction
Ecto.Adapters.SQL.query!(Repo, "SELECT pg_advisory_xact_lock($1)", [lock_key])
case fun.() do case fun.() do
{:ok, result} -> result {:ok, result, notifications} when is_list(notifications) ->
{:error, reason} -> Repo.rollback(reason) # Return result and notifications separately
end {result, notifications}
end)
{:ok, result} ->
# Handle case where no notifications were returned (backward compatibility)
{result, []}
{:error, reason} ->
Repo.rollback(reason)
end
end)
# Extract result and notifications, send notifications after transaction
case result do
{:ok, {cycles, notifications}} ->
if Enum.any?(notifications) do
Ash.Notifier.notify(notifications)
end
{:ok, cycles}
{:error, reason} ->
{:error, reason}
end
end end
defp do_generate_cycles(member, today) do defp do_generate_cycles(member, today) do
@ -236,7 +259,7 @@ defmodule Mv.MembershipFees.CycleGenerator do
cycle_starts = generate_cycle_starts(start_date, end_date, interval) 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)
else else
{:ok, []} {:ok, [], []}
end end
end end
@ -340,17 +363,22 @@ defmodule Mv.MembershipFees.CycleGenerator do
status: :unpaid status: :unpaid
} }
case Ash.create(MembershipFeeCycle, attrs) do # Return notifications to avoid warnings when creating within a transaction
{:ok, cycle} -> {:ok, cycle} case Ash.create(MembershipFeeCycle, attrs, return_notifications?: true) do
{:ok, cycle, notifications} -> {:ok, cycle, notifications}
{:error, reason} -> {:error, {cycle_start, reason}} {:error, reason} -> {:error, {cycle_start, reason}}
end end
end) end)
{successes, errors} = Enum.split_with(results, &match?({:ok, _}, &1)) {successes, errors} = Enum.split_with(results, &match?({:ok, _, _}, &1))
successful_cycles = Enum.map(successes, fn {:ok, cycle} -> cycle end) successful_cycles = Enum.map(successes, fn {:ok, cycle, _notifications} -> cycle end)
all_notifications =
Enum.flat_map(successes, fn {:ok, _cycle, notifications} -> notifications end)
if Enum.empty?(errors) do if Enum.empty?(errors) do
{:ok, successful_cycles} # Return cycles and notifications to be sent after transaction commits
{:ok, successful_cycles, all_notifications}
else else
Logger.warning("Some cycles failed to create: #{inspect(errors)}") Logger.warning("Some cycles failed to create: #{inspect(errors)}")
# Return partial failure with both successful and failed cycles # Return partial failure with both successful and failed cycles