payfrit-works/api/orders/submit.cfm
John Mizerek d8d7efe056 Add user account APIs and fix Lucee header handling
- Add avatar.cfm: GET/POST for user profile photos with multi-extension support
- Add profile.cfm: GET/POST for user profile (name, email, phone)
- Add history.cfm: Order history endpoint with pagination
- Add addresses/list.cfm and add.cfm: Delivery address management
- Add setOrderType.cfm: Set delivery/takeaway type on orders
- Add checkToken.cfm: Debug endpoint for token validation
- Fix headerValue() in Application.cfm to use servlet request object
  (Lucee CGI scope doesn't expose custom HTTP headers like X-User-Token)
- Update public allowlist for new endpoints
- Add privacy.html page

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 20:01:07 -08:00

276 lines
8.7 KiB
Text

<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 o.OrderID, o.OrderStatusID, o.OrderTypeID, o.OrderBusinessID, o.OrderServicePointID,
sp.ServicePointName
FROM Orders o
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
WHERE o.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>
<!--- OrderTypeID: 1=dine-in, 2=takeaway, 3=delivery (0=undecided is invalid for submit) --->
<cfif qOrder.OrderTypeID LT 1 OR qOrder.OrderTypeID GT 3>
<cfset apiAbort({ "OK": false, "ERROR": "bad_type", "MESSAGE": "Order type must be set before submitting (1=dine-in, 2=takeaway, 3=delivery).", "DETAIL": "" })>
</cfif>
<!--- Delivery orders require an address --->
<cfif qOrder.OrderTypeID EQ 3>
<cfset qAddr = queryExecute(
"SELECT OrderAddressID FROM Orders WHERE OrderID = ? LIMIT 1",
[ { value = OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qAddr.OrderAddressID LTE 0 OR isNull(qAddr.OrderAddressID)>
<cfset apiAbort({ "OK": false, "ERROR": "missing_address", "MESSAGE": "Delivery orders require a delivery address.", "DETAIL": "" })>
</cfif>
</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>