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 f57d249fee Add ParentIsInvertedGroup to cart line items
Child items need to know if their parent group is inverted for proper
display logic (showing "NO X" instead of listing selected items).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-08 23:00:49 -07:00

563 lines
18 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">
<!--- Find immediate children where checked by default --->
<cfset var qKids = queryTimed(
"
SELECT ID, Price
FROM Items
WHERE ParentItemID = ?
AND IsCheckedByDefault = 1
AND IsActive = 1
ORDER BY SortOrder, ID
",
[ { value = arguments.ParentItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<!--- Also find default children from templates linked to this item --->
<cfset var qTemplateKids = queryTimed(
"
SELECT i.ID, i.Price
FROM lt_ItemID_TemplateItemID tl
INNER JOIN Items i ON i.ParentItemID = tl.TemplateItemID
WHERE tl.ItemID = ?
AND i.IsCheckedByDefault = 1
AND i.IsActive = 1
ORDER BY i.SortOrder, i.ID
",
[ { value = arguments.ParentItemID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<!--- 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#")>
<cfset arrayAppend(request.attachDebug, " qKids=#qKids.recordCount# rows, qTemplateKids=#qTemplateKids.recordCount# rows")>
<!--- Process direct children --->
<cfloop query="qKids">
<cfset arrayAppend(request.attachDebug, " -> direct child: ItemID=#qKids.ID#")>
<cfset processDefaultChild(arguments.OrderID, arguments.ParentLineItemID, qKids.ID, qKids.Price)>
</cfloop>
<!--- Process template children --->
<cfloop query="qTemplateKids">
<cfset arrayAppend(request.attachDebug, " -> template child: ItemID=#qTemplateKids.ID#")>
<cfset processDefaultChild(arguments.OrderID, arguments.ParentLineItemID, qTemplateKids.ID, qTemplateKids.Price)>
</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 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 order, parent LI, item) - unless ForceNew is set --->
<cfif ForceNew>
<!--- ForceNew: Skip existing lookup, will always create new line item --->
<cfset qExisting = queryNew("ID", "integer")>
<cfelse>
<cfset qExisting = queryTimed(
"
SELECT ID
FROM OrderLineItems
WHERE OrderID = ?
AND ParentOrderLineItemID = ?
AND ItemID = ?
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>
<cfset queryTimed(
"
UPDATE OrderLineItems
SET IsDeleted = b'1'
WHERE ID = ?
",
[ { value = qExisting.ID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
</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>