Update seeds: member without fee type, cycles with various statuses

Add member without membership fee type. Generate cycles for members
with fee types and set different statuses: all paid, all unpaid, and
mixed (paid/unpaid/suspended). Update tests accordingly.
This commit is contained in:
Moritz 2025-12-18 13:53:01 +01:00
parent f25e198b0e
commit 239d784f3c
Signed by: moritz
GPG key ID: 1020A035E5DD0824
2 changed files with 171 additions and 35 deletions

View file

@ -6,6 +6,7 @@
alias Mv.Membership alias Mv.Membership
alias Mv.Accounts alias Mv.Accounts
alias Mv.MembershipFees.MembershipFeeType alias Mv.MembershipFees.MembershipFeeType
alias Mv.MembershipFees.CycleGenerator
# Create example membership fee types # Create example membership fee types
for fee_type_attrs <- [ for fee_type_attrs <- [
@ -131,7 +132,10 @@ Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity
all_fee_types = MembershipFeeType |> Ash.read!() |> Enum.to_list() all_fee_types = MembershipFeeType |> Ash.read!() |> Enum.to_list()
# Create sample members for testing - use upsert to prevent duplicates # Create sample members for testing - use upsert to prevent duplicates
# Assign each member to a fee type using round-robin distribution # 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 = [ member_attrs_list = [
%{ %{
first_name: "Hans", first_name: "Hans",
@ -142,7 +146,9 @@ member_attrs_list = [
city: "München", city: "München",
street: "Hauptstraße", street: "Hauptstraße",
house_number: "42", house_number: "42",
postal_code: "80331" postal_code: "80331",
membership_fee_type_id: Enum.at(all_fee_types, 0).id,
cycle_status: :all_paid
}, },
%{ %{
first_name: "Greta", first_name: "Greta",
@ -154,7 +160,9 @@ member_attrs_list = [
street: "Lindenstraße", street: "Lindenstraße",
house_number: "17", house_number: "17",
postal_code: "20095", postal_code: "20095",
notes: "Interessiert an Fortgeschrittenen-Kursen" notes: "Interessiert an Fortgeschrittenen-Kursen",
membership_fee_type_id: Enum.at(all_fee_types, 1).id,
cycle_status: :all_unpaid
}, },
%{ %{
first_name: "Friedrich", first_name: "Friedrich",
@ -164,7 +172,9 @@ member_attrs_list = [
phone_number: "+49301122334", phone_number: "+49301122334",
city: "Berlin", city: "Berlin",
street: "Kastanienallee", street: "Kastanienallee",
house_number: "8" house_number: "8",
membership_fee_type_id: Enum.at(all_fee_types, 2).id,
cycle_status: :mixed
}, },
%{ %{
first_name: "Marianne", first_name: "Marianne",
@ -175,21 +185,74 @@ member_attrs_list = [
city: "Berlin", city: "Berlin",
street: "Kastanienallee", street: "Kastanienallee",
house_number: "8" house_number: "8"
# No membership_fee_type_id - member without fee type
} }
] ]
# Assign fee types to members using round-robin # Create members and generate cycles
Enum.with_index(member_attrs_list) Enum.each(member_attrs_list, fn member_attrs ->
|> Enum.each(fn {member_attrs, index} -> cycle_status = Map.get(member_attrs, :cycle_status)
# Round-robin assignment: cycle through fee types member_attrs_without_status = Map.delete(member_attrs, :cycle_status)
fee_type = Enum.at(all_fee_types, rem(index, length(all_fee_types)))
member_attrs_with_fee_type = Map.put(member_attrs, :membership_fee_type_id, fee_type.id)
# Use upsert to prevent duplicates based on email # Use upsert to prevent duplicates based on email
Membership.create_member!(member_attrs_with_fee_type, member =
Membership.create_member!(member_attrs_without_status,
upsert?: true, upsert?: true,
upsert_identity: :unique_email upsert_identity: :unique_email
) )
# Generate cycles if member has a fee type
if member.membership_fee_type_id do
# Load member with cycles to check if they already exist
member_with_cycles =
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(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) end)
# Create additional users for user-member linking examples # Create additional users for user-member linking examples
@ -250,13 +313,14 @@ Enum.with_index(linked_members)
# Round-robin assignment: continue cycling through fee types # Round-robin assignment: continue cycling through fee types
# Start from where previous members ended # Start from where previous members ended
fee_type_index = rem(length(member_attrs_list) + index, length(all_fee_types)) fee_type_index = rem(3 + index, length(all_fee_types))
fee_type = Enum.at(all_fee_types, fee_type_index) fee_type = Enum.at(all_fee_types, fee_type_index)
member_attrs_with_fee_type = member_attrs_with_fee_type =
Map.put(member_attrs_without_user, :membership_fee_type_id, fee_type.id) Map.put(member_attrs_without_user, :membership_fee_type_id, fee_type.id)
# Check if user already has a member # Check if user already has a member
member =
if user.member_id == nil do if user.member_id == nil do
# User is free, create member and link - use upsert to prevent duplicates # User is free, create member and link - use upsert to prevent duplicates
Membership.create_member!( Membership.create_member!(
@ -271,6 +335,43 @@ Enum.with_index(linked_members)
upsert_identity: :unique_email upsert_identity: :unique_email
) )
end end
# Generate cycles for linked members
if member.membership_fee_type_id do
# Load member with cycles to check if they already exist
member_with_cycles =
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(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) end)
# Create sample custom field values for some members # Create sample custom field values for some members

View file

@ -43,18 +43,19 @@ defmodule Mv.SeedsTest do
"CustomFields count should remain same after re-running seeds" "CustomFields count should remain same after re-running seeds"
end end
test "all members have membership fee type assigned" do test "at least one member has no membership fee type assigned" do
# Run the seeds script # Run the seeds script
assert Code.eval_file("priv/repo/seeds.exs") assert Code.eval_file("priv/repo/seeds.exs")
# Get all members # Get all members
{:ok, members} = Ash.read(Mv.Membership.Member) {:ok, members} = Ash.read(Mv.Membership.Member)
# All members should have a membership_fee_type_id # At least one member should have no membership_fee_type_id
Enum.each(members, fn member -> members_without_fee_type =
assert member.membership_fee_type_id != nil, Enum.filter(members, fn member -> member.membership_fee_type_id == nil end)
"Member #{member.first_name} #{member.last_name} should have a membership fee type assigned"
end) assert length(members_without_fee_type) > 0,
"At least one member should have no membership fee type assigned"
end end
test "each membership fee type has at least one member" do test "each membership fee type has at least one member" do
@ -65,9 +66,10 @@ defmodule Mv.SeedsTest do
{:ok, fee_types} = Ash.read(Mv.MembershipFees.MembershipFeeType) {:ok, fee_types} = Ash.read(Mv.MembershipFees.MembershipFeeType)
{:ok, members} = Ash.read(Mv.Membership.Member) {:ok, members} = Ash.read(Mv.Membership.Member)
# Group members by fee type # Group members by fee type (excluding nil)
members_by_fee_type = members_by_fee_type =
members members
|> Enum.filter(&(&1.membership_fee_type_id != nil))
|> Enum.group_by(& &1.membership_fee_type_id) |> Enum.group_by(& &1.membership_fee_type_id)
# Each fee type should have at least one member # Each fee type should have at least one member
@ -78,5 +80,38 @@ defmodule Mv.SeedsTest do
"Membership fee type #{fee_type.name} should have at least one member assigned" "Membership fee type #{fee_type.name} should have at least one member assigned"
end) end)
end end
test "members with fee types have cycles with various statuses" do
# Run the seeds script
assert Code.eval_file("priv/repo/seeds.exs")
# Get all members with fee types
{:ok, members} = Ash.read(Mv.Membership.Member)
members_with_fee_types =
members
|> Enum.filter(&(&1.membership_fee_type_id != nil))
# At least one member should have cycles
assert length(members_with_fee_types) > 0,
"At least one member should have a membership fee type"
# Check that cycles exist and have various statuses
all_cycle_statuses =
members_with_fee_types
|> Enum.flat_map(fn member ->
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id)
|> Ash.read!()
end)
|> Enum.map(& &1.status)
# At least one cycle should be paid
assert :paid in all_cycle_statuses, "At least one cycle should be paid"
# At least one cycle should be unpaid
assert :unpaid in all_cycle_statuses, "At least one cycle should be unpaid"
# At least one cycle should be suspended
assert :suspended in all_cycle_statuses, "At least one cycle should be suspended"
end
end end
end end