diff --git a/app/dashboard/index.php b/app/dashboard/index.php index 2045569..3f4db9e 100644 --- a/app/dashboard/index.php +++ b/app/dashboard/index.php @@ -86,7 +86,7 @@

Total Revenue

—

-

Sub-total to date (excl. VAT)

+

Subtotal, excl. VAT

diff --git a/demo_seed.php b/demo_seed.php new file mode 100644 index 0000000..5f6fb6a --- /dev/null +++ b/demo_seed.php @@ -0,0 +1,663 @@ +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"; diff --git a/demo_seed_more_orders.php b/demo_seed_more_orders.php new file mode 100644 index 0000000..dcb4e40 --- /dev/null +++ b/demo_seed_more_orders.php @@ -0,0 +1,134 @@ + + * order -> invoice cycle). This adds 4 more direct orders (no quotation) to + * give the Orders list page realistic volume and status variety: + * - 2 left in Draft (status=0) — never confirmed + * - 2 Confirmed (status=1) — stock-out auto-approved via FIFO bin selection + * + * All writes go through OrderManager (the same class the app itself uses), + * wrapped in dbTransaction(). Safe to re-run: guarded by order.notes markers. + */ + +if (PHP_SAPI !== 'cli') { + http_response_code(403); + exit('Run via CLI only: php demo_seed_more_orders.php'); +} + +$_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/StockManager.php'; +require __DIR__ . '/app/assets/utils/classes/OrderManager.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 buildLineItem(array $products, string $sku, float $qty): array { + $p = $products[$sku]; + $unit_price = (float)$p['price']; + $total_price = round($qty * $unit_price, 4); + $tax_rate = 7.0; + $tax_amount = round($total_price * $tax_rate / 100, 4); + return [ + 'product_sku' => $sku, + 'product_name' => $sku, + 'quantity' => $qty, + 'unit_price' => $unit_price, + 'total_price' => $total_price, + 'tax_amount' => $tax_amount, + 'tax_rate' => $tax_rate, + ]; +} + +echo "\n\033[1m=== mn3wms Demo Data Seeder — more sales orders ===\033[0m\n\n"; + +// ── Resolve shared context (company, warehouse, products, customers, dept) ──── +$sth = $pdo1->prepare("SELECT company_id FROM company_list WHERE channel_name = 'demo' LIMIT 1"); +$sth->execute(); +$company_id = (int)$sth->fetchColumn(); +if (!$company_id) { exit("ERROR: demo company not found — run demo_seed.php first.\n"); } + +$sth = $pdo1->prepare("SELECT user_id FROM user WHERE username = 'admin' LIMIT 1"); +$sth->execute(); +$owner_user_id = (int)$sth->fetchColumn(); + +$sth = $pdo2->prepare("SELECT id FROM md_warehouse WHERE company_id = :c AND warehouse_name = 'Main Warehouse' LIMIT 1"); +$sth->execute([':c' => $company_id]); +$main_wh = (int)$sth->fetchColumn(); + +$sth = $pdo2->prepare("SELECT id FROM md_department WHERE company_id = :c AND dept_code = 'SALES' LIMIT 1"); +$sth->execute([':c' => $company_id]); +$sales_dept_id = (int)$sth->fetchColumn(); +if (!$sales_dept_id) { exit("ERROR: SALES department not found — run demo_seed_transactions.php first.\n"); } + +$sth = $pdo2->prepare("SELECT sku, price FROM md_product WHERE company_id = :c"); +$sth->execute([':c' => $company_id]); +$products = $sth->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE); + +$sth = $pdo2->prepare("SELECT id, contact_name FROM md_contact WHERE company_id = :c AND contact_type = 1 ORDER BY id"); +$sth->execute([':c' => $company_id]); +$customers = $sth->fetchAll(PDO::FETCH_ASSOC); +$customer_ids = array_column($customers, 'id'); + +$logging = ['user_id' => $owner_user_id, 'dt' => date('Y-m-d H:i:s'), 'login' => null, 'action' => 'seed_more_orders']; + +$orderMgmt = new OrderManager($pdo2, $company_id); + +// ── Order definitions ────────────────────────────────────────────────────── +$order_defs = [ + ['marker' => 'Demo seed extra order 1', 'customer_idx' => 1, 'confirm' => false, + 'items' => [['sku' => 'EL-001', 'qty' => 10], ['sku' => 'PK-002', 'qty' => 5]]], + ['marker' => 'Demo seed extra order 2', 'customer_idx' => 2, 'confirm' => true, + 'items' => [['sku' => 'BV-003', 'qty' => 20], ['sku' => 'OF-002', 'qty' => 8]]], + ['marker' => 'Demo seed extra order 3', 'customer_idx' => 3, 'confirm' => true, + 'items' => [['sku' => 'EL-004', 'qty' => 5], ['sku' => 'PK-003', 'qty' => 30]]], + ['marker' => 'Demo seed extra order 4', 'customer_idx' => 0, 'confirm' => false, + 'items' => [['sku' => 'OF-004', 'qty' => 10], ['sku' => 'BV-001', 'qty' => 15]]], +]; + +echo "--- Sales orders ---\n"; + +foreach ($order_defs as $d) { + $sth = $pdo2->prepare("SELECT id, status FROM td_order WHERE company_id = :c AND notes = :n LIMIT 1"); + $sth->execute([':c' => $company_id, ':n' => $d['marker']]); + $existing = $sth->fetch(PDO::FETCH_ASSOC); + + if ($existing) { + skip("{$d['marker']} already exists (id={$existing['id']}, status={$existing['status']})"); + continue; + } + + $customer_id = $customer_ids[$d['customer_idx']]; + $items = array_map( + fn($it) => buildLineItem($products, $it['sku'], $it['qty']) + ['warehouse_id' => $main_wh], + $d['items'] + ); + + $order_id = dbTransaction($pdo2, function ($pdo2) use ($orderMgmt, $customer_id, $sales_dept_id, $items, $logging, $d) { + // No 'source' -> direct WMS order -> starts as Draft (status=0) immediately. + $oid = $orderMgmt->saveOrder([ + 'id' => 0, 'contact_id' => $customer_id, 'department_id' => $sales_dept_id, + 'order_date' => date('Y-m-d'), 'items' => $items, 'discount' => 0, + 'shipping_fee' => 0, 'notes' => $d['marker'], + ], $logging); + + if ($d['confirm']) { + $orderMgmt->confirmOrder($oid, bin2hex(random_bytes(16)), $logging, true); // auto-approve stock-out + } + + return $oid; + }); + + $state = $d['confirm'] ? 'confirmed (stock-out auto-approved)' : 'left as draft'; + ok("Created order {$d['marker']} (id={$order_id}), {$state}"); +} + +echo "\n\033[1m=== Done ===\033[0m\n\n"; diff --git a/demo_seed_transactions.php b/demo_seed_transactions.php new file mode 100644 index 0000000..b9f580f --- /dev/null +++ b/demo_seed_transactions.php @@ -0,0 +1,901 @@ + Bangna) + * 3. Chart of accounts, departments, GL posting formulas + * 4. Sales cycle: quotation -> sales order -> invoice -> GL post + * 5. AR: receipt billing -> receipt -> GL post + * 6. Purchasing cycle: purchase request -> PO -> receive -> purchase invoice -> GL post + * 7. AP: payment billing -> payment -> GL post + * 8. Supplier return (confirmed, restocks reversed) + * 9. Customer return (draft only — see note below) + * 10. Barcode labels for a handful of products + * + * Every write goes through the same Manager classes + engine-file patterns + * the app itself uses (dbTransaction wrapping, GlManager posting exactly as + * order/api/engine/issue_invoice.php and finance/api/engine/manage_receipt.php + * do it), so the data matches what the real UI would have produced. + * + * NOTE — customer returns: ReturnManager::confirmReturn() reads zone/aisle/bin + * from td_return_item rows, but td_return_item has no zone/aisle/bin columns + * (see setup.php) and no engine file back-fills them before calling + * confirmReturn(). Confirming ANY customer return in this app currently + * throws "Location - - - does not exist" from WarehouseManager::occupyBin(). + * This script creates one customer return and leaves it in draft status — + * confirming it is not possible until that's fixed. + * + * Safe to re-run: every insert is guarded by an existence check. + */ + +if (PHP_SAPI !== 'cli') { + http_response_code(403); + exit('Run via CLI only: php demo_seed_transactions.php'); +} + +$_SESSION = []; + +require_once __DIR__ . '/app/config.php'; +require_once __DIR__ . '/app/dbconn.php'; +require_once __DIR__ . '/app/assets/utils/db_helpers.php'; +require_once __DIR__ . '/app/assets/utils/classes/WarehouseManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/StockManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/QuotationManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/OrderManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/InvoiceManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/ReturnManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/PurchaseRequestManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/PurchaseOrderManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/SupplierReturnManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/ReceiptBillingManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/ReceiptManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/PaymentBillingManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/PaymentManager.php'; +require_once __DIR__ . '/app/assets/utils/classes/BarcodeManager.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/ChartOfAccounts.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/DepartmentManager.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/AccountFormulaManager.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/PostingWindowGuard.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/GlManager.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/posting/BasePosting.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/posting/InvoicePosting.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/posting/PurchaseInvoicePosting.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/posting/ReceiptPosting.php'; +require_once __DIR__ . '/app/assets/utils/classes_ac/posting/PaymentPosting.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 warn(string $msg): void { echo "\033[31m !\033[0m {$msg}\n"; } + +echo "\n\033[1m=== mn3wms Demo Transaction Seeder ===\033[0m\n\n"; + +// ───────────────────────────────────────────────────────────────────────────── +// 0. Resolve company / owner / warehouses / contacts seeded by demo_seed.php +// ───────────────────────────────────────────────────────────────────────────── +$sth = $pdo1->prepare("SELECT company_id FROM company_list WHERE channel_name = 'demo' LIMIT 1"); +$sth->execute(); +$company_id = (int)($sth->fetchColumn() ?: 0); +if (!$company_id) exit("ERROR: demo company not found — run demo_seed.php first.\n"); + +$sth = $pdo1->prepare("SELECT user_id FROM user WHERE username = 'admin' LIMIT 1"); +$sth->execute(); +$owner_user_id = (int)($sth->fetchColumn() ?: 0); +if (!$owner_user_id) exit("ERROR: demo owner user not found — run demo_seed.php first.\n"); + +$sth = $pdo2->prepare("SELECT id, warehouse_name FROM md_warehouse WHERE company_id = :c ORDER BY id"); +$sth->execute([':c' => $company_id]); +$warehouses = $sth->fetchAll(PDO::FETCH_KEY_PAIR); // id => name +if (count($warehouses) < 2) exit("ERROR: expected 2 warehouses — run demo_seed.php first.\n"); +$wh_ids = array_keys($warehouses); +$main_wh = $wh_ids[0]; // Main Warehouse +$bangna_wh = $wh_ids[1]; // Bangna Distribution Center + +$sth = $pdo2->prepare("SELECT id, contact_name, contact_type FROM md_contact WHERE company_id = :c ORDER BY id"); +$sth->execute([':c' => $company_id]); +$contacts = $sth->fetchAll(PDO::FETCH_ASSOC); +if (count($contacts) < 7) exit("ERROR: expected 7 contacts — run demo_seed.php first.\n"); +$customer_ids = array_column(array_filter($contacts, fn($c) => (int)$c['contact_type'] === 1), 'id'); +$supplier_ids = array_column(array_filter($contacts, fn($c) => (int)$c['contact_type'] === 2), 'id'); +$customer_ids = array_values($customer_ids); +$supplier_ids = array_values($supplier_ids); + +$sth = $pdo2->prepare("SELECT sku, uom, cost_price, price FROM md_product WHERE company_id = :c ORDER BY id"); +$sth->execute([':c' => $company_id]); +$products = $sth->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE); +if (count($products) < 14) exit("ERROR: expected 14 products — run demo_seed.php first.\n"); + +$logging = ['user_id' => $owner_user_id, 'dt' => date('Y-m-d H:i:s'), 'login' => null, 'action' => 'seed_transactions']; + +$whMgmt = new WarehouseManager($pdo2, $company_id); +$stockMgmt = new StockManager($pdo2, $company_id); + +/** Find the next unused simple-location bin label "A-N" for a warehouse. */ +function nextFreeBin(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 CAST(SUBSTRING(bin, 3) AS UNSIGNED) ASC LIMIT 1" + ); + $sth->execute([':c' => $company_id, ':w' => $warehouse_id]); + $bin = $sth->fetchColumn(); + if (!$bin) throw new Exception("No free bin available in warehouse {$warehouse_id}."); + return $bin; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 1. Additional stock-in replenishment (restock events) +// ───────────────────────────────────────────────────────────────────────────── +echo "--- Restock (additional stock-in) ---\n"; + +$restock_defs = [ + ['sku' => 'EL-001', 'warehouse' => $main_wh, 'qty' => 40, 'supplier_idx' => 0], + ['sku' => 'OF-001', 'warehouse' => $main_wh, 'qty' => 60, 'supplier_idx' => 1], + ['sku' => 'BV-002', 'warehouse' => $bangna_wh, 'qty' => 35, 'supplier_idx' => 2], +]; + +foreach ($restock_defs as $r) { + $sth = $pdo2->prepare( + "SELECT COUNT(*) FROM `td_stock_{$r['warehouse']}` + WHERE company_id = :c AND product_sku = :sku AND type = 'in' AND description = 'Restock (demo seed)'" + ); + $sth->execute([':c' => $company_id, ':sku' => $r['sku']]); + if ((int)$sth->fetchColumn() > 0) { + skip("Restock for {$r['sku']} in warehouse {$r['warehouse']} already exists"); + continue; + } + + $bin = nextFreeBin($pdo2, $company_id, $r['warehouse']); + $supplier_id = $supplier_ids[$r['supplier_idx']]; + $uuid = bin2hex(random_bytes(16)); + $p = $products[$r['sku']]; + + dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $r, $bin, $supplier_id, $logging, $uuid, $p) { + $stock_id = $stockMgmt->saveStockIn([ + 'id' => 0, 'warehouse' => $r['warehouse'], 'product_sku' => $r['sku'], + 'quantity' => $r['qty'], 'price' => $p['cost_price'], + 'zone' => $bin, 'aisle' => $bin, 'bin' => $bin, + 'contact_id' => $supplier_id, 'description' => 'Restock (demo seed)', + ], $logging, $uuid); + $stockMgmt->approveStock($stock_id, $r['warehouse'], 'in', $whMgmt); + }); + ok("Restocked {$r['qty']} {$p['uom']} of {$r['sku']} into {$warehouses[$r['warehouse']]} bin {$bin}"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 2. Stock-out (direct dispatch) — dedicated batch in + full-bin out +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- Stock-out (direct dispatch) ---\n"; + +$dispatch_defs = [ + ['sku' => 'PK-003', 'warehouse' => $main_wh, 'qty' => 25, 'customer_idx' => 0], + ['sku' => 'OF-003', 'warehouse' => $main_wh, 'qty' => 15, 'customer_idx' => 1], +]; + +foreach ($dispatch_defs as $d) { + $sth = $pdo2->prepare( + "SELECT COUNT(*) FROM `td_stock_{$d['warehouse']}` + WHERE company_id = :c AND product_sku = :sku AND type = 'out' AND description = 'Direct dispatch (demo seed)'" + ); + $sth->execute([':c' => $company_id, ':sku' => $d['sku']]); + if ((int)$sth->fetchColumn() > 0) { + skip("Dispatch for {$d['sku']} already exists"); + continue; + } + + $bin = nextFreeBin($pdo2, $company_id, $d['warehouse']); + $customer_id = $customer_ids[$d['customer_idx']]; + $p = $products[$d['sku']]; + + dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $d, $bin, $customer_id, $logging, $p) { + // Receive a dedicated batch first (so we don't touch demo_seed.php's opening-stock bins) + $in_uuid = bin2hex(random_bytes(16)); + $in_id = $stockMgmt->saveStockIn([ + 'id' => 0, 'warehouse' => $d['warehouse'], 'product_sku' => $d['sku'], + 'quantity' => $d['qty'], 'price' => $p['cost_price'], + 'zone' => $bin, 'aisle' => $bin, 'bin' => $bin, + 'contact_id' => $customer_id, 'description' => 'Batch for dispatch (demo seed)', + ], $logging, $in_uuid); + $stockMgmt->approveStock($in_id, $d['warehouse'], 'in', $whMgmt); + + // Dispatch it straight out (saveStockOut takes the whole bin) + $out_uuid = bin2hex(random_bytes(16)); + $out_id = $stockMgmt->saveStockOut([ + 'id' => 0, 'warehouse' => $d['warehouse'], 'product_sku' => $d['sku'], + 'zone' => $bin, 'aisle' => $bin, 'bin' => $bin, + 'contact_id' => $customer_id, 'description' => 'Direct dispatch (demo seed)', + ], $logging, $out_uuid); + $stockMgmt->approveStock($out_id, $d['warehouse'], 'out', $whMgmt); + }); + ok("Dispatched {$d['qty']} {$p['uom']} of {$d['sku']} from {$warehouses[$d['warehouse']]}"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3. Stock transfer — Main Warehouse -> Bangna Distribution Center +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- Stock transfer (Main -> Bangna) ---\n"; + +$transfer_defs = [ + ['sku' => 'BV-001', 'qty' => 30], + ['sku' => 'PK-002', 'qty' => 12], +]; + +foreach ($transfer_defs as $t) { + $sth = $pdo2->prepare( + "SELECT COUNT(*) FROM `td_stock_{$main_wh}` + WHERE company_id = :c AND product_sku = :sku AND type = 'transfer' AND description = 'Transfer to Bangna (demo seed)'" + ); + $sth->execute([':c' => $company_id, ':sku' => $t['sku']]); + if ((int)$sth->fetchColumn() > 0) { + skip("Transfer for {$t['sku']} already exists"); + continue; + } + + // Bin allocation is independent of the transaction below — resolve both up front. + $from_bin = nextFreeBin($pdo2, $company_id, $main_wh); + $to_bin = nextFreeBin($pdo2, $company_id, $bangna_wh); + $p = $products[$t['sku']]; + + dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $t, $from_bin, $to_bin, $main_wh, $bangna_wh, $logging, $p) { + // Receive a dedicated batch first (so we don't touch demo_seed.php's opening-stock bins) + $in_uuid = bin2hex(random_bytes(16)); + $in_id = $stockMgmt->saveStockIn([ + 'id' => 0, 'warehouse' => $main_wh, 'product_sku' => $t['sku'], + 'quantity' => $t['qty'], 'price' => $p['cost_price'], + 'zone' => $from_bin, 'aisle' => $from_bin, 'bin' => $from_bin, + 'contact_id' => 0, 'description' => 'Batch for transfer (demo seed)', + ], $logging, $in_uuid); + $stockMgmt->approveStock($in_id, $main_wh, 'in', $whMgmt); + + $tr_uuid = bin2hex(random_bytes(16)); + $out_id = $stockMgmt->saveStockTransfer([ + 'id' => 0, 'warehouse_from' => $main_wh, 'warehouse_to' => $bangna_wh, + 'product_sku' => $t['sku'], + 'zone_from' => $from_bin, 'aisle_from' => $from_bin, 'bin_from' => $from_bin, + 'zone_to' => $to_bin, 'aisle_to' => $to_bin, 'bin_to' => $to_bin, + 'contact_id' => 0, 'description' => 'Transfer to Bangna (demo seed)', + ], $logging, $tr_uuid); + $stockMgmt->approveStock($out_id, $main_wh, 'transfer', $whMgmt); + }); + ok("Transferred {$t['qty']} {$p['uom']} of {$t['sku']}: {$warehouses[$main_wh]} bin {$from_bin} -> {$warehouses[$bangna_wh]} bin {$to_bin}"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 4. Chart of accounts + departments + GL posting formulas +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- Chart of accounts ---\n"; + +$coa = new ChartOfAccounts($pdo2, $company_id); + +$account_defs = [ + ['code' => '1000', 'name' => 'Cash and Bank', 'type' => 'asset'], + ['code' => '1150', 'name' => 'Input VAT', 'type' => 'asset'], + ['code' => '1200', 'name' => 'Accounts Receivable', 'type' => 'asset'], + ['code' => '2000', 'name' => 'Accounts Payable', 'type' => 'liability'], + ['code' => '2100', 'name' => 'Output VAT', 'type' => 'liability'], + ['code' => '4000', 'name' => 'Sales Revenue', 'type' => 'revenue'], + ['code' => '5000', 'name' => 'Cost of Goods Sold', 'type' => 'expense'], +]; + +$account_ids = []; +foreach ($account_defs as $a) { + $existing = $coa->getByCode($a['code']); + if ($existing) { + skip("Account {$a['code']} — {$a['name']} already exists"); + $account_ids[$a['code']] = (int)$existing['id']; + continue; + } + $id = $coa->create([ + 'account_code' => $a['code'], 'account_name' => $a['name'], 'account_type' => $a['type'], + 'account_category' => '', 'parent_code' => null, 'is_posting' => 1, 'status' => 1, + ]); + $account_ids[$a['code']] = $id; + ok("Created account {$a['code']} — {$a['name']} ({$a['type']})"); +} + +echo "\n--- Departments ---\n"; + +$deptMgmt = new DepartmentManager($pdo2, $company_id); +$dept_defs = [ + ['code' => 'SALES', 'name' => 'Sales'], + ['code' => 'WH', 'name' => 'Warehouse Operations'], +]; +$dept_ids = []; +foreach ($dept_defs as $d) { + $sth = $pdo2->prepare("SELECT id FROM md_department WHERE company_id = :c AND dept_code = :code LIMIT 1"); + $sth->execute([':c' => $company_id, ':code' => $d['code']]); + $id = (int)($sth->fetchColumn() ?: 0); + if ($id) { + skip("Department {$d['code']} already exists (id={$id})"); + } else { + $id = $deptMgmt->create(['dept_code' => $d['code'], 'dept_name' => $d['name'], 'description' => '', 'status' => 1]); + ok("Created department {$d['code']} — {$d['name']} (id={$id})"); + } + $dept_ids[$d['code']] = $id; +} + +echo "\n--- GL posting formulas ---\n"; + +$formulaMgmt = new AccountFormulaManager($pdo2, $company_id); + +// document_type => [formula_name, items[ [drcr, account_code, amount_key, description] ]] +$formula_defs = [ + 'invoice' => [ + 'name' => 'Standard Sales Invoice', + 'items' => [ + ['D', '1200', 'grand_total', 'Accounts Receivable'], + ['C', '4000', 'total', 'Sales Revenue'], + ['C', '2100', 'tax', 'Output VAT'], + ], + ], + 'purchase_invoice' => [ + 'name' => 'Standard Purchase Invoice', + 'items' => [ + ['D', '5000', 'total', 'Cost of Goods Sold'], + ['D', '1150', 'tax', 'Input VAT'], + ['C', '2000', 'grand_total', 'Accounts Payable'], + ], + ], + 'receipt' => [ + 'name' => 'Standard Receipt', + 'items' => [ + ['D', '1000', 'amount', 'Cash and Bank'], + ['C', '1200', 'amount', 'Accounts Receivable'], + ], + ], + 'payment' => [ + 'name' => 'Standard Payment', + 'items' => [ + ['D', '2000', 'amount', 'Accounts Payable'], + ['C', '1000', 'amount', 'Cash and Bank'], + ], + ], +]; + +$formula_ids = []; +foreach ($formula_defs as $doc_type => $def) { + $sth = $pdo2->prepare( + "SELECT id FROM md_account_formula WHERE company_id = :c AND document_type = :t AND is_default = 1 LIMIT 1" + ); + $sth->execute([':c' => $company_id, ':t' => $doc_type]); + $id = (int)($sth->fetchColumn() ?: 0); + if ($id) { + skip("Default formula for '{$doc_type}' already exists (id={$id})"); + } else { + $items = array_map(fn($i) => [ + 'drcr' => $i[0], 'account_code' => $i[1], 'amount_key' => $i[2], 'description' => $i[3], + ], $def['items']); + $id = $formulaMgmt->save([ + 'id' => 0, 'formula_name' => $def['name'], 'document_type' => $doc_type, + 'description' => '', 'is_default' => 1, 'status' => 1, 'items' => $items, + ]); + ok("Created default GL formula for '{$doc_type}': {$def['name']} (id={$id})"); + } + $formula_ids[$doc_type] = $id; +} + +/** Post (or replace) GL for a document, mirroring order/api/engine/issue_invoice.php exactly. */ +function postGl(PDO $pdo1, PDO $pdo2, int $company_id, string $doc_type, int $doc_id, string $posting_class): array { + require_once __DIR__ . "/app/assets/utils/classes_ac/posting/{$posting_class}.php"; + $posting = new $posting_class($pdo2, $company_id); + $built = $posting->build($doc_id, null); + $guard = new PostingWindowGuard($pdo1, $company_id); + $gl = new GlManager($pdo2, $company_id, $guard); + $meta = ['journal_date' => $built['doc_date'] ?? null]; + $existing = $gl->getBySource($doc_type, $doc_id); + + $pdo2->beginTransaction(); + if ($existing) { + $gl->replace($doc_type, $doc_id, $built['formula_id'], $built['period'], $built['lines'], $meta); + $pdo2->commit(); + return ['action' => 'replaced', 'lines' => count($built['lines'])]; + } + $gl->post($doc_type, $doc_id, $built['formula_id'], $built['period'], $built['lines'], $meta); + $pdo2->commit(); + return ['action' => 'posted', 'lines' => count($built['lines'])]; +} + +/** Build a quotation/order/PR/PO line item with 7% VAT from a product SKU + qty. */ +function buildLineItem(array $products, string $sku, float $qty): array { + $p = $products[$sku]; + $unit_price = (float)$p['price']; + $total_price = round($qty * $unit_price, 4); + $tax_rate = 7.0; + $tax_amount = round($total_price * $tax_rate / 100, 4); + return [ + 'product_sku' => $sku, + 'product_name' => $sku, // demo_seed.php products keyed by SKU; name not needed for GL/report correctness + 'quantity' => $qty, + 'unit_price' => $unit_price, + 'total_price' => $total_price, + 'tax_amount' => $tax_amount, + 'tax_rate' => $tax_rate, + ]; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 5. Sales cycle: quotation -> sales order -> invoice -> GL post +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- Sales cycle (quotation -> order -> invoice) ---\n"; + +$quotMgmt = new QuotationManager($pdo2, $company_id); +$orderMgmt = new OrderManager($pdo2, $company_id); +$invMgmt = new InvoiceManager($pdo2, $company_id); + +// NOTE: OrderManager::linkQuotationToOrder() is broken — it writes to a +// td_quotation.order_id column that does not exist in the schema (see +// setup.php's td_quotation definition). We never call it. The real link +// between a quotation and its order is source='quotation'/source_id=$qid on +// td_order, which QuotationManager::getById() already joins on — that's what +// this script relies on too, both to link and to look the link back up. + +$sth = $pdo2->prepare("SELECT id FROM td_quotation WHERE company_id = :c AND notes = 'Demo seed sales cycle' LIMIT 1"); +$sth->execute([':c' => $company_id]); +$sales_quotation_id = (int)($sth->fetchColumn() ?: 0); + +if ($sales_quotation_id) { + skip("Demo sales-cycle quotation already exists (id={$sales_quotation_id})"); +} else { + $sales_items = [ + buildLineItem($products, 'EL-002', 15), + buildLineItem($products, 'OF-001', 25), + ]; + + $sales_quotation_id = dbTransaction($pdo2, function ($pdo2) use ($quotMgmt, $customer_ids, $dept_ids, $sales_items, $logging) { + $qid = $quotMgmt->save([ + 'id' => 0, 'contact_id' => $customer_ids[0], 'department_id' => $dept_ids['SALES'], + 'quotation_date' => date('Y-m-d'), 'valid_until' => date('Y-m-d', strtotime('+30 days')), + 'items' => $sales_items, 'discount' => 0, 'notes' => 'Demo seed sales cycle', + ], $logging); + $quotMgmt->updateStatus($qid, 'send', $logging); + $quotMgmt->updateStatus($qid, 'accept', $logging); + return $qid; + }); + ok("Created + accepted quotation (id={$sales_quotation_id})"); +} + +$sth = $pdo2->prepare("SELECT id FROM td_order WHERE company_id = :c AND source = 'quotation' AND source_id = :qid LIMIT 1"); +$sth->execute([':c' => $company_id, ':qid' => $sales_quotation_id]); +$sales_order_id = (int)($sth->fetchColumn() ?: 0); + +if ($sales_order_id) { + skip("Order converted from demo quotation already exists (id={$sales_order_id})"); +} else { + // Re-derive line items from the quotation itself (works whether the + // quotation was just created above or already existed from a prior run). + $sth = $pdo2->prepare( + "SELECT item_id, product_sku, product_name, quantity, unit_price, total_price, tax_amount, tax_rate + FROM td_quotation_item WHERE quotation_id = :qid AND company_id = :c ORDER BY item_id" + ); + $sth->execute([':qid' => $sales_quotation_id, ':c' => $company_id]); + $order_items = array_map(fn($it) => $it + ['warehouse_id' => $main_wh], $sth->fetchAll(PDO::FETCH_ASSOC)); + + $sales_order_id = dbTransaction($pdo2, function ($pdo2) use ($orderMgmt, $quotMgmt, $customer_ids, $dept_ids, $order_items, $sales_quotation_id, $logging) { + // Insert with source='quotation' -> starts as status=-2 (pending warehouse assignment) + $oid = $orderMgmt->saveOrder([ + 'id' => 0, 'source' => 'quotation', 'source_id' => $sales_quotation_id, + 'contact_id' => $customer_ids[0], 'department_id' => $dept_ids['SALES'], + 'order_date' => date('Y-m-d'), 'items' => $order_items, 'discount' => 0, + 'shipping_fee' => 0, 'notes' => 'Demo seed sales cycle', + ], $logging); + // Re-save with warehouse_id already set on every item -> auto-promotes -2 to draft (0) + $orderMgmt->saveOrder([ + 'id' => $oid, 'contact_id' => $customer_ids[0], 'department_id' => $dept_ids['SALES'], + 'order_date' => date('Y-m-d'), 'items' => $order_items, 'discount' => 0, + 'shipping_fee' => 0, 'notes' => 'Demo seed sales cycle', + ], $logging); + + $quotMgmt->incrementConvertedQty($sales_quotation_id, array_map( + fn($it) => ['item_id' => $it['item_id'], 'quantity' => $it['quantity']], $order_items + )); + + $orderMgmt->confirmOrder($oid, bin2hex(random_bytes(16)), $logging, true); // auto-approve stock-out + return $oid; + }); + ok("Created + confirmed sales order from quotation (id={$sales_order_id}), stock-out auto-approved"); +} + +$sth = $pdo2->prepare("SELECT id, status FROM td_invoice WHERE company_id = :c AND order_id = :oid AND doc_type = 'invoice' LIMIT 1"); +$sth->execute([':c' => $company_id, ':oid' => $sales_order_id]); +$inv_row = $sth->fetch(PDO::FETCH_ASSOC); + +if ($inv_row) { + skip("Invoice for sales order already exists (id={$inv_row['id']})"); + $sales_invoice_id = (int)$inv_row['id']; +} else { + $sales_invoice_id = dbTransaction($pdo2, function ($pdo2) use ($invMgmt, $sales_order_id, $logging) { + $iid = $invMgmt->createFromOrder($sales_order_id, $logging); + $invMgmt->issueInvoice($iid, $logging, date('Y-m-d', strtotime('+30 days'))); + return $iid; + }); + ok("Created + issued invoice from sales order (id={$sales_invoice_id})"); +} + +$existing_gl = (new GlManager($pdo2, $company_id))->getBySource('invoice', $sales_invoice_id); +if ($existing_gl) { + skip("GL already posted for invoice #{$sales_invoice_id}"); +} else { + $res = postGl($pdo1, $pdo2, $company_id, 'invoice', $sales_invoice_id, 'InvoicePosting'); + ok("GL {$res['action']} for invoice #{$sales_invoice_id} ({$res['lines']} lines)"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 6. AR: receipt billing -> receipt -> GL post +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- AR (receipt billing -> receipt) ---\n"; + +$receiptBillingMgmt = new ReceiptBillingManager($pdo2, $company_id); +$receiptMgmt = new ReceiptManager($pdo2, $company_id); + +$sth = $pdo2->prepare("SELECT id FROM td_receipt_billing WHERE company_id = :c AND notes = 'Demo seed AR' LIMIT 1"); +$sth->execute([':c' => $company_id]); +$receipt_billing_id = (int)($sth->fetchColumn() ?: 0); + +if ($receipt_billing_id) { + skip("Demo receipt billing already exists (id={$receipt_billing_id})"); +} else { + $sth = $pdo2->prepare("SELECT grand_total FROM td_invoice WHERE id = :id AND company_id = :c LIMIT 1"); + $sth->execute([':id' => $sales_invoice_id, ':c' => $company_id]); + $invoice_total = (float)$sth->fetchColumn(); + + $receipt_billing_id = dbTransaction($pdo2, function ($pdo2) use ($receiptBillingMgmt, $customer_ids, $sales_invoice_id, $invoice_total, $logging) { + return $receiptBillingMgmt->createBilling([ + 'contact_id' => $customer_ids[0], 'billing_date' => date('Y-m-d'), 'notes' => 'Demo seed AR', + 'allocations' => [['invoice_id' => $sales_invoice_id, 'amount' => $invoice_total]], + ], $logging); + }); + ok("Created receipt billing for invoice #{$sales_invoice_id} (id={$receipt_billing_id}), amount " . number_format($invoice_total, 2)); +} + +$sth = $pdo2->prepare("SELECT id FROM td_receipt WHERE company_id = :c AND receipt_billing_id = :bid LIMIT 1"); +$sth->execute([':c' => $company_id, ':bid' => $receipt_billing_id]); +$receipt_id = (int)($sth->fetchColumn() ?: 0); + +if ($receipt_id) { + skip("Receipt for billing #{$receipt_billing_id} already exists (id={$receipt_id})"); +} else { + $sth = $pdo2->prepare("SELECT contact_id, amount FROM td_receipt_billing WHERE id = :id LIMIT 1"); + $sth->execute([':id' => $receipt_billing_id]); + $billing = $sth->fetch(PDO::FETCH_ASSOC); + + $receipt_id = dbTransaction($pdo2, function ($pdo2) use ($receiptMgmt, $billing, $receipt_billing_id, $logging) { + return $receiptMgmt->createReceipt([ + 'receipt_billing_id' => $receipt_billing_id, 'receipt_date' => date('Y-m-d'), + 'payment_method' => 'bank_transfer', 'amount' => $billing['amount'], 'notes' => 'Demo seed AR', + ], $logging); + }); + ok("Created receipt #{$receipt_id} for billing #{$receipt_billing_id}, amount " . number_format($billing['amount'], 2)); +} + +$existing_gl = (new GlManager($pdo2, $company_id))->getBySource('receipt', $receipt_id); +if ($existing_gl) { + skip("GL already posted for receipt #{$receipt_id}"); +} else { + $res = postGl($pdo1, $pdo2, $company_id, 'receipt', $receipt_id, 'ReceiptPosting'); + ok("GL {$res['action']} for receipt #{$receipt_id} ({$res['lines']} lines)"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 7. Purchasing cycle: purchase request -> PO -> receive -> purchase invoice -> GL post +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- Purchasing cycle (request -> PO -> receipt -> invoice) ---\n"; + +$prMgmt = new PurchaseRequestManager($pdo2, $company_id); +$poMgmt = new PurchaseOrderManager($pdo2, $company_id); + +$sth = $pdo2->prepare("SELECT id FROM td_purchase_request WHERE company_id = :c AND notes = 'Demo seed purchasing cycle' LIMIT 1"); +$sth->execute([':c' => $company_id]); +$pr_id = (int)($sth->fetchColumn() ?: 0); + +if ($pr_id) { + skip("Demo purchase request already exists (id={$pr_id})"); +} else { + $pr_items = [ + buildLineItem($products, 'EL-003', 20), + buildLineItem($products, 'PK-001', 100), + ]; + $pr_id = dbTransaction($pdo2, function ($pdo2) use ($prMgmt, $supplier_ids, $dept_ids, $pr_items, $logging) { + $rid = $prMgmt->save([ + 'id' => 0, 'contact_id' => $supplier_ids[0], 'department_id' => $dept_ids['WH'], + 'request_date' => date('Y-m-d'), 'required_date' => date('Y-m-d', strtotime('+14 days')), + 'items' => $pr_items, 'discount' => 0, 'shipping_fee' => 0, 'notes' => 'Demo seed purchasing cycle', + ], $logging); + $prMgmt->updateStatus($rid, 'submit', $logging); + $prMgmt->updateStatus($rid, 'approve', $logging); + return $rid; + }); + ok("Created + approved purchase request (id={$pr_id})"); +} + +$sth = $pdo2->prepare("SELECT id FROM td_purchase_order WHERE company_id = :c AND source = 'purchase_request' AND source_id = :rid LIMIT 1"); +$sth->execute([':c' => $company_id, ':rid' => $pr_id]); +$po_id = (int)($sth->fetchColumn() ?: 0); + +if ($po_id) { + skip("PO converted from demo purchase request already exists (id={$po_id})"); +} else { + $sth = $pdo2->prepare( + "SELECT item_id, product_sku, product_name, quantity, unit_price, total_price, tax_amount, tax_rate + FROM td_purchase_request_item WHERE request_id = :rid AND company_id = :c ORDER BY item_id" + ); + $sth->execute([':rid' => $pr_id, ':c' => $company_id]); + $po_items = $sth->fetchAll(PDO::FETCH_ASSOC); + + $po_id = dbTransaction($pdo2, function ($pdo2) use ($poMgmt, $prMgmt, $supplier_ids, $dept_ids, $po_items, $pr_id, $main_wh, $logging) { + $pid = $poMgmt->savePo([ + 'id' => 0, 'source' => 'purchase_request', 'source_id' => $pr_id, + 'contact_id' => $supplier_ids[0], 'department_id' => $dept_ids['WH'], + 'po_date' => date('Y-m-d'), 'expected_date' => date('Y-m-d', strtotime('+7 days')), + 'warehouse_id' => $main_wh, 'items' => $po_items, 'discount' => 0, + 'shipping_fee' => 0, 'notes' => 'Demo seed purchasing cycle', + ], $logging); + // Insert with source set -> status=-2 (pending). Re-save with warehouse_id + // present -> auto-promotes to draft (0), same two-step dance as saveOrder(). + $poMgmt->savePo([ + 'id' => $pid, 'contact_id' => $supplier_ids[0], 'department_id' => $dept_ids['WH'], + 'po_date' => date('Y-m-d'), 'expected_date' => date('Y-m-d', strtotime('+7 days')), + 'warehouse_id' => $main_wh, 'items' => $po_items, 'discount' => 0, + 'shipping_fee' => 0, 'notes' => 'Demo seed purchasing cycle', + ], $logging); + $poMgmt->confirmPo($pid, $logging); + + $prMgmt->incrementConvertedQty($pr_id, array_map( + fn($it) => ['item_id' => $it['item_id'], 'quantity' => $it['quantity']], $po_items + )); + return $pid; + }); + ok("Created + confirmed PO from purchase request (id={$po_id})"); +} + +$sth = $pdo2->prepare( + "SELECT COUNT(*) FROM td_purchase_order_item WHERE order_id = :pid AND company_id = :c AND received_qty > 0" +); +$sth->execute([':pid' => $po_id, ':c' => $company_id]); +if ((int)$sth->fetchColumn() > 0) { + skip("PO #{$po_id} already received"); +} else { + $sth = $pdo2->prepare( + "SELECT item_id, product_sku, unit_price, quantity FROM td_purchase_order_item + WHERE order_id = :pid AND company_id = :c ORDER BY item_id" + ); + $sth->execute([':pid' => $po_id, ':c' => $company_id]); + $po_line_items = $sth->fetchAll(PDO::FETCH_ASSOC); + + // Grab as many free bins as line items in one query — none of these are + // occupied until receivePo() runs, so a single up-front list (rather than + // repeated nextFreeBin() calls, which would all see the same still-empty + // bin and collide) is what keeps each line's location distinct. + $sth = $pdo2->prepare( + "SELECT bin FROM md_bin WHERE company_id = :c AND warehouse = :w AND product_sku IS NULL + ORDER BY CAST(SUBSTRING(bin, 3) AS UNSIGNED) ASC" + ); + $sth->execute([':c' => $company_id, ':w' => $main_wh]); + $free_bins = array_slice($sth->fetchAll(PDO::FETCH_COLUMN), 0, count($po_line_items)); + if (count($free_bins) < count($po_line_items)) { + throw new Exception("Not enough free bins in {$warehouses[$main_wh]} to receive PO #{$po_id}."); + } + + $receive_items = []; + foreach ($po_line_items as $i => $it) { + $bin = $free_bins[$i]; + $receive_items[] = [ + 'item_id' => (int)$it['item_id'], 'product_sku' => $it['product_sku'], + 'warehouse_id' => $main_wh, 'quantity' => (float)$it['quantity'], + 'unit_price' => (float)$it['unit_price'], + 'zone' => $bin, 'aisle' => $bin, 'bin' => $bin, + 'contact_id' => $supplier_ids[0], + ]; + } + + $po_uuid = bin2hex(random_bytes(16)); + dbTransaction($pdo2, function ($pdo2) use ($poMgmt, $whMgmt, $po_id, $receive_items, $po_uuid, $logging) { + $poMgmt->receivePo($po_id, $receive_items, $po_uuid, $logging, true); // auto-approve stock-in + }); + ok("Received PO #{$po_id} — " . count($receive_items) . " line(s) into {$warehouses[$main_wh]}, stock-in auto-approved"); +} + +$sth = $pdo2->prepare( + "SELECT id FROM td_invoice WHERE company_id = :c AND source = 'po' AND source_id = :pid AND doc_type = 'purchase_invoice' LIMIT 1" +); +$sth->execute([':c' => $company_id, ':pid' => $po_id]); +$purchase_invoice_id = (int)($sth->fetchColumn() ?: 0); + +if ($purchase_invoice_id) { + skip("Purchase invoice for PO already exists (id={$purchase_invoice_id})"); +} else { + $purchase_invoice_id = dbTransaction($pdo2, function ($pdo2) use ($invMgmt, $po_id, $logging) { + $iid = $invMgmt->createFromPo($po_id, $logging); + $invMgmt->issueInvoice($iid, $logging, date('Y-m-d', strtotime('+30 days'))); + return $iid; + }); + ok("Created + issued purchase invoice from PO (id={$purchase_invoice_id})"); +} + +$existing_gl = (new GlManager($pdo2, $company_id))->getBySource('purchase_invoice', $purchase_invoice_id); +if ($existing_gl) { + skip("GL already posted for purchase invoice #{$purchase_invoice_id}"); +} else { + $res = postGl($pdo1, $pdo2, $company_id, 'purchase_invoice', $purchase_invoice_id, 'PurchaseInvoicePosting'); + ok("GL {$res['action']} for purchase invoice #{$purchase_invoice_id} ({$res['lines']} lines)"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 8. AP: payment billing -> payment -> GL post +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- AP (payment billing -> payment) ---\n"; + +$paymentBillingMgmt = new PaymentBillingManager($pdo2, $company_id); +$paymentMgmt = new PaymentManager($pdo2, $company_id); + +$sth = $pdo2->prepare("SELECT id FROM td_payment_billing WHERE company_id = :c AND notes = 'Demo seed AP' LIMIT 1"); +$sth->execute([':c' => $company_id]); +$payment_billing_id = (int)($sth->fetchColumn() ?: 0); + +if ($payment_billing_id) { + skip("Demo payment billing already exists (id={$payment_billing_id})"); +} else { + $sth = $pdo2->prepare("SELECT grand_total FROM td_invoice WHERE id = :id AND company_id = :c LIMIT 1"); + $sth->execute([':id' => $purchase_invoice_id, ':c' => $company_id]); + $po_invoice_total = (float)$sth->fetchColumn(); + + $payment_billing_id = dbTransaction($pdo2, function ($pdo2) use ($paymentBillingMgmt, $supplier_ids, $purchase_invoice_id, $po_invoice_total, $logging) { + return $paymentBillingMgmt->createBilling([ + 'contact_id' => $supplier_ids[0], 'billing_date' => date('Y-m-d'), 'notes' => 'Demo seed AP', + 'allocations' => [['invoice_id' => $purchase_invoice_id, 'amount' => $po_invoice_total]], + ], $logging); + }); + ok("Created payment billing for purchase invoice #{$purchase_invoice_id} (id={$payment_billing_id}), amount " . number_format($po_invoice_total, 2)); +} + +$sth = $pdo2->prepare("SELECT id FROM td_payment WHERE company_id = :c AND payment_billing_id = :bid LIMIT 1"); +$sth->execute([':c' => $company_id, ':bid' => $payment_billing_id]); +$payment_id = (int)($sth->fetchColumn() ?: 0); + +if ($payment_id) { + skip("Payment for billing #{$payment_billing_id} already exists (id={$payment_id})"); +} else { + $sth = $pdo2->prepare("SELECT contact_id, amount FROM td_payment_billing WHERE id = :id LIMIT 1"); + $sth->execute([':id' => $payment_billing_id]); + $pbilling = $sth->fetch(PDO::FETCH_ASSOC); + + $payment_id = dbTransaction($pdo2, function ($pdo2) use ($paymentMgmt, $pbilling, $payment_billing_id, $logging) { + return $paymentMgmt->createPayment([ + 'payment_billing_id' => $payment_billing_id, 'payment_date' => date('Y-m-d'), + 'payment_method' => 'bank_transfer', 'amount' => $pbilling['amount'], 'notes' => 'Demo seed AP', + ], $logging); + }); + ok("Created payment #{$payment_id} for billing #{$payment_billing_id}, amount " . number_format($pbilling['amount'], 2)); +} + +$existing_gl = (new GlManager($pdo2, $company_id))->getBySource('payment', $payment_id); +if ($existing_gl) { + skip("GL already posted for payment #{$payment_id}"); +} else { + $res = postGl($pdo1, $pdo2, $company_id, 'payment', $payment_id, 'PaymentPosting'); + ok("GL {$res['action']} for payment #{$payment_id} ({$res['lines']} lines)"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 9. Supplier return — return the full PK-001 line from the demo PO +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- Supplier return ---\n"; + +$supReturnMgmt = new SupplierReturnManager($pdo2, $company_id); + +$sth = $pdo2->prepare("SELECT id FROM td_supplier_return WHERE company_id = :c AND po_id = :pid LIMIT 1"); +$sth->execute([':c' => $company_id, ':pid' => $po_id]); +$supplier_return_id = (int)($sth->fetchColumn() ?: 0); + +if ($supplier_return_id) { + skip("Demo supplier return already exists (id={$supplier_return_id})"); +} else { + // td_purchase_order_item has no warehouse_id column — the PO is single-warehouse + // (td_purchase_order.warehouse_id), which is $main_wh here (set when the PO was created). + $sth = $pdo2->prepare( + "SELECT item_id, product_sku, product_name, quantity, unit_price, total_price, tax_amount, tax_rate, + stock_in_id + FROM td_purchase_order_item + WHERE order_id = :pid AND company_id = :c AND product_sku = 'PK-001' LIMIT 1" + ); + $sth->execute([':pid' => $po_id, ':c' => $company_id]); + $po_line = $sth->fetch(PDO::FETCH_ASSOC); + + $sth = $pdo2->prepare("SELECT zone, aisle, bin FROM `td_stock_{$main_wh}` WHERE id = :id AND company_id = :c LIMIT 1"); + $sth->execute([':id' => $po_line['stock_in_id'], ':c' => $company_id]); + $bin_row = $sth->fetch(PDO::FETCH_ASSOC); + + $return_item = [ + 'product_sku' => $po_line['product_sku'], 'product_name' => $po_line['product_name'], + 'quantity' => $po_line['quantity'], 'unit_price' => $po_line['unit_price'], + 'total_price' => $po_line['total_price'], 'tax_amount' => $po_line['tax_amount'], + 'tax_rate' => $po_line['tax_rate'], 'warehouse_id' => $main_wh, + 'stock_in_id' => $po_line['stock_in_id'], + 'zone' => $bin_row['zone'], 'aisle' => $bin_row['aisle'], 'bin' => $bin_row['bin'], + ]; + + $supplier_return_id = dbTransaction($pdo2, function ($pdo2) use ($supReturnMgmt, $whMgmt, $supplier_ids, $dept_ids, $return_item, $po_id, $logging) { + $rid = $supReturnMgmt->saveReturn([ + 'id' => 0, 'po_id' => $po_id, 'contact_id' => $supplier_ids[0], 'department_id' => $dept_ids['WH'], + 'return_date' => date('Y-m-d'), 'reason' => 'Damaged on arrival (demo seed)', + 'items' => [$return_item], 'tax_adjustment' => 0, 'notes' => 'Demo seed supplier return', + ], $logging); + $supReturnMgmt->confirmReturn($rid, bin2hex(random_bytes(16)), $logging, $whMgmt, true); // auto-approve + return $rid; + }); + ok("Created + confirmed supplier return for {$return_item['quantity']} x {$return_item['product_sku']} (id={$supplier_return_id})"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 10. Customer return — draft only (see NOTE at top of this file: confirming +// customer returns is currently broken in the app itself). +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- Customer return (draft only — confirming is currently broken, see file header) ---\n"; + +$returnMgmt = new ReturnManager($pdo2, $company_id); + +$sth = $pdo2->prepare("SELECT id FROM td_return WHERE company_id = :c AND notes = 'Demo seed customer return' LIMIT 1"); +$sth->execute([':c' => $company_id]); +$customer_return_id = (int)($sth->fetchColumn() ?: 0); + +if ($customer_return_id) { + skip("Demo customer return already exists (id={$customer_return_id})"); +} else { + // Return 5 units of EL-002 against the demo sales order/invoice. + $sth = $pdo2->prepare( + "SELECT item_id, product_sku, product_name, unit_price, tax_rate, warehouse_id, stock_out_id + FROM td_order_item WHERE order_id = :oid AND company_id = :c AND product_sku = 'EL-002' LIMIT 1" + ); + $sth->execute([':oid' => $sales_order_id, ':c' => $company_id]); + $order_line = $sth->fetch(PDO::FETCH_ASSOC); + + $ret_qty = 5.0; + $ret_total = round($ret_qty * (float)$order_line['unit_price'], 4); + $ret_tax = round($ret_total * (float)$order_line['tax_rate'] / 100, 4); + + $customer_return_id = dbTransaction($pdo2, function ($pdo2) use ($returnMgmt, $customer_ids, $order_line, $ret_qty, $ret_total, $ret_tax, $sales_order_id, $sales_invoice_id, $logging) { + return $returnMgmt->saveReturn([ + 'id' => 0, 'order_id' => $sales_order_id, 'invoice_id' => $sales_invoice_id, + 'contact_id' => $customer_ids[0], 'return_date' => date('Y-m-d'), + 'reason' => 'Customer changed mind (demo seed)', 'tax_adjustment' => 0, + 'notes' => 'Demo seed customer return', + 'items' => [[ + 'product_sku' => $order_line['product_sku'], 'product_name' => $order_line['product_name'], + 'quantity' => $ret_qty, 'unit_price' => $order_line['unit_price'], + 'total_price' => $ret_total, 'tax_amount' => $ret_tax, 'tax_rate' => $order_line['tax_rate'], + 'warehouse_id' => $order_line['warehouse_id'], 'stock_out_id' => $order_line['stock_out_id'], + 'stock_out_warehouse_id' => $order_line['warehouse_id'], + ]], + ], $logging); + }); + ok("Created draft customer return for {$ret_qty} x {$order_line['product_sku']} (id={$customer_return_id}) — left in draft, not confirmed"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 11. Barcode labels for a handful of products +// ───────────────────────────────────────────────────────────────────────────── +echo "\n--- Barcode labels ---\n"; + +$barcodeMgmt = new BarcodeManager($pdo2, $company_id); +$label_skus = ['EL-001', 'EL-002', 'OF-001', 'BV-001', 'PK-001']; + +foreach ($label_skus as $sku) { + $sth = $pdo2->prepare( + "SELECT COUNT(*) FROM md_barcode WHERE company_id = :c AND barcode_type = 'sku' AND product_sku = :sku" + ); + $sth->execute([':c' => $company_id, ':sku' => $sku]); + if ((int)$sth->fetchColumn() > 0) { + skip("SKU label for {$sku} already exists"); + continue; + } + $label = dbTransaction($pdo2, function ($pdo2) use ($barcodeMgmt, $sku) { + return $barcodeMgmt->createSkuLabel($sku, '', '', true); + }); + ok("Created SKU label for {$sku}: {$label['barcode']}"); +} + +echo "\n\033[1m=== Done ===\033[0m\n\n"; diff --git a/nodejs/logs/scheduler-error.log b/nodejs/logs/scheduler-error.log index 9fbc691..373e447 100644 --- a/nodejs/logs/scheduler-error.log +++ b/nodejs/logs/scheduler-error.log @@ -16,3 +16,6 @@ 2026-08-03T10:11:12: [2026-08-03T03:11:12.241Z] [PID: 477] [NODE-CRON] [WARN] missed execution at Mon Aug 03 2026 10:00:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. 2026-08-03T10:31:46: [2026-08-03T03:31:46.573Z] [PID: 477] [NODE-CRON] [WARN] missed execution at Mon Aug 03 2026 10:30:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. 2026-08-03T11:08:26: [2026-08-03T04:08:26.686Z] [PID: 477] [NODE-CRON] [WARN] missed execution at Mon Aug 03 2026 11:00:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. +2026-08-04T15:02:31: [2026-08-04T08:02:31.243Z] [PID: 78443] [NODE-CRON] [WARN] missed execution at Tue Aug 04 2026 15:00:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. +2026-08-04T15:32:15: [2026-08-04T08:32:15.561Z] [PID: 78443] [NODE-CRON] [WARN] missed execution at Tue Aug 04 2026 15:30:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. +2026-08-04T17:01:03: [2026-08-04T10:01:03.525Z] [PID: 78443] [NODE-CRON] [WARN] missed execution at Tue Aug 04 2026 17:00:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. diff --git a/nodejs/logs/scheduler.log b/nodejs/logs/scheduler.log index bb3ca5a..7b38dd2 100644 --- a/nodejs/logs/scheduler.log +++ b/nodejs/logs/scheduler.log @@ -48,3 +48,11 @@ 2026-08-03T10:11:12: [2026-08-03T03:11:12.241Z] [PID: 477] [NODE-CRON] [WARN] missed execution at Mon Aug 03 2026 10:00:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. 2026-08-03T10:31:46: [2026-08-03T03:31:46.573Z] [PID: 477] [NODE-CRON] [WARN] missed execution at Mon Aug 03 2026 10:30:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. 2026-08-03T11:08:26: [2026-08-03T04:08:26.686Z] [PID: 477] [NODE-CRON] [WARN] missed execution at Mon Aug 03 2026 11:00:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. +2026-08-04T13:35:14: [scheduler] running — etl_gl + etl_stock (hourly) | low_stock + overdue_invoices alerts (daily) +2026-08-04T15:02:31: [2026-08-04T08:02:31.243Z] [PID: 78443] [NODE-CRON] [WARN] missed execution at Tue Aug 04 2026 15:00:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. +2026-08-04T15:32:15: [2026-08-04T08:32:15.561Z] [PID: 78443] [NODE-CRON] [WARN] missed execution at Tue Aug 04 2026 15:30:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. +2026-08-04T16:00:00: [scheduler] etl_gl slot=16 +2026-08-04T16:00:00: [scheduler] etl_gl slot=16 — no companies +2026-08-04T16:30:00: [scheduler] etl_stock slot=16 +2026-08-04T16:30:00: [scheduler] etl_stock slot=16 — no companies +2026-08-04T17:01:03: [2026-08-04T10:01:03.525Z] [PID: 78443] [NODE-CRON] [WARN] missed execution at Tue Aug 04 2026 17:00:00 GMT+0700 (Indochina Time)! Possible blocking IO or high CPU user at the same process used by node-cron. diff --git a/nodejs/logs/socket.log b/nodejs/logs/socket.log index 440c3c9..61a8b42 100644 --- a/nodejs/logs/socket.log +++ b/nodejs/logs/socket.log @@ -135,3 +135,121 @@ 2026-08-03T10:54:19: [connect] socket=FMnx-fmSk96rxIfwAAAL company=1 user=1 role=owner 2026-08-03T11:13:57: [disconnect] socket=FMnx-fmSk96rxIfwAAAL 2026-08-03T11:13:59: [connect] socket=LgVxBoN_6JuJtxHyAAAN company=1 user=1 role=owner +2026-08-04T13:35:14: Node.js real-time server running on port 3000 +2026-08-04T13:36:21: [connect] socket=BOvxSU23giBsnIXWAAAB company=1 user=1 role=owner +2026-08-04T13:36:25: [disconnect] socket=BOvxSU23giBsnIXWAAAB +2026-08-04T13:36:25: [connect] socket=pJXUXD7HNX_V9peAAAAD company=1 user=1 role=owner +2026-08-04T13:36:27: [disconnect] socket=pJXUXD7HNX_V9peAAAAD +2026-08-04T13:36:27: [connect] socket=St7RkiRumWpwc47xAAAF company=1 user=1 role=owner +2026-08-04T13:36:28: [disconnect] socket=St7RkiRumWpwc47xAAAF +2026-08-04T13:36:28: [connect] socket=a9NsNFmUpIL1vW8uAAAH company=1 user=1 role=owner +2026-08-04T13:40:28: [disconnect] socket=a9NsNFmUpIL1vW8uAAAH +2026-08-04T13:40:28: [connect] socket=uYLFddlhCkOxrcJNAAAJ company=1 user=1 role=owner +2026-08-04T13:48:10: [disconnect] socket=uYLFddlhCkOxrcJNAAAJ +2026-08-04T13:48:11: [connect] socket=VekC6UabiJABBbjhAAAL company=1 user=1 role=owner +2026-08-04T13:50:28: [disconnect] socket=VekC6UabiJABBbjhAAAL +2026-08-04T13:50:29: [connect] socket=gj-glld-ZsxWbGQuAAAN company=1 user=1 role=owner +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 2, type: 'in' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'transfer' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:08:49: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'transfer' } +2026-08-04T14:11:36: [disconnect] socket=gj-glld-ZsxWbGQuAAAN +2026-08-04T14:11:36: [connect] socket=v97BofsQmYBAEJMmAAAP company=1 user=1 role=owner +2026-08-04T14:13:54: [disconnect] socket=v97BofsQmYBAEJMmAAAP +2026-08-04T14:13:54: [connect] socket=WYJSdI9xVDaTML9xAAAR company=1 user=1 role=owner +2026-08-04T14:17:32: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T14:17:32: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T14:21:12: [disconnect] socket=WYJSdI9xVDaTML9xAAAR +2026-08-04T14:21:12: [connect] socket=UhIyCllsOjal4UwCAAAT company=1 user=1 role=owner +2026-08-04T14:21:13: [disconnect] socket=UhIyCllsOjal4UwCAAAT +2026-08-04T14:21:13: [connect] socket=6hZ-_fAyDgWzwsgvAAAV company=1 user=1 role=owner +2026-08-04T14:21:38: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:21:38: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:25:22: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T14:25:22: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T14:25:22: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:25:22: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:29:14: [disconnect] socket=6hZ-_fAyDgWzwsgvAAAV +2026-08-04T14:29:14: [connect] socket=7ocTMfufQlyO36XrAAAX company=1 user=1 role=owner +2026-08-04T14:29:19: [disconnect] socket=7ocTMfufQlyO36XrAAAX +2026-08-04T14:36:58: [connect] socket=kY4WNaECxPvGVj2JAAAZ company=1 user=1 role=owner +2026-08-04T14:37:12: [disconnect] socket=kY4WNaECxPvGVj2JAAAZ +2026-08-04T14:37:12: [connect] socket=OabEymZu5SehwNWnAAAb company=1 user=1 role=owner +2026-08-04T14:37:40: [disconnect] socket=OabEymZu5SehwNWnAAAb +2026-08-04T14:41:55: [connect] socket=kAlZOJl54kd8RXKAAAAd company=1 user=1 role=owner +2026-08-04T14:42:42: [disconnect] socket=kAlZOJl54kd8RXKAAAAd +2026-08-04T14:42:42: [connect] socket=DBSytTfd-o12DxhfAAAf company=1 user=1 role=owner +2026-08-04T14:49:38: [disconnect] socket=DBSytTfd-o12DxhfAAAf +2026-08-04T14:49:39: [connect] socket=sAr1qebAYGyIq8zrAAAh company=1 user=1 role=owner +2026-08-04T14:58:10: [emit] company=1 event=stock_updated { warehouse_id: 2, type: 'in' } +2026-08-04T14:58:10: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:58:10: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T14:58:10: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T14:58:10: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'transfer' } +2026-08-04T14:59:17: [disconnect] socket=sAr1qebAYGyIq8zrAAAh +2026-08-04T14:59:17: [connect] socket=a264uVnqws4cIAy-AAAj company=1 user=1 role=owner +2026-08-04T15:21:19: [disconnect] socket=a264uVnqws4cIAy-AAAj +2026-08-04T15:21:19: [connect] socket=JL7kcUwnQI_NExMkAAAl company=1 user=1 role=owner +2026-08-04T15:21:22: [disconnect] socket=JL7kcUwnQI_NExMkAAAl +2026-08-04T15:27:09: [connect] socket=VcbOCpKEShqcqqM0AAAn company=1 user=1 role=owner +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 2, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'transfer' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'transfer' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'in' } +2026-08-04T15:28:36: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'transfer' } +2026-08-04T15:30:09: [disconnect] socket=VcbOCpKEShqcqqM0AAAn +2026-08-04T15:30:09: [connect] socket=ecBAVshG1Eng4jh5AAAp company=1 user=1 role=owner +2026-08-04T15:33:32: [disconnect] socket=ecBAVshG1Eng4jh5AAAp +2026-08-04T15:33:32: [connect] socket=M43MyEQ7wipYOgd5AAAr company=1 user=1 role=owner +2026-08-04T15:34:32: [disconnect] socket=M43MyEQ7wipYOgd5AAAr +2026-08-04T15:34:32: [connect] socket=TiGDT6OMJsYlixlkAAAt company=1 user=1 role=owner +2026-08-04T15:35:36: [disconnect] socket=TiGDT6OMJsYlixlkAAAt +2026-08-04T15:35:36: [connect] socket=uHRGhZU0TJVvvh_aAAAv company=1 user=1 role=owner +2026-08-04T15:36:24: [disconnect] socket=uHRGhZU0TJVvvh_aAAAv +2026-08-04T15:36:24: [connect] socket=Hsui_73WGENhGdRIAAAx company=1 user=1 role=owner +2026-08-04T15:38:27: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T15:38:27: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T15:38:27: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T15:38:27: [emit] company=1 event=stock_updated { warehouse_id: 1, type: 'out' } +2026-08-04T15:39:55: [disconnect] socket=Hsui_73WGENhGdRIAAAx +2026-08-04T15:39:55: [connect] socket=xSl0-9raxlwU2K9rAAAz company=1 user=1 role=owner +2026-08-04T15:52:53: [disconnect] socket=xSl0-9raxlwU2K9rAAAz +2026-08-04T15:52:53: [connect] socket=n_l9kHYTF1AXNNRYAAA1 company=1 user=1 role=owner +2026-08-04T15:58:37: [disconnect] socket=n_l9kHYTF1AXNNRYAAA1 +2026-08-04T15:58:37: [connect] socket=T9mpzRMT3I1qjj0BAAA3 company=1 user=1 role=owner +2026-08-04T15:58:39: [disconnect] socket=T9mpzRMT3I1qjj0BAAA3 +2026-08-04T15:58:39: [connect] socket=Q1jGtGwwFc49KKxDAAA5 company=1 user=1 role=owner +2026-08-04T15:58:40: [disconnect] socket=Q1jGtGwwFc49KKxDAAA5 +2026-08-04T15:58:41: [connect] socket=Fk7l8SLr2IIRFFHbAAA7 company=1 user=1 role=owner +2026-08-04T15:58:42: [disconnect] socket=Fk7l8SLr2IIRFFHbAAA7 +2026-08-04T15:58:42: [connect] socket=pAuTSmpDDC86EZwXAAA9 company=1 user=1 role=owner +2026-08-04T16:41:11: [disconnect] socket=pAuTSmpDDC86EZwXAAA9 +2026-08-04T16:41:13: [connect] socket=XmFaoUIej-g8203TAAA_ company=1 user=1 role=owner +2026-08-04T16:41:25: [disconnect] socket=XmFaoUIej-g8203TAAA_ +2026-08-04T16:41:28: [connect] socket=TxwzTsUHzqDLHpODAABB company=1 user=1 role=owner +2026-08-04T16:43:43: [disconnect] socket=TxwzTsUHzqDLHpODAABB +2026-08-04T16:44:26: [connect] socket=TOhs2YrBWQkiGDZDAABD company=1 user=1 role=owner +2026-08-04T16:58:05: [disconnect] socket=TOhs2YrBWQkiGDZDAABD +2026-08-04T16:59:15: [connect] socket=p1xNFoGJFKox8FaOAABF company=1 user=1 role=owner +2026-08-04T17:25:03: [disconnect] socket=p1xNFoGJFKox8FaOAABF +2026-08-04T17:25:03: [connect] socket=j3s6wvwe_BxVzCA_AABH company=1 user=1 role=owner +2026-08-04T17:25:05: [disconnect] socket=j3s6wvwe_BxVzCA_AABH +2026-08-04T17:25:05: [connect] socket=4wkwmOWCAvQSicL3AABJ company=1 user=1 role=owner +2026-08-04T17:26:40: [disconnect] socket=4wkwmOWCAvQSicL3AABJ diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 6be9333..de9d2b6 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -12,14 +12,115 @@ "express": "^4.18.0", "node-cron": "^4.2.1", "socket.io": "^4.7.0" + }, + "devDependencies": { + "pm2": "^7.0.3" } }, + "node_modules/@pm2/blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@pm2/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha512-ZcNHqQjMuNRcQ7Z1zJbFIQZO/BDKV3KbiTckWdfbUaYhj7uNmUwb+FbdDWSCkvxNr9dBJQwvV17o6QBkAvgO0g==", + "dev": true, + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@pm2/js-api": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.8.1.tgz", + "integrity": "sha512-n9tDOz1ojyDOs05XthEXrLFVQYbbh2oAN19UakLPyEZDrUyEq05h8wIZU8+dNXBQY/KeFlWMLVA76nnX52ofRg==", + "dev": true, + "dependencies": { + "async": "^2.6.3", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "extrareqp2": "^1.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@pm2/js-api/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/js-api/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/js-api/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@pm2/pm2-version-check": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", + "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", + "dev": true, + "dependencies": { + "debug": "^4.3.1" + } + }, + "node_modules/@pm2/pm2-version-check/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/pm2-version-check/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -60,12 +161,97 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==", + "dev": true + }, + "node_modules/amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==", + "dev": true, + "dependencies": { + "amp": "0.3.1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.0.0-node10", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.0.0-node10.tgz", + "integrity": "sha512-BRrU0Bo1X9dFGw6KgGz6hWrqQuOlVEDOzkb0QSLZY9sXHqA7pNj7yHPVJRz7y/rj4EOJ3d/D5uxH+ee9leYgsg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -75,6 +261,27 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -99,6 +306,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -137,6 +356,79 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-tableau": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", + "integrity": "sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==", + "dev": true, + "dependencies": { + "chalk": "3.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -190,6 +482,27 @@ "url": "https://opencollective.com/express" } }, + "node_modules/croner": { + "version": "4.1.97", + "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", + "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==", + "dev": true + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/dayjs": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.15.tgz", + "integrity": "sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==", + "dev": true + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -199,6 +512,20 @@ "ms": "2.0.0" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -348,6 +675,58 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -357,6 +736,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "dev": true + }, "node_modules/express": { "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", @@ -403,6 +788,33 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extrareqp2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz", + "integrity": "sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -421,6 +833,26 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -439,6 +871,20 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -485,6 +931,55 @@ "node": ">= 0.4" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -497,6 +992,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -541,6 +1045,78 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -559,6 +1135,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -568,6 +1153,91 @@ "node": ">= 0.10" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -652,6 +1322,15 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-cron": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", @@ -661,6 +1340,15 @@ "node": ">=6.0.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -694,6 +1382,61 @@ "node": ">= 0.8" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -709,6 +1452,126 @@ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidusage": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-4.0.1.tgz", + "integrity": "sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pm2": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-7.0.3.tgz", + "integrity": "sha512-zRJOdburpb9OEPB0uqoNT8C1Gp7hPJPVy4Kr67XJNuT9UlMQcOt1WXrYQUmwqKPHk8FyauvP1CPhqoCrCaPw0Q==", + "dev": true, + "dependencies": { + "@pm2/blessed": "0.1.81", + "@pm2/js-api": "0.8.1", + "@pm2/pm2-version-check": "1.0.4", + "amp": "0.3.1", + "amp-message": "0.1.2", + "ansis": "4.0.0-node10", + "async": "3.2.6", + "chokidar": "3.6.0", + "cli-tableau": "2.0.1", + "commander": "2.15.1", + "croner": "4.1.97", + "dayjs": "1.11.15", + "debug": "4.4.3", + "eventemitter2": "6.4.9", + "fast-json-patch": "3.1.1", + "js-yaml": "4.3.0", + "pidusage": "4.0.1", + "pm2-deploy": "1.0.2", + "proxy-agent": "6.5.0", + "semver": "7.7.2", + "tx2": "1.0.5", + "ws": "8.21.0" + }, + "bin": { + "pm2": "bin/pm2", + "pm2-dev": "bin/pm2-dev", + "pm2-docker": "bin/pm2-docker", + "pm2-runtime": "bin/pm2-runtime" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/pm2-deploy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-1.0.2.tgz", + "integrity": "sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==", + "dev": true, + "dependencies": { + "run-series": "^1.1.8", + "tv4": "^1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pm2/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pm2/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/pm2/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -722,6 +1585,54 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -761,6 +1672,38 @@ "node": ">= 0.8" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/run-series": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", + "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -787,6 +1730,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -910,6 +1865,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/socket.io": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", @@ -1020,6 +1985,67 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1029,6 +2055,30 @@ "node": ">= 0.8" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1038,6 +2088,30 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tx2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", + "integrity": "sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==", + "dev": true, + "dependencies": { + "json-stringify-safe": "^5.0.1" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/nodejs/package.json b/nodejs/package.json index 36b96b6..07a8776 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -11,5 +11,8 @@ "express": "^4.18.0", "node-cron": "^4.2.1", "socket.io": "^4.7.0" + }, + "devDependencies": { + "pm2": "^7.0.3" } }