- Fix get_last_completed_cycle to find most recent completed cycle - Fix create_cycle helpers to delete auto-generated cycles first - Fix Ash.destroy return value handling - Fix form selectors to use specific IDs - Fix URL parameter names for filters - Fix Ash.read_one return value expectations in tests
237 lines
7 KiB
Elixir
237 lines
7 KiB
Elixir
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 = conn_with_password_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 !Enum.empty?(cycles) do
|
|
cycle = List.first(cycles)
|
|
|
|
# Switch to Membership Fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
# Change status
|
|
view
|
|
|> element("button[phx-click='mark_cycle_status'][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("#member-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
|
|
{:ok, settings} = Mv.Membership.get_settings()
|
|
|
|
settings
|
|
|> 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("#member-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}")
|
|
|
|
# Switch to Membership Fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
# 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 - Ash.read_one returns {:ok, nil} if not found
|
|
result = MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id) |> Ash.read_one()
|
|
assert result == {:ok, nil}
|
|
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}")
|
|
|
|
# Switch to Membership Fees tab
|
|
view
|
|
|> element("button[phx-click='switch_tab'][phx-value-tab='membership_fees']")
|
|
|> render_click()
|
|
|
|
# Open edit modal
|
|
view
|
|
|> element("button[phx-click='edit_cycle_amount'][phx-value-cycle_id='#{cycle.id}']")
|
|
|> render_click()
|
|
|
|
# Update amount
|
|
view
|
|
|> form("form[phx-submit='save_cycle_amount']", %{"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
|