From d0f0f86176dc24b8e7bab23cf4cd493581ef6a6c Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Tue, 17 Feb 2026 19:01:17 -0800 Subject: [PATCH] Reuse existing PaymentIntent instead of blocking on retry When user abandons checkout and retries, retrieve the existing PaymentIntent from Stripe. If still usable (requires_payment_method, requires_confirmation, requires_action), return its client_secret. If already succeeded, block with clear error. If terminal/canceled, clear and create new one. Co-Authored-By: Claude Opus 4.5 --- api/stripe/createPaymentIntent.cfm | 45 ++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/api/stripe/createPaymentIntent.cfm b/api/stripe/createPaymentIntent.cfm index 42f6322..3be2d44 100644 --- a/api/stripe/createPaymentIntent.cfm +++ b/api/stripe/createPaymentIntent.cfm @@ -83,14 +83,43 @@ try { WHERE o.ID = :orderID ", { orderID: orderID }, { datasource: "payfrit" }); - // Check if order already has a PaymentIntent (idempotency check) - if (qOrder.recordCount > 0 && len(trim(qOrder.StripePaymentIntentID ?: "")) > 0) { - response["OK"] = false; - response["ERROR"] = "existing_payment_intent"; - response["MESSAGE"] = "Order already has a PaymentIntent. Use existing checkout session or retry payment."; - response["PAYMENT_INTENT_ID"] = qOrder.StripePaymentIntentID; - writeOutput(serializeJSON(response)); - abort; + // Check if order already has a PaymentIntent - retrieve and reuse if still valid + existingPiId = qOrder.StripePaymentIntentID ?: ""; + if (qOrder.recordCount > 0 && len(trim(existingPiId)) > 0) { + // Retrieve existing PaymentIntent from Stripe + piRetrieve = new http(); + piRetrieve.setMethod("GET"); + piRetrieve.setUrl("https://api.stripe.com/v1/payment_intents/#existingPiId#"); + piRetrieve.setUsername(stripeSecretKey); + piRetrieve.setPassword(""); + piResult = piRetrieve.send().getPrefix(); + existingPi = deserializeJSON(piResult.fileContent); + + if (!structKeyExists(existingPi, "error")) { + piStatus = existingPi.status ?: ""; + // Reusable states: can still complete payment + if (listFindNoCase("requires_payment_method,requires_confirmation,requires_action", piStatus)) { + // Return existing PaymentIntent - user can retry with same one + response["OK"] = true; + response["CLIENT_SECRET"] = existingPi.client_secret; + response["PAYMENT_INTENT_ID"] = existingPi.id; + response["PUBLISHABLE_KEY"] = application.stripePublishableKey ?: "pk_test_sPBNzSyJ9HcEPJGC7dSo8NqN"; + response["REUSED"] = true; + writeOutput(serializeJSON(response)); + abort; + } else if (piStatus == "succeeded") { + // Already paid - don't create another + response["OK"] = false; + response["ERROR"] = "already_paid"; + response["MESSAGE"] = "This order has already been paid."; + writeOutput(serializeJSON(response)); + abort; + } + // Other terminal states (canceled, etc.) - clear and create new + } + // PaymentIntent not found or terminal - clear it from order + queryExecute("UPDATE Orders SET StripePaymentIntentID = NULL WHERE ID = :orderID", + { orderID: orderID }, { datasource: "payfrit" }); } deliveryFee = 0;