Scope stock table access by company warehouses

This commit is contained in:
Thanakorn S
2026-05-28 15:36:49 +07:00
parent 5df67367fa
commit 9a50238347
11 changed files with 174 additions and 78 deletions
@@ -36,6 +36,19 @@ class BarcodeManager {
// ─────────────────────────────────────────────────────────────
private function stockTableName(int $warehouse_id): string {
if ($warehouse_id <= 0) {
throw new Exception("Invalid warehouse id.");
}
$sth = $this->pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
if (!$sth->fetchColumn()) {
throw new Exception("Warehouse ID {$warehouse_id} not found.");
}
return 'td_stock_' . $warehouse_id;
}
+10 -7
View File
@@ -32,10 +32,10 @@ class ContactManager {
/**
* Check whether a contact is referenced by any active stock transaction
* across all td_stock_* warehouse tables.
* across this company's td_stock_* warehouse tables.
*
* Used as a pre-delete guard to prevent orphaning stock records.
* Scans information_schema to discover all td_stock_* tables dynamically.
* Discovers existing td_stock_* tables through md_warehouse scoped to this company.
* Table names from information_schema are backtick-quoted for safety.
*
* @param int $contact_id The md_contact.id to check.
@@ -44,11 +44,14 @@ class ContactManager {
private function hasActiveStock(int $contact_id): bool {
$sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'td_stock_%'"
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth->execute();
$sth->execute([':company_id' => $this->company_id]);
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
@@ -409,7 +412,7 @@ class ContactManager {
* Soft-delete a contact by negating its company_id.
*
* Blocks deletion if the contact is referenced in any active stock
* transaction across all td_stock_* warehouse tables, preventing
* transaction across this company's td_stock_* warehouse tables, preventing
* broken foreign key references in transaction history.
*
* Must be called inside dbTransaction() by the caller.
+7 -3
View File
@@ -199,10 +199,14 @@ class EtlStockManager
private function discoverStockTables(): array
{
$sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :cid"
);
$sth->execute();
$sth->execute([':cid' => $this->companyId]);
return $sth->fetchAll(PDO::FETCH_COLUMN);
}
+25 -13
View File
@@ -74,9 +74,32 @@ class OrderManager {
throw new Exception("Invalid warehouse id.");
}
$sth = $this->pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
if (!$sth->fetchColumn()) {
throw new Exception("Warehouse ID {$warehouse_id} not found.");
}
return 'td_stock_' . $warehouse_id;
}
private function getStockTables(): array
{
$sth = $this->pdo->prepare(
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth->execute([':company_id' => $this->company_id]);
return $sth->fetchAll(PDO::FETCH_COLUMN);
}
/**
* FIFO bin pick — find the oldest approved stock-in row for a SKU
* in a given warehouse that is still bin-occupied.
@@ -865,13 +888,7 @@ class OrderManager {
}
// ── 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()
AND table_name LIKE 'td_stock_%'"
);
$sth->execute();
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
$tables = $this->getStockTables();
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
foreach ($tables as $table) {
@@ -1017,12 +1034,7 @@ class OrderManager {
}
// Block if any approved stock-out rows exist; user must reverse via ICS first
$sth4 = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
);
$sth4->execute();
$tables = $sth4->fetchAll(PDO::FETCH_COLUMN);
$tables = $this->getStockTables();
foreach ($tables as $table) {
$chk = $this->pdo->prepare(
+8 -5
View File
@@ -31,7 +31,7 @@ class ProductManager {
// ─────────────────────────────────────────────────────────────
/**
* Scan all td_stock_* warehouse tables for any active stock row
* Scan this company's td_stock_* warehouse tables for any active stock row
* that references the given SKU.
*
* Used as a pre-delete guard on products: a product cannot be removed
@@ -45,11 +45,14 @@ class ProductManager {
private function findActiveStock(string $sku): ?string {
$sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'td_stock_%'"
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth->execute();
$sth->execute([':company_id' => $this->company_id]);
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
@@ -97,13 +97,7 @@ class PurchaseOrderManager {
}
if (!$has_any_stock_in) return 0;
$sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'td_stock_%'"
);
$sth->execute();
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
$tables = $this->getStockTables();
$total = 0;
$pending = 0;
@@ -143,9 +137,32 @@ class PurchaseOrderManager {
throw new Exception("Invalid warehouse id.");
}
$sth = $this->pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
if (!$sth->fetchColumn()) {
throw new Exception("Warehouse ID {$warehouse_id} not found.");
}
return 'td_stock_' . $warehouse_id;
}
private function getStockTables(): array
{
$sth = $this->pdo->prepare(
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth->execute([':company_id' => $this->company_id]);
return $sth->fetchAll(PDO::FETCH_COLUMN);
}
private function syncPoItems(int $po_id, array $items): void
{
$this->pdo->prepare(
@@ -729,13 +746,7 @@ class PurchaseOrderManager {
if ($status === -1) throw new Exception("PO is already cancelled.");
// Guard: block if any approved stock-in rows exist for this PO
$sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'td_stock_%'"
);
$sth->execute();
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
$tables = $this->getStockTables();
foreach ($tables as $table) {
$sth = $this->pdo->prepare(
@@ -876,12 +887,7 @@ class PurchaseOrderManager {
throw new Exception('Cannot delete — this PO has linked supplier returns. Delete or cancel the returns first.');
}
$sth4 = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
);
$sth4->execute();
$tables = $sth4->fetchAll(PDO::FETCH_COLUMN);
$tables = $this->getStockTables();
foreach ($tables as $table) {
$sth5 = $this->pdo->prepare(
+25 -9
View File
@@ -68,6 +68,15 @@ class ReturnManager {
throw new Exception("Invalid warehouse id.");
}
$sth = $this->pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
if (!$sth->fetchColumn()) {
throw new Exception("Warehouse ID {$warehouse_id} not found.");
}
return 'td_stock_' . $warehouse_id;
}
@@ -79,11 +88,14 @@ class ReturnManager {
private function deriveReceiptStatus(array $ret): int
{
$sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'td_stock_%'"
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth->execute();
$sth->execute([':company_id' => $this->company_id]);
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
@@ -737,12 +749,16 @@ class ReturnManager {
}
// Block if any approved stock-in rows exist; user must reverse via ICS first
$sth3 = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
$sth = $this->pdo->prepare(
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth3->execute();
$tables = $sth3->fetchAll(PDO::FETCH_COLUMN);
$sth->execute([':company_id' => $this->company_id]);
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
$chk = $this->pdo->prepare(
@@ -44,6 +44,15 @@ class StockManager {
throw new Exception("Invalid warehouse id.");
}
$sth = $this->pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
if (!$sth->fetchColumn()) {
throw new Exception("Warehouse ID {$warehouse_id} not found.");
}
return 'td_stock_' . $warehouse_id;
}
@@ -74,12 +74,14 @@ class StockSourceManager
private function getStockTables(): array
{
$sth = $this->pdo->prepare(
"SELECT table_name
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'td_stock_%'"
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth->execute();
$sth->execute([':company_id' => $this->companyId]);
return $sth->fetchAll(PDO::FETCH_COLUMN);
}
}
@@ -48,6 +48,15 @@ class SupplierReturnManager {
private function stockTableName(int $warehouse_id): string
{
if ($warehouse_id <= 0) throw new Exception("Invalid warehouse id.");
$sth = $this->pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
if (!$sth->fetchColumn()) {
throw new Exception("Warehouse ID {$warehouse_id} not found.");
}
return 'td_stock_' . $warehouse_id;
}
@@ -61,11 +70,14 @@ class SupplierReturnManager {
private function deriveFulfillmentStatus(array $ret): int
{
$sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'td_stock_%'"
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth->execute();
$sth->execute([':company_id' => $this->company_id]);
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
$has_stock_out = false;
@@ -638,12 +650,16 @@ class SupplierReturnManager {
}
// Block if any approved stock-out rows exist; user must reverse via ICS first
$sth3 = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
$sth = $this->pdo->prepare(
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth3->execute();
$tables = $sth3->fetchAll(PDO::FETCH_COLUMN);
$sth->execute([':company_id' => $this->company_id]);
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
$chk = $this->pdo->prepare(
+19 -7
View File
@@ -40,6 +40,15 @@ class WarehouseManager {
throw new Exception("Invalid warehouse id.");
}
$sth = $this->pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
if (!$sth->fetchColumn()) {
throw new Exception("Warehouse ID {$warehouse_id} not found.");
}
return 'td_stock_' . $warehouse_id;
}
@@ -243,7 +252,7 @@ class WarehouseManager {
/**
* Validate that the given row is the globally latest transaction for its SKU.
*
* Scans all td_stock_* tables via UNION ALL to find the single most recent
* Scans this company's td_stock_* tables via UNION ALL to find the single most recent
* transaction date across all warehouses for the SKU. If a newer row exists,
* deletion is blocked to enforce LIFO (last-in-first-out) reversal order.
*
@@ -261,20 +270,23 @@ class WarehouseManager {
string $product_name
): void {
// Discover all td_stock_* tables for this database
// Discover stock tables only for warehouses owned by this company.
$sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'td_stock_%'"
"SELECT t.table_name
FROM md_warehouse w
JOIN information_schema.tables t
ON t.table_schema = DATABASE()
AND t.table_name = CONCAT('td_stock_', w.id)
WHERE w.company_id = :company_id"
);
$sth->execute();
$sth->execute([':company_id' => $this->company_id]);
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
if (empty($tables)) {
return;
}
// Build UNION across all td_stock_* tables to find the global latest date
// Build UNION across this company's td_stock_* tables to find the global latest date
$unions = implode(' UNION ALL ', array_map(
fn($t) => "SELECT `type`, `date` FROM `$t`
WHERE company_id = :company_id