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 html = view |> element("button[phx-click='regenerate_cycles']") |> render_click() # Check that flash message appears (indicates action completed) assert html =~ "regenerated" || html =~ "successfully" || html =~ "erfolgreich" # 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