uuid('id')->primary(); $table->string('name')->nullable(); $table->string('slug')->unique(); $table->string('sku')->nullable()->unique(); $table->text('short_description')->nullable(); $table->longText('description')->nullable(); $table->string('type')->default('simple'); // simple, variable, grouped, external, booking $table->string('stripe_product_id')->nullable(); $table->timestamp('sale_start')->nullable(); $table->timestamp('sale_end')->nullable(); $table->boolean('manage_stock')->default(false); $table->integer('stock_quantity')->default(0); $table->integer('low_stock_threshold')->nullable(); $table->boolean('in_stock')->default(true); $table->string('stock_status')->default('instock'); // instock, outofstock, onbackorder $table->decimal('weight', 10, 2)->nullable(); $table->decimal('length', 10, 2)->nullable(); $table->decimal('width', 10, 2)->nullable(); $table->decimal('height', 10, 2)->nullable(); $table->boolean('virtual')->default(false); $table->boolean('downloadable')->default(false); $table->uuid('parent_id')->nullable(); $table->boolean('featured')->default(false); $table->boolean('is_visible')->default(true); $table->string('status')->default('draft'); // draft, published, archived $table->timestamp('published_at')->nullable(); $table->integer('sort_order')->default(0); $table->json('meta')->nullable(); $table->string('tax_class')->nullable(); $table->timestamps(); $table->softDeletes(); $table->index(['slug', 'status']); $table->index(['featured', 'is_visible', 'status']); $table->index(['type', 'status', 'is_visible']); $table->index('parent_id'); $table->foreign('parent_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); }); } else { // Add new fields to existing products table Schema::table(config('shop.tables.products', 'products'), function (Blueprint $table) { if (!Schema::hasColumn(config('shop.tables.products', 'products'), 'name')) { $table->string('name')->after('id'); } if (!Schema::hasColumn(config('shop.tables.products', 'products'), 'short_description')) { $table->text('short_description')->nullable()->after('sku'); } if (!Schema::hasColumn(config('shop.tables.products', 'products'), 'description')) { $table->longText('description')->nullable()->after('short_description'); } if (!Schema::hasColumn(config('shop.tables.products', 'products'), 'low_stock_threshold')) { $table->integer('low_stock_threshold')->nullable()->after('stock_quantity'); } if (!Schema::hasColumn(config('shop.tables.products', 'products'), 'published_at')) { $table->timestamp('published_at')->nullable()->after('status'); } if (!Schema::hasColumn(config('shop.tables.products', 'products'), 'sort_order')) { $table->integer('sort_order')->default(0)->after('published_at'); } }); } // Product categories table if (!Schema::hasTable(config('shop.tables.product_categories', 'product_categories'))) { Schema::create(config('shop.tables.product_categories', 'product_categories'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('name')->nullable(); $table->string('slug')->unique(); $table->text('description')->nullable(); $table->uuid('parent_id')->nullable(); $table->integer('sort_order')->default(0); $table->boolean('is_visible')->default(true); $table->json('meta')->nullable(); $table->timestamps(); $table->softDeletes(); $table->index(['parent_id', 'is_visible']); $table->index(['slug', 'is_visible']); $table->foreign('parent_id')->references('id')->on(config('shop.tables.product_categories', 'product_categories'))->onDelete('cascade'); }); } // Product category pivot table if (!Schema::hasTable(config('shop.tables.product_category_product', 'product_category_product'))) { Schema::create(config('shop.tables.product_category_product', 'product_category_product'), function (Blueprint $table) { $table->uuid('product_id'); $table->uuid('product_category_id'); $table->integer('sort_order')->default(0); $table->timestamps(); $table->primary(['product_id', 'product_category_id'], 'product_category_product_primary'); $table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); $table->foreign('product_category_id')->references('id')->on(config('shop.tables.product_categories', 'product_categories'))->onDelete('cascade'); }); } // Product prices table (multi-currency support) if (!Schema::hasTable(config('shop.tables.product_prices', 'product_prices'))) { Schema::create(config('shop.tables.product_prices', 'product_prices'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuidMorphs('purchasable'); $table->string('stripe_price_id')->nullable(); $table->string('name')->nullable(); $table->string('type')->default('one_time'); // one_time, recurring $table->string('currency', 3)->default('EUR'); $table->integer('unit_amount')->default(0); // Store as smallest currency unit (cents) $table->integer('sale_unit_amount')->nullable(); $table->boolean('is_default')->default(false); $table->boolean('active')->default(true); $table->string('billing_scheme')->nullable(); // per_unit, tiered $table->string('interval')->nullable(); // day, week, month, year $table->integer('interval_count')->nullable(); $table->integer('trial_period_days')->nullable(); $table->json('meta')->nullable(); $table->timestamps(); $table->index('currency'); $table->index(['purchasable_type', 'purchasable_id', 'is_default', 'active'], 'pp_purchasable_default_active_idx'); }); } // Product attributes table if (!Schema::hasTable(config('shop.tables.product_attributes', 'product_attributes'))) { Schema::create(config('shop.tables.product_attributes', 'product_attributes'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuid('product_id'); $table->string('key'); $table->text('value')->nullable(); $table->string('type')->default('text'); // text, select, color, image $table->integer('sort_order')->default(0); $table->json('meta')->nullable(); $table->timestamps(); $table->index(['product_id', 'key']); $table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); }); } // Product stocks table (claims) if (!Schema::hasTable(config('shop.tables.product_stocks', 'product_stocks'))) { Schema::create(config('shop.tables.product_stocks', 'product_stocks'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuid('product_id'); $table->integer('quantity'); $table->string('type')->default('claimed'); // claimed, adjustment, sale, return $table->string('status')->default('pending'); // pending, completed, cancelled, expired $table->string('reference_type')->nullable(); $table->string('reference_id')->nullable(); $table->timestamp('claimed_from')->nullable(); $table->timestamp('expires_at')->nullable(); $table->text('note')->nullable(); $table->timestamps(); $table->index(['product_id', 'status']); $table->index(['reference_type', 'reference_id']); $table->index(['claimed_from', 'expires_at']); $table->index(['product_id', 'type', 'status', 'claimed_from', 'expires_at']); $table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); }); } // Product stock logs table if (!Schema::hasTable(config('shop.tables.product_stock_logs', 'product_stock_logs'))) { Schema::create(config('shop.tables.product_stock_logs', 'product_stock_logs'), function (Blueprint $table) { $table->id(); $table->uuid('product_id'); $table->integer('quantity_change'); $table->integer('quantity_after'); $table->string('type'); // increase, decrease, adjustment $table->string('reference_type')->nullable(); $table->string('reference_id')->nullable(); $table->text('note')->nullable(); $table->timestamps(); $table->index(['product_id', 'created_at']); $table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); }); } // Product relations table (related, upsell, cross-sell) if (!Schema::hasTable(config('shop.tables.product_relations', 'product_relations'))) { Schema::create(config('shop.tables.product_relations', 'product_relations'), function (Blueprint $table) { $table->uuid('product_id'); $table->uuid('related_product_id'); $table->string('type')->default('related'); // related, upsell, cross-sell $table->integer('sort_order')->default(0); $table->timestamps(); $table->primary(['product_id', 'related_product_id', 'type'], 'product_relations_primary'); $table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); $table->foreign('related_product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); $table->index(['product_id', 'type']); }); } // Product actions table if (!Schema::hasTable(config('shop.tables.product_actions', 'product_actions'))) { Schema::create(config('shop.tables.product_actions', 'product_actions'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuid('product_id')->nullable(); $table->json('events')->nullable(); // e.g. ["purchased","paid","refunded","viewed"] $table->string('class'); // e.g. \App\... $table->string('method')->nullable(); // null means __invoke via constructor params $table->boolean('defer')->default(true); // queued when true, sync otherwise $table->json('parameters')->nullable(); $table->boolean('active')->default(true); $table->integer('sort_order')->default(0); $table->timestamps(); $table->index(['product_id', 'active']); $table->foreign('product_id') ->references('id') ->on(config('shop.tables.products', 'products')) ->nullOnDelete(); }); } // Product purchases table if (!Schema::hasTable(config('shop.tables.product_purchases', 'product_purchases'))) { Schema::create(config('shop.tables.product_purchases', 'product_purchases'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('status')->default('pending'); $table->foreignUuid('cart_id')->nullable(); $table->foreignUuid('price_id')->nullable(); $table->nullableUuidMorphs('purchasable'); $table->nullableUuidMorphs('purchaser'); $table->integer('quantity')->default(1); $table->decimal('amount', 10, 8)->nullable(); $table->decimal('amount_paid', 10, 8)->default(0); $table->string('charge_id')->nullable(); $table->timestamp('from')->nullable(); $table->timestamp('until')->nullable(); $table->json('meta')->nullable(); $table->timestamps(); }); } // Carts table if (!Schema::hasTable(config('shop.tables.carts', 'carts'))) { Schema::create(config('shop.tables.carts', 'carts'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('session_id')->nullable()->unique(); $table->nullableUuidMorphs('customer'); $table->string('currency', 3)->default('USD'); $table->string('status')->default('active'); // active, abandoned, converted, expired $table->timestamp('last_activity_at')->nullable(); $table->timestamp('expires_at')->nullable(); $table->timestamp('converted_at')->nullable(); $table->timestamp('from_date')->nullable(); // Default start date for booking items $table->timestamp('until_date')->nullable(); // Default end date for booking items $table->json('meta')->nullable(); $table->timestamps(); $table->softDeletes(); $table->index(['session_id', 'status']); $table->index(['customer_type', 'customer_id', 'status']); $table->index(['status', 'last_activity_at']); }); } // Cart items table if (!Schema::hasTable(config('shop.tables.cart_items', 'cart_items'))) { Schema::create(config('shop.tables.cart_items', 'cart_items'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuid('cart_id'); $table->uuidMorphs('purchasable'); $table->foreignUuid('purchase_id')->nullable()->constrained(config('shop.tables.product_purchases', 'product_purchases'))->nullOnDelete(); $table->foreignUuid('price_id')->nullable()->constrained(config('shop.tables.product_prices', 'product_prices'))->nullOnDelete(); $table->integer('quantity')->default(1); $table->integer('price')->default(0); // Stored in cents $table->integer('regular_price')->nullable(); // Stored in cents $table->integer('unit_amount')->nullable(); // Base unit price for 1 quantity, 1 day (in cents) $table->integer('subtotal'); // Stored in cents $table->json('parameters')->nullable(); $table->json('meta')->nullable(); $table->timestamp('from')->nullable(); $table->timestamp('until')->nullable(); $table->timestamps(); $table->index(['cart_id', 'purchasable_id']); $table->index(['cart_id', 'purchasable_type', 'purchasable_id']); $table->index(['from', 'until']); $table->foreign('cart_id')->references('id')->on(config('shop.tables.carts', 'carts'))->onDelete('cascade'); }); } // Cart discounts table if (!Schema::hasTable(config('shop.tables.cart_discounts', 'cart_discounts'))) { Schema::create(config('shop.tables.cart_discounts', 'cart_discounts'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuid('cart_id'); $table->string('code')->nullable(); $table->string('type')->default('percentage'); // percentage, fixed, shipping $table->decimal('amount', 10, 2); $table->decimal('discount_amount', 10, 2); $table->json('meta')->nullable(); $table->timestamps(); $table->index('cart_id'); $table->foreign('cart_id')->references('id')->on(config('shop.tables.carts', 'carts'))->onDelete('cascade'); }); } // Payment provider identities table if (!Schema::hasTable(config('shop.tables.payment_provider_identities', 'payment_provider_identities'))) { Schema::create(config('shop.tables.payment_provider_identities', 'payment_provider_identities'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->nullableUuidMorphs('customer'); $table->string('provider_name'); // stripe, paypal, etc. $table->string('customer_identification_id'); // The provider's customer ID $table->json('meta')->nullable(); $table->timestamps(); $table->index(['customer_type', 'customer_id', 'provider_name'], 'ppi_customer_provider_index'); $table->unique(['customer_type', 'customer_id', 'provider_name'], 'ppi_customer_provider_unique'); }); } // Payment methods table if (!Schema::hasTable(config('shop.tables.payment_methods', 'payment_methods'))) { Schema::create(config('shop.tables.payment_methods', 'payment_methods'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuid('payment_provider_identity_id'); $table->string('provider_payment_method_id'); // The provider's payment method ID $table->string('type')->nullable(); // card, bank_account, etc. $table->string('name')->nullable(); // Custom name given by user $table->string('last_digits')->nullable(); // Last 4 digits of card/account $table->string('last_alphanumeric')->nullable(); // Last characters for non-numeric identifiers (e.g., crypto/wallet) $table->string('brand')->nullable(); // visa, mastercard, etc. $table->integer('exp_month')->nullable(); $table->integer('exp_year')->nullable(); $table->timestamp('expires_at')->nullable(); // General expiration timestamp for non-card methods $table->boolean('is_default')->default(false); $table->boolean('is_active')->default(true); $table->json('meta')->nullable(); $table->timestamps(); $table->index(['payment_provider_identity_id', 'is_active']); $table->foreign('payment_provider_identity_id', 'payment_methods_provider_identity_foreign') ->references('id') ->on(config('shop.tables.payment_provider_identities', 'payment_provider_identities')) ->onDelete('cascade'); }); } // ProductActionRuns table if (!Schema::hasTable(config('shop.tables.product_action_runs', 'product_action_runs'))) { Schema::create(config('shop.tables.product_action_runs', 'product_action_runs'), function (Blueprint $table) { $table->id(); $table->morphs('action'); $table->uuid('product_purchase_id')->nullable(); $table->boolean('success')->default(false); $table->timestamps(); $table->foreign('product_purchase_id')->references('id')->on(config('shop.tables.product_purchases', 'product_purchases'))->onDelete('set null'); }); } } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists(config('shop.tables.payment_methods', 'payment_methods')); Schema::dropIfExists(config('shop.tables.payment_provider_identities', 'payment_provider_identities')); Schema::dropIfExists(config('shop.tables.cart_discounts', 'cart_discounts')); Schema::dropIfExists(config('shop.tables.cart_items', 'cart_items')); Schema::dropIfExists(config('shop.tables.carts', 'carts')); Schema::dropIfExists(config('shop.tables.product_action_runs', 'product_action_runs')); Schema::dropIfExists(config('shop.tables.product_purchases', 'product_purchases')); Schema::dropIfExists(config('shop.tables.product_actions', 'product_actions')); Schema::dropIfExists(config('shop.tables.product_category_product', 'product_category_product')); Schema::dropIfExists(config('shop.tables.product_relations', 'product_relations')); Schema::dropIfExists(config('shop.tables.product_stock_logs', 'product_stock_logs')); Schema::dropIfExists(config('shop.tables.product_stocks', 'product_stocks')); Schema::dropIfExists(config('shop.tables.product_attributes', 'product_attributes')); Schema::dropIfExists(config('shop.tables.product_prices', 'product_prices')); Schema::dropIfExists(config('shop.tables.product_category_product', 'product_category_product')); Schema::dropIfExists(config('shop.tables.product_categories', 'product_categories')); Schema::dropIfExists(config('shop.tables.products', 'products')); } };