This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/api/orders/setLineItem.cfm
John Pinkyfloyd 65f236268c Fix attachDefaultChildren to recurse through modifier groups
Default-checked items nested inside modifier groups were not being found
because the function only looked one level deep. Now it recurses through
all child items to find defaults at any depth.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-09 10:04:14 -07:00

615 lines
21 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="nextId" access="public" returntype="numeric" output="false">
<cfargument name="tableName" type="string" required="true">
<cfargument name="idField" type="string" required="true">
<cfset var q = queryTimed(
"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">
<!--- Store debug info in request scope for response --->
<cfif NOT structKeyExists(request, "attachDebug")>
<cfset request.attachDebug = []>
</cfif>
<cfset arrayAppend(request.attachDebug, "attachDefaultChildren: OrderID=#arguments.OrderID#, ParentLI=#arguments.ParentLineItemID#, ParentItemID=#arguments.ParentItemID#")>
<!--- Find ALL immediate children (to recurse into groups) --->
<cfset var qAllKids = queryTimed(
"
SELECT ID, Price, IsCheckedByDefault
FROM Items
WHERE ParentItemID = ?
AND IsActive = 1
ORDER BY SortOrder, ID
",
[ { value = arguments.ParentItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<!--- Also find children from templates linked to this item --->
<cfset var qTemplateKids = queryTimed(
"
SELECT i.ID, i.Price, i.IsCheckedByDefault
FROM lt_ItemID_TemplateItemID tl
INNER JOIN Items i ON i.ParentItemID = tl.TemplateItemID
WHERE tl.ItemID = ?
AND i.IsActive = 1
ORDER BY i.SortOrder, i.ID
",
[ { value = arguments.ParentItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset arrayAppend(request.attachDebug, " qAllKids=#qAllKids.recordCount# rows, qTemplateKids=#qTemplateKids.recordCount# rows")>
<!--- Process direct children --->
<cfloop query="qAllKids">
<cfif val(qAllKids.IsCheckedByDefault) EQ 1>
<!--- This child is default-checked, add it to the order --->
<cfset arrayAppend(request.attachDebug, " -> add default child: ItemID=#qAllKids.ID#")>
<cfset processDefaultChild(arguments.OrderID, arguments.ParentLineItemID, qAllKids.ID, qAllKids.Price)>
<cfelse>
<!--- This child is not default-checked, but recurse into it to find nested defaults --->
<cfset arrayAppend(request.attachDebug, " -> recurse into group: ItemID=#qAllKids.ID#")>
<cfset attachDefaultChildren(arguments.OrderID, arguments.ParentLineItemID, qAllKids.ID)>
</cfif>
</cfloop>
<!--- Process template children --->
<cfloop query="qTemplateKids">
<cfif val(qTemplateKids.IsCheckedByDefault) EQ 1>
<cfset arrayAppend(request.attachDebug, " -> add template child: ItemID=#qTemplateKids.ID#")>
<cfset processDefaultChild(arguments.OrderID, arguments.ParentLineItemID, qTemplateKids.ID, qTemplateKids.Price)>
<cfelse>
<cfset arrayAppend(request.attachDebug, " -> recurse into template group: ItemID=#qTemplateKids.ID#")>
<cfset attachDefaultChildren(arguments.OrderID, arguments.ParentLineItemID, qTemplateKids.ID)>
</cfif>
</cfloop>
</cffunction>
<cffunction name="processDefaultChild" access="public" returntype="void" output="false">
<cfargument name="OrderID" type="numeric" required="true">
<cfargument name="ParentLineItemID" type="numeric" required="true">
<cfargument name="ItemID" type="numeric" required="true">
<cfargument name="Price" type="numeric" required="true">
<!--- If existing, undelete; else insert new --->
<cfset var qExisting = queryTimed(
"
SELECT ID
FROM OrderLineItems
WHERE OrderID = ?
AND ParentOrderLineItemID = ?
AND ItemID = ?
LIMIT 1
",
[
{ value = arguments.OrderID, cfsqltype = "cf_sql_integer" },
{ value = arguments.ParentLineItemID, cfsqltype = "cf_sql_integer" },
{ value = arguments.ItemID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<cfif qExisting.recordCount GT 0>
<cfset queryTimed(
"
UPDATE OrderLineItems
SET IsDeleted = b'0'
WHERE ID = ?
",
[ { value = qExisting.ID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset attachDefaultChildren(arguments.OrderID, qExisting.ID, arguments.ItemID)>
<cfelse>
<cfset var NewLIID = nextId("OrderLineItems","ID")>
<cfset queryTimed(
"
INSERT INTO OrderLineItems (
ID,
ParentOrderLineItemID,
OrderID,
ItemID,
StatusID,
Price,
Quantity,
Remark,
IsDeleted,
AddedOn
) 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 = arguments.ItemID, cfsqltype = "cf_sql_integer" },
{ value = arguments.Price, cfsqltype = "cf_sql_decimal" },
{ value = now(), cfsqltype = "cf_sql_timestamp" }
],
{ datasource = "payfrit" }
)>
<cfset attachDefaultChildren(arguments.OrderID, NewLIID, arguments.ItemID)>
</cfif>
</cffunction>
<cffunction name="loadCartPayload" access="public" returntype="struct" output="false">
<cfargument name="OrderID" type="numeric" required="true">
<cfset var out = {}>
<cfset var qOrder = queryTimed(
"
SELECT
o.ID,
o.UUID,
o.UserID,
o.BusinessID,
o.DeliveryMultiplier,
o.OrderTypeID,
o.DeliveryFee,
o.StatusID,
o.AddressID,
o.PaymentID,
o.Remarks,
o.AddedOn,
o.LastEditedOn,
o.SubmittedOn,
o.ServicePointID,
sp.Name AS ServicePointName,
COALESCE(b.DeliveryFlatFee, 0) AS BusinessDeliveryFee
FROM Orders o
LEFT JOIN Businesses b ON b.ID = o.BusinessID
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
WHERE o.ID = ?
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": val(qOrder.ID),
"UUID": qOrder.UUID ?: "",
"UserID": val(qOrder.UserID),
"BusinessID": val(qOrder.BusinessID),
"DeliveryMultiplier": val(qOrder.DeliveryMultiplier),
"OrderTypeID": val(qOrder.OrderTypeID),
"DeliveryFee": val(qOrder.DeliveryFee),
"StatusID": val(qOrder.StatusID),
"AddressID": val(qOrder.AddressID),
"PaymentID": val(qOrder.PaymentID),
"Remarks": qOrder.Remarks ?: "",
"AddedOn": qOrder.AddedOn,
"LastEditedOn": qOrder.LastEditedOn,
"SubmittedOn": qOrder.SubmittedOn,
"ServicePointID": val(qOrder.ServicePointID),
"ServicePointName": qOrder.ServicePointName ?: "",
"BusinessDeliveryFee": val(qOrder.BusinessDeliveryFee)
}>
<cfset var qLI = queryTimed(
"
SELECT
oli.ID,
oli.ParentOrderLineItemID,
oli.OrderID,
oli.ItemID,
oli.StatusID,
oli.Price,
oli.Quantity,
oli.Remark,
oli.IsDeleted,
oli.AddedOn,
i.Name,
i.ParentItemID,
i.IsCheckedByDefault,
i.IsInvertedGroup,
parent.Name AS ItemParentName,
parent.IsInvertedGroup AS ParentIsInvertedGroup
FROM OrderLineItems oli
INNER JOIN Items i ON i.ID = oli.ItemID
LEFT JOIN Items parent ON parent.ID = i.ParentItemID
WHERE oli.OrderID = ?
ORDER BY oli.ID
",
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset var rows = []>
<cfloop query="qLI">
<cfset arrayAppend(rows, {
"OrderLineItemID": val(qLI.ID),
"ParentOrderLineItemID": val(qLI.ParentOrderLineItemId),
"OrderID": val(qLI.OrderID),
"ItemID": val(qLI.ItemID),
"StatusID": val(qLI.StatusID),
"Price": val(qLI.Price),
"Quantity": val(qLI.Quantity),
"Remark": qLI.Remark ?: "",
"IsDeleted": val(qLI.IsDeleted),
"AddedOn": qLI.AddedOn,
"Name": qLI.Name ?: "",
"ParentItemID": val(qLI.ParentItemID),
"ItemParentName": qLI.ItemParentName ?: "",
"IsCheckedByDefault": val(qLI.IsCheckedByDefault),
"IsInvertedGroup": val(qLI.IsInvertedGroup),
"ParentIsInvertedGroup": val(qLI.ParentIsInvertedGroup)
})>
</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 OrderLineItemID = val( structKeyExists(data,"OrderLineItemID") ? data.OrderLineItemID : 0 )>
<cfset OriginalItemID = structKeyExists(data,"ItemID") ? data.ItemID : 0>
<cfset ItemID = val(OriginalItemID)>
<!--- Decode virtual IDs from menu API (format: menuItemID * 100000 + realItemID) --->
<!--- If ItemID > 100000, extract the real ItemID --->
<cfset WasDecoded = false>
<cfif ItemID GT 100000>
<cfset WasDecoded = true>
<cfset ItemID = ItemID MOD 100000>
</cfif>
<!--- Store debug info for response --->
<cfset request.itemDebug = {
"OriginalItemID": OriginalItemID,
"ParsedItemID": val(OriginalItemID),
"WasDecoded": WasDecoded,
"FinalItemID": ItemID
}>
<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) : "">
<cfset ForceNew = false>
<cfif structKeyExists(data, "ForceNew")>
<cfset ForceNew = (data.ForceNew EQ true OR data.ForceNew EQ 1 OR (isSimpleValue(data.ForceNew) AND lcase(toString(data.ForceNew)) EQ "true"))>
</cfif>
<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 = queryTimed(
"
SELECT ID, Price, ParentItemID, IsActive
FROM Items
WHERE ID = ?
LIMIT 1
",
[ { value = ItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qItem.recordCount EQ 0 OR qItem.IsActive NEQ 1>
<cfset apiAbort({
"OK": false,
"ERROR": "bad_item",
"MESSAGE": "Item not found or inactive. Original=#OriginalItemID# Decoded=#ItemID# WasDecoded=#WasDecoded#",
"DETAIL": "",
"DEBUG_ITEM": request.itemDebug
})>
</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, 0 when deselecting --->
<cfif IsSelected>
<cfset Quantity = 1>
<cfelse>
<cfset Quantity = 0>
</cfif>
<!--- Check if this is an exclusive selection group (max = 1) --->
<!--- If so, deselect siblings when selecting a new item --->
<cfif IsSelected>
<!--- Get the parent line item's ItemID to check maxSel --->
<cfset qParentLI = queryTimed(
"SELECT ItemID FROM OrderLineItems WHERE ID = ? LIMIT 1",
[ { value = ParentLineItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qParentLI.recordCount GT 0>
<!--- Check if this parent item has maxSel = 1 (exclusive selection) --->
<cfset qParentItem = queryTimed(
"SELECT MaxNumSelectionReq FROM Items WHERE ID = ? LIMIT 1",
[ { value = qParentLI.ItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qParentItem.recordCount GT 0 AND val(qParentItem.MaxNumSelectionReq) EQ 1>
<!--- Exclusive selection: deselect all other siblings under this parent --->
<cfset queryTimed(
"
UPDATE OrderLineItems
SET IsDeleted = b'1'
WHERE OrderID = ?
AND ParentOrderLineItemID = ?
AND ItemID != ?
AND IsDeleted = b'0'
",
[
{ value = OrderID, cfsqltype = "cf_sql_integer" },
{ value = ParentLineItemID, cfsqltype = "cf_sql_integer" },
{ value = ItemID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
</cfif>
</cfif>
</cfif>
</cfif>
<!--- Find existing line item - by direct ID if provided, otherwise by order/parent/item --->
<cfif ForceNew>
<!--- ForceNew: Skip existing lookup, will always create new line item --->
<cfset qExisting = queryNew("ID", "integer")>
<cfelseif OrderLineItemID GT 0>
<!--- Direct lookup by OrderLineItemID --->
<cfset qExisting = queryTimed(
"
SELECT ID
FROM OrderLineItems
WHERE ID = ?
AND OrderID = ?
AND IsDeleted = 0
LIMIT 1
",
[
{ value = OrderLineItemID, cfsqltype = "cf_sql_integer" },
{ value = OrderID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<cfelse>
<cfset qExisting = queryTimed(
"
SELECT ID
FROM OrderLineItems
WHERE OrderID = ?
AND ParentOrderLineItemID = ?
AND ItemID = ?
AND IsDeleted = 0
LIMIT 1
",
[
{ value = OrderID, cfsqltype = "cf_sql_integer" },
{ value = ParentLineItemID, cfsqltype = "cf_sql_integer" },
{ value = ItemID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
</cfif>
<!--- Initialize debug array at start of processing --->
<cfset request.attachDebug = ["Flow start: qExisting.recordCount=#qExisting.recordCount#, IsSelected=#IsSelected#"]>
<cfif qExisting.recordCount GT 0>
<!--- Update existing --->
<cfset arrayAppend(request.attachDebug, "Path: update existing")>
<cfif IsSelected>
<cfset queryTimed(
"
UPDATE OrderLineItems
SET
IsDeleted = b'0',
Quantity = ?,
Price = ?,
Remark = ?,
StatusID = 0
WHERE ID = ?
",
[
{ value = Quantity, cfsqltype = "cf_sql_integer" },
{ value = qItem.Price, cfsqltype = "cf_sql_decimal" },
{ value = Remark, cfsqltype = "cf_sql_varchar", null = (len(trim(Remark)) EQ 0) },
{ value = qExisting.ID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<!--- Attach default children for this node (recursively) --->
<cfif NOT structKeyExists(request, "attachDebug")>
<cfset request.attachDebug = []>
</cfif>
<cfset arrayAppend(request.attachDebug, "BEFORE attachDefaultChildren call: OrderID=#OrderID#, LIID=#qExisting.ID#, ItemID=#ItemID#")>
<cfset attachDefaultChildren(OrderID, qExisting.ID, ItemID)>
<cfset arrayAppend(request.attachDebug, "AFTER attachDefaultChildren call")>
<cfelse>
<!--- Deselecting: for modifiers that are default-checked, keep with Quantity=0 --->
<!--- For root items or non-defaults, delete normally --->
<cfif ParentLineItemID GT 0>
<cfset qItemCheck = queryTimed(
"SELECT IsCheckedByDefault FROM Items WHERE ID = ? LIMIT 1",
[ { value = ItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qItemCheck.recordCount GT 0 AND val(qItemCheck.IsCheckedByDefault) EQ 1>
<!--- Default modifier: keep with Quantity=0 for "NO X" display --->
<cfset queryTimed(
"UPDATE OrderLineItems SET Quantity = 0 WHERE ID = ?",
[ { value = qExisting.ID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfelse>
<!--- Non-default modifier: mark as deleted --->
<cfset queryTimed(
"UPDATE OrderLineItems SET IsDeleted = b'1' WHERE ID = ?",
[ { value = qExisting.ID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
</cfif>
<cfelse>
<!--- Root item: always delete --->
<cfset queryTimed(
"UPDATE OrderLineItems SET IsDeleted = b'1' WHERE ID = ?",
[ { value = qExisting.ID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
</cfif>
</cfif>
<cfelse>
<!--- Insert new if selecting, otherwise no-op --->
<cfif IsSelected>
<cfset NewLIID = nextId("OrderLineItems","ID")>
<cfset queryTimed(
"
INSERT INTO OrderLineItems (
ID,
ParentOrderLineItemID,
OrderID,
ItemID,
StatusID,
Price,
Quantity,
Remark,
IsDeleted,
AddedOn
) 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.Price, cfsqltype = "cf_sql_decimal" },
{ value = (ParentLineItemID EQ 0 ? Quantity : 1), cfsqltype = "cf_sql_integer" },
{ value = 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 --->
<cftry>
<cfset queryTimed(
"UPDATE Orders SET LastEditedOn = ? WHERE ID = ?",
[
{ value = now(), cfsqltype = "cf_sql_timestamp" },
{ value = OrderID, cfsqltype = "cf_sql_integer" }
],
{ datasource = "payfrit" }
)>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "update_order_error",
"MESSAGE": "Error updating order timestamp: " & cfcatch.message,
"DETAIL": cfcatch.tagContext[1].template & ":" & cfcatch.tagContext[1].line
})>
</cfcatch>
</cftry>
<cftry>
<cfset payload = loadCartPayload(OrderID)>
<!--- Add debug info to response --->
<cfif structKeyExists(request, "attachDebug")>
<cfset payload["DEBUG_ATTACH"] = request.attachDebug>
</cfif>
<cfset apiAbort(payload)>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "load_cart_error",
"MESSAGE": "Error loading cart: " & cfcatch.message & " | " & (structKeyExists(cfcatch, "detail") ? cfcatch.detail : ""),
"DETAIL": cfcatch.tagContext[1].template & ":" & cfcatch.tagContext[1].line
})>
</cfcatch>
</cftry>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error setting line item: " & cfcatch.message & " | " & (structKeyExists(cfcatch, "detail") ? cfcatch.detail : "") & " | " & (structKeyExists(cfcatch, "sql") ? cfcatch.sql : ""),
"DETAIL": cfcatch.tagContext[1].template & ":" & cfcatch.tagContext[1].line
})>
</cfcatch>
</cftry>