Skip to content

feat(queries): filter engine for invoice queries — status, date, amount range - #946

Merged
Kingsman-99 merged 5 commits into
Stellar-split:mainfrom
maztah1:feat/851-invoice-filter-engine
Sep 28, 2026
Merged

Kingsman-99 merged 5 commits into
Stellar-split:mainfrom
maztah1:feat/851-invoice-filter-engine

Conversation

@maztah1

@maztah1 maztah1 commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

What the issue was

#851 asked for a filter and query engine for listing invoices — client.queryInvoices(filter) accepting a single typed query object that filters by status, creator, date range, amount range and tags, returning a paginated Page with sorting. There was no such API. The SDK had getInvoicesByCreator (raw IDs only) and a separate compileFilter/applyFilter/FilterIndex module built around a recursive and/or FilterCriteria tree, which had no notion of dates, tags, sorting, or paging.

Approach

New module src/invoiceQuery.ts

  • InvoiceFilter — { creator?, status?, minAmount?, maxAmount?, fromDate?, toDate?, tags?, limit?, cursor?, sort? }. All supplied fields combine with AND; within a field, status matches any listed state while tags requires every listed tag.
  • InvoiceQueryEngine — filter → sort → paginate over an in-memory invoice set. Also exported as a standalone queryInvoices(invoices, filter) for callers who already hold the invoices and want no network calls.
  • InvoiceTagIndex — tag → invoices lookup, exposed via engine.tagIndex().
  • InvoicePage — { items, nextCursor?, total }. nextCursor is omitted (not null) on the final page; total always reports the unpaginated match count.

client.queryInvoices(filter) — cursor-pages the creator's invoice IDs, fetches each invoice, then applies the filter in memory.

Decisions worth flagging:

  • creator is required. The contract exposes no global invoice index, so an unscoped query would be an unbounded full scan. This throws a ValidationError rather than silently enumerating everything — the existing getInvoicesByCreator remains available for manual paging.
  • Amounts are compared as bigint, never narrowed through Number, since stroop totals routinely exceed 2^53. Covered by a test at 2^53 + 1.
  • Dates accept Unix seconds or milliseconds, matching the convention Invoice.createdAt already documents. Invoices with no createdAt are excluded from date filters and sort last.
  • Cursors are offset-based (off:<n>), since sorting can reorder the underlying set; an offset stays deterministic for a given (filter, sort) pair. Ties break on invoice ID so pages never repeat or skip.
  • Tags fall back to #hashtags parsed from the memo when an invoice has no explicit tags field, so existing invoices are taggable with no contract change. I added an optional tags?: string[] to Invoice for the explicit case.

How it was tested

57 new tests in test/invoiceQuery.test.ts, all passing:

  • Each filter independently — creator, status (single and multi), min/max amount, from/to date, explicit tags, memo-derived tags, plus inclusive bounds, multi-recipient totals, and >2^53 amounts.
  • Combined filters — a case where four of five candidates each fail exactly one predicate.
  • Sorting — all four orders asserted by exact ID sequence, tie-breaking, and undated-invoice placement.
  • Pagination — default and explicit limits, nextCursor presence/absence, total stability across pages, a full multi-page walk asserting no gaps or duplicates, and a cursor past the end.
  • Empty results — no matches, empty input, unknown tag.
  • Validation — inverted ranges, non-positive limit, unknown sort, malformed cursor.
  • Client integration — queryInvoices against a stubbed client, including that it rejects a missing creator.

Full suite: 213 passed, 1 skipped, 0 failed. tsc --noEmit output diffed against the base branch to confirm no new errors (the repo has pre-existing type errors unrelated to this change).

Note: package.json's test script only runs two specific files; I ran the full suite via npx vitest run.

closes #851

Add client.queryInvoices(filter) backed by a new InvoiceQueryEngine so
callers can filter by status, creator, date range, amount range and tags
through a single typed query object.

- InvoiceFilter composes every predicate with AND semantics; status matches
  any listed state while tags require every listed tag
- Four sort orders (newest/oldest/highest/lowest) with an invoice-ID
  tie-break so paging is deterministic
- Offset-based opaque cursors; nextCursor is omitted on the final page and
  total always reports the unpaginated match count
- Amounts compared as bigint, never narrowed through Number
- Dates accept Unix seconds or milliseconds, matching createdAt's existing
  convention; invoices with an unknown createdAt are excluded from date
  filters and sort last
- InvoiceTagIndex for tag lookups; tags fall back to #hashtags parsed from
  the memo so existing invoices are taggable without a contract change
- queryInvoices requires a creator because the contract exposes no global
  invoice index, and filters the creator's invoices in memory

closes Stellar-split#851
Expose the filter engine on the client. Because the contract has no query
endpoint for status/amount/date, this cursor-pages the creator's invoice
IDs, fetches each invoice, then applies the filter in memory. Requiring a
creator keeps the fetch bounded and turns the missing global index into an
explicit error rather than a silent full scan.
Add a README section covering the InvoiceFilter fields, their AND/OR
semantics, the InvoicePage shape, cursor paging, and the exported engine
for searching an invoice list already held in memory.
@drips-wave

drips-wave Bot commented Sep 26, 2026

Copy link
Copy Markdown

@maztah1 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Kingsman-99
Kingsman-99 merged commit 7be3d95 into Stellar-split:main Sep 28, 2026
1 check passed
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.

Implement filter engine for invoice queries — status, date, amount range

2 participants