feat: implement polymorphic relationships for group items to support bundles and product variants

This commit is contained in:
2026-07-17 14:42:44 -03:00
parent 615eacea91
commit cb0fb2899c
10 changed files with 298 additions and 19 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Domains\Bundle\Models;
use App\Domains\Catalog\Models\GroupItem;
use App\Domains\Shared\Contracts\Buyable;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
#[Fillable([
'tenant_codigo',
@@ -48,6 +50,14 @@ class Bundle extends Model implements Buyable
return $this->hasMany(BundleItem::class, 'bundle_id');
}
/**
* @return MorphMany<GroupItem, $this>
*/
public function groupItems(): MorphMany
{
return $this->morphMany(GroupItem::class, 'groupable');
}
public function getPrice(): float
{
return (float) $this->precio;

View File

@@ -2,8 +2,11 @@
namespace App\Domains\Catalog\Controllers;
use App\Domains\Catalog\Resources\CatalogFeaturedGroupResource;
use App\Domains\Bundle\Models\Bundle;
use App\Domains\Catalog\Models\FeaturedGroup;
use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Catalog\Resources\CatalogFeaturedGroupResource;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller;
@@ -12,9 +15,15 @@ class CatalogController extends Controller
public function index(string $tenant): JsonResponse
{
$featuredGroups = FeaturedGroup::where('tenant_codigo', $tenant)
->with(['groupItems' => function ($query) {
$query->orderBy('order');
}, 'groupItems.variant.product', 'groupItems.variant.attachments', 'groupItems.variant.product.attachments'])
->with([
'groupItems' => fn ($query) => $query->orderBy('order'),
'groupItems.groupable' => function (MorphTo $morphTo): void {
$morphTo->morphWith([
ProductVariant::class => ['product', 'attachments', 'product.attachments'],
Bundle::class => ['items.variant.product', 'items.variant.attachments', 'items.variant.product.attachments'],
]);
},
])
->orderBy('group_order')
->get();

View File

@@ -2,34 +2,53 @@
namespace App\Domains\Catalog\Controllers;
use App\Domains\Bundle\Models\Bundle;
use App\Domains\Catalog\Models\FeaturedGroup;
use App\Domains\Catalog\Models\GroupItem;
use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Catalog\Requests\StoreFeaturedVariantRequest;
use App\Domains\Catalog\Requests\UpdateFeaturedVariantRequest;
use App\Domains\Catalog\Resources\FeaturedVariantResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Routing\Controller;
use Illuminate\Http\JsonResponse;
class FeaturedVariantController extends Controller
{
public function index(string $tenant, FeaturedGroup $featuredGroup): AnonymousResourceCollection
{
abort_if($featuredGroup->tenant_codigo !== $tenant, 404);
return FeaturedVariantResource::collection($featuredGroup->groupItems);
}
public function store(StoreFeaturedVariantRequest $request, string $tenant, FeaturedGroup $featuredGroup): FeaturedVariantResource
{
abort_if($featuredGroup->tenant_codigo !== $tenant, 404);
$variant = $featuredGroup->groupItems()->create($request->validated());
return new FeaturedVariantResource($variant);
$groupableType = $request->mappedGroupableType();
$groupable = $groupableType::query()->findOrFail($request->integer('groupable_id'));
abort_if(
($groupable instanceof ProductVariant && $groupable->product()->where('tenant_codigo', $tenant)->doesntExist())
|| ($groupable instanceof Bundle && $groupable->tenant_codigo !== $tenant),
404
);
$groupItem = $featuredGroup->groupItems()->create([
'groupable_type' => $groupableType,
'groupable_id' => $groupable->getKey(),
'order' => $request->validated('order'),
]);
return new FeaturedVariantResource($groupItem);
}
public function update(UpdateFeaturedVariantRequest $request, string $tenant, FeaturedGroup $featuredGroup, GroupItem $featuredVariant): FeaturedVariantResource
{
abort_if($featuredGroup->tenant_codigo !== $tenant || $featuredVariant->featured_group_id !== $featuredGroup->id, 404);
$featuredVariant->update($request->validated());
return new FeaturedVariantResource($featuredVariant);
}
@@ -37,6 +56,7 @@ class FeaturedVariantController extends Controller
{
abort_if($featuredGroup->tenant_codigo !== $tenant || $featuredVariant->featured_group_id !== $featuredGroup->id, 404);
$featuredVariant->delete();
return response()->json(null, 204);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Domains\Catalog\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class GroupItem extends Model
{
@@ -11,7 +12,8 @@ class GroupItem extends Model
protected $fillable = [
'featured_group_id',
'product_variant_id',
'groupable_type',
'groupable_id',
'order',
];
@@ -20,8 +22,8 @@ class GroupItem extends Model
return $this->belongsTo(FeaturedGroup::class);
}
public function variant(): BelongsTo
public function groupable(): MorphTo
{
return $this->belongsTo(ProductVariant::class, 'product_variant_id');
return $this->morphTo();
}
}

View File

@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
#[Fillable([
'producto_id',
@@ -217,4 +218,12 @@ class ProductVariant extends Model implements Buyable
'attachment_id'
)->withTimestamps();
}
/**
* @return MorphMany<GroupItem, $this>
*/
public function groupItems(): MorphMany
{
return $this->morphMany(GroupItem::class, 'groupable');
}
}

View File

@@ -2,7 +2,10 @@
namespace App\Domains\Catalog\Requests;
use App\Domains\Bundle\Models\Bundle;
use App\Domains\Catalog\Models\ProductVariant;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreFeaturedVariantRequest extends FormRequest
{
@@ -14,8 +17,25 @@ class StoreFeaturedVariantRequest extends FormRequest
public function rules(): array
{
return [
'product_variant_id' => ['required', 'integer'],
'groupable_type' => ['required', 'string', Rule::in(['variant', 'bundle'])],
'groupable_id' => [
'required',
'integer',
Rule::exists(match ($this->input('groupable_type')) {
'bundle' => 'bundles',
default => 'productos_variantes',
}, 'id'),
],
'order' => ['nullable', 'integer'],
];
}
public function mappedGroupableType(): string
{
return match ($this->input('groupable_type')) {
'variant' => ProductVariant::class,
'bundle' => Bundle::class,
default => throw new \InvalidArgumentException('Invalid groupable type'),
};
}
}

View File

@@ -2,9 +2,11 @@
namespace App\Domains\Catalog\Resources;
use App\Domains\Bundle\Models\Bundle;
use App\Domains\Catalog\Models\GroupItem;
use App\Domains\Catalog\Models\ProductVariant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Domains\Catalog\Models\GroupItem;
class CatalogFeaturedGroupResource extends JsonResource
{
@@ -17,25 +19,49 @@ class CatalogFeaturedGroupResource extends JsonResource
'group_order' => $this->group_order,
'items' => $this->whenLoaded('groupItems', function () use ($request) {
return $this->groupItems->map(function (GroupItem $groupItem) use ($request) {
$variant = $groupItem->variant;
$variantResource = (new ProductVariantResource($variant))->toArray($request);
$groupable = $groupItem->groupable;
if ($groupable instanceof Bundle) {
return [
'id' => $groupable->id,
'groupable_type' => 'bundle',
'groupable_id' => $groupable->id,
'nombre' => $groupable->nombre,
'descripcion' => $groupable->descripcion,
'precio' => $groupable->precio,
'cantidad_maxima' => $groupable->stock_tecnico,
'items' => $groupable->items->map(fn ($item) => [
'cantidad' => $item->cantidad,
'variant' => (new ProductVariantResource($item->variant))->toArray($request),
])->values(),
];
}
if (! $groupable instanceof ProductVariant) {
return null;
}
$variantResource = (new ProductVariantResource($groupable))->toArray($request);
$variantResource['groupable_type'] = 'variant';
$variantResource['groupable_id'] = $groupable->id;
if ($this->product_layout === 'row' || $this->product_layout === 'column_with_cart' || $this->product_layout === 'vertical_with_cart') {
// Devuelve el Product Variant Resource completo como pidio el usuario
// "Que todo devuelva el product variant resource"
return $variantResource;
}
// Para column_with_image o vertical_with_image
if ($this->product_layout === 'column_with_image' || $this->product_layout === 'vertical_with_image') {
if (isset($variantResource['images']) && count($variantResource['images']) > 0) {
$variantResource['images'] = [$variantResource['images'][0]];
}
return $variantResource;
}
return $variantResource;
});
})->filter()->values();
}),
];
}

View File

@@ -2,6 +2,8 @@
namespace App\Domains\Catalog\Resources;
use App\Domains\Bundle\Models\Bundle;
use App\Domains\Catalog\Models\ProductVariant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -12,7 +14,12 @@ class FeaturedVariantResource extends JsonResource
return [
'id' => $this->id,
'featured_group_id' => $this->featured_group_id,
'product_variant_id' => $this->product_variant_id,
'groupable_type' => match ($this->groupable_type) {
ProductVariant::class => 'variant',
Bundle::class => 'bundle',
default => 'unknown',
},
'groupable_id' => $this->groupable_id,
'order' => $this->order,
];
}