/** * 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 = queryExecute(" SELECT o.ID, o.BusinessID, o.UserID, o.ServicePointID, o.StatusID, o.OrderTypeID, o.Remarks, o.AddedOn, o.LastEditedOn, o.SubmittedOn, o.TipAmount, u.UserFirstName AS FirstName, u.UserLastName AS LastName, u.UserContactNumber AS ContactNumber, u.UserEmailAddress AS EmailAddress, sp.ServicePointName AS Name, sp.ServicePointTypeID AS TypeID, b.BusinessName AS BizName, b.BusinessTaxRate AS TaxRate FROM Orders o LEFT JOIN Users u ON u.UserID = o.UserID LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.ServicePointID LEFT JOIN Businesses b ON b.BusinessID = 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 = queryExecute(" 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 = queryExecute(" SELECT DISTINCT u.UserID AS ID, u.UserFirstName AS FirstName, (SELECT r.AccessToken FROM TaskRatings r INNER JOIN Tasks t2 ON t2.ID = r.TaskID WHERE t2.OrderID = :orderID AND r.ForUserID = u.UserID 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.UserID = 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"] = "https://biz.payfrit.com/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"; } }