From 1d56d27350eb337957a8c5ff6fce8618bb49ac7e Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 14 Jul 2026 16:02:26 -0300 Subject: [PATCH] feat: implement Bundle and BundleItem models, migrations, and polymorphic relationships for cart and purchase items --- app/Domains/Bundle/Models/Bundle.php | 104 ++++++++++++++++++ app/Domains/Bundle/Models/BundleItem.php | 46 ++++++++ app/Domains/Cart/Models/CartItem.php | 12 +- app/Domains/Purchase/Models/PurchaseItem.php | 12 +- ...2026_07_14_184744_create_bundles_table.php | 33 ++++++ ...07_14_184754_create_bundle_items_table.php | 30 +++++ ..._14_184802_make_cart_items_polymorphic.php | 48 ++++++++ ...184812_make_purchase_items_polymorphic.php | 48 ++++++++ 8 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 app/Domains/Bundle/Models/Bundle.php create mode 100644 app/Domains/Bundle/Models/BundleItem.php create mode 100644 database/migrations/2026_07_14_184744_create_bundles_table.php create mode 100644 database/migrations/2026_07_14_184754_create_bundle_items_table.php create mode 100644 database/migrations/2026_07_14_184802_make_cart_items_polymorphic.php create mode 100644 database/migrations/2026_07_14_184812_make_purchase_items_polymorphic.php diff --git a/app/Domains/Bundle/Models/Bundle.php b/app/Domains/Bundle/Models/Bundle.php new file mode 100644 index 0000000..e1b664a --- /dev/null +++ b/app/Domains/Bundle/Models/Bundle.php @@ -0,0 +1,104 @@ + 'decimal:2', + ]; + } + + /** + * @return BelongsTo + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo'); + } + + /** + * @return HasMany + */ + public function items(): HasMany + { + return $this->hasMany(BundleItem::class, 'bundle_id'); + } + + public function incrementReservedStock(int $amount): void + { + if ($amount < 0) { + throw new \InvalidArgumentException('El monto a incrementar debe ser positivo.'); + } + + foreach ($this->items as $item) { + $item->variant->incrementReservedStock($amount * $item->cantidad); + } + } + + public function decrementReservedStock(int $amount): void + { + if ($amount < 0) { + throw new \InvalidArgumentException('El monto a decrementar debe ser positivo.'); + } + + foreach ($this->items as $item) { + $item->variant->decrementReservedStock($amount * $item->cantidad); + } + } + + public function confirmReservedStock(int $amount): void + { + if ($amount < 0) { + throw new \InvalidArgumentException('El monto a confirmar debe ser positivo.'); + } + + foreach ($this->items as $item) { + $item->variant->confirmReservedStock($amount * $item->cantidad); + } + } + + protected function stockTecnico(): Attribute + { + return Attribute::get(function () { + if ($this->items->isEmpty()) { + return 0; + } + + $minStock = PHP_INT_MAX; + foreach ($this->items as $item) { + // Ensure variant is loaded + if ($item->variant) { + $itemStock = floor($item->variant->stock_tecnico / $item->cantidad); + if ($itemStock < $minStock) { + $minStock = $itemStock; + } + } + } + + return $minStock === PHP_INT_MAX ? 0 : (int) $minStock; + }); + } +} diff --git a/app/Domains/Bundle/Models/BundleItem.php b/app/Domains/Bundle/Models/BundleItem.php new file mode 100644 index 0000000..61f011b --- /dev/null +++ b/app/Domains/Bundle/Models/BundleItem.php @@ -0,0 +1,46 @@ + 'integer', + 'producto_variante_id' => 'integer', + 'cantidad' => 'integer', + ]; + } + + /** + * @return BelongsTo + */ + public function bundle(): BelongsTo + { + return $this->belongsTo(Bundle::class, 'bundle_id'); + } + + /** + * @return BelongsTo + */ + public function variant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class, 'producto_variante_id'); + } +} diff --git a/app/Domains/Cart/Models/CartItem.php b/app/Domains/Cart/Models/CartItem.php index 1062182..c60a90f 100644 --- a/app/Domains/Cart/Models/CartItem.php +++ b/app/Domains/Cart/Models/CartItem.php @@ -10,7 +10,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; #[Fillable([ 'cart_id', - 'producto_variante_id', + 'buyable_id', + 'buyable_type', 'cantidad', ])] class CartItem extends Model @@ -23,7 +24,8 @@ class CartItem extends Model { return [ 'cart_id' => 'integer', - 'producto_variante_id' => 'integer', + 'buyable_id' => 'integer', + 'buyable_type' => 'string', 'cantidad' => 'integer', ]; } @@ -37,10 +39,10 @@ class CartItem extends Model } /** - * @return BelongsTo + * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ - public function variant(): BelongsTo + public function buyable() { - return $this->belongsTo(ProductVariant::class, 'producto_variante_id'); + return $this->morphTo(); } } diff --git a/app/Domains/Purchase/Models/PurchaseItem.php b/app/Domains/Purchase/Models/PurchaseItem.php index c06f665..03e3ddb 100644 --- a/app/Domains/Purchase/Models/PurchaseItem.php +++ b/app/Domains/Purchase/Models/PurchaseItem.php @@ -10,7 +10,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; #[Fillable([ 'compra_id', - 'producto_variante_id', + 'buyable_id', + 'buyable_type', 'cantidad', 'precio_unitario', 'discount_total', @@ -27,7 +28,8 @@ class PurchaseItem extends Model { return [ 'compra_id' => 'integer', - 'producto_variante_id' => 'integer', + 'buyable_id' => 'integer', + 'buyable_type' => 'string', 'cantidad' => 'integer', 'precio_unitario' => 'decimal:2', 'discount_total' => 'decimal:2', @@ -45,10 +47,10 @@ class PurchaseItem extends Model } /** - * @return BelongsTo + * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ - public function variant(): BelongsTo + public function buyable() { - return $this->belongsTo(ProductVariant::class, 'producto_variante_id'); + return $this->morphTo(); } } diff --git a/database/migrations/2026_07_14_184744_create_bundles_table.php b/database/migrations/2026_07_14_184744_create_bundles_table.php new file mode 100644 index 0000000..965ab18 --- /dev/null +++ b/database/migrations/2026_07_14_184744_create_bundles_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('tenant_codigo', 10); + $table->string('nombre'); + $table->text('descripcion')->nullable(); + $table->decimal('precio', 10, 2); + $table->timestamps(); + + $table->foreign('tenant_codigo')->references('codigo')->on('tenants')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bundles'); + } +}; diff --git a/database/migrations/2026_07_14_184754_create_bundle_items_table.php b/database/migrations/2026_07_14_184754_create_bundle_items_table.php new file mode 100644 index 0000000..87fddff --- /dev/null +++ b/database/migrations/2026_07_14_184754_create_bundle_items_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('bundle_id')->constrained('bundles')->onDelete('cascade'); + $table->foreignId('producto_variante_id')->constrained('productos_variantes')->onDelete('cascade'); + $table->integer('cantidad'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bundle_items'); + } +}; diff --git a/database/migrations/2026_07_14_184802_make_cart_items_polymorphic.php b/database/migrations/2026_07_14_184802_make_cart_items_polymorphic.php new file mode 100644 index 0000000..fe1a0d1 --- /dev/null +++ b/database/migrations/2026_07_14_184802_make_cart_items_polymorphic.php @@ -0,0 +1,48 @@ +string('buyable_type')->nullable(); + $table->unsignedBigInteger('buyable_id')->nullable(); + }); + + // Copy existing data + \Illuminate\Support\Facades\DB::table('carrito_items')->update([ + 'buyable_type' => 'App\Domains\Catalog\Models\ProductVariant', + 'buyable_id' => \Illuminate\Support\Facades\DB::raw('producto_variante_id'), + ]); + + Schema::table('carrito_items', function (Blueprint $table) { + $table->dropForeign(['producto_variante_id']); + $table->dropColumn('producto_variante_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('carrito_items', function (Blueprint $table) { + $table->foreignId('producto_variante_id')->nullable()->constrained('productos_variantes')->onDelete('cascade'); + }); + + \Illuminate\Support\Facades\DB::table('carrito_items')->update([ + 'producto_variante_id' => \Illuminate\Support\Facades\DB::raw('buyable_id'), + ]); + + Schema::table('carrito_items', function (Blueprint $table) { + $table->dropColumn(['buyable_type', 'buyable_id']); + }); + } +}; diff --git a/database/migrations/2026_07_14_184812_make_purchase_items_polymorphic.php b/database/migrations/2026_07_14_184812_make_purchase_items_polymorphic.php new file mode 100644 index 0000000..86ae979 --- /dev/null +++ b/database/migrations/2026_07_14_184812_make_purchase_items_polymorphic.php @@ -0,0 +1,48 @@ +string('buyable_type')->nullable(); + $table->unsignedBigInteger('buyable_id')->nullable(); + }); + + // Copy existing data + \Illuminate\Support\Facades\DB::table('compra_items')->update([ + 'buyable_type' => 'App\Domains\Catalog\Models\ProductVariant', + 'buyable_id' => \Illuminate\Support\Facades\DB::raw('producto_variante_id'), + ]); + + Schema::table('compra_items', function (Blueprint $table) { + $table->dropForeign(['producto_variante_id']); + $table->dropColumn('producto_variante_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('compra_items', function (Blueprint $table) { + $table->foreignId('producto_variante_id')->nullable()->constrained('productos_variantes')->onDelete('cascade'); + }); + + \Illuminate\Support\Facades\DB::table('compra_items')->update([ + 'producto_variante_id' => \Illuminate\Support\Facades\DB::raw('buyable_id'), + ]); + + Schema::table('compra_items', function (Blueprint $table) { + $table->dropColumn(['buyable_type', 'buyable_id']); + }); + } +};