uid: T-167 title: Pay-with-WOPC v2 β stepped form, sign-triggered payment release, and an Airwallex-style transfers surface status: done area: bank-access created: 2026-07-16 updated: 2026-07-16 owner: girafeev1 assignee: Bank Access (Diagnosticcs) related: T-024, I-033
T-167 β Pay-with-WOPC v2 + Airwallex transfers surface¶
Why (owner request, 2026-07-16 β four parts, verbatim excerpts)¶
- Form alignment: "There're currently 3 tabs 'Contractor Information', 'From Invoices' and 'Manual Entry'⦠the workflow now paginate and forces the user to enter Contractor information first THEN allow the user to either use information 'From Invoices'/'From Coaching Sessions' or 'Manual Entry'. I guess the Pay with WOPC pipeline didn't get updated in this sense⦠Can you read into the '5050 Subcontractor Fees' workflow and have the Pay with WOPC pipeline aligned? (I'm thinking that it should be hardcoded unlike the workflow that seems to facilitate high degree of customization)"
- Sign-triggered payment: "Is it actually possible that the transaction gets approved from the web app after the WOPC is signed, further extending the current Pay with WOPC pipeline and integrate with Airwallex this way?"
- Per-payment release policy: "can you create a check box for auto approve payment after WOPC is signed vs. manually approve payment EVEN AFTER the WOPC is signed?"
- Airwallex-style portal: "Can you study the Airwallex web UI to further improve our current Airwallex web app portal so that it looks and functions like the Airwallex page? β¦ wish that our web app is able to approve transactions by being able to view transactions that are awaiting approvals, but I'm trying to see if we could push further"
Hard constraint (researched 2026-07-16, official Airwallex docs)¶
Airwallex exposes NO API to approve/reject/recall a transfer in IN_APPROVAL β its approval
workflow is web-app-only by design ("approved by a user in the web app"; workflows configured at
Transfers β Transfer approvals). Only webhooks exist for the transitions
(payout.transfer.in_approval / .scheduled / .approval_rejected / .approval_recalled /
.approval_blocked). Source: airwallex.com/docs β Payouts β Manage approvals for transfers.
Design consequence β flip the trigger. We cannot approve an already-created transfer via API, so the
app must hold the payment and defer transfers/create until the release trigger fires:
- Pay-with-WOPC no longer creates the Airwallex transfer at drawer-confirm time when a signature is
required. Instead it persists a queued payment (all transfer params + a minted idempotency
request_id+ release policy) alongside the WOPC + signing request. - Release trigger = WOPC signature recorded (auto mode) or an explicit "Release payment" click
(manual mode). Release calls the existing T-024
createTransfer(idempotent via the storedrequest_id). - If the owner's Airwallex-side approval workflow is active, a released transfer may STILL land
IN_APPROVALin Airwallex β that last gate is theirs and cannot be bypassed via API; the owner can relax it in the Airwallex portal (Transfers β Transfer approvals) so releases execute directly. Surface this honestly in the UI (status chip + deep link to Airwallex's approvals page).
Safety model (extends T-024's confirm rule β owner-directed)¶
T-024's hard rule was "the send must be an explicit user confirm step, never automatic." The owner is
explicitly extending it (request #3): with the opt-in checkbox the human authorizations become
(a) the creator ticking "release automatically once signed" at creation (seeing the exact amounts)
and (b) the director signing the WOPC document that carries those exact amounts. Manual mode
(checkbox off, the default) keeps a third gate: a confirm-gated "Release payment" click even after
signing. Server-side: auto-release runs from the signature-recording path against the stored queued
payment only (never a raw amount from a request), reusing the stored request_id.
Plan (phases; each merges via GitHub PR after the heavy-tsc gate)¶
- P1 β Stepped Pay-with-WOPC form (#1). Rebuild
PaymentConfirmationForm(wopc mode; flat 3-tab layout atcomponents/accounting/PaymentConfirmationForm.tsx:1188) as a hardcoded step wizard modeled onWorkflowWizard.tsx's UX (antd Steps, Next/Back, per-step validation, no template engine): Step 1 Contractor Information (mandatory, validated) β Step 2 Line items with the source choice (From Invoices / Manual Entry β coaching-sessions source optional, owner to confirm) β Step 3 review + the release-policy checkbox (P2). Scoped to wopc/counterparty mode; the transaction-matching mode keeps its current behavior. - P2 β Payment release queue (#2+#3). Queued-payment record (Firestore, tebs-erl; carries transfer
params, request_id, releasePolicy, wopcRef, creator, timestamps + audit log). Checkbox in the modal
(default OFF). Hook on WOPC
recordSignature: auto β release (create transfer, notify result incl. IN_APPROVAL honesty); manual β notification + "Pending payments" queue with confirm-gated Release. Failure/expiry handling (insufficient balance, API error β queue shows failed + retry keeps request_id). - P3 β Transfers surface (#4). A "Transfers" panel on the Airwallex page: live list via the T-024
getTransfersclient method (unused by UI today) β status chips (Awaiting approval / Processing / Paid / Failed), the pending-payments queue, deep link to Airwallex's approvals page for IN_APPROVAL rows. Study Airwallex's public UI patterns (docs screenshots + their Embedded Transfer component, which may allow embedding real Airwallex UI β evaluate as a deeper integration). Webhook wiring (payout.transfer.*) into the existing verified/api/airwallex/webhookreceiver to keep statuses live (needsAIRWALLEX_WEBHOOK_SECRETconfigured β carried follow-up from T-024).
Open questions for the owner¶
- P1 Step 2: include a "From Coaching Sessions" source (parity with the 5050 flow) or keep Invoices + Manual only?
- Default for the auto-release checkbox is OFF (manual) β confirm.
- Your Airwallex-side approval workflow: keep as an extra gate (released transfers pause IN_APPROVAL in Airwallex) or relax it there so releases execute immediately? (Setting lives in the Airwallex portal; not API-changeable.)
Log¶
- 2026-07-16 pt7 β Deploy attempt #3 SUCCEEDED β commanded deploy complete.
- β Attestation: read AGENTS.md. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
- After the pt5/pt6 hotfixes (#906
549f4da2, #907129d0c16) and a clean-room gate (npm ci+tsc --incremental false+ localnext buildtype phase), the third deploy of the owner's one-off command ("Proceed to P3 then deploy if no further steps needed") built green:dpl_GsbMRwAcmJaojmhyA8QLhVwwY84y, READY, target production, alias assigned (eop.theestablishers.com/pms.theestablishers.com), serving main @129d0c16. - Now live: T-167 P1 (stepped Pay-with-WOPC + "From Projects" + coaching source), P2 (signature-hold β auto-release queue), P3 (transfers surface), plus I-033 (card-merchant enrichment pagination fix) which had been merged but never deployed.
-
Deploy command consumed β no further deploys without a new explicit owner command (2026-07-05 guardrail). Owner-gated follow-ups unchanged (browser verification,
AIRWALLEX_WEBHOOK_SECRET, the oldde52788cIN_APPROVAL transfer in the Airwallex portal). -
2026-07-16 pt6 β Deploy attempt #2 also failed; root cause of BOTH gate misses found.
- β Attestation: read AGENTS.md. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
-
Second Vercel failure: implicit-
anyrender params inAirwallexTransfersPanel(P3). The real reason the local gate kept passing while Vercel failed: the sandbox'snode_modulespredated two weeks of lockfile changes (nonpm ciafter syncing main), so the local TypeScript/antd versions inferred differently from Vercel's clean-room install. Fixed: all 8 tablerenderparams explicitly typed; deps re-synced (npm ci); gate re-run cache-free on fresh deps + a full localnext buildtype-phase check before re-deploying. Gate lesson (supersedes pt5's cache-only theory):npm ciagainst the current lockfile BEFORE the type gate β a stale node_modules invalidates the gate. -
2026-07-16 pt4 β DONE: P3 shipped (transfers surface) β task complete at merge.
- β Attestation: read AGENTS.md. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
- P3 shipped:
AirwallexTransfersPanelon the Bank Access Airwallex section β live transfer list (GET /api/airwallex/transfers via the T-024getTransfers, status chips fromAIRWALLEX_TRANSFER_STATUS_META), the signature-held queue rows (GET /api/airwallex/transfers/queued), an attention banner + "Open Airwallex" deep link for IN_APPROVAL rows (no approve API exists), refresh on demand + auto-refresh after a send/queue, and the webhook now mirrorspayout.transfer.*/transfer.*statuses onto released queue entries. - Verdict (vs plan): all three phases landed β P1 stepped form + "From Projects" rename + coaching source; P2 signature-hold β auto-release (owner relaxed the Airwallex API-approval gate: "Gate is relaxed"); P3 transfers surface. Owner deploy command received ("Proceed to P3 then deploy if no further steps needed") β deploy fired once after this merge.
- Related commit SHAs (append-only, newest last):
df236bc6(#900 task open + design),1e60473c(#901 P1),3cba5356(#902 P2),59df587a(#903 β EMPTY merge, no content: the P3 work had been committed on the agent's localmainwhile the pushed branch still held the P2 state; caught by verifying the merged tree),a5e69808(#904 β the real P3, recovered via reflog75208266),98cfaffc(#905 SHA-list correction), + the deploy hotfix commit (#906): the first deploy of this work FAILED on a TS2448 in the P2 drawer (signatureHoldreferenced inconfirmAndSend's deps before its declaration) that a stale incremental-tsc cache let through the local gate β declaration hoisted; gate rule hardened in AGENTS.md (--incremental false). - Not yet configured (follow-ups, owner-gated):
AIRWALLEX_WEBHOOK_SECRETenv + registering the webhook URL in Airwallex (until then the webhook mirror is inert β the panel's Refresh covers it); browser verification of the three phases after deploy. -
Blast radius: Airwallex payout surface (drawer, payouts section, new panel + queue lib/routes), the Pay-with-WOPC form (stepped mode; flat mode untouched for matching callers), the 5050 workflow label ("From Projects"),
recordSignature(append-only best-effort hook), WOPC delete/void (append-only cancel hook). OCBC, accounting import, and the legacy Payments route untouched. -
2026-07-16 pt3 β P2 shipped: signature-hold β auto-release (no manual queue).
- β Attestation: read AGENTS.md. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
- Owner decisions (verbatim): "No need for 'when you click Release (default)' cos I don't want to integrate the web app to hold any pending transactions. Only hold for WOPC signature. proceed to P2 and adjust P3 accordingly" β so the WOPC signature is the ONLY hold; released payments send automatically; no manual-release mode, no pending-payments queue UI (P3 = transfers surface only). Approval-workflow research (owner-prompted): Airwallex workflows ARE optional per config β the setup asks "Require API-initiated transfers to go through the same approval workflow?" (Yes/No) + amount layers; there is NO per-transfer bypass, which confirms the hold-then-release design. Owner then flipped the setting: "Gate is relaxed" (2026-07-16) β API-created transfers now skip Airwallex approval, so released payments execute directly.
- What shipped:
lib/airwallex/paymentQueue.server.ts(queue store + CAS-claimed release engine, idempotent via the storedrequest_id; system-owner notifications on release/failed-release; cancel + list helpers) Β·POST /api/airwallex/transfers/queue(validation mirrors transfers/create: confirm gate, UUID request_id, enums; requireswopc_reference+ a SAVED beneficiary_id) Β·create-pendingreturnssignatureRequiredΒ· prefill carrieswopcSignatureRequiredβAirwallexTransferDrawerqueues instead of sending (review alert + "Confirm & queue (sends after signature)" + a dedicated queued result screen) Β· release hook appended torecordSignature(best-effort, never unwinds the signature) Β· WOPC delete/void cancel awaiting entries. - Safety chain for an auto-released payment: creator confirms exact amounts on the review screen β
counter-signing director signs the WOPC carrying those amounts β release fires with the pre-minted
idempotency key. Failures land as
release_failed+ an error notification (money never silently lost, never double-sent). -
Still open (P3): the Airwallex transfers surface (live status list incl. queue entries, Airwallex-style layout, webhook wiring).
-
2026-07-16 pt2 β Owner decisions received + P1 shipped.
- β Attestation: read AGENTS.md. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4
- Decisions (owner, verbatim): (1) "step 2 SHOULD also offer 'From Coaching Sessions. But instead of saying 'From Invoices', it should be 'From Projects' (for both the 5050 workflow and the Bank Access page as well)"; (2) "It would actually be wonderful if our Web App operates before the airwallex approval pipelineβ¦ a) if pay directly after WOPC is signed is selected, then pay directlyβ¦ But if all transactions need approval currently, then I'll relax the configuration manually on Airwallex, and forget about b) as well as it's just a nice to have" β so P2 = hold-then-release (auto-on-sign checkbox vs manual release, default manual), option (b) dropped, owner relaxes the Airwallex-side approval workflow themselves; (3) checkbox design follows from (2); (4) portal questions answered by (1)-(3).
- P1 shipped:
PaymentConfirmationFormgains a hardcodedsteppedmode (Steps header, step 0 = Contractor Information gated on contractor name + payee abbreviation, step 1 = line-item sources, Back/Next nav) β enabled on both Pay-with-WOPC surfaces (WOPCPaymentModal,CreateWOPCModal); flat-tab behavior unchanged for the transaction-matching callers. "From Coaching Sessions" added to the stepped form by reusing the 5050 wizard'sLineItemsCoachingModule(payout eligibility, null transaction β no amount cap). "From Invoices" β "From Projects" renamed everywhere β form tab, wizard + module-renderer (with a render-time normalization for saved templates still carrying the old label), template editor defaults, server seed, module-label map, and the template-editor empty-state text. -
Next: P2 (payment release queue + checkbox) then P3 (transfers surface).
-
2026-07-16 β Opened from the owner's four-part request. β Attestation: read AGENTS.md (incl. the 2026-07-05 deployment guardrail); board checked by scope (extends done T-024; no open task covers this). Researched + recorded the no-approval-API constraint. Proposed by Bank Access (Diagnosticcs); approach per the owner's request text above; P2 safety model awaiting explicit owner confirmation of the auto-release default. Source: Bank Access (Diagnosticcs) Β· https://claude.ai/code/session_01S4ztAsDZfR97qXjhPdwLn4