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/get.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

132 lines
4.7 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>
/**
* Get Tab Details
* Returns full tab info with orders and members.
*
* POST: { TabID: int, UserID: int }
*/
try {
requestData = deserializeJSON(toString(getHttpRequestData().content));
tabID = val(requestData.TabID ?: 0);
userID = val(requestData.UserID ?: 0);
if (tabID == 0) apiAbort({ "OK": false, "ERROR": "missing_TabID" });
if (userID == 0) apiAbort({ "OK": false, "ERROR": "missing_UserID" });
// Verify user is a member of this tab
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;
// Get tab
qTab = queryTimed("
SELECT t.*, b.Name AS BusinessName, b.PayfritFee, b.TaxRate,
sp.Name AS ServicePointName,
u.FirstName AS OwnerFirstName, u.LastName AS OwnerLastName
FROM Tabs t
JOIN Businesses b ON b.ID = t.BusinessID
LEFT JOIN ServicePoints sp ON sp.ID = t.ServicePointID
JOIN Users u ON u.ID = t.OwnerUserID
WHERE t.ID = :tabID LIMIT 1
", { tabID: { value: tabID, cfsqltype: "cf_sql_integer" } });
if (qTab.recordCount == 0) apiAbort({ "OK": false, "ERROR": "tab_not_found" });
// Get members
qMembers = queryTimed("
SELECT tm.ID, tm.UserID, tm.RoleID, tm.StatusID, tm.JoinedOn,
u.FirstName, u.LastName, u.ImageExtension
FROM TabMembers tm
JOIN Users u ON u.ID = tm.UserID
WHERE tm.TabID = :tabID AND tm.StatusID = 1
ORDER BY tm.RoleID, tm.JoinedOn
", { tabID: { value: tabID, cfsqltype: "cf_sql_integer" } });
members = [];
for (row in qMembers) {
arrayAppend(members, {
"UserID": row.UserID,
"FirstName": row.FirstName,
"LastName": row.LastName,
"RoleID": row.RoleID,
"IsOwner": row.RoleID == 1,
"ImageExtension": row.ImageExtension ?: "",
"JoinedOn": toISO8601(row.JoinedOn)
});
}
// Get orders on this tab
qOrders = queryTimed("
SELECT tbo.OrderID, tbo.UserID, tbo.ApprovalStatus, tbo.SubtotalCents, tbo.TaxCents, tbo.AddedOn,
o.StatusID AS OrderStatusID, o.UUID AS OrderUUID,
u.FirstName, u.LastName
FROM TabOrders tbo
JOIN Orders o ON o.ID = tbo.OrderID
JOIN Users u ON u.ID = tbo.UserID
WHERE tbo.TabID = :tabID
ORDER BY tbo.AddedOn DESC
", { tabID: { value: tabID, cfsqltype: "cf_sql_integer" } });
orders = [];
for (row in qOrders) {
// If member (not owner), only show own orders
if (!isOwner && row.UserID != userID) continue;
arrayAppend(orders, {
"OrderID": row.OrderID,
"OrderUUID": row.OrderUUID,
"UserID": row.UserID,
"UserName": "#row.FirstName# #row.LastName#",
"ApprovalStatus": row.ApprovalStatus,
"SubtotalCents": row.SubtotalCents,
"TaxCents": row.TaxCents,
"OrderStatusID": row.OrderStatusID,
"AddedOn": toISO8601(row.AddedOn)
});
}
apiAbort({
"OK": true,
"TAB": {
"ID": qTab.ID,
"UUID": qTab.UUID,
"BusinessID": qTab.BusinessID,
"BusinessName": qTab.BusinessName,
"OwnerUserID": qTab.OwnerUserID,
"OwnerName": "#qTab.OwnerFirstName# #qTab.OwnerLastName#",
"ServicePointID": val(qTab.ServicePointID),
"ServicePointName": qTab.ServicePointName ?: "",
"StatusID": qTab.StatusID,
"AuthAmountCents": qTab.AuthAmountCents,
"RunningTotalCents": qTab.RunningTotalCents,
"RemainingCents": qTab.AuthAmountCents - qTab.RunningTotalCents,
"TipAmountCents": val(qTab.TipAmountCents),
"FinalCaptureCents": val(qTab.FinalCaptureCents),
"PaymentStatus": qTab.PaymentStatus,
"OpenedOn": toISO8601(qTab.OpenedOn),
"ClosedOn": isDate(qTab.ClosedOn) ? toISO8601(qTab.ClosedOn) : "",
"IsOwner": isOwner,
"PayfritFee": val(qTab.PayfritFee),
"TaxRate": val(qTab.TaxRate)
},
"MEMBERS": members,
"ORDERS": orders
});
} catch (any e) {
apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message });
}
</cfscript>