Add Woo Manager demo:seed for store walkthroughs.
Deploy Ladill Woo Manager / deploy (push) Successful in 2m24s
Deploy Ladill Woo Manager / deploy (push) Successful in 2m24s
Seed stores, products, and orders within Free caps and paid-tier volume. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Woo\ProSubscription;
|
||||||
|
use App\Models\WooCategory;
|
||||||
|
use App\Models\WooOrder;
|
||||||
|
use App\Models\WooProduct;
|
||||||
|
use App\Models\WooStore;
|
||||||
|
use App\Services\Woo\SubscriptionService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed local demo WooCommerce manager data (no live WordPress).
|
||||||
|
*/
|
||||||
|
class DemoSeedCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'demo:seed
|
||||||
|
{identity : User email or public_id}
|
||||||
|
{--plan=free : free|pro|enterprise}
|
||||||
|
{--reset : Wipe this user\'s stores/subscription before seeding}';
|
||||||
|
|
||||||
|
protected $description = 'Seed idempotent demo stores, categories, products, and orders for a user';
|
||||||
|
|
||||||
|
public function handle(SubscriptionService $subscriptions): int
|
||||||
|
{
|
||||||
|
$plan = strtolower((string) $this->option('plan'));
|
||||||
|
if (! in_array($plan, ['free', 'pro', 'enterprise'], true)) {
|
||||||
|
$this->error('Invalid --plan. Use free, pro, or enterprise.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->resolveUser((string) $this->argument('identity'));
|
||||||
|
if (! $user) {
|
||||||
|
$this->error('User not found for identity: '.$this->argument('identity'));
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('reset')) {
|
||||||
|
$this->resetTenant($user);
|
||||||
|
$this->info('Reset demo data for '.$user->email);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assignPlan($subscriptions, $user, $plan);
|
||||||
|
|
||||||
|
$existing = WooStore::query()
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->where('metadata->demo', true)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($existing > 0 && ! $this->option('reset')) {
|
||||||
|
$this->info('Demo stores already present; plan ensured (idempotent).');
|
||||||
|
$this->line(sprintf('user=%s plan=%s stores=%d', $user->email, $plan, $existing));
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$storeSpecs = $this->storeSpecs($plan);
|
||||||
|
foreach ($storeSpecs as $spec) {
|
||||||
|
$this->seedStore($user, $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info(sprintf(
|
||||||
|
'Seeded %d demo store(s) for %s (%s).',
|
||||||
|
count($storeSpecs),
|
||||||
|
$user->email,
|
||||||
|
$plan,
|
||||||
|
));
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveUser(string $identity): ?User
|
||||||
|
{
|
||||||
|
return User::query()
|
||||||
|
->where(function ($query) use ($identity) {
|
||||||
|
$query->where('email', $identity)->orWhere('public_id', $identity);
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignPlan(SubscriptionService $subscriptions, User $user, string $plan): void
|
||||||
|
{
|
||||||
|
if ($plan === 'free') {
|
||||||
|
ProSubscription::query()->where('user_id', $user->id)->delete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscriptions->activatePrepaid(
|
||||||
|
$user,
|
||||||
|
$plan === 'enterprise' ? ProSubscription::PLAN_ENTERPRISE : ProSubscription::PLAN_PRO,
|
||||||
|
12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resetTenant(User $user): void
|
||||||
|
{
|
||||||
|
DB::transaction(function () use ($user) {
|
||||||
|
WooStore::query()->where('user_id', $user->id)->each(function (WooStore $store) {
|
||||||
|
$store->delete();
|
||||||
|
});
|
||||||
|
ProSubscription::query()->where('user_id', $user->id)->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{site_name: string, site_url: string, products: int, orders: int, categories: list<string>}>
|
||||||
|
*/
|
||||||
|
private function storeSpecs(string $plan): array
|
||||||
|
{
|
||||||
|
$baseCategories = ['Apparel', 'Accessories', 'Home'];
|
||||||
|
|
||||||
|
return match ($plan) {
|
||||||
|
'enterprise' => [
|
||||||
|
[
|
||||||
|
'site_name' => 'Demo Accra Shop',
|
||||||
|
'site_url' => 'https://demo-accra.example.com',
|
||||||
|
'products' => 30,
|
||||||
|
'orders' => 12,
|
||||||
|
'categories' => $baseCategories,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'site_name' => 'Demo Kumasi Shop',
|
||||||
|
'site_url' => 'https://demo-kumasi.example.com',
|
||||||
|
'products' => 24,
|
||||||
|
'orders' => 8,
|
||||||
|
'categories' => ['Electronics', 'Gadgets'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'site_name' => 'Demo Wholesale',
|
||||||
|
'site_url' => 'https://demo-wholesale.example.com',
|
||||||
|
'products' => 20,
|
||||||
|
'orders' => 6,
|
||||||
|
'categories' => ['Bulk', 'Packaging'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'pro' => [
|
||||||
|
[
|
||||||
|
'site_name' => 'Demo Pro Shop',
|
||||||
|
'site_url' => 'https://demo-pro-shop.example.com',
|
||||||
|
'products' => 35,
|
||||||
|
'orders' => 10,
|
||||||
|
'categories' => $baseCategories,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'site_name' => 'Demo Outlet',
|
||||||
|
'site_url' => 'https://demo-outlet.example.com',
|
||||||
|
'products' => 15,
|
||||||
|
'orders' => 5,
|
||||||
|
'categories' => ['Clearance', 'Seasonal'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
default => [
|
||||||
|
[
|
||||||
|
'site_name' => 'Demo Free Shop',
|
||||||
|
'site_url' => 'https://demo-free-shop.example.com',
|
||||||
|
'products' => 15,
|
||||||
|
'orders' => 4,
|
||||||
|
'categories' => $baseCategories,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{site_name: string, site_url: string, products: int, orders: int, categories: list<string>} $spec
|
||||||
|
*/
|
||||||
|
private function seedStore(User $user, array $spec): void
|
||||||
|
{
|
||||||
|
$store = WooStore::query()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'site_url' => $spec['site_url'],
|
||||||
|
'site_name' => $spec['site_name'],
|
||||||
|
'status' => WooStore::STATUS_ACTIVE,
|
||||||
|
'connected_at' => now(),
|
||||||
|
'metadata' => [
|
||||||
|
'demo' => true,
|
||||||
|
'currency' => 'GHS',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$categoryIds = [];
|
||||||
|
foreach ($spec['categories'] as $i => $name) {
|
||||||
|
$externalId = $i + 1;
|
||||||
|
WooCategory::query()->create([
|
||||||
|
'woo_store_id' => $store->id,
|
||||||
|
'external_id' => $externalId,
|
||||||
|
'name' => $name,
|
||||||
|
'slug' => Str::slug($name),
|
||||||
|
'product_count' => 0,
|
||||||
|
]);
|
||||||
|
$categoryIds[] = $externalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = [];
|
||||||
|
for ($i = 1; $i <= $spec['products']; $i++) {
|
||||||
|
$cat = $categoryIds[($i - 1) % max(count($categoryIds), 1)] ?? null;
|
||||||
|
$product = WooProduct::query()->create([
|
||||||
|
'woo_store_id' => $store->id,
|
||||||
|
'external_id' => $i,
|
||||||
|
'name' => sprintf('%s Product %02d', $spec['site_name'], $i),
|
||||||
|
'slug' => 'demo-product-'.$store->id.'-'.$i,
|
||||||
|
'sku' => 'DEMO-'.$store->id.'-'.$i,
|
||||||
|
'status' => WooProduct::STATUS_PUBLISH,
|
||||||
|
'type' => 'simple',
|
||||||
|
'regular_price_minor' => 2_500 + ($i * 100),
|
||||||
|
'stock_quantity' => 10 + $i,
|
||||||
|
'manage_stock' => true,
|
||||||
|
'category_external_ids' => $cat ? [$cat] : [],
|
||||||
|
'short_description' => 'Local demo product (no live WordPress).',
|
||||||
|
]);
|
||||||
|
$products[] = $product;
|
||||||
|
|
||||||
|
if ($cat) {
|
||||||
|
WooCategory::query()
|
||||||
|
->where('woo_store_id', $store->id)
|
||||||
|
->where('external_id', $cat)
|
||||||
|
->increment('product_count');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 1; $i <= $spec['orders']; $i++) {
|
||||||
|
$product = $products[($i - 1) % count($products)];
|
||||||
|
$qty = 1 + ($i % 3);
|
||||||
|
$lineTotal = (int) $product->regular_price_minor * $qty;
|
||||||
|
|
||||||
|
WooOrder::query()->create([
|
||||||
|
'woo_store_id' => $store->id,
|
||||||
|
'external_order_id' => 1000 + $i,
|
||||||
|
'order_number' => (string) (1000 + $i),
|
||||||
|
'wc_status' => $i % 4 === 0 ? 'completed' : 'processing',
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
'customer_name' => 'Demo Customer '.$i,
|
||||||
|
'customer_email' => 'customer'.$i.'@example.com',
|
||||||
|
'customer_phone' => '+233200000'.str_pad((string) $i, 3, '0', STR_PAD_LEFT),
|
||||||
|
'line_items' => [[
|
||||||
|
'id' => $product->external_id,
|
||||||
|
'name' => $product->name,
|
||||||
|
'quantity' => $qty,
|
||||||
|
'total' => number_format($lineTotal / 100, 2, '.', ''),
|
||||||
|
'sku' => $product->sku,
|
||||||
|
]],
|
||||||
|
'currency' => 'GHS',
|
||||||
|
'total_minor' => $lineTotal,
|
||||||
|
'fulfillment_status' => $i % 3 === 0
|
||||||
|
? WooOrder::FULFILLMENT_DELIVERED
|
||||||
|
: WooOrder::FULFILLMENT_NEW,
|
||||||
|
'wc_updated_at' => now()->subDays($spec['orders'] - $i),
|
||||||
|
'metadata' => ['demo' => true],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Woo\ProSubscription;
|
||||||
|
use App\Models\WooOrder;
|
||||||
|
use App\Models\WooProduct;
|
||||||
|
use App\Models\WooStore;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DemoSeedCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_free_plan_seeds_one_store_within_product_limit(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['email' => 'demo-free@ladill.com']);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', ['identity' => $user->email, '--plan' => 'free'])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('woo_pro_subscriptions', ['user_id' => $user->id]);
|
||||||
|
|
||||||
|
$stores = WooStore::query()->where('user_id', $user->id)->get();
|
||||||
|
$this->assertCount(1, $stores);
|
||||||
|
$this->assertSame(WooStore::STATUS_ACTIVE, $stores->first()->status);
|
||||||
|
|
||||||
|
$productCount = WooProduct::query()->where('woo_store_id', $stores->first()->id)->count();
|
||||||
|
$this->assertLessThanOrEqual(20, $productCount);
|
||||||
|
$this->assertGreaterThan(0, $productCount);
|
||||||
|
$this->assertGreaterThan(0, WooOrder::query()->where('woo_store_id', $stores->first()->id)->count());
|
||||||
|
$this->assertGreaterThan(0, $stores->first()->categories()->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pro_plan_assigns_subscription_and_multiple_stores(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['public_id' => 'woo-demo-pro-001']);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', ['identity' => 'woo-demo-pro-001', '--plan' => 'pro'])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$sub = ProSubscription::query()->where('user_id', $user->id)->first();
|
||||||
|
$this->assertNotNull($sub);
|
||||||
|
$this->assertSame(ProSubscription::PLAN_PRO, $sub->plan);
|
||||||
|
$this->assertTrue($sub->entitled());
|
||||||
|
|
||||||
|
$this->assertCount(2, WooStore::query()->where('user_id', $user->id)->get());
|
||||||
|
$this->assertGreaterThan(20, WooProduct::query()
|
||||||
|
->whereIn('woo_store_id', WooStore::query()->where('user_id', $user->id)->pluck('id'))
|
||||||
|
->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_idempotent_without_reset_and_reset_reseeds(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['email' => 'demo-pro@ladill.com']);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', ['identity' => $user->email, '--plan' => 'enterprise'])
|
||||||
|
->assertSuccessful();
|
||||||
|
$firstCount = WooStore::query()->where('user_id', $user->id)->count();
|
||||||
|
$this->assertSame(3, $firstCount);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', ['identity' => $user->email, '--plan' => 'enterprise'])
|
||||||
|
->assertSuccessful();
|
||||||
|
$this->assertSame(3, WooStore::query()->where('user_id', $user->id)->count());
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => $user->email,
|
||||||
|
'--plan' => 'free',
|
||||||
|
'--reset' => true,
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$this->assertCount(1, WooStore::query()->where('user_id', $user->id)->get());
|
||||||
|
$this->assertDatabaseMissing('woo_pro_subscriptions', ['user_id' => $user->id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user