Objective
- Restore a working payment path for orders where a client has cancelled Pay at Close (PAC) — currently neither the client portal nor staff-facing manual payment works after PAC is cancelled.
- Eliminate the manual escalation to Guardian as the only resolution for affected orders.
- Fix the
ManualPaymentModalcrash that prevents staff from processing payment as a fallback.
Background
- When a client cancels PAC, the FlexFund webhook fires but hits a blocked state transition in
src/routes/webhooks/flexfund/flexfundWebhook.ts— the payment record is already in a state the handler cannot transition out of, leaving the order stuck. - After PAC cancellation, the backend attempts to look up a Finix subscription that no longer exists (voided on Finix's side), returning a
NOT_FOUNDerror. The order is left with no valid payment instrument attached. - When staff attempt to process payment manually as a workaround,
ManualPaymentModal.tsxcrashes at line 51 withTypeError: Cannot read properties of undefined (reading 'toFixed')— a payment amount value is returning undefined. This eliminates the only available fallback. - The FlexFund webhook error has fired 72 times since June 8, suggesting this is not an isolated incident.
- Known affected order: 4335 Maple Valley Dr, Cumming, GA 30040 — escalated to Guardian with their number provided to the client.
Product Decisions
Locked
(None yet — pending repro and engineering investigation)
Open
- Post-cancellation payment method — When PAC is cancelled, should the order fall back to Guardian (CardPointe) as the payment method automatically, or should staff be prompted to select a new payment method?
- Client portal behavior — Should the client portal surface a new payment option immediately after PAC cancellation, or only after staff take an action on the order?
Scope
Backend
src/routes/webhooks/flexfund/flexfundWebhook.ts— blocked state transition on PAC cancellation webhook; state machine does not handle transition out of current payment status.src/routes/webhooks/webhook.ts— webhook routing entry point; review how FlexFund cancellation events are dispatched.- Finix subscription lookup — no file path found in codebase search; investigate where the
GET /subscriptions/:idcall originates after PAC cancellation.
Frontend
src/app/tools/inspections/[id]/components/ManualPaymentModal.tsx— crashes at line 51 (toFixedcalled on undefined); payment amount is not being guarded before render.src/app/tools/inspections/[id]/components/ServicesPayments.tsx— likely renders the payment section and passes amount props toManualPaymentModal; investigate what value is passed when PAC has been cancelled.src/app/client/job/[slug]/pay-beta/PaymentBetaClient.tsx— client-facing payment UI; investigate whether PAC cancellation leaves this in a broken state with no payment method available.src/app/tools/inspections/[id]/components/EditChargesModal.tsx— surfaced in PAC-related search; review for related state handling.
References
- PostHog — FlexFund webhook blocked state transition (72 occurrences)
- PostHog — Finix subscription not found (14 occurrences)
- PostHog — ManualPaymentModal TypeError (6 occurrences)
- Related: ATT-1658 — ACH payment shows paid before funds received (related payment state transition issue)
Suspected Repro Steps
(unconfirmed — validate before assigning)
- Create or locate an inspection order with Pay at Close (PAC) selected as the payment method
- After PAC is selected, cancel the PAC option on the order
- Attempt to pay through the client portal — expected: payment option available; actual: payment fails or is unavailable
- As a staff member, open the order in Attik and attempt to manually process payment via the Manual Payment modal — expected: modal loads with amount; actual: modal crashes
What to watch for during repro:
- Does the FlexFund webhook fire on PAC cancellation? Check backend logs around
flexfundWebhook.ts - Does the order retain a valid payment method after PAC is cancelled, or is it left with a detached/null instrument?
- Does the Manual Payment modal crash immediately on open, or only after interacting with it?