From 3b6c0b9fc95a8ddb1ddcea170ac43988af09bfe6 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 24 Feb 2026 09:15:04 +0100 Subject: [PATCH 1/6] fix: sort Fee Type by name in LiveView and exports Use Ash related-field sort (membership_fee_type.name) instead of membership_fee_type_id so column order is alphabetical. Load membership_fee_type when sorting by it even if column is hidden. In-memory re-sort (Build) uses loaded fee type name. Co-authored-by: Cursor --- lib/mv/membership/member_export/build.ex | 34 ++++++++++++------- .../controllers/member_export_controller.ex | 7 ++-- lib/mv_web/live/member_live/index.ex | 26 +++++++++++--- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/lib/mv/membership/member_export/build.ex b/lib/mv/membership/member_export/build.ex index 8a5aa60..9a1c03a 100644 --- a/lib/mv/membership/member_export/build.ex +++ b/lib/mv/membership/member_export/build.ex @@ -133,7 +133,10 @@ defmodule Mv.Membership.MemberExport.Build do "membership_fee_status" in parsed.member_fields need_groups = "groups" in parsed.member_fields - need_membership_fee_type = "membership_fee_type" in parsed.member_fields + + need_membership_fee_type = + "membership_fee_type" in parsed.member_fields or + parsed.sort_field == "membership_fee_type" query = Member @@ -199,10 +202,9 @@ defmodule Mv.Membership.MemberExport.Build do field_atom = String.to_existing_atom(field) if field_atom in Mv.Constants.member_fields() or field_atom == :membership_fee_type do - sort_field = - if field_atom == :membership_fee_type, do: :membership_fee_type_id, else: field_atom - - sort_by_field(members, sort_field, order) + key_fn = sort_key_fn_for_field(field_atom) + compare_fn = build_compare_fn(order) + Enum.sort_by(members, key_fn, compare_fn) else members end @@ -212,13 +214,17 @@ defmodule Mv.Membership.MemberExport.Build do defp sort_members_in_memory(members, _field, _order), do: members - defp sort_by_field(members, field_atom, order) do - key_fn = fn member -> Map.get(member, field_atom) end - compare_fn = build_compare_fn(order) - - Enum.sort_by(members, key_fn, compare_fn) + defp sort_key_fn_for_field(:membership_fee_type) do + fn member -> + case Map.get(member, :membership_fee_type) do + nil -> nil + rel -> Map.get(rel, :name) + end + end end + defp sort_key_fn_for_field(field_atom), do: fn member -> Map.get(member, field_atom) end + defp build_compare_fn("asc"), do: fn a, b -> a <= b end defp build_compare_fn("desc"), do: fn a, b -> b <= a end defp build_compare_fn(_), do: fn _a, _b -> true end @@ -261,7 +267,7 @@ defmodule Mv.Membership.MemberExport.Build do defp apply_fee_type_sort(query, order) do order_atom = if order == "desc", do: :desc, else: :asc - {Ash.Query.sort(query, membership_fee_type_id: order_atom), false} + {Ash.Query.sort(query, [{"membership_fee_type.name", order_atom}]), false} end defp apply_standard_member_sort(query, field, order) do @@ -275,9 +281,11 @@ defmodule Mv.Membership.MemberExport.Build do order_atom = if order == "desc", do: :desc, else: :asc sort_field = - if field_atom == :membership_fee_type, do: :membership_fee_type_id, else: field_atom + if field_atom == :membership_fee_type, + do: {"membership_fee_type.name", order_atom}, + else: {field_atom, order_atom} - {Ash.Query.sort(query, [{sort_field, order_atom}]), false} + {Ash.Query.sort(query, [sort_field]), false} else {query, false} end diff --git a/lib/mv_web/controllers/member_export_controller.ex b/lib/mv_web/controllers/member_export_controller.ex index b5386a9..715f86a 100644 --- a/lib/mv_web/controllers/member_export_controller.ex +++ b/lib/mv_web/controllers/member_export_controller.ex @@ -238,7 +238,10 @@ defmodule MvWeb.MemberExportController do parsed.computed_fields != [] and "membership_fee_status" in parsed.computed_fields need_groups = "groups" in parsed.member_fields - need_membership_fee_type = "membership_fee_type" in parsed.member_fields + + need_membership_fee_type = + "membership_fee_type" in parsed.member_fields or + parsed.sort_field == "membership_fee_type" query = Member @@ -368,7 +371,7 @@ defmodule MvWeb.MemberExportController do defp apply_membership_fee_type_sort_export(query, order) do order_atom = if order == "desc", do: :desc, else: :asc - {Ash.Query.sort(query, membership_fee_type_id: order_atom), false} + {Ash.Query.sort(query, [{"membership_fee_type.name", order_atom}]), false} end defp apply_member_field_sort_export(query, field, order) do diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 889a818..8fb50b9 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -965,9 +965,10 @@ defmodule MvWeb.MemberLive.Index do query = Ash.Query.load(query, groups: [:id, :name, :slug]) - # Load membership_fee_type when the column is visible + # Load membership_fee_type when the column is visible or when sorting by it query = - if :membership_fee_type in socket.assigns.member_fields_visible do + if :membership_fee_type in socket.assigns.member_fields_visible or + socket.assigns.sort_field in [:membership_fee_type, "membership_fee_type"] do Ash.Query.load(query, membership_fee_type: [:id, :name]) else query @@ -1133,9 +1134,9 @@ defmodule MvWeb.MemberLive.Index do field in [:groups, "groups"] -> {query, true} - # Membership fee type sort -> by FK at DB + # Membership fee type sort -> by related name at DB field in [:membership_fee_type, "membership_fee_type"] -> - {Ash.Query.sort(query, membership_fee_type_id: order), false} + {Ash.Query.sort(query, [{"membership_fee_type.name", order}]), false} # Custom field sort -> after load custom_field_sort?(field) -> @@ -1777,6 +1778,15 @@ defmodule MvWeb.MemberLive.Index do end end) + # If fee type is visible but start_date was not in the list, append it + with_extras = + if :membership_fee_type in (member_fields_visible || []) and + :membership_fee_type not in with_extras do + with_extras ++ [:membership_fee_type] + else + with_extras + end + if :groups in (member_fields_visible || []), do: with_extras ++ [:groups], else: with_extras end @@ -1815,6 +1825,14 @@ defmodule MvWeb.MemberLive.Index do &expand_db_string_for_export(&1, membership_fee_type_visible, computed_strings) ) + # If fee type is visible but start_date was not in the list, append it before computed/groups + db_with_extras = + if membership_fee_type_visible and "membership_fee_type" not in db_with_extras do + db_with_extras ++ ["membership_fee_type"] + else + db_with_extras + end + # Any remaining computed fields not inserted above (future-proof) remaining_computed = computed_strings From 83264325f1f7ea1180bf48edc2763b134964235c Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 24 Feb 2026 09:15:08 +0100 Subject: [PATCH 2/6] fix: include Fee Type in export when Start Date not in fields Append membership_fee_type to column list when it is visible but membership_fee_start_date was not in the selection (MemberExport, export_column_order, build_export_member_fields_list). Co-authored-by: Cursor --- lib/mv/membership/member_export.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/mv/membership/member_export.ex b/lib/mv/membership/member_export.ex index a017480..bbfbb6e 100644 --- a/lib/mv/membership/member_export.ex +++ b/lib/mv/membership/member_export.ex @@ -433,6 +433,14 @@ defmodule Mv.Membership.MemberExport do expand_field_with_computed(f, member_fields, computed_fields) end) + # If fee type is visible but start_date was not in the list, it won't be in db_with_insert + db_with_insert = + if "membership_fee_type" in member_fields and "membership_fee_type" not in db_with_insert do + db_with_insert ++ ["membership_fee_type"] + else + db_with_insert + end + remaining = Enum.reject(computed_fields, &(&1 in db_with_insert)) db_with_insert ++ remaining end From f211f45cb2cb01dd228c7797888a729c6239235c Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 24 Feb 2026 09:15:41 +0100 Subject: [PATCH 3/6] test: export and PDF regression for Fee Type without start_date Add test for CSV export with only first_name and membership_fee_type. Add test for PDF export with same field set (status and content-type). Co-authored-by: Cursor --- priv/gettext/de/LC_MESSAGES/default.po | 2 +- priv/gettext/en/LC_MESSAGES/default.po | 2 +- .../member_export_controller_test.exs | 69 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 9825efd..2f4c1b8 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -2917,6 +2917,6 @@ msgstr "Für die Vereinfacht-Integration erforderlich und kann nicht deaktiviert #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgid "Fee Type" msgstr "Beitragsart" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index b69f3df..a76c9f6 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2917,6 +2917,6 @@ msgstr "Required for Vereinfacht integration and cannot be disabled." #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgid "Fee Type" msgstr "Fee Type" diff --git a/test/mv_web/controllers/member_export_controller_test.exs b/test/mv_web/controllers/member_export_controller_test.exs index de7b417..cfc89ec 100644 --- a/test/mv_web/controllers/member_export_controller_test.exs +++ b/test/mv_web/controllers/member_export_controller_test.exs @@ -177,6 +177,39 @@ defmodule MvWeb.MemberExportControllerTest do assert body =~ "Alice" end + # Regression: when membership_fee_start_date is not in member_fields, Fee Type must still be exported (append fallback) + test "export includes Fee Type when only first_name and membership_fee_type are requested (no start_date)", + %{ + conn: conn, + member1: m1 + } do + payload = %{ + "selected_ids" => [m1.id], + "member_fields" => ["first_name", "membership_fee_type"], + "custom_field_ids" => [], + "query" => nil, + "sort_field" => nil, + "sort_order" => nil + } + + conn = get(conn, "/members") + csrf_token = csrf_token_from_conn(conn) + + conn = + post(conn, "/members/export.csv", %{ + "payload" => Jason.encode!(payload), + "_csrf_token" => csrf_token + }) + + assert conn.status == 200 + body = response(conn, 200) + header = body |> export_lines() |> hd() + + assert header =~ "Fee Type" + assert header =~ "First Name" + assert body =~ "Alice" + end + test "export includes membership_fee_status computed field when requested", %{ conn: conn, member1: m1 @@ -532,4 +565,40 @@ defmodule MvWeb.MemberExportControllerTest do assert membership_idx < active_idx end end + + describe "POST /members/export.pdf" do + test "PDF export includes Fee Type column when requested without membership_fee_start_date", + %{ + conn: conn + } do + m = + Fixtures.member_fixture(%{first_name: "PDF", last_name: "Test", email: "pdf@example.com"}) + + payload = %{ + "selected_ids" => [m.id], + "member_fields" => ["first_name", "membership_fee_type"], + "custom_field_ids" => [], + "query" => nil, + "sort_field" => nil, + "sort_order" => nil + } + + conn = get(conn, "/members") + csrf_token = csrf_token_from_conn(conn) + + conn = + post(conn, "/members/export.pdf", %{ + "payload" => Jason.encode!(payload), + "_csrf_token" => csrf_token + }) + + assert conn.status == 200 + assert get_resp_header(conn, "content-type") |> List.first() =~ "application/pdf" + + body = response(conn, 200) + + # PDF is generated successfully with Fee Type in columns (regression: fee type without start_date) + assert is_binary(body) and byte_size(body) > 0 + end + end end From 94bcb5dc8c37ba290d545c0897c6ffb65a0fd366 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 24 Feb 2026 09:15:04 +0100 Subject: [PATCH 4/6] fix: sort Fee Type by name in LiveView and exports Use Ash related-field sort (membership_fee_type.name) instead of membership_fee_type_id so column order is alphabetical. Load membership_fee_type when sorting by it even if column is hidden. In-memory re-sort (Build) uses loaded fee type name. --- lib/mv/membership/member_export/build.ex | 34 ++++++++++++------- .../controllers/member_export_controller.ex | 7 ++-- lib/mv_web/live/member_live/index.ex | 26 +++++++++++--- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/lib/mv/membership/member_export/build.ex b/lib/mv/membership/member_export/build.ex index 8a5aa60..9a1c03a 100644 --- a/lib/mv/membership/member_export/build.ex +++ b/lib/mv/membership/member_export/build.ex @@ -133,7 +133,10 @@ defmodule Mv.Membership.MemberExport.Build do "membership_fee_status" in parsed.member_fields need_groups = "groups" in parsed.member_fields - need_membership_fee_type = "membership_fee_type" in parsed.member_fields + + need_membership_fee_type = + "membership_fee_type" in parsed.member_fields or + parsed.sort_field == "membership_fee_type" query = Member @@ -199,10 +202,9 @@ defmodule Mv.Membership.MemberExport.Build do field_atom = String.to_existing_atom(field) if field_atom in Mv.Constants.member_fields() or field_atom == :membership_fee_type do - sort_field = - if field_atom == :membership_fee_type, do: :membership_fee_type_id, else: field_atom - - sort_by_field(members, sort_field, order) + key_fn = sort_key_fn_for_field(field_atom) + compare_fn = build_compare_fn(order) + Enum.sort_by(members, key_fn, compare_fn) else members end @@ -212,13 +214,17 @@ defmodule Mv.Membership.MemberExport.Build do defp sort_members_in_memory(members, _field, _order), do: members - defp sort_by_field(members, field_atom, order) do - key_fn = fn member -> Map.get(member, field_atom) end - compare_fn = build_compare_fn(order) - - Enum.sort_by(members, key_fn, compare_fn) + defp sort_key_fn_for_field(:membership_fee_type) do + fn member -> + case Map.get(member, :membership_fee_type) do + nil -> nil + rel -> Map.get(rel, :name) + end + end end + defp sort_key_fn_for_field(field_atom), do: fn member -> Map.get(member, field_atom) end + defp build_compare_fn("asc"), do: fn a, b -> a <= b end defp build_compare_fn("desc"), do: fn a, b -> b <= a end defp build_compare_fn(_), do: fn _a, _b -> true end @@ -261,7 +267,7 @@ defmodule Mv.Membership.MemberExport.Build do defp apply_fee_type_sort(query, order) do order_atom = if order == "desc", do: :desc, else: :asc - {Ash.Query.sort(query, membership_fee_type_id: order_atom), false} + {Ash.Query.sort(query, [{"membership_fee_type.name", order_atom}]), false} end defp apply_standard_member_sort(query, field, order) do @@ -275,9 +281,11 @@ defmodule Mv.Membership.MemberExport.Build do order_atom = if order == "desc", do: :desc, else: :asc sort_field = - if field_atom == :membership_fee_type, do: :membership_fee_type_id, else: field_atom + if field_atom == :membership_fee_type, + do: {"membership_fee_type.name", order_atom}, + else: {field_atom, order_atom} - {Ash.Query.sort(query, [{sort_field, order_atom}]), false} + {Ash.Query.sort(query, [sort_field]), false} else {query, false} end diff --git a/lib/mv_web/controllers/member_export_controller.ex b/lib/mv_web/controllers/member_export_controller.ex index b5386a9..715f86a 100644 --- a/lib/mv_web/controllers/member_export_controller.ex +++ b/lib/mv_web/controllers/member_export_controller.ex @@ -238,7 +238,10 @@ defmodule MvWeb.MemberExportController do parsed.computed_fields != [] and "membership_fee_status" in parsed.computed_fields need_groups = "groups" in parsed.member_fields - need_membership_fee_type = "membership_fee_type" in parsed.member_fields + + need_membership_fee_type = + "membership_fee_type" in parsed.member_fields or + parsed.sort_field == "membership_fee_type" query = Member @@ -368,7 +371,7 @@ defmodule MvWeb.MemberExportController do defp apply_membership_fee_type_sort_export(query, order) do order_atom = if order == "desc", do: :desc, else: :asc - {Ash.Query.sort(query, membership_fee_type_id: order_atom), false} + {Ash.Query.sort(query, [{"membership_fee_type.name", order_atom}]), false} end defp apply_member_field_sort_export(query, field, order) do diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 889a818..8fb50b9 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -965,9 +965,10 @@ defmodule MvWeb.MemberLive.Index do query = Ash.Query.load(query, groups: [:id, :name, :slug]) - # Load membership_fee_type when the column is visible + # Load membership_fee_type when the column is visible or when sorting by it query = - if :membership_fee_type in socket.assigns.member_fields_visible do + if :membership_fee_type in socket.assigns.member_fields_visible or + socket.assigns.sort_field in [:membership_fee_type, "membership_fee_type"] do Ash.Query.load(query, membership_fee_type: [:id, :name]) else query @@ -1133,9 +1134,9 @@ defmodule MvWeb.MemberLive.Index do field in [:groups, "groups"] -> {query, true} - # Membership fee type sort -> by FK at DB + # Membership fee type sort -> by related name at DB field in [:membership_fee_type, "membership_fee_type"] -> - {Ash.Query.sort(query, membership_fee_type_id: order), false} + {Ash.Query.sort(query, [{"membership_fee_type.name", order}]), false} # Custom field sort -> after load custom_field_sort?(field) -> @@ -1777,6 +1778,15 @@ defmodule MvWeb.MemberLive.Index do end end) + # If fee type is visible but start_date was not in the list, append it + with_extras = + if :membership_fee_type in (member_fields_visible || []) and + :membership_fee_type not in with_extras do + with_extras ++ [:membership_fee_type] + else + with_extras + end + if :groups in (member_fields_visible || []), do: with_extras ++ [:groups], else: with_extras end @@ -1815,6 +1825,14 @@ defmodule MvWeb.MemberLive.Index do &expand_db_string_for_export(&1, membership_fee_type_visible, computed_strings) ) + # If fee type is visible but start_date was not in the list, append it before computed/groups + db_with_extras = + if membership_fee_type_visible and "membership_fee_type" not in db_with_extras do + db_with_extras ++ ["membership_fee_type"] + else + db_with_extras + end + # Any remaining computed fields not inserted above (future-proof) remaining_computed = computed_strings From 1c8c5ae83bb143bab566663e5662799fb33a5449 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 24 Feb 2026 09:15:08 +0100 Subject: [PATCH 5/6] fix: include Fee Type in export when Start Date not in fields Append membership_fee_type to column list when it is visible but membership_fee_start_date was not in the selection (MemberExport, export_column_order, build_export_member_fields_list). --- lib/mv/membership/member_export.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/mv/membership/member_export.ex b/lib/mv/membership/member_export.ex index a017480..bbfbb6e 100644 --- a/lib/mv/membership/member_export.ex +++ b/lib/mv/membership/member_export.ex @@ -433,6 +433,14 @@ defmodule Mv.Membership.MemberExport do expand_field_with_computed(f, member_fields, computed_fields) end) + # If fee type is visible but start_date was not in the list, it won't be in db_with_insert + db_with_insert = + if "membership_fee_type" in member_fields and "membership_fee_type" not in db_with_insert do + db_with_insert ++ ["membership_fee_type"] + else + db_with_insert + end + remaining = Enum.reject(computed_fields, &(&1 in db_with_insert)) db_with_insert ++ remaining end From d5df2338a7a91ec700039a56112acc3f40cd0b33 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 24 Feb 2026 09:15:41 +0100 Subject: [PATCH 6/6] test: export and PDF regression for Fee Type without start_date Add test for CSV export with only first_name and membership_fee_type. Add test for PDF export with same field set (status and content-type). --- priv/gettext/de/LC_MESSAGES/default.po | 2 +- priv/gettext/en/LC_MESSAGES/default.po | 2 +- .../member_export_controller_test.exs | 69 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 9825efd..2f4c1b8 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -2917,6 +2917,6 @@ msgstr "Für die Vereinfacht-Integration erforderlich und kann nicht deaktiviert #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgid "Fee Type" msgstr "Beitragsart" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index b69f3df..a76c9f6 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2917,6 +2917,6 @@ msgstr "Required for Vereinfacht integration and cannot be disabled." #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgid "Fee Type" msgstr "Fee Type" diff --git a/test/mv_web/controllers/member_export_controller_test.exs b/test/mv_web/controllers/member_export_controller_test.exs index de7b417..cfc89ec 100644 --- a/test/mv_web/controllers/member_export_controller_test.exs +++ b/test/mv_web/controllers/member_export_controller_test.exs @@ -177,6 +177,39 @@ defmodule MvWeb.MemberExportControllerTest do assert body =~ "Alice" end + # Regression: when membership_fee_start_date is not in member_fields, Fee Type must still be exported (append fallback) + test "export includes Fee Type when only first_name and membership_fee_type are requested (no start_date)", + %{ + conn: conn, + member1: m1 + } do + payload = %{ + "selected_ids" => [m1.id], + "member_fields" => ["first_name", "membership_fee_type"], + "custom_field_ids" => [], + "query" => nil, + "sort_field" => nil, + "sort_order" => nil + } + + conn = get(conn, "/members") + csrf_token = csrf_token_from_conn(conn) + + conn = + post(conn, "/members/export.csv", %{ + "payload" => Jason.encode!(payload), + "_csrf_token" => csrf_token + }) + + assert conn.status == 200 + body = response(conn, 200) + header = body |> export_lines() |> hd() + + assert header =~ "Fee Type" + assert header =~ "First Name" + assert body =~ "Alice" + end + test "export includes membership_fee_status computed field when requested", %{ conn: conn, member1: m1 @@ -532,4 +565,40 @@ defmodule MvWeb.MemberExportControllerTest do assert membership_idx < active_idx end end + + describe "POST /members/export.pdf" do + test "PDF export includes Fee Type column when requested without membership_fee_start_date", + %{ + conn: conn + } do + m = + Fixtures.member_fixture(%{first_name: "PDF", last_name: "Test", email: "pdf@example.com"}) + + payload = %{ + "selected_ids" => [m.id], + "member_fields" => ["first_name", "membership_fee_type"], + "custom_field_ids" => [], + "query" => nil, + "sort_field" => nil, + "sort_order" => nil + } + + conn = get(conn, "/members") + csrf_token = csrf_token_from_conn(conn) + + conn = + post(conn, "/members/export.pdf", %{ + "payload" => Jason.encode!(payload), + "_csrf_token" => csrf_token + }) + + assert conn.status == 200 + assert get_resp_header(conn, "content-type") |> List.first() =~ "application/pdf" + + body = response(conn, 200) + + # PDF is generated successfully with Fee Type in columns (regression: fee type without start_date) + assert is_binary(body) and byte_size(body) > 0 + end + end end