530 lines
14 KiB
Elixir
530 lines
14 KiB
Elixir
# 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, 1–2 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 (mitglied1–5 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 4–6 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 0–2 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 mitglied1–4 with roles Mitglied, Vorstand, Kassenwart, Buchhaltung")
|
||
IO.puts(" - Groups: Vorstand, Jugend, Newsletter (with assignments)")
|
||
IO.puts(" - Custom field values: ~80% filled (16 members, 4–6 fields each)")
|
||
IO.puts(" - Join form enabled; 4 membership applications (join requests) for UI testing")
|