/** * Get Order Detail * Returns full order info including line items, customer details, and staff who worked on the order * * GET: ?OrderID=123 * POST: { OrderID: 123 } */ response = { "OK": false }; try { // Get OrderID from request orderID = 0; // Check URL params if (structKeyExists(url, "OrderID")) { orderID = val(url.OrderID); } // Check POST body if (orderID == 0) { requestBody = toString(getHttpRequestData().content); if (len(requestBody)) { requestData = deserializeJSON(requestBody); if (structKeyExists(requestData, "OrderID")) { orderID = val(requestData.OrderID); } } } if (orderID == 0) { response["ERROR"] = "missing_order_id"; response["MESSAGE"] = "OrderID is required"; writeOutput(serializeJSON(response)); abort; } // Get order details qOrder = queryTimed(" SELECT o.OrderID, o.OrderBusinessID, o.OrderUserID, o.OrderServicePointID, o.OrderStatusID, o.OrderTypeID, o.OrderRemarks, o.OrderAddedOn, o.OrderLastEditedOn, o.OrderSubmittedOn, o.OrderTipAmount, u.UserFirstName, u.UserLastName, u.UserContactNumber, u.UserEmailAddress, sp.ServicePointName, sp.ServicePointTypeID, b.BusinessName, b.BusinessTaxRate FROM Orders o LEFT JOIN Users u ON u.UserID = o.OrderUserID LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID LEFT JOIN Businesses b ON b.BusinessID = o.OrderBusinessID WHERE o.OrderID = :orderID ", { orderID: orderID }); if (qOrder.recordCount == 0) { response["ERROR"] = "order_not_found"; response["MESSAGE"] = "Order not found"; writeOutput(serializeJSON(response)); abort; } // Get line items (excluding deleted items) qItems = queryTimed(" SELECT oli.OrderLineItemID, oli.OrderLineItemItemID, oli.OrderLineItemParentOrderLineItemID, oli.OrderLineItemQuantity, oli.OrderLineItemPrice, oli.OrderLineItemRemark, i.ItemName, i.ItemPrice, i.ItemIsCheckedByDefault FROM OrderLineItems oli INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID WHERE oli.OrderLineItemOrderID = :orderID AND oli.OrderLineItemIsDeleted = 0 ORDER BY oli.OrderLineItemID ", { orderID: orderID }); // Build line items array with parent-child structure lineItems = []; itemsById = {}; // First pass: create all items (use bracket notation to preserve key casing) for (row in qItems) { item = structNew("ordered"); item["LineItemID"] = val(row.OrderLineItemID); item["ItemID"] = val(row.OrderLineItemItemID); item["ParentLineItemID"] = val(row.OrderLineItemParentOrderLineItemID); item["ItemName"] = row.ItemName ?: ""; item["Quantity"] = val(row.OrderLineItemQuantity); item["UnitPrice"] = val(row.OrderLineItemPrice); item["Remarks"] = row.OrderLineItemRemark ?: ""; item["IsDefault"] = (val(row.ItemIsCheckedByDefault) == 1); item["Modifiers"] = []; itemsById[row.OrderLineItemID] = item; } // Second pass: build hierarchy for (row in qItems) { item = itemsById[row.OrderLineItemID]; parentID = row.OrderLineItemParentOrderLineItemID; if (parentID > 0 && structKeyExists(itemsById, parentID)) { // This is a modifier - add to parent (use bracket notation) arrayAppend(itemsById[parentID]["Modifiers"], item); } else { // This is a top-level item arrayAppend(lineItems, item); } } // Calculate subtotal from root line items (use bracket notation) subtotal = 0; for (item in lineItems) { itemTotal = item["UnitPrice"] * item["Quantity"]; // Add modifier prices for (mod in item["Modifiers"]) { itemTotal += mod["UnitPrice"] * mod["Quantity"]; } subtotal += itemTotal; } // Calculate tax using business tax rate or default 8.25% taxRate = isNumeric(qOrder.BusinessTaxRate) && qOrder.BusinessTaxRate > 0 ? qOrder.BusinessTaxRate : 0.0825; tax = subtotal * taxRate; // Get tip from order tip = isNumeric(qOrder.OrderTipAmount) ? qOrder.OrderTipAmount : 0; // Calculate total total = subtotal + tax + tip; // Get staff who worked on this order (LEFT JOIN instead of correlated subquery) qStaff = queryTimed(" SELECT DISTINCT u.UserID, u.UserFirstName, r.TaskRatingAccessToken AS RatingToken FROM Tasks t INNER JOIN Users u ON u.UserID = t.TaskClaimedByUserID LEFT JOIN TaskRatings r ON r.TaskRatingTaskID = t.TaskID AND r.TaskRatingForUserID = u.UserID AND r.TaskRatingDirection = 'customer_rates_worker' AND r.TaskRatingCompletedOn IS NULL AND r.TaskRatingExpiresOn > NOW() WHERE t.TaskOrderID = :orderID AND t.TaskClaimedByUserID > 0 ", { orderID: orderID }); // Build staff array with avatar URLs and rating tokens (use ordered structs) staff = []; for (row in qStaff) { staffMember = structNew("ordered"); staffMember["UserID"] = row.UserID; staffMember["FirstName"] = row.UserFirstName; staffMember["AvatarUrl"] = "https://biz.payfrit.com/uploads/users/" & row.UserID & ".jpg"; staffMember["RatingToken"] = row.RatingToken ?: ""; arrayAppend(staff, staffMember); } // Build response (use ordered structs to preserve key casing) customer = structNew("ordered"); customer["UserID"] = qOrder.OrderUserID; customer["FirstName"] = qOrder.UserFirstName; customer["LastName"] = qOrder.UserLastName; customer["Phone"] = qOrder.UserContactNumber; customer["Email"] = qOrder.UserEmailAddress; servicePoint = structNew("ordered"); servicePoint["ServicePointID"] = qOrder.OrderServicePointID; servicePoint["Name"] = qOrder.ServicePointName; servicePoint["TypeID"] = qOrder.ServicePointTypeID; order = structNew("ordered"); order["OrderID"] = qOrder.OrderID; order["BusinessID"] = qOrder.OrderBusinessID; order["BusinessName"] = qOrder.BusinessName ?: ""; order["Status"] = qOrder.OrderStatusID; order["StatusText"] = getStatusText(qOrder.OrderStatusID); order["OrderTypeID"] = qOrder.OrderTypeID ?: 0; order["OrderTypeName"] = getOrderTypeName(qOrder.OrderTypeID ?: 0); order["Subtotal"] = subtotal; order["Tax"] = tax; order["Tip"] = tip; order["Total"] = total; order["Notes"] = qOrder.OrderRemarks; order["CreatedOn"] = dateTimeFormat(qOrder.OrderAddedOn, "yyyy-mm-dd HH:nn:ss"); order["SubmittedOn"] = len(qOrder.OrderSubmittedOn) ? dateTimeFormat(qOrder.OrderSubmittedOn, "yyyy-mm-dd HH:nn:ss") : ""; order["UpdatedOn"] = len(qOrder.OrderLastEditedOn) ? dateTimeFormat(qOrder.OrderLastEditedOn, "yyyy-mm-dd HH:nn:ss") : ""; order["Customer"] = customer; order["ServicePoint"] = servicePoint; order["LineItems"] = lineItems; order["Staff"] = staff; response["OK"] = true; response["ORDER"] = order; } catch (any e) { response["ERROR"] = "server_error"; response["MESSAGE"] = e.message; } logPerf(); writeOutput(serializeJSON(response)); // Helper functions function getStatusText(status) { switch (status) { case 0: return "Cart"; case 1: return "Submitted"; case 2: return "In Progress"; case 3: return "Ready"; case 4: return "On the Way"; case 5: return "Complete"; case 6: return "Cancelled"; case 7: return "Deleted"; default: return "Unknown"; } } function getOrderTypeName(orderType) { switch (orderType) { case 1: return "Dine-in"; case 2: return "Takeaway"; case 3: return "Delivery"; default: return "Unknown"; } }