mitgliederverwaltung/priv/repo/seeds_dev.exs
simon 28f97184b3
All checks were successful
continuous-integration/drone/push Build is passing
Merge branch 'main' into feature/308-web-form
2026-03-11 02:05:13 +01:00

530 lines
14 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Dev/local seeds: run only in dev and test (Mix.env in [:dev, :test]).
# Creates 20 sample members, groups, and optional custom field values.
# Requires bootstrap seeds to have run first.
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 "Dev seeds require 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()
# Countries: mostly Germany, 12 exceptions (index 7 = Österreich, index 14 = Schweiz)
countries_list =
List.duplicate("Deutschland", 20)
|> List.replace_at(7, "Österreich")
|> List.replace_at(14, "Schweiz")
# 20 members: varied names, cities, join dates; fee types distributed over all members (round-robin)
member_configs = [
%{
first_name: "Anna",
last_name: "Schmidt",
city: "München",
street: "Hauptstraße",
house_number: "1",
postal_code: "80331",
join_date: ~D[2022-01-10]
},
%{
first_name: "Bruno",
last_name: "Müller",
city: "Hamburg",
street: "Lindenstraße",
house_number: "5",
postal_code: "20095",
join_date: ~D[2022-03-15]
},
%{
first_name: "Clara",
last_name: "Fischer",
city: "Berlin",
street: "Kastanienallee",
house_number: "12",
postal_code: "10435",
join_date: ~D[2022-05-20]
},
%{
first_name: "David",
last_name: "Weber",
city: "Köln",
street: "Rheinstraße",
house_number: "8",
postal_code: "50667",
join_date: ~D[2022-07-01]
},
%{
first_name: "Elena",
last_name: "Wagner",
city: "Frankfurt",
street: "Goetheplatz",
house_number: "3",
postal_code: "60313",
join_date: ~D[2022-09-12]
},
%{
first_name: "Felix",
last_name: "Becker",
city: "Stuttgart",
street: "Königstraße",
house_number: "22",
postal_code: "70173",
join_date: ~D[2023-01-05]
},
%{
first_name: "Greta",
last_name: "Schulz",
city: "Düsseldorf",
street: "Schadowstraße",
house_number: "14",
postal_code: "40212",
join_date: ~D[2023-02-14]
},
%{
first_name: "Henrik",
last_name: "Hoffmann",
city: "Leipzig",
street: "Nikolaistraße",
house_number: "7",
postal_code: "04109",
join_date: ~D[2023-04-20]
},
%{
first_name: "Ines",
last_name: "Koch",
city: "Dortmund",
street: "Westenhellweg",
house_number: "45",
postal_code: "44137",
join_date: ~D[2023-06-08]
},
%{
first_name: "Jakob",
last_name: "Richter",
city: "Essen",
street: "Kettwiger Straße",
house_number: "2",
postal_code: "45127",
join_date: ~D[2023-08-11]
},
%{
first_name: "Laura",
last_name: "Klein",
city: "Dresden",
street: "Prager Straße",
house_number: "9",
postal_code: "01069",
join_date: ~D[2023-10-01]
},
%{
first_name: "Max",
last_name: "Wolf",
city: "Hannover",
street: "Georgstraße",
house_number: "50",
postal_code: "30159",
join_date: ~D[2023-11-15]
},
%{
first_name: "Nina",
last_name: "Schröder",
city: "Nürnberg",
street: "Königstraße",
house_number: "73",
postal_code: "90402",
join_date: ~D[2024-01-20]
},
%{
first_name: "Oliver",
last_name: "Neumann",
city: "Bremen",
street: "Obernstraße",
house_number: "31",
postal_code: "28195",
join_date: ~D[2024-03-10]
},
%{
first_name: "Paula",
last_name: "Schwarz",
city: "Mannheim",
street: "Planken",
house_number: "11",
postal_code: "68161",
join_date: ~D[2024-05-22]
},
%{
first_name: "Quirin",
last_name: "Zimmermann",
city: "Karlsruhe",
street: "Kaiserstraße",
house_number: "145",
postal_code: "76133",
join_date: ~D[2024-07-07]
},
%{
first_name: "Rosa",
last_name: "Braun",
city: "Wiesbaden",
street: "Wilhelmstraße",
house_number: "6",
postal_code: "65183",
join_date: ~D[2024-09-01]
},
%{
first_name: "Stefan",
last_name: "Krüger",
city: "Münster",
street: "Ludgeristraße",
house_number: "18",
postal_code: "48143",
join_date: ~D[2024-10-15]
},
%{
first_name: "Thea",
last_name: "Hartmann",
city: "Augsburg",
street: "Maximilianstraße",
house_number: "4",
postal_code: "86150",
join_date: ~D[2024-11-20]
},
%{
first_name: "Uwe",
last_name: "Lange",
city: "Bonn",
street: "Remigiusstraße",
house_number: "27",
postal_code: "53111",
join_date: ~D[2024-12-01]
}
]
# Fee type index per member: 0..4 round-robin for all 20 (each type used 4 times)
# Cycle status: all_paid, all_unpaid, mixed (varied)
cycle_statuses = [
:all_paid,
:all_unpaid,
:mixed,
:all_paid,
:mixed,
:all_unpaid,
:all_paid,
:mixed,
:all_unpaid,
:all_paid,
:mixed,
:all_paid,
:all_unpaid,
:mixed,
:all_paid,
:mixed,
:all_unpaid,
:all_paid,
:mixed,
:all_paid
]
# Indices of members that get an exit date (5 distributed: 3, 7, 11, 15, 19)
exit_date_member_indices = [3, 7, 11, 15, 19]
Enum.with_index(member_configs)
|> Enum.each(fn {config, index} ->
email = "mitglied#{index + 1}@example.de"
fee_type_index = rem(index, length(all_fee_types))
fee_type_id = Enum.at(all_fee_types, fee_type_index).id
cycle_status = Enum.at(cycle_statuses, index)
# Set fee type at create so cycles are generated with correct interval (no interval-change conflict)
base_attrs = %{
first_name: config.first_name,
last_name: config.last_name,
email: email,
join_date: config.join_date,
city: config.city,
street: config.street,
house_number: config.house_number,
postal_code: config.postal_code,
country: Enum.at(countries_list, index)
}
base_attrs =
if fee_type_id,
do: Map.put(base_attrs, :membership_fee_type_id, fee_type_id),
else: base_attrs
member =
Membership.create_member!(base_attrs,
upsert?: true,
upsert_identity: :unique_email,
actor: admin_user_with_role
)
if not is_nil(member.membership_fee_type_id) and not is_nil(cycle_status) do
member_with_cycles =
Ash.load!(member, :membership_fee_cycles, actor: admin_user_with_role)
cycles =
if Enum.empty?(member_with_cycles.membership_fee_cycles) do
{:ok, new_cycles, _} =
CycleGenerator.generate_cycles_for_member(member.id,
skip_lock?: true,
actor: admin_user_with_role
)
new_cycles
else
member_with_cycles.membership_fee_cycles
end
cycles
|> Enum.sort_by(& &1.cycle_start, Date)
|> Enum.with_index()
|> Enum.each(fn {cycle, idx} ->
status =
case cycle_status do
:all_paid ->
:paid
:all_unpaid ->
:unpaid
:mixed ->
case rem(idx, 3) do
0 -> :paid
1 -> :unpaid
2 -> :suspended
end
_ ->
cycle.status
end
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
if index in exit_date_member_indices do
exit_date = Date.add(config.join_date, 365)
Membership.update_member(member, %{exit_date: exit_date}, actor: admin_user_with_role)
end
end)
# Groups (idempotent)
group_configs = [
%{name: "Vorstand", description: "Gremium Vorstand"},
%{name: "Jugend", description: "Jugendbereich"},
%{name: "Newsletter", description: "Empfänger*innen Newsletter"}
]
existing_groups =
case Membership.list_groups(actor: admin_user_with_role) do
{:ok, list} -> list
{:error, _} -> []
end
existing_names_lower = MapSet.new(existing_groups, &String.downcase(&1.name))
seed_groups =
Enum.reduce(group_configs, %{}, fn config, acc ->
name = config.name
if MapSet.member?(existing_names_lower, String.downcase(name)) do
group = Enum.find(existing_groups, &(String.downcase(&1.name) == String.downcase(name)))
Map.put(acc, name, group)
else
{:ok, group} =
Membership.create_group(%{name: name, description: config.description},
actor: admin_user_with_role
)
Map.put(acc, name, group)
end
end)
# Test users: create users linked to members (same email as member so sync is no-op),
# each with a different role for testing authorization.
all_members = Ash.read!(Membership.Member, actor: admin_user_with_role)
test_users_config = [
{"mitglied1@example.de", "Mitglied"},
{"mitglied2@example.de", "Vorstand"},
{"mitglied3@example.de", "Kassenwart"},
{"mitglied4@example.de", "Buchhaltung"}
]
roles_by_name =
Mv.Authorization.Role
|> Ash.read!(authorize?: false, domain: Mv.Authorization)
|> Map.new(&{&1.name, &1})
Enum.each(test_users_config, fn {email, role_name} ->
member = Enum.find(all_members, &(&1.email == email))
role = roles_by_name[role_name]
if not is_nil(member) and not is_nil(role) do
user =
Accounts.create_user!(
%{email: email, member: %{id: member.id}},
upsert?: true,
upsert_identity: :unique_email,
actor: admin_user_with_role
)
user
|> Ash.Changeset.for_update(:update_user, %{role_id: role.id}, domain: Mv.Accounts)
|> Ash.update!(actor: admin_user_with_role)
end
end)
# Assign some members to groups (mitglied15 to Vorstand/Newsletter etc.)
member_group_assignments = [
{"mitglied1@example.de", ["Vorstand", "Newsletter"]},
{"mitglied2@example.de", ["Jugend", "Newsletter"]},
{"mitglied3@example.de", ["Vorstand"]},
{"mitglied4@example.de", ["Newsletter"]},
{"mitglied5@example.de", ["Newsletter"]}
]
find_member = fn email -> Enum.find(all_members, &(&1.email == email)) end
Enum.each(member_group_assignments, fn {email, group_names} ->
member = find_member.(email)
if member do
Enum.each(group_names, fn group_name ->
group = seed_groups[group_name]
if group do
Membership.create_member_group(
%{member_id: member.id, group_id: group.id},
actor: admin_user_with_role
)
end
end)
end
end)
# Custom field values for ~80% of members (16 of 20): most of the 6 fields filled per member
all_custom_fields = Ash.read!(Membership.CustomField, actor: admin_user_with_role)
find_field = fn name -> Enum.find(all_custom_fields, &(&1.name == name)) end
# 16 members with 46 custom field values each (Geburtsdatum, Datenschutz, SEPA, Rechnungs-E-Mail, IBAN, Stunden)
custom_value_assignments =
Enum.map(1..16, fn n ->
email = "mitglied#{n}@example.de"
# Vary birth dates and values per index
base_date = Date.add(~D[1970-01-01], 365 * (n + 10) + rem(n * 31, 365))
values = [
{"Geburtsdatum", %{"_union_type" => "date", "_union_value" => base_date}},
{"Datenschutzerklärung akzeptiert",
%{"_union_type" => "boolean", "_union_value" => n in [1, 2, 3, 5, 7, 9, 11, 13, 15]}},
{"SEPA-Mandat", %{"_union_type" => "boolean", "_union_value" => rem(n, 3) != 0}},
{"Rechnungs-E-Mail",
%{"_union_type" => "email", "_union_value" => "rechnung#{n}@example.de"}},
{"IBAN",
%{
"_union_type" => "string",
"_union_value" =>
"DE8937040044#{String.pad_leading(to_string(rem(532013000 + n, 1_000_000_000)), 10, "0")}"
}},
{"Stunden ehrenamtlich", %{"_union_type" => "integer", "_union_value" => 5 + rem(n * 3, 25)}}
]
# Drop 02 fields per member so not all have 6 (still ~80% overall filled)
drop_count = rem(n, 3)
{email, Enum.take(values, 6 - drop_count)}
end)
for {email, values} <- custom_value_assignments do
member = find_member.(email)
if member do
Enum.each(values, fn {field_name, value} ->
field = find_field.(field_name)
if field do
Membership.CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: field.id,
value: value
})
|> Ash.create!(
upsert?: true,
upsert_identity: :unique_custom_field_per_member,
actor: admin_user_with_role
)
end
end)
end
end
# Join form: enable so membership application list is visible in dev
case Membership.get_settings() do
{:ok, settings} ->
unless settings.join_form_enabled do
Membership.update_settings(settings, %{
join_form_enabled: true,
join_form_field_ids: settings.join_form_field_ids || ["email", "first_name", "last_name", "city"],
join_form_field_required: settings.join_form_field_required || %{
"email" => true,
"first_name" => false,
"last_name" => false,
"city" => false
}
})
end
_ ->
:ok
end
# Membership applications (join requests) for UI testing: 4 submitted, 1 with extra form_data
join_request_configs = [
%{email: "antrag1@example.de", first_name: "Sandra", last_name: "Meier", form_data: %{"city" => "Berlin"}},
%{email: "antrag2@example.de", first_name: "Thomas", last_name: "Bauer", form_data: %{}},
%{email: "antrag3@example.de", first_name: "Julia", last_name: "Krause", form_data: %{"city" => "Hamburg", "notes" => "Interesse an Jugendgruppe"}},
%{email: "antrag4@example.de", first_name: "Michael", last_name: "Schmitt", form_data: %{"city" => "München"}}
]
for config <- join_request_configs do
attrs = %{
email: config.email,
first_name: config.first_name,
last_name: config.last_name,
form_data: config.form_data || %{},
schema_version: 1
}
Mv.Membership.JoinRequest
|> Ash.Changeset.for_create(:create_submitted, attrs)
|> Ash.create!(authorize?: false, domain: Mv.Membership)
end
IO.puts("✅ Dev seeds completed.")
IO.puts(" - Members: 20 with country (mostly Deutschland, 1 Österreich, 1 Schweiz), fee types distributed, 5 with exit date")
IO.puts(" - Test users: 4 linked to mitglied14 with roles Mitglied, Vorstand, Kassenwart, Buchhaltung")
IO.puts(" - Groups: Vorstand, Jugend, Newsletter (with assignments)")
IO.puts(" - Custom field values: ~80% filled (16 members, 46 fields each)")
IO.puts(" - Join form enabled; 4 membership applications (join requests) for UI testing")