uid: T-184 title: "Pay withβ¦" dropdown on Bank Access status: doing area: accounting created: 2026-07-25 updated: 2026-09-24 owner: girafeev1 related: T-178, T-199, T-167, T-204, T-205
T-184 β "Pay withβ¦" on Bank Access¶
Why¶
From T-178's owner-revised build order, phase 7: a "Pay withβ¦" dropdown on Bank Access, so an outbound payment can be originated against an existing document rather than typed free-hand.
The WOPC-first flow already exists in the other direction: POST /api/accounting/wopc/create-pending
mints a WOPC before the bank transaction, returns a matching reference
(#ERL-WOPC|JC-2026-007) for the transfer remarks, and wopc-auto-match.ts links the two when the
transaction syncs. This task is the counterpart entry point on the Bank Access side.
Scope¶
- A "Pay withβ¦" affordance offering the documents that can originate a payment (unpaid WOPCs first; see T-186 for vendor invoices).
- Reuse the existing matching-reference scheme rather than inventing a second linking mechanism.
Related¶
T-185 stops the same document being paid twice through this or any other route.
Source¶
Split out of T-178's phase 7 on 2026-07-25 at the owner's request ("Split Phase 7 in T-178"). Phase 7 was a catch-all of three unrelated items; each now stands alone.
Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Assignment (2026-07-28)¶
Owner: "Regarding T-184, I think 'Bank Access (Diagnostics)' should be the most suitable agent to work on it" β this task is assigned to the Bank Access (Diagnostics) agent. It fits: the work is a Bank Access-side entry point ("Pay withβ¦"), and the queue-at-issue step below needs the Airwallex transfer particulars that agent already owns. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Note from T-178 (2026-07-28)¶
Reimbursement WOPCs joined the signature-release pipeline: a payment queued behind one now releases the moment the co-director signs, and the receipts settle themselves at that same moment (see T-178). What T-178 deliberately did NOT build is the queuing step at issue time β the queue's authorization design requires the creator to review the exact transfer (amount, saved beneficiary, source account), which the Reimbursements confirm doesn't collect. That is THIS task's scope: when "Pay withβ¦" lands, reimbursement WOPCs should be offerable like fee WOPCs, and queuing one behind its signature completes the owner's "payment transaction submitted simultaneously" end to end. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
2026-09-24 β Picked up Β· scope narrowed to ERL's Airwallex Β· open questions to the owner¶
β Read AGENTS.md Β· checked the board by scope (T-184 is the covering task; T-199 Phase 3 points here for reimbursement origination) Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Status todo β doing. Owner, verbatim (2026-09-24): "Okay, let's pick up T-184 and implement it
only with ERL's Airwallex and leave OCBC out of it. Let me know if anything still need settling before
we proceed"
Scope decision β Airwallex (ERL) only, OCBC excluded. Every OCBC transfer needs a separate approval on OneToken, so an OCBC payment can never be released automatically when a WOPC is signed β the whole point of this task. OCBC-paid WOPCs stay manual and still link up through the existing auto-match.
What already exists (verified 2026-09-24, not assumed): - An issued reimbursement WOPC already carries the payee's bank name, bank code, account number and holder name (the account the operator picked at issue, T-194). - The unpaid-WOPC list already reports each WOPC's signing state β signed / awaiting signature / needs no signature β which is exactly what decides "hold until signed" vs "send now". - The Airwallex payment drawer can already hold a payment behind a signature and release it the moment the WOPC is signed (T-167); only an appointed payer with an authenticator code can set one up (T-204); and it sits behind the Airwallex lock (T-205). - Airwallex carries our transfer reference into its own feed β the one reimbursement already paid through Airwallex reads "Pay 2999.60 HKD to Jeffero Chan (ERL-WOPC/2026-011)" β so the existing auto-match links the payout to its WOPC, books it to GL 2110 and flips Records to "Reimbursed" with no new linking mechanism (reuse, as this task's scope asked).
The two gaps this task closes: 1. Nothing lets you pay an existing unpaid WOPC β the only WOPC entry on Bank Access creates a new contractor-fee WOPC. 2. The drawer picks the saved Airwallex recipient by name only β wrong if a person has two accounts or two people share a name.
Open questions put to the owner before any code (answers to be recorded here): - Q1 β where the payment is set up (Bank Access β Airwallex "Pay withβ¦", vs inside the Reimbursements tab). - Q2 β how the Airwallex recipient is found (by account number; create from the WOPC's bank details when none exists), and whether the existing contractor-fee flow switches from name-matching too.
Firestore: expected NO structure change β queued payments are created in their existing shape; the recipient link is derived by account number each time, not stored. To be re-confirmed before build.
2026-09-24 (pt2) β Owner's answers: two payment modes chosen at WOPC creation Β· findings Β· two questions pending¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Owner, verbatim (2026-09-24):
1. For "Where do you set up the payment" 1. If the person who's in charge of creating the WOPC would like the WOPC to be paid immediately after it's signed, it's only gotta be paying out of ERL's Airwallex account. Implement this to our web app so that our web app could facilitate this while requiring the person to be authenticated with the TOTP 2. Or, allow the person to pre-select which bank account ERL should pay out of, so that a person with access to bank access could go into either bank account (meaning, including OCBC) and make transaction with a created, unpaid WOPC (is this infras already exists?) 2. Regarding "How is the Airwallex recipient found?", bank account information of the recipient should be already properly stored on our web app's accessible database
The design this sets (replaces the "Pay withβ¦ on Bank Access only" framing): whoever creates the WOPC picks how it gets paid β - Mode 1 β pay automatically when signed: ERL's Airwallex only; the creator authorises it with their authenticator code at creation; the payment is held and released the moment the WOPC is signed. - Mode 2 β pre-select the paying account, pay later by hand: the creator names which ERL account (Airwallex or OCBC) it should be paid from; someone with Bank Access later pays that existing, unpaid WOPC from that bank's page. This brings OCBC back into scope for manual payment only β updates the pt1 scope row.
Owner's question β "does this infrastructure already exist?" Answer: partly. Verified 2026-09-24:
- Exists: the unpaid-WOPC list (with each WOPC's signing state); ~~both payment forms (Airwallex and OCBC)
already accept a WOPC's details and reference~~; auto-match links a payment back to its WOPC afterwards.
β superseded by pt5: the OCBC form accepts a reference, but the OCBC connection can only move money
between ERL's OWN OCBC accounts (Internal Transfer β Same Owner); it cannot send to anyone else's bank,
so it cannot pay a WOPC. Edit-out signed: Bank Access (Diagnosticcs) Β·
https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
- Does not exist: paying an existing WOPC from either bank page β "Pay with WOPC" on both pages always
creates a new WOPC (WOPCPaymentModal β create-pending). Nor is there anywhere to record which
account a WOPC should be paid from.
- Mode 1 already exists for contractor-fee WOPCs created through Bank Access β Airwallex (T-167); it is
missing from the reimbursement issue flow.
Q2 premise checked against production data (2026-09-24): stored bank details are complete wherever
they exist β but they exist for very few people. Contacts (aote-system/individuals): 29 people, only 2
with a bank account on file (Jeffero Chan Γ4 accounts, Jake Ngai Γ1) β all 5 complete (bank code +
account number + holder). Contractor payee directory (tebs-epl/payees): 2 records, both complete. Any
other payee cannot be paid by either mode until their bank account is added in Contacts.
Interpretation of Q2 (to be built unless the owner objects): the stored bank details are the source of truth β the app finds the matching saved Airwallex recipient by account number, and creates it from the stored details when none exists; no manual recipient picking. Applied to the existing contractor-fee flow too (it currently matches by name only).
Pending owner answers (no code until answered):
- QA β structure gate: Mode 2 needs the WOPC to remember which account it should be paid from. Proposed:
a new payout.from on WOPC documents (tebs-epl β payees/{abbreviation}/wopc/{docId}), holding the
company account's id (e.g. ERL-OCBC-β¦). This refines the 2026-08-04 "funding account derived, never
stored" decision for the advance instruction only (the record of where a payment actually came from
stays derived from the payment). Full before/after put to the owner in chat. NOT approved yet.
- QB β WOPCs that are never signed (non-director staff reimbursements; fees to non-related parties):
under Mode 1, pay as soon as the creator confirms with their code, or not offer Mode 1 for them.
2026-09-24 (pt3) β Owner's second round: settled points Β· one structure approval pending¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Owner, verbatim (2026-09-24):
1. I think on the bank accounts, we should allow the user to pay with a) existed WOPC (that are unpaid), and b) a new one 2. So you're saying that the "create transaction automatically upon signed WOPC" already exists for sub-contractor fee? But does it requires the user to do TOTP when creating the WOPC? If not, we need to extend this mechanism there as well 3. In this case, when creating a WOPC, do not show contacts in the dropdown for people with no bank account info, or show them, but do not allow them being selected and having them grayed out with a tag indicating that these people have no bank account info stored 4. For WOPCs that are never signed, I think we need to be careful with the wording here and refer them to those that needs to be signed or not, while that they're UNPAID. For WOPC that are unpaid but does not require signature, (a) is logical if "code" refers to the TOTP 5. I think that the ERL account id should be stored somewhere, but if info in the payout field will and should essentially be the same as what's written to transaction, then the web app might as well just reads into what's written there before further transaction confirming info are written to it. You see what I mean here?
Settled:
1. Both bank pages (Airwallex and OCBC): "Pay with WOPC" offers an existing unpaid WOPC or a new one.
2. Contractor-fee auto-pay already requires the authenticator code β twice: once to unlock Airwallex
(T-205) and once to authorise the held payment (T-204). Verified gap: the WOPC document is
created one step before the payment's code prompt (WOPCPaymentModal β create-pending, then the
drawer), so abandoning at the prompt leaves an unpaid WOPC behind. Rebuilt flow: when a WOPC is set to
pay automatically, the code is verified before the WOPC is created. The same mechanism extends to
reimbursement WOPCs.
3. People with no bank account on file are shown greyed out, cannot be chosen, and carry a "No bank
account on file" tag (the owner's second option β it tells the creator why). Reimbursement payees are
not picked from a dropdown (they are whoever submitted the receipts), so the same rule greys out that
person's Reimburse action with the same tag. Consequence stated to the owner: today only Jeffero Chan and
Jake Ngai have bank details stored.
4. Wording: WOPCs are described as requiring a signature or not β and paid or unpaid β never as
"never signed". An unpaid WOPC that does not require a signature, set to pay automatically, is paid as
soon as its creator confirms with their authenticator (TOTP) code.
5. Owner's storage design adopted as the proposal (replaces payout.from): the ERL account id is
written into the WOPC's existing transaction section at creation, as the plan; the payment's id and
date join it when the payment is linked. Verified safe: every WOPC reader decides "paid" from
transaction.id (isWopcPaid, the lifecycle, the Records list), and the shared accessor only reads
id/date, so an account alone flips nothing to paid. Structure approval of
transaction.bankAccountId pending β no code until the owner says yes.
2026-09-24 (pt4) β transaction.bankAccountId approved Β· Phase 1 of 3 built¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Owner, verbatim (2026-09-24): "transaction.bankAccountId approve"
Structure change β approved. Proposed by Bank Access (Diagnosticcs) (adopting the owner's own design
from pt3 point 5); approved by the owner, quoted above. Database tebs-epl, path
payees/{abbreviation}/wopc/{docId}. The whole document as it is in production, with the one addition:
bank: map β the recipient's bank (unchanged)
accountHolderName Β· accountNumber Β· code Β· name: string
contractor: map
address: map { country Β· line1 Β· line2 Β· line3: string }
name: string
lineItems: array
payee: map { abbreviation Β· name: string }
transaction: map
+ bankAccountId: string + ADDED β ERL account to pay from (e.g. "ERL-AWX-HKD");
+ set at creation as the plan, overwritten with the account the
+ money ACTUALLY left from when the payment is linked
date: timestamp (unchanged β filled in when paid)
id: string (unchanged β filled in when paid; still the ONLY "paid" signal)
WOPC: map
closingDirectorId Β· currency Β· notes Β· refNumber Β· status: string
currentRevision Β· totalAmount: number
issuedDate Β· paymentDate: timestamp
created: map { at, by } Β· deleted: map { at, by, reason }
pdf: map { fileId, signedFileId, storagePath }
signature: map { at, by, requestId, source }
- Why: a WOPC's creator now chooses which ERL account pays it (Mode 2) or that Airwallex pays it automatically (Mode 1); the choice has to survive until someone pays it, possibly days later, from whichever bank page it was planned for.
- Considered and rejected: (a) a separate
payout.fromsection β my first proposal; the owner pointed out the account belongs where the payment's own id and date will sit, so one section tells the whole story. (b) Keep deriving it, never store it (the 2026-08-04 T-199 "Option A" decision) β still right for a payment that has happened, but a plan made before any payment exists has nothing to derive from. (c) Reusebankβ that map is the recipient's bank; putting the payer's account there would mix the two sides of the payment. - Why the name: it matches the field the bank's own transaction record already uses for the same
fact (
transaction.transactional.bankAccountIdintebs-erl), so it reads the same everywhere. - Safety check done before approval: every reader decides "paid" from
transaction.id(isWopcPaid, the lifecycle, the Records list); an account on its own flips nothing to paid. The 40 existing WOPCs don't carry it, so they appear on both banks' "unpaid" lists.
Phase 1 (foundations) β what it does, in plain words: 1. A WOPC can now remember which ERL account it should be paid from. Creating a WOPC (contractor-fee or reimbursement) accepts an optional choice of Airwallex HKD, OCBC Current or OCBC Savings β only the accounts connected to Bank Access (Fubon and the closed accounts can't be chosen, since the app can't pay from them). Nothing on screen offers the choice yet; Phase 3 adds it. 2. When a payment is linked to a WOPC (by the automatic reference match or by reimbursement settlement), the account the money actually came from is written over the plan. 3. The unpaid-WOPC list can now be asked for "WOPCs this bank can pay": it hides WOPCs planned for the other bank and WOPCs that already have a payment held for signature, and says whether the recipient's bank details are on file (with the account number masked). Phase 2's picker uses this. 4. People with no bank account on file are greyed out in the contractor search when creating a WOPC that still has to be paid (Bank Access "Pay with WOPC" and Records "Create Pending WOPC"), tagged "No bank account on file", and can't be chosen. Not applied where a WOPC records a payment that has already left the bank (linking an existing transaction) β blocking there would stop the books recording money already sent. The server refuses a WOPC planned for an ERL account unless it carries a bank code and a full account number (the form's "last 4 digits only" shortcut can't be paid). 5. Reimbursements: the employee's name is greyed and each problem is its own tag ("No bank account on file", "No legal name", β¦) instead of the generic "Can't be paid yet"; Reimburse was already disabled for them (T-183). The readiness label changed from "No bank account" to "No bank account on file" so both screens use the same words.
Firestore touched by Phase 1: WOPC documents gain transaction.bankAccountId exactly as approved
above (written at creation when a choice is given, and at payment link). Bank transactions, the payment
queue and individuals are read only β no change to their shape.
Still not true after Phase 1: nobody can pay an existing unpaid WOPC from a bank page yet (Phase 2), and nobody can choose the pay-from account or "pay automatically" when creating a WOPC yet (Phase 3).
2026-09-24 (pt5) β Phase 2 (Airwallex) built Β· correction: OCBC can't pay a WOPC today Β· owner question¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Phase 1 merged: PR #1042 β 35654bd (board mirrored).
Correction (found while building Phase 2). In pt2 I told the owner both payment forms "already accept a
WOPC's details". That is wrong for OCBC. Verified in code: the only money-moving OCBC call the app has is
an Internal Transfer β Same Owner (groupType: 'ITSO', lib/ocbc/velocity-client.ts), which only
moves money between ERL's own OCBC accounts. The OCBC page can look up an FPS ID (fps-lookup) but has
no FPS send, and nothing that sends to another bank by account number. So no WOPC can be paid to a
contractor or employee from the OCBC page today. The existing OCBC "Pay with WOPC" (create new) makes the
WOPC and opens the transfer form, but that form can't send to the recipient's bank. Mode 2 with OCBC as the
chosen account is therefore not buildable on the current OCBC connection. It is put to the owner
(below) rather than worked around silently.
Phase 2 β Airwallex side, in plain words:
1. Pay an existing unpaid WOPC from the Airwallex page. "Pay with WOPC" now opens with a choice:
Pay an existing unpaid WOPC or Create a new WOPC.
- The existing list shows every unpaid WOPC that Airwallex can pay (not planned for OCBC, and not
already held for an automatic payment).
- Each row shows who it pays (account masked, "β’β’β’β’ 1234"), the amount, whether it requires a
signature (and whether it's signed), and the planned account.
- WOPCs whose recipient has no bank account on file are greyed out and can't be chosen.
- Reimbursement WOPCs appear here too. This is the first place in the app where an issued
reimbursement WOPC can be paid directly.
2. The recipient comes from the WOPC's stored bank details, never from a name.
- The payment form asks the server for "the recipient of WOPC X". The server reads the WOPC's own bank
details, finds the saved Airwallex recipient with the same account number and bank code, and
locks it in.
- If none exists, the server saves one to Airwallex from those details and records it in the audit
log.
- The browser only ever sends the WOPC reference, so the form can't swap in a different account.
- If it can't be matched or saved (for example the WOPC has only the last 4 digits, or no address,
which Airwallex needs to save a new recipient), the form says exactly why. The payer can then pick
a recipient by hand, warned to use only the account on the WOPC.
- Name matching remains only for the dormant reimbursement link that carries no WOPC.
3. Signature handling.
- A WOPC that requires a signature and isn't signed yet is held when confirmed, and sent the
moment it's signed. This is the existing T-167 queue.
- A signed WOPC, or one that requires no signature, is sent on confirm with the authenticator code
(T-204).
4. Airwallex reason: business_expenses for reimbursement WOPCs and professional_business_services
for contractor fees. This also applies to the reimbursement link.
5. A new WOPC created on the Airwallex page records Airwallex HKD as its planned account. As a result,
it's refused unless the payee's full account number and bank code are on file.
Firestore touched by Phase 2:
- Reads WOPCs (tebs-epl) and the payment queue (tebs-erl).
- New WOPCs from the Airwallex page write transaction.bankAccountId exactly as approved in pt4.
- Saving an Airwallex recipient adds an entry to the existing audit log (aote-system auditLogs)
in its existing shape.
- No structure change. Recipients are saved in Airwallex, not Firestore.
Still not true after Phase 2: - OCBC can't pay a WOPC. This waits on the owner's answer below. - Airwallex can't save a new recipient automatically when the WOPC has no address. Today no Contacts record carries an address, so a reimbursement WOPC can only be paid when the person is already a saved Airwallex recipient with the same account number. The two contractor-directory payees have addresses. - Mode 1/Mode 2 choice at creation is Phase 3. - Not verified end-to-end against live Airwallex from here: the live keys are not readable from the sandbox. The recipient format was taken from Airwallex's own schema on the test account.
Owner question (pending): how should a WOPC planned for OCBC be paid β - (A) show its details and reference on the OCBC page to pay by hand in OCBC's own banking, and let auto-match link it; - (B) build FPS sending into the app; - (C) keep OCBC out of WOPC payments for now?
Commit SHAs touching T-184 (append-only, newest last): 79c94a3 (#1039) Β· aa55453 (#1040) Β·
4d90504 (#1041) Β· 7f405a7 β squashed 35654bd (#1042, Phase 1) Β· 94353ee β squashed 0be2f9b
(#1043, Phase 2) Β· c1c851e β squashed f02a52d (#1044, Phase 2b) Β· 8cd44e6 β squashed c99338c
(#1045, Phase 2c) Β· dfc1c39 β squashed 20268d7 (#1046, Phase 3) Β· 9fc06c2 β squashed 5b14f71
(#1048, deploy record) Β· local agent's capture f2e9f55, 57762cc (branch, carried over) Β· OCBC = the
commit that adds pt10 (#1049).
2026-09-24 (pt6) β Owner: OCBC stays in-app, no FPS; Airwallex pays from our stored details Β· Phase 2b Β· one approval pending¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Owner, verbatim (2026-09-24):
1. We don't make things that routes us back to OCBC's own online banking, that's the whole point of having the OCBC "portal" on our EOP. If you need further html scraping, and that you're unable to do it, draft a message so that I could forward it to a local agent. 2. We currently don't integrate FPS transactions for OCBC because OCBC charges a service fee per FPS transaction (verify it, and if it's true, we'll leave it as such) 3. I've been making transactions with ERL's Airwallex without having to need to store payee's address, I guess that's because we're not relying Airwallex to store recipient information but our web app. If you can confirm this, then please integrate and implement what we're trying to do here without needing to utilize Airwallex in this regards
1 β OCBC stays in the EOP portal. Option (A) from pt5 (paying by hand in OCBC's own banking) is
rejected. The in-app route that avoids FPS is Velocity's transfer to another customer's OCBC account
(believed product code ITOA). Both payees with bank details on file bank with OCBC (035). The app has
never sent that transfer type, and its API calls can only be observed from a live, OneToken-approved
session. The sandbox can't do that, so I've written a brief for a local agent:
docs/handoff-ocbc-third-party-transfer-capture.md. OCBC WOPC payments are built once that capture
lands.
2 β FPS fee verified: true. OCBC Bank (Hong Kong) Business Customer Bank Service Fees Guide (effective 18 March 2026), section "Outward FPS (Faster Payment System)": - a single FPS transfer to a non-OCBC account costs HK$5 up to HK$500,000, and HK$10 above that; - a batch or payroll FPS transfer costs HK$1 per item.
For comparison, the same guide lists CHATS via e-banking at HK$55. FPS stays out, as the owner decided.
3 β Confirmed: Airwallex doesn't need to hold the recipient, or an address. Verified 2026-09-24 on the Airwallex test account: - A payment carrying its recipient's bank details directly, with no saved recipient and no address, passes Airwallex's transfer validation. - A missing account number, or a 2-digit bank code, is rejected. So the validation is real, not a rubber stamp. - Airwallex's published form rules list an address for Hong Kong recipients, but that isn't enforced for a local Hong Kong dollar payment.
Not checkable from here: the live account, since its keys aren't readable from the sandbox. The first real payment is the live confirmation.
Phase 2b β built (supersedes pt5 point 2):
- Airwallex payments of a WOPC now carry the WOPC's own bank details as the recipient. The payment
screen sends only the WOPC's reference. The server loads the WOPC, checks it's still unpaid, not
voided and not already held, then builds the recipient from the WOPC's stored bank code, account
number and holder name, and sends it inside the payment.
- Nothing is saved in Airwallex, and nothing is matched by name. The pt5 lookup-and-save endpoint
(/api/airwallex/beneficiaries/for-wopc) is removed.
- The payment must be exactly the WOPC's total, in its currency. The server refuses anything else,
and the screen locks both fields.
- The screen shows who is paid. A "Pays" box shows the holder's name, bank and account β’β’β’β’ 1234,
marked "the bank account on the WOPC". If the details can't be paid (only the last 4 digits, no
3-digit bank code, not HKD), it says so plainly and won't continue.
- The unpaid list's "No bank account on file" now also covers WOPCs holding only the last 4 digits,
or no 3-digit bank code.
Firestore touched by Phase 2b:
- WOPCs and the payment queue are read only.
- The audit-log entry for a send now also records the WOPC reference, as one more line in the existing
changes list (existing shape).
- No structure change.
β οΈ One structure approval pending β payments held for a signature. - Why it's needed: A WOPC that requires a signature and isn't signed yet has its payment held and sent when it's signed (T-167). The held payment is stored, and today it must name a saved Airwallex recipient. So until this is approved, a held payment still asks the payer to choose a saved Airwallex recipient, and the screen shows the WOPC's account to match against. - The proposal: don't store a recipient in the held payment at all. When the WOPC is signed, build the recipient from the WOPC's bank details exactly as an immediate payment does, using the WOPC reference the held payment already stores.
Database tebs-erl, path accounting/wopcPaymentQueue/entries/{id}, whole document (from the
code; production currently holds 0 entries, so nothing needs migrating):
wopcRef: string β the WOPC whose signature releases it (unchanged β reused)
status: string awaiting_signature | releasing | released | release_failed | cancelled
transferRequest: map β the exact Airwallex request fired on release
request_id: string (idempotency key)
transfer_currency: string
transfer_method: string LOCAL | SWIFT
reason: string
transfer_amount: number
β beneficiary_id: string β REMOVED for WOPC payments β the recipient is derived at release
β from the WOPC's bank details via wopcRef (stored β derived)
source_currency: string
reference?: string
fee_paid_by?: string
swift_charge_option?: string
lock_rate_on_create?: boolean
createdBy: string
createdByRole?: string
createdAt: timestamp
updatedAt: timestamp
releasedAt?: timestamp
releasedBySignatureOf?: string
transferId?: string
transferStatus?: string
failureReason?: string
cancelReason?: string
- Considered and rejected: adding
transferRequest.beneficiary(a copy of the recipient's bank details, including the full account number) β it copies bank details into a second document, against the references-not-copies rule (T-193). - Considered and rejected: keeping
beneficiary_idβ it needs a recipient saved in Airwallex, which the owner rejected. - The existing field that does the job:
wopcRef, already stored. It's why nothing needs adding. - What breaks without it: held payments keep depending on recipients saved in Airwallex.
- Side effect worth knowing: the payment goes to the bank details on the WOPC as signed, which is the document the signer approves.
- Not built. Awaiting the owner's yes/no.
2026-09-24 (pt7) β Held-payment change approved and built (Phase 2c)¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Owner, verbatim (2026-09-24): "Approve beneficiary_id to be removed from held payments"
Structure change β approved and applied.
- Proposed by Bank Access (Diagnosticcs) in pt6; approved by the owner, quoted above.
- Database tebs-erl, path accounting/wopcPaymentQueue/entries/{id}. Production held 0 entries
when this landed, so there was nothing to migrate.
- Whole document after the change:
wopcRef: string β now also how the recipient is found at release
status: string awaiting_signature | releasing | released | release_failed | cancelled
transferRequest: map
request_id: string
transfer_currency: string
transfer_method: string
reason: string
transfer_amount: number
β beneficiary_id: string β REMOVED β no longer written; recipient built from the WOPC at release
source_currency: string
reference?: string
fee_paid_by?: string
swift_charge_option?: string
lock_rate_on_create?: boolean
createdBy Β· createdByRole? Β· createdAt Β· updatedAt Β· releasedAt? Β· releasedBySignatureOf? Β·
transferId? Β· transferStatus? Β· failureReason? Β· cancelReason? (all unchanged)
What it does, in plain words:
- A payment held for a WOPC signature no longer names an Airwallex recipient.
- The moment the WOPC is signed, the app reads the bank details on the WOPC, builds the recipient from
them, and sends the payment. The same happens on a "Process Payment (retry)", which also keeps the
original idempotency key, so a retry can never pay twice.
- If the WOPC can't be paid from its details (voided, or only the last 4 digits), the release fails
visibly (release_failed plus an owner notification) instead of paying anyone else.
- When holding a payment, the app first checks that the WOPC is still payable, that the amount and
currency are exactly the WOPC's, and that its bank details can be paid. So a payment can't be accepted
today and then fail at signing for a reason that was already knowable.
- On the payment screen, held payments now get the same read-only "Pays" box as immediate ones,
plus a line saying the payment waits for the signature. The temporary "choose a saved recipient" step
from pt6 is gone.
- Airwallex holds no recipient for any WOPC payment, immediate or held.
- The code keeps a fallback for any held entry that still carries a stored beneficiary_id: it's sent
as it was. None exist in production; this is only for safety.
Firestore touched: - Held payments: shape changed exactly as above. - WOPCs: read only. - Audit log: the existing shape, now noting "(from WOPC β¦)" as the recipient.
Still not true:
- OCBC can't pay a WOPC yet. That waits on the local agent's capture
(docs/handoff-ocbc-third-party-transfer-capture.md).
- Phase 3 (choosing how a WOPC gets paid at creation) isn't built.
- The "no address needed" finding was verified on the Airwallex test account only. The first live
payment confirms it.
2026-09-24 (pt8) β Phase 3 built: the creator chooses how a WOPC is paid Β· double-payment guard¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Owner, verbatim (2026-09-24): "Approve beneficiary_id to be removed from held payments Continue from where you left off." (Phase 3 was the next agreed step; its design was settled in pt2βpt3.)
Phase 3 β in plain words. Creating a WOPC now asks "How will this be paid?" in both places a WOPC
is made to be paid: Records β Create Pending WOPC (contractor fees) and Reimbursements β
Reimburse.
- Pay automatically from Airwallex HKD (appointed payers only).
- The creator types their authenticator code. The server checks it before the WOPC is created,
so a refused code creates nothing.
- A WOPC that requires a signature has its payment held, and sent the moment it's signed.
- A WOPC that requires no signature is paid straight away.
- Either way the money goes to the bank account on the WOPC. Reimbursements go out as "Business
Expenses", contractor fees as "Professional Business Services".
- If the payment can't be sent after the WOPC is made, the confirmation says so plainly, and the
WOPC can be paid from Airwallex β Pay with WOPC β Pay an existing unpaid WOPC.
- Pay later by hand, from a chosen account. Airwallex HKD, OCBC Current or OCBC Savings. The WOPC
records the plan (transaction.bankAccountId) and appears on that bank's "Pay an existing unpaid WOPC"
list. Choosing OCBC shows a note that paying from the OCBC page isn't available yet.
- The choice is required. The create button stays off until one is made.
- Reimburse only: when the operator links a bank payment that has already been made, the choice
is hidden, since the money has already gone out.
- Airwallex β Pay with WOPC β Create a new WOPC now uses the same code-first path. The code is typed
in that window, and "Create & pay" creates the WOPC and pays (or holds) it in one step, with no
separate payment drawer. This closes the pt3 gap: abandoning the payment can no longer leave an unpaid
WOPC behind.
Double-payment guard (a gap found while building this, in the Phase 2 code; not deployed).
- The gap: after a WOPC is paid immediately, the books only mark it paid once the payment reaches the
bank feed and is linked. Until then it still looked unpaid and could be paid again from "Pay an
existing unpaid WOPC".
- The fix: before a WOPC is offered on that list, or paid, the app now asks Airwallex whether a
payment carrying that WOPC's reference was made in the last 60 days and hasn't failed or been
cancelled. If so, the WOPC is hidden from the list, and paying it is refused with the transfer's id and
status.
- If Airwallex can't be asked at that moment, paying is refused (fail closed); the list still
loads.
- Every WOPC payment is now forced to carry the WOPC's own reference (#ERL-WOPC|β¦) on the server,
and the payment screen locks that field. That's what makes the check reliable, and it's also what
auto-match links by.
- No Firestore reads or writes: it asks Airwallex directly.
Firestore touched by Phase 3:
- New WOPCs write transaction.bankAccountId (approved in pt4).
- Payments held at creation are written in the pt7-approved shape (no recipient).
- The audit log gets entries in its existing shape. They record a refused code or a refusal to pay, a
held payment, a sent payment, and a failed one.
- No structure change.
Still not true: - OCBC: - a WOPC planned for OCBC waits on OCBC's list until the in-app OCBC transfer is built from the local agent's capture; - the OCBC page's own "Pay with WOPC β new" is unchanged: it creates the WOPC, but its transfer form can't reach another person's bank. - Not tested against the live accounts: Airwallex's live keys and a real authenticator code aren't available in this sandbox. The first real "Create & pay" is the live check. - Not deployed.
2026-09-24 (pt9) β Deployed to production¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
- Owner, verbatim (2026-09-24): "Deploy" β a one-off order.
- What went out: Vercel deployment
dpl_6TdjHJiFChoHTxrj7PVvn27W5w6yatmain9d94e0a. It is READY, its target is production, and the production address points to it. That covers Phases 1, 2, 2b, 2c and 3 (#1042β#1046), plus the T-205 auto-connect (#1047). - Checked beforehand: the full type-check (unfiltered,
--incremental false) passed on that exact commit, on dependencies installed fresh from the unchanged lockfile. - Status stays
doing: OCBC payment of WOPCs still waits on the local agent's capture (docs/handoff-ocbc-third-party-transfer-capture.md). The first live "Create & pay" and the first "Pay an existing unpaid WOPC" are the live checks of the Airwallex side.
2026-09-24 (pt10a) β OCBC "other-account" transfer captured & documented (the "ITOA" question)¶
β
Read AGENTS.md. Handoff from Bank Access (Diagnostics) asked a local agent to capture Velocity's
transfer-to-another-OCBC-account flow (owner logged in, owner-initiated, owner-approved on OneToken; the
agent only observed a redacted HAR). Two real HK$0.01 transfers were compared β one to the owner's own
OCBC account, one to a third-party OCBC account (co-director). Contract:
docs/ocbc/velocity-itoa-contract.md.
Two findings that shape this task's build:
1. There is no separate "ITOA" flow. Paying another OCBC customer uses the same product code
(groupType: ITSO), the same SUBMIT endpoint, and the same payload as a same-owner internal
transfer, with beneficiary bank fields empty β verified against a real third-party transfer. Only the
(encrypted) beneficiary account + name differ; no saved payee is required (a raw account number is
validated by mask-name/inquiry, which returns the holder's name). So the app's existing ITSO
plumbing can already pay any OCBC account β this is an integration-wiring job, not a new product.
2. β οΈ OCBC's remark field rejects # and | (owner-confirmed; needs an owner/Bank-Access decision).
The WOPC link scheme puts #ERL-WOPC|<id> in the transfer remarks for wopc-auto-match to find;
those characters cannot be typed (reference came through 11β13 chars, separators stripped). The
link-by-remark design cannot work over OCBC as-is. Options recorded in the contract Β§5:
(a) an OCBC-safe alphanumeric tag, or (b) link by attributes (source+beneficiary acct + amount +
value date + Velocity's trxnReferenceNo). Not decided here.
Still outstanding (look-only): the posted-history narrative text, and the non-OCBC/FPS/CHATS fee (not captured β the test payee is OCBC, so it stayed internal/free).
No code changed. Raw HAR never committed (redacted by an allowlist extractor, leak-scanned clean). Deliverable is docs-only; opening/merging the PR needs the owner's GitHub credential (this session has none).
Source: EOP Local Assistance (Jefito) Β· local session 40d67433-b487-4305-b162-b3b6f7027d68
Carried over verbatim from branch claude/t184-ocbc-otheraccount-contract (which had no pull request and
was cut from a much older main, so it couldn't merge cleanly), together with the contract file it
links. Source (edit): Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
2026-09-24 (pt10) β OCBC pays WOPCs to OCBC accounts; remark = display reference¶
β Read AGENTS.md Β· checked the board by scope Β· tracking T-184. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
Owner's decision, relayed in the local agent's handoff, which the owner forwarded (2026-09-24, verbatim):
WOPC remark decision (owner, 2026-09-24) β SCOPE: the OCBC transaction remark ONLY. The WOPC number keeps rendering, storing and generating exactly as it does today [β¦] Remark string = the WOPC's display reference, with the leading # dropped β ERL-WOPC/JC-2026-007
Checked before building (the handoff asked for it):
- / and - survive in OCBC's history. Past OCBC transfers in the books show their remarks
verbatim: ERL-WOPC/2025-007, β¦-008, β¦-009, β¦-010 all appear intact before "TRANSFER-DEBIT", and
a 43-character remark survived whole. So the display form works, and the [A-Z0-9]-only fallback
isn't needed.
- MT940 doesn't apply to OCBC. All 101 OCBC transactions in the books come from the Velocity data
feed (api_import). MT940 was only ever the closed Dah Sing accounts' import.
- Auto-match already reads ERL-WOPC/JC-2026-007 (with or without #, / or |), so it needs
no change. The handoff expected an edit to the matcher. It was checked and found unnecessary, and a
test now pins it.
- Not checkable from here: the two HK$0.01 test transfers aren't in the books yet. The first real
OCBC WOPC payment is the live check of the remark.
What it does now, in plain words:
- OCBC β Pay with WOPC offers the same two choices as Airwallex: Pay an existing unpaid WOPC or
Create a new WOPC.
- OCBC can only pay people who bank with OCBC. WOPCs for anyone else are greyed out, tagged "Not an
OCBC account β pay from Airwallex". Creating a new one for a non-OCBC payee is stopped before anything
is created.
- Paying one:
- OCBC first confirms the account on the WOPC, and the screen shows "Pays: ERL-WOPC/β¦, and requires the WOPC's exact total.
- Then the usual OneToken approval follows.
- Afterwards the screen syncs the payment into the books and runs auto-match, as it already did.
- Only Jake and you can pay a WOPC this way, and each one is recorded in the audit log.
- Double-payment guard extended to the books: a WOPC is hidden, and a payment refused, if a debit in
the last 60 days names it but was never linked (e.g. the amounts differed). This applies to both banks.
- Choosing "pay later from OCBC" at creation is only offered, and only accepted, when the payee
banks with OCBC. Otherwise the WOPC could be paid by neither bank: OCBC can't reach the payee, and
Airwallex's list hides WOPCs planned for OCBC.
- FPS is switched off on the OCBC screen. It was never real: choosing FPS showed "Payment
Submitted" without sending anything (a leftover placeholder). It now can't be chosen, and the old code
path reports an error instead of a false success.
Firestore:
- The books are read for the new double-payment check.
- The audit log gets entries in its existing shape.
- WOPCs record transaction.bankAccountId at link, as approved in pt4.
- No structure change.
Still not true: - A payee at a non-OCBC bank can't be paid from OCBC. Airwallex pays them. FPS is out by the owner's rule; CHATS (HK$55 per payment by e-banking, per the fee guide) hasn't been asked for. - Not live-tested: the sandbox has no OCBC session or OneToken. - Not deployed. - Noticed, not changed: OCBC's general (non-WOPC) transfer screen still lets any signed-in user start a transfer. Only the OneToken approval stops it. The WOPC path now requires an appointed payer; whether the general screen should too is the owner's call.