Use val() instead of ?: for SUM results that may be empty strings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
175 lines
5.2 KiB
Text
175 lines
5.2 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfsetting enablecfoutputonly="true">
|
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
|
<cfheader name="Cache-Control" value="no-store">
|
|
|
|
<cfscript>
|
|
/**
|
|
* Order History API
|
|
* Returns list of completed/submitted orders for the authenticated user
|
|
*
|
|
* GET: ?limit=20&offset=0
|
|
*/
|
|
|
|
function apiAbort(required struct payload) {
|
|
writeOutput(serializeJSON(payload));
|
|
abort;
|
|
}
|
|
|
|
// Helper to get header value - use servlet request object (CGI scope doesn't expose custom HTTP headers in Lucee)
|
|
function getHeader(name) {
|
|
try {
|
|
req = getPageContext().getRequest();
|
|
val = req.getHeader(arguments.name);
|
|
if (!isNull(val)) return trim(val);
|
|
} catch (any e) {
|
|
// Fall back to CGI scope
|
|
k = "HTTP_" & ucase(reReplace(arguments.name, "[^A-Za-z0-9]", "_", "all"));
|
|
if (structKeyExists(cgi, k)) return trim(cgi[k]);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Get authenticated user - try request scope first, then do token lookup
|
|
userId = 0;
|
|
if (structKeyExists(request, "UserID") && isNumeric(request.UserID) && request.UserID > 0) {
|
|
userId = request.UserID;
|
|
} else {
|
|
userToken = getHeader("X-User-Token");
|
|
if (len(userToken)) {
|
|
try {
|
|
qTok = queryExecute(
|
|
"SELECT UserID FROM UserTokens WHERE Token = ? LIMIT 1",
|
|
[ { value = userToken, cfsqltype = "cf_sql_varchar" } ],
|
|
{ datasource = "payfrit" }
|
|
);
|
|
if (qTok.recordCount EQ 1) {
|
|
userId = qTok.UserID;
|
|
}
|
|
} catch (any e) { /* ignore */ }
|
|
}
|
|
}
|
|
|
|
if (userId <= 0) {
|
|
apiAbort({ "OK": false, "ERROR": "not_logged_in", "MESSAGE": "Authentication required" });
|
|
}
|
|
|
|
// Parse params
|
|
limit = val(url.limit ?: 20);
|
|
offset = val(url.offset ?: 0);
|
|
if (limit < 1) limit = 20;
|
|
if (limit > 100) limit = 100;
|
|
if (offset < 0) offset = 0;
|
|
|
|
try {
|
|
// Get orders for this user (exclude carts - status 0)
|
|
qOrders = queryExecute("
|
|
SELECT
|
|
o.OrderID,
|
|
o.OrderUUID,
|
|
o.OrderBusinessID,
|
|
o.OrderStatusID,
|
|
o.OrderTypeID,
|
|
o.OrderAddedOn,
|
|
o.OrderLastEditedOn,
|
|
b.BusinessName,
|
|
COALESCE(ot.tt_OrderTypeName, 'Unknown') as OrderTypeName
|
|
FROM Orders o
|
|
LEFT JOIN Businesses b ON b.BusinessID = o.OrderBusinessID
|
|
LEFT JOIN tt_OrderTypes ot ON ot.tt_OrderTypeID = o.OrderTypeID
|
|
WHERE o.OrderUserID = :userId
|
|
AND o.OrderStatusID > 0
|
|
ORDER BY o.OrderAddedOn DESC
|
|
LIMIT :limit OFFSET :offset
|
|
", {
|
|
userId: { value = userId, cfsqltype = "cf_sql_integer" },
|
|
limit: { value = limit, cfsqltype = "cf_sql_integer" },
|
|
offset: { value = offset, cfsqltype = "cf_sql_integer" }
|
|
});
|
|
|
|
// Get total count
|
|
qCount = queryExecute("
|
|
SELECT COUNT(*) as TotalCount
|
|
FROM Orders
|
|
WHERE OrderUserID = :userId
|
|
AND OrderStatusID > 0
|
|
", { userId: { value = userId, cfsqltype = "cf_sql_integer" } });
|
|
|
|
// Build orders array with item counts and totals
|
|
orders = [];
|
|
for (row in qOrders) {
|
|
// Get line item count and calculate total
|
|
qItems = queryExecute("
|
|
SELECT
|
|
COUNT(*) as ItemCount,
|
|
SUM(OrderLineItemQuantity * OrderLineItemPrice) as Subtotal
|
|
FROM OrderLineItems
|
|
WHERE OrderLineItemOrderID = :orderId
|
|
AND OrderLineItemParentOrderLineItemID = 0
|
|
AND (OrderLineItemIsDeleted = 0 OR OrderLineItemIsDeleted IS NULL)
|
|
", { orderId: { value = row.OrderID, cfsqltype = "cf_sql_integer" } });
|
|
|
|
itemCount = val(qItems.ItemCount);
|
|
subtotal = val(qItems.Subtotal);
|
|
tax = subtotal * 0.0875;
|
|
total = subtotal + tax;
|
|
|
|
// Get status text
|
|
statusText = "";
|
|
switch (row.OrderStatusID) {
|
|
case 1: statusText = "Submitted"; break;
|
|
case 2: statusText = "In Progress"; break;
|
|
case 3: statusText = "Ready"; break;
|
|
case 4: statusText = "Completed"; break;
|
|
case 5: statusText = "Cancelled"; break;
|
|
default: statusText = "Unknown";
|
|
}
|
|
|
|
// Safely format dates
|
|
createdAt = "";
|
|
try {
|
|
if (!isNull(row.OrderAddedOn) && len(trim(row.OrderAddedOn))) {
|
|
createdAt = dateTimeFormat(row.OrderAddedOn, "yyyy-mm-dd'T'HH:nn:ss");
|
|
}
|
|
} catch (any de) { createdAt = ""; }
|
|
|
|
completedAt = "";
|
|
try {
|
|
if (row.OrderStatusID >= 4 && !isNull(row.OrderLastEditedOn) && len(trim(row.OrderLastEditedOn))) {
|
|
completedAt = dateTimeFormat(row.OrderLastEditedOn, "yyyy-mm-dd'T'HH:nn:ss");
|
|
}
|
|
} catch (any de) { completedAt = ""; }
|
|
|
|
arrayAppend(orders, {
|
|
"OrderID": val(row.OrderID),
|
|
"OrderUUID": row.OrderUUID ?: "",
|
|
"BusinessID": val(row.OrderBusinessID),
|
|
"BusinessName": row.BusinessName ?: "Unknown",
|
|
"OrderTotal": round(val(total) * 100) / 100,
|
|
"OrderStatusID": val(row.OrderStatusID),
|
|
"StatusName": statusText,
|
|
"OrderTypeID": val(row.OrderTypeID),
|
|
"TypeName": row.OrderTypeName ?: "Unknown",
|
|
"ItemCount": val(itemCount),
|
|
"CreatedAt": createdAt,
|
|
"CompletedAt": completedAt
|
|
});
|
|
}
|
|
|
|
writeOutput(serializeJSON({
|
|
"OK": true,
|
|
"ORDERS": orders,
|
|
"TOTAL_COUNT": qCount.TotalCount
|
|
}));
|
|
|
|
} catch (any e) {
|
|
apiAbort({
|
|
"OK": false,
|
|
"ERROR": "server_error",
|
|
"MESSAGE": "Failed to load order history",
|
|
"DETAIL": e.message,
|
|
"DEBUG_LINE": e.tagContext[1].line ?: 0,
|
|
"DEBUG_TEMPLATE": e.tagContext[1].template ?: ""
|
|
});
|
|
}
|
|
</cfscript>
|