Finish the half-built History Mini App — a Telegram Mini App that lists a group's shared expenses with live, debounced, per-field search and infinite pagination. Launched via the group-bound, HISTORY-scoped, 30-min deep-link token minted by /history.
This is the next thing to build. The visible half is the frontend, but the bigger lift is the backend: search is currently a stub.
Backend
- Real search. Today
TransactionsSearchParams has only title, and TransactionsSearchService.search() ignores it — it returns every transaction for the group. Implement actual filtering in the TransactionRepository Postgres adapter via a dynamic WHERE (all filters optional; empty = match all):
- title — case-insensitive substring (
ILIKE)
- participant(s) — "involves member":
payer OR present in recipients; multi-select, any-of
- amount — min/max range (only meaningful together with a currency)
- currency — exact match (from the group's used currencies)
- date — from/to range on
timestamp (stored UTC)
- Pagination — keyset/cursor, not
OFFSET (degrades on deep history). ORDER BY timestamp DESC, id DESC; cursor = last-seen (timestamp, id); return a next-cursor. Page size ~30.
- API wiring. Thread query params through
GET /app/api/group/transactions?title=¤cy=&amountMin=&amountMax=&from=&to=&participant=&cursor= into TransactionsSearchParams. Keep the existing BigDecimal/Instant serializers.
Frontend
- Filter bar — one control per field, no Search button; search fires on edit, debounced (~300ms).
- Infinite scroll —
IntersectionObserver sentinel; append pages by cursor.
- Transaction widget rework:
- Resolve payer + recipient IDs → names via
GET /app/api/group/members (fetch once, cache in memory).
- Render payer, recipients (the
.participants area is currently empty), amount, currency, title, date.
- Replace hand-rolled
Amount.formatAmount with Intl.NumberFormat (locale + currency aware).
- States — loading, empty, and error states + an
ErrorBoundary (today HistoryApp does setTransactions(await res.json()) with no handling).
- Lifecycle —
WebApp.ready() (dismiss the Telegram splash) and WebApp.expand() (full viewport).
Digested from #294 (Mini App findings)
Open questions
Notes / technical details
- Token TTL is by design. Deep-linked apps keep the short 30-min token (indefinite tokens would be bad). Expiry surfaces as
401 + X-Auth-Error: token_expired, which the FE already maps to a "reopen" message — no refresh path for deep-linked launches. Some sort of an HTTP interceptor might be installed, so that if a Mini App makes an HTTP request with an expired token, it is cleared (like the AppWrapper does on init).
- Language mismatch (deferred). i18n currently keys off the user's Telegram
language_code while the bot uses GroupConfig.language, so the app and bot can render different languages. Solvable later (we know user↔group at auth time and can return the group language). Not in scope here.
Finish the half-built History Mini App — a Telegram Mini App that lists a group's shared expenses with live, debounced, per-field search and infinite pagination. Launched via the group-bound,
HISTORY-scoped, 30-min deep-link token minted by/history.This is the next thing to build. The visible half is the frontend, but the bigger lift is the backend: search is currently a stub.
Backend
TransactionsSearchParamshas onlytitle, andTransactionsSearchService.search()ignores it — it returns every transaction for the group. Implement actual filtering in theTransactionRepositoryPostgres adapter via a dynamicWHERE(all filters optional; empty = match all):ILIKE)payerOR present inrecipients; multi-select, any-oftimestamp(stored UTC)OFFSET(degrades on deep history).ORDER BY timestamp DESC, id DESC; cursor = last-seen(timestamp, id); return a next-cursor. Page size ~30.GET /app/api/group/transactions?title=¤cy=&amountMin=&amountMax=&from=&to=&participant=&cursor=intoTransactionsSearchParams. Keep the existingBigDecimal/Instantserializers.Frontend
IntersectionObserversentinel; append pages by cursor.GET /app/api/group/members(fetch once, cache in memory)..participantsarea is currently empty), amount, currency, title, date.Amount.formatAmountwithIntl.NumberFormat(locale + currency aware).ErrorBoundary(todayHistoryAppdoessetTransactions(await res.json())with no handling).WebApp.ready()(dismiss the Telegram splash) andWebApp.expand()(full viewport).Digested from #294 (Mini App findings)
Transaction.tsxdoesn't render participants — the.participantsdiv is empty, so history cards never show who paid / who owes (needs/group/membersto resolve IDs → names).group/membersendpoint has no client caller (consumed here, for names). The frontend only callsauth/validationandgroup/transactions;group/membersandgroup/currencieshave no client callers.HistoryApp.tsxdoessetTransactions(await response.json())with no error handling (failed/non-JSON response throws unhandled); no loading indicator, no empty state, noErrorBoundary.AppWrappershows a blank screen (null) while auth is in flight.WebApp.ready()(dismiss Telegram's loading placeholder) and noWebApp.expand()(full viewport). NoMainButton/BackButtonintegration (MainButtonis the idiomatic submit affordance for the future expense form).Amount.formatAmounthand-trims trailing zeros with string slicing and is locale-blind — preferIntl.NumberFormat.Open questions
Notes / technical details
401+X-Auth-Error: token_expired, which the FE already maps to a "reopen" message — no refresh path for deep-linked launches. Some sort of an HTTP interceptor might be installed, so that if a Mini App makes an HTTP request with an expired token, it is cleared (like theAppWrapperdoes on init).language_codewhile the bot usesGroupConfig.language, so the app and bot can render different languages. Solvable later (we know user↔group at auth time and can return the group language). Not in scope here.