*/ private const VOLUMES = [ 'free' => [ 'locations' => 1, 'products_per_location' => 40, 'stations' => 0, 'sales_per_location' => 12, ], 'pro' => [ 'locations' => 3, 'products_per_location' => 55, 'stations' => 3, 'sales_per_location' => 25, ], 'enterprise' => [ 'locations' => 5, 'products_per_location' => 70, 'stations' => 5, 'sales_per_location' => 40, ], ]; private const PRODUCT_NAMES = [ 'Bottled Water', 'Soft Drink', 'Rice Pack', 'Chicken Wrap', 'Veggie Bowl', 'Coffee', 'Tea', 'Pastry', 'Sandwich', 'Energy Bar', 'Phone Charger', 'USB Cable', 'Notebook', 'Pen Set', 'Tote Bag', 'Shampoo', 'Soap', 'Toothpaste', 'Detergent', 'Tissue Pack', ]; /** * @return array{user: User, plan: string, counts: array} */ public function seed(string $identity, string $plan, bool $reset = false): array { $plan = $this->normalizePlan($plan); $user = $this->resolveUser($identity); if ($reset) { $this->resetForOwner($user->public_id); } $this->assignPlan($user, $plan); $volumes = self::VOLUMES[$plan]; $stations = $this->seedStations($user->public_id, $volumes['stations']); $categories = $this->seedCategories($user->public_id); $locations = $this->seedLocations($user->public_id, $volumes['locations']); foreach ($locations as $index => $location) { $this->seedRegister($user->public_id, $location, $index === 0); $products = $this->seedProducts( $user->public_id, $location, $categories, $stations, $volumes['products_per_location'], ); $this->seedCashSales( $user->public_id, $location, $products, $volumes['sales_per_location'], ); } if ($plan !== 'free') { $this->seedTeam($user); } return [ 'user' => $user, 'plan' => $plan, 'counts' => [ 'locations' => PosLocation::owned($user->public_id)->count(), 'products' => PosProduct::owned($user->public_id)->count(), 'stations' => PosStation::owned($user->public_id)->count(), 'sales' => PosSale::owned($user->public_id)->count(), 'registers' => PosDevice::owned($user->public_id)->where('type', 'register')->count(), ], ]; } public function resolveUser(string $identity): User { $identity = trim($identity); $user = User::query() ->where(function ($q) use ($identity) { $q->where('email', $identity)->orWhere('public_id', $identity); }) ->first(); if ($user) { return $user; } $email = str_contains($identity, '@') ? $identity : $identity.'@demo.ladill.local'; $publicId = str_contains($identity, '@') ? (string) Str::uuid() : $identity; return User::query()->create([ 'public_id' => $publicId, 'name' => 'Demo User', 'email' => $email, 'password' => bcrypt('password'), 'email_verified_at' => now(), ]); } public function assignPlan(User $user, string $plan): void { if ($plan === 'free') { ProSubscription::query()->where('user_id', $user->id)->delete(); return; } $price = $plan === ProSubscription::PLAN_ENTERPRISE ? (int) config('pos.plans.enterprise.price_minor', 249000) : (int) config('pos.plans.pro.price_minor', config('pos.pro.price_minor', 179000)); ProSubscription::updateOrCreate( ['user_id' => $user->id], [ 'plan' => $plan, 'status' => ProSubscription::STATUS_ACTIVE, 'price_minor' => $price, 'currency' => (string) config('pos.pro.currency', 'GHS'), 'auto_renew' => false, 'started_at' => now(), 'current_period_end' => now()->addYear(), 'last_charged_at' => now(), 'canceled_at' => null, 'last_reference' => 'demo-pos-'.$plan.'-'.$user->id, 'last_error' => null, ], ); } public function resetForOwner(string $ownerRef): void { DB::transaction(function () use ($ownerRef) { $saleIds = PosSale::owned($ownerRef)->pluck('id'); if ($saleIds->isNotEmpty()) { $lineIds = PosSaleLine::query()->whereIn('pos_sale_id', $saleIds)->pluck('id'); if ($lineIds->isNotEmpty() && DB::getSchemaBuilder()->hasTable('pos_sale_line_modifiers')) { DB::table('pos_sale_line_modifiers')->whereIn('pos_sale_line_id', $lineIds)->delete(); } PosSaleLine::query()->whereIn('pos_sale_id', $saleIds)->delete(); PosPayment::query()->whereIn('pos_sale_id', $saleIds)->delete(); PosSale::owned($ownerRef)->delete(); } if (DB::getSchemaBuilder()->hasTable('pos_product_modifier_group')) { $productIds = PosProduct::owned($ownerRef)->pluck('id'); if ($productIds->isNotEmpty()) { DB::table('pos_product_modifier_group')->whereIn('product_id', $productIds)->delete(); } } PosProduct::owned($ownerRef)->delete(); PosDevice::owned($ownerRef)->delete(); PosMember::owned($ownerRef)->delete(); if (DB::getSchemaBuilder()->hasTable('pos_tables')) { DB::table('pos_tables')->where('owner_ref', $ownerRef)->delete(); } if (DB::getSchemaBuilder()->hasTable('pos_customer_displays')) { DB::table('pos_customer_displays')->where('owner_ref', $ownerRef)->delete(); } if (DB::getSchemaBuilder()->hasTable('pos_modifiers')) { DB::table('pos_modifiers')->where('owner_ref', $ownerRef)->delete(); } if (DB::getSchemaBuilder()->hasTable('pos_modifier_groups')) { DB::table('pos_modifier_groups')->where('owner_ref', $ownerRef)->delete(); } PosCategory::owned($ownerRef)->delete(); PosStation::owned($ownerRef)->delete(); PosLocation::owned($ownerRef)->delete(); }); } /** @return list */ private function seedStations(string $ownerRef, int $count): array { if ($count <= 0) { return []; } $stations = PosStation::owned($ownerRef)->orderBy('id')->get()->all(); $names = ['Hot Kitchen', 'Cold Prep', 'Bar', 'Grill', 'Dessert']; for ($i = count($stations); $i < $count; $i++) { $stations[] = PosStation::query()->create([ 'owner_ref' => $ownerRef, 'name' => $names[$i] ?? ('Station '.($i + 1)), 'position' => $i + 1, ]); } return array_slice($stations, 0, $count); } /** @return list */ private function seedCategories(string $ownerRef): array { $names = ['Beverages', 'Food', 'Retail', 'Services']; $categories = []; foreach ($names as $i => $name) { $categories[] = PosCategory::query()->firstOrCreate( ['owner_ref' => $ownerRef, 'name' => $name], ['position' => $i + 1], ); } return $categories; } /** @return list */ private function seedLocations(string $ownerRef, int $count): array { $locations = PosLocation::owned($ownerRef)->orderBy('id')->get()->all(); $names = ['Main Register', 'Airport Kiosk', 'Mall Counter', 'Campus Store', 'Harbour Outlet']; for ($i = count($locations); $i < $count; $i++) { $locations[] = PosLocation::query()->create([ 'owner_ref' => $ownerRef, 'name' => $names[$i] ?? ('Location '.($i + 1)), 'currency' => 'GHS', 'is_default' => count($locations) === 0, 'service_style' => PosLocation::STYLE_RETAIL, 'receipt_footer' => 'Thank you for shopping with Demo POS.', ]); } if ($locations !== [] && ! collect($locations)->contains(fn (PosLocation $l) => $l->is_default)) { $locations[0]->forceFill(['is_default' => true])->save(); } return array_slice($locations, 0, $count); } private function seedRegister(string $ownerRef, PosLocation $location, bool $primary): void { $exists = PosDevice::owned($ownerRef) ->where('location_id', $location->id) ->where('type', 'register') ->exists(); if ($exists) { return; } PosDevice::query()->create([ 'owner_ref' => $ownerRef, 'location_id' => $location->id, 'name' => $location->name.' Till', 'type' => 'register', 'status' => PosDevice::STATUS_ONLINE, 'last_online_at' => now(), 'config' => ['demo' => true, 'primary' => $primary], ]); } /** * @param list $categories * @param list $stations * @return list */ private function seedProducts( string $ownerRef, PosLocation $location, array $categories, array $stations, int $count, ): array { $products = PosProduct::owned($ownerRef) ->where('location_id', $location->id) ->orderBy('id') ->get() ->all(); for ($i = count($products); $i < $count; $i++) { $name = self::PRODUCT_NAMES[$i % count(self::PRODUCT_NAMES)]; $products[] = PosProduct::query()->create([ 'owner_ref' => $ownerRef, 'location_id' => $location->id, 'category_id' => $categories[$i % count($categories)]->id, 'station_id' => $stations !== [] ? $stations[$i % count($stations)]->id : null, 'name' => $name.' '.($i + 1), 'sku' => 'POS-'.$location->id.'-'.str_pad((string) ($i + 1), 3, '0', STR_PAD_LEFT), 'price_minor' => 500 + (($i % 20) * 250), 'currency' => 'GHS', 'is_active' => true, ]); } return array_slice($products, 0, $count); } /** * @param list $products */ private function seedCashSales(string $ownerRef, PosLocation $location, array $products, int $count): void { if ($products === []) { return; } $existing = PosSale::owned($ownerRef)->where('location_id', $location->id)->count(); for ($i = $existing; $i < $count; $i++) { $lineCount = 1 + ($i % 3); $lines = []; $subtotal = 0; for ($l = 0; $l < $lineCount; $l++) { $product = $products[($i + $l) % count($products)]; $qty = 1 + ($l % 2); $lineTotal = $product->price_minor * $qty; $subtotal += $lineTotal; $lines[] = [$product, $qty, $lineTotal]; } $sale = PosSale::query()->create([ 'owner_ref' => $ownerRef, 'location_id' => $location->id, 'reference' => 'DEMO-'.$location->id.'-'.str_pad((string) ($i + 1), 4, '0', STR_PAD_LEFT).'-'.substr(md5($ownerRef.$i), 0, 6), 'status' => PosSale::STATUS_PAID, 'payment_method' => PosSale::METHOD_CASH, 'order_type' => PosSale::ORDER_COUNTER, 'kitchen_status' => PosSale::KITCHEN_NONE, 'customer_name' => 'Cash Customer '.($i + 1), 'subtotal_minor' => $subtotal, 'total_minor' => $subtotal, 'currency' => 'GHS', 'paid_at' => now()->subDays($i % 10)->subMinutes($i * 3), 'opened_at' => now()->subDays($i % 10)->subMinutes(($i * 3) + 5), 'closed_at' => now()->subDays($i % 10)->subMinutes($i * 3), ]); foreach ($lines as $pos => [$product, $qty, $lineTotal]) { PosSaleLine::query()->create([ 'pos_sale_id' => $sale->id, 'product_id' => $product->id, 'station_id' => $product->station_id, 'name' => $product->name, 'unit_price_minor' => $product->price_minor, 'quantity' => $qty, 'line_total_minor' => $lineTotal, 'position' => $pos, ]); } PosPayment::query()->create([ 'pos_sale_id' => $sale->id, 'owner_ref' => $ownerRef, 'amount_minor' => $subtotal, 'method' => PosPayment::METHOD_CASH, 'status' => PosPayment::STATUS_PAID, 'paid_at' => $sale->paid_at, ]); } } private function seedTeam(User $owner): void { if (PosMember::owned($owner->public_id)->exists()) { return; } $cashier = User::query()->firstOrCreate( ['email' => 'demo-cashier-'.$owner->id.'@ladill.local'], [ 'public_id' => (string) Str::uuid(), 'name' => 'Demo Cashier', 'password' => bcrypt('password'), 'email_verified_at' => now(), ], ); $location = PosLocation::owned($owner->public_id)->orderBy('id')->first(); PosMember::query()->create([ 'owner_ref' => $owner->public_id, 'user_ref' => $cashier->public_id, 'role' => PosMember::ROLE_CASHIER, 'location_id' => $location?->id, ]); } private function normalizePlan(string $plan): string { $plan = strtolower(trim($plan)); if (! in_array($plan, ['free', 'pro', 'enterprise'], true)) { throw new \InvalidArgumentException('Plan must be free, pro, or enterprise.'); } return $plan; } }