feat: implement product variant CRUD operations and tenant configuration seeding

This commit is contained in:
2026-06-29 11:59:07 -03:00
parent 9b7d728117
commit 0206ee2985
10 changed files with 227 additions and 96 deletions

View File

@@ -21,7 +21,7 @@ class ProductVariantController extends Controller
{
$query = ProductVariant::query()
->whereHas('product', fn ($query) => $query->where('tenant_codigo', $tenant->codigo))
->with(['product', 'definitions.attribute'])
->with(['product', 'definitions.attribute', 'attachments'])
->latest();
return ProductVariantResource::collection($query->paginateFromRequest())->response();
@@ -41,7 +41,7 @@ class ProductVariantController extends Controller
{
$productVariant = $this->resolveScopedVariant($tenant, $productVariant);
return ProductVariantResource::make($productVariant->load(['product', 'definitions.attribute']));
return ProductVariantResource::make($productVariant->load(['product', 'definitions.attribute', 'attachments']));
}
public function update(UpdateProductVariantRequest $request, Tenant $tenant, ProductVariant $productVariant, ProductService $productService): ProductVariantResource

View File

@@ -2,6 +2,7 @@
namespace App\Domains\Catalog\Requests;
use App\Domains\Shared\Rules\ImageOrBase64Rule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -59,6 +60,8 @@ class StoreProductVariantRequest extends FormRequest
},
],
'definitions.*.value' => ['nullable', 'string'],
'images' => ['sometimes', 'nullable', 'array'],
'images.*' => ['required', new ImageOrBase64Rule()],
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Domains\Catalog\Requests;
use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Shared\Rules\ImageOrBase64Rule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -63,6 +64,8 @@ class UpdateProductVariantRequest extends FormRequest
},
],
'definitions.*.value' => ['nullable', 'string'],
'images' => ['sometimes', 'nullable', 'array'],
'images.*' => ['required', new ImageOrBase64Rule()],
];
}
}

View File

@@ -25,6 +25,9 @@ class ProductVariantResource extends JsonResource
'precio' => $this->precio,
'product' => ProductResource::make($this->whenLoaded('product')),
'definitions' => ProductVariantDefinitionResource::collection($this->whenLoaded('definitions')),
'images' => $this->whenLoaded('attachments', fn () =>
$this->attachments->map(fn ($attachment) => $attachment->getTemporaryUrl(1440))->values()
),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];

View File

@@ -2,14 +2,17 @@
namespace App\Domains\Catalog\Services;
use App\Domains\Attachable\Services\AttachmentService;
use App\Domains\Catalog\Models\Attribute;
use App\Domains\Catalog\Models\Product;
use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
class ProductService
{
public function __construct(protected AttachmentService $attachmentService) {}
/**
* Create a product.
*
@@ -80,9 +83,16 @@ class ProductService
public function createVariant(Product $product, array $data): ProductVariant
{
return DB::transaction(function () use ($product, $data) {
$images = $data['images'] ?? [];
unset($data['images']);
$variant = $product->createVariant($data);
return $variant->load(['product', 'definitions.attribute']);
if (! empty($images)) {
$this->syncImages($variant, $images);
}
return $variant->load(['product', 'definitions.attribute', 'attachments']);
});
}
@@ -94,11 +104,19 @@ class ProductService
public function updateVariant(ProductVariant $variant, array $data): ProductVariant
{
return DB::transaction(function () use ($variant, $data) {
$hasImages = array_key_exists('images', $data);
$images = $data['images'] ?? [];
unset($data['images']);
/** @var Product $product */
$product = $variant->product;
$updatedVariant = $product->updateVariant($variant, $data);
return $updatedVariant->load(['product', 'definitions.attribute']);
if ($hasImages) {
$this->syncImages($updatedVariant, $images);
}
return $updatedVariant->load(['product', 'definitions.attribute', 'attachments']);
});
}
@@ -138,6 +156,29 @@ class ProductService
});
}
/**
* Upload a list of image files/base64 strings and sync them to a variant.
*
* When called on update, the existing attachments are detached first so the
* final set always matches exactly what was sent in the request.
*
* @param array<int, UploadedFile|string> $images
*/
protected function syncImages(ProductVariant $variant, array $images): void
{
// Detach any existing attachments before replacing them
$variant->attachments()->detach();
$attachmentIds = [];
foreach ($images as $image) {
$attachment = $this->attachmentService->store($image, 'variants');
$attachmentIds[] = $attachment->id;
}
$variant->attachments()->sync($attachmentIds);
}
/**
* Delete an attribute.
*/