[test] contact to setting modules
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* OrderManager
|
||||
*
|
||||
* Handles all read and write operations for sales orders (td_order)
|
||||
* and their downstream stock-out effects on td_stock_<warehouse> tables.
|
||||
* and their downstream stock-out effects on td_stock_<warehouse_id> tables.
|
||||
*
|
||||
* Method order:
|
||||
* Transaction basis → getOrderList, getOrderById, generateOrderNumber,
|
||||
@@ -16,25 +16,24 @@
|
||||
* No separate child table exists. SKU-level reporting is served by
|
||||
* td_stock_* rows (source='order') rather than querying items JSON.
|
||||
* - confirmOrder() picks racks via FIFO — oldest approved stock-in row
|
||||
* still rack-occupied, ordered by td_stock_<wh>.date ASC.
|
||||
* still rack-occupied, ordered by td_stock_<warehouse_id>.date ASC.
|
||||
* - confirmOrder() creates stock-out rows with status=0 (draft).
|
||||
* Warehouse staff approve them via the existing approve_stock.php
|
||||
* engine, which handles rack release and balance adjustment.
|
||||
* - cancelOrder() sets td_order.status = -1 and soft-deletes ALL linked
|
||||
* stock-out rows (status → -1) across all td_stock_* tables.
|
||||
* Cancellation is blocked if any active invoice (status != -1) or
|
||||
* active return (status != -1) is linked to the order.
|
||||
* - Cancel logic is intentionally self-contained here. status=-1 is
|
||||
* an order-domain concept with no rack/balance side effects, so
|
||||
* WarehouseManager and StockManager are not involved.
|
||||
* stock-out rows (status → -1) across all td_stock_* tables. If linked
|
||||
* stock-out rows were already approved, their rack and balance effects are
|
||||
* reversed before the rows are cancelled.
|
||||
* Cancellation is blocked if any active invoice (status != 4 / void) or
|
||||
* active return (status != -1 / cancelled) is linked to the order.
|
||||
* - Cancel logic stays in the order domain, but uses WarehouseManager for
|
||||
* the same rack and balance reversal rules as manual stock-out deletion.
|
||||
*
|
||||
* Note: Write methods do NOT manage their own DB transactions.
|
||||
* Callers must wrap multi-step operations inside dbTransaction().
|
||||
*
|
||||
* Security: All SQL uses PDO prepared statements with bound parameters.
|
||||
* Dynamic table names (td_stock_<warehouse>) are sanitised with
|
||||
* preg_replace('/[^a-zA-Z0-9_]/', '', ...) before interpolation —
|
||||
* no user input ever reaches a table identifier directly.
|
||||
* Dynamic stock table names are derived only from md_warehouse.id.
|
||||
*/
|
||||
class OrderManager {
|
||||
|
||||
@@ -67,31 +66,13 @@ class OrderManager {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the td_stock_<warehouse> table name for a warehouse_id.
|
||||
*
|
||||
* Does not filter by status — allows reading inactive warehouses
|
||||
* for historical order lookups.
|
||||
*
|
||||
* @param int $warehouse_id
|
||||
* @return string Sanitised table name e.g. "td_stock_Main".
|
||||
* @throws Exception If warehouse not found.
|
||||
*/
|
||||
private function resolveStockTable(int $warehouse_id): string
|
||||
private function stockTableNameFromWarehouseId(int $warehouse_id): string
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT warehouse_name FROM md_warehouse
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
|
||||
$name = $sth->fetchColumn();
|
||||
|
||||
if (!$name) {
|
||||
throw new Exception("Warehouse ID {$warehouse_id} not found.");
|
||||
if ($warehouse_id <= 0) {
|
||||
throw new Exception("Invalid warehouse id.");
|
||||
}
|
||||
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $name);
|
||||
return "td_stock_{$safe}";
|
||||
return 'td_stock_' . $warehouse_id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,10 +89,18 @@ class OrderManager {
|
||||
*/
|
||||
private function pickFifoRack(int $warehouse_id, string $product_sku): ?array
|
||||
{
|
||||
$table = $this->resolveStockTable($warehouse_id);
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT s.*, r.zone, r.aisle, r.rack
|
||||
"SELECT s.*, r.zone, r.aisle, r.rack,
|
||||
(s.`in` - COALESCE((
|
||||
SELECT SUM(o.`out`)
|
||||
FROM `{$table}` o
|
||||
WHERE o.company_id = s.company_id
|
||||
AND o.ref_id = s.id
|
||||
AND o.`out` > 0
|
||||
AND o.status != -1
|
||||
), 0)) AS available_qty
|
||||
FROM `{$table}` s
|
||||
INNER JOIN md_rack r
|
||||
ON r.company_id = s.company_id
|
||||
@@ -120,9 +109,10 @@ class OrderManager {
|
||||
AND r.product_sku IS NOT NULL
|
||||
WHERE s.company_id = :company_id
|
||||
AND s.product_sku = :product_sku
|
||||
AND s.type = 'in'
|
||||
AND s.`in` > 0
|
||||
AND s.status = 1
|
||||
ORDER BY s.date ASC
|
||||
HAVING available_qty > 0
|
||||
ORDER BY s.date ASC, s.id ASC
|
||||
LIMIT 1"
|
||||
);
|
||||
$sth->execute([
|
||||
@@ -354,7 +344,7 @@ class OrderManager {
|
||||
*
|
||||
* Flow per item:
|
||||
* 1. pickFifoRack() — find oldest rack-occupied stock-in row for the SKU.
|
||||
* 2. INSERT into td_stock_<wh> with type='out', status=0,
|
||||
* 2. INSERT into td_stock_<warehouse_id> with type='out', status=0,
|
||||
* source='order', source_id=order_id.
|
||||
* 3. Write back stock_out_id into the item object in td_order.items.
|
||||
*
|
||||
@@ -396,17 +386,20 @@ class OrderManager {
|
||||
throw new Exception("Cannot confirm an order with no items.");
|
||||
}
|
||||
|
||||
// ── Per-item: FIFO pick + insert stock-out row ────────────────────
|
||||
foreach ($items as $i => &$item) {
|
||||
|
||||
$warehouse_id = (int)($item['warehouse_id'] ?? 0);
|
||||
$product_sku = $item['product_sku'] ?? '';
|
||||
$quantity = (float)($item['quantity'] ?? 0);
|
||||
|
||||
if (!$warehouse_id || !$product_sku) {
|
||||
throw new Exception(
|
||||
"Item #{$i}: missing warehouse_id or product_sku."
|
||||
);
|
||||
}
|
||||
if ($quantity <= 0) {
|
||||
throw new Exception("Item #{$i}: quantity must be greater than zero.");
|
||||
}
|
||||
|
||||
$rack_stock = $this->pickFifoRack($warehouse_id, $product_sku);
|
||||
|
||||
@@ -417,7 +410,17 @@ class OrderManager {
|
||||
);
|
||||
}
|
||||
|
||||
$table = $this->resolveStockTable($warehouse_id);
|
||||
$available_qty = (float)($rack_stock['available_qty'] ?? $rack_stock['in'] ?? 0);
|
||||
if ($quantity - $available_qty > 0.000001) {
|
||||
$name = $item['product_name'] ?? $product_sku;
|
||||
throw new Exception(
|
||||
"FIFO rack {$rack_stock['zone']}-{$rack_stock['aisle']}-{$rack_stock['rack']} " .
|
||||
"for \"{$name}\" has only {$available_qty} available unit(s), " .
|
||||
"but the order requests {$quantity}."
|
||||
);
|
||||
}
|
||||
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
$item_uuid = $uuid . '_' . $i;
|
||||
$item_log = [array_merge($logging, ['action' => 'confirm_item'])];
|
||||
|
||||
@@ -439,7 +442,7 @@ class OrderManager {
|
||||
':company_id' => $this->company_id,
|
||||
':date' => date('Y-m-d H:i:s'),
|
||||
':product_sku' => $product_sku,
|
||||
':quantity' => (float)$item['quantity'],
|
||||
':quantity' => $quantity,
|
||||
':zone' => $rack_stock['zone'],
|
||||
':aisle' => $rack_stock['aisle'],
|
||||
':rack' => $rack_stock['rack'],
|
||||
@@ -493,17 +496,18 @@ class OrderManager {
|
||||
*
|
||||
* Business rule:
|
||||
* An order can be cancelled (Draft or Confirmed) as long as no active
|
||||
* downstream documents exist. Active means status != -1 (not voided /
|
||||
* not cancelled).
|
||||
* downstream documents exist. For invoices, active means not voided
|
||||
* (status != 4). For returns, active means not cancelled (status != -1).
|
||||
*
|
||||
* Downstream documents that block cancellation:
|
||||
* - td_invoice where order_id = $order_id AND status != -1
|
||||
* - td_invoice where order_id = $order_id AND status != 4
|
||||
* - td_return where order_id = $order_id AND status != -1
|
||||
*
|
||||
* Stock-out rows are children of the order — they follow the parent
|
||||
* and are never independently approved. On cancel, ALL stock-out rows
|
||||
* linked to this order (any status except already -1) are soft-deleted
|
||||
* (status → -1) across all td_stock_* tables.
|
||||
* Stock-out rows are children of the order — they follow the parent.
|
||||
* On cancel, approved rows first re-occupy the original source rack and
|
||||
* reverse the stock-out balance. Then ALL stock-out rows linked to this
|
||||
* order (any status except already -1) are soft-deleted (status → -1)
|
||||
* across all td_stock_* tables.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
@@ -541,7 +545,7 @@ class OrderManager {
|
||||
"SELECT COUNT(*) FROM td_invoice
|
||||
WHERE company_id = :company_id
|
||||
AND order_id = :order_id
|
||||
AND status != -1"
|
||||
AND status != 4"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':order_id' => $order_id]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
@@ -566,7 +570,7 @@ class OrderManager {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Soft-delete all linked stock-out rows ─────────────────────────
|
||||
// ── Reverse approved stock-out side effects, then soft-delete rows ─
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
@@ -574,11 +578,54 @@ class OrderManager {
|
||||
);
|
||||
$sth->execute();
|
||||
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
|
||||
if (!preg_match('/^td_stock_(\d+)$/', (string)$table, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$warehouse_id = (int)$matches[1];
|
||||
|
||||
$row_sth = $this->pdo->prepare(
|
||||
"SELECT id, product_sku, `out`, zone, aisle, rack, ref_id, status
|
||||
FROM `{$table}`
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'order'
|
||||
AND source_id = :order_id
|
||||
AND type = 'out'
|
||||
AND status != -1"
|
||||
);
|
||||
$row_sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':order_id' => $order_id,
|
||||
]);
|
||||
$rows = $row_sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ((int)$row['status'] !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$whMgmt->occupyRack(
|
||||
$warehouse_id,
|
||||
(string)$row['zone'],
|
||||
(string)$row['aisle'],
|
||||
(string)$row['rack'],
|
||||
(string)$row['product_sku'],
|
||||
(int)$row['ref_id']
|
||||
);
|
||||
$whMgmt->adjustBalance(
|
||||
'out',
|
||||
$warehouse_id,
|
||||
(string)$row['product_sku'],
|
||||
(float)$row['out'],
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$safe}` SET status = -1
|
||||
"UPDATE `{$table}` SET status = -1
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'order'
|
||||
AND source_id = :order_id
|
||||
|
||||
Reference in New Issue
Block a user