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 by clicking on the amount span view |> element("span[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