Seed locations, products, and cash sales within Free caps and Pro multi-site data. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\Pos\DemoSeeder;
|
||||||
|
use App\Services\Pos\SubscriptionService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed plan-aware POS demo data for a platform identity (email or public_id).
|
||||||
|
*/
|
||||||
|
class DemoSeedCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'demo:seed
|
||||||
|
{identity : Email or public_id of the local User mirror}
|
||||||
|
{--plan=free : free|pro|enterprise}
|
||||||
|
{--reset : Wipe tenant-scoped demo data before reseeding}';
|
||||||
|
|
||||||
|
protected $description = 'Seed realistic POS demo data for a Free/Pro/Enterprise account';
|
||||||
|
|
||||||
|
public function handle(DemoSeeder $seeder, SubscriptionService $subscriptions): int
|
||||||
|
{
|
||||||
|
$identity = (string) $this->argument('identity');
|
||||||
|
$plan = strtolower((string) $this->option('plan'));
|
||||||
|
$reset = (bool) $this->option('reset');
|
||||||
|
|
||||||
|
if (! in_array($plan, ['free', 'pro', 'enterprise'], true)) {
|
||||||
|
$this->error('Invalid --plan. Use free, pro, or enterprise.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $seeder->seed($identity, $plan, $reset);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $result['user'];
|
||||||
|
$effective = $subscriptions->planKey($user);
|
||||||
|
|
||||||
|
$this->info(sprintf(
|
||||||
|
'Seeded POS demo for %s (%s) plan=%s (effective=%s)%s',
|
||||||
|
$user->email,
|
||||||
|
$user->public_id,
|
||||||
|
$result['plan'],
|
||||||
|
$effective,
|
||||||
|
$reset ? ' [reset]' : '',
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->table(
|
||||||
|
['Metric', 'Count'],
|
||||||
|
collect($result['counts'])->map(fn ($v, $k) => [$k, (string) $v])->values()->all()
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Pos;
|
||||||
|
|
||||||
|
use App\Models\Pos\ProSubscription;
|
||||||
|
use App\Models\PosCategory;
|
||||||
|
use App\Models\PosDevice;
|
||||||
|
use App\Models\PosLocation;
|
||||||
|
use App\Models\PosMember;
|
||||||
|
use App\Models\PosPayment;
|
||||||
|
use App\Models\PosProduct;
|
||||||
|
use App\Models\PosSale;
|
||||||
|
use App\Models\PosSaleLine;
|
||||||
|
use App\Models\PosStation;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class DemoSeeder
|
||||||
|
{
|
||||||
|
/** @var array<string, array{locations: int, products_per_location: int, stations: int, sales_per_location: int}> */
|
||||||
|
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<string, int>}
|
||||||
|
*/
|
||||||
|
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<PosStation> */
|
||||||
|
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<PosCategory> */
|
||||||
|
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<PosLocation> */
|
||||||
|
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<PosCategory> $categories
|
||||||
|
* @param list<PosStation> $stations
|
||||||
|
* @return list<PosProduct>
|
||||||
|
*/
|
||||||
|
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<PosProduct> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Pos\ProSubscription;
|
||||||
|
use App\Models\PosLocation;
|
||||||
|
use App\Models\PosProduct;
|
||||||
|
use App\Models\PosSale;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Pos\SubscriptionService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DemoSeedCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
config([
|
||||||
|
'pos.pro.enabled' => true,
|
||||||
|
'billing.api_url' => 'https://billing.test',
|
||||||
|
'billing.api_key' => 'test-key',
|
||||||
|
'pos.free.max_locations' => 1,
|
||||||
|
'pos.free.max_products' => 50,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_free_seed_respects_location_and_product_caps(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'demo-pos-free',
|
||||||
|
'name' => 'Demo Free',
|
||||||
|
'email' => 'demo-free@ladill.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$exit = Artisan::call('demo:seed', ['identity' => $user->email, '--plan' => 'free']);
|
||||||
|
$this->assertSame(0, $exit);
|
||||||
|
|
||||||
|
$this->assertSame(1, PosLocation::owned($user->public_id)->count());
|
||||||
|
$this->assertLessThanOrEqual(50, PosProduct::owned($user->public_id)->count());
|
||||||
|
$this->assertGreaterThan(0, PosProduct::owned($user->public_id)->count());
|
||||||
|
$this->assertGreaterThan(0, PosSale::owned($user->public_id)->where('payment_method', 'cash')->count());
|
||||||
|
$this->assertSame('free', app(SubscriptionService::class)->planKey($user));
|
||||||
|
$this->assertNull(ProSubscription::where('user_id', $user->id)->first());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pro_seed_multi_location_and_reset_is_tenant_scoped(): void
|
||||||
|
{
|
||||||
|
$target = User::create([
|
||||||
|
'public_id' => 'demo-pos-pro',
|
||||||
|
'name' => 'Demo Pro',
|
||||||
|
'email' => 'demo-pro@ladill.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
$other = User::create([
|
||||||
|
'public_id' => 'other-pos-owner',
|
||||||
|
'name' => 'Other',
|
||||||
|
'email' => 'other-pos@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
PosLocation::create([
|
||||||
|
'owner_ref' => $other->public_id,
|
||||||
|
'name' => 'Keep Branch',
|
||||||
|
'currency' => 'GHS',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Artisan::call('demo:seed', ['identity' => $target->public_id, '--plan' => 'pro']);
|
||||||
|
$this->assertTrue(app(SubscriptionService::class)->isPro($target));
|
||||||
|
$this->assertGreaterThanOrEqual(2, PosLocation::owned($target->public_id)->count());
|
||||||
|
$this->assertGreaterThan(50, PosProduct::owned($target->public_id)->count());
|
||||||
|
|
||||||
|
Artisan::call('demo:seed', [
|
||||||
|
'identity' => $target->email,
|
||||||
|
'--plan' => 'enterprise',
|
||||||
|
'--reset' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('pos_locations', ['owner_ref' => $other->public_id, 'name' => 'Keep Branch']);
|
||||||
|
$this->assertSame('enterprise', ProSubscription::where('user_id', $target->id)->value('plan'));
|
||||||
|
$this->assertGreaterThanOrEqual(4, PosLocation::owned($target->public_id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_idempotent_without_reset(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'demo-pos-idem',
|
||||||
|
'name' => 'Demo',
|
||||||
|
'email' => 'demo-idem-pos@ladill.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Artisan::call('demo:seed', ['identity' => $user->email, '--plan' => 'free']);
|
||||||
|
$products = PosProduct::owned($user->public_id)->count();
|
||||||
|
$sales = PosSale::owned($user->public_id)->count();
|
||||||
|
|
||||||
|
Artisan::call('demo:seed', ['identity' => $user->email, '--plan' => 'free']);
|
||||||
|
$this->assertSame($products, PosProduct::owned($user->public_id)->count());
|
||||||
|
$this->assertSame($sales, PosSale::owned($user->public_id)->count());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user