501, 'dt' => date('Y-m-d H:i:s'), 'login' => date('Y-m-d H:i:s')]; } // ── Reset ───────────────────────────────────────────────────────────────────── echo PHP_EOL . "=== RESET ===" . PHP_EOL; $p1 = $pdo1; $p2 = $pdo2; // wms_codex_test $p1->exec("DELETE FROM company_usage WHERE company_id = " . COMPANY_ID); // wms2_codex_test $p2->exec("DELETE FROM td_invoice_item WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM td_invoice WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM td_order_item WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM td_order WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM td_purchase_order_item WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM td_purchase_order WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM td_quotation_item WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM td_quotation WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM td_stock_" . WH_ID . " WHERE company_id = " . COMPANY_ID); $p2->exec("DELETE FROM warehouse_balance WHERE company_id = " . COMPANY_ID); $p2->exec("UPDATE md_rack SET product_sku = NULL, td_stock_id = 0 WHERE company_id = " . COMPANY_ID); echo "State reset complete." . PHP_EOL . PHP_EOL; // ── T19: Stock In — Create ──────────────────────────────────────────────────── echo "=== T19: Stock In Create ===" . PHP_EOL; $stock_in_id = 0; try { $data = [ 'warehouse' => WH_ID, 'product_sku' => SKU, 'quantity' => 10, 'price' => 5.00, 'zone' => ZONE, 'aisle' => AISLE, 'rack' => RACK, 'contact_id' => 0, 'description' => 'Test stock in', 'lot_number' => '', 'serial_number' => '', ]; dbTransaction($p2, function($pdo) use ($data, &$stock_in_id) { $stock = new StockManager($pdo, COMPANY_ID); $stock_in_id = $stock->saveStockIn($data, logging(), bin2hex(random_bytes(16))); }); ok('T19a: stock_in_id > 0', $stock_in_id > 0, "id={$stock_in_id}"); // Verify rack occupied $rack = $p2->prepare("SELECT product_sku, td_stock_id FROM md_rack WHERE company_id=? AND zone=? AND aisle=? AND rack=?"); $rack->execute([COMPANY_ID, ZONE, AISLE, RACK]); $rrow = $rack->fetch(PDO::FETCH_ASSOC); ok('T19b: rack occupied with correct SKU', $rrow['product_sku'] === SKU, "sku={$rrow['product_sku']}"); ok('T19c: rack td_stock_id matches', (int)$rrow['td_stock_id'] === $stock_in_id, "td_stock_id={$rrow['td_stock_id']}"); } catch (Exception $e) { ok('T19: Stock In', false, $e->getMessage()); } // ── T20: Stock In — Approve ─────────────────────────────────────────────────── echo PHP_EOL . "=== T20: Stock In Approve ===" . PHP_EOL; try { dbTransaction($p2, function($pdo) use ($stock_in_id) { $stock = new StockManager($pdo, COMPANY_ID); $whMgmt = new WarehouseManager($pdo, COMPANY_ID); $stock->approveStock($stock_in_id, WH_ID, 'in', $whMgmt); }); // Check warehouse_balance $sth = $p2->prepare("SELECT total_in FROM warehouse_balance WHERE company_id=? AND warehouse_id=? AND product_sku=?"); $sth->execute([COMPANY_ID, WH_ID, SKU]); $bal = $sth->fetch(PDO::FETCH_ASSOC); ok('T20a: warehouse_balance updated', !empty($bal), "total_in=" . ($bal['total_in'] ?? 'null')); ok('T20b: total_in = 10', (float)($bal['total_in'] ?? 0) === 10.0, (string)($bal['total_in'] ?? 0)); // Check stock row status = 1 $sth2 = $p2->prepare("SELECT status FROM td_stock_" . WH_ID . " WHERE id=? AND company_id=?"); $sth2->execute([$stock_in_id, COMPANY_ID]); $srow = $sth2->fetch(PDO::FETCH_ASSOC); ok('T20c: stock status = 1 after approve', (int)$srow['status'] === 1, "status={$srow['status']}"); } catch (Exception $e) { ok('T20: Stock Approve', false, $e->getMessage()); } // ── T21: Sales Order — Create Draft ────────────────────────────────────────── echo PHP_EOL . "=== T21: Sales Order Create ===" . PHP_EOL; $order_id = 0; try { $qty = 2.0; $uprice = 10.0; $total = $qty * $uprice; $data = [ 'contact_id' => 0, 'department_id'=> 0, 'order_date' => date('Y-m-d'), 'discount' => 0, 'tax_adjustment' => 0, 'shipping_fee' => 0, 'notes' => 'Test order', 'items' => [[ 'product_sku' => SKU, 'product_name' => 'Test Product 001', 'quantity' => $qty, 'unit_price' => $uprice, 'total_price' => $total, 'tax_amount' => 0, 'tax_rate' => 0, 'warehouse_id' => WH_ID, 'lot_number' => '', ]], ]; $om = new OrderManager($p2, COMPANY_ID); $order_id = $om->saveOrder($data, logging()); ok('T21a: order_id > 0', $order_id > 0, "id={$order_id}"); $sth = $p2->prepare("SELECT status, grand_total FROM td_order WHERE id=? AND company_id=?"); $sth->execute([$order_id, COMPANY_ID]); $orow = $sth->fetch(PDO::FETCH_ASSOC); ok('T21b: order status = 0 (draft)', (int)$orow['status'] === 0, "status={$orow['status']}"); ok('T21c: grand_total = 20.00', (float)$orow['grand_total'] === 20.0, "total={$orow['grand_total']}"); $sth2 = $p2->prepare("SELECT COUNT(*) FROM td_order_item WHERE order_id=? AND company_id=?"); $sth2->execute([$order_id, COMPANY_ID]); ok('T21d: 1 order item created', (int)$sth2->fetchColumn() === 1); } catch (Exception $e) { ok('T21: Sales Order Create', false, $e->getMessage()); } // ── T22: Sales Order — Confirm ─────────────────────────────────────────────── echo PHP_EOL . "=== T22: Sales Order Confirm ===" . PHP_EOL; try { $uuid = bin2hex(random_bytes(16)); dbTransaction($p2, function($pdo) use ($order_id, $uuid) { $om = new OrderManager($pdo, COMPANY_ID); $om->confirmOrder($order_id, $uuid, logging()); }); $sth = $p2->prepare("SELECT status FROM td_order WHERE id=? AND company_id=?"); $sth->execute([$order_id, COMPANY_ID]); $orow = $sth->fetch(PDO::FETCH_ASSOC); ok('T22a: order status = 1 (confirmed)', (int)$orow['status'] === 1, "status={$orow['status']}"); // Check a stock-out draft row was created $sth2 = $p2->prepare("SELECT COUNT(*) FROM td_stock_" . WH_ID . " WHERE company_id=? AND source='order' AND source_id=? AND `out`>0"); $sth2->execute([COMPANY_ID, $order_id]); ok('T22b: draft stock-out row created', (int)$sth2->fetchColumn() === 1); } catch (Exception $e) { ok('T22: Sales Order Confirm', false, $e->getMessage()); } // ── T23: Invoice — Create from Order ───────────────────────────────────────── echo PHP_EOL . "=== T23: Invoice from Confirmed Order ===" . PHP_EOL; $invoice_id = 0; try { $im = new InvoiceManager($p2, COMPANY_ID); dbTransaction($p2, function($pdo) use ($order_id, &$invoice_id) { $im2 = new InvoiceManager($pdo, COMPANY_ID); $invoice_id = $im2->createFromOrder($order_id, logging()); }); ok('T23a: invoice_id > 0', $invoice_id > 0, "id={$invoice_id}"); $sth = $p2->prepare("SELECT status, doc_type, grand_total FROM td_invoice WHERE id=? AND company_id=?"); $sth->execute([$invoice_id, COMPANY_ID]); $inv = $sth->fetch(PDO::FETCH_ASSOC); ok('T23b: doc_type = invoice', $inv['doc_type'] === 'invoice', "doc_type={$inv['doc_type']}"); ok('T23c: invoice status = 0 (draft)', (int)$inv['status'] === 0, "status={$inv['status']}"); ok('T23d: grand_total = 20.00', (float)$inv['grand_total'] === 20.0, "total={$inv['grand_total']}"); $sth2 = $p2->prepare("SELECT COUNT(*) FROM td_invoice_item WHERE invoice_id=? AND company_id=?"); $sth2->execute([$invoice_id, COMPANY_ID]); ok('T23e: invoice items copied', (int)$sth2->fetchColumn() >= 1); // Block duplicate invoice $blocked = false; try { dbTransaction($p2, function($pdo) use ($order_id) { (new InvoiceManager($pdo, COMPANY_ID))->createFromOrder($order_id, logging()); }); } catch (Exception $e2) { $blocked = str_contains($e2->getMessage(), 'already exists'); } ok('T23f: duplicate invoice blocked', $blocked); } catch (Exception $e) { ok('T23: Invoice from Order', false, $e->getMessage()); } // ── T24: Purchase Order — Create ───────────────────────────────────────────── echo PHP_EOL . "=== T24: PO Create ===" . PHP_EOL; $po_id = 0; try { $qty = 5.0; $uprice = 4.50; $total = $qty * $uprice; $data = [ 'contact_id' => 0, 'department_id' => 0, 'po_date' => date('Y-m-d'), 'expected_date' => date('Y-m-d', strtotime('+7 days')), 'warehouse_id' => WH_ID, 'discount' => 0, 'tax_adjustment'=> 0, 'shipping_fee' => 0, 'notes' => 'Test PO', 'items' => [[ 'product_sku' => SKU, 'product_name' => 'Test Product 001', 'quantity' => $qty, 'unit_price' => $uprice, 'total_price' => $total, 'tax_amount' => 0, 'tax_rate' => 0, ]], ]; $pom = new PurchaseOrderManager($p2, COMPANY_ID); $po_id = $pom->savePo($data, logging()); ok('T24a: po_id > 0', $po_id > 0, "id={$po_id}"); $sth = $p2->prepare("SELECT status, grand_total, expected_date FROM td_purchase_order WHERE id=? AND company_id=?"); $sth->execute([$po_id, COMPANY_ID]); $porow = $sth->fetch(PDO::FETCH_ASSOC); ok('T24b: PO status = 0 (draft)', (int)$porow['status'] === 0, "status={$porow['status']}"); ok('T24c: grand_total = 22.50', (float)$porow['grand_total'] === 22.5, "total={$porow['grand_total']}"); ok('T24d: expected_date saved', !empty($porow['expected_date']), "expected_date={$porow['expected_date']}"); $sth2 = $p2->prepare("SELECT COUNT(*) FROM td_purchase_order_item WHERE order_id=? AND company_id=?"); $sth2->execute([$po_id, COMPANY_ID]); ok('T24e: 1 PO item created', (int)$sth2->fetchColumn() === 1); } catch (Exception $e) { ok('T24: PO Create', false, $e->getMessage()); } // ── T25: PO Confirm ────────────────────────────────────────────────────────── echo PHP_EOL . "=== T25: PO Confirm ===" . PHP_EOL; try { $pom = new PurchaseOrderManager($p2, COMPANY_ID); $pom->confirmPo($po_id, logging()); $sth = $p2->prepare("SELECT status FROM td_purchase_order WHERE id=? AND company_id=?"); $sth->execute([$po_id, COMPANY_ID]); $porow = $sth->fetch(PDO::FETCH_ASSOC); ok('T25a: PO status = 1 (confirmed)', (int)$porow['status'] === 1, "status={$porow['status']}"); // Confirm again should throw $blocked = false; try { $pom->confirmPo($po_id, logging()); } catch (Exception $e2) { $blocked = true; } ok('T25b: double-confirm blocked', $blocked); } catch (Exception $e) { ok('T25: PO Confirm', false, $e->getMessage()); } // ── T26: Quotation Create ──────────────────────────────────────────────────── echo PHP_EOL . "=== T26: Quotation Create ===" . PHP_EOL; $quotation_id = 0; try { $qty = 3.0; $uprice = 10.0; $total = $qty * $uprice; $data = [ 'contact_id' => 0, 'department_id' => 0, 'quotation_date' => date('Y-m-d'), 'valid_until' => date('Y-m-d', strtotime('+30 days')), 'discount' => 0, 'notes' => 'Test quotation', 'items' => [[ 'product_sku' => SKU, 'product_name' => 'Test Product 001', 'quantity' => $qty, 'unit_price' => $uprice, 'total_price' => $total, 'tax_amount' => 0, 'tax_rate' => 0, ]], ]; $qm = new QuotationManager($p2, COMPANY_ID); $quotation_id = $qm->save($data, logging()); ok('T26a: quotation_id > 0', $quotation_id > 0, "id={$quotation_id}"); $sth = $p2->prepare("SELECT status, grand_total, quotation_date, valid_until FROM td_quotation WHERE id=? AND company_id=?"); $sth->execute([$quotation_id, COMPANY_ID]); $qrow = $sth->fetch(PDO::FETCH_ASSOC); ok('T26b: quotation status = 0 (draft)', (int)$qrow['status'] === 0, "status={$qrow['status']}"); ok('T26c: grand_total = 30.00', (float)$qrow['grand_total'] === 30.0, "total={$qrow['grand_total']}"); ok('T26d: quotation_date saved', !empty($qrow['quotation_date']), "qdate={$qrow['quotation_date']}"); ok('T26e: valid_until saved', !empty($qrow['valid_until']), "valid_until={$qrow['valid_until']}"); $sth2 = $p2->prepare("SELECT COUNT(*) FROM td_quotation_item WHERE quotation_id=? AND company_id=?"); $sth2->execute([$quotation_id, COMPANY_ID]); ok('T26f: 1 quotation item created', (int)$sth2->fetchColumn() === 1); } catch (Exception $e) { ok('T26: Quotation Create', false, $e->getMessage()); } // ── T27: Usage increment grep check ────────────────────────────────────────── echo PHP_EOL . "=== T27: Usage Increment Wiring ===" . PHP_EOL; $increment_files = [ 'ics/api/engine/manage_stock_in.php', 'ics/api/engine/manage_stock_out.php', 'ics/api/engine/manage_stock_transfer.php', 'order/api/engine/manage_order.php', 'order/api/engine/manage_invoice.php', 'po/api/engine/manage_po.php', 'revenue/api/engine/manage_order.php', 'revenue/api/engine/manage_quotation.php', 'finance/api/engine/manage_receipt.php', 'finance/api/engine/manage_payment.php', ]; $missing = []; foreach ($increment_files as $rel) { $path = APP_ROOT . '/' . $rel; if (!file_exists($path)) { $missing[] = $rel . ' (FILE MISSING)'; continue; } $src = file_get_contents($path); if (!str_contains($src, '->increment()')) { $missing[] = $rel; } } ok('T27: all 10 engine files have increment()', empty($missing), empty($missing) ? '10/10 found' : 'Missing: ' . implode(', ', $missing)); // ── Summary ─────────────────────────────────────────────────────────────────── echo PHP_EOL . "=== SUMMARY ===" . PHP_EOL; $total = $pass + $fail; echo "Passed: {$pass}/{$total}" . PHP_EOL; if ($fail > 0) { echo PHP_EOL . "FAILED TESTS:" . PHP_EOL; foreach ($results as [$s, $l, $d]) { if ($s === 'FAIL') echo " - {$l}" . ($d ? ": {$d}" : '') . PHP_EOL; } } exit($fail === 0 ? 0 : 1);