Skip to content

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)

  1. 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)"
  2. 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?"
  3. 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?"
  4. 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 stored request_id).
  • If the owner's Airwallex-side approval workflow is active, a released transfer may STILL land IN_APPROVAL in 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 at components/accounting/PaymentConfirmationForm.tsx:1188) as a hardcoded step wizard modeled on WorkflowWizard.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 getTransfers client 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/webhook receiver to keep statuses live (needs AIRWALLEX_WEBHOOK_SECRET configured β€” carried follow-up from T-024).

Open questions for the owner

  1. P1 Step 2: include a "From Coaching Sessions" source (parity with the 5050 flow) or keep Invoices + Manual only?
  2. Default for the auto-release checkbox is OFF (manual) β€” confirm.
  3. 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, #907 129d0c16) and a clean-room gate (npm ci + tsc --incremental false + local next build type 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 old de52788c IN_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-any render params in AirwallexTransfersPanel (P3). The real reason the local gate kept passing while Vercel failed: the sandbox's node_modules predated two weeks of lockfile changes (no npm ci after syncing main), so the local TypeScript/antd versions inferred differently from Vercel's clean-room install. Fixed: all 8 table render params explicitly typed; deps re-synced (npm ci); gate re-run cache-free on fresh deps + a full local next build type-phase check before re-deploying. Gate lesson (supersedes pt5's cache-only theory): npm ci against 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: AirwallexTransfersPanel on the Bank Access Airwallex section β€” live transfer list (GET /api/airwallex/transfers via the T-024 getTransfers, status chips from AIRWALLEX_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 mirrors payout.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 local main while the pushed branch still held the P2 state; caught by verifying the merged tree), a5e69808 (#904 β€” the real P3, recovered via reflog 75208266), 98cfaffc (#905 SHA-list correction), + the deploy hotfix commit (#906): the first deploy of this work FAILED on a TS2448 in the P2 drawer (signatureHold referenced in confirmAndSend'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_SECRET env + 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 stored request_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; requires wopc_reference + a SAVED beneficiary_id) Β· create-pending returns signatureRequired Β· prefill carries wopcSignatureRequired β†’ AirwallexTransferDrawer queues instead of sending (review alert + "Confirm & queue (sends after signature)" + a dedicated queued result screen) Β· release hook appended to recordSignature (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: PaymentConfirmationForm gains a hardcoded stepped mode (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's LineItemsCoachingModule (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