test: add comprehensive tests for membership fee UI components
Add tests for all membership fee UI components following TDD principles:
This commit is contained in:
parent
bc989422e2
commit
5789079ab0
9 changed files with 1695 additions and 0 deletions
197
test/mv_web/helpers/membership_fee_helpers_test.exs
Normal file
197
test/mv_web/helpers/membership_fee_helpers_test.exs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
||||
@moduledoc """
|
||||
Tests for MembershipFeeHelpers module.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias MvWeb.Helpers.MembershipFeeHelpers
|
||||
alias Mv.MembershipFees.CalendarCycles
|
||||
|
||||
describe "format_currency/1" do
|
||||
test "formats decimal amount correctly" do
|
||||
assert MembershipFeeHelpers.format_currency(Decimal.new("60.00")) == "60,00 €"
|
||||
assert MembershipFeeHelpers.format_currency(Decimal.new("5.5")) == "5,50 €"
|
||||
assert MembershipFeeHelpers.format_currency(Decimal.new("100")) == "100,00 €"
|
||||
assert MembershipFeeHelpers.format_currency(Decimal.new("0.99")) == "0,99 €"
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_interval/1" do
|
||||
test "formats all interval types correctly" do
|
||||
assert MembershipFeeHelpers.format_interval(:monthly) == "Monthly"
|
||||
assert MembershipFeeHelpers.format_interval(:quarterly) == "Quarterly"
|
||||
assert MembershipFeeHelpers.format_interval(:half_yearly) == "Half-yearly"
|
||||
assert MembershipFeeHelpers.format_interval(:yearly) == "Yearly"
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_cycle_range/2" do
|
||||
test "formats yearly cycle range correctly" do
|
||||
cycle_start = ~D[2024-01-01]
|
||||
interval = :yearly
|
||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
||||
|
||||
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
||||
assert result =~ "2024"
|
||||
assert result =~ "01.01"
|
||||
assert result =~ "31.12"
|
||||
end
|
||||
|
||||
test "formats quarterly cycle range correctly" do
|
||||
cycle_start = ~D[2024-01-01]
|
||||
interval = :quarterly
|
||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
||||
|
||||
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
||||
assert result =~ "2024"
|
||||
assert result =~ "01.01"
|
||||
assert result =~ "31.03"
|
||||
end
|
||||
|
||||
test "formats monthly cycle range correctly" do
|
||||
cycle_start = ~D[2024-03-01]
|
||||
interval = :monthly
|
||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
||||
|
||||
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
||||
assert result =~ "2024"
|
||||
assert result =~ "01.03"
|
||||
assert result =~ "31.03"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_last_completed_cycle/2" do
|
||||
test "returns last completed cycle for member" do
|
||||
# Create test data
|
||||
fee_type =
|
||||
Mv.MembershipFees.MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test Type",
|
||||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
member =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id,
|
||||
join_date: ~D[2022-01-01]
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
# Create cycles
|
||||
cycle_2022 =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
cycle_start: ~D[2022-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :paid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
cycle_2023 =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :paid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
# Assuming we're in 2024, last completed should be 2023
|
||||
last_cycle = MembershipFeeHelpers.get_last_completed_cycle(member, Date.utc_today())
|
||||
|
||||
assert last_cycle.id == cycle_2023.id
|
||||
end
|
||||
|
||||
test "returns nil if no cycles exist" do
|
||||
fee_type =
|
||||
Mv.MembershipFees.MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test Type",
|
||||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
member =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
last_cycle = MembershipFeeHelpers.get_last_completed_cycle(member, Date.utc_today())
|
||||
assert last_cycle == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_current_cycle/2" do
|
||||
test "returns current cycle for member" do
|
||||
fee_type =
|
||||
Mv.MembershipFees.MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test Type",
|
||||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
member =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test#{System.unique_integer([:positive])}@example.com",
|
||||
membership_fee_type_id: fee_type.id,
|
||||
join_date: ~D[2023-01-01]
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
today = Date.utc_today()
|
||||
current_year_start = %{today | month: 1, day: 1}
|
||||
|
||||
current_cycle =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
cycle_start: current_year_start,
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
result = MembershipFeeHelpers.get_current_cycle(member, today)
|
||||
|
||||
assert result.id == current_cycle.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "status_color/1" do
|
||||
test "returns correct color classes for statuses" do
|
||||
assert MembershipFeeHelpers.status_color(:paid) == "text-success"
|
||||
assert MembershipFeeHelpers.status_color(:unpaid) == "text-error"
|
||||
assert MembershipFeeHelpers.status_color(:suspended) == "text-base-content/60"
|
||||
end
|
||||
end
|
||||
|
||||
describe "status_icon/1" do
|
||||
test "returns correct icon names for statuses" do
|
||||
assert MembershipFeeHelpers.status_icon(:paid) == "hero-check-circle"
|
||||
assert MembershipFeeHelpers.status_icon(:unpaid) == "hero-x-circle"
|
||||
assert MembershipFeeHelpers.status_icon(:suspended) == "hero-pause-circle"
|
||||
end
|
||||
end
|
||||
end
|
||||
210
test/mv_web/live/membership_fee_type_live/form_test.exs
Normal file
210
test/mv_web/live/membership_fee_type_live/form_test.exs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||
@moduledoc """
|
||||
Tests for membership fee types create/edit form.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.Membership.Member
|
||||
|
||||
require Ash.Query
|
||||
|
||||
setup 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 = log_in_user(build_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
|
||||
|
||||
describe "create form" do
|
||||
test "creates new membership fee type", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types/new")
|
||||
|
||||
form_data = %{
|
||||
"membership_fee_type[name]" => "New Type",
|
||||
"membership_fee_type[amount]" => "75.00",
|
||||
"membership_fee_type[interval]" => "yearly",
|
||||
"membership_fee_type[description]" => "Test description"
|
||||
}
|
||||
|
||||
{:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|> form("form", form_data)
|
||||
|> render_submit()
|
||||
|
||||
assert to == "/membership_fee_types"
|
||||
|
||||
# Verify type was created
|
||||
type =
|
||||
MembershipFeeType
|
||||
|> Ash.Query.filter(name == "New Type")
|
||||
|> Ash.read_one!()
|
||||
|
||||
assert type.amount == Decimal.new("75.00")
|
||||
assert type.interval == :yearly
|
||||
end
|
||||
|
||||
test "interval field is editable on create", %{conn: conn} do
|
||||
{:ok, view, html} = live(conn, "/membership_fee_types/new")
|
||||
|
||||
# Interval field should be editable (not disabled)
|
||||
refute html =~ "disabled" || html =~ "readonly"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit form" do
|
||||
test "loads existing type data", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{name: "Existing Type", amount: Decimal.new("60.00")})
|
||||
|
||||
{:ok, view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
||||
|
||||
assert html =~ "Existing Type"
|
||||
assert html =~ "60" || html =~ "60,00"
|
||||
end
|
||||
|
||||
test "interval field is grayed out on edit", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
{:ok, view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
||||
|
||||
# Interval field should be disabled
|
||||
assert html =~ "disabled" || html =~ "readonly"
|
||||
end
|
||||
|
||||
test "amount change warning displays on edit", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
|
||||
create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
||||
|
||||
# Change amount
|
||||
html =
|
||||
view
|
||||
|> form("form", %{"membership_fee_type[amount]" => "75.00"})
|
||||
|> render_change()
|
||||
|
||||
# Should show warning
|
||||
assert html =~ "Warning" || html =~ "Warnung" || html =~ "affected"
|
||||
end
|
||||
|
||||
test "amount change warning shows correct affected member count", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
|
||||
|
||||
# Create 3 members
|
||||
Enum.each(1..3, fn _ ->
|
||||
create_member(%{membership_fee_type_id: fee_type.id})
|
||||
end)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
||||
|
||||
# Change amount
|
||||
html =
|
||||
view
|
||||
|> form("form", %{"membership_fee_type[amount]" => "75.00"})
|
||||
|> render_change()
|
||||
|
||||
# Should show affected count
|
||||
assert html =~ "3" || html =~ "members" || html =~ "Mitglieder"
|
||||
end
|
||||
|
||||
test "amount change can be confirmed", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
||||
|
||||
# Change amount and confirm
|
||||
view
|
||||
|> form("form", %{"membership_fee_type[amount]" => "75.00"})
|
||||
|> render_change()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='confirm_amount_change']")
|
||||
|> render_click()
|
||||
|
||||
# Amount should be updated
|
||||
updated_type = Ash.read_one!(MembershipFeeType |> Ash.Query.filter(id == ^fee_type.id))
|
||||
assert updated_type.amount == Decimal.new("75.00")
|
||||
end
|
||||
|
||||
test "amount change can be cancelled", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
||||
|
||||
# Change amount and cancel
|
||||
view
|
||||
|> form("form", %{"membership_fee_type[amount]" => "75.00"})
|
||||
|> render_change()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='cancel_amount_change']")
|
||||
|> render_click()
|
||||
|
||||
# Amount should remain unchanged
|
||||
updated_type = Ash.read_one!(MembershipFeeType |> Ash.Query.filter(id == ^fee_type.id))
|
||||
assert updated_type.amount == Decimal.new("50.00")
|
||||
end
|
||||
|
||||
test "validation errors display correctly", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types/new")
|
||||
|
||||
# Submit with invalid data
|
||||
html =
|
||||
view
|
||||
|> form("form", %{"membership_fee_type[name]" => "", "membership_fee_type[amount]" => ""})
|
||||
|> render_submit()
|
||||
|
||||
# Should show validation errors
|
||||
assert html =~ "can't be blank" || html =~ "darf nicht leer sein" || html =~ "required"
|
||||
end
|
||||
end
|
||||
|
||||
describe "permissions" do
|
||||
test "only admin can access", %{conn: conn} do
|
||||
# This test assumes non-admin users cannot access
|
||||
{:ok, _view, html} = live(conn, "/membership_fee_types/new")
|
||||
|
||||
# Should show the form (admin user in setup)
|
||||
assert html =~ "Membership Fee Type" || html =~ "Beitragsart"
|
||||
end
|
||||
end
|
||||
end
|
||||
152
test/mv_web/live/membership_fee_type_live/index_test.exs
Normal file
152
test/mv_web/live/membership_fee_type_live/index_test.exs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
|
||||
@moduledoc """
|
||||
Tests for membership fee types list view.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.Membership.Member
|
||||
|
||||
require Ash.Query
|
||||
|
||||
setup 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 = log_in_user(build_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
|
||||
|
||||
describe "list display" do
|
||||
test "displays all membership fee types with correct data", %{conn: conn} do
|
||||
fee_type1 =
|
||||
create_fee_type(%{name: "Regular", amount: Decimal.new("60.00"), interval: :yearly})
|
||||
|
||||
fee_type2 =
|
||||
create_fee_type(%{name: "Reduced", amount: Decimal.new("30.00"), interval: :yearly})
|
||||
|
||||
{:ok, view, html} = live(conn, "/membership_fee_types")
|
||||
|
||||
assert html =~ "Regular"
|
||||
assert html =~ "Reduced"
|
||||
assert html =~ "60" || html =~ "60,00"
|
||||
assert html =~ "30" || html =~ "30,00"
|
||||
assert html =~ "Yearly" || html =~ "Jährlich"
|
||||
end
|
||||
|
||||
test "member count column shows correct count", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
# Create 3 members with this fee type
|
||||
Enum.each(1..3, fn _ ->
|
||||
create_member(%{membership_fee_type_id: fee_type.id})
|
||||
end)
|
||||
|
||||
{:ok, view, html} = live(conn, "/membership_fee_types")
|
||||
|
||||
assert html =~ "3" || html =~ "Members" || html =~ "Mitglieder"
|
||||
end
|
||||
|
||||
test "create button navigates to form", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types")
|
||||
|
||||
{:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|> element("a[href='/membership_fee_types/new']")
|
||||
|> render_click()
|
||||
|
||||
assert to == "/membership_fee_types/new"
|
||||
end
|
||||
|
||||
test "edit button per row navigates to edit form", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types")
|
||||
|
||||
{:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|> element("a[href='/membership_fee_types/#{fee_type.id}/edit']")
|
||||
|> render_click()
|
||||
|
||||
assert to == "/membership_fee_types/#{fee_type.id}/edit"
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete functionality" do
|
||||
test "delete button disabled if type is in use", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
{:ok, view, html} = live(conn, "/membership_fee_types")
|
||||
|
||||
# Delete button should be disabled
|
||||
assert html =~ "disabled" || html =~ "cursor-not-allowed"
|
||||
end
|
||||
|
||||
test "delete button works if type is not in use", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
# No members assigned
|
||||
|
||||
{:ok, view, _html} = live(conn, "/membership_fee_types")
|
||||
|
||||
# Delete button should be enabled
|
||||
view
|
||||
|> element("button[phx-click='delete'][phx-value-id='#{fee_type.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Type should be deleted
|
||||
assert_raise Ash.Error.Query.NotFound, fn ->
|
||||
Ash.read_one!(MembershipFeeType |> Ash.Query.filter(id == ^fee_type.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "permissions" do
|
||||
test "only admin can access", %{conn: conn} do
|
||||
# This test assumes non-admin users cannot access
|
||||
# Adjust based on actual permission implementation
|
||||
{:ok, _view, html} = live(conn, "/membership_fee_types")
|
||||
|
||||
# Should show the page (admin user in setup)
|
||||
assert html =~ "Membership Fee Types" || html =~ "Beitragsarten"
|
||||
end
|
||||
end
|
||||
end
|
||||
167
test/mv_web/member_live/form_membership_fee_type_test.exs
Normal file
167
test/mv_web/member_live/form_membership_fee_type_test.exs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||
@moduledoc """
|
||||
Tests for membership fee type dropdown in member form.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Membership.Member
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
|
||||
require Ash.Query
|
||||
|
||||
setup 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 = log_in_user(build_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
|
||||
|
||||
describe "membership fee type dropdown" do
|
||||
test "displays in form", %{conn: conn} do
|
||||
{:ok, view, html} = live(conn, "/members/new")
|
||||
|
||||
# Should show membership fee type dropdown
|
||||
assert html =~ "membership_fee_type_id" || html =~ "Membership Fee Type" ||
|
||||
html =~ "Beitragsart"
|
||||
end
|
||||
|
||||
test "shows available types", %{conn: conn} do
|
||||
fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly})
|
||||
fee_type2 = create_fee_type(%{name: "Type 2", interval: :yearly})
|
||||
|
||||
{:ok, view, html} = live(conn, "/members/new")
|
||||
|
||||
assert html =~ "Type 1"
|
||||
assert html =~ "Type 2"
|
||||
end
|
||||
|
||||
test "filters to same interval types if member has type", %{conn: conn} do
|
||||
yearly_type = create_fee_type(%{name: "Yearly Type", interval: :yearly})
|
||||
monthly_type = create_fee_type(%{name: "Monthly Type", interval: :monthly})
|
||||
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
||||
|
||||
{:ok, view, html} = live(conn, "/members/#{member.id}/edit")
|
||||
|
||||
# Should show yearly type but not monthly
|
||||
assert html =~ "Yearly Type"
|
||||
refute html =~ "Monthly Type"
|
||||
end
|
||||
|
||||
test "shows warning if different interval selected", %{conn: conn} do
|
||||
yearly_type = create_fee_type(%{name: "Yearly Type", interval: :yearly})
|
||||
monthly_type = create_fee_type(%{name: "Monthly Type", interval: :monthly})
|
||||
|
||||
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
|
||||
|
||||
# Try to select monthly type (should show warning)
|
||||
html =
|
||||
view
|
||||
|> form("form", %{"member[membership_fee_type_id]" => monthly_type.id})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "Warning" || html =~ "Warnung" || html =~ "not allowed"
|
||||
end
|
||||
|
||||
test "warning cleared if same interval selected", %{conn: conn} do
|
||||
yearly_type1 = create_fee_type(%{name: "Yearly Type 1", interval: :yearly})
|
||||
yearly_type2 = create_fee_type(%{name: "Yearly Type 2", interval: :yearly})
|
||||
|
||||
member = create_member(%{membership_fee_type_id: yearly_type1.id})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
|
||||
|
||||
# Select another yearly type (should not show warning)
|
||||
html =
|
||||
view
|
||||
|> form("form", %{"member[membership_fee_type_id]" => yearly_type2.id})
|
||||
|> render_change()
|
||||
|
||||
refute html =~ "Warning" || html =~ "Warnung"
|
||||
end
|
||||
|
||||
test "form saves with selected membership fee type", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/new")
|
||||
|
||||
form_data = %{
|
||||
"member[first_name]" => "Test",
|
||||
"member[last_name]" => "Member",
|
||||
"member[email]" => "test#{System.unique_integer([:positive])}@example.com",
|
||||
"member[membership_fee_type_id]" => fee_type.id
|
||||
}
|
||||
|
||||
{:error, {:live_redirect, %{to: _to}}} =
|
||||
view
|
||||
|> form("form", form_data)
|
||||
|> render_submit()
|
||||
|
||||
# Verify member was created with fee type
|
||||
member =
|
||||
Member
|
||||
|> Ash.Query.filter(email == ^form_data["member[email]"])
|
||||
|> Ash.read_one!()
|
||||
|
||||
assert member.membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
||||
test "new members get default membership fee type", %{conn: conn} do
|
||||
# Set default fee type in settings
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
Mv.Membership.Setting
|
||||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/new")
|
||||
|
||||
# Form should have default fee type selected
|
||||
html = render(view)
|
||||
assert html =~ fee_type.name || html =~ "selected"
|
||||
end
|
||||
end
|
||||
end
|
||||
153
test/mv_web/member_live/index/membership_fee_status_test.exs
Normal file
153
test/mv_web/member_live/index/membership_fee_status_test.exs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
||||
@moduledoc """
|
||||
Tests for MembershipFeeStatus helper module.
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias MvWeb.MemberLive.Index.MembershipFeeStatus
|
||||
alias Mv.Membership.Member
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
|
||||
require Ash.Query
|
||||
|
||||
# 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 "load_cycles_for_members/2" do
|
||||
test "efficiently loads cycles for members" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
create_cycle(member1, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
||||
create_cycle(member2, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
|
||||
|
||||
query =
|
||||
Member
|
||||
|> Ash.Query.filter(id in [^member1.id, ^member2.id])
|
||||
|> MembershipFeeStatus.load_cycles_for_members()
|
||||
|
||||
members = Ash.read!(query)
|
||||
|
||||
assert length(members) == 2
|
||||
|
||||
# Verify cycles are loaded
|
||||
member1_loaded = Enum.find(members, &(&1.id == member1.id))
|
||||
member2_loaded = Enum.find(members, &(&1.id == member2.id))
|
||||
|
||||
assert member1_loaded.membership_fee_cycles != nil
|
||||
assert member2_loaded.membership_fee_cycles != nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_cycle_status_for_member/2" do
|
||||
test "returns status of last completed cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2022-01-01], status: :paid})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
status = MembershipFeeStatus.get_cycle_status_for_member(member, today, false)
|
||||
|
||||
# Should return status of 2023 cycle (last completed)
|
||||
assert status == :unpaid
|
||||
end
|
||||
|
||||
test "returns status of current cycle when show_current is true" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2024-01-01], status: :suspended})
|
||||
|
||||
today = ~D[2024-06-15]
|
||||
status = MembershipFeeStatus.get_cycle_status_for_member(member, today, true)
|
||||
|
||||
# Should return status of 2024 cycle (current)
|
||||
assert status == :suspended
|
||||
end
|
||||
|
||||
test "returns nil if no cycles exist" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
today = Date.utc_today()
|
||||
status = MembershipFeeStatus.get_cycle_status_for_member(member, today, false)
|
||||
|
||||
assert status == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_cycle_status_badge/1" do
|
||||
test "returns badge component for paid status" do
|
||||
result = MembershipFeeStatus.format_cycle_status_badge(:paid)
|
||||
assert result =~ "text-success"
|
||||
assert result =~ "hero-check-circle"
|
||||
end
|
||||
|
||||
test "returns badge component for unpaid status" do
|
||||
result = MembershipFeeStatus.format_cycle_status_badge(:unpaid)
|
||||
assert result =~ "text-error"
|
||||
assert result =~ "hero-x-circle"
|
||||
end
|
||||
|
||||
test "returns badge component for suspended status" do
|
||||
result = MembershipFeeStatus.format_cycle_status_badge(:suspended)
|
||||
assert result =~ "text-base-content/60"
|
||||
assert result =~ "hero-pause-circle"
|
||||
end
|
||||
|
||||
test "handles nil status gracefully" do
|
||||
result = MembershipFeeStatus.format_cycle_status_badge(nil)
|
||||
assert result =~ "text-base-content/60"
|
||||
end
|
||||
end
|
||||
end
|
||||
226
test/mv_web/member_live/index_membership_fee_status_test.exs
Normal file
226
test/mv_web/member_live/index_membership_fee_status_test.exs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
||||
@moduledoc """
|
||||
Tests for membership fee status column in member list 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 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 = log_in_user(build_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 "status column display" do
|
||||
test "shows status column in member list", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
||||
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Should show membership fee status column
|
||||
assert html =~ "Membership Fee Status" || html =~ "Mitgliedsbeitrag Status"
|
||||
end
|
||||
|
||||
test "shows last completed cycle status by default", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2022-01-01], status: :paid})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Should show unpaid status (2023 is last completed)
|
||||
html = render(view)
|
||||
assert html =~ "hero-x-circle" || html =~ "unpaid"
|
||||
end
|
||||
|
||||
test "toggle switches to current cycle view", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
today = Date.utc_today()
|
||||
current_year_start = %{today | month: 1, day: 1}
|
||||
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
||||
create_cycle(member, fee_type, %{cycle_start: current_year_start, status: :suspended})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Toggle to current cycle
|
||||
view
|
||||
|> element("button[phx-click='toggle_current_cycle']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
# Should show suspended status (current cycle)
|
||||
assert html =~ "hero-pause-circle" || html =~ "suspended"
|
||||
end
|
||||
|
||||
test "shows correct color coding for paid status", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "text-success" || html =~ "hero-check-circle"
|
||||
end
|
||||
|
||||
test "shows correct color coding for unpaid status", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "text-error" || html =~ "hero-x-circle"
|
||||
end
|
||||
|
||||
test "shows correct color coding for suspended status", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :suspended})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "text-base-content/60" || html =~ "hero-pause-circle"
|
||||
end
|
||||
|
||||
test "handles members without cycles gracefully", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
# No cycles created
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
html = render(view)
|
||||
# Should not crash, may show empty or default state
|
||||
assert html =~ member.first_name
|
||||
end
|
||||
end
|
||||
|
||||
describe "filters" do
|
||||
test "filter unpaid in last cycle works", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
# Member with unpaid last cycle
|
||||
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member1, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
|
||||
|
||||
# Member with paid last cycle
|
||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member2, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members?membership_fee_status_filter=unpaid_last")
|
||||
|
||||
html = render(view)
|
||||
assert html =~ member1.first_name
|
||||
refute html =~ member2.first_name
|
||||
end
|
||||
|
||||
test "filter unpaid in current cycle works", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
today = Date.utc_today()
|
||||
current_year_start = %{today | month: 1, day: 1}
|
||||
|
||||
# Member with unpaid current cycle
|
||||
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member1, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||
|
||||
# Member with paid current cycle
|
||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members?membership_fee_status_filter=unpaid_current")
|
||||
|
||||
html = render(view)
|
||||
assert html =~ member1.first_name
|
||||
refute html =~ member2.first_name
|
||||
end
|
||||
end
|
||||
|
||||
describe "performance" do
|
||||
test "loads cycles efficiently without N+1 queries", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
# Create multiple members with cycles
|
||||
Enum.each(1..5, fn _ ->
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
||||
end)
|
||||
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Should render without errors (N+1 would cause performance issues)
|
||||
assert html =~ "Members" || html =~ "Mitglieder"
|
||||
end
|
||||
end
|
||||
end
|
||||
221
test/mv_web/member_live/membership_fee_integration_test.exs
Normal file
221
test/mv_web/member_live/membership_fee_integration_test.exs
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||
@moduledoc """
|
||||
Integration tests for membership fee UI workflows.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Membership.Member
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
|
||||
require Ash.Query
|
||||
|
||||
setup 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 = log_in_user(build_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
|
||||
|
||||
describe "end-to-end workflows" do
|
||||
test "create type → assign to member → view cycles → change status", %{conn: conn} do
|
||||
# Create type
|
||||
fee_type = create_fee_type(%{name: "Regular", interval: :yearly})
|
||||
|
||||
# Assign to member
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
# View cycles
|
||||
{:ok, view, html} = live(conn, "/members/#{member.id}")
|
||||
|
||||
assert html =~ "Membership Fees" || html =~ "Mitgliedsbeiträge"
|
||||
|
||||
# Get a cycle
|
||||
cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|
||||
if length(cycles) > 0 do
|
||||
cycle = List.first(cycles)
|
||||
|
||||
# Change status
|
||||
view
|
||||
|> element("button[phx-click='mark_as_paid'][phx-value-cycle-id='#{cycle.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Verify status changed
|
||||
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
||||
assert updated_cycle.status == :paid
|
||||
end
|
||||
end
|
||||
|
||||
test "change member type → cycles regenerate", %{conn: conn} do
|
||||
fee_type1 =
|
||||
create_fee_type(%{name: "Type 1", interval: :yearly, amount: Decimal.new("50.00")})
|
||||
|
||||
fee_type2 =
|
||||
create_fee_type(%{name: "Type 2", interval: :yearly, amount: Decimal.new("75.00")})
|
||||
|
||||
member = create_member(%{membership_fee_type_id: fee_type1.id})
|
||||
|
||||
# Change type
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
|
||||
|
||||
view
|
||||
|> form("form", %{"member[membership_fee_type_id]" => fee_type2.id})
|
||||
|> render_submit()
|
||||
|
||||
# Verify cycles regenerated with new amount
|
||||
cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.Query.filter(status == :unpaid)
|
||||
|> Ash.read!()
|
||||
|
||||
# Future unpaid cycles should have new amount
|
||||
Enum.each(cycles, fn cycle ->
|
||||
if Date.compare(cycle.cycle_start, Date.utc_today()) != :lt do
|
||||
assert Decimal.equal?(cycle.amount, fee_type2.amount)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test "update settings → new members get default type", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
|
||||
# Update settings
|
||||
Mv.Membership.Setting
|
||||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||
default_membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|> Ash.update!()
|
||||
|
||||
# Create new member
|
||||
{:ok, view, _html} = live(conn, "/members/new")
|
||||
|
||||
form_data = %{
|
||||
"member[first_name]" => "New",
|
||||
"member[last_name]" => "Member",
|
||||
"member[email]" => "new#{System.unique_integer([:positive])}@example.com"
|
||||
}
|
||||
|
||||
{:error, {:live_redirect, %{to: _to}}} =
|
||||
view
|
||||
|> form("form", form_data)
|
||||
|> render_submit()
|
||||
|
||||
# Verify member got default type
|
||||
member =
|
||||
Member
|
||||
|> Ash.Query.filter(email == ^form_data["member[email]"])
|
||||
|> Ash.read_one!()
|
||||
|
||||
assert member.membership_fee_type_id == fee_type.id
|
||||
end
|
||||
|
||||
test "delete cycle → confirmation → cycle deleted", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
cycle =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
||||
|
||||
# Delete cycle with confirmation
|
||||
view
|
||||
|> element("button[phx-click='delete_cycle'][phx-value-cycle-id='#{cycle.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Confirm deletion
|
||||
view
|
||||
|> element("button[phx-click='confirm_delete_cycle'][phx-value-cycle-id='#{cycle.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Verify cycle deleted
|
||||
assert_raise Ash.Error.Query.NotFound, fn ->
|
||||
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
||||
end
|
||||
end
|
||||
|
||||
test "edit cycle amount → modal → amount updated", %{conn: conn} do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
cycle =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
})
|
||||
|> Ash.create!()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
||||
|
||||
# Open edit modal
|
||||
view
|
||||
|> element("button[phx-click='edit_cycle_amount'][phx-value-cycle-id='#{cycle.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Update amount
|
||||
view
|
||||
|> form("form", %{"amount" => "75.00"})
|
||||
|> render_submit()
|
||||
|
||||
# Verify amount updated
|
||||
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
||||
assert updated_cycle.amount == Decimal.new("75.00")
|
||||
end
|
||||
end
|
||||
end
|
||||
232
test/mv_web/member_live/show_membership_fees_test.exs
Normal file
232
test/mv_web/member_live/show_membership_fees_test.exs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
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 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 = log_in_user(build_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}")
|
||||
|
||||
# Should show cycles table
|
||||
assert html =~ "Membership Fees" || html =~ "Mitgliedsbeiträge"
|
||||
assert html =~ "2022" || html =~ "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}")
|
||||
|
||||
# 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 dropdown" do
|
||||
test "shows only same-interval types", %{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 but not monthly
|
||||
assert html =~ "Yearly Type"
|
||||
refute html =~ "Monthly Type"
|
||||
end
|
||||
|
||||
test "shows warning if different interval selected", %{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}")
|
||||
|
||||
# Try to select monthly type (should show warning)
|
||||
# Note: This test may need adjustment based on actual implementation
|
||||
html =
|
||||
view
|
||||
|> form("form", %{"membership_fee_type_id" => monthly_type.id})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "Warning" || html =~ "Warnung" || html =~ "not allowed"
|
||||
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}")
|
||||
|
||||
# Mark as paid
|
||||
view
|
||||
|> element("button[phx-click='mark_as_paid'][phx-value-cycle-id='#{cycle.id}']")
|
||||
|> 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}")
|
||||
|
||||
# Mark as suspended
|
||||
view
|
||||
|> element("button[phx-click='mark_as_suspended'][phx-value-cycle-id='#{cycle.id}']")
|
||||
|> 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}")
|
||||
|
||||
# Mark as unpaid
|
||||
view
|
||||
|> element("button[phx-click='mark_as_unpaid'][phx-value-cycle-id='#{cycle.id}']")
|
||||
|> 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})
|
||||
member = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
||||
|
||||
# Trigger regeneration
|
||||
view
|
||||
|> element("button[phx-click='regenerate_cycles']")
|
||||
|> render_click()
|
||||
|
||||
# Should have cycles generated
|
||||
cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue