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:
kelly
2025-12-03 12:47:33 -07:00
parent f8f219f00b
commit b92ba4b86d
5 changed files with 347 additions and 3 deletions

View File

@@ -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',
],
]);
}
}

View File

@@ -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');
}
}

View 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.");
}
}
}
}

View 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;
}
}

View File

@@ -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: {