This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/api/tabs/addOrder.cfm
John Mizerek 4c0479db5c Add Open Tabs feature: tab APIs, presence tracking, shared tabs, cron, portal settings
- New api/tabs/ directory with 13 endpoints: open, close, cancel, get, getActive,
  addOrder, increaseAuth, addMember, removeMember, getPresence, approveOrder,
  rejectOrder, pendingOrders
- New api/presence/heartbeat.cfm for beacon-based user presence tracking
- New cron/expireTabs.cfm for idle tab expiry and presence cleanup
- Modified submit.cfm for tab-aware order submission (skip payment, update running total)
- Modified getOrCreateCart.cfm to auto-detect active tab and set TabID on new carts
- Modified webhook.cfm to handle tab capture events (metadata type=tab_close)
- Modified businesses/get.cfm and updateTabs.cfm with new tab config columns
- Updated portal tab settings UI with auth amounts, max members, approval toggle
- Added tab and presence endpoints to Application.cfm public allowlist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:56:07 -08:00

147 lines
5.9 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<cfscript>
/**
* Add Order to Tab
* Links an order to the tab. Auto-approves for owner, pending for members.
*
* POST: { TabID: int, OrderID: int, UserID: int }
*/
try {
requestData = deserializeJSON(toString(getHttpRequestData().content));
tabID = val(requestData.TabID ?: 0);
orderID = val(requestData.OrderID ?: 0);
userID = val(requestData.UserID ?: 0);
if (tabID == 0) apiAbort({ "OK": false, "ERROR": "missing_TabID" });
if (orderID == 0) apiAbort({ "OK": false, "ERROR": "missing_OrderID" });
if (userID == 0) apiAbort({ "OK": false, "ERROR": "missing_UserID" });
// Get tab
qTab = queryTimed("
SELECT t.ID, t.StatusID, t.BusinessID, t.OwnerUserID, t.AuthAmountCents, t.RunningTotalCents,
b.TabApprovalRequired, b.TabAutoIncreaseThreshold
FROM Tabs t
JOIN Businesses b ON b.ID = t.BusinessID
WHERE t.ID = :tabID LIMIT 1
", { tabID: { value: tabID, cfsqltype: "cf_sql_integer" } });
if (qTab.recordCount == 0) apiAbort({ "OK": false, "ERROR": "tab_not_found" });
if (qTab.StatusID != 1) apiAbort({ "OK": false, "ERROR": "tab_not_open" });
// Verify user is a member
qMember = queryTimed("
SELECT RoleID FROM TabMembers
WHERE TabID = :tabID AND UserID = :userID AND StatusID = 1 LIMIT 1
", {
tabID: { value: tabID, cfsqltype: "cf_sql_integer" },
userID: { value: userID, cfsqltype: "cf_sql_integer" }
});
if (qMember.recordCount == 0) apiAbort({ "OK": false, "ERROR": "not_a_member" });
isOwner = qMember.RoleID == 1;
// Verify order belongs to same business and is in cart state
qOrder = queryTimed("
SELECT ID, BusinessID, StatusID, UserID FROM Orders
WHERE ID = :orderID LIMIT 1
", { orderID: { value: orderID, cfsqltype: "cf_sql_integer" } });
if (qOrder.recordCount == 0) apiAbort({ "OK": false, "ERROR": "order_not_found" });
if (qOrder.BusinessID != qTab.BusinessID) apiAbort({ "OK": false, "ERROR": "wrong_business" });
if (qOrder.StatusID != 0) apiAbort({ "OK": false, "ERROR": "order_not_in_cart", "MESSAGE": "Order must be in cart state." });
// Calculate order subtotal + tax
qTotals = queryTimed("
SELECT COALESCE(SUM(oli.Price * oli.Quantity), 0) AS Subtotal
FROM OrderLineItems oli
WHERE oli.OrderID = :orderID AND oli.IsDeleted = 0
", { orderID: { value: orderID, cfsqltype: "cf_sql_integer" } });
subtotal = val(qTotals.Subtotal);
subtotalCents = round(subtotal * 100);
// Get tax rate from business
qBizTax = queryTimed("SELECT TaxRate FROM Businesses WHERE ID = :bizID", {
bizID: { value: qTab.BusinessID, cfsqltype: "cf_sql_integer" }
});
taxRate = val(qBizTax.TaxRate);
taxCents = round(subtotalCents * taxRate);
// Determine approval status
approvalStatus = "approved";
if (!isOwner && qTab.TabApprovalRequired == 1) {
approvalStatus = "pending";
}
// Check if adding this would exceed authorization (only for auto-approved orders)
newRunning = qTab.RunningTotalCents;
if (approvalStatus == "approved") {
newRunning = qTab.RunningTotalCents + subtotalCents + taxCents;
if (newRunning > qTab.AuthAmountCents) {
apiAbort({
"OK": false, "ERROR": "exceeds_authorization",
"MESSAGE": "This order would exceed your tab authorization. Please increase your authorization first.",
"RUNNING_TOTAL_CENTS": qTab.RunningTotalCents,
"ORDER_CENTS": subtotalCents + taxCents,
"AUTH_AMOUNT_CENTS": qTab.AuthAmountCents
});
}
}
// Link order to tab
queryTimed("UPDATE Orders SET TabID = :tabID WHERE ID = :orderID", {
tabID: { value: tabID, cfsqltype: "cf_sql_integer" },
orderID: { value: orderID, cfsqltype: "cf_sql_integer" }
});
// Insert into TabOrders
queryTimed("
INSERT INTO TabOrders (TabID, OrderID, UserID, ApprovalStatus, SubtotalCents, TaxCents, AddedOn)
VALUES (:tabID, :orderID, :userID, :status, :subtotalCents, :taxCents, NOW())
ON DUPLICATE KEY UPDATE ApprovalStatus = VALUES(ApprovalStatus), SubtotalCents = VALUES(SubtotalCents), TaxCents = VALUES(TaxCents)
", {
tabID: { value: tabID, cfsqltype: "cf_sql_integer" },
orderID: { value: orderID, cfsqltype: "cf_sql_integer" },
userID: { value: userID, cfsqltype: "cf_sql_integer" },
status: { value: approvalStatus, cfsqltype: "cf_sql_varchar" },
subtotalCents: { value: subtotalCents, cfsqltype: "cf_sql_integer" },
taxCents: { value: taxCents, cfsqltype: "cf_sql_integer" }
});
// If auto-approved, update running total
if (approvalStatus == "approved") {
queryTimed("
UPDATE Tabs SET RunningTotalCents = :newRunning, LastActivityOn = NOW()
WHERE ID = :tabID
", {
newRunning: { value: newRunning, cfsqltype: "cf_sql_integer" },
tabID: { value: tabID, cfsqltype: "cf_sql_integer" }
});
}
// Check if auto-increase threshold reached
needsIncrease = false;
threshold = val(qTab.TabAutoIncreaseThreshold);
if (threshold > 0 && qTab.AuthAmountCents > 0) {
needsIncrease = (newRunning / qTab.AuthAmountCents) >= threshold;
}
apiAbort({
"OK": true,
"APPROVAL_STATUS": approvalStatus,
"RUNNING_TOTAL_CENTS": newRunning,
"AUTH_REMAINING_CENTS": qTab.AuthAmountCents - newRunning,
"NEEDS_INCREASE": needsIncrease,
"ORDER_SUBTOTAL_CENTS": subtotalCents,
"ORDER_TAX_CENTS": taxCents
});
} catch (any e) {
apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message });
}
</cfscript>