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 { 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; 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 * 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. * 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. * Table names from information_schema are backtick-quoted for safety.
* *
* @param int $contact_id The md_contact.id to check. * @param int $contact_id The md_contact.id to check.
@@ -44,11 +44,14 @@ class ContactManager {
private function hasActiveStock(int $contact_id): bool { private function hasActiveStock(int $contact_id): bool {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables "SELECT t.table_name
WHERE table_schema = DATABASE() FROM md_warehouse w
AND table_name LIKE 'td_stock_%'" 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); $tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) { foreach ($tables as $table) {
@@ -409,7 +412,7 @@ class ContactManager {
* Soft-delete a contact by negating its company_id. * Soft-delete a contact by negating its company_id.
* *
* Blocks deletion if the contact is referenced in any active stock * 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. * broken foreign key references in transaction history.
* *
* Must be called inside dbTransaction() by the caller. * Must be called inside dbTransaction() by the caller.
+7 -3
View File
@@ -199,10 +199,14 @@ class EtlStockManager
private function discoverStockTables(): array private function discoverStockTables(): array
{ {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables "SELECT t.table_name
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'" 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); return $sth->fetchAll(PDO::FETCH_COLUMN);
} }
+25 -13
View File
@@ -74,9 +74,32 @@ class OrderManager {
throw new Exception("Invalid warehouse id."); 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; 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 * FIFO bin pick — find the oldest approved stock-in row for a SKU
* in a given warehouse that is still bin-occupied. * 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 ─ // ── Reverse approved stock-out side effects, then soft-delete rows ─
$sth = $this->pdo->prepare( $tables = $this->getStockTables();
"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);
$whMgmt = new WarehouseManager($this->pdo, $this->company_id); $whMgmt = new WarehouseManager($this->pdo, $this->company_id);
foreach ($tables as $table) { foreach ($tables as $table) {
@@ -1017,12 +1034,7 @@ class OrderManager {
} }
// Block if any approved stock-out rows exist; user must reverse via ICS first // Block if any approved stock-out rows exist; user must reverse via ICS first
$sth4 = $this->pdo->prepare( $tables = $this->getStockTables();
"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);
foreach ($tables as $table) { foreach ($tables as $table) {
$chk = $this->pdo->prepare( $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. * that references the given SKU.
* *
* Used as a pre-delete guard on products: a product cannot be removed * 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 { private function findActiveStock(string $sku): ?string {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables "SELECT t.table_name
WHERE table_schema = DATABASE() FROM md_warehouse w
AND table_name LIKE 'td_stock_%'" 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); $tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) { foreach ($tables as $table) {
@@ -97,13 +97,7 @@ class PurchaseOrderManager {
} }
if (!$has_any_stock_in) return 0; if (!$has_any_stock_in) return 0;
$sth = $this->pdo->prepare( $tables = $this->getStockTables();
"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);
$total = 0; $total = 0;
$pending = 0; $pending = 0;
@@ -143,9 +137,32 @@ class PurchaseOrderManager {
throw new Exception("Invalid warehouse id."); 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; 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 private function syncPoItems(int $po_id, array $items): void
{ {
$this->pdo->prepare( $this->pdo->prepare(
@@ -729,13 +746,7 @@ class PurchaseOrderManager {
if ($status === -1) throw new Exception("PO is already cancelled."); if ($status === -1) throw new Exception("PO is already cancelled.");
// Guard: block if any approved stock-in rows exist for this PO // Guard: block if any approved stock-in rows exist for this PO
$sth = $this->pdo->prepare( $tables = $this->getStockTables();
"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);
foreach ($tables as $table) { foreach ($tables as $table) {
$sth = $this->pdo->prepare( $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.'); throw new Exception('Cannot delete — this PO has linked supplier returns. Delete or cancel the returns first.');
} }
$sth4 = $this->pdo->prepare( $tables = $this->getStockTables();
"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);
foreach ($tables as $table) { foreach ($tables as $table) {
$sth5 = $this->pdo->prepare( $sth5 = $this->pdo->prepare(
+25 -9
View File
@@ -68,6 +68,15 @@ class ReturnManager {
throw new Exception("Invalid warehouse id."); 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; return 'td_stock_' . $warehouse_id;
} }
@@ -79,11 +88,14 @@ class ReturnManager {
private function deriveReceiptStatus(array $ret): int private function deriveReceiptStatus(array $ret): int
{ {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables "SELECT t.table_name
WHERE table_schema = DATABASE() FROM md_warehouse w
AND table_name LIKE 'td_stock_%'" 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); $tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) { foreach ($tables as $table) {
@@ -737,12 +749,16 @@ class ReturnManager {
} }
// Block if any approved stock-in rows exist; user must reverse via ICS first // Block if any approved stock-in rows exist; user must reverse via ICS first
$sth3 = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables "SELECT t.table_name
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'" 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(); $sth->execute([':company_id' => $this->company_id]);
$tables = $sth3->fetchAll(PDO::FETCH_COLUMN); $tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) { foreach ($tables as $table) {
$chk = $this->pdo->prepare( $chk = $this->pdo->prepare(
@@ -44,6 +44,15 @@ class StockManager {
throw new Exception("Invalid warehouse id."); 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; return 'td_stock_' . $warehouse_id;
} }
@@ -74,12 +74,14 @@ class StockSourceManager
private function getStockTables(): array private function getStockTables(): array
{ {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name "SELECT t.table_name
FROM information_schema.tables FROM md_warehouse w
WHERE table_schema = DATABASE() JOIN information_schema.tables t
AND table_name LIKE 'td_stock_%'" 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); return $sth->fetchAll(PDO::FETCH_COLUMN);
} }
} }
@@ -48,6 +48,15 @@ class SupplierReturnManager {
private function stockTableName(int $warehouse_id): string private function stockTableName(int $warehouse_id): string
{ {
if ($warehouse_id <= 0) throw new Exception("Invalid warehouse id."); 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; return 'td_stock_' . $warehouse_id;
} }
@@ -61,11 +70,14 @@ class SupplierReturnManager {
private function deriveFulfillmentStatus(array $ret): int private function deriveFulfillmentStatus(array $ret): int
{ {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables "SELECT t.table_name
WHERE table_schema = DATABASE() FROM md_warehouse w
AND table_name LIKE 'td_stock_%'" 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); $tables = $sth->fetchAll(PDO::FETCH_COLUMN);
$has_stock_out = false; $has_stock_out = false;
@@ -638,12 +650,16 @@ class SupplierReturnManager {
} }
// Block if any approved stock-out rows exist; user must reverse via ICS first // Block if any approved stock-out rows exist; user must reverse via ICS first
$sth3 = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables "SELECT t.table_name
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'" 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(); $sth->execute([':company_id' => $this->company_id]);
$tables = $sth3->fetchAll(PDO::FETCH_COLUMN); $tables = $sth->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) { foreach ($tables as $table) {
$chk = $this->pdo->prepare( $chk = $this->pdo->prepare(
+19 -7
View File
@@ -40,6 +40,15 @@ class WarehouseManager {
throw new Exception("Invalid warehouse id."); 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; return 'td_stock_' . $warehouse_id;
} }
@@ -243,7 +252,7 @@ class WarehouseManager {
/** /**
* Validate that the given row is the globally latest transaction for its SKU. * 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, * 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. * deletion is blocked to enforce LIFO (last-in-first-out) reversal order.
* *
@@ -261,20 +270,23 @@ class WarehouseManager {
string $product_name string $product_name
): void { ): void {
// Discover all td_stock_* tables for this database // Discover stock tables only for warehouses owned by this company.
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables "SELECT t.table_name
WHERE table_schema = DATABASE() FROM md_warehouse w
AND table_name LIKE 'td_stock_%'" 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); $tables = $sth->fetchAll(PDO::FETCH_COLUMN);
if (empty($tables)) { if (empty($tables)) {
return; 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( $unions = implode(' UNION ALL ', array_map(
fn($t) => "SELECT `type`, `date` FROM `$t` fn($t) => "SELECT `type`, `date` FROM `$t`
WHERE company_id = :company_id WHERE company_id = :company_id