header('X-Perf-Token') ?? $request->input('token', '')); if (!hash_equals($expected, $provided)) { return ResponseHelper::returnError('Invalid perf token', 401); } return null; } private function actingUser(): ?User { $hash = (string) env('PERF_ACTOR_HASH', ''); if ($hash !== '') { $u = User::where('hashkey', $hash)->first(); if ($u) return $u; } return User::where('acct_type', UserTypes::ULTIMATE->value) ->orderBy('id', 'asc') ->first(); } private function clampCount($raw): int { $n = (int) $raw; if ($n < 1) $n = 1; if ($n > self::DEFAULT_LIMIT) $n = self::DEFAULT_LIMIT; return $n; } private function ms(float $start): float { return round((microtime(true) - $start) * 1000, 3); } public function ping(Request $request) { $deny = $this->authorize($request); if ($deny) return $deny; return Response::json([ 'success' => true, 'ts' => now()->toIso8601String(), 'php' => PHP_VERSION, ]); } /** * Core user seeder, returns ['hashes' => [...], 'ms' => float]. */ private function _seedUsers(User $actor, int $count, string $type, ?int $parentId, string $prefix): array { $parentId = $parentId ?? $actor->id; $hashed = Hash::make('Perf12345!'); $start = microtime(true); $created = []; DB::beginTransaction(); try { foreach (range(1, $count) as $i) { $suffix = Str::random(10); $u = User::create([ 'username' => "{$prefix}_{$suffix}", 'name' => "Perf User {$i}", 'mobile_number' => '09' . str_pad((string) random_int(0, 999999999), 9, '0', STR_PAD_LEFT), 'password' => $hashed, 'acct_type' => $type, 'parentuid' => $parentId, 'active' => true, ]); $created[] = $u->hashkey; } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); throw $e; } return ['hashes' => $created, 'ms' => $this->ms($start)]; } private function _seedStores(User $actor, int $count, string $category, ?int $ownerId, string $prefix): array { $start = microtime(true); $created = []; DB::beginTransaction(); try { foreach (range(1, $count) as $i) { $storeCode = StoreController::generateStoreCode($category); $s = Store::create([ 'storecode' => $storeCode, 'name' => "{$prefix} " . Str::random(8), 'description' => "Synthetic store for perf testing #{$i}", 'address' => 'Perf Lane ' . random_int(1, 9999), 'category' => $category, 'subcategory' => '', 'owner_id' => $ownerId, 'created_by' => $actor->id, 'is_active' => true, 'status' => 'active', ]); $created[] = $s->hashkey; } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); throw $e; } return ['hashes' => $created, 'ms' => $this->ms($start)]; } private function _seedProducts(User $actor, int $count, ?Store $store, bool $attach, string $prefix): array { $start = microtime(true); $created = []; DB::beginTransaction(); try { foreach (range(1, $count) as $i) { $price = random_int(10, 5000); $available = random_int(10, 500); $p = Product::create([ 'name' => "{$prefix} " . Str::random(8), 'description' => "Synthetic product for perf testing #{$i}", 'price' => $price, 'unitname' => 'pc', 'available' => $available, 'category' => 'Perf', 'subcategory' => 'Synthetic', 'created_by' => $actor->id, 'is_active' => true, ]); if ($store && $attach) { $store->products()->attach($p->id, [ 'available' => $available, 'price' => $price, 'description' => $p->description, 'is_active' => true, ]); } $created[] = $p->hashkey; } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); throw $e; } return ['hashes' => $created, 'ms' => $this->ms($start)]; } /** * POST /api/perf/seed/users * body: { count, type?, parent_hash?, prefix? } */ public function seedUsers(Request $request) { $deny = $this->authorize($request); if ($deny) return $deny; $actor = $this->actingUser(); if (!$actor) return ResponseHelper::returnError('No actor user available', 500); $count = $this->clampCount($request->input('count', 10)); $type = (string) $request->input('type', UserTypes::USER->value); $prefix = (string) $request->input('prefix', 'perf'); if (!UserTypes::tryFrom($type)) { return ResponseHelper::returnError("Invalid user type '{$type}'", 422); } $parentId = null; if ($p = $request->input('parent_hash')) { $parent = User::where('hashkey', (string) $p)->first(); if ($parent) $parentId = $parent->id; } try { $r = $this->_seedUsers($actor, $count, $type, $parentId, $prefix); } catch (\Throwable $e) { return ResponseHelper::returnError($e->getMessage()); } return Response::json([ 'success' => true, 'count' => count($r['hashes']), 'total_ms' => $r['ms'], 'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3), 'sample' => array_slice($r['hashes'], 0, 5), ]); } /** * POST /api/perf/seed/stores * body: { count, owner_hash?, category?, prefix? } */ public function seedStores(Request $request) { $deny = $this->authorize($request); if ($deny) return $deny; $actor = $this->actingUser(); if (!$actor) return ResponseHelper::returnError('No actor user available', 500); $count = $this->clampCount($request->input('count', 10)); $category = (string) $request->input('category', 'General'); $prefix = (string) $request->input('prefix', 'PerfStore'); $ownerId = null; if ($h = $request->input('owner_hash')) { $owner = User::where('hashkey', (string) $h)->first(); if ($owner) $ownerId = $owner->id; } try { $r = $this->_seedStores($actor, $count, $category, $ownerId, $prefix); } catch (\Throwable $e) { return ResponseHelper::returnError($e->getMessage()); } return Response::json([ 'success' => true, 'count' => count($r['hashes']), 'total_ms' => $r['ms'], 'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3), 'sample' => array_slice($r['hashes'], 0, 5), ]); } /** * POST /api/perf/seed/products * body: { count, target_store_hash?, prefix?, attach_to_store? } */ public function seedProducts(Request $request) { $deny = $this->authorize($request); if ($deny) return $deny; $actor = $this->actingUser(); if (!$actor) return ResponseHelper::returnError('No actor user available', 500); $count = $this->clampCount($request->input('count', 10)); $prefix = (string) $request->input('prefix', 'PerfProduct'); $attach = (bool) $request->input('attach_to_store', true); $store = null; if ($h = $request->input('target_store_hash')) { $store = Store::where('hashkey', (string) $h)->first(); if (!$store) return ResponseHelper::returnError('Target store not found', 404); } try { $r = $this->_seedProducts($actor, $count, $store, $attach, $prefix); } catch (\Throwable $e) { return ResponseHelper::returnError($e->getMessage()); } return Response::json([ 'success' => true, 'count' => count($r['hashes']), 'total_ms' => $r['ms'], 'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3), 'attached_to_store' => $store?->hashkey, 'sample' => array_slice($r['hashes'], 0, 5), ]); } /** * POST /api/perf/seed/batch * body: { users?, stores?, products?, prefix?, type?, category? } * Runs all three seeders sequentially with per-phase timings. */ public function seedBatch(Request $request) { $deny = $this->authorize($request); if ($deny) return $deny; $actor = $this->actingUser(); if (!$actor) return ResponseHelper::returnError('No actor user available', 500); $prefix = (string) $request->input('prefix', 'perf'); $type = (string) $request->input('type', UserTypes::USER->value); if (!UserTypes::tryFrom($type)) { return ResponseHelper::returnError("Invalid user type '{$type}'", 422); } $category = (string) $request->input('category', 'General'); $users = max(0, (int) $request->input('users', 0)); $stores = max(0, (int) $request->input('stores', 0)); $products = max(0, (int) $request->input('products', 0)); $phases = []; $totalStart = microtime(true); try { if ($users > 0) { $r = $this->_seedUsers($actor, $this->clampCount($users), $type, null, $prefix); $phases['users'] = ['count' => count($r['hashes']), 'ms' => $r['ms']]; } if ($stores > 0) { $r = $this->_seedStores($actor, $this->clampCount($stores), $category, null, $prefix . 'Store'); $phases['stores'] = ['count' => count($r['hashes']), 'ms' => $r['ms']]; } if ($products > 0) { $r = $this->_seedProducts($actor, $this->clampCount($products), null, false, $prefix . 'Product'); $phases['products'] = ['count' => count($r['hashes']), 'ms' => $r['ms']]; } } catch (\Throwable $e) { return ResponseHelper::returnError($e->getMessage()); } return Response::json([ 'success' => true, 'total_ms' => $this->ms($totalStart), 'phases' => $phases, ]); } /** * POST /api/perf/pos/simulate * body: { store_hash, items?, cycles?, complete? } * * Runs end-to-end POS cycles entirely server-side: open session, add N * line items, optionally complete + archive. Returns per-phase timings. */ public function simulatePos(Request $request) { $deny = $this->authorize($request); if ($deny) return $deny; $actor = $this->actingUser(); if (!$actor) return ResponseHelper::returnError('No actor user available', 500); $storeHash = (string) $request->input('store_hash', ''); if ($storeHash === '') return ResponseHelper::returnError('store_hash is required', 422); $store = Store::where('hashkey', $storeHash)->first(); if (!$store) return ResponseHelper::returnError('Store not found', 404); $items = max(1, min(200, (int) $request->input('items', 5))); $cycles = max(1, min(100, (int) $request->input('cycles', 1))); $complete = (bool) $request->input('complete', true); // Pull a pool of products attached to this store; fall back to global // active products if the store has none yet. $productIds = DB::table('prd_str') ->where('store_id', $store->id) ->where('is_active', true) ->pluck('product_id') ->toArray(); if (empty($productIds)) { $productIds = Product::where('is_active', true) ->limit(max(50, $items * 2)) ->pluck('id') ->toArray(); } if (empty($productIds)) { return ResponseHelper::returnError('No products available to simulate sales', 422); } $products = Product::whereIn('id', $productIds)->get(['id', 'hashkey', 'price'])->keyBy('id'); $cycleResults = []; $totalStart = microtime(true); for ($c = 1; $c <= $cycles; $c++) { $cStart = microtime(true); // 1. Open session $t = microtime(true); $session = PosSession::create([ 'access_key' => Str::random(32), 'store_id' => $store->id, 'customer_name' => 'Perf Customer ' . $c, 'status' => 'active', 'created_by' => $actor->id, ]); $openMs = $this->ms($t); // 2. Add items (raw inserts for speed, mirrors PosController::addItem) $t = microtime(true); $now = now(); $rows = []; $total = 0; for ($i = 0; $i < $items; $i++) { $pid = $productIds[array_rand($productIds)]; $product = $products[$pid] ?? null; if (!$product) continue; $qty = random_int(1, 5); $price = (int) $product->price; $line = $price * $qty; $total += $line; $rows[] = [ 'pos_session_id' => $session->id, 'product_id' => $pid, 'quantity' => $qty, 'price_at_sale' => $price, 'total_price' => $line, 'hashkey' => Str::uuid()->toString() . Str::random(100), 'created_at' => $now, 'updated_at' => $now, 'created_by' => $actor->id, ]; } if (!empty($rows)) { DB::table('pos_transactions')->insert($rows); } $addMs = $this->ms($t); // 3. Optionally complete the session $completeMs = null; if ($complete) { $t = microtime(true); DB::table('pos_sessions')->where('id', $session->id)->update([ 'total_amount' => $total, 'received_amount' => $total, 'change_amount' => 0, 'status' => 'completed', 'payment_method' => 'cash', 'updated_at' => $now, 'updated_by' => $actor->id, ]); $completeMs = $this->ms($t); } $cycleResults[] = [ 'session_hash' => $session->hashkey, 'items' => count($rows), 'total' => $total, 'open_ms' => $openMs, 'add_items_ms' => $addMs, 'complete_ms' => $completeMs, 'cycle_ms' => $this->ms($cStart), ]; } $totalMs = $this->ms($totalStart); $cycleMsValues = array_column($cycleResults, 'cycle_ms'); $avg = $cycleMsValues ? array_sum($cycleMsValues) / count($cycleMsValues) : 0.0; return Response::json([ 'success' => true, 'store_hash' => $store->hashkey, 'cycles' => $cycles, 'items_per_cycle' => $items, 'total_ms' => $totalMs, 'avg_cycle_ms' => round($avg, 3), 'min_cycle_ms' => $cycleMsValues ? min($cycleMsValues) : 0, 'max_cycle_ms' => $cycleMsValues ? max($cycleMsValues) : 0, 'detail' => $cycleResults, ]); } }