feat: add dev environment seeders and fixes
- Add DevCleanupSeeder to remove non-Thunder Bud products (keeps only TB- prefix) - Add DevMediaSyncSeeder to update brand/product media paths from MinIO - Fix CustomerController to pass $benefits array to feature-disabled view - Update BrandSeeder to include Twisties brand - Make vite.config.js read VITE_PORT from env (fixes port conflict) Run on dev.cannabrands.app: php artisan db:seed --class=DevCleanupSeeder php artisan db:seed --class=DevMediaSyncSeeder
This commit is contained in:
@@ -22,6 +22,12 @@ class CustomerController extends Controller
|
||||
'business' => $business,
|
||||
'feature' => 'Customers',
|
||||
'description' => 'The Customers feature requires CRM to be enabled for your business.',
|
||||
'benefits' => [
|
||||
'Manage all your customer accounts in one place',
|
||||
'Track contact information and order history',
|
||||
'Build stronger customer relationships',
|
||||
'Access customer insights and analytics',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -42,6 +48,12 @@ class CustomerController extends Controller
|
||||
'business' => $business,
|
||||
'feature' => 'Customers',
|
||||
'description' => 'The Customers feature requires CRM to be enabled for your business.',
|
||||
'benefits' => [
|
||||
'Manage all your customer accounts in one place',
|
||||
'Track contact information and order history',
|
||||
'Build stronger customer relationships',
|
||||
'Access customer insights and analytics',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class BrandSeeder extends Seeder
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* All 14 brands belong to Cannabrands (the seller/manufacturer company)
|
||||
* All 12 brands belong to Cannabrands (the seller/manufacturer company)
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
@@ -139,6 +139,6 @@ class BrandSeeder extends Seeder
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->info('✅ Created 14 brands under Cannabrands business');
|
||||
$this->command->info('✅ Created 12 brands under Cannabrands business');
|
||||
}
|
||||
}
|
||||
|
||||
168
database/seeders/DevCleanupSeeder.php
Normal file
168
database/seeders/DevCleanupSeeder.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Development Environment Cleanup Seeder
|
||||
*
|
||||
* This seeder is intended to be run manually on the dev.cannabrands.app environment
|
||||
* to remove test/sample data and keep only Thunder Bud products.
|
||||
*
|
||||
* Run with: php artisan db:seed --class=DevCleanupSeeder
|
||||
*/
|
||||
class DevCleanupSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Thunder Bud SKU prefix to preserve.
|
||||
* Only products with SKUs starting with this prefix will be kept.
|
||||
*/
|
||||
protected string $thunderBudPrefix = 'TB-';
|
||||
|
||||
/**
|
||||
* Brands to remove (test/sample brands).
|
||||
*/
|
||||
protected array $brandsToRemove = [
|
||||
'bulk',
|
||||
'twisites', // misspelled version only
|
||||
];
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('Starting dev environment cleanup...');
|
||||
|
||||
// Step 1: Remove non-Thunder Bud products
|
||||
$this->removeNonThunderBudProducts();
|
||||
|
||||
// Step 2: Remove test brands (Bulk, Twisties, Twisites)
|
||||
$this->removeTestBrands();
|
||||
|
||||
// Step 3: Clean up orphaned data
|
||||
$this->cleanupOrphanedData();
|
||||
|
||||
$this->command->info('Dev environment cleanup complete!');
|
||||
}
|
||||
|
||||
protected function removeNonThunderBudProducts(): void
|
||||
{
|
||||
$this->command->info('Removing non-Thunder Bud products...');
|
||||
|
||||
// Find products that DON'T have the TB- prefix
|
||||
$productsToDelete = Product::where('sku', 'not like', $this->thunderBudPrefix.'%')->get();
|
||||
$count = $productsToDelete->count();
|
||||
|
||||
if ($count === 0) {
|
||||
$this->command->info('No non-Thunder Bud products found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->command->info("Found {$count} products to remove.");
|
||||
|
||||
// Delete related data first (order items, inventory, etc.)
|
||||
$productIds = $productsToDelete->pluck('id')->toArray();
|
||||
|
||||
// Delete order items referencing these products
|
||||
$orderItemsDeleted = DB::table('order_items')
|
||||
->whereIn('product_id', $productIds)
|
||||
->delete();
|
||||
|
||||
if ($orderItemsDeleted > 0) {
|
||||
$this->command->info("Deleted {$orderItemsDeleted} order items.");
|
||||
}
|
||||
|
||||
// Delete inventory records
|
||||
if (DB::getSchemaBuilder()->hasTable('inventories')) {
|
||||
$inventoryDeleted = DB::table('inventories')
|
||||
->whereIn('product_id', $productIds)
|
||||
->delete();
|
||||
|
||||
if ($inventoryDeleted > 0) {
|
||||
$this->command->info("Deleted {$inventoryDeleted} inventory records.");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete product variants if they exist
|
||||
if (DB::getSchemaBuilder()->hasTable('product_variants')) {
|
||||
$variantsDeleted = DB::table('product_variants')
|
||||
->whereIn('product_id', $productIds)
|
||||
->delete();
|
||||
|
||||
if ($variantsDeleted > 0) {
|
||||
$this->command->info("Deleted {$variantsDeleted} product variants.");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete products
|
||||
$deleted = Product::whereIn('id', $productIds)->delete();
|
||||
$this->command->info("Deleted {$deleted} non-Thunder Bud products.");
|
||||
|
||||
// List remaining Thunder Bud products
|
||||
$remaining = Product::count();
|
||||
$this->command->info("Remaining products: {$remaining}");
|
||||
}
|
||||
|
||||
protected function removeTestBrands(): void
|
||||
{
|
||||
$this->command->info('Removing test brands (Bulk, Twisites)...');
|
||||
|
||||
$brandsToDelete = Brand::whereIn('slug', $this->brandsToRemove)->get();
|
||||
|
||||
if ($brandsToDelete->isEmpty()) {
|
||||
$this->command->info('No test brands found to remove.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($brandsToDelete as $brand) {
|
||||
// Check if brand has products (should have been deleted in previous step)
|
||||
$productCount = $brand->products()->count();
|
||||
|
||||
if ($productCount > 0) {
|
||||
$this->command->warn("Brand '{$brand->name}' still has {$productCount} products. Deleting them first...");
|
||||
$brand->products()->delete();
|
||||
}
|
||||
|
||||
$brand->delete();
|
||||
$this->command->info("Deleted brand: {$brand->name}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function cleanupOrphanedData(): void
|
||||
{
|
||||
$this->command->info('Cleaning up orphaned data...');
|
||||
|
||||
// Delete orders with no items
|
||||
$emptyOrders = DB::table('orders')
|
||||
->whereNotExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('order_items')
|
||||
->whereColumn('order_items.order_id', 'orders.id');
|
||||
})
|
||||
->delete();
|
||||
|
||||
if ($emptyOrders > 0) {
|
||||
$this->command->info("Deleted {$emptyOrders} empty orders.");
|
||||
}
|
||||
|
||||
// Delete orphaned promotions (if table exists)
|
||||
if (DB::getSchemaBuilder()->hasTable('promotions')) {
|
||||
$orphanedPromotions = DB::table('promotions')
|
||||
->whereNotNull('brand_id')
|
||||
->whereNotExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('brands')
|
||||
->whereColumn('brands.id', 'promotions.brand_id');
|
||||
})
|
||||
->delete();
|
||||
|
||||
if ($orphanedPromotions > 0) {
|
||||
$this->command->info("Deleted {$orphanedPromotions} orphaned promotions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
164
database/seeders/DevMediaSyncSeeder.php
Normal file
164
database/seeders/DevMediaSyncSeeder.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Development Environment Media Sync Seeder
|
||||
*
|
||||
* This seeder updates brand and product media paths in the database
|
||||
* to match the expected MinIO structure. It does NOT copy files -
|
||||
* files should be synced separately using mc (MinIO Client) or rsync.
|
||||
*
|
||||
* Run with: php artisan db:seed --class=DevMediaSyncSeeder
|
||||
*
|
||||
* To sync actual files from local to dev MinIO, use:
|
||||
* mc mirror local-minio/media/businesses/cannabrands/brands/ dev-minio/media/businesses/cannabrands/brands/
|
||||
*/
|
||||
class DevMediaSyncSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Thunder Bud SKU prefix.
|
||||
*/
|
||||
protected string $thunderBudPrefix = 'TB-';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('Syncing media paths for dev environment...');
|
||||
|
||||
$this->syncBrandMedia();
|
||||
$this->syncProductMedia();
|
||||
|
||||
$this->command->info('Dev media sync complete!');
|
||||
$this->command->newLine();
|
||||
$this->command->info('Next steps:');
|
||||
$this->command->line('1. Sync actual media files to dev MinIO using mc mirror or similar');
|
||||
$this->command->line('2. Verify images are accessible at the configured AWS_URL');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update brand logo_path and banner_path to match expected structure.
|
||||
*/
|
||||
protected function syncBrandMedia(): void
|
||||
{
|
||||
$this->command->info('Syncing brand media paths...');
|
||||
|
||||
$brands = Brand::with('business')->get();
|
||||
$updated = 0;
|
||||
|
||||
foreach ($brands as $brand) {
|
||||
$businessSlug = $brand->business->slug ?? 'cannabrands';
|
||||
|
||||
// Set expected paths based on MinIO structure
|
||||
$basePath = "businesses/{$businessSlug}/brands/{$brand->slug}/branding";
|
||||
|
||||
// Try to find actual files or set expected paths
|
||||
$logoPath = $this->findMediaFile($basePath, 'logo', ['png', 'jpg', 'jpeg']);
|
||||
$bannerPath = $this->findMediaFile($basePath, 'banner', ['jpg', 'jpeg', 'png']);
|
||||
|
||||
// Update if we found paths or if current paths are null
|
||||
if ($logoPath || $bannerPath || ! $brand->logo_path || ! $brand->banner_path) {
|
||||
$brand->logo_path = $logoPath ?? "{$basePath}/logo.png";
|
||||
$brand->banner_path = $bannerPath ?? "{$basePath}/banner.jpg";
|
||||
$brand->save();
|
||||
$updated++;
|
||||
|
||||
$this->command->line(" - {$brand->name}: logo={$brand->logo_path}, banner={$brand->banner_path}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info("Updated {$updated} brand media paths.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Thunder Bud product image_path fields.
|
||||
*/
|
||||
protected function syncProductMedia(): void
|
||||
{
|
||||
$this->command->info('Syncing Thunder Bud product media paths...');
|
||||
|
||||
// Find Thunder Bud brand
|
||||
$thunderBudBrand = Brand::where('slug', 'thunder-bud')
|
||||
->orWhere('slug', 'thunderbud')
|
||||
->first();
|
||||
|
||||
if (! $thunderBudBrand) {
|
||||
$this->command->warn('Thunder Bud brand not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$businessSlug = $thunderBudBrand->business->slug ?? 'cannabrands';
|
||||
$brandSlug = $thunderBudBrand->slug;
|
||||
|
||||
// Find all Thunder Bud products (TB- prefix only)
|
||||
$products = Product::where('sku', 'like', $this->thunderBudPrefix.'%')->get();
|
||||
|
||||
$updated = 0;
|
||||
|
||||
foreach ($products as $product) {
|
||||
// Set expected image path based on SKU
|
||||
$imagePath = "businesses/{$businessSlug}/brands/{$brandSlug}/products/{$product->sku}/images";
|
||||
|
||||
// Try to find actual image file
|
||||
$actualImage = $this->findProductImage($imagePath);
|
||||
|
||||
if ($actualImage) {
|
||||
$product->image_path = $actualImage;
|
||||
$product->save();
|
||||
$updated++;
|
||||
$this->command->line(" - {$product->sku}: {$actualImage}");
|
||||
} else {
|
||||
// Set a default expected path
|
||||
$expectedPath = "{$imagePath}/{$product->slug}.png";
|
||||
$product->image_path = $expectedPath;
|
||||
$product->save();
|
||||
$updated++;
|
||||
$this->command->line(" - {$product->sku}: {$expectedPath} (expected)");
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info("Updated {$updated} product media paths.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a media file in MinIO with the given base path and name.
|
||||
*/
|
||||
protected function findMediaFile(string $basePath, string $name, array $extensions): ?string
|
||||
{
|
||||
foreach ($extensions as $ext) {
|
||||
$path = "{$basePath}/{$name}.{$ext}";
|
||||
if (Storage::exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a product image in the given path.
|
||||
*/
|
||||
protected function findProductImage(string $imagePath): ?string
|
||||
{
|
||||
try {
|
||||
$files = Storage::files($imagePath);
|
||||
|
||||
// Return the first image file found
|
||||
foreach ($files as $file) {
|
||||
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['png', 'jpg', 'jpeg', 'webp'])) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Directory doesn't exist
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export default defineConfig(({ mode }) => {
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
port: parseInt(env.VITE_PORT || '5173'),
|
||||
strictPort: true,
|
||||
origin: viteOrigin,
|
||||
cors: {
|
||||
|
||||
Reference in New Issue
Block a user