Make seed script deterministic and idempotent for fee type assignments

Fix update action name from :update to :update_member for Member resource
This commit is contained in:
Moritz 2025-12-22 16:56:12 +01:00
parent 3241dd7d96
commit a03056e6ae

View file

@ -129,7 +129,12 @@ Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity
|> Ash.update!() |> Ash.update!()
# Load all membership fee types for assignment # 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 # Create sample members for testing - use upsert to prevent duplicates
# Member 1: Hans - All cycles paid # 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) member_attrs_without_status = Map.delete(member_attrs, :cycle_status)
# Use upsert to prevent duplicates based on email # 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 = member =
Membership.create_member!(member_attrs_without_status, Membership.create_member!(member_attrs_without_fee_type,
upsert?: true, upsert?: true,
upsert_identity: :unique_email 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 # 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 # Load member with cycles to check if they already exist
member_with_cycles = member_with_cycles =
member final_member
|> Ash.load!(:membership_fee_cycles) |> Ash.load!(:membership_fee_cycles)
# Only generate if no cycles exist yet (to avoid duplicates on re-run) # 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 if Enum.empty?(member_with_cycles.membership_fee_cycles) do
# Generate cycles # Generate cycles
{:ok, new_cycles, _notifications} = {: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 new_cycles
else else
@ -311,36 +331,47 @@ Enum.with_index(linked_members)
user = member_attrs.user user = member_attrs.user
member_attrs_without_user = Map.delete(member_attrs, :user) member_attrs_without_user = Map.delete(member_attrs, :user)
# Round-robin assignment: continue cycling through fee types # Use upsert to prevent duplicates based on email
# Start from where previous members ended # First create/update member without membership_fee_type_id to avoid overwriting existing assignments
fee_type_index = rem(3 + index, length(all_fee_types)) member_attrs_without_fee_type = Map.delete(member_attrs_without_user, :membership_fee_type_id)
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)
# Check if user already has a member # Check if user already has a member
member = member =
if user.member_id == nil do if user.member_id == nil do
# User is free, create member and link - use upsert to prevent duplicates # User is free, create member and link - use upsert to prevent duplicates
Membership.create_member!( 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?: true,
upsert_identity: :unique_email upsert_identity: :unique_email
) )
else else
# User already has a member, just create the member without linking - use upsert to prevent duplicates # 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?: true,
upsert_identity: :unique_email upsert_identity: :unique_email
) )
end 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 # 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 # Load member with cycles to check if they already exist
member_with_cycles = member_with_cycles =
member final_member
|> Ash.load!(:membership_fee_cycles) |> Ash.load!(:membership_fee_cycles)
# Only generate if no cycles exist yet (to avoid duplicates on re-run) # 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 if Enum.empty?(member_with_cycles.membership_fee_cycles) do
# Generate cycles # Generate cycles
{:ok, new_cycles, _notifications} = {: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 new_cycles
else else