Split seeds into bootstrap and dev-only
This commit is contained in:
parent
0b23b816fb
commit
edd8657c92
5 changed files with 824 additions and 805 deletions
|
|
@ -2,811 +2,15 @@
|
|||
#
|
||||
# mix run priv/repo/seeds.exs
|
||||
#
|
||||
# Bootstrap runs in all environments. Dev seeds (members, groups, sample data)
|
||||
# run only in dev and test.
|
||||
|
||||
alias Mv.Accounts
|
||||
alias Mv.Membership
|
||||
alias Mv.MembershipFees.CycleGenerator
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
# Always run bootstrap (fee types, custom fields, roles, admin, system user, settings)
|
||||
Code.eval_file("priv/repo/seeds_bootstrap.exs")
|
||||
|
||||
require Ash.Query
|
||||
|
||||
# Create example membership fee types (no admin user yet; skip authorization for bootstrap)
|
||||
for fee_type_attrs <- [
|
||||
%{
|
||||
name: "Standard (Jährlich)",
|
||||
amount: Decimal.new("120.00"),
|
||||
interval: :yearly,
|
||||
description: "Standard jährlicher Mitgliedsbeitrag"
|
||||
},
|
||||
%{
|
||||
name: "Standard (Halbjährlich)",
|
||||
amount: Decimal.new("65.00"),
|
||||
interval: :half_yearly,
|
||||
description: "Standard halbjährlicher Mitgliedsbeitrag"
|
||||
},
|
||||
%{
|
||||
name: "Standard (Vierteljährlich)",
|
||||
amount: Decimal.new("35.00"),
|
||||
interval: :quarterly,
|
||||
description: "Standard vierteljährlicher Mitgliedsbeitrag"
|
||||
},
|
||||
%{
|
||||
name: "Standard (Monatlich)",
|
||||
amount: Decimal.new("12.00"),
|
||||
interval: :monthly,
|
||||
description: "Standard monatlicher Mitgliedsbeitrag"
|
||||
}
|
||||
] do
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, fee_type_attrs)
|
||||
|> Ash.create!(
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_name,
|
||||
authorize?: false,
|
||||
domain: Mv.MembershipFees
|
||||
)
|
||||
# In dev and test only: run dev seeds (20 members, groups, custom field values)
|
||||
if Mix.env() in [:dev, :test] do
|
||||
Code.eval_file("priv/repo/seeds_dev.exs")
|
||||
end
|
||||
|
||||
for attrs <- [
|
||||
# Basic example fields (for testing)
|
||||
%{
|
||||
name: "String Field",
|
||||
value_type: :string,
|
||||
description: "Example for a field of type string",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Date Field",
|
||||
value_type: :date,
|
||||
description: "Example for a field of type date",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Boolean Field",
|
||||
value_type: :boolean,
|
||||
description: "Example for a field of type boolean",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Email Field",
|
||||
value_type: :email,
|
||||
description: "Example for a field of type email",
|
||||
required: false
|
||||
},
|
||||
# Realistic custom fields
|
||||
%{
|
||||
name: "Membership Number",
|
||||
value_type: :string,
|
||||
description: "Unique membership identification number",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Emergency Contact",
|
||||
value_type: :string,
|
||||
description: "Emergency contact person name and phone",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "T-Shirt Size",
|
||||
value_type: :string,
|
||||
description: "T-Shirt size for events (XS, S, M, L, XL, XXL)",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Newsletter Subscription",
|
||||
value_type: :boolean,
|
||||
description: "Whether member wants to receive newsletter",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Date of Last Medical Check",
|
||||
value_type: :date,
|
||||
description: "Date of last medical examination",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Secondary Email",
|
||||
value_type: :email,
|
||||
description: "Alternative email address",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Membership Type",
|
||||
value_type: :string,
|
||||
description: "Type of membership (e.g., Regular, Student, Senior)",
|
||||
required: false
|
||||
},
|
||||
%{
|
||||
name: "Parking Permit",
|
||||
value_type: :boolean,
|
||||
description: "Whether member has parking permit",
|
||||
required: false
|
||||
}
|
||||
] do
|
||||
# Bootstrap: no admin user yet; CustomField create requires admin, so skip authorization
|
||||
Membership.create_custom_field!(
|
||||
attrs,
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_name,
|
||||
authorize?: false
|
||||
)
|
||||
end
|
||||
|
||||
# Admin email: default for dev/test so seed_admin has a target
|
||||
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||
System.put_env("ADMIN_EMAIL", admin_email)
|
||||
|
||||
# In dev/test, set fallback password so seed_admin creates the admin user when none is set
|
||||
if Mix.env() in [:dev, :test] and is_nil(System.get_env("ADMIN_PASSWORD")) and
|
||||
is_nil(System.get_env("ADMIN_PASSWORD_FILE")) do
|
||||
System.put_env("ADMIN_PASSWORD", "testpassword")
|
||||
end
|
||||
|
||||
# Create all authorization roles (idempotent - creates only if they don't exist)
|
||||
# Roles are created using create_role_with_system_flag to allow setting is_system_role
|
||||
role_configs = [
|
||||
%{
|
||||
name: "Mitglied",
|
||||
description: "Default member role with access to own data only",
|
||||
permission_set_name: "own_data",
|
||||
is_system_role: true
|
||||
},
|
||||
%{
|
||||
name: "Vorstand",
|
||||
description: "Board member with read access to all member data",
|
||||
permission_set_name: "read_only",
|
||||
is_system_role: false
|
||||
},
|
||||
%{
|
||||
name: "Kassenwart",
|
||||
description: "Treasurer with full member and payment management",
|
||||
permission_set_name: "normal_user",
|
||||
is_system_role: false
|
||||
},
|
||||
%{
|
||||
name: "Buchhaltung",
|
||||
description: "Accounting with read-only access for auditing",
|
||||
permission_set_name: "read_only",
|
||||
is_system_role: false
|
||||
},
|
||||
%{
|
||||
name: "Admin",
|
||||
description: "Administrator with unrestricted access",
|
||||
permission_set_name: "admin",
|
||||
is_system_role: false
|
||||
}
|
||||
]
|
||||
|
||||
# Create or update each role
|
||||
Enum.each(role_configs, fn role_data ->
|
||||
# Bind role name to variable to avoid issues with ^ pinning in macros
|
||||
role_name = role_data.name
|
||||
|
||||
case Mv.Authorization.Role
|
||||
|> Ash.Query.filter(name == ^role_name)
|
||||
|> Ash.read_one(authorize?: false, domain: Mv.Authorization) do
|
||||
{:ok, existing_role} when not is_nil(existing_role) ->
|
||||
# Role exists - update if needed (preserve is_system_role)
|
||||
if existing_role.permission_set_name != role_data.permission_set_name or
|
||||
existing_role.description != role_data.description do
|
||||
existing_role
|
||||
|> Ash.Changeset.for_update(:update_role, %{
|
||||
description: role_data.description,
|
||||
permission_set_name: role_data.permission_set_name
|
||||
})
|
||||
|> Ash.update!(authorize?: false, domain: Mv.Authorization)
|
||||
end
|
||||
|
||||
{:ok, nil} ->
|
||||
# Role doesn't exist - create it
|
||||
Mv.Authorization.Role
|
||||
|> Ash.Changeset.for_create(:create_role_with_system_flag, role_data)
|
||||
|> Ash.create!(authorize?: false, domain: Mv.Authorization)
|
||||
|
||||
{:error, error} ->
|
||||
IO.puts("Warning: Failed to check for role #{role_data.name}: #{inspect(error)}")
|
||||
end
|
||||
end)
|
||||
|
||||
# Get admin role for assignment to admin user
|
||||
admin_role =
|
||||
case Mv.Authorization.Role
|
||||
|> Ash.Query.filter(name == "Admin")
|
||||
|> Ash.read_one(authorize?: false, domain: Mv.Authorization) do
|
||||
{:ok, role} when not is_nil(role) -> role
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
if is_nil(admin_role) do
|
||||
raise "Failed to create or find admin role. Cannot proceed with member seeding."
|
||||
end
|
||||
|
||||
# Create/update admin user via Release.seed_admin (uses ADMIN_EMAIL, ADMIN_PASSWORD / ADMIN_PASSWORD_FILE).
|
||||
# Reduces duplication and exercises the same path as production entrypoint.
|
||||
Mv.Release.seed_admin()
|
||||
|
||||
# Load admin user with role for use as actor in member operations
|
||||
# This ensures all member operations have proper authorization
|
||||
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)
|
||||
|
||||
{:ok, nil} ->
|
||||
raise "Admin user not found after creation/assignment"
|
||||
|
||||
{:error, error} ->
|
||||
raise "Failed to load admin user: #{inspect(error)}"
|
||||
end
|
||||
|
||||
# Create system user for systemic operations (email sync, validations, cycle generation)
|
||||
# This user is used by Mv.Helpers.SystemActor for operations that must always run
|
||||
# Email is configurable via SYSTEM_ACTOR_EMAIL environment variable
|
||||
system_user_email = Mv.Helpers.SystemActor.system_user_email()
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^system_user_email)
|
||||
|> Ash.read_one(domain: Mv.Accounts, authorize?: false) do
|
||||
{:ok, existing_system_user} when not is_nil(existing_system_user) ->
|
||||
# System user already exists - ensure it has admin role
|
||||
# Use authorize?: false for bootstrap; :update_internal bypasses system-user modification block
|
||||
existing_system_user
|
||||
|> Ash.Changeset.for_update(:update_internal, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update!(authorize?: false)
|
||||
|
||||
{:ok, nil} ->
|
||||
# System user doesn't exist - create it with admin role
|
||||
# SECURITY: System user must NOT be able to log in:
|
||||
# - No password (hashed_password = nil) - prevents password login
|
||||
# - No OIDC ID (oidc_id = nil) - prevents OIDC login
|
||||
# - This user is ONLY for internal system operations via SystemActor
|
||||
# If either hashed_password or oidc_id is set, the user could potentially log in
|
||||
# Use authorize?: false for bootstrap - system user creation happens before system actor exists
|
||||
Accounts.create_user!(%{email: system_user_email},
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_email,
|
||||
authorize?: false
|
||||
)
|
||||
|> Ash.Changeset.for_update(:update_internal, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update!(authorize?: false)
|
||||
|
||||
{:error, error} ->
|
||||
# Log error but don't fail seeds - SystemActor will fall back to admin user
|
||||
IO.puts("Warning: Failed to create system user: #{inspect(error)}")
|
||||
IO.puts("SystemActor will fall back to admin user (#{admin_email})")
|
||||
end
|
||||
|
||||
# Load all membership fee types for assignment (admin actor for authorization)
|
||||
# Sort by name to ensure deterministic order
|
||||
all_fee_types =
|
||||
MembershipFeeType
|
||||
|> Ash.Query.sort(name: :asc)
|
||||
|> Ash.read!(actor: admin_user_with_role, domain: Mv.MembershipFees)
|
||||
|> Enum.to_list()
|
||||
|
||||
# Create sample members for testing - use upsert to prevent duplicates
|
||||
# Member 1: Hans - All cycles paid
|
||||
# Member 2: Greta - All cycles unpaid
|
||||
# Member 3: Friedrich - Mixed cycles (paid, unpaid, suspended)
|
||||
# Member 4: Marianne - No membership fee type
|
||||
member_attrs_list = [
|
||||
%{
|
||||
first_name: "Hans",
|
||||
last_name: "Müller",
|
||||
email: "hans.mueller@example.de",
|
||||
join_date: ~D[2023-01-15],
|
||||
city: "München",
|
||||
street: "Hauptstraße",
|
||||
house_number: "42",
|
||||
postal_code: "80331",
|
||||
membership_fee_type_id: Enum.at(all_fee_types, 0).id,
|
||||
cycle_status: :all_paid
|
||||
},
|
||||
%{
|
||||
first_name: "Greta",
|
||||
last_name: "Schmidt",
|
||||
email: "greta.schmidt@example.de",
|
||||
join_date: ~D[2023-02-01],
|
||||
city: "Hamburg",
|
||||
street: "Lindenstraße",
|
||||
house_number: "17",
|
||||
postal_code: "20095",
|
||||
notes: "Interessiert an Fortgeschrittenen-Kursen",
|
||||
membership_fee_type_id: Enum.at(all_fee_types, 1).id,
|
||||
cycle_status: :all_unpaid
|
||||
},
|
||||
%{
|
||||
first_name: "Friedrich",
|
||||
last_name: "Wagner",
|
||||
email: "friedrich.wagner@example.de",
|
||||
join_date: ~D[2022-11-10],
|
||||
city: "Berlin",
|
||||
street: "Kastanienallee",
|
||||
house_number: "8",
|
||||
postal_code: "10435",
|
||||
membership_fee_type_id: Enum.at(all_fee_types, 2).id,
|
||||
cycle_status: :mixed
|
||||
},
|
||||
%{
|
||||
first_name: "Marianne",
|
||||
last_name: "Wagner",
|
||||
email: "marianne.wagner@example.de",
|
||||
join_date: ~D[2022-11-10],
|
||||
city: "Berlin",
|
||||
street: "Kastanienallee",
|
||||
house_number: "8",
|
||||
postal_code: "10435"
|
||||
# No membership_fee_type_id - member without fee type
|
||||
}
|
||||
]
|
||||
|
||||
# Create members and generate cycles
|
||||
Enum.each(member_attrs_list, fn member_attrs ->
|
||||
cycle_status = Map.get(member_attrs, :cycle_status)
|
||||
member_attrs_without_status = Map.delete(member_attrs, :cycle_status)
|
||||
|
||||
# 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 =
|
||||
Membership.create_member!(member_attrs_without_fee_type,
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_email,
|
||||
actor: admin_user_with_role
|
||||
)
|
||||
|
||||
# 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
|
||||
{:ok, updated} =
|
||||
Membership.update_member(
|
||||
member,
|
||||
%{
|
||||
membership_fee_type_id: member_attrs_without_status.membership_fee_type_id
|
||||
},
|
||||
actor: admin_user_with_role
|
||||
)
|
||||
|
||||
updated
|
||||
else
|
||||
member
|
||||
end
|
||||
|
||||
# Generate cycles if member has a fee type
|
||||
if final_member.membership_fee_type_id do
|
||||
# Load member with cycles to check if they already exist (actor required for auth)
|
||||
member_with_cycles =
|
||||
Ash.load!(final_member, :membership_fee_cycles, actor: admin_user_with_role)
|
||||
|
||||
# Only generate if no cycles exist yet (to avoid duplicates on re-run)
|
||||
cycles =
|
||||
if Enum.empty?(member_with_cycles.membership_fee_cycles) do
|
||||
# Generate cycles
|
||||
{:ok, new_cycles, _notifications} =
|
||||
CycleGenerator.generate_cycles_for_member(final_member.id,
|
||||
skip_lock?: true,
|
||||
actor: admin_user_with_role
|
||||
)
|
||||
|
||||
new_cycles
|
||||
else
|
||||
# Use existing cycles
|
||||
member_with_cycles.membership_fee_cycles
|
||||
end
|
||||
|
||||
# Set cycle statuses based on member type
|
||||
if cycle_status do
|
||||
cycles
|
||||
|> Enum.sort_by(& &1.cycle_start, Date)
|
||||
|> Enum.with_index()
|
||||
|> Enum.each(fn {cycle, index} ->
|
||||
status =
|
||||
case cycle_status do
|
||||
:all_paid ->
|
||||
:paid
|
||||
|
||||
:all_unpaid ->
|
||||
:unpaid
|
||||
|
||||
:mixed ->
|
||||
# Mix: first paid, second unpaid, third suspended, then repeat
|
||||
case rem(index, 3) do
|
||||
0 -> :paid
|
||||
1 -> :unpaid
|
||||
2 -> :suspended
|
||||
end
|
||||
end
|
||||
|
||||
# Only update if status is different
|
||||
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
|
||||
end)
|
||||
|
||||
# Create additional users for user-member linking examples (no password by default)
|
||||
# Only admin gets a password (admin_set_password when created); all other users have no password.
|
||||
additional_users = [
|
||||
%{email: "hans.mueller@example.de"},
|
||||
%{email: "greta.schmidt@example.de"},
|
||||
%{email: "maria.weber@example.de"},
|
||||
%{email: "thomas.klein@example.de"}
|
||||
]
|
||||
|
||||
created_users =
|
||||
Enum.map(additional_users, fn user_attrs ->
|
||||
user =
|
||||
Accounts.create_user!(user_attrs,
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_email,
|
||||
actor: admin_user_with_role
|
||||
)
|
||||
|
||||
# Reload user to ensure all fields (including member_id) are loaded
|
||||
Accounts.User
|
||||
|> Ash.Query.filter(id == ^user.id)
|
||||
|> Ash.read_one!(domain: Mv.Accounts, actor: admin_user_with_role)
|
||||
end)
|
||||
|
||||
# Create members with linked users to demonstrate the 1:1 relationship
|
||||
# Only create if users don't already have members
|
||||
linked_members = [
|
||||
%{
|
||||
first_name: "Maria",
|
||||
last_name: "Weber",
|
||||
email: "maria.weber@example.de",
|
||||
join_date: ~D[2023-03-15],
|
||||
city: "Frankfurt",
|
||||
street: "Goetheplatz",
|
||||
house_number: "5",
|
||||
postal_code: "60313",
|
||||
notes: "Linked to user account",
|
||||
# Link to the third user (maria.weber@example.de)
|
||||
user: Enum.at(created_users, 2)
|
||||
},
|
||||
%{
|
||||
first_name: "Thomas",
|
||||
last_name: "Klein",
|
||||
email: "thomas.klein@example.de",
|
||||
join_date: ~D[2023-04-01],
|
||||
city: "Köln",
|
||||
street: "Rheinstraße",
|
||||
house_number: "23",
|
||||
postal_code: "50667",
|
||||
notes: "Linked to user account - needs payment follow-up",
|
||||
# Link to the fourth user (thomas.klein@example.de)
|
||||
user: Enum.at(created_users, 3)
|
||||
}
|
||||
]
|
||||
|
||||
# Create the linked members - use upsert to prevent duplicates
|
||||
# Assign fee types to linked members using round-robin
|
||||
# Continue from where we left off with the previous members
|
||||
Enum.with_index(linked_members)
|
||||
|> Enum.each(fn {member_attrs, index} ->
|
||||
user = member_attrs.user
|
||||
member_attrs_without_user = Map.delete(member_attrs, :user)
|
||||
|
||||
# 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_user, :membership_fee_type_id)
|
||||
|
||||
# Check if user already has a member
|
||||
member =
|
||||
if user.member_id == nil do
|
||||
# User is free, create member and link - use upsert to prevent duplicates
|
||||
# Use authorize?: false for User lookup during relationship management (bootstrap phase)
|
||||
Membership.create_member!(
|
||||
Map.put(member_attrs_without_fee_type, :user, %{id: user.id}),
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_email,
|
||||
actor: admin_user_with_role,
|
||||
authorize?: false
|
||||
)
|
||||
else
|
||||
# User already has a member, just create the member without linking - use upsert to prevent duplicates
|
||||
Membership.create_member!(member_attrs_without_fee_type,
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_email,
|
||||
actor: admin_user_with_role
|
||||
)
|
||||
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)
|
||||
|
||||
{:ok, updated} =
|
||||
Membership.update_member(member, %{membership_fee_type_id: fee_type.id},
|
||||
actor: admin_user_with_role
|
||||
)
|
||||
|
||||
updated
|
||||
else
|
||||
member
|
||||
end
|
||||
|
||||
# Generate cycles for linked members
|
||||
if final_member.membership_fee_type_id do
|
||||
# Load member with cycles to check if they already exist (actor required for auth)
|
||||
member_with_cycles =
|
||||
Ash.load!(final_member, :membership_fee_cycles, actor: admin_user_with_role)
|
||||
|
||||
# Only generate if no cycles exist yet (to avoid duplicates on re-run)
|
||||
cycles =
|
||||
if Enum.empty?(member_with_cycles.membership_fee_cycles) do
|
||||
# Generate cycles
|
||||
{:ok, new_cycles, _notifications} =
|
||||
CycleGenerator.generate_cycles_for_member(final_member.id,
|
||||
skip_lock?: true,
|
||||
actor: admin_user_with_role
|
||||
)
|
||||
|
||||
new_cycles
|
||||
else
|
||||
# Use existing cycles
|
||||
member_with_cycles.membership_fee_cycles
|
||||
end
|
||||
|
||||
# Set some cycles to paid for linked members (mixed status)
|
||||
cycles
|
||||
|> Enum.sort_by(& &1.cycle_start, Date)
|
||||
|> Enum.with_index()
|
||||
|> Enum.each(fn {cycle, index} ->
|
||||
# Every other cycle is paid, rest unpaid
|
||||
status = if rem(index, 2) == 0, do: :paid, else: :unpaid
|
||||
|
||||
# Only update if status is different
|
||||
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)
|
||||
|
||||
# Create example groups (idempotent: create only if name does not exist)
|
||||
group_configs = [
|
||||
%{name: "Vorstand", description: "Gremium Vorstand"},
|
||||
%{name: "Trainer*innen", description: "Alle lizenzierten Trainer*innen"},
|
||||
%{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
|
||||
group =
|
||||
Membership.create_group!(%{name: name, description: config.description},
|
||||
actor: admin_user_with_role
|
||||
)
|
||||
|
||||
Map.put(acc, name, group)
|
||||
end
|
||||
end)
|
||||
|
||||
# Create sample custom field values for some members
|
||||
all_members = Ash.read!(Membership.Member, actor: admin_user_with_role)
|
||||
all_custom_fields = Ash.read!(Membership.CustomField, actor: admin_user_with_role)
|
||||
|
||||
# Helper function to find custom field by name
|
||||
find_field = fn name -> Enum.find(all_custom_fields, &(&1.name == name)) end
|
||||
find_member = fn email -> Enum.find(all_members, &(&1.email == email)) end
|
||||
|
||||
# Assign seed members to groups (idempotent: duplicate create_member_group is skipped)
|
||||
member_group_assignments = [
|
||||
{"hans.mueller@example.de", ["Vorstand", "Newsletter"]},
|
||||
{"greta.schmidt@example.de", ["Jugend", "Newsletter"]},
|
||||
{"friedrich.wagner@example.de", ["Trainer*innen"]},
|
||||
{"maria.weber@example.de", ["Newsletter"]},
|
||||
{"thomas.klein@example.de", ["Newsletter"]}
|
||||
]
|
||||
|
||||
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
|
||||
case Membership.create_member_group(
|
||||
%{member_id: member.id, group_id: group.id},
|
||||
actor: admin_user_with_role
|
||||
) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, _} -> :ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
# Add custom field values for Hans Müller
|
||||
if hans = find_member.("hans.mueller@example.de") do
|
||||
[
|
||||
{find_field.("Membership Number"),
|
||||
%{"_union_type" => "string", "_union_value" => "M-2023-001"}},
|
||||
{find_field.("T-Shirt Size"), %{"_union_type" => "string", "_union_value" => "L"}},
|
||||
{find_field.("Newsletter Subscription"),
|
||||
%{"_union_type" => "boolean", "_union_value" => true}},
|
||||
{find_field.("Membership Type"), %{"_union_type" => "string", "_union_value" => "Regular"}},
|
||||
{find_field.("Parking Permit"), %{"_union_type" => "boolean", "_union_value" => true}},
|
||||
{find_field.("Secondary Email"),
|
||||
%{"_union_type" => "email", "_union_value" => "hans.m@private.de"}}
|
||||
]
|
||||
|> Enum.each(fn {field, value} ->
|
||||
if field do
|
||||
Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: hans.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
|
||||
|
||||
# Add custom field values for Greta Schmidt
|
||||
if greta = find_member.("greta.schmidt@example.de") do
|
||||
[
|
||||
{find_field.("Membership Number"),
|
||||
%{"_union_type" => "string", "_union_value" => "M-2023-015"}},
|
||||
{find_field.("T-Shirt Size"), %{"_union_type" => "string", "_union_value" => "M"}},
|
||||
{find_field.("Newsletter Subscription"),
|
||||
%{"_union_type" => "boolean", "_union_value" => true}},
|
||||
{find_field.("Membership Type"), %{"_union_type" => "string", "_union_value" => "Student"}},
|
||||
{find_field.("Emergency Contact"),
|
||||
%{"_union_type" => "string", "_union_value" => "Anna Schmidt, +49301234567"}}
|
||||
]
|
||||
|> Enum.each(fn {field, value} ->
|
||||
if field do
|
||||
Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: greta.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
|
||||
|
||||
# Add custom field values for Friedrich Wagner
|
||||
if friedrich = find_member.("friedrich.wagner@example.de") do
|
||||
[
|
||||
{find_field.("Membership Number"),
|
||||
%{"_union_type" => "string", "_union_value" => "M-2022-042"}},
|
||||
{find_field.("T-Shirt Size"), %{"_union_type" => "string", "_union_value" => "XL"}},
|
||||
{find_field.("Newsletter Subscription"),
|
||||
%{"_union_type" => "boolean", "_union_value" => false}},
|
||||
{find_field.("Membership Type"), %{"_union_type" => "string", "_union_value" => "Senior"}},
|
||||
{find_field.("Parking Permit"), %{"_union_type" => "boolean", "_union_value" => false}},
|
||||
{find_field.("Date of Last Medical Check"),
|
||||
%{"_union_type" => "date", "_union_value" => ~D[2024-03-15]}}
|
||||
]
|
||||
|> Enum.each(fn {field, value} ->
|
||||
if field do
|
||||
Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: friedrich.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
|
||||
|
||||
# Create or update global settings (singleton)
|
||||
default_club_name = System.get_env("ASSOCIATION_NAME") || "Club Name"
|
||||
|
||||
case Membership.get_settings() do
|
||||
{:ok, existing_settings} ->
|
||||
# Settings exist, update if club_name is different from env var
|
||||
# Also ensure exit_date is set to false by default if not already configured
|
||||
updates =
|
||||
%{}
|
||||
|> then(fn acc ->
|
||||
if existing_settings.club_name != default_club_name,
|
||||
do: Map.put(acc, :club_name, default_club_name),
|
||||
else: acc
|
||||
end)
|
||||
|> then(fn acc ->
|
||||
visibility_config = existing_settings.member_field_visibility || %{}
|
||||
# Ensure exit_date is set to false if not already configured
|
||||
if not Map.has_key?(visibility_config, "exit_date") and
|
||||
not Map.has_key?(visibility_config, :exit_date) do
|
||||
updated_visibility = Map.put(visibility_config, "exit_date", false)
|
||||
Map.put(acc, :member_field_visibility, updated_visibility)
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
if map_size(updates) > 0 do
|
||||
{:ok, _updated} = Membership.update_settings(existing_settings, updates)
|
||||
end
|
||||
|
||||
{:ok, nil} ->
|
||||
# Settings don't exist yet, create with exit_date defaulting to false
|
||||
{:ok, _settings} =
|
||||
Membership.Setting
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
club_name: default_club_name,
|
||||
member_field_visibility: %{"exit_date" => false}
|
||||
})
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
IO.puts("✅ Seeds completed successfully!")
|
||||
IO.puts("📝 Created sample data:")
|
||||
IO.puts(" - Global settings: club_name = #{default_club_name}")
|
||||
IO.puts(" - Membership fee types: 4 types (Yearly, Half-yearly, Quarterly, Monthly)")
|
||||
IO.puts(" - Custom fields: 12 fields (String, Date, Boolean, Email, + 8 realistic fields)")
|
||||
|
||||
password_configured =
|
||||
System.get_env("ADMIN_PASSWORD") != nil or System.get_env("ADMIN_PASSWORD_FILE") != nil
|
||||
|
||||
IO.puts(
|
||||
" - Admin user: #{admin_email} (password: #{if password_configured, do: "set", else: "not set"})"
|
||||
)
|
||||
|
||||
IO.puts(" - Sample members: Hans, Greta, Friedrich")
|
||||
IO.puts(" - Groups: Vorstand, Trainer*innen, Jugend, Newsletter (with members assigned)")
|
||||
|
||||
IO.puts(
|
||||
" - Additional users: hans.mueller@example.de, greta.schmidt@example.de, maria.weber@example.de, thomas.klein@example.de"
|
||||
)
|
||||
|
||||
IO.puts(
|
||||
" - Linked members: Maria Weber ↔ maria.weber@example.de, Thomas Klein ↔ thomas.klein@example.de"
|
||||
)
|
||||
|
||||
IO.puts(
|
||||
" - Custom field values: Sample data for Hans (6 fields), Greta (5 fields), Friedrich (6 fields)"
|
||||
)
|
||||
|
||||
IO.puts("🔗 Visit the application to see user-member relationships and custom fields in action!")
|
||||
IO.puts("✅ All seeds completed.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue