From 623b94cb3dbfe9e176e6215e592e92a6641d3471 Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Wed, 25 Feb 2026 10:45:01 -0800 Subject: [PATCH] Fix expireTabs cron: verify Stripe response before updating DB The cron was marking tabs as 'captured' without checking if the Stripe capture actually succeeded. Also, it never loaded the Stripe config (api/config/stripe.cfm), so the API key was empty and all captures were silently failing. Now includes the Stripe config, checks every response status, and reverts the tab to open on capture failure. Co-Authored-By: Claude Opus 4.6 --- cron/expireTabs.cfm | 106 +++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 32 deletions(-) diff --git a/cron/expireTabs.cfm b/cron/expireTabs.cfm index 72469d9..b1f708d 100644 --- a/cron/expireTabs.cfm +++ b/cron/expireTabs.cfm @@ -2,6 +2,7 @@ + /** @@ -19,6 +20,12 @@ function apiAbort(required struct payload) { } try { + stripeSecretKey = application.stripeSecretKey ?: ""; + if (stripeSecretKey == "") { + writeLog(file="tab_cron", text="FATAL: stripeSecretKey not available. Aborting."); + apiAbort({ "OK": false, "ERROR": "no_stripe_key" }); + } + expiredCount = 0; capturedCount = 0; presenceCleaned = 0; @@ -47,18 +54,32 @@ try { if (qOrders.OrderCount == 0) { // No orders - cancel the tab and release hold - stripeSecretKey = application.stripeSecretKey ?: ""; httpCancel = new http(); httpCancel.setMethod("POST"); httpCancel.setUrl("https://api.stripe.com/v1/payment_intents/#tab.StripePaymentIntentID#/cancel"); httpCancel.setUsername(stripeSecretKey); httpCancel.setPassword(""); - httpCancel.send(); + cancelResult = httpCancel.send().getPrefix(); + cancelData = deserializeJSON(cancelResult.fileContent); - queryExecute(" - UPDATE Tabs SET StatusID = 5, ClosedOn = NOW(), PaymentStatus = 'cancelled' - WHERE ID = :tabID - ", { tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); + if (structKeyExists(cancelData, "status") && cancelData.status == "canceled") { + queryExecute(" + UPDATE Tabs SET StatusID = 5, ClosedOn = NOW(), PaymentStatus = 'cancelled' + WHERE ID = :tabID + ", { tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); + writeLog(file="tab_cron", text="Tab ##tab.ID## cancelled (no orders, hold released)."); + } else { + errMsg = structKeyExists(cancelData, "error") && structKeyExists(cancelData.error, "message") ? cancelData.error.message : "Cancel failed"; + queryExecute(" + UPDATE Tabs SET PaymentStatus = 'cancel_failed', PaymentError = :err + WHERE ID = :tabID + ", { + err: { value: errMsg, cfsqltype: "cf_sql_varchar" }, + tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } + }, { datasource: "payfrit" }); + writeLog(file="tab_cron", text="Tab ##tab.ID## cancel FAILED: #errMsg#"); + continue; + } } else { // Has orders - capture with 0% tip (auto-close) payfritFee = isNumeric(tab.PayfritFee) ? tab.PayfritFee : 0.05; @@ -68,37 +89,58 @@ try { totalBeforeCard = totalSubtotal + totalTax + platformFee; cardFeeCents = round((totalBeforeCard + 30) / (1 - 0.029)) - totalBeforeCard; captureCents = totalBeforeCard + cardFeeCents; - appFeeCents = round(platformFee * 2); - if (captureCents > 0) { - stripeSecretKey = application.stripeSecretKey ?: ""; - httpCapture = new http(); - httpCapture.setMethod("POST"); - httpCapture.setUrl("https://api.stripe.com/v1/payment_intents/#tab.StripePaymentIntentID#/capture"); - httpCapture.setUsername(stripeSecretKey); - httpCapture.setPassword(""); - httpCapture.addParam(type="formfield", name="amount_to_capture", value=captureCents); - httpCapture.addParam(type="formfield", name="application_fee_amount", value=appFeeCents); - httpCapture.send(); + // Cap at authorized amount + if (captureCents > tab.AuthAmountCents) { + captureCents = tab.AuthAmountCents; } - queryExecute(" - UPDATE Tabs - SET StatusID = 3, ClosedOn = NOW(), PaymentStatus = 'captured', - CapturedOn = NOW(), FinalCaptureCents = :captureCents, TipAmountCents = 0 - WHERE ID = :tabID - ", { - captureCents: { value: captureCents, cfsqltype: "cf_sql_integer" }, - tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } - }, { datasource: "payfrit" }); + // Mark as closing + queryExecute("UPDATE Tabs SET StatusID = 2 WHERE ID = :tabID", + { tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); - // Mark approved orders as paid - queryExecute(" - UPDATE Orders SET PaymentStatus = 'paid', PaymentCompletedOn = NOW() - WHERE ID IN (SELECT OrderID FROM TabOrders WHERE TabID = :tabID AND ApprovalStatus = 'approved') - ", { tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); + httpCapture = new http(); + httpCapture.setMethod("POST"); + httpCapture.setUrl("https://api.stripe.com/v1/payment_intents/#tab.StripePaymentIntentID#/capture"); + httpCapture.setUsername(stripeSecretKey); + httpCapture.setPassword(""); + httpCapture.addParam(type="formfield", name="amount_to_capture", value=captureCents); + httpCapture.addParam(type="formfield", name="metadata[type]", value="tab_auto_close"); + httpCapture.addParam(type="formfield", name="metadata[tab_id]", value=tab.ID); + captureResult = httpCapture.send().getPrefix(); + captureData = deserializeJSON(captureResult.fileContent); - capturedCount++; + if (structKeyExists(captureData, "status") && captureData.status == "succeeded") { + queryExecute(" + UPDATE Tabs + SET StatusID = 3, ClosedOn = NOW(), PaymentStatus = 'captured', + CapturedOn = NOW(), FinalCaptureCents = :captureCents, TipAmountCents = 0 + WHERE ID = :tabID + ", { + captureCents: { value: captureCents, cfsqltype: "cf_sql_integer" }, + tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } + }, { datasource: "payfrit" }); + + // Mark approved orders as paid + queryExecute(" + UPDATE Orders SET PaymentStatus = 'paid', PaymentCompletedOn = NOW() + WHERE ID IN (SELECT OrderID FROM TabOrders WHERE TabID = :tabID AND ApprovalStatus = 'approved') + ", { tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); + + capturedCount++; + writeLog(file="tab_cron", text="Tab ##tab.ID## auto-closed. Captured #captureCents# cents."); + } else { + errMsg = structKeyExists(captureData, "error") && structKeyExists(captureData.error, "message") ? captureData.error.message : "Capture failed"; + queryExecute(" + UPDATE Tabs SET StatusID = 1, PaymentStatus = 'capture_failed', PaymentError = :err + WHERE ID = :tabID + ", { + err: { value: errMsg, cfsqltype: "cf_sql_varchar" }, + tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } + }, { datasource: "payfrit" }); + writeLog(file="tab_cron", text="Tab ##tab.ID## capture FAILED: #errMsg#. Tab reverted to open."); + continue; + } } // Release all members