Timeline of Changes
1. Original state (pre-Feb 26, 2026)
payment_intent.succeeded had NO projectInvoiceId check It simply forwarded ALL payment_intent.succeeded events to NEW_ORDER_URL (catalog new order) This worked because checkout sessions for invoice payments didn’t put projectInvoiceId in PaymentIntent metadata — only in the checkout session metadata 2. ec214f0 — Feb 26, 2026 — “enhance Stripe checkout with autoPay logging and metadata”
This is the breaking commit. It added projectInvoiceId into payment_intent_data.metadata on the checkout session creation: payment_intent_data: {
setup_future_usage: "off_session",
metadata: {
companyId,
projectInvoiceId, // ← NEW — now PaymentIntent carries this
from: "agencyhandy",
},
},
In the same commit, a new guard was added to payment_intent.succeeded: if (projectInvoiceId) {
// call handle-single-payment
return;
}
if (isInvoicePayment) {
// ignore subscription renewals
return;
}
// only catalog events reach NEW_ORDER_URL
Intent was correct: for checkout-session-based invoice payments, payment_intent.succeeded should call handle-single-payment as a safety net alongside checkout.session.completed 3. c98217f — Feb 27, 2026 (next day) — “enhance Stripe checkout for subscription invoices”
Realized that both checkout.session.completed AND payment_intent.succeeded were calling handle-single-payment, causing duplicate processing Fix: collapsed the two separate guards into one blanket skip: if (projectInvoiceId || isInvoicePayment) {
// skip entirely — "handled by checkout.session.completed / invoice events"
return;
}
This fixed the duplicate problem for checkout-session flows 4. The new direct PaymentIntent flow (current state)
Frontend now confirms PaymentIntent directly (no checkout session) The PaymentIntent still carries projectInvoiceId in metadata (from the ec214f0 change) Event sequence: charge.succeeded → payment_intent.succeeded checkout.session.completed never fires (no checkout session was created) But payment_intent.succeeded sees projectInvoiceId → hits the blanket skip → nothing processes the payment Result: invoice not updated, project not live, no notifications Summary for CTO/Team
Root Cause (one sentence)
The projectInvoiceId metadata was added to PaymentIntent for checkout-session tracing, and subsequently used as a blanket signal to skip payment_intent.succeeded processing — assuming checkout.session.completed always handles it. The new direct-PaymentIntent flow breaks that assumption because no checkout session exists.