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.
198 lines
7.6 KiB
PHP
198 lines
7.6 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../helpers.php';
|
|
require_once __DIR__ . '/_cartPayload.php';
|
|
runAuth();
|
|
|
|
/**
|
|
* Set Line Item (add/update/remove items in cart)
|
|
* POST: { OrderID, ItemID, IsSelected, Quantity?, Remark?, ParentOrderLineItemID?, OrderLineItemID?, ForceNew? }
|
|
*/
|
|
|
|
function nextId(string $table, string $idField): int {
|
|
$q = queryOne("SELECT IFNULL(MAX({$idField}),0) + 1 AS NextID FROM {$table}", []);
|
|
return (int) $q['NextID'];
|
|
}
|
|
|
|
function attachDefaultChildren(int $OrderID, int $ParentLineItemID, int $ParentItemID): void {
|
|
// Direct children
|
|
$qAllKids = queryTimed(
|
|
"SELECT ID, Price, IsCheckedByDefault FROM Items WHERE ParentItemID = ? AND IsActive = 1 ORDER BY SortOrder, ID",
|
|
[$ParentItemID]
|
|
);
|
|
|
|
// Template-linked children
|
|
$qTemplateKids = queryTimed(
|
|
"SELECT i.ID, i.Price, i.IsCheckedByDefault
|
|
FROM lt_ItemID_TemplateItemID tl
|
|
INNER JOIN Items i ON i.ParentItemID = tl.TemplateItemID
|
|
WHERE tl.ItemID = ? AND i.IsActive = 1
|
|
ORDER BY i.SortOrder, i.ID",
|
|
[$ParentItemID]
|
|
);
|
|
|
|
$allKids = array_merge($qAllKids, $qTemplateKids);
|
|
|
|
foreach ($allKids as $kid) {
|
|
if ((int) $kid['IsCheckedByDefault'] === 1) {
|
|
processDefaultChild($OrderID, $ParentLineItemID, (int) $kid['ID'], (float) $kid['Price']);
|
|
} else {
|
|
attachDefaultChildren($OrderID, $ParentLineItemID, (int) $kid['ID']);
|
|
}
|
|
}
|
|
}
|
|
|
|
function processDefaultChild(int $OrderID, int $ParentLineItemID, int $ItemID, float $Price): void {
|
|
$qExisting = queryOne(
|
|
"SELECT ID FROM OrderLineItems WHERE OrderID = ? AND ParentOrderLineItemID = ? AND ItemID = ? LIMIT 1",
|
|
[$OrderID, $ParentLineItemID, $ItemID]
|
|
);
|
|
|
|
if ($qExisting) {
|
|
queryTimed("UPDATE OrderLineItems SET IsDeleted = 0 WHERE ID = ?", [(int) $qExisting['ID']]);
|
|
attachDefaultChildren($OrderID, (int) $qExisting['ID'], $ItemID);
|
|
} else {
|
|
$NewLIID = nextId('OrderLineItems', 'ID');
|
|
queryTimed(
|
|
"INSERT INTO OrderLineItems (ID, ParentOrderLineItemID, OrderID, ItemID, StatusID, Price, Quantity, Remark, IsDeleted, AddedOn)
|
|
VALUES (?, ?, ?, ?, 0, ?, 1, NULL, 0, NOW())",
|
|
[$NewLIID, $ParentLineItemID, $OrderID, $ItemID, $Price]
|
|
);
|
|
attachDefaultChildren($OrderID, $NewLIID, $ItemID);
|
|
}
|
|
}
|
|
|
|
// --- Main logic ---
|
|
|
|
$data = readJsonBody();
|
|
$OrderID = (int) ($data['OrderID'] ?? 0);
|
|
$ParentLineItemID = (int) ($data['ParentOrderLineItemID'] ?? 0);
|
|
$OrderLineItemID = (int) ($data['OrderLineItemID'] ?? 0);
|
|
$OriginalItemID = $data['ItemID'] ?? 0;
|
|
$ItemID = (int) $OriginalItemID;
|
|
|
|
// Decode virtual IDs (format: menuItemID * 100000 + realItemID)
|
|
$WasDecoded = false;
|
|
if ($ItemID > 100000) {
|
|
$WasDecoded = true;
|
|
$ItemID = $ItemID % 100000;
|
|
}
|
|
|
|
$IsSelected = false;
|
|
if (isset($data['IsSelected'])) {
|
|
$v = $data['IsSelected'];
|
|
$IsSelected = ($v === true || $v === 1 || (is_string($v) && strtolower($v) === 'true'));
|
|
}
|
|
$Quantity = (int) ($data['Quantity'] ?? 0);
|
|
$Remark = (string) ($data['Remark'] ?? '');
|
|
$ForceNew = false;
|
|
if (isset($data['ForceNew'])) {
|
|
$v = $data['ForceNew'];
|
|
$ForceNew = ($v === true || $v === 1 || (is_string($v) && strtolower($v) === 'true'));
|
|
}
|
|
|
|
if ($OrderID <= 0 || $ItemID <= 0) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'missing_params', 'MESSAGE' => 'OrderID and ItemID are required.']);
|
|
}
|
|
|
|
try {
|
|
// Load item
|
|
$qItem = queryOne("SELECT ID, Price, ParentItemID, IsActive FROM Items WHERE ID = ? LIMIT 1", [$ItemID]);
|
|
if (!$qItem || (int) $qItem['IsActive'] !== 1) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'bad_item', 'MESSAGE' => "Item not found or inactive. Original={$OriginalItemID} Decoded={$ItemID} WasDecoded=" . ($WasDecoded ? 'true' : 'false')]);
|
|
}
|
|
|
|
// Root vs modifier rules
|
|
if ($ParentLineItemID === 0) {
|
|
if ($IsSelected && $Quantity <= 0) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'bad_quantity', 'MESSAGE' => 'Root line items require Quantity > 0.']);
|
|
}
|
|
} else {
|
|
$Quantity = $IsSelected ? 1 : 0;
|
|
|
|
// Exclusive selection group handling
|
|
if ($IsSelected) {
|
|
$qParentLI = queryOne("SELECT ItemID FROM OrderLineItems WHERE ID = ? LIMIT 1", [$ParentLineItemID]);
|
|
if ($qParentLI) {
|
|
$qParentItem = queryOne("SELECT MaxNumSelectionReq FROM Items WHERE ID = ? LIMIT 1", [(int) $qParentLI['ItemID']]);
|
|
if ($qParentItem && (int) $qParentItem['MaxNumSelectionReq'] === 1) {
|
|
queryTimed(
|
|
"UPDATE OrderLineItems SET IsDeleted = 1 WHERE OrderID = ? AND ParentOrderLineItemID = ? AND ItemID != ? AND IsDeleted = 0",
|
|
[$OrderID, $ParentLineItemID, $ItemID]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find existing line item
|
|
if ($ForceNew) {
|
|
$qExisting = null;
|
|
} elseif ($OrderLineItemID > 0) {
|
|
$qExisting = queryOne(
|
|
"SELECT ID FROM OrderLineItems WHERE ID = ? AND OrderID = ? AND IsDeleted = 0 LIMIT 1",
|
|
[$OrderLineItemID, $OrderID]
|
|
);
|
|
} else {
|
|
$qExisting = queryOne(
|
|
"SELECT ID FROM OrderLineItems WHERE OrderID = ? AND ParentOrderLineItemID = ? AND ItemID = ? AND IsDeleted = 0 LIMIT 1",
|
|
[$OrderID, $ParentLineItemID, $ItemID]
|
|
);
|
|
}
|
|
|
|
if ($qExisting) {
|
|
// Update existing
|
|
if ($IsSelected) {
|
|
queryTimed(
|
|
"UPDATE OrderLineItems SET IsDeleted = 0, Quantity = ?, Price = ?, Remark = ?, StatusID = 0 WHERE ID = ?",
|
|
[$Quantity, (float) $qItem['Price'], trim($Remark) !== '' ? $Remark : null, (int) $qExisting['ID']]
|
|
);
|
|
attachDefaultChildren($OrderID, (int) $qExisting['ID'], $ItemID);
|
|
} else {
|
|
// Deselecting
|
|
if ($ParentLineItemID > 0) {
|
|
$qItemCheck = queryOne("SELECT IsCheckedByDefault FROM Items WHERE ID = ? LIMIT 1", [$ItemID]);
|
|
if ($qItemCheck && (int) $qItemCheck['IsCheckedByDefault'] === 1) {
|
|
// Default modifier: keep with Quantity=0
|
|
queryTimed("UPDATE OrderLineItems SET Quantity = 0 WHERE ID = ?", [(int) $qExisting['ID']]);
|
|
} else {
|
|
queryTimed("UPDATE OrderLineItems SET IsDeleted = 1 WHERE ID = ?", [(int) $qExisting['ID']]);
|
|
}
|
|
} else {
|
|
// Root item: always delete
|
|
queryTimed("UPDATE OrderLineItems SET IsDeleted = 1 WHERE ID = ?", [(int) $qExisting['ID']]);
|
|
}
|
|
}
|
|
} else {
|
|
// Insert new if selecting
|
|
if ($IsSelected) {
|
|
$NewLIID = nextId('OrderLineItems', 'ID');
|
|
queryTimed(
|
|
"INSERT INTO OrderLineItems (ID, ParentOrderLineItemID, OrderID, ItemID, StatusID, Price, Quantity, Remark, IsDeleted, AddedOn)
|
|
VALUES (?, ?, ?, ?, 0, ?, ?, ?, 0, NOW())",
|
|
[
|
|
$NewLIID,
|
|
$ParentLineItemID,
|
|
$OrderID,
|
|
$ItemID,
|
|
(float) $qItem['Price'],
|
|
($ParentLineItemID === 0 ? $Quantity : 1),
|
|
trim($Remark) !== '' ? $Remark : null,
|
|
]
|
|
);
|
|
attachDefaultChildren($OrderID, $NewLIID, $ItemID);
|
|
}
|
|
}
|
|
|
|
// Touch order last edited
|
|
queryTimed("UPDATE Orders SET LastEditedOn = NOW() WHERE ID = ?", [$OrderID]);
|
|
|
|
$payload = loadCartPayload($OrderID);
|
|
jsonResponse($payload);
|
|
|
|
} catch (Exception $e) {
|
|
jsonResponse([
|
|
'OK' => false,
|
|
'ERROR' => 'server_error',
|
|
'MESSAGE' => 'DB error setting line item: ' . $e->getMessage(),
|
|
]);
|
|
}
|