Skip to content

Feat/invoice pagination#37

Merged
emilwojtaszek merged 9 commits into
mainfrom
feat/invoice-pagination
Feb 12, 2026
Merged

Feat/invoice pagination#37
emilwojtaszek merged 9 commits into
mainfrom
feat/invoice-pagination

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Feb 11, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Paginated invoice lists in UI with page navigation and "Showing X–Y of Z" summaries; API responses now include data + meta (page, per_page, total_count, total_pages). Defaults: page=1, per_page=25 (max 100). List responses exclude large payload fields.
  • Performance

    • New database indexes to speed up ordering and ILIKE text searches.
  • Tests

    • Added coverage for pagination behavior, counts, defaults, and excluded fields.
  • Documentation

    • Added an architectural decision document describing the pagination approach.

emilwojtaszek and others added 7 commits February 12, 2026 00:43
Add pg_trgm extension and indexes to support paginated invoice queries:
- Compound index on (company_id, issue_date, inserted_at) for default sort
- Compound index on (company_id, type, status) for filtered listings
- GIN trigram indexes on invoice_number, seller_name, buyer_name for ILIKE

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Exclude xml_content from list queries via @list_fields select
- Add LIMIT/OFFSET pagination with :page and :per_page filters
- Add count_invoices/2 for counting with same filter logic
- Add list_invoices_paginated/2 returning entries + metadata map
- Add extract_pagination/1 helper with clamping (per_page max 100)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create PaginationMeta OpenAPI schema for page/per_page/total metadata
- Update InvoiceListResponse to include meta alongside data
- Add page and per_page query parameters to index operation spec
- Controller index action now calls list_invoices_paginated/2
- Add maybe_put_integer/3 helper for parsing pagination params

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use list_invoices_paginated/2 instead of list_invoices/2
- Assign page, per_page, total_count, total_pages to socket
- Parse page param from URL, filter change resets to page 1
- Render DaisyUI join button group: Prev / page numbers / Next
- Show "Showing X-Y of Z invoices" text
- visible_pages/2: show all when <= 7, otherwise 5-page window
- pagination_params/2: preserves current filters with target page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Context tests:
- Pagination defaults (page 1, per_page 25)
- page/per_page respected, pages don't overlap
- per_page capped at 100
- xml_content excluded from list results
- count_invoices/2 with company scoping and filters
- list_invoices_paginated/2 returns correct metadata

API controller tests:
- Paginated response includes meta key
- Default pagination (page 1, per_page 25)
- Custom page/per_page params work
- xml_content not in list response

LiveView tests:
- Pagination controls render when >1 page
- No pagination controls for single page
- Page 2 navigation shows correct range
- Filter change resets to page 1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents decisions on offset-based pagination, two-query approach,
pg_trgm extension for ILIKE search, and xml_content exclusion from
list queries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Implements offset-based pagination for invoices across API and LiveView, returns data plus pagination meta, excludes xml_content from list queries, adds pg_trgm and compound DB indexes for search and ordering, and records the decision in a new ADR.

Changes

Cohort / File(s) Summary
Documentation
docs/adr/0015-invoice-pagination-search-indexes.md
New ADR: offset pagination, two-query (count+data) approach, excluded fields, and index strategy.
Core Business Logic
lib/ksef_hub/invoices.ex
Adds list_invoices_paginated/2, count_invoices/2, @list_fields, pagination defaults, extract_pagination/1, clamp/3; selects limited fields (omits xml_content) and applies LIMIT/OFFSET.
API Layer & Schemas
lib/ksef_hub_web/controllers/api/invoice_controller.ex, lib/ksef_hub_web/schemas/invoice_list_response.ex, lib/ksef_hub_web/schemas/pagination_meta.ex
Controller now parses page/per_page, uses paginated listing, returns {data, meta}; adds PaginationMeta OpenAPI schema and updates response schema.
LiveView
lib/ksef_hub_web/live/invoice_live/index.ex
Uses list_invoices_paginated, assigns entries and pagination metadata, adds helpers (maybe_put_page, pagination_params, visible_pages) and pagination UI; resets to page 1 on filter changes.
Database Migration
priv/repo/migrations/20260212000001_add_invoice_pagination_indexes.exs
Ensures pg_trgm extension; adds compound indexes (company_id, issue_date DESC, inserted_at DESC) and (company_id, type, status), plus GIN trigram indexes for invoice_number, seller_name, buyer_name.
Tests
test/ksef_hub/invoices_test.exs, test/ksef_hub_web/controllers/api/invoice_controller_test.exs, test/ksef_hub_web/live/invoice_live/index_test.exs
Adds tests for pagination defaults, clamping, page boundaries, exclusion of xml_content, API meta shape, LiveView pagination rendering, navigation, and filter-reset behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Controller as Controller
    participant Invoices as InvoicesModule
    participant DB as Database

    Client->>Controller: GET /api/invoices?page=2&per_page=10
    Controller->>Controller: parse page/per_page into filters
    Controller->>Invoices: list_invoices_paginated(company_id, filters)
    Invoices->>Invoices: extract_pagination / clamp values
    Invoices->>DB: COUNT(*) with filters
    DB-->>Invoices: total_count
    Invoices->>DB: SELECT limited fields LIMIT 10 OFFSET 10
    DB-->>Invoices: entries
    Invoices-->>Controller: %{entries, page, per_page, total_count, total_pages}
    Controller-->>Client: JSON {data: entries, meta: {page, per_page, total_count, total_pages}}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Feat/openapi docs #23: Related edits to the InvoiceListResponse/OpenApiSpex schema; this PR updates that schema to include pagination meta.
  • Feat/multi company support #21: Changes to lib/ksef_hub/invoices.ex company-scoped listing APIs; overlaps with added pagination helpers and query behavior.
  • Feat/cloud run deploy #22: Modifies invoice_live/index.ex filter/assign flow; overlaps with LiveView pagination and filter-reset logic.

Poem

🐰 Hop, hop, offset and bound—
Pages now lay their rows upon the ground.
Trigrams hum while invoices take flight,
Metadata glints in soft moonlight.
I nibble a page and cheerfully abound.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (22 files):

⚔️ .env.example (content)
⚔️ CLAUDE.md (content)
⚔️ Dockerfile (content)
⚔️ README.md (content)
⚔️ cloud-run/service.yaml (content)
⚔️ config/runtime.exs (content)
⚔️ docker-compose.yml (content)
⚔️ docs/adr/0004-pdf-generation-xsltproc.md (content)
⚔️ docs/prd.md (content)
⚔️ lib/ksef_hub/invoices.ex (content)
⚔️ lib/ksef_hub/pdf.ex (content)
⚔️ lib/ksef_hub/pdf/behaviour.ex (content)
⚔️ lib/ksef_hub_web/components/core_components.ex (content)
⚔️ lib/ksef_hub_web/components/layouts.ex (content)
⚔️ lib/ksef_hub_web/controllers/api/invoice_controller.ex (content)
⚔️ lib/ksef_hub_web/controllers/invoice_pdf_controller.ex (content)
⚔️ lib/ksef_hub_web/live/invoice_live/index.ex (content)
⚔️ lib/ksef_hub_web/schemas/invoice_list_response.ex (content)
⚔️ test/ksef_hub/invoices_test.exs (content)
⚔️ test/ksef_hub/pdf_test.exs (content)
⚔️ test/ksef_hub_web/controllers/api/invoice_controller_test.exs (content)
⚔️ test/ksef_hub_web/live/invoice_live/index_test.exs (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/invoice pagination' directly and clearly describes the main change: implementing pagination for invoices, which is the primary objective of this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/invoice-pagination
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/invoice-pagination
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/ksef_hub_web/controllers/api/invoice_controller.ex`:
- Around line 313-326: The `@spec` for maybe_put_integer is too narrow; update it
to reflect that the third argument can be an integer as well as a string or nil.
Change the `@spec` for maybe_put_integer/3 to something like
maybe_put_integer(map(), atom(), String.t() | integer() | nil) :: map() so it
matches the clauses in the function (see maybe_put_integer/3).
🧹 Nitpick comments (5)
priv/repo/migrations/20260212000001_add_invoice_pagination_indexes.exs (2)

7-10: Consider adding explicit column order for DESC sorting.

The default ORDER BY in list_invoices/2 uses desc: i.issue_date, desc: i.inserted_at. B-tree indexes scan backwards for DESC but are more efficient when column order matches the query. Consider:

CREATE INDEX ... ON invoices (company_id, issue_date DESC, inserted_at DESC)
♻️ Proposed fix
-    create index(:invoices, [:company_id, :issue_date, :inserted_at],
-             name: :invoices_company_date_idx
-           )
+    execute("""
+      CREATE INDEX invoices_company_date_idx 
+      ON invoices (company_id, issue_date DESC, inserted_at DESC)
+    """)

And update down/0:

-    drop_if_exists index(:invoices, [:company_id, :issue_date, :inserted_at],
-                     name: :invoices_company_date_idx
-                   )
+    execute("DROP INDEX IF EXISTS invoices_company_date_idx")

44-44: Dropping pg_trgm extension may affect other features.

If pg_trgm is used by other parts of the application or added by a different migration, dropping it here could cause issues during rollback. Consider removing this line or checking if other dependencies exist.

♻️ Safer rollback
-    execute("DROP EXTENSION IF EXISTS pg_trgm")
+    # Note: pg_trgm extension left in place as it may be used by other features
lib/ksef_hub/invoices.ex (1)

78-83: Minor redundancy: extract_pagination is called twice.

list_invoices_paginated/2 calls extract_pagination(filters) on line 79, then merges the result back into filters on line 80, which is then passed to list_invoices/2 where extract_pagination is called again (line 34). The merge is unnecessary since list_invoices re-extracts pagination.

This doesn't cause bugs but adds slight overhead.

♻️ Remove redundant merge
 def list_invoices_paginated(company_id, filters \\ %{}) do
   {page, per_page} = extract_pagination(filters)
-  filters = Map.merge(filters, %{page: page, per_page: per_page})

-  entries = list_invoices(company_id, filters)
+  entries = list_invoices(company_id, Map.put(filters, :page, page) |> Map.put(:per_page, per_page))
   total_count = count_invoices(company_id, filters)

Or simply pass filters unchanged since list_invoices handles extraction.

lib/ksef_hub_web/live/invoice_live/index.ex (2)

93-101: Missing @spec for maybe_put_page.

For consistency with the other maybe_put_* helpers and to comply with coding guidelines requiring type specifications for all functions, consider adding a spec.

📝 Proposed fix
+  `@spec` maybe_put_page(map(), atom(), String.t() | nil) :: map()
   defp maybe_put_page(map, _key, nil), do: map
   defp maybe_put_page(map, _key, ""), do: map

107-130: Consider using a pipeline for cleaner construction.

The function is correct but verbose. A more idiomatic approach would use pipelines with maybe_put helpers. This is optional since the current implementation works correctly.

♻️ Proposed refactor using pipelines
   `@spec` pagination_params(map(), pos_integer()) :: map()
   defp pagination_params(filters, target_page) do
-    params = %{}
-    params = if filters[:type], do: Map.put(params, "type", filters[:type]), else: params
-    params = if filters[:status], do: Map.put(params, "status", filters[:status]), else: params
-
-    params =
-      if filters[:date_from],
-        do: Map.put(params, "date_from", Date.to_iso8601(filters[:date_from])),
-        else: params
-
-    params =
-      if filters[:date_to],
-        do: Map.put(params, "date_to", Date.to_iso8601(filters[:date_to])),
-        else: params
-
-    params = if filters[:query], do: Map.put(params, "query", filters[:query]), else: params
-
-    if target_page > 1 do
-      Map.put(params, "page", Integer.to_string(target_page))
-    else
-      params
-    end
+    %{}
+    |> maybe_put("type", filters[:type])
+    |> maybe_put("status", filters[:status])
+    |> maybe_put("date_from", filters[:date_from] && Date.to_iso8601(filters[:date_from]))
+    |> maybe_put("date_to", filters[:date_to] && Date.to_iso8601(filters[:date_to]))
+    |> maybe_put("query", filters[:query])
+    |> maybe_put("page", if(target_page > 1, do: Integer.to_string(target_page)))
   end

Comment thread lib/ksef_hub_web/controllers/api/invoice_controller.ex Outdated
- Use explicit DESC column order in pagination index for efficient scans
- Keep pg_trgm extension on rollback to avoid breaking other features
- Remove redundant pagination merge in list_invoices_paginated/2
- Add missing @SPEC for maybe_put_page/3 in LiveView
- Widen maybe_put_integer/3 spec to accept integer values
- Refactor pagination_params/2 to use pipeline with maybe_put/3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/ksef_hub/invoices.ex`:
- Around line 284-289: Update the `@spec` for clamp to reflect that the first
argument can be any term (not only integer), e.g. change the head spec from
clamp(integer(), integer(), integer()) :: integer() to clamp(term(), integer(),
integer()) :: integer() so it matches both the guarded clause (defp clamp(value,
min_val, max_val) when is_integer(value)) and the fallback clause (defp clamp(_,
min_val, _max_val)), which returns an integer.
🧹 Nitpick comments (1)
lib/ksef_hub/invoices.ex (1)

78-83: Minor: Redundant pagination extraction.

extract_pagination is called here at line 79, then list_invoices at line 81 calls it again internally. Consider passing the already-extracted values to avoid the duplicate computation, or accept that list_invoices should remain self-contained.

♻️ Optional: Add internal helper to avoid duplication
  def list_invoices_paginated(company_id, filters \\ %{}) do
    {page, per_page} = extract_pagination(filters)

-   entries = list_invoices(company_id, filters)
+   entries = do_list_invoices(company_id, filters, page, per_page)
    total_count = count_invoices(company_id, filters)
    total_pages = max(ceil(total_count / per_page), 1)
    # ...
  end

+ defp do_list_invoices(company_id, filters, page, per_page) do
+   Invoice
+   |> where([i], i.company_id == ^company_id)
+   |> apply_filters(filters)
+   |> order_by([i], desc: i.issue_date, desc: i.inserted_at)
+   |> select([i], struct(i, ^@list_fields))
+   |> limit(^per_page)
+   |> offset(^((page - 1) * per_page))
+   |> Repo.all()
+ end

Comment thread lib/ksef_hub/invoices.ex Outdated
…action

Also widen clamp/3 spec to accept term() matching its fallback clause.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant