Demo Data Population

This commit is contained in:
Thanakorn
2026-08-14 14:10:31 +07:00
parent b2c437426b
commit dd48a8b96d
9 changed files with 2905 additions and 1 deletions
+663
View File
@@ -0,0 +1,663 @@
<?php
/**
* Demo Data Seeder
* Run once after setup.php on a fresh install: php demo_seed.php
*
* What it does:
* 1. Reads DB credentials from app/config.php (same as setup.php)
* 2. Creates a demo company + owner user (bypasses email verification —
* inserted directly with status='active' so you can log in immediately)
* 3. Creates 2 warehouses with bin ranges
* 4. Creates product categories + products
* 5. Creates contact types + contacts (customers/suppliers)
* 6. Receives opening stock for every product into Main Warehouse, approved
* (so dashboard/report balances are non-zero from the first login)
* 7. Receives a few later restock/replenishment batches (additional stock-in,
* separate from the opening-stock batch) into fresh bins in Main Warehouse
* and Bangna Distribution Center
* 8. Ships a few products out to customers (stock-out)
* 9. Transfers a few products from Main Warehouse to Bangna Distribution
* Center (stock-transfer)
*
* Together, steps 6-9 give the Stock In / Stock Out / Stock Transfer list
* pages several realistic entries each, instead of just one opening-balance batch.
*
* All writes go through the same Manager classes the app itself uses
* (WarehouseManager, ProductManager, ContactManager, StockManager), so the
* data is guaranteed to match what the UI/API would have produced.
*
* Safe to re-run: every insert is guarded by an existence check.
*
* No company_smtp row is created for the demo company, which means login
* skips the OTP email step entirely (see login_otp.php) — you can log in
* with just username + password.
*/
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit('Run via CLI only: php demo_seed.php');
}
$_SESSION = []; // Manager classes read $_SESSION['login_user_id'] for audit logs; CLI has no session.
require __DIR__ . '/app/config.php';
require __DIR__ . '/app/dbconn.php';
require __DIR__ . '/app/assets/utils/db_helpers.php';
require __DIR__ . '/app/assets/utils/classes/WarehouseManager.php';
require __DIR__ . '/app/assets/utils/classes/ProductManager.php';
require __DIR__ . '/app/assets/utils/classes/ContactManager.php';
require __DIR__ . '/app/assets/utils/classes/StockManager.php';
function info(string $msg): void { echo "\033[36m •\033[0m {$msg}\n"; }
function ok(string $msg): void { echo "\033[32m ✓\033[0m {$msg}\n"; }
function skip(string $msg): void { echo "\033[33m –\033[0m {$msg}\n"; }
function findProductDef(array $product_defs, string $sku): array
{
foreach ($product_defs as $p) {
if ($p['sku'] === $sku) return $p;
}
throw new Exception("demo_seed.php: unknown SKU '{$sku}' referenced by a later seeding step.");
}
/**
* Find the first unoccupied bin in a warehouse (simple-mode: zone = aisle = bin).
* Queried fresh on every call so it stays correct as bins fill up / free up
* across the run — safe to call repeatedly even against a warehouse that
* already has unrelated stock in it.
*/
function findFreeBin(PDO $pdo2, int $company_id, int $warehouse_id): string
{
$sth = $pdo2->prepare(
"SELECT bin FROM md_bin
WHERE company_id = :c AND warehouse = :w AND product_sku IS NULL
ORDER BY id ASC
LIMIT 1"
);
$sth->execute([':c' => $company_id, ':w' => $warehouse_id]);
$bin = $sth->fetchColumn();
if ($bin === false) {
throw new Exception("demo_seed.php: no free bin available in warehouse #{$warehouse_id}.");
}
return $bin;
}
echo "\n\033[1m=== mn3wms Demo Data Seeder ===\033[0m\n\n";
// ─────────────────────────────────────────────────────────────────────────────
// 1. Owner user + company (pdo1 = wms auth/identity db)
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Owner account & company ---\n";
$demo_username = 'admin';
$demo_email = 'thanakorn.inbox@gmail.com';
$demo_password = 'Demo@12345';
$demo_channel = 'demo';
$sth = $pdo1->prepare("SELECT user_id FROM user WHERE username = :u OR email = :e LIMIT 1");
$sth->execute([':u' => $demo_username, ':e' => $demo_email]);
$owner_user_id = (int)($sth->fetchColumn() ?: 0);
if ($owner_user_id) {
skip("User '{$demo_username}' already exists (user_id={$owner_user_id})");
} else {
$hashed = password_hash($demo_password, PASSWORD_BCRYPT);
$sth = $pdo1->prepare("
INSERT INTO user (username, name, surname, email, password, status, profile_picture, license, app_access)
VALUES (:username, :name, :surname, :email, :password, 'active', '', 'owner', 'all')
");
$sth->execute([
':username' => $demo_username,
':name' => 'Thanakorn',
':surname' => 'Admin',
':email' => $demo_email,
':password' => $hashed,
]);
$owner_user_id = (int)$pdo1->lastInsertId();
ok("Created user '{$demo_username}' (user_id={$owner_user_id}), password: {$demo_password}");
}
$sth = $pdo1->prepare("SELECT company_id FROM company_list WHERE channel_name = :c LIMIT 1");
$sth->execute([':c' => $demo_channel]);
$company_id = (int)($sth->fetchColumn() ?: 0);
if ($company_id) {
skip("Company channel '{$demo_channel}' already exists (company_id={$company_id})");
} else {
$sth = $pdo1->prepare("
INSERT INTO company_list (channel_name, company_name, company_name2, branch, branch_no, email, phone, fx)
VALUES (:channel_name, :company_name, :company_name2, :branch, :branch_no, :email, :phone, 'thb')
");
$sth->execute([
':channel_name' => $demo_channel,
':company_name' => 'Demo Trading Co., Ltd.',
':company_name2' => 'Demo Trading',
':branch' => 'สำนักงานใหญ่',
':branch_no' => '00000',
':email' => $demo_email,
':phone' => '021234567',
]);
$company_id = (int)$pdo1->lastInsertId();
ok("Created company 'Demo Trading Co., Ltd.' (company_id={$company_id})");
}
$sth = $pdo1->prepare("SELECT 1 FROM company_map_user WHERE company_id = :c AND user_id = :u LIMIT 1");
$sth->execute([':c' => $company_id, ':u' => $owner_user_id]);
if ($sth->fetchColumn()) {
skip("company_map_user link already exists");
} else {
$pdo1->prepare("
INSERT INTO company_map_user (company_id, user_id, role, app_access, created_at)
VALUES (:company_id, :user_id, 'owner', 'all', NOW())
")->execute([':company_id' => $company_id, ':user_id' => $owner_user_id]);
ok("Linked user to company as owner");
}
$pdo1->prepare("UPDATE user SET default_company = :c WHERE user_id = :u AND (default_company IS NULL OR default_company = 0)")
->execute([':c' => $company_id, ':u' => $owner_user_id]);
// No company_smtp row is inserted on purpose — login_otp.php skips the OTP
// step entirely when a company has no SMTP configured.
$logging = ['user_id' => $owner_user_id, 'dt' => date('Y-m-d H:i:s'), 'login' => null, 'action' => 'seed'];
// ─────────────────────────────────────────────────────────────────────────────
// 2. Warehouses + storage/bins (pdo2 = wms2 business db)
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- Warehouses ---\n";
$whMgmt = new WarehouseManager($pdo2, $company_id);
$warehouse_defs = [
['name' => 'Main Warehouse', 'location' => '123 Sukhumvit Rd, Bangkok', 'bin_to' => 30],
['name' => 'Bangna Distribution Center', 'location' => '456 Bangna-Trad Rd, Samut Prakan', 'bin_to' => 15],
];
$warehouse_ids = []; // name => id
foreach ($warehouse_defs as $wh) {
$sth = $pdo2->prepare("SELECT id FROM md_warehouse WHERE company_id = :c AND warehouse_name = :n LIMIT 1");
$sth->execute([':c' => $company_id, ':n' => $wh['name']]);
$id = (int)($sth->fetchColumn() ?: 0);
if ($id) {
skip("Warehouse '{$wh['name']}' already exists (id={$id})");
} else {
dbTransaction($pdo2, function ($pdo2) use ($whMgmt, $wh, $company_id, $owner_user_id, $logging, &$id) {
$id = $whMgmt->saveWarehouse([
'id' => 0,
'warehouse_name' => $wh['name'],
'location' => $wh['location'],
'manager' => $owner_user_id,
'description' => 'Demo warehouse',
'status' => 1,
], $logging);
});
$whMgmt->createStockTableForWarehouse($id);
ok("Created warehouse '{$wh['name']}' (id={$id}) + td_stock_{$id} table");
}
$warehouse_ids[$wh['name']] = $id;
// Simple-mode storage: one bin range "A-1".."A-N" (zone=aisle=bin=same value)
$sth = $pdo2->prepare("SELECT id FROM md_storage WHERE company_id = :c AND warehouse = :w LIMIT 1");
$sth->execute([':c' => $company_id, ':w' => $id]);
if ($sth->fetchColumn()) {
skip("Storage range for '{$wh['name']}' already exists");
} else {
dbTransaction($pdo2, function ($pdo2) use ($whMgmt, $id, $wh, $logging) {
$whMgmt->saveStorage([
'id' => 0,
'warehouse' => (string)$id,
'bin_from' => 'A-1',
'bin_to' => 'A-' . $wh['bin_to'],
'description' => 'Demo storage range',
'status' => 1,
], $logging, false);
});
ok("Created storage range A-1..A-{$wh['bin_to']} for '{$wh['name']}' ({$wh['bin_to']} bins)");
}
}
$main_wh_id = $warehouse_ids['Main Warehouse'];
$bangna_wh_id = $warehouse_ids['Bangna Distribution Center'];
// ─────────────────────────────────────────────────────────────────────────────
// 3. Product categories + products
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- Products ---\n";
$prodMgmt = new ProductManager($pdo2, $company_id);
$category_defs = [
'electronics' => 'Electronics',
'office-supplies' => 'Office Supplies',
'beverages' => 'Beverages',
'packaging-materials' => 'Packaging Materials',
];
$category_ids = []; // slug => id
foreach ($category_defs as $slug => $name) {
$sth = $pdo2->prepare("SELECT id FROM md_product_category WHERE company_id = :c AND slug = :s LIMIT 1");
$sth->execute([':c' => $company_id, ':s' => $slug]);
$id = (int)($sth->fetchColumn() ?: 0);
if ($id) {
skip("Category '{$name}' already exists (id={$id})");
} else {
dbTransaction($pdo2, function ($pdo2) use ($prodMgmt, $name, $slug, $logging) {
$prodMgmt->saveCategory([
'id' => 0,
'category' => $name,
'slug' => $slug,
'description' => '',
'status' => 1,
], $logging);
});
$sth = $pdo2->prepare("SELECT id FROM md_product_category WHERE company_id = :c AND slug = :s LIMIT 1");
$sth->execute([':c' => $company_id, ':s' => $slug]);
$id = (int)$sth->fetchColumn();
ok("Created category '{$name}' (id={$id})");
}
$category_ids[$slug] = $id;
}
$product_defs = [
['sku' => 'EL-001', 'name' => 'Wireless Mouse', 'cat' => 'electronics', 'uom' => 'pcs', 'price' => 590, 'cost' => 350, 'min' => 10, 'reorder' => 20, 'qty' => 120],
['sku' => 'EL-002', 'name' => 'USB-C Cable 1m', 'cat' => 'electronics', 'uom' => 'pcs', 'price' => 190, 'cost' => 90, 'min' => 20, 'reorder' => 40, 'qty' => 250],
['sku' => 'EL-003', 'name' => 'Bluetooth Speaker', 'cat' => 'electronics', 'uom' => 'pcs', 'price' => 1290, 'cost' => 780, 'min' => 5, 'reorder' => 10, 'qty' => 45],
['sku' => 'EL-004', 'name' => 'Power Bank 10000mAh', 'cat' => 'electronics', 'uom' => 'pcs', 'price' => 890, 'cost' => 520, 'min' => 8, 'reorder' => 15, 'qty' => 60],
['sku' => 'OF-001', 'name' => 'A4 Paper Ream', 'cat' => 'office-supplies', 'uom' => 'ream', 'price' => 120, 'cost' => 75, 'min' => 30, 'reorder' => 60, 'qty' => 300],
['sku' => 'OF-002', 'name' => 'Ballpoint Pen (Box of 12)', 'cat' => 'office-supplies', 'uom' => 'box', 'price' => 85, 'cost' => 45, 'min' => 15, 'reorder' => 30, 'qty' => 150],
['sku' => 'OF-003', 'name' => 'Stapler', 'cat' => 'office-supplies', 'uom' => 'pcs', 'price' => 65, 'cost' => 35, 'min' => 10, 'reorder' => 20, 'qty' => 80],
['sku' => 'OF-004', 'name' => 'Sticky Notes Pack', 'cat' => 'office-supplies', 'uom' => 'pack', 'price' => 45, 'cost' => 22, 'min' => 20, 'reorder' => 40, 'qty' => 200],
['sku' => 'BV-001', 'name' => 'Bottled Water 600ml (Pack of 12)', 'cat' => 'beverages', 'uom' => 'pack', 'price' => 72, 'cost' => 48, 'min' => 25, 'reorder' => 50, 'qty' => 180],
['sku' => 'BV-002', 'name' => 'Instant Coffee 3-in-1 (Box of 30)', 'cat' => 'beverages', 'uom' => 'box', 'price' => 145, 'cost' => 98, 'min' => 15, 'reorder' => 30, 'qty' => 90],
['sku' => 'BV-003', 'name' => 'Green Tea Bags (Box of 25)', 'cat' => 'beverages', 'uom' => 'box', 'price' => 89, 'cost' => 55, 'min' => 12, 'reorder' => 24, 'qty' => 70],
['sku' => 'PK-001', 'name' => 'Corrugated Box Medium', 'cat' => 'packaging-materials', 'uom' => 'pcs', 'price' => 18, 'cost' => 9, 'min' => 100, 'reorder' => 200, 'qty' => 500],
['sku' => 'PK-002', 'name' => 'Bubble Wrap Roll', 'cat' => 'packaging-materials', 'uom' => 'roll', 'price' => 250, 'cost' => 160, 'min' => 10, 'reorder' => 20, 'qty' => 40],
['sku' => 'PK-003', 'name' => 'Packing Tape', 'cat' => 'packaging-materials', 'uom' => 'pcs', 'price' => 35, 'cost' => 18, 'min' => 40, 'reorder' => 80, 'qty' => 220],
];
foreach ($product_defs as $p) {
$sth = $pdo2->prepare("SELECT id FROM md_product WHERE company_id = :c AND sku = :s LIMIT 1");
$sth->execute([':c' => $company_id, ':s' => $p['sku']]);
if ($sth->fetchColumn()) {
skip("Product '{$p['sku']}' already exists");
continue;
}
dbTransaction($pdo2, function ($pdo2) use ($prodMgmt, $p, $category_ids, $logging) {
$prodMgmt->saveProduct([
'id' => 0,
'product_name' => $p['name'],
'sku' => $p['sku'],
'barcode' => '',
'uom' => $p['uom'],
'price' => $p['price'],
'cost_price' => $p['cost'],
'min_stock' => $p['min'],
'reorder_point' => $p['reorder'],
'category' => $category_ids[$p['cat']],
'description' => '',
'status' => 1,
], $logging, '');
});
ok("Created product {$p['sku']} — {$p['name']}");
}
// ─────────────────────────────────────────────────────────────────────────────
// 4. Contact types + contacts
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- Contacts ---\n";
$contactMgmt = new ContactManager($pdo2, $company_id);
$contact_type_defs = ['Customer', 'Supplier'];
$contact_type_ids = [];
foreach ($contact_type_defs as $name) {
$sth = $pdo2->prepare("SELECT id FROM md_contact_type WHERE company_id = :c AND contact_type = :n LIMIT 1");
$sth->execute([':c' => $company_id, ':n' => $name]);
$id = (int)($sth->fetchColumn() ?: 0);
if ($id) {
skip("Contact type '{$name}' already exists (id={$id})");
} else {
dbTransaction($pdo2, function ($pdo2) use ($contactMgmt, $name, $logging) {
$contactMgmt->saveContactType([
'id' => 0,
'contact_type' => $name,
'description' => '',
'status' => 1,
], $logging);
});
$sth = $pdo2->prepare("SELECT id FROM md_contact_type WHERE company_id = :c AND contact_type = :n LIMIT 1");
$sth->execute([':c' => $company_id, ':n' => $name]);
$id = (int)$sth->fetchColumn();
ok("Created contact type '{$name}' (id={$id})");
}
$contact_type_ids[$name] = $id;
}
$contact_defs = [
['name' => 'Siam Retail Co., Ltd.', 'type' => 'Customer', 'tax' => '0105561012341'],
['name' => 'Bangkok Mart', 'type' => 'Customer', 'tax' => '0105561012342'],
['name' => 'Northern Distribution Partners', 'type' => 'Customer', 'tax' => '0105561012343'],
['name' => 'Chao Phraya Trading', 'type' => 'Customer', 'tax' => '0105561012344'],
['name' => 'Global Parts Supply Co., Ltd.', 'type' => 'Supplier', 'tax' => '0105561098761'],
['name' => 'Thai Packaging Industries', 'type' => 'Supplier', 'tax' => '0105561098762'],
['name' => 'Asia Electronics Wholesale', 'type' => 'Supplier', 'tax' => '0105561098763'],
];
$supplier_ids = [];
$customer_ids = [];
foreach ($contact_defs as $c) {
$sth = $pdo2->prepare("SELECT id FROM md_contact WHERE company_id = :c AND contact_name = :n LIMIT 1");
$sth->execute([':c' => $company_id, ':n' => $c['name']]);
$id = (int)($sth->fetchColumn() ?: 0);
if ($id) {
skip("Contact '{$c['name']}' already exists (id={$id})");
} else {
dbTransaction($pdo2, function ($pdo2) use ($contactMgmt, $c, $contact_type_ids, $logging) {
$contactMgmt->saveContact([
'id' => 0,
'contact_name' => $c['name'],
'tax_id' => $c['tax'],
'organization' => $c['name'],
'branch' => 'สำนักงานใหญ่',
'contact_type' => $contact_type_ids[$c['type']],
'billing_address' => '99 Business Rd, Bangkok 10110, Thailand',
'shipping_location' => 'Head Office',
'shipping_address' => '99 Business Rd, Bangkok 10110, Thailand',
'remark' => '',
'status' => 1,
], $logging, '');
});
$sth = $pdo2->prepare("SELECT id FROM md_contact WHERE company_id = :c AND contact_name = :n LIMIT 1");
$sth->execute([':c' => $company_id, ':n' => $c['name']]);
$id = (int)$sth->fetchColumn();
ok("Created contact '{$c['name']}' ({$c['type']}, id={$id})");
}
if ($c['type'] === 'Supplier') {
$supplier_ids[] = $id;
} else {
$customer_ids[] = $id;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 5. Opening stock — receive every product into Main Warehouse bins, approved
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- Opening stock (Main Warehouse) ---\n";
$stockMgmt = new StockManager($pdo2, $company_id);
$sth = $pdo2->prepare("SELECT COUNT(*) FROM `td_stock_{$main_wh_id}` WHERE company_id = :c AND type = 'in'");
$sth->execute([':c' => $company_id]);
if ((int)$sth->fetchColumn() > 0) {
skip("Opening stock already exists in Main Warehouse — skipping stock-in step");
} else {
$bin_index = 1;
foreach ($product_defs as $p) {
$bin = 'A-' . $bin_index;
$bin_index++;
$supplier_id = $supplier_ids[array_rand($supplier_ids)];
$uuid = bin2hex(random_bytes(16));
dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $main_wh_id, $p, $bin, $supplier_id, $logging, $uuid) {
$stock_id = $stockMgmt->saveStockIn([
'id' => 0,
'warehouse' => $main_wh_id,
'product_sku' => $p['sku'],
'quantity' => $p['qty'],
'price' => $p['cost'],
'zone' => $bin,
'aisle' => $bin,
'bin' => $bin,
'contact_id' => $supplier_id,
'description' => 'Opening stock (demo seed)',
], $logging, $uuid);
$stockMgmt->approveStock($stock_id, $main_wh_id, 'in', $whMgmt);
});
ok("Received {$p['qty']} {$p['uom']} of {$p['sku']} into bin {$bin}");
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 6. Restock — later replenishment receipts from suppliers, into fresh bins
// (separate batch from opening stock, so the Stock In list shows more than
// one receiving event). Bin is resolved dynamically (first free bin in the
// target warehouse) rather than hardcoded, so this stays correct regardless
// of which bins earlier runs / other seed steps have already taken.
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- Restock (additional stock-in) ---\n";
$restock_marker = 'Restock (demo seed)';
$restock_defs = [
['warehouse' => $main_wh_id, 'warehouse_name' => 'Main Warehouse', 'sku' => 'EL-001', 'qty' => 80],
['warehouse' => $main_wh_id, 'warehouse_name' => 'Main Warehouse', 'sku' => 'OF-001', 'qty' => 200],
['warehouse' => $bangna_wh_id, 'warehouse_name' => 'Bangna Distribution Center', 'sku' => 'BV-001', 'qty' => 120],
['warehouse' => $main_wh_id, 'warehouse_name' => 'Main Warehouse', 'sku' => 'EL-002', 'qty' => 100],
['warehouse' => $main_wh_id, 'warehouse_name' => 'Main Warehouse', 'sku' => 'PK-001', 'qty' => 150],
['warehouse' => $bangna_wh_id, 'warehouse_name' => 'Bangna Distribution Center', 'sku' => 'BV-002', 'qty' => 60],
['warehouse' => $bangna_wh_id, 'warehouse_name' => 'Bangna Distribution Center', 'sku' => 'OF-002', 'qty' => 90],
];
foreach ($restock_defs as $r) {
$p = findProductDef($product_defs, $r['sku']);
$warehouse_id = $r['warehouse'];
$sth = $pdo2->prepare(
"SELECT COUNT(*) FROM `td_stock_{$warehouse_id}`
WHERE company_id = :c AND type = 'in' AND description = :d AND product_sku = :s"
);
$sth->execute([':c' => $company_id, ':d' => $restock_marker, ':s' => $r['sku']]);
if ((int)$sth->fetchColumn() > 0) {
skip("Restock of {$r['sku']} into '{$r['warehouse_name']}' already exists");
continue;
}
$bin = findFreeBin($pdo2, $company_id, $warehouse_id);
$supplier_id = $supplier_ids[array_rand($supplier_ids)];
$uuid = bin2hex(random_bytes(16));
dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $warehouse_id, $p, $r, $bin, $supplier_id, $logging, $uuid, $restock_marker) {
$stock_id = $stockMgmt->saveStockIn([
'id' => 0,
'warehouse' => $warehouse_id,
'product_sku' => $r['sku'],
'quantity' => $r['qty'],
'price' => $p['cost'],
'zone' => $bin,
'aisle' => $bin,
'bin' => $bin,
'contact_id' => $supplier_id,
'description' => $restock_marker,
], $logging, $uuid);
$stockMgmt->approveStock($stock_id, $warehouse_id, 'in', $whMgmt);
});
ok("Received {$r['qty']} {$p['uom']} of {$r['sku']} into bin {$bin} ({$r['warehouse_name']})");
}
// ─────────────────────────────────────────────────────────────────────────────
// 7. Stock-out — ship product out to customers.
//
// saveStockOut() always takes the *entire* quantity sitting in the source
// bin (there is no partial-out parameter), so to produce a stock-out of a
// specific, controlled quantity we first receive a small dedicated batch
// into a fresh bin, approve it, then immediately move that whole batch out
// again. This leaves the opening-stock / restock balances untouched while
// still creating a standalone, realistic outbound movement.
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- Stock-out (Main Warehouse) ---\n";
$dispatch_in_marker = 'Batch for dispatch (demo seed)';
$dispatch_out_marker = 'Direct dispatch (demo seed)';
$stockout_defs = [
['sku' => 'PK-003', 'qty' => 25, 'customer' => 'Siam Retail Co., Ltd.'],
['sku' => 'OF-003', 'qty' => 15, 'customer' => 'Bangkok Mart'],
['sku' => 'EL-004', 'qty' => 10, 'customer' => 'Chao Phraya Trading'],
['sku' => 'BV-002', 'qty' => 20, 'customer' => 'Northern Distribution Partners'],
['sku' => 'PK-001', 'qty' => 40, 'customer' => 'Siam Retail Co., Ltd.'],
['sku' => 'EL-002', 'qty' => 30, 'customer' => 'Bangkok Mart'],
];
foreach ($stockout_defs as $so) {
$p = findProductDef($product_defs, $so['sku']);
$sth = $pdo2->prepare(
"SELECT COUNT(*) FROM `td_stock_{$main_wh_id}`
WHERE company_id = :c AND type = 'out' AND description = :d AND product_sku = :s"
);
$sth->execute([':c' => $company_id, ':d' => $dispatch_out_marker, ':s' => $so['sku']]);
if ((int)$sth->fetchColumn() > 0) {
skip("Dispatch of {$so['sku']} already exists");
continue;
}
$sth = $pdo2->prepare("SELECT id FROM md_contact WHERE company_id = :c AND contact_name = :n LIMIT 1");
$sth->execute([':c' => $company_id, ':n' => $so['customer']]);
$customer_id = (int)$sth->fetchColumn();
$sku = $so['sku'];
$qty = $so['qty'];
$cost = $p['cost'];
$uom = $p['uom'];
dbTransaction($pdo2, function ($pdo2) use (
$stockMgmt, $whMgmt, $company_id, $main_wh_id, $sku, $qty, $cost, $customer_id,
$logging, $dispatch_in_marker, $dispatch_out_marker
) {
$bin = findFreeBin($pdo2, $company_id, $main_wh_id);
$in_uuid = bin2hex(random_bytes(16));
$stock_id = $stockMgmt->saveStockIn([
'id' => 0,
'warehouse' => $main_wh_id,
'product_sku' => $sku,
'quantity' => $qty,
'price' => $cost,
'zone' => $bin,
'aisle' => $bin,
'bin' => $bin,
'contact_id' => $customer_id,
'description' => $dispatch_in_marker,
], $logging, $in_uuid);
$stockMgmt->approveStock($stock_id, $main_wh_id, 'in', $whMgmt);
$out_uuid = bin2hex(random_bytes(16));
$out_id = $stockMgmt->saveStockOut([
'id' => 0,
'warehouse' => $main_wh_id,
'product_sku' => $sku,
'zone' => $bin,
'aisle' => $bin,
'bin' => $bin,
'contact_id' => $customer_id,
'description' => $dispatch_out_marker,
], $logging, $out_uuid);
$stockMgmt->approveStock($out_id, $main_wh_id, 'out', $whMgmt);
});
ok("Shipped {$qty} {$uom} of {$sku} to {$so['customer']}");
}
// ─────────────────────────────────────────────────────────────────────────────
// 8. Stock-transfer — move product from Main Warehouse to Bangna Distribution
// Center. Same batch-then-move approach as stock-out above: receive a
// dedicated small batch into a fresh Main Warehouse bin, then transfer that
// whole batch to a fresh Bangna bin, so opening-stock / restock balances
// in Main Warehouse are left untouched.
// ─────────────────────────────────────────────────────────────────────────────
echo "\n--- Stock-transfer (Main Warehouse -> Bangna Distribution Center) ---\n";
$transfer_in_marker = 'Batch for transfer (demo seed)';
$transfer_out_marker = 'Transfer to Bangna (demo seed)';
$transfer_defs = [
['sku' => 'BV-001', 'qty' => 30],
['sku' => 'PK-002', 'qty' => 12],
['sku' => 'OF-004', 'qty' => 20],
['sku' => 'EL-003', 'qty' => 15],
['sku' => 'OF-002', 'qty' => 25],
['sku' => 'PK-001', 'qty' => 50],
];
foreach ($transfer_defs as $t) {
$p = findProductDef($product_defs, $t['sku']);
$sth = $pdo2->prepare(
"SELECT COUNT(*) FROM `td_stock_{$main_wh_id}`
WHERE company_id = :c AND type = 'transfer' AND description = :d
AND product_sku = :s AND `out` > 0"
);
$sth->execute([':c' => $company_id, ':d' => $transfer_out_marker, ':s' => $t['sku']]);
if ((int)$sth->fetchColumn() > 0) {
skip("Transfer of {$t['sku']} to Bangna Distribution Center already exists");
continue;
}
$sku = $t['sku'];
$qty = $t['qty'];
$cost = $p['cost'];
$uom = $p['uom'];
dbTransaction($pdo2, function ($pdo2) use (
$stockMgmt, $whMgmt, $company_id, $main_wh_id, $bangna_wh_id, $sku, $qty, $cost,
$logging, $transfer_in_marker, $transfer_out_marker
) {
$from_bin = findFreeBin($pdo2, $company_id, $main_wh_id);
$in_uuid = bin2hex(random_bytes(16));
$stock_id = $stockMgmt->saveStockIn([
'id' => 0,
'warehouse' => $main_wh_id,
'product_sku' => $sku,
'quantity' => $qty,
'price' => $cost,
'zone' => $from_bin,
'aisle' => $from_bin,
'bin' => $from_bin,
'contact_id' => 0,
'description' => $transfer_in_marker,
], $logging, $in_uuid);
$stockMgmt->approveStock($stock_id, $main_wh_id, 'in', $whMgmt);
$to_bin = findFreeBin($pdo2, $company_id, $bangna_wh_id);
$transfer_uuid = bin2hex(random_bytes(16));
$transfer_id = $stockMgmt->saveStockTransfer([
'id' => 0,
'warehouse_from' => $main_wh_id,
'zone_from' => $from_bin,
'aisle_from' => $from_bin,
'bin_from' => $from_bin,
'warehouse_to' => $bangna_wh_id,
'zone_to' => $to_bin,
'aisle_to' => $to_bin,
'bin_to' => $to_bin,
'product_sku' => $sku,
'contact_id' => 0,
'description' => $transfer_out_marker,
], $logging, $transfer_uuid);
$stockMgmt->approveStock($transfer_id, $main_wh_id, 'transfer', $whMgmt);
});
ok("Transferred {$qty} {$uom} of {$sku} from Main Warehouse to Bangna Distribution Center");
}
// ─────────────────────────────────────────────────────────────────────────────
echo "\n\033[1m=== Done ===\033[0m\n\n";
echo "Log in at /login/index.php with:\n";
echo " Username: {$demo_username}\n";
echo " Password: {$demo_password}\n";
echo " (No OTP step — the demo company has no SMTP configured.)\n\n";