From a03056e6aeabae53c363cb6899c133f1d7ab219b Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 22 Dec 2025 16:56:12 +0100 Subject: [PATCH] Make seed script deterministic and idempotent for fee type assignments Fix update action name from :update to :update_member for Member resource --- priv/repo/seeds.exs | 65 +++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index f9a9b3c..2fc27c4 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -129,7 +129,12 @@ Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity |> Ash.update!() # Load all membership fee types for assignment -all_fee_types = MembershipFeeType |> Ash.read!() |> Enum.to_list() +# Sort by name to ensure deterministic order +all_fee_types = + MembershipFeeType + |> Ash.Query.sort(name: :asc) + |> Ash.read!() + |> Enum.to_list() # Create sample members for testing - use upsert to prevent duplicates # Member 1: Hans - All cycles paid @@ -195,17 +200,32 @@ Enum.each(member_attrs_list, fn member_attrs -> member_attrs_without_status = Map.delete(member_attrs, :cycle_status) # Use upsert to prevent duplicates based on email + # First create/update member without membership_fee_type_id to avoid overwriting existing assignments + member_attrs_without_fee_type = Map.delete(member_attrs_without_status, :membership_fee_type_id) + member = - Membership.create_member!(member_attrs_without_status, + Membership.create_member!(member_attrs_without_fee_type, upsert?: true, upsert_identity: :unique_email ) + # Only set membership_fee_type_id if member doesn't have one yet (idempotent) + final_member = + if is_nil(member.membership_fee_type_id) and Map.has_key?(member_attrs_without_status, :membership_fee_type_id) do + member + |> Ash.Changeset.for_update(:update_member, %{ + membership_fee_type_id: member_attrs_without_status.membership_fee_type_id + }) + |> Ash.update!() + else + member + end + # Generate cycles if member has a fee type - if member.membership_fee_type_id do + if final_member.membership_fee_type_id do # Load member with cycles to check if they already exist member_with_cycles = - member + final_member |> Ash.load!(:membership_fee_cycles) # Only generate if no cycles exist yet (to avoid duplicates on re-run) @@ -213,7 +233,7 @@ Enum.each(member_attrs_list, fn member_attrs -> if Enum.empty?(member_with_cycles.membership_fee_cycles) do # Generate cycles {:ok, new_cycles, _notifications} = - CycleGenerator.generate_cycles_for_member(member.id, skip_lock?: true) + CycleGenerator.generate_cycles_for_member(final_member.id, skip_lock?: true) new_cycles else @@ -311,36 +331,47 @@ Enum.with_index(linked_members) user = member_attrs.user member_attrs_without_user = Map.delete(member_attrs, :user) - # Round-robin assignment: continue cycling through fee types - # Start from where previous members ended - fee_type_index = rem(3 + index, length(all_fee_types)) - fee_type = Enum.at(all_fee_types, fee_type_index) - - member_attrs_with_fee_type = - Map.put(member_attrs_without_user, :membership_fee_type_id, fee_type.id) + # Use upsert to prevent duplicates based on email + # First create/update member without membership_fee_type_id to avoid overwriting existing assignments + member_attrs_without_fee_type = Map.delete(member_attrs_without_user, :membership_fee_type_id) # Check if user already has a member member = if user.member_id == nil do # User is free, create member and link - use upsert to prevent duplicates Membership.create_member!( - Map.put(member_attrs_with_fee_type, :user, %{id: user.id}), + Map.put(member_attrs_without_fee_type, :user, %{id: user.id}), upsert?: true, upsert_identity: :unique_email ) else # User already has a member, just create the member without linking - use upsert to prevent duplicates - Membership.create_member!(member_attrs_with_fee_type, + Membership.create_member!(member_attrs_without_fee_type, upsert?: true, upsert_identity: :unique_email ) end + # Only set membership_fee_type_id if member doesn't have one yet (idempotent) + final_member = + if is_nil(member.membership_fee_type_id) do + # Assign deterministically using round-robin + # Start from where previous members ended (3 members before this) + fee_type_index = rem(3 + index, length(all_fee_types)) + fee_type = Enum.at(all_fee_types, fee_type_index) + + member + |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) + |> Ash.update!() + else + member + end + # Generate cycles for linked members - if member.membership_fee_type_id do + if final_member.membership_fee_type_id do # Load member with cycles to check if they already exist member_with_cycles = - member + final_member |> Ash.load!(:membership_fee_cycles) # Only generate if no cycles exist yet (to avoid duplicates on re-run) @@ -348,7 +379,7 @@ Enum.with_index(linked_members) if Enum.empty?(member_with_cycles.membership_fee_cycles) do # Generate cycles {:ok, new_cycles, _notifications} = - CycleGenerator.generate_cycles_for_member(member.id, skip_lock?: true) + CycleGenerator.generate_cycles_for_member(final_member.id, skip_lock?: true) new_cycles else