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/beacons/save.cfm
John Mizerek be29352b21 Migrate beacon APIs to shard-only system
- save.cfm: Auto-allocates shard+Major to business, creates ServicePoint with Minor
- list.cfm: Lists ServicePoints with BeaconMinor (removed legacy Beacons table)
- list_all.cfm: Returns shard UUIDs instead of legacy beacon UUIDs
- lookup.cfm: Removed legacy UUID lookup, shard-only resolution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-14 19:33:49 -08:00

194 lines
5.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">
<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;
}
function normStr(v) {
if (isNull(v)) return "";
return trim(toString(v));
}
data = readJsonBody();
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
apiAbort({ OK=false, ERROR="no_business_selected" });
}
businessID = request.BusinessID;
// Get business and check if it has a shard assigned
qBiz = queryExecute(
"SELECT ID, Name, BeaconShardID, BeaconMajor FROM Businesses WHERE ID = ? LIMIT 1",
[ { value=businessID, cfsqltype="cf_sql_integer" } ],
{ datasource="payfrit" }
);
if (qBiz.recordCount EQ 0) {
apiAbort({ OK=false, ERROR="invalid_business", MESSAGE="Business not found" });
}
// If business doesn't have a shard, allocate one
shardID = qBiz.BeaconShardID;
major = qBiz.BeaconMajor;
if (isNull(shardID) || val(shardID) EQ 0) {
// Find an unassigned shard
qFreeShard = queryExecute(
"SELECT bs.ID FROM BeaconShards bs
WHERE bs.IsActive = 1
AND bs.ID NOT IN (SELECT BeaconShardID FROM Businesses WHERE BeaconShardID IS NOT NULL)
ORDER BY bs.ID
LIMIT 1",
[],
{ datasource="payfrit" }
);
if (qFreeShard.recordCount EQ 0) {
apiAbort({ OK=false, ERROR="no_shards_available", MESSAGE="No beacon shards available" });
}
shardID = qFreeShard.ID;
// Find next available major for this shard (in case multiple businesses share a shard in future)
qMaxMajor = queryExecute(
"SELECT COALESCE(MAX(BeaconMajor), 0) AS MaxMajor FROM Businesses WHERE BeaconShardID = ?",
[ { value=shardID, cfsqltype="cf_sql_integer" } ],
{ datasource="payfrit" }
);
major = val(qMaxMajor.MaxMajor) + 1;
// Assign shard to business
queryExecute(
"UPDATE Businesses SET BeaconShardID = ?, BeaconMajor = ? WHERE ID = ?",
[
{ value=shardID, cfsqltype="cf_sql_integer" },
{ value=major, cfsqltype="cf_sql_smallint" },
{ value=businessID, cfsqltype="cf_sql_integer" }
],
{ datasource="payfrit" }
);
}
// Get the shard UUID
qShard = queryExecute(
"SELECT UUID FROM BeaconShards WHERE ID = ?",
[ { value=shardID, cfsqltype="cf_sql_integer" } ],
{ datasource="payfrit" }
);
shardUUID = qShard.UUID;
// Service point handling
spName = structKeyExists(data, "Name") ? normStr(data.Name) : "";
if (len(spName) EQ 0) {
apiAbort({ OK=false, ERROR="missing_name", MESSAGE="Service point name is required" });
}
servicePointID = 0;
if (structKeyExists(data, "ServicePointID") && isNumeric(data.ServicePointID) && int(data.ServicePointID) GT 0) {
servicePointID = int(data.ServicePointID);
}
// Check if we're updating an existing service point or creating new
if (servicePointID GT 0) {
// Update existing service point
qSP = queryExecute(
"SELECT ID, BeaconMinor FROM ServicePoints WHERE ID = ? AND BusinessID = ? LIMIT 1",
[
{ value=servicePointID, cfsqltype="cf_sql_integer" },
{ value=businessID, cfsqltype="cf_sql_integer" }
],
{ datasource="payfrit" }
);
if (qSP.recordCount EQ 0) {
apiAbort({ OK=false, ERROR="invalid_service_point", MESSAGE="Service point not found" });
}
minor = qSP.BeaconMinor;
// If no minor assigned yet, get next available
if (isNull(minor)) {
qMaxMinor = queryExecute(
"SELECT COALESCE(MAX(BeaconMinor), 0) AS MaxMinor FROM ServicePoints WHERE BusinessID = ?",
[ { value=businessID, cfsqltype="cf_sql_integer" } ],
{ datasource="payfrit" }
);
minor = val(qMaxMinor.MaxMinor) + 1;
}
// Update service point
queryExecute(
"UPDATE ServicePoints SET Name = ?, BeaconMinor = ?, IsActive = 1 WHERE ID = ?",
[
{ value=spName, cfsqltype="cf_sql_varchar" },
{ value=minor, cfsqltype="cf_sql_smallint" },
{ value=servicePointID, cfsqltype="cf_sql_integer" }
],
{ datasource="payfrit" }
);
} else {
// Create new service point with next available minor
qMaxMinor = queryExecute(
"SELECT COALESCE(MAX(BeaconMinor), 0) AS MaxMinor FROM ServicePoints WHERE BusinessID = ?",
[ { value=businessID, cfsqltype="cf_sql_integer" } ],
{ datasource="payfrit" }
);
minor = val(qMaxMinor.MaxMinor) + 1;
queryExecute(
"INSERT INTO ServicePoints (BusinessID, Name, TypeID, IsActive, BeaconMinor, SortOrder)
VALUES (?, ?, 1, 1, ?, ?)",
[
{ value=businessID, cfsqltype="cf_sql_integer" },
{ value=spName, cfsqltype="cf_sql_varchar" },
{ value=minor, cfsqltype="cf_sql_smallint" },
{ value=minor, cfsqltype="cf_sql_integer" }
],
{ datasource="payfrit" }
);
qNewSP = queryExecute("SELECT LAST_INSERT_ID() AS ID", [], { datasource="payfrit" });
servicePointID = qNewSP.ID;
}
// Return the beacon configuration for programming
writeOutput(serializeJSON({
OK = true,
ERROR = "",
ServicePointID = servicePointID,
ServicePointName = spName,
BusinessID = businessID,
BusinessName = qBiz.Name,
ShardID = shardID,
UUID = shardUUID,
Major = major,
Minor = minor
}));
</cfscript>
<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>