# 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 by index (last 2 without fee type) 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 first 18, nil for last 2 # 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, nil ] Enum.with_index(member_configs) |> Enum.each(fn {config, index} -> email = "mitglied#{index + 1}@example.de" fee_type_index = if index >= 18, do: nil, else: rem(index, length(all_fee_types)) fee_type_id = if fee_type_index, do: Enum.at(all_fee_types, fee_type_index).id, else: nil cycle_status = Enum.at(cycle_statuses, index) # Do not include membership_fee_type_id in upsert so re-runs do not overwrite # existing assignments; set via update below only when member has none 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) } member = Membership.create_member!(base_attrs, upsert?: true, upsert_identity: :unique_email, actor: admin_user_with_role ) final_member = if is_nil(member.membership_fee_type_id) and fee_type_id do {:ok, updated} = Membership.update_member(member, %{membership_fee_type_id: fee_type_id}, actor: admin_user_with_role ) updated else member end if not is_nil(final_member.membership_fee_type_id) and not is_nil(cycle_status) do member_with_cycles = Ash.load!(final_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(final_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 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)") 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")