- 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>
83 lines
3.2 KiB
Text
83 lines
3.2 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 Member to Tab
|
|
* Tab owner adds a user. Validates target user not already on a tab.
|
|
*
|
|
* POST: { TabID: int, OwnerUserID: int, TargetUserID: int }
|
|
*/
|
|
|
|
try {
|
|
requestData = deserializeJSON(toString(getHttpRequestData().content));
|
|
tabID = val(requestData.TabID ?: 0);
|
|
ownerUserID = val(requestData.OwnerUserID ?: 0);
|
|
targetUserID = val(requestData.TargetUserID ?: 0);
|
|
|
|
if (tabID == 0) apiAbort({ "OK": false, "ERROR": "missing_TabID" });
|
|
if (ownerUserID == 0) apiAbort({ "OK": false, "ERROR": "missing_OwnerUserID" });
|
|
if (targetUserID == 0) apiAbort({ "OK": false, "ERROR": "missing_TargetUserID" });
|
|
|
|
// Verify tab exists, is open, and requester is owner
|
|
qTab = queryTimed("
|
|
SELECT t.ID, t.OwnerUserID, t.StatusID, t.BusinessID, b.TabMaxMembers
|
|
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" });
|
|
if (qTab.OwnerUserID != ownerUserID) apiAbort({ "OK": false, "ERROR": "not_owner" });
|
|
|
|
// Check member limit
|
|
qCount = queryTimed("SELECT COUNT(*) AS Cnt FROM TabMembers WHERE TabID = :tabID AND StatusID = 1", {
|
|
tabID: { value: tabID, cfsqltype: "cf_sql_integer" }
|
|
});
|
|
if (qCount.Cnt >= val(qTab.TabMaxMembers)) {
|
|
apiAbort({ "OK": false, "ERROR": "max_members", "MESSAGE": "Tab has reached the maximum number of members." });
|
|
}
|
|
|
|
// Check target user not already on any tab
|
|
qExisting = queryTimed("
|
|
SELECT t.ID, b.Name AS BusinessName
|
|
FROM TabMembers tm JOIN Tabs t ON t.ID = tm.TabID JOIN Businesses b ON b.ID = t.BusinessID
|
|
WHERE tm.UserID = :uid AND tm.StatusID = 1 AND t.StatusID = 1 LIMIT 1
|
|
", { uid: { value: targetUserID, cfsqltype: "cf_sql_integer" } });
|
|
|
|
if (qExisting.recordCount > 0) {
|
|
apiAbort({ "OK": false, "ERROR": "user_already_on_tab", "MESSAGE": "This user is already on a tab." });
|
|
}
|
|
|
|
// Check target user exists
|
|
qTarget = queryTimed("SELECT FirstName, LastName FROM Users WHERE ID = :uid LIMIT 1", {
|
|
uid: { value: targetUserID, cfsqltype: "cf_sql_integer" }
|
|
});
|
|
if (qTarget.recordCount == 0) apiAbort({ "OK": false, "ERROR": "user_not_found" });
|
|
|
|
// Add member
|
|
queryTimed("
|
|
INSERT INTO TabMembers (TabID, UserID, RoleID, StatusID, JoinedOn)
|
|
VALUES (:tabID, :userID, 2, 1, NOW())
|
|
ON DUPLICATE KEY UPDATE StatusID = 1, LeftOn = NULL, JoinedOn = NOW()
|
|
", {
|
|
tabID: { value: tabID, cfsqltype: "cf_sql_integer" },
|
|
userID: { value: targetUserID, cfsqltype: "cf_sql_integer" }
|
|
});
|
|
|
|
apiAbort({
|
|
"OK": true,
|
|
"MEMBER": {
|
|
"UserID": targetUserID,
|
|
"FirstName": qTarget.FirstName,
|
|
"LastName": qTarget.LastName,
|
|
"RoleID": 2
|
|
}
|
|
});
|
|
|
|
} catch (any e) {
|
|
apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message });
|
|
}
|
|
</cfscript>
|