Membership Fee 6 - UI Components & LiveViews closes #280 #304
12 changed files with 177 additions and 43 deletions
|
|
@ -102,6 +102,9 @@ defmodule Mv.Membership.Member do
|
||||||
where [changing(:user)]
|
where [changing(:user)]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Auto-assign default membership fee type if not explicitly set
|
||||||
|
change Mv.Membership.Member.Changes.SetDefaultMembershipFeeType
|
||||||
|
|
||||||
# Auto-calculate membership_fee_start_date if not manually set
|
# Auto-calculate membership_fee_start_date if not manually set
|
||||||
# Requires both join_date and membership_fee_type_id to be present
|
# Requires both join_date and membership_fee_type_id to be present
|
||||||
change Mv.MembershipFees.Changes.SetMembershipFeeStartDate
|
change Mv.MembershipFees.Changes.SetMembershipFeeStartDate
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
defmodule Mv.Membership.Member.Changes.SetDefaultMembershipFeeType do
|
||||||
|
@moduledoc """
|
||||||
|
Ash change that automatically assigns the default membership fee type to new members
|
||||||
|
if no membership_fee_type_id is explicitly provided.
|
||||||
|
|
||||||
|
This change reads the default_membership_fee_type_id from global settings and
|
||||||
|
assigns it to the member if membership_fee_type_id is nil.
|
||||||
|
"""
|
||||||
|
use Ash.Resource.Change
|
||||||
|
|
||||||
|
def change(changeset, _opts, _context) do
|
||||||
|
# Only set default if membership_fee_type_id is not already set
|
||||||
|
current_type_id = Ash.Changeset.get_attribute(changeset, :membership_fee_type_id)
|
||||||
|
|
||||||
|
if is_nil(current_type_id) do
|
||||||
|
case Mv.Membership.get_settings() do
|
||||||
|
{:ok, settings} ->
|
||||||
|
if settings.default_membership_fee_type_id do
|
||||||
|
Ash.Changeset.force_change_attribute(
|
||||||
|
changeset,
|
||||||
|
:membership_fee_type_id,
|
||||||
|
settings.default_membership_fee_type_id
|
||||||
|
)
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, _error} ->
|
||||||
|
# If settings can't be loaded, continue without default
|
||||||
|
# This prevents member creation from failing if settings are misconfigured
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -126,11 +126,17 @@ defmodule MvWeb.Helpers.MembershipFeeHelpers do
|
||||||
fee_type ->
|
fee_type ->
|
||||||
cycles = member.membership_fee_cycles || []
|
cycles = member.membership_fee_cycles || []
|
||||||
|
|
||||||
|
# Get all completed cycles (cycle_end < today)
|
||||||
|
completed_cycles =
|
||||||
cycles
|
cycles
|
||||||
|> Enum.filter(fn cycle ->
|
|> Enum.filter(fn cycle ->
|
||||||
CalendarCycles.last_completed_cycle?(cycle.cycle_start, fee_type.interval, today)
|
cycle_end = CalendarCycles.calculate_cycle_end(cycle.cycle_start, fee_type.interval)
|
||||||
|
Date.compare(today, cycle_end) == :gt
|
||||||
end)
|
end)
|
||||||
|> List.first()
|
|
||||||
|
# Return the most recent completed cycle (highest cycle_start)
|
||||||
|
completed_cycles
|
||||||
|
|> Enum.max_by(& &1.cycle_start, Date, fn -> nil end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,9 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
|
|
||||||
case Decimal.parse(amount_str) do
|
case Decimal.parse(amount_str) do
|
||||||
{amount, _} when is_struct(amount, Decimal) ->
|
{amount, _} when is_struct(amount, Decimal) ->
|
||||||
case Ash.update(cycle, :update, %{amount: amount}) do
|
case cycle
|
||||||
|
|> Ash.Changeset.for_update(:update, %{amount: amount})
|
||||||
|
|> Ash.update() do
|
||||||
{:ok, updated_cycle} ->
|
{:ok, updated_cycle} ->
|
||||||
updated_cycles = replace_cycle(socket.assigns.cycles, updated_cycle)
|
updated_cycles = replace_cycle(socket.assigns.cycles, updated_cycle)
|
||||||
|
|
||||||
|
|
@ -489,6 +491,16 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
|> assign(:deleting_cycle, nil)
|
|> assign(:deleting_cycle, nil)
|
||||||
|> put_flash(:info, gettext("Cycle deleted"))}
|
|> put_flash(:info, gettext("Cycle deleted"))}
|
||||||
|
|
||||||
|
{:ok, _destroyed} ->
|
||||||
|
# Handle case where return_destroyed? is true
|
||||||
|
updated_cycles = Enum.reject(socket.assigns.cycles, &(&1.id == cycle_id))
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:cycles, updated_cycles)
|
||||||
|
|> assign(:deleting_cycle, nil)
|
||||||
|
|> put_flash(:info, gettext("Cycle deleted"))}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
||||||
test "formats yearly cycle range correctly" do
|
test "formats yearly cycle range correctly" do
|
||||||
cycle_start = ~D[2024-01-01]
|
cycle_start = ~D[2024-01-01]
|
||||||
interval = :yearly
|
interval = :yearly
|
||||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
_cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
||||||
|
|
||||||
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
||||||
assert result =~ "2024"
|
assert result =~ "2024"
|
||||||
|
|
@ -42,7 +42,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
||||||
test "formats quarterly cycle range correctly" do
|
test "formats quarterly cycle range correctly" do
|
||||||
cycle_start = ~D[2024-01-01]
|
cycle_start = ~D[2024-01-01]
|
||||||
interval = :quarterly
|
interval = :quarterly
|
||||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
_cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
||||||
|
|
||||||
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
||||||
assert result =~ "2024"
|
assert result =~ "2024"
|
||||||
|
|
@ -53,7 +53,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
|
||||||
test "formats monthly cycle range correctly" do
|
test "formats monthly cycle range correctly" do
|
||||||
cycle_start = ~D[2024-03-01]
|
cycle_start = ~D[2024-03-01]
|
||||||
interval = :monthly
|
interval = :monthly
|
||||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
_cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)
|
||||||
|
|
||||||
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
result = MembershipFeeHelpers.format_cycle_range(cycle_start, interval)
|
||||||
assert result =~ "2024"
|
assert result =~ "2024"
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
|
|
||||||
{:error, {:live_redirect, %{to: to}}} =
|
{:error, {:live_redirect, %{to: to}}} =
|
||||||
view
|
view
|
||||||
|> form("form", form_data)
|
|> form("#membership-fee-type-form", form_data)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert to == "/membership_fee_types"
|
assert to == "/membership_fee_types"
|
||||||
|
|
@ -84,7 +84,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "interval field is editable on create", %{conn: conn} do
|
test "interval field is editable on create", %{conn: conn} do
|
||||||
{:ok, view, html} = live(conn, "/membership_fee_types/new")
|
{:ok, _view, html} = live(conn, "/membership_fee_types/new")
|
||||||
|
|
||||||
# Interval field should be editable (not disabled)
|
# Interval field should be editable (not disabled)
|
||||||
refute html =~ "disabled" || html =~ "readonly"
|
refute html =~ "disabled" || html =~ "readonly"
|
||||||
|
|
@ -95,7 +95,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
test "loads existing type data", %{conn: conn} do
|
test "loads existing type data", %{conn: conn} do
|
||||||
fee_type = create_fee_type(%{name: "Existing Type", amount: Decimal.new("60.00")})
|
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")
|
{:ok, _view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
||||||
|
|
||||||
assert html =~ "Existing Type"
|
assert html =~ "Existing Type"
|
||||||
assert html =~ "60" || html =~ "60,00"
|
assert html =~ "60" || html =~ "60,00"
|
||||||
|
|
@ -104,7 +104,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
test "interval field is grayed out on edit", %{conn: conn} do
|
test "interval field is grayed out on edit", %{conn: conn} do
|
||||||
fee_type = create_fee_type(%{interval: :yearly})
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
|
||||||
{:ok, view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
{:ok, _view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
|
||||||
|
|
||||||
# Interval field should be disabled
|
# Interval field should be disabled
|
||||||
assert html =~ "disabled" || html =~ "readonly"
|
assert html =~ "disabled" || html =~ "readonly"
|
||||||
|
|
@ -119,7 +119,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
# Change amount
|
# Change amount
|
||||||
html =
|
html =
|
||||||
view
|
view
|
||||||
|> form("form", %{"membership_fee_type[amount]" => "75.00"})
|
|> form("#membership-fee-type-form", %{"membership_fee_type[amount]" => "75.00"})
|
||||||
|> render_change()
|
|> render_change()
|
||||||
|
|
||||||
# Should show warning
|
# Should show warning
|
||||||
|
|
@ -139,7 +139,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
# Change amount
|
# Change amount
|
||||||
html =
|
html =
|
||||||
view
|
view
|
||||||
|> form("form", %{"membership_fee_type[amount]" => "75.00"})
|
|> form("#membership-fee-type-form", %{"membership_fee_type[amount]" => "75.00"})
|
||||||
|> render_change()
|
|> render_change()
|
||||||
|
|
||||||
# Should show affected count
|
# Should show affected count
|
||||||
|
|
@ -153,13 +153,18 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
|
|
||||||
# Change amount and confirm
|
# Change amount and confirm
|
||||||
view
|
view
|
||||||
|> form("form", %{"membership_fee_type[amount]" => "75.00"})
|
|> form("#membership-fee-type-form", %{"membership_fee_type[amount]" => "75.00"})
|
||||||
|> render_change()
|
|> render_change()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='confirm_amount_change']")
|
|> element("button[phx-click='confirm_amount_change']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Submit the form to actually save the change
|
||||||
|
view
|
||||||
|
|> form("#membership-fee-type-form", %{"membership_fee_type[amount]" => "75.00"})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
# Amount should be updated
|
# Amount should be updated
|
||||||
updated_type = Ash.read_one!(MembershipFeeType |> Ash.Query.filter(id == ^fee_type.id))
|
updated_type = Ash.read_one!(MembershipFeeType |> Ash.Query.filter(id == ^fee_type.id))
|
||||||
assert updated_type.amount == Decimal.new("75.00")
|
assert updated_type.amount == Decimal.new("75.00")
|
||||||
|
|
@ -172,7 +177,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
|
|
||||||
# Change amount and cancel
|
# Change amount and cancel
|
||||||
view
|
view
|
||||||
|> form("form", %{"membership_fee_type[amount]" => "75.00"})
|
|> form("#membership-fee-type-form", %{"membership_fee_type[amount]" => "75.00"})
|
||||||
|> render_change()
|
|> render_change()
|
||||||
|
|
||||||
view
|
view
|
||||||
|
|
@ -190,7 +195,10 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|
||||||
# Submit with invalid data
|
# Submit with invalid data
|
||||||
html =
|
html =
|
||||||
view
|
view
|
||||||
|> form("form", %{"membership_fee_type[name]" => "", "membership_fee_type[amount]" => ""})
|
|> form("#membership-fee-type-form", %{
|
||||||
|
"membership_fee_type[name]" => "",
|
||||||
|
"membership_fee_type[amount]" => ""
|
||||||
|
})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# Should show validation errors
|
# Should show validation errors
|
||||||
|
|
|
||||||
|
|
@ -57,13 +57,13 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
|
||||||
|
|
||||||
describe "list display" do
|
describe "list display" do
|
||||||
test "displays all membership fee types with correct data", %{conn: conn} do
|
test "displays all membership fee types with correct data", %{conn: conn} do
|
||||||
fee_type1 =
|
_fee_type1 =
|
||||||
create_fee_type(%{name: "Regular", amount: Decimal.new("60.00"), interval: :yearly})
|
create_fee_type(%{name: "Regular", amount: Decimal.new("60.00"), interval: :yearly})
|
||||||
|
|
||||||
fee_type2 =
|
_fee_type2 =
|
||||||
create_fee_type(%{name: "Reduced", amount: Decimal.new("30.00"), interval: :yearly})
|
create_fee_type(%{name: "Reduced", amount: Decimal.new("30.00"), interval: :yearly})
|
||||||
|
|
||||||
{:ok, view, html} = live(conn, "/membership_fee_types")
|
{:ok, _view, html} = live(conn, "/membership_fee_types")
|
||||||
|
|
||||||
assert html =~ "Regular"
|
assert html =~ "Regular"
|
||||||
assert html =~ "Reduced"
|
assert html =~ "Reduced"
|
||||||
|
|
@ -80,7 +80,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
|
||||||
create_member(%{membership_fee_type_id: fee_type.id})
|
create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok, view, html} = live(conn, "/membership_fee_types")
|
{:ok, _view, html} = live(conn, "/membership_fee_types")
|
||||||
|
|
||||||
assert html =~ "3" || html =~ "Members" || html =~ "Mitglieder"
|
assert html =~ "3" || html =~ "Members" || html =~ "Mitglieder"
|
||||||
end
|
end
|
||||||
|
|
@ -115,7 +115,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
|
||||||
fee_type = create_fee_type(%{interval: :yearly})
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
create_member(%{membership_fee_type_id: fee_type.id})
|
create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
|
||||||
{:ok, view, html} = live(conn, "/membership_fee_types")
|
{:ok, _view, html} = live(conn, "/membership_fee_types")
|
||||||
|
|
||||||
# Delete button should be disabled
|
# Delete button should be disabled
|
||||||
assert html =~ "disabled" || html =~ "cursor-not-allowed"
|
assert html =~ "disabled" || html =~ "cursor-not-allowed"
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
|
|
||||||
describe "membership fee type dropdown" do
|
describe "membership fee type dropdown" do
|
||||||
test "displays in form", %{conn: conn} do
|
test "displays in form", %{conn: conn} do
|
||||||
{:ok, view, html} = live(conn, "/members/new")
|
{:ok, _view, html} = live(conn, "/members/new")
|
||||||
|
|
||||||
# Should show membership fee type dropdown
|
# Should show membership fee type dropdown
|
||||||
assert html =~ "membership_fee_type_id" || html =~ "Membership Fee Type" ||
|
assert html =~ "membership_fee_type_id" || html =~ "Membership Fee Type" ||
|
||||||
|
|
@ -65,10 +65,10 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows available types", %{conn: conn} do
|
test "shows available types", %{conn: conn} do
|
||||||
fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly})
|
_fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly})
|
||||||
fee_type2 = create_fee_type(%{name: "Type 2", interval: :yearly})
|
_fee_type2 = create_fee_type(%{name: "Type 2", interval: :yearly})
|
||||||
|
|
||||||
{:ok, view, html} = live(conn, "/members/new")
|
{:ok, _view, html} = live(conn, "/members/new")
|
||||||
|
|
||||||
assert html =~ "Type 1"
|
assert html =~ "Type 1"
|
||||||
assert html =~ "Type 2"
|
assert html =~ "Type 2"
|
||||||
|
|
@ -76,11 +76,11 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|
||||||
|
|
||||||
test "filters to same interval types if member has type", %{conn: conn} do
|
test "filters to same interval types if member has type", %{conn: conn} do
|
||||||
yearly_type = create_fee_type(%{name: "Yearly Type", interval: :yearly})
|
yearly_type = create_fee_type(%{name: "Yearly Type", interval: :yearly})
|
||||||
monthly_type = create_fee_type(%{name: "Monthly Type", interval: :monthly})
|
_monthly_type = create_fee_type(%{name: "Monthly Type", interval: :monthly})
|
||||||
|
|
||||||
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
member = create_member(%{membership_fee_type_id: yearly_type.id})
|
||||||
|
|
||||||
{:ok, view, html} = live(conn, "/members/#{member.id}/edit")
|
{:ok, _view, html} = live(conn, "/members/#{member.id}/edit")
|
||||||
|
|
||||||
# Should show yearly type but not monthly
|
# Should show yearly type but not monthly
|
||||||
assert html =~ "Yearly Type"
|
assert html =~ "Yearly Type"
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
defp create_cycle(member, fee_type, attrs) do
|
defp create_cycle(member, fee_type, attrs) do
|
||||||
|
# Delete any auto-generated cycles first to avoid conflicts
|
||||||
|
existing_cycles =
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Query.filter(member_id == ^member.id)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||||
|
|
||||||
default_attrs = %{
|
default_attrs = %{
|
||||||
cycle_start: ~D[2023-01-01],
|
cycle_start: ~D[2023-01-01],
|
||||||
amount: Decimal.new("50.00"),
|
amount: Decimal.new("50.00"),
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,14 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
defp create_cycle(member, fee_type, attrs) do
|
defp create_cycle(member, fee_type, attrs) do
|
||||||
|
# Delete any auto-generated cycles first to avoid conflicts
|
||||||
|
existing_cycles =
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Query.filter(member_id == ^member.id)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||||
|
|
||||||
default_attrs = %{
|
default_attrs = %{
|
||||||
cycle_start: ~D[2023-01-01],
|
cycle_start: ~D[2023-01-01],
|
||||||
amount: Decimal.new("50.00"),
|
amount: Decimal.new("50.00"),
|
||||||
|
|
@ -178,7 +186,21 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
||||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
create_cycle(member2, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid})
|
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")
|
# Verify cycles exist in database
|
||||||
|
cycles1 =
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Query.filter(member_id == ^member1.id)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
cycles2 =
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Query.filter(member_id == ^member2.id)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
assert length(cycles1) > 0
|
||||||
|
assert length(cycles2) > 0
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members?membership_fee_filter=unpaid_last")
|
||||||
|
|
||||||
html = render(view)
|
html = render(view)
|
||||||
assert html =~ member1.first_name
|
assert html =~ member1.first_name
|
||||||
|
|
@ -199,7 +221,21 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
|
||||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :paid})
|
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/members?membership_fee_status_filter=unpaid_current")
|
# Verify cycles exist in database
|
||||||
|
cycles1 =
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Query.filter(member_id == ^member1.id)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
cycles2 =
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Query.filter(member_id == ^member2.id)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
assert length(cycles1) > 0
|
||||||
|
assert length(cycles2) > 0
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members?membership_fee_filter=unpaid_current")
|
||||||
|
|
||||||
html = render(view)
|
html = render(view)
|
||||||
assert html =~ member1.first_name
|
assert html =~ member1.first_name
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,14 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
if !Enum.empty?(cycles) do
|
if !Enum.empty?(cycles) do
|
||||||
cycle = List.first(cycles)
|
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
|
# Change status
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='mark_as_paid'][phx-value-cycle-id='#{cycle.id}']")
|
|> element("button[phx-click='mark_cycle_status'][phx-value-cycle_id='#{cycle.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Verify status changed
|
# Verify status changed
|
||||||
|
|
@ -102,7 +107,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
|
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
|
||||||
|
|
||||||
view
|
view
|
||||||
|> form("form", %{"member[membership_fee_type_id]" => fee_type2.id})
|
|> form("#member-form", %{"member[membership_fee_type_id]" => fee_type2.id})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# Verify cycles regenerated with new amount
|
# Verify cycles regenerated with new amount
|
||||||
|
|
@ -124,7 +129,9 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
fee_type = create_fee_type(%{interval: :yearly})
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
|
||||||
# Update settings
|
# Update settings
|
||||||
Mv.Membership.Setting
|
{:ok, settings} = Mv.Membership.get_settings()
|
||||||
|
|
||||||
|
settings
|
||||||
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
|> Ash.Changeset.for_update(:update_membership_fee_settings, %{
|
||||||
default_membership_fee_type_id: fee_type.id
|
default_membership_fee_type_id: fee_type.id
|
||||||
})
|
})
|
||||||
|
|
@ -141,7 +148,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
|
|
||||||
{:error, {:live_redirect, %{to: _to}}} =
|
{:error, {:live_redirect, %{to: _to}}} =
|
||||||
view
|
view
|
||||||
|> form("form", form_data)
|
|> form("#member-form", form_data)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# Verify member got default type
|
# Verify member got default type
|
||||||
|
|
@ -170,20 +177,24 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
{: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
|
# Delete cycle with confirmation
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='delete_cycle'][phx-value-cycle-id='#{cycle.id}']")
|
|> element("button[phx-click='delete_cycle'][phx-value-cycle_id='#{cycle.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Confirm deletion
|
# Confirm deletion
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='confirm_delete_cycle'][phx-value-cycle-id='#{cycle.id}']")
|
|> element("button[phx-click='confirm_delete_cycle'][phx-value-cycle_id='#{cycle.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Verify cycle deleted
|
# Verify cycle deleted - Ash.read_one returns {:ok, nil} if not found
|
||||||
assert_raise Ash.Error.Query.NotFound, fn ->
|
result = MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id) |> Ash.read_one()
|
||||||
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
|
assert result == {:ok, nil}
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "edit cycle amount → modal → amount updated", %{conn: conn} do
|
test "edit cycle amount → modal → amount updated", %{conn: conn} do
|
||||||
|
|
@ -203,14 +214,19 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
{: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
|
# Open edit modal
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='edit_cycle_amount'][phx-value-cycle-id='#{cycle.id}']")
|
|> element("button[phx-click='edit_cycle_amount'][phx-value-cycle_id='#{cycle.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Update amount
|
# Update amount
|
||||||
view
|
view
|
||||||
|> form("form", %{"amount" => "75.00"})
|
|> form("form[phx-submit='save_cycle_amount']", %{"amount" => "75.00"})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# Verify amount updated
|
# Verify amount updated
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,14 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|
||||||
|
|
||||||
# Helper to create a cycle
|
# Helper to create a cycle
|
||||||
defp create_cycle(member, fee_type, attrs) do
|
defp create_cycle(member, fee_type, attrs) do
|
||||||
|
# Delete any auto-generated cycles first to avoid conflicts
|
||||||
|
existing_cycles =
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Query.filter(member_id == ^member.id)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||||
|
|
||||||
default_attrs = %{
|
default_attrs = %{
|
||||||
cycle_start: ~D[2023-01-01],
|
cycle_start: ~D[2023-01-01],
|
||||||
amount: Decimal.new("50.00"),
|
amount: Decimal.new("50.00"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue