/** * 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.ID, o.BusinessID, o.UserID, o.ServicePointID, o.StatusID, o.OrderTypeID, o.Remarks, o.AddedOn, o.LastEditedOn, o.SubmittedOn, o.TipAmount, u.FirstName, u.LastName, u.ContactNumber, u.EmailAddress, sp.Name AS Name, sp.TypeID AS TypeID, b.Name AS BizName, b.TaxRate FROM Orders o LEFT JOIN Users u ON u.ID = o.UserID LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID LEFT JOIN Businesses b ON b.ID = o.BusinessID WHERE o.ID = :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.ID, oli.ItemID, oli.ParentOrderLineItemID, oli.Quantity, oli.Price, oli.Remark, i.Name, i.Price, i.IsCheckedByDefault FROM OrderLineItems oli INNER JOIN Items i ON i.ID = oli.ItemID WHERE oli.OrderID = :orderID AND oli.IsDeleted = 0 ORDER BY oli.ID ", { 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.ID); item["ItemID"] = val(row.ID); item["ParentLineItemID"] = val(row.ParentOrderLineItemID); item["Name"] = row.Name ?: ""; item["Quantity"] = val(row.Quantity); item["UnitPrice"] = val(row.Price); item["Remarks"] = row.Remark ?: ""; item["IsDefault"] = (val(row.IsCheckedByDefault) == 1); item["Modifiers"] = []; itemsById[row.ID] = item; } // Second pass: build hierarchy for (row in qItems) { item = itemsById[row.ID]; parentID = row.ParentOrderLineItemID; 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.TaxRate) && qOrder.TaxRate > 0 ? qOrder.TaxRate : 0.0825; tax = subtotal * taxRate; // Get tip from order tip = isNumeric(qOrder.TipAmount) ? qOrder.TipAmount : 0; // Calculate total total = subtotal + tax + tip; // Get staff who worked on this order (from Tasks table) with pending rating tokens qStaff = queryTimed(" SELECT DISTINCT u.ID, u.FirstName, (SELECT r.AccessToken FROM TaskRatings r INNER JOIN Tasks t2 ON t2.ID = r.TaskID WHERE t2.OrderID = :orderID AND r.ForUserID = u.ID AND r.Direction = 'customer_rates_worker' AND r.CompletedOn IS NULL AND r.ExpiresOn > NOW() LIMIT 1) AS RatingToken FROM Tasks t INNER JOIN Users u ON u.ID = t.ClaimedByUserID WHERE t.OrderID = :orderID AND t.ClaimedByUserID > 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.ID; staffMember["FirstName"] = row.FirstName; staffMember["AvatarUrl"] = application.baseUrl & "/uploads/users/" & row.ID & ".jpg"; staffMember["RatingToken"] = row.RatingToken ?: ""; arrayAppend(staff, staffMember); } // Build response (use ordered structs to preserve key casing) customer = structNew("ordered"); customer["UserID"] = qOrder.UserID; customer["FirstName"] = qOrder.FirstName; customer["LastName"] = qOrder.LastName; customer["Phone"] = qOrder.ContactNumber; customer["Email"] = qOrder.EmailAddress; servicePoint = structNew("ordered"); servicePoint["ServicePointID"] = qOrder.ServicePointID; servicePoint["Name"] = qOrder.Name; servicePoint["TypeID"] = qOrder.TypeID; order = structNew("ordered"); order["OrderID"] = qOrder.ID; order["BusinessID"] = qOrder.BusinessID; order["Name"] = qOrder.Name ?: ""; order["Status"] = qOrder.StatusID; order["StatusText"] = getStatusText(qOrder.StatusID); 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.Remarks; order["CreatedOn"] = dateTimeFormat(qOrder.AddedOn, "yyyy-mm-dd HH:nn:ss"); order["SubmittedOn"] = len(qOrder.SubmittedOn) ? dateTimeFormat(qOrder.SubmittedOn, "yyyy-mm-dd HH:nn:ss") : ""; order["UpdatedOn"] = len(qOrder.LastEditedOn) ? dateTimeFormat(qOrder.LastEditedOn, "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; } 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"; } }