525 lines
17 KiB
Elixir
525 lines
17 KiB
Elixir
# Script for populating the database. You can run it as:
|
|
#
|
|
# mix run priv/repo/seeds.exs
|
|
#
|
|
|
|
alias Mv.Membership
|
|
alias Mv.Accounts
|
|
alias Mv.MembershipFees.MembershipFeeType
|
|
alias Mv.MembershipFees.CycleGenerator
|
|
|
|
# Create example membership fee types
|
|
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)
|
|
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
|
|
Membership.create_custom_field!(
|
|
attrs,
|
|
upsert?: true,
|
|
upsert_identity: :unique_name
|
|
)
|
|
end
|
|
|
|
# Create admin user for testing
|
|
Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity: :unique_email)
|
|
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|
|
|> Ash.update!()
|
|
|
|
# Load all membership fee types for assignment
|
|
# Sort by name to ensure deterministic order
|
|
all_fee_types =
|
|
MembershipFeeType
|
|
|> Ash.Query.sort(name: :asc)
|
|
|> Ash.read!()
|
|
|> 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],
|
|
phone_number: "+49301234567",
|
|
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],
|
|
phone_number: "+49309876543",
|
|
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],
|
|
phone_number: "+49301122334",
|
|
city: "Berlin",
|
|
street: "Kastanienallee",
|
|
house_number: "8",
|
|
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],
|
|
phone_number: "+49301122334",
|
|
city: "Berlin",
|
|
street: "Kastanienallee",
|
|
house_number: "8"
|
|
# 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
|
|
)
|
|
|
|
# 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
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{
|
|
membership_fee_type_id: member_attrs_without_status.membership_fee_type_id
|
|
})
|
|
|> Ash.update!()
|
|
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
|
|
member_with_cycles =
|
|
final_member
|
|
|> Ash.load!(:membership_fee_cycles)
|
|
|
|
# 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)
|
|
|
|
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!()
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
end)
|
|
|
|
# Create additional users for user-member linking examples
|
|
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 ->
|
|
Accounts.create_user!(user_attrs, upsert?: true, upsert_identity: :unique_email)
|
|
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|
|
|> Ash.update!()
|
|
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],
|
|
phone_number: "+49301357924",
|
|
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],
|
|
phone_number: "+49302468135",
|
|
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
|
|
Membership.create_member!(
|
|
Map.put(member_attrs_without_fee_type, :user, %{id: user.id}),
|
|
upsert?: true,
|
|
upsert_identity: :unique_email
|
|
)
|
|
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
|
|
)
|
|
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)
|
|
|
|
member
|
|
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
|
|> Ash.update!()
|
|
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
|
|
member_with_cycles =
|
|
final_member
|
|
|> Ash.load!(:membership_fee_cycles)
|
|
|
|
# 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)
|
|
|
|
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!()
|
|
end
|
|
end)
|
|
end
|
|
end)
|
|
|
|
# Create sample custom field values for some members
|
|
all_members = Ash.read!(Membership.Member)
|
|
all_custom_fields = Ash.read!(Membership.CustomField)
|
|
|
|
# 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
|
|
|
|
# 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)
|
|
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)
|
|
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)
|
|
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
|
|
if existing_settings.club_name != default_club_name do
|
|
{:ok, _updated} =
|
|
Membership.update_settings(existing_settings, %{club_name: default_club_name})
|
|
end
|
|
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)")
|
|
IO.puts(" - Admin user: admin@mv.local (password: testpassword)")
|
|
IO.puts(" - Sample members: Hans, Greta, Friedrich")
|
|
|
|
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!")
|