- Set join_date in past to ensure cycles can be generated - Check for flash message to verify action completion - More reliable test that works with cycle generation logic
274 lines
8.6 KiB
Elixir
274 lines
8.6 KiB
Elixir
defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
|
@moduledoc """
|
|
Tests for membership fees section in member detail view.
|
|
"""
|
|
use MvWeb.ConnCase, async: false
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Mv.Membership.Member
|
|
alias Mv.MembershipFees.MembershipFeeType
|
|
alias Mv.MembershipFees.MembershipFeeCycle
|
|
|
|
require Ash.Query
|
|
|
|
setup %{conn: conn} do
|
|
# Create admin user
|
|
{:ok, user} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:register_with_password, %{
|
|
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
|
password: "testpassword123"
|
|
})
|
|
|> Ash.create()
|
|
|
|
conn = conn_with_password_user(conn, user)
|
|
%{conn: conn, user: user}
|
|
end
|
|
|
|
# Helper to create a membership fee type
|
|
defp create_fee_type(attrs) do
|
|
default_attrs = %{
|
|
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
|
amount: Decimal.new("50.00"),
|
|
interval: :yearly
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
|
|
MembershipFeeType
|
|
|> Ash.Changeset.for_create(:create, attrs)
|
|
|> Ash.create!()
|
|
end
|
|
|
|
# Helper to create a member
|
|
defp create_member(attrs) do
|
|
default_attrs = %{
|
|
first_name: "Test",
|
|
last_name: "Member",
|
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
|
|
Member
|
|
|> Ash.Changeset.for_create(:create_member, attrs)
|
|
|> Ash.create!()
|
|
end
|
|
|
|
# Helper to create a cycle
|
|
defp create_cycle(member, fee_type, attrs) do
|
|
default_attrs = %{
|
|
cycle_start: ~D[2023-01-01],
|
|
amount: Decimal.new("50.00"),
|
|
member_id: member.id,
|
|
membership_fee_type_id: fee_type.id,
|
|
status: :unpaid
|
|
}
|
|
|
|
attrs = Map.merge(default_attrs, attrs)
|
|
|
|
MembershipFeeCycle
|
|
|> Ash.Changeset.for_create(:create, attrs)
|
|
|> Ash.create!()
|
|
end
|
|
|
|
describe "cycles table display" do
|
|
test "displays all cycles for member", %{conn: conn} do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
_cycle1 = create_cycle(member, fee_type, %{cycle_start: ~D[2022-01-01], status: :paid})
|
|
_cycle2 = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
|
|
|
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Switch to membership fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
html = render(view)
|
|
|
|
# Should show cycles table
|
|
assert html =~ "Membership Fees" || html =~ "Mitgliedsbeiträge"
|
|
# Check for formatted cycle dates (e.g., "01.01.2022" or "2022")
|
|
assert html =~ "2022" || html =~ "2023" || html =~ "01.01.2022" || html =~ "01.01.2023"
|
|
end
|
|
|
|
test "table columns show correct data", %{conn: conn} do
|
|
fee_type = create_fee_type(%{interval: :yearly, amount: Decimal.new("60.00")})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
create_cycle(member, fee_type, %{
|
|
cycle_start: ~D[2023-01-01],
|
|
amount: Decimal.new("60.00"),
|
|
status: :paid
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Switch to membership fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
html = render(view)
|
|
|
|
# Should show interval, amount, status
|
|
assert html =~ "Yearly" || html =~ "Jährlich"
|
|
assert html =~ "60" || html =~ "60,00"
|
|
assert html =~ "paid" || html =~ "bezahlt"
|
|
end
|
|
end
|
|
|
|
describe "membership fee type display" do
|
|
test "shows assigned membership fee type", %{conn: conn} do
|
|
yearly_type = create_fee_type(%{interval: :yearly, name: "Yearly Type"})
|
|
_monthly_type = create_fee_type(%{interval: :monthly, name: "Monthly Type"})
|
|
|
|
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
|
|
|
{:ok, _view, html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Should show yearly type name
|
|
assert html =~ "Yearly Type"
|
|
end
|
|
|
|
test "shows no type message when no type assigned", %{conn: conn} do
|
|
member = create_member(%{})
|
|
|
|
{:ok, _view, html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Should show message about no type assigned
|
|
assert html =~ "No membership fee type assigned" || html =~ "No type"
|
|
end
|
|
end
|
|
|
|
describe "status change actions" do
|
|
test "mark as paid works", %{conn: conn} do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
|
|
|
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Switch to membership fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
# Mark as paid
|
|
view
|
|
|> element("button[phx-click='mark_cycle_status'][phx-value-cycle_id='#{cycle.id}'][phx-value-status='paid']")
|
|
|> render_click()
|
|
|
|
# Verify cycle is now paid
|
|
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
|
assert updated_cycle.status == :paid
|
|
end
|
|
|
|
test "mark as suspended works", %{conn: conn} do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
|
|
|
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Switch to membership fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
# Mark as suspended
|
|
view
|
|
|> element("button[phx-click='mark_cycle_status'][phx-value-cycle_id='#{cycle.id}'][phx-value-status='suspended']")
|
|
|> render_click()
|
|
|
|
# Verify cycle is now suspended
|
|
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
|
assert updated_cycle.status == :suspended
|
|
end
|
|
|
|
test "mark as unpaid works", %{conn: conn} do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
member = create_member(%{membership_fee_type_id: fee_type.id})
|
|
|
|
cycle = create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
|
|
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Switch to membership fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
# Mark as unpaid
|
|
view
|
|
|> element("button[phx-click='mark_cycle_status'][phx-value-cycle_id='#{cycle.id}'][phx-value-status='unpaid']")
|
|
|> render_click()
|
|
|
|
# Verify cycle is now unpaid
|
|
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
|
assert updated_cycle.status == :unpaid
|
|
end
|
|
end
|
|
|
|
describe "cycle regeneration" do
|
|
test "manual regeneration works", %{conn: conn} do
|
|
fee_type = create_fee_type(%{interval: :yearly})
|
|
# Create member with join_date in the past to ensure cycles can be generated
|
|
member =
|
|
create_member(%{
|
|
membership_fee_type_id: fee_type.id,
|
|
join_date: ~D[2020-01-15]
|
|
})
|
|
|
|
# Get initial cycle count (may be 0 if cycles weren't auto-generated)
|
|
initial_cycles =
|
|
MembershipFeeCycle
|
|
|> Ash.Query.filter(member_id == ^member.id)
|
|
|> Ash.read!()
|
|
|
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Switch to membership fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
# Trigger regeneration
|
|
view
|
|
|> element("button[phx-click='regenerate_cycles']")
|
|
|> render_click()
|
|
|
|
# Wait for flash message to appear (indicates action completed)
|
|
assert_has(view, "flash", text: "regenerated", count: :any)
|
|
|
|
# Check that cycles exist (should have at least some cycles for a member from 2020)
|
|
cycles =
|
|
MembershipFeeCycle
|
|
|> Ash.Query.filter(member_id == ^member.id)
|
|
|> Ash.read!()
|
|
|
|
# Should have cycles generated (at least initial ones, possibly more)
|
|
assert length(cycles) >= length(initial_cycles)
|
|
# For a member from 2020, we should definitely have cycles by now
|
|
assert length(cycles) > 0
|
|
end
|
|
end
|
|
|
|
describe "edge cases" do
|
|
test "handles members without membership fee type gracefully", %{conn: conn} do
|
|
# No fee type
|
|
member = create_member(%{})
|
|
|
|
{:ok, _view, html} = live(conn, "/members/#{member.id}")
|
|
|
|
# Should not crash
|
|
assert html =~ member.first_name
|
|
end
|
|
end
|
|
end
|