Stripe checkout audit · Webhook analysis · Recovery status · June 1, 2026
2
Transactions Audited
1
Data Bug Found
4
Checkout Issues
1
P0 Webhook Bug
🚨 P0 BUG FOUND — stripeWebhook.js (Line ~185)
Bug: resolvedPlanName is used inside the Meta CAPI call (line ~185) BEFORE it is declared with const resolvedPlanName = ... (line 194). In JavaScript strict mode (Deno), this is a Temporal Dead Zone violation. The plan_name field in every Meta CAPI Purchase event is receiving undefined.
Impact: All Meta CAPI Purchase events are missing the plan_name field. This reduces signal quality for Meta's conversion optimization and may affect ROAS and lookalike audience quality.
Fix: Move const resolvedPlanName = metadata.plan_name || productName; to BEFORE the metaCapi invocation call (line ~185).
All CheckoutAttempt records with status='started' audited. Orders table queried for payment_status='unpaid'. Both returned as above.
Root Cause
Malformed email address stored in CheckoutAttempt entity. Missing '@' character means: (1) No Stripe session was created (Stripe API would reject it). (2) The abandoned checkout recovery email system explicitly skips it. (3) No Order was ever created. The customer was likely blocked at checkout or entered a typo.
Fix Recommendation
1. Add server-side email validation in createCheckoutSessionTiered BEFORE calling Stripe. Return 400 if email is invalid. 2. Add client-side validation on all checkout forms to reject emails without '@' before submission. 3. This specific record should be marked 'abandoned' manually — no recovery email is possible without a valid address.
Recovery Email: CANNOT SEND
Cannot send — email address is invalid (missing '@'). Manual cleanup only.
Root Cause
Stripe marks sessions as 'incomplete' or 'open' when a customer opens the hosted checkout page but does not complete payment. These expire automatically after 24 hours. Without a corresponding CheckoutAttempt or Order record, this indicates the customer abandoned the Stripe-hosted page directly. This is standard expected behavior and does not represent an app bug.
Fix Recommendation
No app fix required. Ensure trackCheckoutAttempt fires BEFORE redirecting to Stripe checkout URL so the email is captured for recovery. Currently, if a customer is redirected to Stripe but the CheckoutAttempt record was not created first, no recovery email can be sent.
Recovery Email: CANNOT SEND
No email captured. If CheckoutAttempt had been created first (with email), the 3-email recovery sequence would have fired automatically within 1 hour.
TXN-001 — Specific Bug: Invalid Email in CheckoutAttempt
Stored value in CheckoutAttempt.email:
"soheila.quigpro.com"Missing the @ symbol — this is NOT a valid email address.
processAbandonedCheckout explicitly skips it:
if (!attempt.email || !attempt.email.includes('@') || !attempt.email.includes('.')) continue;Result: Recovery email NEVER sent. Record is stuck in 'started' forever.
Stripe was never called:
CheckoutAttempt.stripe_session_id = "" (empty)The checkout session was created in the DB but Stripe was never reached — likely a form submission race condition or the customer typed an invalid email and the frontend saved it before validation.
Active webhook endpoint
PASSstripeWebhook (canonical)
stripeWebhookCheckout returns 410 Gone — correctly deprecated.
Signature verification
PASSconstructEventAsync with STRIPE_WEBHOOK_SECRET
Using async Deno-compatible method — correct.
Idempotency guard
PASSChecks for existing Order by stripe_session_id before creating
Prevents duplicate orders on webhook retry.
checkout.session.completed handler
PASSAll package types handled
Routes: permit_ready, feasibility, quick_insight, lot_fit, builder_bundle, concept.
charge.failed handler
PASSSends payment decline email to customer
Routes customer to correct resume URL by package_type.
resolvedPlanName used before declaration
FAILBUG: resolvedPlanName referenced in metaCapi call BEFORE it is declared with const on line 194
Line ~185: metaCapi call uses resolvedPlanName. Line 194: const resolvedPlanName = ... This is a JavaScript temporal dead zone bug. The metaCapi call will receive 'undefined' for plan_name on every purchase.
Meta CAPI on purchase
WARNFires for all purchases with amount > 0
Due to the resolvedPlanName bug above, plan_name will always be undefined in the CAPI event. Fix: move const resolvedPlanName = ... declaration to BEFORE the metaCapi call.
Order.lead_id backfill
PASSAsync — non-blocking
Correctly fires after order creation without blocking Stripe response.
Email delivery after post-tasks
PASSawait Promise.all(emailTasks)
Email tasks run in correct sequence after post-processing.
Fix resolvedPlanName temporal dead zone bug in stripeWebhook.js
functions/stripeWebhook.js line ~185
Move const resolvedPlanName declaration BEFORE the metaCapi invocation. Every Meta CAPI Purchase event is currently missing plan_name.
Add server-side email validation before Stripe session creation
functions/createCheckoutSessionTiered.js
Add: if (!user_email || !user_email.includes('@')) return Response.json({ error: 'Valid email required' }, { status: 400 }). Prevents invalid email being saved to CheckoutAttempt.
Migrate legacy createCheckoutSession (concept plans) to createCheckoutSessionTiered
pages/PlanCheckout.jsx + functions/createCheckoutSession.js
createCheckoutSession accepts client-submitted conceptPrice with no server-side validation — price tampering risk on concept plan purchases.
Add server-side price validation to createServiceCheckout (Permit Package)
functions/createServiceCheckout.js
Add minimum price check: if (total_price < 4000 && package_slug.includes('permit_package')) reject. Prevents $1 Permit Package purchases.
Add price validation table to Builder Bundle checkout
functions/createCheckoutSession.js (builder_bundle path)
Define authoritative bundle prices server-side and validate bundlePrice against known SKUs.
Resolve Engineer Review package_type inconsistency
functions/createFeasibilityCheckout.js vs createCheckoutSessionTiered.js
createFeasibilityCheckout uses package_type='feasibility' but stripeWebhook checks isEngineerReview = packageType === 'lot_fit'. Some $349 ER purchases may not trigger generatePropertyVerificationReport.
Manually mark TXN-001 CheckoutAttempt as abandoned
Database — CheckoutAttempt entity
Set status='abandoned' on the record with email='soheila.quigpro.com'. It will never progress and recovery emails cannot be sent. Cleanup only.
TXN-001 (soheila.quigpro.com) — CANNOT SEND
Email address is malformed (no '@'). processAbandonedCheckout skips it. No recovery possible. Mark record as abandoned manually.
TXN-002 (Stripe-side open session) — NO EMAIL CAPTURED
Customer opened Stripe checkout and left. No CheckoutAttempt was created before redirect, so no email is available for recovery. Ensure trackCheckoutAttempt fires BEFORE Stripe redirect.
processAbandonedCheckout — OPERATIONAL ✅
System ran successfully (sent1=0 sent2=0 sent3=0). No eligible records to process. 3-email recovery sequence (1h / 24h / 48h) is correctly configured and will fire for future valid abandoned checkouts.
QuiPlans Stripe Investigation Report · June 1, 2026