[test] contact to setting modules
This commit is contained in:
@@ -19,8 +19,7 @@
|
||||
* 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 derived from DB-sourced warehouse
|
||||
* names sanitised with preg_replace('/[^a-zA-Z0-9_]/', '', ...) before interpolation.
|
||||
* Dynamic stock table names are derived only from md_warehouse.id.
|
||||
*/
|
||||
class StockManager {
|
||||
|
||||
@@ -36,34 +35,13 @@ class StockManager {
|
||||
// Private helpers
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve the dynamic td_stock_<warehouse> table name for a warehouse_id.
|
||||
*
|
||||
* Looks up warehouse_name from md_warehouse (no status filter — unlike
|
||||
* WarehouseManager::resolveWarehouseTable, this serves read flows that may
|
||||
* need to access inactive warehouses for historical record retrieval).
|
||||
* The name is sanitised with preg_replace before being used as a table
|
||||
* identifier, preventing SQL injection via malicious warehouse names.
|
||||
*
|
||||
* @param int $warehouse_id The md_warehouse.id to resolve.
|
||||
* @return string The sanitised table name, e.g. "td_stock_Main".
|
||||
* @throws Exception If no warehouse is found for the given ID.
|
||||
*/
|
||||
private function resolveTable(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 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;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -83,14 +61,14 @@ class StockManager {
|
||||
*/
|
||||
public function getStockList(int $warehouse_id, string $type): array
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
$column = $type === 'out' ? 'ROUND(a.out, 2)' : 'ROUND(a.in, 2)';
|
||||
|
||||
// Transfer list: show only the outbound side (out > 0) to avoid duplicate display
|
||||
$extra_cond = ($type === 'transfer') ? 'AND a.out > 0' : '';
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, {$column} AS quantity, b.product_name
|
||||
"SELECT a.*, {$column} AS quantity, b.product_name, b.uom
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_product b
|
||||
ON a.company_id = b.company_id
|
||||
@@ -114,18 +92,18 @@ class StockManager {
|
||||
* stock in detail view. Joins md_lot to include lot expiry_date when available.
|
||||
*
|
||||
* @param int $warehouse_id The warehouse the stock_in belongs to.
|
||||
* @param int $id The td_stock_<wh>.id of the stock_in row.
|
||||
* @param int $id The td_stock_<warehouse_id>.id of the stock_in row.
|
||||
* @return array|false Full row with 'quantity', 'contact_name', 'product_name',
|
||||
* 'expiry_date', or false if not found.
|
||||
*/
|
||||
public function getStockInById(int $warehouse_id, int $id): array|false
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, a.in AS quantity,
|
||||
b.contact_name,
|
||||
c.product_name,
|
||||
c.product_name, c.uom,
|
||||
d.expiry_date
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b
|
||||
@@ -149,18 +127,18 @@ class StockManager {
|
||||
* stock out detail view.
|
||||
*
|
||||
* @param int $warehouse_id The warehouse the stock_out belongs to.
|
||||
* @param int $id The td_stock_<wh>.id of the stock_out row.
|
||||
* @param int $id The td_stock_<warehouse_id>.id of the stock_out row.
|
||||
* @return array|false Full row with 'quantity', 'contact_name', 'product_name',
|
||||
* or false if not found.
|
||||
*/
|
||||
public function getStockOutById(int $warehouse_id, int $id): array|false
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, a.out AS quantity,
|
||||
b.contact_name,
|
||||
c.product_name
|
||||
c.product_name, c.uom
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b
|
||||
ON a.company_id = b.company_id AND a.contact_id = b.id
|
||||
@@ -182,19 +160,19 @@ class StockManager {
|
||||
* transfer detail view.
|
||||
*
|
||||
* @param int $warehouse_id The warehouse holding the outbound (from) row.
|
||||
* @param int $id The td_stock_<wh>.id of the outbound transfer row.
|
||||
* @param int $id The td_stock_<warehouse_id>.id of the outbound transfer row.
|
||||
* @return array|false Outbound row with 'quantity', 'contact_name', 'product_name',
|
||||
* and a 'ref' key containing the inbound row, or false if not found.
|
||||
*/
|
||||
public function getTransferById(int $warehouse_id, int $id): array|false
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
|
||||
// Fetch the outbound (from) row
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, a.out AS quantity,
|
||||
b.contact_name,
|
||||
c.product_name
|
||||
c.product_name, c.uom
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b
|
||||
ON a.company_id = b.company_id AND a.contact_id = b.id
|
||||
@@ -209,12 +187,12 @@ class StockManager {
|
||||
|
||||
// Resolve the inbound (to) row via ref_warehouse + uuid
|
||||
$to_warehouse_id = (int)$output['ref_warehouse'];
|
||||
$to_table = $this->resolveTable($to_warehouse_id);
|
||||
$to_table = $this->stockTableNameFromWarehouseId($to_warehouse_id);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, a.in AS quantity,
|
||||
b.contact_name,
|
||||
c.product_name
|
||||
c.product_name, c.uom
|
||||
FROM `{$to_table}` a
|
||||
LEFT JOIN md_contact b
|
||||
ON a.company_id = b.company_id AND a.contact_id = b.id
|
||||
@@ -237,9 +215,9 @@ class StockManager {
|
||||
*
|
||||
* Insert flow (id = 0):
|
||||
* 1. Upserts md_lot if lot_number + expiry_date are provided.
|
||||
* 2. Inserts the td_stock_<wh> row.
|
||||
* 3. Calls WarehouseManager::occupyRack() to mark the rack as taken.
|
||||
* 4. Calls WarehouseManager::adjustBalance() to update warehouse_balance.
|
||||
* 2. Inserts the td_stock_<warehouse_id> row.
|
||||
* 3. Calls WarehouseManager::occupyRack() to reserve the rack.
|
||||
* warehouse_balance is updated later by approveStock().
|
||||
*
|
||||
* Update flow (id > 0):
|
||||
* - Updates contact_id, description, and log only.
|
||||
@@ -328,7 +306,18 @@ class StockManager {
|
||||
|
||||
$td_stock_id = (int)$this->pdo->lastInsertId();
|
||||
|
||||
// occupyRack and adjustBalance deferred — called from approveStock() only.
|
||||
// Reserve the location immediately so draft stock-in rows cannot
|
||||
// leave the same rack available for another receipt. Balance still
|
||||
// changes only when approveStock() runs.
|
||||
$whMgmt->occupyRack(
|
||||
$warehouse_id,
|
||||
$data['zone'],
|
||||
$data['aisle'],
|
||||
$data['rack'],
|
||||
$data['product_sku'],
|
||||
$td_stock_id
|
||||
);
|
||||
|
||||
return $td_stock_id;
|
||||
}
|
||||
}
|
||||
@@ -338,7 +327,7 @@ class StockManager {
|
||||
*
|
||||
* Insert flow (id = 0):
|
||||
* 1. Validates the rack is occupied with the correct SKU / lot / serial.
|
||||
* 2. Inserts the td_stock_<wh> row, copying quantity and lot info from the rack.
|
||||
* 2. Inserts the td_stock_<warehouse_id> row, copying quantity and lot info from the rack.
|
||||
* 3. Calls WarehouseManager::releaseRack() to free the rack slot.
|
||||
* 4. Calls WarehouseManager::adjustBalance() to update warehouse_balance.
|
||||
*
|
||||
@@ -609,7 +598,7 @@ class StockManager {
|
||||
':date' => date('Y-m-d H:i:s'),
|
||||
':product_sku' => $data['product_sku'],
|
||||
':quantity' => $quantity,
|
||||
':ref_warehouse' => $whMgmt->getWarehouseName($to_warehouse),
|
||||
':ref_warehouse' => $to_warehouse,
|
||||
':zone' => $from_zone,
|
||||
':aisle' => $from_aisle,
|
||||
':rack' => $from_rack,
|
||||
@@ -637,7 +626,7 @@ class StockManager {
|
||||
':date' => date('Y-m-d H:i:s'),
|
||||
':product_sku' => $data['product_sku'],
|
||||
':quantity' => $quantity,
|
||||
':ref_warehouse' => $whMgmt->getWarehouseName($from_warehouse),
|
||||
':ref_warehouse' => $from_warehouse,
|
||||
':ref_id' => $from_stock_id,
|
||||
':zone' => $to_zone,
|
||||
':aisle' => $to_aisle,
|
||||
@@ -686,7 +675,7 @@ class StockManager {
|
||||
*/
|
||||
public function approveStock(int $id, int $warehouse_id, string $type, WarehouseManager $whMgmt): void
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
|
||||
// Load the row and validate ownership / status
|
||||
$sth = $this->pdo->prepare(
|
||||
@@ -724,31 +713,52 @@ class StockManager {
|
||||
|
||||
} elseif ($type === 'out') {
|
||||
|
||||
// Release rack now that stock-out is approved
|
||||
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
|
||||
$remaining_qty = 0.0;
|
||||
if ((int)($row['ref_id'] ?? 0) > 0) {
|
||||
$remaining_sth = $this->pdo->prepare(
|
||||
"SELECT src.`in` - COALESCE(SUM(out_rows.`out`), 0) AS remaining_qty
|
||||
FROM `{$table}` src
|
||||
LEFT JOIN `{$table}` out_rows
|
||||
ON out_rows.company_id = src.company_id
|
||||
AND out_rows.ref_id = src.id
|
||||
AND out_rows.`out` > 0
|
||||
AND out_rows.status != -1
|
||||
WHERE src.id = :ref_id
|
||||
AND src.company_id = :company_id
|
||||
GROUP BY src.id, src.`in`"
|
||||
);
|
||||
$remaining_sth->execute([
|
||||
':ref_id' => (int)$row['ref_id'],
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
$remaining_qty = (float)$remaining_sth->fetchColumn();
|
||||
}
|
||||
|
||||
// Release the rack only when the source batch is fully consumed.
|
||||
if ($remaining_qty <= 0.000001) {
|
||||
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
|
||||
}
|
||||
$whMgmt->adjustBalance('out', $warehouse_id, $row['product_sku'], 0, (float)$row['out']);
|
||||
|
||||
} elseif ($type === 'transfer') {
|
||||
|
||||
// Transfer always has two rows in two different tables:
|
||||
// outbound row → td_stock_<from> (out > 0, ref_warehouse = to_name)
|
||||
// inbound row → td_stock_<to> (in > 0, ref_warehouse = from_name)
|
||||
// outbound row → td_stock_<from_id> (out > 0, ref_warehouse = to_id)
|
||||
// inbound row → td_stock_<to_id> (in > 0, ref_warehouse = from_id)
|
||||
// Both rows share the same uuid. We always approve both atomically.
|
||||
// Identify which side we were given and derive the other.
|
||||
|
||||
$is_outbound = (float)$row['out'] > 0;
|
||||
|
||||
// The outbound row lives in the from-warehouse table (already loaded as $row/$table).
|
||||
// The inbound row lives in td_stock_<ref_warehouse>.
|
||||
// The inbound row lives in td_stock_<ref_warehouse_id>.
|
||||
$from_row = $is_outbound ? $row : null;
|
||||
$from_table = $is_outbound ? $table : null;
|
||||
$from_wh_id = $is_outbound ? $warehouse_id : null;
|
||||
|
||||
// Resolve the paired table from ref_warehouse
|
||||
$paired_wh_name = $row['ref_warehouse'];
|
||||
$paired_safe = preg_replace('/[^a-zA-Z0-9_]/', '', $paired_wh_name);
|
||||
$paired_table = "td_stock_{$paired_safe}";
|
||||
$paired_wh_id = $whMgmt->getWarehouseIdByName($paired_wh_name);
|
||||
// Resolve the paired table from ref_warehouse id
|
||||
$paired_wh_id = (int)$row['ref_warehouse'];
|
||||
$paired_table = $this->stockTableNameFromWarehouseId($paired_wh_id);
|
||||
|
||||
// If we received the inbound side, swap so $from_* is always outbound
|
||||
if (!$is_outbound) {
|
||||
@@ -816,4 +826,4 @@ class StockManager {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user