feat: implement polymorphic relationships for group items to support bundles and product variants
This commit is contained in:
parent
615eacea91
commit
cb0fb2899c
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
use App\Domains\Catalog\Models\ProductVariant;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('group_items', function (Blueprint $table) {
|
||||
$table->nullableMorphs('groupable');
|
||||
});
|
||||
|
||||
DB::table('group_items')->update([
|
||||
'groupable_type' => ProductVariant::class,
|
||||
'groupable_id' => DB::raw('product_variant_id'),
|
||||
]);
|
||||
|
||||
$productVariantForeignKey = collect(Schema::getForeignKeys('group_items'))
|
||||
->first(fn (array $foreignKey): bool => $foreignKey['columns'] === ['product_variant_id']);
|
||||
|
||||
Schema::table('group_items', function (Blueprint $table) use ($productVariantForeignKey) {
|
||||
if ($productVariantForeignKey !== null) {
|
||||
$table->dropForeign($productVariantForeignKey['name']);
|
||||
}
|
||||
|
||||
$table->dropColumn('product_variant_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::table('group_items')->where('groupable_type', '!=', ProductVariant::class)->exists()) {
|
||||
throw new RuntimeException('Cannot roll back groupable group items while bundle items exist.');
|
||||
}
|
||||
|
||||
Schema::table('group_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('product_variant_id')->nullable();
|
||||
});
|
||||
|
||||
DB::table('group_items')->update([
|
||||
'product_variant_id' => DB::raw('groupable_id'),
|
||||
]);
|
||||
|
||||
Schema::table('group_items', function (Blueprint $table) {
|
||||
$table->dropMorphs('groupable');
|
||||
$table->foreign('product_variant_id')
|
||||
->references('id')
|
||||
->on('productos_variantes')
|
||||
->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Catalog;
|
||||
|
||||
use App\Domains\Attachable\Enums\AttachmentType;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Bundle\Models\Bundle;
|
||||
use App\Domains\Catalog\Models\Category;
|
||||
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||
use App\Domains\Catalog\Models\Product;
|
||||
use App\Domains\Catalog\Models\ProductVariant;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class GroupItemTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private FeaturedGroup $featuredGroup;
|
||||
|
||||
private ProductVariant $variant;
|
||||
|
||||
private Bundle $bundle;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$headerAttachment = $this->createAttachment('header.png');
|
||||
$footerAttachment = $this->createAttachment('footer.png');
|
||||
|
||||
$tenant = Tenant::query()->create([
|
||||
'codigo' => 'group-test',
|
||||
'nombre' => 'Group Test',
|
||||
'dominio' => 'group.test',
|
||||
'primary_color' => '#111111',
|
||||
'secondary_color' => '#222222',
|
||||
'danger_color' => '#333333',
|
||||
'success_color' => '#28a745',
|
||||
'header_bg_color' => '#444444',
|
||||
'footer_bg_color' => '#555555',
|
||||
'header_logo_id' => $headerAttachment->id,
|
||||
'footer_logo_id' => $footerAttachment->id,
|
||||
]);
|
||||
|
||||
$category = Category::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'nombre' => 'Group items',
|
||||
]);
|
||||
|
||||
$product = Product::query()->create([
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'categoria_id' => $category->id,
|
||||
'slug' => 'group-item-product',
|
||||
'nombre' => 'Group Item Product',
|
||||
'precio' => 100,
|
||||
]);
|
||||
|
||||
$this->variant = ProductVariant::query()->create([
|
||||
'producto_id' => $product->id,
|
||||
'stock' => 10,
|
||||
]);
|
||||
|
||||
$this->bundle = Bundle::query()->create([
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'nombre' => 'Group Item Bundle',
|
||||
'precio' => 150,
|
||||
]);
|
||||
|
||||
$this->featuredGroup = FeaturedGroup::query()->create([
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'group_name' => 'Featured',
|
||||
'product_layout' => 'row',
|
||||
'group_order' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_a_group_item_can_reference_a_product_variant(): void
|
||||
{
|
||||
$groupItem = $this->variant->groupItems()->create([
|
||||
'featured_group_id' => $this->featuredGroup->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(ProductVariant::class, $groupItem->groupable);
|
||||
$this->assertTrue($groupItem->groupable->is($this->variant));
|
||||
$this->assertTrue($this->variant->groupItems->first()->is($groupItem));
|
||||
}
|
||||
|
||||
public function test_a_group_item_can_reference_a_bundle(): void
|
||||
{
|
||||
$groupItem = $this->bundle->groupItems()->create([
|
||||
'featured_group_id' => $this->featuredGroup->id,
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(Bundle::class, $groupItem->groupable);
|
||||
$this->assertTrue($groupItem->groupable->is($this->bundle));
|
||||
$this->assertTrue($this->bundle->groupItems->first()->is($groupItem));
|
||||
}
|
||||
|
||||
private function createAttachment(string $filename): Attachment
|
||||
{
|
||||
return Attachment::query()->create([
|
||||
'key' => (string) Str::uuid(),
|
||||
'path' => 'tests/'.$filename,
|
||||
'filename' => $filename,
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue