- Add beacon-sharding API endpoints for scalable iBeacon addressing (64 shard UUIDs × 65k businesses = ~4.2M capacity) - Fix callServer.cfm to save UserID when creating Call Server tasks - Fix getDetails.cfm to return customer info from Task.UserID when Order.UserID is null (for tasks without orders) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
164 lines
5 KiB
Text
164 lines
5 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfsetting enablecfoutputonly="true">
|
|
|
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
|
<cfheader name="Cache-Control" value="no-store">
|
|
|
|
<!---
|
|
AllocateBusinessBeaconNamespace API
|
|
===================================
|
|
Allocates or retrieves the beacon namespace (shard UUID + major) for a business.
|
|
|
|
Request:
|
|
POST /api/beacon-sharding/allocate_business_namespace.cfm
|
|
{ "BusinessID": 123 }
|
|
|
|
Response:
|
|
{
|
|
"OK": true,
|
|
"BusinessID": 123,
|
|
"BeaconShardUUID": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
|
|
"BeaconMajor": 42,
|
|
"ShardID": 1
|
|
}
|
|
|
|
Logic:
|
|
1. If business already has namespace assigned, return it
|
|
2. Otherwise, find shard with lowest utilization
|
|
3. Assign next available Major within that shard
|
|
4. Increment shard's BusinessCount
|
|
--->
|
|
|
|
<cftry>
|
|
<cfscript>
|
|
function apiAbort(obj) {
|
|
writeOutput(serializeJSON(obj));
|
|
abort;
|
|
}
|
|
|
|
function readJsonBody() {
|
|
raw = toString(getHttpRequestData().content);
|
|
if (isNull(raw) || len(trim(raw)) EQ 0) return {};
|
|
try {
|
|
parsed = deserializeJSON(raw);
|
|
} catch(any e) {
|
|
apiAbort({ OK=false, ERROR="bad_json", MESSAGE="Invalid JSON body" });
|
|
}
|
|
if (!isStruct(parsed)) return {};
|
|
return parsed;
|
|
}
|
|
|
|
data = readJsonBody();
|
|
httpHeaders = getHttpRequestData().headers;
|
|
|
|
// Get BusinessID from: session > body > X-Business-ID header > URL
|
|
bizId = 0;
|
|
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID) && request.BusinessID GT 0) {
|
|
bizId = int(request.BusinessID);
|
|
}
|
|
if (bizId LTE 0 && structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID) && data.BusinessID GT 0) {
|
|
bizId = int(data.BusinessID);
|
|
}
|
|
if (bizId LTE 0 && structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"]) && httpHeaders["X-Business-ID"] GT 0) {
|
|
bizId = int(httpHeaders["X-Business-ID"]);
|
|
}
|
|
if (bizId LTE 0 && structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID) && url.BusinessID GT 0) {
|
|
bizId = int(url.BusinessID);
|
|
}
|
|
if (bizId LTE 0) {
|
|
apiAbort({ OK=false, ERROR="missing_business_id", MESSAGE="BusinessID is required" });
|
|
}
|
|
</cfscript>
|
|
|
|
<!--- Verify business exists --->
|
|
<cfquery name="qBiz" datasource="payfrit">
|
|
SELECT ID, BeaconShardID, BeaconMajor
|
|
FROM Businesses
|
|
WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
|
LIMIT 1
|
|
</cfquery>
|
|
|
|
<cfif qBiz.recordCount EQ 0>
|
|
<cfset apiAbort({ OK=false, ERROR="invalid_business", MESSAGE="Business not found" })>
|
|
</cfif>
|
|
|
|
<!--- If already allocated, return existing namespace --->
|
|
<cfif qBiz.BeaconShardID GT 0 AND NOT isNull(qBiz.BeaconMajor)>
|
|
<cfquery name="qShard" datasource="payfrit">
|
|
SELECT UUID FROM BeaconShards WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#qBiz.BeaconShardID#">
|
|
</cfquery>
|
|
<cfoutput>#serializeJSON({
|
|
OK = true,
|
|
BusinessID = bizId,
|
|
BeaconShardUUID = qShard.UUID,
|
|
BeaconMajor = qBiz.BeaconMajor,
|
|
ShardID = qBiz.BeaconShardID,
|
|
AlreadyAllocated = true
|
|
})#</cfoutput>
|
|
<cfabort>
|
|
</cfif>
|
|
|
|
<!--- Find shard with lowest utilization --->
|
|
<cfquery name="qShard" datasource="payfrit">
|
|
SELECT ID, UUID, BusinessCount
|
|
FROM BeaconShards
|
|
WHERE IsActive = 1
|
|
AND BusinessCount < MaxBusinesses
|
|
ORDER BY BusinessCount ASC
|
|
LIMIT 1
|
|
FOR UPDATE
|
|
</cfquery>
|
|
|
|
<cfif qShard.recordCount EQ 0>
|
|
<cfset apiAbort({ OK=false, ERROR="no_available_shards", MESSAGE="All beacon shards are at capacity" })>
|
|
</cfif>
|
|
|
|
<cfset shardId = qShard.ID>
|
|
<cfset shardUUID = qShard.UUID>
|
|
|
|
<!--- Find next available Major within this shard --->
|
|
<!--- Major values: 0-65535 (uint16), we start from 1 to avoid 0 --->
|
|
<cfquery name="qMaxMajor" datasource="payfrit">
|
|
SELECT COALESCE(MAX(BeaconMajor), 0) AS MaxMajor
|
|
FROM Businesses
|
|
WHERE BeaconShardID = <cfqueryparam cfsqltype="cf_sql_integer" value="#shardId#">
|
|
</cfquery>
|
|
|
|
<cfset nextMajor = qMaxMajor.MaxMajor + 1>
|
|
|
|
<!--- Sanity check --->
|
|
<cfif nextMajor GT 65535>
|
|
<cfset apiAbort({ OK=false, ERROR="shard_full", MESSAGE="Shard has reached maximum major value" })>
|
|
</cfif>
|
|
|
|
<!--- Assign namespace to business --->
|
|
<cfquery datasource="payfrit">
|
|
UPDATE Businesses
|
|
SET BeaconShardID = <cfqueryparam cfsqltype="cf_sql_integer" value="#shardId#">,
|
|
BeaconMajor = <cfqueryparam cfsqltype="cf_sql_smallint" value="#nextMajor#">
|
|
WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
|
AND (BeaconShardID IS NULL OR BeaconMajor IS NULL)
|
|
</cfquery>
|
|
|
|
<!--- Increment shard's business count --->
|
|
<cfquery datasource="payfrit">
|
|
UPDATE BeaconShards
|
|
SET BusinessCount = BusinessCount + 1
|
|
WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#shardId#">
|
|
</cfquery>
|
|
|
|
<cfoutput>#serializeJSON({
|
|
OK = true,
|
|
BusinessID = bizId,
|
|
BeaconShardUUID = shardUUID,
|
|
BeaconMajor = nextMajor,
|
|
ShardID = shardId,
|
|
AlreadyAllocated = false
|
|
})#</cfoutput>
|
|
|
|
<cfcatch type="any">
|
|
<cfheader statuscode="200" statustext="OK">
|
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
|
<cfoutput>#serializeJSON({ OK=false, ERROR="server_error", MESSAGE=cfcatch.message, DETAIL=cfcatch.detail })#</cfoutput>
|
|
</cfcatch>
|
|
</cftry>
|