Membership Fee 6 - UI Components & LiveViews closes #280 #304

Open
moritz wants to merge 65 commits from feature/280_membership_fee_ui into main
2 changed files with 59 additions and 10 deletions
Showing only changes of commit 9a1f0fbfa6 - Show all commits

View file

@ -242,6 +242,63 @@ defmodule Mv.Membership.Member do
{:ok, member} {:ok, member}
end end
end) end)
# Trigger cycle regeneration when join_date or exit_date changes
# Regenerates cycles based on new dates
# Note: Cycle generation runs synchronously in test environment, asynchronously in production
# CycleGenerator uses advisory locks and transactions internally to prevent race conditions
change after_action(fn changeset, member, _context ->
join_date_changed = Ash.Changeset.changing_attribute?(changeset, :join_date)
exit_date_changed = Ash.Changeset.changing_attribute?(changeset, :exit_date)
if (join_date_changed || exit_date_changed) && member.membership_fee_type_id do
if Application.get_env(:mv, :sql_sandbox, false) do
# Run synchronously in test environment for DB sandbox compatibility
# Use skip_lock?: true to avoid nested transactions (after_action runs within action transaction)
# Return notifications to Ash so they are sent after commit
case Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(
member.id,
today: Date.utc_today(),
skip_lock?: true
) do
{:ok, _cycles, notifications} ->
{:ok, member, notifications}
{:error, reason} ->
require Logger
Logger.warning(
"Failed to regenerate cycles for member #{member.id}: #{inspect(reason)}"
)
{:ok, member}
end
else
# Run asynchronously in other environments
# Send notifications explicitly since they cannot be returned via after_action
Task.start(fn ->
case Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(member.id) do
{:ok, _cycles, notifications} ->
# Send notifications manually for async case
if Enum.any?(notifications) do
Ash.Notifier.notify(notifications)
end
{:error, reason} ->
require Logger
Logger.warning(
"Failed to regenerate cycles for member #{member.id}: #{inspect(reason)}"
)
end
end)
{:ok, member}
end
else
{:ok, member}
end
end)
end end
# Action to handle fuzzy search on specific fields # Action to handle fuzzy search on specific fields
@ -395,11 +452,6 @@ defmodule Mv.Membership.Member do
end end
end end
# Join date not in the future
validate compare(:join_date, less_than_or_equal_to: &Date.utc_today/0),
where: [present(:join_date)],
message: "cannot be in the future"
# Exit date not before join date # Exit date not before join date
validate compare(:exit_date, greater_than: :join_date), validate compare(:exit_date, greater_than: :join_date),
where: [present([:join_date, :exit_date])], where: [present([:join_date, :exit_date])],

View file

@ -58,12 +58,9 @@ defmodule Mv.Membership.MemberTest do
assert {:ok, _member} = Membership.create_member(attrs2) assert {:ok, _member} = Membership.create_member(attrs2)
end end
test "Join date is optional but must not be in the future" do test "Join date can be in the future" do
attrs = Map.put(@valid_attrs, :join_date, Date.utc_today() |> Date.add(1)) attrs = Map.put(@valid_attrs, :join_date, Date.utc_today() |> Date.add(1))
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert {:ok, _member} = Membership.create_member(attrs)
assert error_message(errors, :join_date) =~ "cannot be in the future"
attrs2 = Map.delete(@valid_attrs, :join_date)
assert {:ok, _member} = Membership.create_member(attrs2)
end end
test "Exit date is optional but must not be before join date if both are specified" do test "Exit date is optional but must not be before join date if both are specified" do