checkpoint

This commit is contained in:
John Mizerek 2025-12-28 22:34:01 -08:00
parent 848544ba53
commit 363964d9c6
14 changed files with 1513 additions and 565 deletions

View file

@ -5,7 +5,7 @@
sessiontimeout="#CreateTimeSpan(0,0,30,0)#"
clientstorage="cookie">
<CFSET application.datasource = "payfrit">
<CFSET application.datasource = "payfrit_local">
<cfset application.businessMasterObj = new library.cfc.businessMaster(odbc = application.datasource) />
<cfset application.twilioObj = new library.cfc.twilio() />

View file

@ -2,53 +2,70 @@
<cfsetting enablecfoutputonly="true">
<!---
FILE: C:\lucee\tomcat\webapps\ROOT\biz.payfrit.com\api\Application.cfm
Payfrit API Application.cfm
MVP CHANGE:
- Public allowlist for:
/api/businesses/list.cfm
/api/servicepoints/list.cfm
FIX: Provide a DEFAULT datasource so endpoints can call queryExecute()
without specifying { datasource="payfrit" } every time.
IMPORTANT:
- Do NOT rely on exact SCRIPT_NAME equality; in some deployments it may include
the site folder prefix (e.g. /biz.payfrit.com/api/...).
- So we allowlist by "contains" match.
Token-auth gate for /api endpoints.
Public allowlist (NO auth):
- /api/login.cfm
- /api/logout.cfm
- /api/auth/login.cfm
- /api/auth/logout.cfm
- /api/businesses/list.cfm
- /api/servicepoints/list.cfm
Authenticated requests should send:
- Header: X-User-Token: <token from /api/auth/login.cfm>
- Header: X-Business-ID: <selected BusinessID> (required for most endpoints)
--->
<cfset request.IsApiRequest = true>
<cfinclude template="../Application.cfm">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<!--- OPTION A: default datasource for the whole app (THIS fixes "missing datasource") --->
<cfapplication
name="payfrit_api"
sessionmanagement="true"
clientmanagement="false"
setclientcookies="false"
datasource="payfrit"
>
<cfscript>
function apiAbort(obj) {
writeOutput(serializeJSON(obj));
function apiAbort(payload) {
writeOutput(serializeJSON(payload));
abort;
}
// Determine current request path
scriptName = "";
function headerValue(name) {
k = "HTTP_" & ucase(reReplace(arguments.name, "[^A-Za-z0-9]", "_", "all"));
if (structKeyExists(cgi, k)) return trim(cgi[k]);
return "";
}
// Determine request path
request._api_scriptName = "";
if (structKeyExists(cgi, "SCRIPT_NAME")) {
scriptName = cgi.SCRIPT_NAME;
request._api_scriptName = cgi.SCRIPT_NAME;
} else if (structKeyExists(cgi, "PATH_INFO")) {
scriptName = cgi.PATH_INFO;
request._api_scriptName = cgi.PATH_INFO;
}
request._api_path = lcase(request._api_scriptName);
// Public allowlist
request._api_isPublic = false;
if (len(request._api_path)) {
if (findNoCase("/api/login.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/logout.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/auth/login.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/auth/logout.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/businesses/list.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true;
}
// MVP allowlist: PUBLIC endpoint(s) under /api
isPublicEndpoint = false;
if (len(scriptName)) {
// Use contains (case-insensitive) so it works whether SCRIPT_NAME is:
// /api/servicepoints/list.cfm OR /biz.payfrit.com/api/servicepoints/list.cfm
if (findNoCase("/api/businesses/list.cfm", scriptName) GT 0) {
isPublicEndpoint = true;
}
if (findNoCase("/api/servicepoints/list.cfm", scriptName) GT 0) {
isPublicEndpoint = true;
}
}
// Copy auth from session if present
// Carry session values into request (if present)
if (!structKeyExists(request, "UserID") && structKeyExists(session, "UserID")) {
request.UserID = Duplicate(session.UserID);
}
@ -56,13 +73,39 @@ if (!structKeyExists(request, "BusinessID") && structKeyExists(session, "Busines
request.BusinessID = Duplicate(session.BusinessID);
}
// Enforce auth for all /api endpoints EXCEPT allowlisted public endpoints
if (!isPublicEndpoint) {
// Token auth: X-User-Token -> request.UserID
request._api_userToken = headerValue("X-User-Token");
if (len(request._api_userToken)) {
try {
request._api_qTok = queryExecute(
"SELECT UserID FROM UserTokens WHERE Token = ? LIMIT 1",
[ { value = request._api_userToken, cfsqltype = "cf_sql_varchar" } ],
{ datasource = "payfrit" }
);
if (request._api_qTok.recordCount EQ 1) {
request.UserID = request._api_qTok.UserID;
session.UserID = request._api_qTok.UserID;
}
} catch (any e) {
// ignore; treated as unauthenticated
}
}
// Business header: X-Business-ID -> request.BusinessID
request._api_hdrBiz = headerValue("X-Business-ID");
if (len(request._api_hdrBiz) && isNumeric(request._api_hdrBiz)) {
request.BusinessID = int(request._api_hdrBiz);
session.BusinessID = request.BusinessID;
}
// Enforce auth (except public)
if (!request._api_isPublic) {
if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
apiAbort({ OK=false, ERROR="not_logged_in" });
apiAbort({ "OK": false, "ERROR": "not_logged_in" });
}
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
apiAbort({ OK=false, ERROR="no_business_selected" });
apiAbort({ "OK": false, "ERROR": "no_business_selected" });
}
}
</cfscript>

View file

@ -1,50 +0,0 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<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();
if (!structKeyExists(request,"UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) apiAbort({OK=false,ERROR="not_logged_in"});
if (!structKeyExists(request,"BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) apiAbort({OK=false,ERROR="no_business_selected"});
if (!structKeyExists(data,"ServicePointID") || !isNumeric(data.ServicePointID) || int(data.ServicePointID) LTE 0) {
apiAbort({OK=false,ERROR="missing_servicepoint_id"});
}
spid = int(data.ServicePointID);
</cfscript>
<cfquery datasource="#application.datasource#">
UPDATE ServicePoints
SET IsActive = 0
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spid#">
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
</cfquery>
<cfquery name="qCheck" datasource="#application.datasource#">
SELECT ServicePointID
FROM ServicePoints
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spid#">
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
LIMIT 1
</cfquery>
<cfif qCheck.recordCount EQ 0>
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found" })#</cfoutput>
<cfabort>
</cfif>
<cfoutput>#serializeJSON({ OK=true, ERROR="", ServicePointID=spid })#</cfoutput>

View file

@ -1,67 +0,0 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<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();
if (!structKeyExists(request,"UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) apiAbort({OK=false,ERROR="not_logged_in"});
if (!structKeyExists(request,"BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) apiAbort({OK=false,ERROR="no_business_selected"});
if (!structKeyExists(data,"ServicePointID") || !isNumeric(data.ServicePointID) || int(data.ServicePointID) LTE 0) {
apiAbort({OK=false,ERROR="missing_servicepoint_id"});
}
spid = int(data.ServicePointID);
</cfscript>
<cfquery name="q" datasource="#application.datasource#">
SELECT
ServicePointID,
BusinessID,
ServicePointName,
ServicePointTypeID,
ServicePointCode,
Description,
SortOrder,
IsActive,
CreatedAt,
UpdatedAt
FROM ServicePoints
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spid#">
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
LIMIT 1
</cfquery>
<cfif q.recordCount EQ 0>
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found" })#</cfoutput>
<cfabort>
</cfif>
<cfset sp = {
"ServicePointID"=q.ServicePointID,
"BusinessID"=q.BusinessID,
"ServicePointName"=q.ServicePointName,
"ServicePointTypeID"=q.ServicePointTypeID,
"ServicePointCode"=q.ServicePointCode,
"Description"=q.Description,
"SortOrder"=q.SortOrder,
"IsActive"=q.IsActive,
"CreatedAt"=(q.CreatedAt & ""),
"UpdatedAt"=(q.UpdatedAt & "")
}>
<cfoutput>#serializeJSON({ OK=true, ERROR="", SERVICEPOINT=sp })#</cfoutput>

View file

@ -1,82 +0,0 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<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();
if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
apiAbort({ OK=false, ERROR="not_logged_in" });
}
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
apiAbort({ OK=false, ERROR="no_business_selected" });
}
onlyActive = true;
if (structKeyExists(data, "onlyActive")) {
if (isBoolean(data.onlyActive)) {
onlyActive = data.onlyActive;
} else if (isNumeric(data.onlyActive)) {
onlyActive = (int(data.onlyActive) EQ 1);
} else if (isSimpleValue(data.onlyActive)) {
onlyActive = (lcase(trim(toString(data.onlyActive))) EQ "true");
}
}
</cfscript>
<cfquery name="q" datasource="#application.datasource#">
SELECT
ServicePointID,
BusinessID,
ServicePointName,
ServicePointTypeID,
ServicePointCode,
Description,
SortOrder,
IsActive,
CreatedAt,
UpdatedAt
FROM ServicePoints
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
<cfif onlyActive>
AND IsActive = 1
</cfif>
ORDER BY SortOrder, ServicePointName, ServicePointID
</cfquery>
<cfset sps = []>
<cfloop query="q">
<cfset arrayAppend(sps, {
"ServicePointID"=q.ServicePointID,
"BusinessID"=q.BusinessID,
"ServicePointName"=q.ServicePointName,
"ServicePointTypeID"=q.ServicePointTypeID,
"ServicePointCode"=q.ServicePointCode,
"Description"=q.Description,
"SortOrder"=q.SortOrder,
"IsActive"=q.IsActive,
"CreatedAt"=(q.CreatedAt & ""),
"UpdatedAt"=(q.UpdatedAt & "")
})>
</cfloop>
<cfoutput>#serializeJSON({ OK=true, ERROR="", BUSINESSID=(request.BusinessID & ""), COUNT=arrayLen(sps), SERVICEPOINTS=sps })#</cfoutput>

View file

@ -1,136 +0,0 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<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,"UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) apiAbort({OK=false,ERROR="not_logged_in"});
if (!structKeyExists(request,"BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) apiAbort({OK=false,ERROR="no_business_selected"});
if (!structKeyExists(data,"ServicePointName") || len(normStr(data.ServicePointName)) EQ 0) {
apiAbort({OK=false,ERROR="missing_servicepoint_name",MESSAGE="ServicePointName is required"});
}
spid = 0;
if (structKeyExists(data,"ServicePointID") && isNumeric(data.ServicePointID) && int(data.ServicePointID) GT 0) {
spid = int(data.ServicePointID);
}
spName = normStr(data.ServicePointName);
spTypeID = (structKeyExists(data,"ServicePointTypeID") && isNumeric(data.ServicePointTypeID)) ? int(data.ServicePointTypeID) : 0;
spCode = structKeyExists(data,"ServicePointCode") ? normStr(data.ServicePointCode) : "";
descr = structKeyExists(data,"Description") ? normStr(data.Description) : "";
sortOrd = (structKeyExists(data,"SortOrder") && isNumeric(data.SortOrder)) ? int(data.SortOrder) : 0;
isActive = 1;
if (structKeyExists(data,"IsActive")) {
if (isBoolean(data.IsActive)) isActive = (data.IsActive ? 1 : 0);
else if (isNumeric(data.IsActive)) isActive = int(data.IsActive);
else if (isSimpleValue(data.IsActive)) isActive = (lcase(trim(toString(data.IsActive))) EQ "true" ? 1 : 0);
}
</cfscript>
<cfif spid GT 0>
<cfquery datasource="#application.datasource#">
UPDATE ServicePoints
SET
ServicePointName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#spName#">,
ServicePointTypeID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spTypeID#">,
ServicePointCode = <cfqueryparam cfsqltype="cf_sql_varchar" value="#spCode#" null="#(len(spCode) EQ 0)#">,
Description = <cfqueryparam cfsqltype="cf_sql_varchar" value="#descr#" null="#(len(descr) EQ 0)#">,
SortOrder = <cfqueryparam cfsqltype="cf_sql_integer" value="#sortOrd#">,
IsActive = <cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spid#">
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
</cfquery>
<cfquery name="qCheck" datasource="#application.datasource#">
SELECT ServicePointID
FROM ServicePoints
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spid#">
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
LIMIT 1
</cfquery>
<cfif qCheck.recordCount EQ 0>
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found" })#</cfoutput>
<cfabort>
</cfif>
<cfelse>
<cfquery datasource="#application.datasource#">
INSERT INTO ServicePoints (
BusinessID,
ServicePointName,
ServicePointTypeID,
ServicePointCode,
Description,
SortOrder,
IsActive
) VALUES (
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#spName#">,
<cfqueryparam cfsqltype="cf_sql_integer" value="#spTypeID#">,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#spCode#" null="#(len(spCode) EQ 0)#">,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#descr#" null="#(len(descr) EQ 0)#">,
<cfqueryparam cfsqltype="cf_sql_integer" value="#sortOrd#">,
<cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">
)
</cfquery>
<cfquery name="qId" datasource="#application.datasource#">
SELECT LAST_INSERT_ID() AS ServicePointID
</cfquery>
<cfset spid = qId.ServicePointID>
</cfif>
<cfquery name="qOut" datasource="#application.datasource#">
SELECT
ServicePointID,
BusinessID,
ServicePointName,
ServicePointTypeID,
ServicePointCode,
Description,
SortOrder,
IsActive,
CreatedAt,
UpdatedAt
FROM ServicePoints
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spid#">
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
LIMIT 1
</cfquery>
<cfset sp = {
"ServicePointID"=qOut.ServicePointID,
"BusinessID"=qOut.BusinessID,
"ServicePointName"=qOut.ServicePointName,
"ServicePointTypeID"=qOut.ServicePointTypeID,
"ServicePointCode"=qOut.ServicePointCode,
"Description"=qOut.Description,
"SortOrder"=qOut.SortOrder,
"IsActive"=qOut.IsActive,
"CreatedAt"=(qOut.CreatedAt & ""),
"UpdatedAt"=(qOut.UpdatedAt & "")
}>
<cfoutput>#serializeJSON({ OK=true, ERROR="", SERVICEPOINT=sp })#</cfoutput>

114
api/auth/login.cfm Normal file
View file

@ -0,0 +1,114 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfscript>
/*
PATH:
C:\lucee\tomcat\webapps\ROOT\biz.payfrit.com\api\auth\login.cfm
INPUT (JSON):
{ "username": "...", "password": "..." }
OUTPUT (JSON):
{ OK:true, ERROR:"", UserID:123, UserFirstName:"...", Token:"..." }
Uses existing UserTokens table:
TokenID (auto), UserID, Token, CreatedAt (DEFAULT CURRENT_TIMESTAMP)
-> INSERT does NOT include CreatedAt.
*/
function apiAbort(required struct payload) {
writeOutput(serializeJSON(payload));
abort;
}
function readJsonBody() {
var raw = getHttpRequestData().content;
if (isNull(raw)) raw = "";
if (!len(trim(raw))) return {};
try {
var data = deserializeJSON(raw);
if (isStruct(data)) return data;
} catch (any e) {}
return {};
}
function normalizeUsername(required string u) {
var x = trim(arguments.u);
x = replace(x, " ", "", "all");
x = replace(x, "(", "", "all");
x = replace(x, ")", "", "all");
x = replace(x, "-", "", "all");
return x;
}
var data = readJsonBody();
var username = structKeyExists(data, "username") ? normalizeUsername("" & data.username) : "";
var password = structKeyExists(data, "password") ? ("" & data.password) : "";
if (!len(username) || !len(password)) {
apiAbort({ "OK": false, "ERROR": "missing_fields" });
}
try {
var q = queryExecute(
"
SELECT UserID, UserFirstName
FROM Users
WHERE
(
(UserEmailAddress = ?) OR
(UserContactNumber = ?)
)
AND UserPassword = ?
AND UserIsEmailVerified = 1
AND UserIsContactVerified > 0
LIMIT 1
",
[
{ value = username, cfsqltype = "cf_sql_varchar" },
{ value = username, cfsqltype = "cf_sql_varchar" },
{ value = hash(password), cfsqltype = "cf_sql_varchar" }
],
{ datasource = "payfrit" }
);
if (q.recordCount NEQ 1) {
apiAbort({ "OK": false, "ERROR": "bad_credentials" });
}
var token = replace(createUUID(), "-", "", "all");
queryExecute(
"INSERT INTO UserTokens (UserID, Token) VALUES (?, ?)",
[
{ value = q.UserID, cfsqltype = "cf_sql_integer" },
{ value = token, cfsqltype = "cf_sql_varchar" }
],
{ datasource = "payfrit" }
);
// Optional: also set session for browser tools
cflock timeout="15" throwontimeout="yes" type="exclusive" scope="session" {
session.UserID = q.UserID;
}
request.UserID = q.UserID;
writeOutput(serializeJSON({
"OK": true,
"ERROR": "",
"UserID": q.UserID,
"UserFirstName": q.UserFirstName,
"Token": token
}));
abort;
} catch (any e) {
apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error during login",
"DETAIL": e.message
});
}
</cfscript>

View file

@ -1,81 +1,52 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<cfscript>
/*
PUBLIC MVP endpoint
Returns Businesses list with STRICT casing:
- Businesses
- BusinessID
- BusinessName
Lucee-safe JSON output (NO encodeForJSON)
*/
function jsonString(val) {
// serializeJSON("abc") -> "\"abc\""
// we strip the surrounding quotes
var s = serializeJSON(toString(val));
return mid(s, 2, len(s) - 2);
}
function apiAbort(obj) {
writeOutput('{');
writeOutput('"OK":false');
if (structKeyExists(obj, "ERROR")) {
writeOutput(',"ERROR":"' & jsonString(obj.ERROR) & '"');
}
if (structKeyExists(obj, "DETAIL")) {
writeOutput(',"DETAIL":"' & jsonString(obj.DETAIL) & '"');
}
writeOutput('}');
function apiAbort(payload) {
writeOutput(serializeJSON(payload));
abort;
}
/* ---- datasource resolution (unchanged) ---- */
dsn = "";
if (structKeyExists(application, "datasource")) {
dsn = application.datasource;
} else if (structKeyExists(application, "dsn")) {
dsn = application.dsn;
}
try {
q = queryExecute(
"
SELECT
BusinessID,
BusinessName
FROM Businesses
ORDER BY BusinessName
",
[],
{ datasource = "payfrit" }
);
if (!len(dsn)) {
// Convert query -> array of structs (JSON-native)
rows = [];
for (i = 1; i <= q.recordCount; i++) {
arrayAppend(rows, {
"BusinessID": q.BusinessID[i],
"BusinessName": q.BusinessName[i]
});
}
// Provide BOTH keys to satisfy any Flutter casing expectation
writeOutput(serializeJSON({
"OK": true,
"ERROR": "",
"VERSION": "businesses_list_v3",
"BUSINESSES": rows,
"Businesses": rows
}));
abort;
} catch (any e) {
apiAbort({
ERROR = "missing_datasource",
DETAIL = "No datasource configured"
"OK": false,
"ERROR": "server_error",
"DETAIL": e.message
});
}
/* ---- query ---- */
q = queryExecute(
"
SELECT
BusinessID,
BusinessName
FROM Businesses
ORDER BY BusinessName
",
[],
{ datasource = dsn }
);
/* ---- output ---- */
writeOutput('{');
writeOutput('"OK":true');
writeOutput(',"COUNT":' & q.recordCount);
writeOutput(',"Businesses":[');
for (i = 1; i LTE q.recordCount; i = i + 1) {
if (i GT 1) writeOutput(',');
writeOutput('{');
writeOutput('"BusinessID":' & q.BusinessID[i]);
writeOutput(',"BusinessName":"' & jsonString(q.BusinessName[i]) & '"');
writeOutput('}');
}
writeOutput(']}');
</cfscript>

98
api/menu/items.cfm Normal file
View file

@ -0,0 +1,98 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
<cfset var raw = getHttpRequestData().content>
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
<cfreturn {}>
</cfif>
<cftry>
<cfset var data = deserializeJSON(raw)>
<cfif isStruct(data)>
<cfreturn data>
<cfelse>
<cfreturn {}>
</cfif>
<cfcatch>
<cfreturn {}>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="apiAbort" access="public" returntype="void" output="true">
<cfargument name="payload" type="struct" required="true">
<cfcontent type="application/json; charset=utf-8">
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
<cfabort>
</cffunction>
<cfset data = readJsonBody()>
<cfset BusinessID = 0>
<cfif structKeyExists(data, "BusinessID")>
<cfset BusinessID = val(data.BusinessID)>
</cfif>
<cfif BusinessID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_businessid", "MESSAGE": "BusinessID is required.", "DETAIL": "" })>
</cfif>
<cftry>
<cfset q = queryExecute(
"
SELECT
i.ItemID,
i.ItemCategoryID,
i.ItemName,
i.ItemDescription,
i.ItemParentItemID,
i.ItemPrice,
i.ItemIsActive,
i.ItemIsCheckedByDefault,
i.ItemRequiresChildSelection,
i.ItemMaxNumSelectionReq,
i.ItemIsCollapsible,
i.ItemSortOrder
FROM Items i
INNER JOIN Categories c
ON c.CategoryID = i.ItemCategoryID
WHERE c.CategoryBusinessID = ?
ORDER BY i.ItemParentItemID, i.ItemSortOrder, i.ItemID
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset rows = []>
<cfloop query="q">
<cfset arrayAppend(rows, {
"ItemID": q.ItemID,
"ItemCategoryID": q.ItemCategoryID,
"ItemName": q.ItemName,
"ItemDescription": q.ItemDescription,
"ItemParentItemID": q.ItemParentItemID,
"ItemPrice": q.ItemPrice,
"ItemIsActive": q.ItemIsActive,
"ItemIsCheckedByDefault": q.ItemIsCheckedByDefault,
"ItemRequiresChildSelection": q.ItemRequiresChildSelection,
"ItemMaxNumSelectionReq": q.ItemMaxNumSelectionReq,
"ItemIsCollapsible": q.ItemIsCollapsible,
"ItemSortOrder": q.ItemSortOrder
})>
</cfloop>
<cfset apiAbort({
"OK": true,
"ERROR": "",
"Items": rows,
"COUNT": arrayLen(rows)
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error loading items",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>

135
api/orders/getCart.cfm Normal file
View file

@ -0,0 +1,135 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
<cfset var raw = getHttpRequestData().content>
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
<cfreturn {}>
</cfif>
<cftry>
<cfset var data = deserializeJSON(raw)>
<cfif isStruct(data)>
<cfreturn data>
<cfelse>
<cfreturn {}>
</cfif>
<cfcatch>
<cfreturn {}>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="apiAbort" access="public" returntype="void" output="true">
<cfargument name="payload" type="struct" required="true">
<cfcontent type="application/json; charset=utf-8">
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
<cfabort>
</cffunction>
<cfset data = readJsonBody()>
<cfset OrderID = val( structKeyExists(data,"OrderID") ? data.OrderID : 0 )>
<cfif OrderID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_orderid", "MESSAGE": "OrderID is required.", "DETAIL": "" })>
</cfif>
<cftry>
<cfset qOrder = queryExecute(
"
SELECT
OrderID,
OrderUUID,
OrderUserID,
OrderBusinessID,
OrderBusinessDeliveryMultiplier,
OrderTypeID,
OrderDeliveryFee,
OrderStatusID,
OrderAddressID,
OrderPaymentID,
OrderRemarks,
OrderAddedOn,
OrderLastEditedOn,
OrderSubmittedOn,
OrderServicePointID
FROM Orders
WHERE OrderID = ?
LIMIT 1
",
[ { value = OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qOrder.recordCount EQ 0>
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Order not found.", "DETAIL": "" })>
</cfif>
<cfset qLI = queryExecute(
"
SELECT
OrderLineItemID,
OrderLineItemParentOrderLineItemID,
OrderLineItemOrderID,
OrderLineItemItemID,
OrderLineItemStatusID,
OrderLineItemPrice,
OrderLineItemQuantity,
OrderLineItemRemark,
OrderLineItemIsDeleted,
OrderLineItemAddedOn
FROM OrderLineItems
WHERE OrderLineItemOrderID = ?
ORDER BY OrderLineItemID
",
[ { value = OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset rows = []>
<cfloop query="qLI">
<cfset arrayAppend(rows, {
"OrderLineItemID": qLI.OrderLineItemID,
"OrderLineItemParentOrderLineItemID": qLI.OrderLineItemParentOrderLineItemID,
"OrderLineItemOrderID": qLI.OrderLineItemOrderID,
"OrderLineItemItemID": qLI.OrderLineItemItemID,
"OrderLineItemStatusID": qLI.OrderLineItemStatusID,
"OrderLineItemPrice": qLI.OrderLineItemPrice,
"OrderLineItemQuantity": qLI.OrderLineItemQuantity,
"OrderLineItemRemark": qLI.OrderLineItemRemark,
"OrderLineItemIsDeleted": qLI.OrderLineItemIsDeleted,
"OrderLineItemAddedOn": qLI.OrderLineItemAddedOn
})>
</cfloop>
<cfset apiAbort({
"OK": true,
"ERROR": "",
"Order": {
"OrderID": qOrder.OrderID,
"OrderUUID": qOrder.OrderUUID,
"OrderUserID": qOrder.OrderUserID,
"OrderBusinessID": qOrder.OrderBusinessID,
"OrderBusinessDeliveryMultiplier": qOrder.OrderBusinessDeliveryMultiplier,
"OrderTypeID": qOrder.OrderTypeID,
"OrderDeliveryFee": qOrder.OrderDeliveryFee,
"OrderStatusID": qOrder.OrderStatusID,
"OrderAddressID": qOrder.OrderAddressID,
"OrderPaymentID": qOrder.OrderPaymentID,
"OrderRemarks": qOrder.OrderRemarks,
"OrderAddedOn": qOrder.OrderAddedOn,
"OrderLastEditedOn": qOrder.OrderLastEditedOn,
"OrderSubmittedOn": qOrder.OrderSubmittedOn,
"OrderServicePointID": qOrder.OrderServicePointID
},
"OrderLineItems": rows
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error loading cart",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>

View file

@ -0,0 +1,263 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
<cfset var raw = getHttpRequestData().content>
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
<cfreturn {}>
</cfif>
<cftry>
<cfset var data = deserializeJSON(raw)>
<cfif isStruct(data)>
<cfreturn data>
<cfelse>
<cfreturn {}>
</cfif>
<cfcatch>
<cfreturn {}>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="apiAbort" access="public" returntype="void" output="true">
<cfargument name="payload" type="struct" required="true">
<cfcontent type="application/json; charset=utf-8">
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
<cfabort>
</cffunction>
<cffunction name="loadCartPayload" access="public" returntype="struct" output="false">
<cfargument name="OrderID" type="numeric" required="true">
<cfset var out = {}>
<cfset var qOrder = queryExecute(
"
SELECT
OrderID,
OrderUUID,
OrderUserID,
OrderBusinessID,
OrderBusinessDeliveryMultiplier,
OrderTypeID,
OrderDeliveryFee,
OrderStatusID,
OrderAddressID,
OrderPaymentID,
OrderRemarks,
OrderAddedOn,
OrderLastEditedOn,
OrderSubmittedOn,
OrderServicePointID
FROM Orders
WHERE OrderID = ?
LIMIT 1
",
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qOrder.recordCount EQ 0>
<cfreturn { "OK": false, "ERROR": "not_found", "MESSAGE": "Order not found", "DETAIL": "" }>
</cfif>
<cfset out.Order = {
"OrderID": qOrder.OrderID,
"OrderUUID": qOrder.OrderUUID,
"OrderUserID": qOrder.OrderUserID,
"OrderBusinessID": qOrder.OrderBusinessID,
"OrderBusinessDeliveryMultiplier": qOrder.OrderBusinessDeliveryMultiplier,
"OrderTypeID": qOrder.OrderTypeID,
"OrderDeliveryFee": qOrder.OrderDeliveryFee,
"OrderStatusID": qOrder.OrderStatusID,
"OrderAddressID": qOrder.OrderAddressID,
"OrderPaymentID": qOrder.OrderPaymentID,
"OrderRemarks": qOrder.OrderRemarks,
"OrderAddedOn": qOrder.OrderAddedOn,
"OrderLastEditedOn": qOrder.OrderLastEditedOn,
"OrderSubmittedOn": qOrder.OrderSubmittedOn,
"OrderServicePointID": qOrder.OrderServicePointID
}>
<cfset var qLI = queryExecute(
"
SELECT
OrderLineItemID,
OrderLineItemParentOrderLineItemID,
OrderLineItemOrderID,
OrderLineItemItemID,
OrderLineItemStatusID,
OrderLineItemPrice,
OrderLineItemQuantity,
OrderLineItemRemark,
OrderLineItemIsDeleted,
OrderLineItemAddedOn
FROM OrderLineItems
WHERE OrderLineItemOrderID = ?
ORDER BY OrderLineItemID
",
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset var rows = []>
<cfloop query="qLI">
<cfset arrayAppend(rows, {
"OrderLineItemID": qLI.OrderLineItemID,
"OrderLineItemParentOrderLineItemID": qLI.OrderLineItemParentOrderLineItemID,
"OrderLineItemOrderID": qLI.OrderLineItemOrderID,
"OrderLineItemItemID": qLI.OrderLineItemItemID,
"OrderLineItemStatusID": qLI.OrderLineItemStatusID,
"OrderLineItemPrice": qLI.OrderLineItemPrice,
"OrderLineItemQuantity": qLI.OrderLineItemQuantity,
"OrderLineItemRemark": qLI.OrderLineItemRemark,
"OrderLineItemIsDeleted": qLI.OrderLineItemIsDeleted,
"OrderLineItemAddedOn": qLI.OrderLineItemAddedOn
})>
</cfloop>
<cfset out.OrderLineItems = rows>
<cfset out.OK = true>
<cfset out.ERROR = "">
<cfreturn out>
</cffunction>
<cfset data = readJsonBody()>
<cfset BusinessID = val( structKeyExists(data,"BusinessID") ? data.BusinessID : 0 )>
<cfset OrderServicePointID = val( structKeyExists(data,"OrderServicePointID") ? data.OrderServicePointID : 0 )>
<cfset OrderTypeID = val( structKeyExists(data,"OrderTypeID") ? data.OrderTypeID : 0 )>
<cfset OrderUserID = val( structKeyExists(data,"OrderUserID") ? data.OrderUserID : 0 )>
<cfif BusinessID LTE 0 OR OrderServicePointID LTE 0 OR OrderTypeID NEQ 1 OR OrderUserID LTE 0>
<cfset apiAbort({
"OK": false,
"ERROR": "missing_params",
"MESSAGE": "BusinessID, OrderServicePointID, OrderTypeID=1, and OrderUserID are required.",
"DETAIL": ""
})>
</cfif>
<cftry>
<!--- Find existing cart (OrderStatusID=0 assumed cart) --->
<cfset qFind = queryExecute(
"
SELECT OrderID
FROM Orders
WHERE OrderUserID = ?
AND OrderBusinessID = ?
AND OrderTypeID = 1
AND OrderStatusID = 0
AND OrderServicePointID = ?
ORDER BY OrderID DESC
LIMIT 1
",
[
{ value = OrderUserID, cfsqltype = "cf_sql_integer" },
{ value = BusinessID, cfsqltype = "cf_sql_integer" },
{ value = OrderServicePointID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<cfif qFind.recordCount GT 0>
<cfset payload = loadCartPayload(qFind.OrderID)>
<cfset apiAbort(payload)>
</cfif>
<!--- Create new cart order --->
<cfset qBiz = queryExecute(
"
SELECT BusinessDeliveryMultiplier, BusinessDeliveryFlatFee
FROM Businesses
WHERE BusinessID = ?
LIMIT 1
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qBiz.recordCount EQ 0>
<cfset apiAbort({ "OK": false, "ERROR": "bad_business", "MESSAGE": "Business not found", "DETAIL": "" })>
</cfif>
<cfset nowDt = now()>
<cfset newUUID = createUUID()>
<!--- Generate new OrderID (table is not auto-inc in SSOT) --->
<cfset qNext = queryExecute(
"SELECT IFNULL(MAX(OrderID),0) + 1 AS NextID FROM Orders",
[],
{ datasource = "payfrit" }
)>
<cfset NewOrderID = qNext.NextID>
<cfset queryExecute(
"
INSERT INTO Orders (
OrderID,
OrderUUID,
OrderUserID,
OrderBusinessID,
OrderBusinessDeliveryMultiplier,
OrderTypeID,
OrderDeliveryFee,
OrderStatusID,
OrderAddressID,
OrderPaymentID,
OrderRemarks,
OrderAddedOn,
OrderLastEditedOn,
OrderSubmittedOn,
OrderServicePointID
) VALUES (
?,
?,
?,
?,
?,
1,
?,
0,
NULL,
NULL,
NULL,
?,
?,
NULL,
?
)
",
[
{ value = NewOrderID, cfsqltype = "cf_sql_integer" },
{ value = newUUID, cfsqltype = "cf_sql_varchar" },
{ value = OrderUserID, cfsqltype = "cf_sql_integer" },
{ value = BusinessID, cfsqltype = "cf_sql_integer" },
{ value = qBiz.BusinessDeliveryMultiplier, cfsqltype = "cf_sql_decimal" },
{ value = qBiz.BusinessDeliveryFlatFee, cfsqltype = "cf_sql_decimal" },
{ value = nowDt, cfsqltype = "cf_sql_timestamp" },
{ value = nowDt, cfsqltype = "cf_sql_timestamp" },
{ value = OrderServicePointID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<!--- Per your rule: OrderID is determined by selecting highest after creation --->
<cfset qLatest = queryExecute(
"SELECT MAX(OrderID) AS OrderID FROM Orders",
[],
{ datasource = "payfrit" }
)>
<cfset FinalOrderID = qLatest.OrderID>
<cfset payload = loadCartPayload(FinalOrderID)>
<cfset apiAbort(payload)>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error creating cart",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>

399
api/orders/setLineItem.cfm Normal file
View file

@ -0,0 +1,399 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
<cfset var raw = getHttpRequestData().content>
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
<cfreturn {}>
</cfif>
<cftry>
<cfset var data = deserializeJSON(raw)>
<cfif isStruct(data)>
<cfreturn data>
<cfelse>
<cfreturn {}>
</cfif>
<cfcatch>
<cfreturn {}>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="apiAbort" access="public" returntype="void" output="true">
<cfargument name="payload" type="struct" required="true">
<cfcontent type="application/json; charset=utf-8">
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
<cfabort>
</cffunction>
<cffunction name="nextId" access="public" returntype="numeric" output="false">
<cfargument name="tableName" type="string" required="true">
<cfargument name="idField" type="string" required="true">
<cfset var q = queryExecute(
"SELECT IFNULL(MAX(#arguments.idField#),0) + 1 AS NextID FROM #arguments.tableName#",
[],
{ datasource = "payfrit" }
)>
<cfreturn q.NextID>
</cffunction>
<cffunction name="attachDefaultChildren" access="public" returntype="void" output="false">
<cfargument name="OrderID" type="numeric" required="true">
<cfargument name="ParentLineItemID" type="numeric" required="true">
<cfargument name="ParentItemID" type="numeric" required="true">
<!--- Find immediate children where checked by default --->
<cfset var qKids = queryExecute(
"
SELECT ItemID, ItemPrice
FROM Items
WHERE ItemParentItemID = ?
AND ItemIsCheckedByDefault = 1
AND ItemIsActive = b'1'
ORDER BY ItemSortOrder, ItemID
",
[ { value = arguments.ParentItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfloop query="qKids">
<!--- If existing, undelete; else insert new --->
<cfset var qExisting = queryExecute(
"
SELECT OrderLineItemID
FROM OrderLineItems
WHERE OrderLineItemOrderID = ?
AND OrderLineItemParentOrderLineItemID = ?
AND OrderLineItemItemID = ?
LIMIT 1
",
[
{ value = arguments.OrderID, cfsqltype = "cf_sql_integer" },
{ value = arguments.ParentLineItemID, cfsqltype = "cf_sql_integer" },
{ value = qKids.ItemID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<cfif qExisting.recordCount GT 0>
<cfset queryExecute(
"
UPDATE OrderLineItems
SET OrderLineItemIsDeleted = b'0'
WHERE OrderLineItemID = ?
",
[ { value = qExisting.OrderLineItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset attachDefaultChildren(arguments.OrderID, qExisting.OrderLineItemID, qKids.ItemID)>
<cfelse>
<cfset var NewLIID = nextId("OrderLineItems","OrderLineItemID")>
<cfset queryExecute(
"
INSERT INTO OrderLineItems (
OrderLineItemID,
OrderLineItemParentOrderLineItemID,
OrderLineItemOrderID,
OrderLineItemItemID,
OrderLineItemStatusID,
OrderLineItemPrice,
OrderLineItemQuantity,
OrderLineItemRemark,
OrderLineItemIsDeleted,
OrderLineItemAddedOn
) VALUES (
?,
?,
?,
?,
0,
?,
1,
NULL,
b'0',
?
)
",
[
{ value = NewLIID, cfsqltype = "cf_sql_integer" },
{ value = arguments.ParentLineItemID, cfsqltype = "cf_sql_integer" },
{ value = arguments.OrderID, cfsqltype = "cf_sql_integer" },
{ value = qKids.ItemID, cfsqltype = "cf_sql_integer" },
{ value = qKids.ItemPrice, cfsqltype = "cf_sql_decimal" },
{ value = now(), cfsqltype = "cf_sql_timestamp" }
],
{ datasource = "payfrit" }
)>
<cfset attachDefaultChildren(arguments.OrderID, NewLIID, qKids.ItemID)>
</cfif>
</cfloop>
</cffunction>
<cffunction name="loadCartPayload" access="public" returntype="struct" output="false">
<cfargument name="OrderID" type="numeric" required="true">
<cfset var out = {}>
<cfset var qOrder = queryExecute(
"
SELECT
OrderID,
OrderUUID,
OrderUserID,
OrderBusinessID,
OrderBusinessDeliveryMultiplier,
OrderTypeID,
OrderDeliveryFee,
OrderStatusID,
OrderAddressID,
OrderPaymentID,
OrderRemarks,
OrderAddedOn,
OrderLastEditedOn,
OrderSubmittedOn,
OrderServicePointID
FROM Orders
WHERE OrderID = ?
LIMIT 1
",
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qOrder.recordCount EQ 0>
<cfreturn { "OK": false, "ERROR": "not_found", "MESSAGE": "Order not found", "DETAIL": "" }>
</cfif>
<cfset out.Order = {
"OrderID": qOrder.OrderID,
"OrderUUID": qOrder.OrderUUID,
"OrderUserID": qOrder.OrderUserID,
"OrderBusinessID": qOrder.OrderBusinessID,
"OrderBusinessDeliveryMultiplier": qOrder.OrderBusinessDeliveryMultiplier,
"OrderTypeID": qOrder.OrderTypeID,
"OrderDeliveryFee": qOrder.OrderDeliveryFee,
"OrderStatusID": qOrder.OrderStatusID,
"OrderAddressID": qOrder.OrderAddressID,
"OrderPaymentID": qOrder.OrderPaymentID,
"OrderRemarks": qOrder.OrderRemarks,
"OrderAddedOn": qOrder.OrderAddedOn,
"OrderLastEditedOn": qOrder.OrderLastEditedOn,
"OrderSubmittedOn": qOrder.OrderSubmittedOn,
"OrderServicePointID": qOrder.OrderServicePointID
}>
<cfset var qLI = queryExecute(
"
SELECT
OrderLineItemID,
OrderLineItemParentOrderLineItemID,
OrderLineItemOrderID,
OrderLineItemItemID,
OrderLineItemStatusID,
OrderLineItemPrice,
OrderLineItemQuantity,
OrderLineItemRemark,
OrderLineItemIsDeleted,
OrderLineItemAddedOn
FROM OrderLineItems
WHERE OrderLineItemOrderID = ?
ORDER BY OrderLineItemID
",
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset var rows = []>
<cfloop query="qLI">
<cfset arrayAppend(rows, {
"OrderLineItemID": qLI.OrderLineItemID,
"OrderLineItemParentOrderLineItemID": qLI.OrderLineItemParentOrderLineItemID,
"OrderLineItemOrderID": qLI.OrderLineItemOrderID,
"OrderLineItemItemID": qLI.OrderLineItemItemID,
"OrderLineItemStatusID": qLI.OrderLineItemStatusID,
"OrderLineItemPrice": qLI.OrderLineItemPrice,
"OrderLineItemQuantity": qLI.OrderLineItemQuantity,
"OrderLineItemRemark": qLI.OrderLineItemRemark,
"OrderLineItemIsDeleted": qLI.OrderLineItemIsDeleted,
"OrderLineItemAddedOn": qLI.OrderLineItemAddedOn
})>
</cfloop>
<cfset out.OrderLineItems = rows>
<cfset out.OK = true>
<cfset out.ERROR = "">
<cfreturn out>
</cffunction>
<cfset data = readJsonBody()>
<cfset OrderID = val( structKeyExists(data,"OrderID") ? data.OrderID : 0 )>
<cfset ParentLineItemID = val( structKeyExists(data,"ParentOrderLineItemID") ? data.ParentOrderLineItemID : 0 )>
<cfset ItemID = val( structKeyExists(data,"ItemID") ? data.ItemID : 0 )>
<cfset IsSelected = false>
<cfif structKeyExists(data, "IsSelected")>
<cfset IsSelected = (data.IsSelected EQ true OR data.IsSelected EQ 1 OR (isSimpleValue(data.IsSelected) AND lcase(toString(data.IsSelected)) EQ "true"))>
</cfif>
<cfset Quantity = structKeyExists(data,"Quantity") ? val(data.Quantity) : 0>
<cfset Remark = structKeyExists(data,"Remark") ? toString(data.Remark) : "">
<cfif OrderID LTE 0 OR ItemID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "OrderID and ItemID are required.", "DETAIL": "" })>
</cfif>
<cftry>
<!--- Load item price --->
<cfset qItem = queryExecute(
"
SELECT ItemID, ItemPrice, ItemParentItemID, ItemIsActive
FROM Items
WHERE ItemID = ?
LIMIT 1
",
[ { value = ItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qItem.recordCount EQ 0 OR qItem.ItemIsActive NEQ true>
<cfset apiAbort({ "OK": false, "ERROR": "bad_item", "MESSAGE": "Item not found or inactive.", "DETAIL": "" })>
</cfif>
<!--- Root vs modifier rules --->
<cfif ParentLineItemID EQ 0>
<!--- Root item quantity required when selecting --->
<cfif IsSelected AND Quantity LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "bad_quantity", "MESSAGE": "Root line items require Quantity > 0.", "DETAIL": "" })>
</cfif>
<cfelse>
<!--- Modifier quantity is implicitly tied => force 1 when selecting --->
<cfif IsSelected>
<cfset Quantity = 1>
<cfelse>
<cfset Quantity = 1>
</cfif>
</cfif>
<!--- Find existing line item (by order, parent LI, item) --->
<cfset qExisting = queryExecute(
"
SELECT OrderLineItemID
FROM OrderLineItems
WHERE OrderLineItemOrderID = ?
AND OrderLineItemParentOrderLineItemID = ?
AND OrderLineItemItemID = ?
LIMIT 1
",
[
{ value = OrderID, cfsqltype = "cf_sql_integer" },
{ value = ParentLineItemID, cfsqltype = "cf_sql_integer" },
{ value = ItemID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<cfif qExisting.recordCount GT 0>
<!--- Update existing --->
<cfif IsSelected>
<cfset queryExecute(
"
UPDATE OrderLineItems
SET
OrderLineItemIsDeleted = b'0',
OrderLineItemQuantity = ?,
OrderLineItemPrice = ?,
OrderLineItemRemark = ?,
OrderLineItemStatusID = 0
WHERE OrderLineItemID = ?
",
[
{ value = Quantity, cfsqltype = "cf_sql_integer" },
{ value = qItem.ItemPrice, cfsqltype = "cf_sql_decimal" },
{ value = (len(trim(Remark)) EQ 0 ? javacast("null","") : Remark), cfsqltype = "cf_sql_varchar", null = (len(trim(Remark)) EQ 0) },
{ value = qExisting.OrderLineItemID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<!--- Attach default children for this node (recursively) --->
<cfset attachDefaultChildren(OrderID, qExisting.OrderLineItemID, ItemID)>
<cfelse>
<cfset queryExecute(
"
UPDATE OrderLineItems
SET OrderLineItemIsDeleted = b'1'
WHERE OrderLineItemID = ?
",
[ { value = qExisting.OrderLineItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
</cfif>
<cfelse>
<!--- Insert new if selecting, otherwise no-op --->
<cfif IsSelected>
<cfset NewLIID = nextId("OrderLineItems","OrderLineItemID")>
<cfset queryExecute(
"
INSERT INTO OrderLineItems (
OrderLineItemID,
OrderLineItemParentOrderLineItemID,
OrderLineItemOrderID,
OrderLineItemItemID,
OrderLineItemStatusID,
OrderLineItemPrice,
OrderLineItemQuantity,
OrderLineItemRemark,
OrderLineItemIsDeleted,
OrderLineItemAddedOn
) VALUES (
?,
?,
?,
?,
0,
?,
?,
?,
b'0',
?
)
",
[
{ value = NewLIID, cfsqltype = "cf_sql_integer" },
{ value = ParentLineItemID, cfsqltype = "cf_sql_integer" },
{ value = OrderID, cfsqltype = "cf_sql_integer" },
{ value = ItemID, cfsqltype = "cf_sql_integer" },
{ value = qItem.ItemPrice, cfsqltype = "cf_sql_decimal" },
{ value = (ParentLineItemID EQ 0 ? Quantity : 1), cfsqltype = "cf_sql_integer" },
{ value = (len(trim(Remark)) EQ 0 ? javacast("null","") : Remark), cfsqltype = "cf_sql_varchar", null = (len(trim(Remark)) EQ 0) },
{ value = now(), cfsqltype = "cf_sql_timestamp" }
],
{ datasource = "payfrit" }
)>
<cfset attachDefaultChildren(OrderID, NewLIID, ItemID)>
</cfif>
</cfif>
<!--- Touch order last edited --->
<cfset queryExecute(
"UPDATE Orders SET OrderLastEditedOn = ? WHERE OrderID = ?",
[
{ value = now(), cfsqltype = "cf_sql_timestamp" },
{ value = OrderID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<cfset payload = loadCartPayload(OrderID)>
<cfset apiAbort(payload)>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error setting line item",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>

261
api/orders/submit.cfm Normal file
View file

@ -0,0 +1,261 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
<cfset var raw = getHttpRequestData().content>
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
<cfreturn {}>
</cfif>
<cftry>
<cfset var data = deserializeJSON(raw)>
<cfif isStruct(data)>
<cfreturn data>
<cfelse>
<cfreturn {}>
</cfif>
<cfcatch>
<cfreturn {}>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="apiAbort" access="public" returntype="void" output="true">
<cfargument name="payload" type="struct" required="true">
<cfcontent type="application/json; charset=utf-8">
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
<cfabort>
</cffunction>
<cffunction name="buildLineItemsGraph" access="public" returntype="struct" output="false">
<cfargument name="OrderID" type="numeric" required="true">
<cfset var out = {}>
<cfset out.items = {}> <!--- lineItemId -> struct --->
<cfset out.children = {}> <!--- parentLineItemId -> array(lineItemId) --->
<cfset out.itemMeta = {}> <!--- ItemID -> struct(meta) --->
<cfset var qLI = queryExecute(
"
SELECT
OrderLineItemID,
OrderLineItemParentOrderLineItemID,
OrderLineItemItemID,
OrderLineItemIsDeleted
FROM OrderLineItems
WHERE OrderLineItemOrderID = ?
ORDER BY OrderLineItemID
",
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qLI.recordCount EQ 0>
<cfreturn out>
</cfif>
<cfset var itemIds = []>
<cfloop query="qLI">
<cfset out.items[qLI.OrderLineItemID] = {
"id": qLI.OrderLineItemID,
"parentId": qLI.OrderLineItemParentOrderLineItemID,
"itemId": qLI.OrderLineItemItemID,
"isDeleted": (qLI.OrderLineItemIsDeleted EQ true)
}>
<cfif NOT structKeyExists(out.children, qLI.OrderLineItemParentOrderLineItemID)>
<cfset out.children[qLI.OrderLineItemParentOrderLineItemID] = []>
</cfif>
<cfset arrayAppend(out.children[qLI.OrderLineItemParentOrderLineItemID], qLI.OrderLineItemID)>
<cfset arrayAppend(itemIds, qLI.OrderLineItemItemID)>
</cfloop>
<!--- Load meta for involved items --->
<cfset var uniq = {} >
<cfloop array="#itemIds#" index="iid">
<cfset uniq[iid] = true>
</cfloop>
<cfset var uniqIds = structKeyArray(uniq)>
<cfif arrayLen(uniqIds) GT 0>
<cfset var inList = arrayToList(uniqIds)>
<cfset var qMeta = queryExecute(
"
SELECT
ItemID,
ItemRequiresChildSelection,
ItemMaxNumSelectionReq
FROM Items
WHERE ItemID IN (#inList#)
",
[],
{ datasource = "payfrit" }
)>
<cfloop query="qMeta">
<cfset out.itemMeta[qMeta.ItemID] = {
"requires": qMeta.ItemRequiresChildSelection,
"maxSel": qMeta.ItemMaxNumSelectionReq
}>
</cfloop>
</cfif>
<cfreturn out>
</cffunction>
<cffunction name="hasSelectedDescendant" access="public" returntype="boolean" output="false">
<cfargument name="graph" type="struct" required="true">
<cfargument name="lineItemId" type="numeric" required="true">
<cfset var stack = []>
<cfif structKeyExists(arguments.graph.children, arguments.lineItemId)>
<cfset stack = duplicate(arguments.graph.children[arguments.lineItemId])>
</cfif>
<cfloop condition="arrayLen(stack) GT 0">
<cfset var id = stack[arrayLen(stack)]>
<cfset arrayDeleteAt(stack, arrayLen(stack))>
<cfif structKeyExists(arguments.graph.items, id)>
<cfset var node = arguments.graph.items[id]>
<cfif NOT node.isDeleted>
<cfreturn true>
</cfif>
<cfif structKeyExists(arguments.graph.children, id)>
<cfset var kids = arguments.graph.children[id]>
<cfloop array="#kids#" index="kidId">
<cfset arrayAppend(stack, kidId)>
</cfloop>
</cfif>
</cfif>
</cfloop>
<cfreturn false>
</cffunction>
<cfset data = readJsonBody()>
<cfset OrderID = val( structKeyExists(data,"OrderID") ? data.OrderID : 0 )>
<cfif OrderID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_orderid", "MESSAGE": "OrderID is required.", "DETAIL": "" })>
</cfif>
<cftry>
<cfset qOrder = queryExecute(
"
SELECT OrderID, OrderStatusID, OrderTypeID
FROM Orders
WHERE OrderID = ?
LIMIT 1
",
[ { value = OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qOrder.recordCount EQ 0>
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Order not found.", "DETAIL": "" })>
</cfif>
<cfif qOrder.OrderStatusID NEQ 0>
<cfset apiAbort({ "OK": false, "ERROR": "bad_state", "MESSAGE": "Order is not in cart state.", "DETAIL": "" })>
</cfif>
<cfif qOrder.OrderTypeID NEQ 1>
<cfset apiAbort({ "OK": false, "ERROR": "bad_type", "MESSAGE": "Only dine-in orders (OrderTypeID=1) are supported in this MVP submit endpoint.", "DETAIL": "" })>
</cfif>
<!--- Must have at least one non-deleted root line item --->
<cfset qRoots = queryExecute(
"
SELECT COUNT(*) AS Cnt
FROM OrderLineItems
WHERE OrderLineItemOrderID = ?
AND OrderLineItemParentOrderLineItemID = 0
AND OrderLineItemIsDeleted = b'0'
",
[ { value = OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qRoots.Cnt LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "empty_order", "MESSAGE": "Order has no items.", "DETAIL": "" })>
</cfif>
<!--- Validate requires-descendant and max immediate selections --->
<cfset graph = buildLineItemsGraph(OrderID)>
<!--- Loop all non-deleted nodes and validate --->
<cfloop collection="#graph.items#" item="k">
<cfset node = graph.items[k]>
<cfif node.isDeleted>
<!--- skip deleted nodes --->
<cfcontinue>
</cfif>
<cfset meta = structKeyExists(graph.itemMeta, node.itemId) ? graph.itemMeta[node.itemId] : { "requires": 0, "maxSel": 0 }>
<!--- max immediate selections --->
<cfset maxSel = val(meta.maxSel)>
<cfif maxSel GT 0>
<cfset selCount = 0>
<cfif structKeyExists(graph.children, node.id)>
<cfset kids = graph.children[node.id]>
<cfloop array="#kids#" index="kidId">
<cfif structKeyExists(graph.items, kidId)>
<cfset kidNode = graph.items[kidId]>
<cfif NOT kidNode.isDeleted>
<cfset selCount = selCount + 1>
</cfif>
</cfif>
</cfloop>
</cfif>
<cfif selCount GT maxSel>
<cfset apiAbort({
"OK": false,
"ERROR": "max_selection_exceeded",
"MESSAGE": "Too many selections under a modifier group.",
"DETAIL": "LineItemID #node.id# has #selCount# immediate children selected; max is #maxSel#."
})>
</cfif>
</cfif>
<!--- requires descendant selection --->
<cfif val(meta.requires) EQ 1>
<cfif NOT hasSelectedDescendant(graph, node.id)>
<cfset apiAbort({
"OK": false,
"ERROR": "required_selection_missing",
"MESSAGE": "A required modifier selection is missing.",
"DETAIL": "LineItemID #node.id# requires at least one descendant selection."
})>
</cfif>
</cfif>
</cfloop>
<!--- Submit: mark submitted + status 1 --->
<cfset queryExecute(
"
UPDATE Orders
SET
OrderStatusID = 1,
OrderSubmittedOn = ?,
OrderLastEditedOn = ?
WHERE OrderID = ?
",
[
{ value = now(), cfsqltype = "cf_sql_timestamp" },
{ value = now(), cfsqltype = "cf_sql_timestamp" },
{ value = OrderID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<cfset apiAbort({ "OK": true, "ERROR": "", "OrderID": OrderID, "MESSAGE": "submitted" })>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error submitting order",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>

View file

@ -1,137 +1,136 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<cfcontent type="application/json; charset=utf-8">
<cfscript>
/*
FILE: C:\lucee\tomcat\webapps\ROOT\biz.payfrit.com\api\servicepoints\list.cfm
MVP:
- PUBLIC (no login required)
- Reads BusinessID from JSON body
- Optional: onlyActive (default true)
- Returns hump-case JSON:
OK, ERROR, BusinessID, COUNT, ServicePoints
*/
function jsonString(val) {
// Lucee-safe string escaping:
// serializeJSON("abc") -> "\"abc\"" ; strip surrounding quotes
s = serializeJSON(toString(val));
return mid(s, 2, len(s) - 2);
}
function apiAbort(obj) {
writeOutput('{');
writeOutput('"OK":false');
if (structKeyExists(obj, "ERROR")) {
writeOutput(',"ERROR":"' & jsonString(obj.ERROR) & '"');
}
if (structKeyExists(obj, "MESSAGE")) {
writeOutput(',"MESSAGE":"' & jsonString(obj.MESSAGE) & '"');
}
if (structKeyExists(obj, "DETAIL")) {
writeOutput(',"DETAIL":"' & jsonString(obj.DETAIL) & '"');
}
writeOutput('}');
function apiAbort(payload) {
writeOutput(serializeJSON(payload));
abort;
}
function readJsonBody() {
raw = toString(getHttpRequestData().content);
if (isNull(raw) || len(trim(raw)) EQ 0) return {};
// Resolve BusinessID tolerant MVP way
bizId = 0;
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID)) {
bizId = int(request.BusinessID);
}
if (bizId LTE 0) {
try {
parsed = deserializeJSON(raw);
} catch(any e) {
apiAbort({ ERROR="bad_json", MESSAGE="Invalid JSON body" });
raw = toString(getHttpRequestData().content);
if (len(trim(raw))) {
body = deserializeJSON(raw);
if (isStruct(body) && structKeyExists(body, "BusinessID") && isNumeric(body.BusinessID)) {
bizId = int(body.BusinessID);
}
}
} catch (any e) {}
}
if (bizId LTE 0 && structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID)) {
bizId = int(url.BusinessID);
}
if (bizId LTE 0) {
apiAbort({ "OK": false, "ERROR": "missing_businessid", "DETAIL": "" });
}
try {
// Detect the correct business FK column in ServicePoints
qCols = queryExecute(
"
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ServicePoints'
",
[],
{ datasource: "payfrit" }
);
cols = [];
for (r in qCols) arrayAppend(cols, r.COLUMN_NAME);
// Candidates in preferred order (add more if needed)
candidates = [
"BusinessID",
"BusinessId",
"ServicePointBusinessID",
"ServicePointsBusinessID",
"Business_ID",
"Business",
"BusinessFk",
"BusinessFK"
];
bizCol = "";
// First: exact candidate match
for (c in candidates) {
for (colName in cols) {
if (lcase(colName) EQ lcase(c)) {
bizCol = colName;
break;
}
}
if (len(bizCol)) break;
}
if (!isStruct(parsed)) return {};
return parsed;
}
data = readJsonBody();
// Require BusinessID in JSON body for public MVP endpoint
if (!structKeyExists(data, "BusinessID") || !isNumeric(data.BusinessID) || int(data.BusinessID) LTE 0) {
apiAbort({ ERROR="missing_businessid", MESSAGE="Body must include numeric BusinessID" });
}
BusinessID = int(data.BusinessID);
// Default: only active service points unless onlyActive is explicitly false/0
onlyActive = true;
if (structKeyExists(data, "onlyActive")) {
if (isBoolean(data.onlyActive)) {
onlyActive = data.onlyActive;
} else if (isNumeric(data.onlyActive)) {
onlyActive = (int(data.onlyActive) EQ 1);
} else if (isSimpleValue(data.onlyActive)) {
onlyActive = (lcase(trim(toString(data.onlyActive))) EQ "true");
// Second: heuristic: any column containing "business" and "id"
if (!len(bizCol)) {
for (colName in cols) {
if (findNoCase("business", colName) && findNoCase("id", colName)) {
bizCol = colName;
break;
}
}
}
}
// Datasource (use existing app config)
dsn = "";
if (structKeyExists(application, "datasource") && len(trim(toString(application.datasource)))) {
dsn = trim(toString(application.datasource));
} else if (structKeyExists(application, "dsn") && len(trim(toString(application.dsn)))) {
dsn = trim(toString(application.dsn));
}
if (!len(dsn)) {
apiAbort({ ERROR="missing_datasource", MESSAGE="application.datasource is not set" });
if (!len(bizCol)) {
apiAbort({
"OK": false,
"ERROR": "schema_mismatch",
"DETAIL": "Could not find a BusinessID-like column in ServicePoints. Available columns: " & arrayToList(cols, ", ")
});
}
// Build SQL using detected column name (safe because it comes from INFORMATION_SCHEMA)
sql = "
SELECT
ServicePointID,
ServicePointName
FROM ServicePoints
WHERE #bizCol# = ?
ORDER BY ServicePointName
";
q = queryExecute(
sql,
[ { value: bizId, cfsqltype: "cf_sql_integer" } ],
{ datasource: "payfrit" }
);
servicePoints = [];
for (row in q) {
arrayAppend(servicePoints, {
"ServicePointID": row.ServicePointID,
"ServicePointName": row.ServicePointName
});
}
writeOutput(serializeJSON({
"OK": true,
"ERROR": "",
"DETAIL": "",
"BusinessID": bizId,
"BusinessColumn": bizCol,
"COUNT": arrayLen(servicePoints),
"ServicePoints": servicePoints
}));
} catch (any e) {
apiAbort({
"OK": false,
"ERROR": "db_error",
"DETAIL": e.message
});
}
</cfscript>
<cfquery name="q" datasource="#dsn#">
SELECT
ServicePointID,
ServicePointBusinessID,
ServicePointName,
ServicePointTypeID,
ServicePointCode,
ServicePointDescription,
ServicePointSortOrder,
ServicePointIsActive,
ServicePointCreatedAt,
ServicePointUpdatedAt
FROM ServicePoints
WHERE ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#BusinessID#">
<cfif onlyActive>
AND ServicePointIsActive = 1
</cfif>
ORDER BY ServicePointSortOrder, ServicePointName, ServicePointID
</cfquery>
<cfscript>
// Manual JSON output for exact key casing
writeOutput('{');
writeOutput('"OK":true');
writeOutput(',"ERROR":""');
writeOutput(',"BusinessID":' & BusinessID);
writeOutput(',"COUNT":' & q.recordCount);
writeOutput(',"ServicePoints":[');
for (i = 1; i LTE q.recordCount; i = i + 1) {
if (i GT 1) writeOutput(',');
writeOutput('{');
writeOutput('"ServicePointID":' & q.ServicePointID[i]);
writeOutput(',"BusinessID":' & q.ServicePointBusinessID[i]);
writeOutput(',"ServicePointName":"' & jsonString(q.ServicePointName[i]) & '"');
writeOutput(',"ServicePointTypeID":' & q.ServicePointTypeID[i]);
writeOutput(',"ServicePointCode":' & (isNull(q.ServicePointCode[i]) ? 'null' : '"' & jsonString(q.ServicePointCode[i]) & '"'));
writeOutput(',"ServicePointDescription":' & (isNull(q.ServicePointDescription[i]) ? 'null' : '"' & jsonString(q.ServicePointDescription[i]) & '"'));
writeOutput(',"ServicePointSortOrder":' & q.ServicePointSortOrder[i]);
writeOutput(',"ServicePointIsActive":' & q.ServicePointIsActive[i]);
writeOutput(',"ServicePointCreatedAt":"' & jsonString(q.ServicePointCreatedAt[i] & "") & '"');
writeOutput(',"ServicePointUpdatedAt":"' & jsonString(q.ServicePointUpdatedAt[i] & "") & '"');
writeOutput('}');
}
writeOutput(']}');
</cfscript>