383 lines
18 KiB
PHP
383 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use App\Models\User;
|
|
use Workdo\Account\Models\Customer;
|
|
use Workdo\Account\Models\Vendor;
|
|
use Workdo\ProductService\Models\ProductServiceItem;
|
|
use App\Models\Warehouse;
|
|
use App\Models\PurchaseInvoice;
|
|
use App\Models\PurchaseInvoiceItem;
|
|
use App\Models\SalesInvoice;
|
|
use App\Models\SalesInvoiceItem;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use App\Events\PostPurchaseInvoice;
|
|
use App\Events\PostSalesInvoice;
|
|
use Illuminate\Support\Str;
|
|
use Workdo\Account\Helpers\AccountUtility;
|
|
use Workdo\ProductService\Models\WarehouseStock;
|
|
|
|
class ErpStressTest extends Command
|
|
{
|
|
protected $signature = 'erp:stress-test {--count=50 : Number of transactions to generate} {--wipe : Wipe existing transaction records before generating}';
|
|
protected $description = 'Stress test full cycle: generates multiple Purchase and Sales Invoices to test Warehouse and Ledgers.';
|
|
|
|
public function handle()
|
|
{
|
|
$count = (int) $this->option('count');
|
|
$this->info("Starting ERP Stress Test (Generating {$count} Purchases & Sales)...");
|
|
|
|
if ($this->option('wipe')) {
|
|
$this->info("Wiping transaction tables...");
|
|
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
|
SalesInvoice::truncate();
|
|
SalesInvoiceItem::truncate();
|
|
PurchaseInvoice::truncate();
|
|
PurchaseInvoiceItem::truncate();
|
|
\Workdo\Account\Models\JournalEntry::truncate();
|
|
\Workdo\Account\Models\JournalEntryItem::truncate();
|
|
|
|
if (class_exists(\Workdo\Warehouse\Models\WarehouseStockMovement::class)) {
|
|
\Workdo\Warehouse\Models\WarehouseStockMovement::truncate();
|
|
}
|
|
if (class_exists(\Workdo\ProductService\Models\WarehouseStock::class)) {
|
|
\Workdo\ProductService\Models\WarehouseStock::truncate();
|
|
}
|
|
|
|
\Workdo\Account\Models\Customer::truncate();
|
|
\Workdo\Account\Models\Vendor::truncate();
|
|
\Workdo\ProductService\Models\ProductServiceItem::truncate();
|
|
Warehouse::truncate();
|
|
User::whereIn('type', ['client', 'vendor'])->delete();
|
|
|
|
\Spatie\Activitylog\Models\Activity::truncate();
|
|
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
|
$this->info("Transaction tables wiped cleanly.");
|
|
}
|
|
|
|
// 1. Get Company User (Creator)
|
|
$company = User::where('type', 'company')->first();
|
|
if (!$company) {
|
|
$this->error("No company user found.");
|
|
return;
|
|
}
|
|
|
|
Auth::login($company);
|
|
$creatorId = creatorId();
|
|
$this->info("Authenticated as Company: {$company->name}");
|
|
|
|
// Setup Accounting default data (creates missing Chart of Accounts)
|
|
AccountUtility::defaultdata($creatorId);
|
|
|
|
// 2. Setup Vendors & Customers
|
|
$faker = \Faker\Factory::create('en_PH');
|
|
$vendors = [];
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$vendorEmail = $faker->unique()->safeEmail();
|
|
$vendorUser = User::firstOrCreate(
|
|
['email' => $vendorEmail],
|
|
['name' => $faker->name(), 'password' => \Hash::make('123456'), 'type' => 'vendor', 'created_by' => $creatorId]
|
|
);
|
|
$vendors[] = Vendor::firstOrCreate(
|
|
['contact_person_email' => $vendorEmail],
|
|
[
|
|
'user_id' => $vendorUser->id, 'vendor_code' => 'V-' . strtoupper(Str::random(5)), 'company_name' => $faker->company() . ' Pharmacy',
|
|
'contact_person_name' => $vendorUser->name, 'billing_address' => ['name' => $vendorUser->name, 'address_line_1' => $faker->streetAddress(), 'city' => $faker->city(), 'state' => $faker->stateAbbr(), 'country' => 'Philippines', 'zip_code' => $faker->postcode()],
|
|
'shipping_address' => ['name' => $vendorUser->name, 'address_line_1' => $faker->streetAddress(), 'city' => $faker->city(), 'state' => $faker->stateAbbr(), 'country' => 'Philippines', 'zip_code' => $faker->postcode()],
|
|
'same_as_billing' => true, 'creator_id' => $company->id, 'created_by' => $creatorId
|
|
]
|
|
);
|
|
}
|
|
|
|
$customers = [];
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$customerEmail = $faker->unique()->safeEmail();
|
|
$customerUser = User::firstOrCreate(
|
|
['email' => $customerEmail],
|
|
['name' => $faker->name(), 'password' => \Hash::make('123456'), 'type' => 'client', 'created_by' => $creatorId]
|
|
);
|
|
$customers[] = Customer::firstOrCreate(
|
|
['contact_person_email' => $customerEmail],
|
|
[
|
|
'user_id' => $customerUser->id, 'customer_code' => 'C-' . strtoupper(Str::random(5)), 'company_name' => $faker->company() . ' Clinic',
|
|
'contact_person_name' => $customerUser->name, 'billing_address' => ['name' => $customerUser->name, 'address_line_1' => $faker->streetAddress(), 'city' => $faker->city(), 'state' => $faker->stateAbbr(), 'country' => 'Philippines', 'zip_code' => $faker->postcode()],
|
|
'shipping_address' => ['name' => $customerUser->name, 'address_line_1' => $faker->streetAddress(), 'city' => $faker->city(), 'state' => $faker->stateAbbr(), 'country' => 'Philippines', 'zip_code' => $faker->postcode()],
|
|
'same_as_billing' => true, 'creator_id' => $company->id, 'created_by' => $creatorId
|
|
]
|
|
);
|
|
}
|
|
|
|
// 3. Setup Products (Medicines)
|
|
$medicineNames = ['Paracetamol', 'Ibuprofen', 'Amoxicillin', 'Cetirizine', 'Losartan', 'Amlodipine', 'Metformin', 'Omeprazole', 'Simvastatin', 'Azithromycin'];
|
|
$products = [];
|
|
foreach ($medicineNames as $medName) {
|
|
$productSku = 'MED-' . strtoupper(Str::random(6));
|
|
$products[] = ProductServiceItem::firstOrCreate(
|
|
['sku' => $productSku],
|
|
[
|
|
'name' => $medName . ' ' . $faker->numberBetween(100, 500) . 'mg',
|
|
'sale_price' => $faker->randomFloat(2, 50, 200),
|
|
'purchase_price' => $faker->randomFloat(2, 10, 40),
|
|
'type' => 'product',
|
|
'min_stock_level' => 100, // For low stock alerts
|
|
'created_by' => $creatorId
|
|
]
|
|
);
|
|
}
|
|
|
|
// Dead Stock Product (Purchased > 90 days ago, never sold)
|
|
$deadProduct = ProductServiceItem::firstOrCreate(
|
|
['sku' => 'DEAD-' . strtoupper(Str::random(6))],
|
|
[
|
|
'name' => 'Expired generic antibiotic',
|
|
'sale_price' => 150.00,
|
|
'purchase_price' => 50.00,
|
|
'type' => 'product',
|
|
'min_stock_level' => 10,
|
|
'created_by' => $creatorId
|
|
]
|
|
);
|
|
|
|
// 4. Setup Warehouses
|
|
$warehouses = [];
|
|
for ($i = 0; $i < 2; $i++) {
|
|
$warehouseName = $faker->city() . ' Distribution Center';
|
|
$warehouses[] = Warehouse::firstOrCreate(
|
|
['name' => $warehouseName, 'created_by' => $creatorId],
|
|
['address' => $faker->streetAddress(), 'city' => $faker->city(), 'zip_code' => $faker->postcode()]
|
|
);
|
|
}
|
|
|
|
$this->info("Entities Verified. Generating {$count} transactions...");
|
|
|
|
$bar = $this->output->createProgressBar($count * 2 + 1);
|
|
$bar->start();
|
|
|
|
// 5. Setup Dead Stock (95 days ago)
|
|
$deadVendor = $vendors[array_rand($vendors)];
|
|
$deadWarehouse = $warehouses[array_rand($warehouses)];
|
|
$deadDate = now()->subDays(95);
|
|
$deadQty = 200;
|
|
|
|
$deadPurchase = new PurchaseInvoice();
|
|
$deadPurchase->invoice_number = 'P-TEST-DEAD';
|
|
$deadPurchase->invoice_date = $deadDate;
|
|
$deadPurchase->due_date = $deadDate->copy()->addDays(7);
|
|
$deadPurchase->vendor_id = $deadVendor->user_id;
|
|
$deadPurchase->warehouse_id = $deadWarehouse->id;
|
|
$deadPurchase->status = 'posted';
|
|
$deadPurchase->subtotal = $deadQty * $deadProduct->purchase_price;
|
|
$deadPurchase->total_amount = $deadQty * $deadProduct->purchase_price;
|
|
$deadPurchase->balance_amount = $deadQty * $deadProduct->purchase_price;
|
|
$deadPurchase->creator_id = $company->id;
|
|
$deadPurchase->created_by = $creatorId;
|
|
$deadPurchase->save();
|
|
|
|
PurchaseInvoiceItem::create([
|
|
'invoice_id' => $deadPurchase->id,
|
|
'product_id' => $deadProduct->id,
|
|
'quantity' => $deadQty,
|
|
'unit_price' => $deadProduct->purchase_price,
|
|
]);
|
|
|
|
if (class_exists(\Workdo\ProductService\Models\WarehouseStock::class)) {
|
|
$stock = \Workdo\ProductService\Models\WarehouseStock::firstOrCreate(
|
|
['warehouse_id' => $deadWarehouse->id, 'product_id' => $deadProduct->id],
|
|
['quantity' => 0, 'created_by' => $creatorId, 'workspace' => getActiveWorkSpace() ?? 1]
|
|
);
|
|
$stock->quantity += $deadQty;
|
|
$stock->save();
|
|
}
|
|
|
|
if (class_exists(\Workdo\Warehouse\Models\WarehouseStockMovement::class)) {
|
|
\Workdo\Warehouse\Models\WarehouseStockMovement::create([
|
|
'product_id' => $deadProduct->id,
|
|
'warehouse_id' => $deadWarehouse->id,
|
|
'quantity' => $deadQty,
|
|
'type' => 'in',
|
|
'reason' => 'Purchase Receipt',
|
|
'reference_type' => PurchaseInvoice::class,
|
|
'reference_id' => $deadPurchase->id,
|
|
'description' => 'Received Dead Stock',
|
|
'created_by' => $creatorId,
|
|
'workspace_id' => getActiveWorkSpace() ?? 1,
|
|
'created_at' => $deadDate,
|
|
]);
|
|
}
|
|
$bar->advance();
|
|
|
|
// 6. Setup Low Stock product explicitly to ensure at least one triggers the alert
|
|
$lowStockProduct = $products[0]; // pick the first one
|
|
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$randomVendor = $vendors[array_rand($vendors)];
|
|
$randomWarehouse = $warehouses[array_rand($warehouses)];
|
|
$randomProduct = $products[array_rand($products)];
|
|
|
|
// Purchase Invoice
|
|
$purchaseInvoiceNumber = 'P-TEST-' . strtoupper(Str::random(5)) . '-' . $i;
|
|
$purchase = new PurchaseInvoice();
|
|
$purchase->invoice_number = $purchaseInvoiceNumber;
|
|
$purchase->invoice_date = now()->subDays(rand(1, 30));
|
|
$purchase->due_date = now()->addDays(7);
|
|
$purchase->vendor_id = $randomVendor->user_id;
|
|
$purchase->warehouse_id = $randomWarehouse->id;
|
|
$purchase->status = 'draft';
|
|
|
|
$purchaseQty = rand(10, 100);
|
|
$purchaseTotal = $purchaseQty * $randomProduct->purchase_price;
|
|
|
|
$purchase->subtotal = $purchaseTotal;
|
|
$purchase->total_amount = $purchaseTotal;
|
|
$purchase->balance_amount = $purchaseTotal;
|
|
$purchase->creator_id = $company->id;
|
|
$purchase->created_by = $creatorId;
|
|
$purchase->save();
|
|
|
|
PurchaseInvoiceItem::create([
|
|
'invoice_id' => $purchase->id,
|
|
'product_id' => $randomProduct->id,
|
|
'quantity' => $purchaseQty,
|
|
'unit_price' => $randomProduct->purchase_price,
|
|
]);
|
|
|
|
try {
|
|
PostPurchaseInvoice::dispatch($purchase);
|
|
$purchase->update(['status' => 'posted']);
|
|
|
|
// Simulate GRN / Receiving movement for the dashboard
|
|
if (class_exists(\Workdo\Warehouse\Models\WarehouseStockMovement::class)) {
|
|
\Workdo\Warehouse\Models\WarehouseStockMovement::create([
|
|
'product_id' => $randomProduct->id,
|
|
'warehouse_id' => $randomWarehouse->id,
|
|
'quantity' => $purchaseQty,
|
|
'type' => 'in',
|
|
'reason' => 'Purchase Receipt',
|
|
'reference_type' => PurchaseInvoice::class,
|
|
'reference_id' => $purchase->id,
|
|
'description' => 'Received from Purchase ' . $purchaseInvoiceNumber,
|
|
'created_by' => $creatorId,
|
|
'workspace_id' => getActiveWorkSpace() ?? 1,
|
|
'created_at' => $purchase->invoice_date,
|
|
]);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Log and ignore to continue stress test
|
|
}
|
|
$bar->advance();
|
|
|
|
$randomCustomer = $customers[array_rand($customers)];
|
|
$randomSaleWarehouse = $warehouses[array_rand($warehouses)];
|
|
|
|
// To ensure low stock alert is triggered for products[0], we randomly sell a lot of it, while keeping purchasing random.
|
|
// Also ensure we NEVER sell the dead stock product.
|
|
$randomSaleProduct = $i % 5 === 0 ? $lowStockProduct : $products[array_rand($products)];
|
|
|
|
// Sales Invoice
|
|
$salesInvoiceNumber = 'S-TEST-' . strtoupper(Str::random(5)) . '-' . $i;
|
|
$sale = new SalesInvoice();
|
|
$sale->invoice_number = $salesInvoiceNumber;
|
|
$sale->invoice_date = now()->subDays(rand(1, 30));
|
|
$sale->due_date = now()->addDays(7);
|
|
$sale->customer_id = $randomCustomer->user_id;
|
|
$sale->warehouse_id = $randomSaleWarehouse->id;
|
|
$sale->status = 'draft';
|
|
|
|
// Generate higher sales qty for the designated low stock product to drain it
|
|
$saleQty = ($randomSaleProduct->id === $lowStockProduct->id) ? rand(20, 50) : rand(1, 5);
|
|
$saleTotal = $saleQty * $randomSaleProduct->sale_price;
|
|
|
|
$sale->subtotal = $saleTotal;
|
|
$sale->total_amount = $saleTotal;
|
|
$sale->balance_amount = $saleTotal;
|
|
$sale->creator_id = $company->id;
|
|
$sale->created_by = $creatorId;
|
|
$sale->save();
|
|
|
|
SalesInvoiceItem::create([
|
|
'invoice_id' => $sale->id,
|
|
'product_id' => $randomSaleProduct->id,
|
|
'quantity' => $saleQty,
|
|
'unit_price' => $randomSaleProduct->sale_price,
|
|
]);
|
|
|
|
try {
|
|
PostSalesInvoice::dispatch($sale);
|
|
$sale->update(['status' => 'posted']);
|
|
|
|
// Simulate Fulfillment/Dispatch movement and deduct stock
|
|
if (class_exists(\Workdo\Warehouse\Models\WarehouseStockMovement::class)) {
|
|
\Workdo\Warehouse\Models\WarehouseStockMovement::create([
|
|
'product_id' => $randomSaleProduct->id,
|
|
'warehouse_id' => $randomSaleWarehouse->id,
|
|
'quantity' => $saleQty,
|
|
'type' => 'out',
|
|
'reason' => 'Sales Dispatch',
|
|
'reference_type' => SalesInvoice::class,
|
|
'reference_id' => $sale->id,
|
|
'description' => 'Dispatched for Sale ' . $salesInvoiceNumber,
|
|
'created_by' => $creatorId,
|
|
'workspace_id' => getActiveWorkSpace() ?? 1,
|
|
'created_at' => $sale->invoice_date,
|
|
]);
|
|
}
|
|
|
|
// Deduct stock (since PostSalesInvoice doesn't do it automatically)
|
|
if (class_exists(\Workdo\ProductService\Models\WarehouseStock::class)) {
|
|
$stock = \Workdo\ProductService\Models\WarehouseStock::where('warehouse_id', $randomSaleWarehouse->id)
|
|
->where('product_id', $randomSaleProduct->id)
|
|
->first();
|
|
if ($stock) {
|
|
$stock->decrement('quantity', $saleQty);
|
|
}
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
// Log and ignore
|
|
}
|
|
$bar->advance();
|
|
}
|
|
|
|
// Final drain of the low stock product to guarantee it goes under 100
|
|
if (class_exists(\Workdo\ProductService\Models\WarehouseStock::class)) {
|
|
$totalLowStock = \Workdo\ProductService\Models\WarehouseStock::where('product_id', $lowStockProduct->id)->sum('quantity');
|
|
if ($totalLowStock > 50) {
|
|
$drainQty = $totalLowStock - 10; // leave 10 left (which is < 100 min stock)
|
|
$drainWarehouse = $warehouses[0];
|
|
|
|
\Workdo\Warehouse\Models\WarehouseStockMovement::create([
|
|
'product_id' => $lowStockProduct->id,
|
|
'warehouse_id' => $drainWarehouse->id,
|
|
'quantity' => $drainQty,
|
|
'type' => 'out',
|
|
'reason' => 'Inventory Adjustment',
|
|
'reference_type' => null,
|
|
'reference_id' => 0,
|
|
'description' => 'Forced stock drain for Low Stock Alert',
|
|
'created_by' => $creatorId,
|
|
'workspace_id' => getActiveWorkSpace() ?? 1,
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
$stock = \Workdo\ProductService\Models\WarehouseStock::where('warehouse_id', $drainWarehouse->id)
|
|
->where('product_id', $lowStockProduct->id)
|
|
->first();
|
|
if ($stock) {
|
|
$stock->decrement('quantity', $drainQty);
|
|
}
|
|
}
|
|
}
|
|
|
|
$bar->finish();
|
|
$this->newLine(2);
|
|
|
|
$this->info("ERP Stress Test Completed!");
|
|
$this->info("Total Purchases Generated: {$count}");
|
|
$this->info("Total Sales Generated: {$count}");
|
|
$this->info("Transactions can now be viewed in the ERP Dashboard!");
|
|
}
|
|
}
|