Membership Fee 6 - UI Components & LiveViews closes #280 #304
2 changed files with 171 additions and 35 deletions
|
|
@ -6,6 +6,7 @@
|
|||
alias Mv.Membership
|
||||
alias Mv.Accounts
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.MembershipFees.CycleGenerator
|
||||
|
||||
# Create example membership fee types
|
||||
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()
|
||||
|
||||
# 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 = [
|
||||
%{
|
||||
first_name: "Hans",
|
||||
|
|
@ -142,7 +146,9 @@ member_attrs_list = [
|
|||
city: "München",
|
||||
street: "Hauptstraße",
|
||||
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",
|
||||
|
|
@ -154,7 +160,9 @@ member_attrs_list = [
|
|||
street: "Lindenstraße",
|
||||
house_number: "17",
|
||||
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",
|
||||
|
|
@ -164,7 +172,9 @@ member_attrs_list = [
|
|||
phone_number: "+49301122334",
|
||||
city: "Berlin",
|
||||
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",
|
||||
|
|
@ -175,21 +185,74 @@ member_attrs_list = [
|
|||
city: "Berlin",
|
||||
street: "Kastanienallee",
|
||||
house_number: "8"
|
||||
# No membership_fee_type_id - member without fee type
|
||||
}
|
||||
]
|
||||
|
||||
# Assign fee types to members using round-robin
|
||||
Enum.with_index(member_attrs_list)
|
||||
|> Enum.each(fn {member_attrs, index} ->
|
||||
# Round-robin assignment: cycle through fee types
|
||||
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)
|
||||
# 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
|
||||
Membership.create_member!(member_attrs_with_fee_type,
|
||||
member =
|
||||
Membership.create_member!(member_attrs_without_status,
|
||||
upsert?: true,
|
||||
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)
|
||||
|
||||
# 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
|
||||
# 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)
|
||||
|
||||
member_attrs_with_fee_type =
|
||||
Map.put(member_attrs_without_user, :membership_fee_type_id, 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!(
|
||||
|
|
@ -271,6 +335,43 @@ Enum.with_index(linked_members)
|
|||
upsert_identity: :unique_email
|
||||
)
|
||||
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)
|
||||
|
||||
# Create sample custom field values for some members
|
||||
|
|
|
|||
|
|
@ -43,18 +43,19 @@ defmodule Mv.SeedsTest do
|
|||
"CustomFields count should remain same after re-running seeds"
|
||||
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
|
||||
assert Code.eval_file("priv/repo/seeds.exs")
|
||||
|
||||
# Get all members
|
||||
{:ok, members} = Ash.read(Mv.Membership.Member)
|
||||
|
||||
# All members should have a membership_fee_type_id
|
||||
Enum.each(members, fn member ->
|
||||
assert member.membership_fee_type_id != nil,
|
||||
"Member #{member.first_name} #{member.last_name} should have a membership fee type assigned"
|
||||
end)
|
||||
# At least one member should have no membership_fee_type_id
|
||||
members_without_fee_type =
|
||||
Enum.filter(members, fn member -> member.membership_fee_type_id == nil end)
|
||||
|
||||
assert length(members_without_fee_type) > 0,
|
||||
"At least one member should have no membership fee type assigned"
|
||||
end
|
||||
|
||||
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, members} = Ash.read(Mv.Membership.Member)
|
||||
|
||||
# Group members by fee type
|
||||
# Group members by fee type (excluding nil)
|
||||
members_by_fee_type =
|
||||
members
|
||||
|> Enum.filter(&(&1.membership_fee_type_id != nil))
|
||||
|> Enum.group_by(& &1.membership_fee_type_id)
|
||||
|
||||
# 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"
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue