From 9a1f0fbfa614cfc05d997988850db0207dcb6007 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 16 Dec 2025 17:24:24 +0100 Subject: [PATCH] Remove future date validation for join_date Allow join_date to be set in the future. Only validation remaining is that exit_date must be after join_date. --- lib/membership/member.ex | 62 ++++++++++++++++++++++++++++++--- test/membership/member_test.exs | 7 ++-- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 50ababe..5f7df47 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -242,6 +242,63 @@ defmodule Mv.Membership.Member do {:ok, member} 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 # Action to handle fuzzy search on specific fields @@ -395,11 +452,6 @@ defmodule Mv.Membership.Member do 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 validate compare(:exit_date, greater_than: :join_date), where: [present([:join_date, :exit_date])], diff --git a/test/membership/member_test.exs b/test/membership/member_test.exs index 1bf594a..653a2d4 100644 --- a/test/membership/member_test.exs +++ b/test/membership/member_test.exs @@ -58,12 +58,9 @@ defmodule Mv.Membership.MemberTest do assert {:ok, _member} = Membership.create_member(attrs2) 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)) - assert {:error, %Ash.Error.Invalid{errors: errors}} = 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) + assert {:ok, _member} = Membership.create_member(attrs) end test "Exit date is optional but must not be before join date if both are specified" do