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:
parent
48fa6e4482
commit
623b94cb3d
1 changed files with 74 additions and 32 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Reference in a new issue