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 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-02-25 10:45:01 -08:00
parent 48fa6e4482
commit 623b94cb3d

View file

@ -2,6 +2,7 @@
<cfset request._api_isPublic = true> <cfset request._api_isPublic = true>
<cfsetting enablecfoutputonly="true"> <cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true"> <cfcontent type="application/json; charset=utf-8" reset="true">
<cfinclude template="../api/config/stripe.cfm">
<cfscript> <cfscript>
/** /**
@ -19,6 +20,12 @@ function apiAbort(required struct payload) {
} }
try { 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; expiredCount = 0;
capturedCount = 0; capturedCount = 0;
presenceCleaned = 0; presenceCleaned = 0;
@ -47,18 +54,32 @@ try {
if (qOrders.OrderCount == 0) { if (qOrders.OrderCount == 0) {
// No orders - cancel the tab and release hold // No orders - cancel the tab and release hold
stripeSecretKey = application.stripeSecretKey ?: "";
httpCancel = new http(); httpCancel = new http();
httpCancel.setMethod("POST"); httpCancel.setMethod("POST");
httpCancel.setUrl("https://api.stripe.com/v1/payment_intents/#tab.StripePaymentIntentID#/cancel"); httpCancel.setUrl("https://api.stripe.com/v1/payment_intents/#tab.StripePaymentIntentID#/cancel");
httpCancel.setUsername(stripeSecretKey); httpCancel.setUsername(stripeSecretKey);
httpCancel.setPassword(""); httpCancel.setPassword("");
httpCancel.send(); cancelResult = httpCancel.send().getPrefix();
cancelData = deserializeJSON(cancelResult.fileContent);
queryExecute(" if (structKeyExists(cancelData, "status") && cancelData.status == "canceled") {
UPDATE Tabs SET StatusID = 5, ClosedOn = NOW(), PaymentStatus = 'cancelled' queryExecute("
WHERE ID = :tabID UPDATE Tabs SET StatusID = 5, ClosedOn = NOW(), PaymentStatus = 'cancelled'
", { tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); 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 { } else {
// Has orders - capture with 0% tip (auto-close) // Has orders - capture with 0% tip (auto-close)
payfritFee = isNumeric(tab.PayfritFee) ? tab.PayfritFee : 0.05; payfritFee = isNumeric(tab.PayfritFee) ? tab.PayfritFee : 0.05;
@ -68,37 +89,58 @@ try {
totalBeforeCard = totalSubtotal + totalTax + platformFee; totalBeforeCard = totalSubtotal + totalTax + platformFee;
cardFeeCents = round((totalBeforeCard + 30) / (1 - 0.029)) - totalBeforeCard; cardFeeCents = round((totalBeforeCard + 30) / (1 - 0.029)) - totalBeforeCard;
captureCents = totalBeforeCard + cardFeeCents; captureCents = totalBeforeCard + cardFeeCents;
appFeeCents = round(platformFee * 2);
if (captureCents > 0) { // Cap at authorized amount
stripeSecretKey = application.stripeSecretKey ?: ""; if (captureCents > tab.AuthAmountCents) {
httpCapture = new http(); captureCents = tab.AuthAmountCents;
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();
} }
queryExecute(" // Mark as closing
UPDATE Tabs queryExecute("UPDATE Tabs SET StatusID = 2 WHERE ID = :tabID",
SET StatusID = 3, ClosedOn = NOW(), PaymentStatus = 'captured', { tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
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 httpCapture = new http();
queryExecute(" httpCapture.setMethod("POST");
UPDATE Orders SET PaymentStatus = 'paid', PaymentCompletedOn = NOW() httpCapture.setUrl("https://api.stripe.com/v1/payment_intents/#tab.StripePaymentIntentID#/capture");
WHERE ID IN (SELECT OrderID FROM TabOrders WHERE TabID = :tabID AND ApprovalStatus = 'approved') httpCapture.setUsername(stripeSecretKey);
", { tabID: { value: tab.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); 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 // Release all members