feat(seeds): add an opt-in bulk member seed for load testing

This commit is contained in:
Simon 2026-07-16 09:39:12 +02:00
parent c482a2630b
commit 95c25142f0
2 changed files with 134 additions and 0 deletions

View file

@ -30,6 +30,11 @@ reset-database:
seed-database:
mix run priv/repo/seeds.exs
# Seed a large batch of members for load / infinite-scroll testing (opt-in; NOT
# part of `seed-database`). Set BULK_MEMBER_COUNT to change the count (default 300).
seed-members-bulk:
mix run priv/repo/seeds_bulk_members.exs
start-database:
docker compose up -d

View file

@ -0,0 +1,129 @@
# Optional BULK member seed for load / infinite-scroll testing.
#
# This is NOT part of the default dev seed (`mix run priv/repo/seeds.exs`), so the
# normal dev database stays small. Run it explicitly when you need many members:
#
# just seed-members-bulk
# BULK_MEMBER_COUNT=500 mix run priv/repo/seeds_bulk_members.exs
#
# Requires the bootstrap seeds (admin user + fee types) to have run first. Members
# are upserted by email ("bulk-member-<n>@example.de"), so re-running is idempotent
# and does not duplicate members or their fee cycles.
alias Mv.Accounts
alias Mv.Membership
alias Mv.MembershipFees.CycleGenerator
alias Mv.MembershipFees.MembershipFeeType
require Ash.Query
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
admin_user_with_role =
case Accounts.User
|> Ash.Query.filter(email == ^admin_email)
|> Ash.read_one(domain: Mv.Accounts, authorize?: false) do
{:ok, user} when not is_nil(user) ->
user |> Ash.load!(:role, authorize?: false)
_ ->
raise "Bulk seed requires bootstrap: admin user not found (#{admin_email})"
end
all_fee_types =
MembershipFeeType
|> Ash.Query.sort(name: :asc)
|> Ash.read!(actor: admin_user_with_role, domain: Mv.MembershipFees)
|> Enum.to_list()
count =
case Integer.parse(System.get_env("BULK_MEMBER_COUNT") || "300") do
{n, _} when n > 0 -> n
_ -> 300
end
first_names =
~w(Anna Bruno Clara David Elena Felix Greta Henrik Ines Jakob Laura Max Nina
Oliver Paula Quirin Rosa Stefan Thea Uwe Vera Wolf Xenia Yannick Zoe Ada
Ben Carl Dora Emil)
last_names =
~w(Schmidt Müller Fischer Weber Wagner Becker Schulz Hoffmann Koch Richter Klein
Wolf Neumann Schwarz Zimmermann Braun Krüger Hartmann Lange Werner Krause
Lehmann Köhler Herrmann Walter)
cities = [
{"München", "80331"},
{"Hamburg", "20095"},
{"Berlin", "10435"},
{"Köln", "50667"},
{"Frankfurt", "60313"},
{"Stuttgart", "70173"},
{"Düsseldorf", "40212"},
{"Dortmund", "44137"},
{"Essen", "45127"},
{"Dresden", "01069"},
{"Nürnberg", "90402"},
{"Bremen", "28195"}
]
statuses = [:paid, :unpaid, :suspended]
IO.puts("Seeding #{count} bulk members (set BULK_MEMBER_COUNT to change)…")
Enum.each(1..count, fn i ->
{city, postal_code} = Enum.at(cities, rem(i, length(cities)))
attrs = %{
first_name: Enum.at(first_names, rem(i, length(first_names))),
last_name: Enum.at(last_names, rem(div(i, length(first_names)) + i, length(last_names))),
email: "bulk-member-#{i}@example.de",
join_date: Date.add(~D[2020-01-01], rem(i * 7, 2000)),
city: city,
street: "Teststraße",
house_number: Integer.to_string(rem(i, 200) + 1),
postal_code: postal_code,
country: "Deutschland"
}
attrs =
case all_fee_types do
[] -> attrs
types -> Map.put(attrs, :membership_fee_type_id, Enum.at(types, rem(i, length(types))).id)
end
member =
Membership.create_member!(attrs,
upsert?: true,
upsert_identity: :unique_email,
actor: admin_user_with_role
)
# Give the member fee cycles + a round-robin status so the fee-status column is
# populated at scale. Only generate when none exist yet (idempotent re-runs).
if not is_nil(member.membership_fee_type_id) do
member = Ash.load!(member, :membership_fee_cycles, actor: admin_user_with_role)
if Enum.empty?(member.membership_fee_cycles) do
{:ok, cycles, _} =
CycleGenerator.generate_cycles_for_member(member.id,
skip_lock?: true,
actor: admin_user_with_role
)
status = Enum.at(statuses, rem(i, length(statuses)))
Enum.each(cycles, fn cycle ->
if cycle.status != status do
cycle
|> Ash.Changeset.for_update(:update, %{status: status})
|> Ash.update!(actor: admin_user_with_role, domain: Mv.MembershipFees)
end
end)
end
end
if rem(i, 50) == 0, do: IO.puts("#{i}/#{count}")
end)
IO.puts("✅ Bulk seed complete: #{count} members.")