payfrit-api/api/orders/submit.php
John Mizerek 1f81d98c52 Initial PHP API migration from CFML
Complete port of all 163 API endpoints from Lucee/CFML to PHP 8.3.
Shared helpers in api/helpers.php (DB, auth, request/response, security).
PDO prepared statements throughout. Same JSON response shapes as CFML.
2026-03-14 14:26:59 -07:00

203 lines
7.3 KiB
PHP

<?php
require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../grants/_grantUtils.php';
runAuth();
/**
* Submit Order (card payment flow)
* POST: { OrderID: int }
* Validates cart, checks grant, validates modifier selections, then marks as submitted.
*/
function buildLineItemsGraph(int $OrderID): array {
$out = ['items' => [], 'children' => [], 'itemMeta' => []];
$qLI = queryTimed(
"SELECT ID, ParentOrderLineItemID, ItemID, IsDeleted FROM OrderLineItems WHERE OrderID = ? ORDER BY ID",
[$OrderID]
);
if (empty($qLI)) return $out;
$itemIds = [];
foreach ($qLI as $row) {
$id = (int) $row['ID'];
$parentId = (int) $row['ParentOrderLineItemID'];
$out['items'][$id] = [
'id' => $id,
'parentId' => $parentId,
'itemId' => (int) $row['ItemID'],
'isDeleted' => (bool) $row['IsDeleted'],
];
$out['children'][$parentId][] = $id;
$itemIds[] = (int) $row['ItemID'];
}
$uniq = array_unique($itemIds);
if (!empty($uniq)) {
$placeholders = implode(',', array_fill(0, count($uniq), '?'));
$qMeta = queryTimed(
"SELECT ID, RequiresChildSelection, MaxNumSelectionReq FROM Items WHERE ID IN ({$placeholders})",
array_values($uniq)
);
foreach ($qMeta as $row) {
$out['itemMeta'][(int) $row['ID']] = [
'requires' => (int) $row['RequiresChildSelection'],
'maxSel' => (int) $row['MaxNumSelectionReq'],
];
}
}
return $out;
}
function hasSelectedDescendant(array $graph, int $lineItemId): bool {
$stack = $graph['children'][$lineItemId] ?? [];
while (!empty($stack)) {
$id = array_pop($stack);
if (isset($graph['items'][$id])) {
$node = $graph['items'][$id];
if (!$node['isDeleted']) return true;
foreach ($graph['children'][$id] ?? [] as $kidId) {
$stack[] = $kidId;
}
}
}
return false;
}
// --- Main logic ---
$data = readJsonBody();
$OrderID = (int) ($data['OrderID'] ?? 0);
if ($OrderID <= 0) {
apiAbort(['OK' => false, 'ERROR' => 'missing_orderid', 'MESSAGE' => 'OrderID is required.']);
}
try {
$qOrder = queryOne(
"SELECT ID, UserID, StatusID, OrderTypeID, BusinessID, ServicePointID, GrantID, GrantOwnerBusinessID
FROM Orders WHERE ID = ? LIMIT 1",
[$OrderID]
);
if (!$qOrder) {
apiAbort(['OK' => false, 'ERROR' => 'not_found', 'MESSAGE' => 'Order not found.']);
}
if ((int) $qOrder['StatusID'] !== 0) {
apiAbort(['OK' => false, 'ERROR' => 'bad_state', 'MESSAGE' => 'Order is not in cart state.']);
}
$orderTypeID = (int) $qOrder['OrderTypeID'];
if ($orderTypeID < 1 || $orderTypeID > 3) {
apiAbort(['OK' => false, 'ERROR' => 'bad_type', 'MESSAGE' => 'Order type must be set before submitting (1=dine-in, 2=takeaway, 3=delivery).']);
}
// Delivery requires address
if ($orderTypeID === 3) {
$qAddr = queryOne("SELECT AddressID FROM Orders WHERE ID = ? LIMIT 1", [$OrderID]);
if (!$qAddr || (int) ($qAddr['AddressID'] ?? 0) <= 0) {
apiAbort(['OK' => false, 'ERROR' => 'missing_address', 'MESSAGE' => 'Delivery orders require a delivery address.']);
}
}
// Re-validate grant
$grantID = (int) ($qOrder['GrantID'] ?? 0);
if ($grantID > 0) {
$qGrantCheck = queryOne(
"SELECT StatusID, TimePolicyType, TimePolicyData FROM ServicePointGrants WHERE ID = ? LIMIT 1",
[$grantID]
);
if (!$qGrantCheck || (int) $qGrantCheck['StatusID'] !== 1) {
apiAbort(['OK' => false, 'ERROR' => 'grant_revoked', 'MESSAGE' => 'Access to this service point has been revoked.']);
}
if (!isGrantTimeActive($qGrantCheck['TimePolicyType'], $qGrantCheck['TimePolicyData'])) {
apiAbort(['OK' => false, 'ERROR' => 'grant_time_expired', 'MESSAGE' => 'Service point access is no longer available at this time.']);
}
}
// Must have at least one root item
$qRoots = queryOne(
"SELECT COUNT(*) AS Cnt FROM OrderLineItems WHERE OrderID = ? AND ParentOrderLineItemID = 0 AND IsDeleted = 0",
[$OrderID]
);
if ((int) ($qRoots['Cnt'] ?? 0) <= 0) {
apiAbort(['OK' => false, 'ERROR' => 'empty_order', 'MESSAGE' => 'Order has no items.']);
}
// Validate modifier selections
$graph = buildLineItemsGraph($OrderID);
foreach ($graph['items'] as $node) {
if ($node['isDeleted']) continue;
$meta = $graph['itemMeta'][$node['itemId']] ?? ['requires' => 0, 'maxSel' => 0];
// Max immediate selections
if ($meta['maxSel'] > 0) {
$selCount = 0;
foreach ($graph['children'][$node['id']] ?? [] as $kidId) {
if (isset($graph['items'][$kidId]) && !$graph['items'][$kidId]['isDeleted']) {
$selCount++;
}
}
if ($selCount > $meta['maxSel']) {
apiAbort([
'OK' => false, 'ERROR' => 'max_selection_exceeded',
'MESSAGE' => 'Too many selections under a modifier group.',
'DETAIL' => "LineItemID {$node['id']} has {$selCount} immediate children selected; max is {$meta['maxSel']}.",
]);
}
}
// Requires descendant
if ($meta['requires'] === 1 && !hasSelectedDescendant($graph, $node['id'])) {
apiAbort([
'OK' => false, 'ERROR' => 'required_selection_missing',
'MESSAGE' => 'A required modifier selection is missing.',
'DETAIL' => "LineItemID {$node['id']} requires at least one descendant selection.",
]);
}
}
// Tab-aware submit
$tabID = 0;
$qOrderTab = queryOne("SELECT TabID FROM Orders WHERE ID = ? LIMIT 1", [$OrderID]);
if ((int) ($qOrderTab['TabID'] ?? 0) > 0) {
$tabID = (int) $qOrderTab['TabID'];
$qTabCheck = queryOne("SELECT StatusID, OwnerUserID FROM Tabs WHERE ID = ? AND StatusID = 1 LIMIT 1", [$tabID]);
if (!$qTabCheck) {
apiAbort(['OK' => false, 'ERROR' => 'tab_not_open', 'MESSAGE' => 'The tab associated with this order is no longer open.']);
}
if ((int) $qOrder['UserID'] !== (int) $qTabCheck['OwnerUserID']) {
$qApproval = queryOne(
"SELECT ApprovalStatus FROM TabOrders WHERE TabID = ? AND OrderID = ? LIMIT 1",
[$tabID, $OrderID]
);
if (!$qApproval || $qApproval['ApprovalStatus'] !== 'approved') {
apiAbort(['OK' => false, 'ERROR' => 'not_approved', 'MESSAGE' => 'This order needs tab owner approval before submitting.']);
}
}
}
// Submit
queryTimed(
"UPDATE Orders SET StatusID = 1, SubmittedOn = NOW(), LastEditedOn = NOW() WHERE ID = ?",
[$OrderID]
);
// Tab running total update
if ($tabID > 0) {
queryTimed("UPDATE Tabs SET LastActivityOn = NOW() WHERE ID = ?", [$tabID]);
}
jsonResponse(['OK' => true, 'ERROR' => '', 'OrderID' => $OrderID, 'MESSAGE' => 'submitted', 'TAB_ID' => $tabID]);
} catch (Exception $e) {
jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => 'DB error submitting order', 'DETAIL' => $e->getMessage()]);
}