uid: T-149 title: Seamless, continuous OCBC transaction history (backend auto-paginates + auto-walks OCBC's date-range mechanism) status: done area: bank-access created: 2026-07-02 updated: 2026-07-02 owner: girafeev1 assignee: Bank Access (Diagnosticcs) related: T-145
Renumbered T-148 β T-149 (2026-07-02). This task was handed over as T-148, but a different task β T-148 "GCP service-account refactor" (Infrastructure) β merged to
mainfirst and took the T-148 UID. Per AGENTS.md "whoever merges second renumbers," this OCBC-history task moved to the real next-free T-149 (README bumped T-149 β T-150). The handover commit2f1ef101and its prose still say "T-148" (immutable history); only this forward identity moved. Source: Bank Access (Diagnosticcs) Β· session_01S4ztAsDZfR97qXjhPdwLn4.
T-149 β Continuous OCBC tx history (no manual date-range fiddling)¶
Why (owner, long-standing request)¶
On Bank Access > OCBC (after login) the app shows only the transactions inside a filter date range, and to see anything else the owner must keep re-adjusting the date range. The owner has asked for a long time for the app to present a seamless, continuous transaction history where the backend does the fetching in accordance with OCBC's own mechanism β auto-adjusting / paginating across date ranges transparently β so the user never touches a filter.
Owner (verbatim, 2026-07-02):
"the web app shows that tx history shown are only within a certain date range, and asked me to adjust the date range in order for it to show my desire tx⦠I have requested a long time ago for the web app to be able to produce seemless, continuous tx history, with the backend of it fetching in accordance to OCBC's mechanism so that our web app can produce the seemless, continuous tx history without readjusting any filter while the backend does the adjustment or continuously fetching data from different range for user."
Routing (why this is yours, not Accounting's)¶
This is the Bank Access page + its fetch layer β your charter. Accounting (Infrastructure) owns the accounting-import side of OCBC (T-145: dedup, same-day ordering, period locks) and built the reusable primitives you should lean on (below), but the continuous-fetch/pagination + page UX is Bank Access. Handed over deliberately to avoid the cross-agent overlap the owner just untangled on T-145. Proposed by Accounting (Infrastructure), owner-approved 2026-07-02 ("draft a handover message and pass it on to the newly onboarded Bank Access agent" β "Proceed").
The OCBC API contract (verified from a live DevTools HAR, 2026-07-02)¶
Endpoint: GET /digital/api/hk/portfolio-corp/v1/casa/transaction-history/
(BASE_URL = https://velocity.ocbc.com/digital/api/hk).
Query params:
| param | meaning |
|---|---|
| accTokenId | the CASA account token (from getCurrentAccounts() β /portfolio-corp/v1/account/inquiry-list/CURRENT_ACCOUNT + /casa/detail/{id}) |
| filterBy | MONTH or RANGE β the two real modes the portal uses |
| searchValueFr / searchValueTo | window bounds, YYYY-MM-DD |
| page | 1-based page number |
| limit | page size (the portal uses 20) |
Headers (the client already sends these β see velocity-client.getHeaders()):
x-acc-op (session token), x-source-id: BV, x-source-country: HK,
x-source-date-time (date only), x-correlation-id (fresh UUID/call).
Response data:
- transactionList[] β newest-first; each row has postDate, valueDate,
sortBy (all day-granular T00:00:00+08:00 β no intraday time),
debitAmount/creditAmount, balanceAmount (running balance), clientReference,
seqNo. bankReference/tag86Info/trxnParticularDesc come back empty.
- pageable β { offset, page, size, total, paged, unpaged }. total = the
exact number of rows in the whole window (not just this page) β the reliable
paging signal.
- maxRecordReachFlag β boolean. Observed: true for small MONTH windows
that were fully returned; false for the 1-year RANGE where only 20 of 38
were returned. Treat it as a secondary hint; drive paging off pageable.total
vs rows-fetched, not this flag.
Decisive observations from the HAR (6 live calls):
1. RANGE accepts a wide window β the portal itself pulled a full year
(filterBy=RANGE, 2024-09-03 β 2025-09-02) in one request. So you can fetch
in large chunks, not month-by-month.
2. The portal only ever requests page=1&limit=20 and, when there's more,
shows "max record reached" and makes the user narrow the filter β that IS the
pain. Our backend just has to do the paging/window-walking the portal refuses
to do.
3. Example: RANGE year returned pageable.total=38, size=20, page=1 β
18 rows remain on page=2.
What to build¶
- A continuous fetch service (extend
velocity-client.getAllTransactions, which today is incomplete β see gotchas): given an account, walk the whole history: - Use
filterBy=RANGEwith a wide window (β€ ~1 year per chunk). - Page within a window:
page=1,2,3β¦untilpage*limit β₯ pageable.total(or a page returns< limitrows). Consider a largerlimitto cut round-trips (see gotcha #2). - Walk windows backward in time: after a window is exhausted, set the next
searchValueTo = earliest date fetched β 1 day,searchValueFr = that β ~1yr, repeat until a window returns 0 rows (reached account opening). - Continuous page UX (
components/finance/OCBCVelocityDashboard.tsx+pages/api/ocbc/velocity/transactions.ts): drop the "adjust the date range" requirement β infinite-scroll / progressive load backed by the service, so the user sees one continuous ledger. Keep an optional jump-to-date, but never require a filter to see history. - Order + de-dup at window boundaries β reuse T-145's primitives (do NOT
reinvent):
lib/accounting/bankTxFingerprint.tsβ reconcileBankTxImport/matchesBankTxβ balance-aware dedup so a tx appearing at the edge of two windows isn't shown twice.orderByBalanceChainβ same-day order from the running-balance chain.- β οΈ Same-day ordering caveat (owner-raised): an oscillating balance ($20β$0β$20 with identical amounts) makes the balance chain ambiguous. The robust ordering key is the bank's own feed order (each day comes back newest-first) captured at fetch time β prefer that; use the balance chain only where feed order is unavailable. (T-145 is landing the persisted-feed-order approach for the accounting register; align with it.)
Gotchas / bugs to fix (found while verifying the HAR)¶
filterByvalue is wrong in the code.velocity-client.getAllTransactionspassesfilterBy: 'DATE_RANGE'; the sync + tx API pass'DATE_RANGE'too. The real values areRANGE/MONTHβDATE_RANGEis not one OCBC documents. It has "worked" (OCBC seems to key offsearchValueFr/Towhen present), but useRANGEto be correct and to guarantee the wide-window behavior.- Does
limitactually cap? OPEN QUESTION.sync-accounting.tsandtransactions.tsrequestlimit: 500/ user-supplied, but the portal only ever used20. If OCBC silently caps page size, any single-page fetch truncates and you must page. Verify against a window with >20 (ideally >500) rows before trusting a biglimit. (I-023 reconciled to the cent on tested ranges, so it's probably honored β but confirm, because silent truncation would drop history.) getAllTransactionsalso only walks pages within one window, never across windows, and has apage > 100safety cap β fine as a page-loop, but it's not the continuous-history service on its own.
Acceptance criteria¶
- Bank Access > OCBC shows a continuous ledger with no manual date-range adjustment; the backend fetches across OCBC's mechanism transparently.
- No duplicate rows at window boundaries; same-day rows ordered correctly.
- Reaches account opening (stops when a window returns 0), and is resilient to the
portfolio-corp-12006transient OCBC error already handled in the tx API.
Source¶
Accounting (Infrastructure) Β· handover authored 2026-07-02 Β·
https://claude.ai/code/session_015P6KzVYsQCLgEmUjR9bMwM
(HAR analysis: db7669a4-velocity.ocbc.com.har, 6 live casa/transaction-history
calls). For the Bank Access agent (Bank Access (Diagnosticcs) Β·
session_01S4ztAsDZfR97qXjhPdwLn4).
Progress β 2026-07-02 (Bank Access (Diagnosticcs))¶
- β Attestation: read AGENTS.md + the api-catalog; checked the board by scope (this is the OCBC continuous-history feature; renumbered from the T-148 handover after the GCP-SA T-148 took that UID). Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4.
- Root cause pinned. The dashboard ALREADY loads the full default range [2024-09-03 β today]
newest-first in 365-day chunks and renders progressively β so the "must adjust the date range" pain
wasn't a missing backend walk, it was per-window truncation: each chunk hit
/api/ocbc/velocity/transactions, which returned only ONE page (and used the wrongfilterBy: 'DATE_RANGE'). OCBC returns a page at a time and reports the window's true size inpageable.total; if it caps the page size, a wide window silently dropped rows β the owner narrowed the filter until a window fit in one page. So the fix is server-side full-window pagination, not a UI rewrite. - Shipped (backend-only, low-risk):
lib/ocbc/velocity-types.tsβfilterBytype corrected'DATE_RANGE'β'RANGE'.lib/ocbc/velocity-client.tsβgetAllTransactionsrewritten to page offpageable.total(not the requestedlimit, which OCBC may cap) withfilterBy: 'RANGE', de-duping on a balance-aware composite key and preserving the bank's newest-first feed order.pages/api/ocbc/velocity/transactions.tsβ returns the COMPLETE window viagetAllTransactions(was one page); the dashboard's existing full-range chunking then renders continuous history, no filter adjustment needed.pages/api/ocbc/velocity/sync-accounting.tsβfilterByvalue fix only ('RANGE'); its single-page import fetch is flagged for T-145 Phase B, not restructured here (respecting the T-145/T-149 split).- Deliberate decisions:
- T-145 primitives NOT reused β the handover said to reuse
lib/accounting/bankTxFingerprint.ts, but those live only on the unmergedclaude/t145-ocbc-balance-dedupbranch, notmain. Coupling a display feature to another agent's in-flight file would be fragile + create cross-agent conflict, so the fetch layer uses a small self-contained balance-aware dedup + feed-order concat. Unify once T-145 merges. - No frontend rewrite. The existing progressive chunked render already delivers a continuous ledger once truncation is fixed; the RangePicker stays as an OPTIONAL jump-to-date (default = full history). Infinite-scroll + a backend backward-walk-to-opening are optional future refinements, not needed to resolve the pain.
- Verification: heavy
tsc --noEmit+next buildtype-check phase clean (AGENTS.md build-gate). NOT live-verified against OCBC β this sandbox has no OCBC Velocity session (owner logs in via the web app). Owner-gated live check: on Bank Access > OCBC, confirm the full ledger renders without adjusting the date range (especially a window that previously showed "max record reached"). - Open follow-ups: (a) owner live browser verification; (b) T-145 to page the accounting sync off
pageable.totaltoo (same truncation on the import side); (c) optional infinite-scroll + backend auto-walk to account opening; (d) confirm whether OCBC honorslimit > 20(paging is correct either way). - Blast radius: OCBC Velocity fetch layer only β
velocity-client.getAllTransactions, the transactions API route, thefilterBytype, and a one-line value fix in the accounting-sync route. No change to OCBC payments/2FA/statements, to Airwallex, or to the accounting import's dedup (T-145's domain). - DONE (merged to
main, PR #846). Verdict: the full-window pagination fix landed and type-checks clean (heavytsc+next buildtype phase). Merged β live-verified β the OCBC ledger has NOT been exercised against a real Velocity session from this sandbox; if the owner's browser check on Bank AccessOCBC shows any gap (e.g. OCBC rejects
filterBy: 'RANGE'or paging behaves differently), reopen this task and append the rework SHAs. Related commit SHAs (append-only, newest last):132562bc(feat) - the statusβdone flip + the #846 squash-merge commit.