From 363964d9c62ccd060e601122a2c2c15ff10f0fd2 Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Sun, 28 Dec 2025 22:34:01 -0800 Subject: [PATCH] checkpoint --- Application.cfm | 2 +- api/Application.cfm | 119 ++++++---- api/New folder/delete.cfm | 50 ----- api/New folder/get.cfm | 67 ------ api/New folder/list.cfm | 82 ------- api/New folder/save.cfm | 136 ----------- api/auth/login.cfm | 114 ++++++++++ api/businesses/list.cfm | 109 ++++----- api/menu/items.cfm | 98 ++++++++ api/orders/getCart.cfm | 135 +++++++++++ api/orders/getOrCreateCart.cfm | 263 ++++++++++++++++++++++ api/orders/setLineItem.cfm | 399 +++++++++++++++++++++++++++++++++ api/orders/submit.cfm | 261 +++++++++++++++++++++ api/servicepoints/list.cfm | 243 ++++++++++---------- 14 files changed, 1513 insertions(+), 565 deletions(-) delete mode 100644 api/New folder/delete.cfm delete mode 100644 api/New folder/get.cfm delete mode 100644 api/New folder/list.cfm delete mode 100644 api/New folder/save.cfm create mode 100644 api/auth/login.cfm create mode 100644 api/menu/items.cfm create mode 100644 api/orders/getCart.cfm create mode 100644 api/orders/getOrCreateCart.cfm create mode 100644 api/orders/setLineItem.cfm create mode 100644 api/orders/submit.cfm diff --git a/Application.cfm b/Application.cfm index bc4d830..7a6ef49 100644 --- a/Application.cfm +++ b/Application.cfm @@ -5,7 +5,7 @@ sessiontimeout="#CreateTimeSpan(0,0,30,0)#" clientstorage="cookie"> - + diff --git a/api/Application.cfm b/api/Application.cfm index b71632e..19daefe 100644 --- a/api/Application.cfm +++ b/api/Application.cfm @@ -2,53 +2,70 @@ - - - - - + + -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" }); } } diff --git a/api/New folder/delete.cfm b/api/New folder/delete.cfm deleted file mode 100644 index 3c5e52e..0000000 --- a/api/New folder/delete.cfm +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - -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); - - - - UPDATE ServicePoints - SET IsActive = 0 - WHERE ServicePointID = - AND BusinessID = - - - - SELECT ServicePointID - FROM ServicePoints - WHERE ServicePointID = - AND BusinessID = - LIMIT 1 - - - - #serializeJSON({ OK=false, ERROR="not_found" })# - - - -#serializeJSON({ OK=true, ERROR="", ServicePointID=spid })# diff --git a/api/New folder/get.cfm b/api/New folder/get.cfm deleted file mode 100644 index 2167619..0000000 --- a/api/New folder/get.cfm +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - -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); - - - - SELECT - ServicePointID, - BusinessID, - ServicePointName, - ServicePointTypeID, - ServicePointCode, - Description, - SortOrder, - IsActive, - CreatedAt, - UpdatedAt - FROM ServicePoints - WHERE ServicePointID = - AND BusinessID = - LIMIT 1 - - - - #serializeJSON({ OK=false, ERROR="not_found" })# - - - - - -#serializeJSON({ OK=true, ERROR="", SERVICEPOINT=sp })# diff --git a/api/New folder/list.cfm b/api/New folder/list.cfm deleted file mode 100644 index 8854425..0000000 --- a/api/New folder/list.cfm +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - -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"); - } -} - - - - SELECT - ServicePointID, - BusinessID, - ServicePointName, - ServicePointTypeID, - ServicePointCode, - Description, - SortOrder, - IsActive, - CreatedAt, - UpdatedAt - FROM ServicePoints - WHERE BusinessID = - - AND IsActive = 1 - - ORDER BY SortOrder, ServicePointName, ServicePointID - - - - - - - -#serializeJSON({ OK=true, ERROR="", BUSINESSID=(request.BusinessID & ""), COUNT=arrayLen(sps), SERVICEPOINTS=sps })# diff --git a/api/New folder/save.cfm b/api/New folder/save.cfm deleted file mode 100644 index 3b7e839..0000000 --- a/api/New folder/save.cfm +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - -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); -} - - - - - UPDATE ServicePoints - SET - ServicePointName = , - ServicePointTypeID = , - ServicePointCode = , - Description = , - SortOrder = , - IsActive = - WHERE ServicePointID = - AND BusinessID = - - - - SELECT ServicePointID - FROM ServicePoints - WHERE ServicePointID = - AND BusinessID = - LIMIT 1 - - - - #serializeJSON({ OK=false, ERROR="not_found" })# - - - - - INSERT INTO ServicePoints ( - BusinessID, - ServicePointName, - ServicePointTypeID, - ServicePointCode, - Description, - SortOrder, - IsActive - ) VALUES ( - , - , - , - , - , - , - - ) - - - - SELECT LAST_INSERT_ID() AS ServicePointID - - - - - - SELECT - ServicePointID, - BusinessID, - ServicePointName, - ServicePointTypeID, - ServicePointCode, - Description, - SortOrder, - IsActive, - CreatedAt, - UpdatedAt - FROM ServicePoints - WHERE ServicePointID = - AND BusinessID = - LIMIT 1 - - - - -#serializeJSON({ OK=true, ERROR="", SERVICEPOINT=sp })# diff --git a/api/auth/login.cfm b/api/auth/login.cfm new file mode 100644 index 0000000..7bcd638 --- /dev/null +++ b/api/auth/login.cfm @@ -0,0 +1,114 @@ + + + + +/* + 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 + }); +} + diff --git a/api/businesses/list.cfm b/api/businesses/list.cfm index b9a4d2f..6ae3abc 100644 --- a/api/businesses/list.cfm +++ b/api/businesses/list.cfm @@ -1,81 +1,52 @@ + + + -/* - 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(']}'); diff --git a/api/menu/items.cfm b/api/menu/items.cfm new file mode 100644 index 0000000..3865f1d --- /dev/null +++ b/api/menu/items.cfm @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + #serializeJSON(arguments.payload)# + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/orders/getCart.cfm b/api/orders/getCart.cfm new file mode 100644 index 0000000..6333b91 --- /dev/null +++ b/api/orders/getCart.cfm @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + #serializeJSON(arguments.payload)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/orders/getOrCreateCart.cfm b/api/orders/getOrCreateCart.cfm new file mode 100644 index 0000000..ec753cc --- /dev/null +++ b/api/orders/getOrCreateCart.cfm @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + #serializeJSON(arguments.payload)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/orders/setLineItem.cfm b/api/orders/setLineItem.cfm new file mode 100644 index 0000000..0c6d332 --- /dev/null +++ b/api/orders/setLineItem.cfm @@ -0,0 +1,399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + #serializeJSON(arguments.payload)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.", "DETAIL": "" })> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/orders/submit.cfm b/api/orders/submit.cfm new file mode 100644 index 0000000..1274cf8 --- /dev/null +++ b/api/orders/submit.cfm @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + #serializeJSON(arguments.payload)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/servicepoints/list.cfm b/api/servicepoints/list.cfm index a198886..d505135 100644 --- a/api/servicepoints/list.cfm +++ b/api/servicepoints/list.cfm @@ -1,137 +1,136 @@ - - - + -/* - 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 + }); } - - - SELECT - ServicePointID, - ServicePointBusinessID, - ServicePointName, - ServicePointTypeID, - ServicePointCode, - ServicePointDescription, - ServicePointSortOrder, - ServicePointIsActive, - ServicePointCreatedAt, - ServicePointUpdatedAt - FROM ServicePoints - WHERE ServicePointBusinessID = - - AND ServicePointIsActive = 1 - - ORDER BY ServicePointSortOrder, ServicePointName, ServicePointID - - - -// 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(']}'); -