- Create TicketValiditySchemaTest to verify database schema for ticket validity. - Update CatalogModelsTest to include tests for event date attributes and selection options. - Introduce EventDateTextFormatterTest for formatting event dates in Spanish. - Refactor EventModelsTest to include validity time relationships. - Add SaleDetailResourceTest to ensure correct serialization of purchase items. - Enhance TicketTest with validity time checks and status management. - Implement ValidityTimeResourceTest to validate resource output for different validity types. - Add ValidityTimeTest to verify casting and validity checks for validity time types.
784 lines
28 KiB
PHP
784 lines
28 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Catalog\Services;
|
|
|
|
use App\Domains\Attachable\Models\Attachment;
|
|
use App\Domains\Attachable\Services\AttachmentService;
|
|
use App\Domains\Catalog\Enums\CatalogItemType;
|
|
use App\Domains\Catalog\Models\Attribute;
|
|
use App\Domains\Catalog\Models\CatalogItem;
|
|
use App\Domains\Catalog\Models\Category;
|
|
use App\Domains\Catalog\Models\Inventory;
|
|
use App\Domains\Catalog\Models\ItemAttribute;
|
|
use App\Domains\Catalog\Models\Variant;
|
|
use App\Domains\Event\Models\EventDate;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
|
|
class CatalogService
|
|
{
|
|
public function __construct(protected AttachmentService $attachmentService) {}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
public function create(array $data): CatalogItem
|
|
{
|
|
return DB::transaction(function () use ($data): CatalogItem {
|
|
$type = CatalogItemType::from(
|
|
$data['type'] ?? CatalogItemType::Standard->value,
|
|
);
|
|
$variants = $data['variants'] ?? [];
|
|
$images = $data['images'] ?? [];
|
|
$attributeCodes = $data['attribute_codes'] ?? [];
|
|
$multiSelectAttributeCodes = $data['multi_select_attribute_codes'] ?? [];
|
|
$components = $data['components'] ?? [];
|
|
$hasDirectStock = array_key_exists('real_stock', $data);
|
|
$realStock = (int) ($data['real_stock'] ?? 0);
|
|
|
|
$hasEventDateVariants = $variants !== [] && collect($variants)->every(
|
|
fn (array $variant): bool => ! empty($variant['event_date_id'])
|
|
|| ! empty($variant['event_date_ids'])
|
|
);
|
|
|
|
if ($hasEventDateVariants && ! in_array('event_date', $attributeCodes, true)) {
|
|
throw ValidationException::withMessages([
|
|
'attribute_codes' => [
|
|
__('api.catalog.event_date_attribute_required'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$hasVariants = $attributeCodes !== [] || $hasEventDateVariants;
|
|
|
|
if (array_diff($multiSelectAttributeCodes, $attributeCodes) !== []) {
|
|
throw ValidationException::withMessages([
|
|
'multi_select_attribute_codes' => [
|
|
__('api.catalog.multi_select_attribute_not_on_item'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$this->validateUniqueVariantCombinations($variants, $attributeCodes);
|
|
|
|
if ($type === CatalogItemType::Bundle) {
|
|
$this->validateBundleData($data, $components);
|
|
} else {
|
|
if (array_key_exists('components', $data)) {
|
|
throw ValidationException::withMessages([
|
|
'components' => [__('api.catalog.standard_with_components')],
|
|
]);
|
|
}
|
|
|
|
$this->validateInventoryStrategy(
|
|
$data,
|
|
$variants,
|
|
$hasVariants,
|
|
$hasDirectStock,
|
|
);
|
|
}
|
|
|
|
unset(
|
|
$data['variants'],
|
|
$data['images'],
|
|
$data['attribute_codes'],
|
|
$data['multi_select_attribute_codes'],
|
|
$data['components'],
|
|
$data['real_stock'],
|
|
$data['reserved_stock'],
|
|
$data['sold_units'],
|
|
$data['inventory_id'],
|
|
);
|
|
|
|
$data['type'] = $type;
|
|
|
|
if ($type === CatalogItemType::Bundle) {
|
|
$data['inventory_id'] = null;
|
|
$data['inventory_policy'] = null;
|
|
$data['has_tickets'] = false;
|
|
} elseif ($hasVariants) {
|
|
$data['inventory_id'] = null;
|
|
} else {
|
|
$data['inventory_id'] = $this->createInventory($realStock)->id;
|
|
}
|
|
|
|
$catalogItem = CatalogItem::query()->create($data);
|
|
$itemAttributes = $type === CatalogItemType::Standard
|
|
? $this->createItemAttributes($catalogItem, $attributeCodes, $multiSelectAttributeCodes)
|
|
: [];
|
|
|
|
if ($type === CatalogItemType::Bundle) {
|
|
$this->createBundleComponents($catalogItem, $components);
|
|
}
|
|
|
|
$createdVariants = [];
|
|
foreach ($variants as $index => $variantData) {
|
|
$createdVariants[] = [
|
|
'variant' => $this->createVariant(
|
|
$catalogItem,
|
|
$variantData,
|
|
$itemAttributes,
|
|
$index,
|
|
),
|
|
'images' => $variantData['images'] ?? [],
|
|
'index' => $index,
|
|
];
|
|
}
|
|
|
|
$this->attachImages($catalogItem, $images, 'images');
|
|
|
|
foreach ($createdVariants as $createdVariant) {
|
|
$this->attachImages(
|
|
$createdVariant['variant'],
|
|
$createdVariant['images'],
|
|
"variants.{$createdVariant['index']}.images",
|
|
);
|
|
}
|
|
|
|
return $catalogItem->load([
|
|
'attachments',
|
|
'inventory',
|
|
'category',
|
|
'brand',
|
|
'validityTime',
|
|
'itemAttributes.attribute',
|
|
'variants.inventory',
|
|
'variants.attachments',
|
|
'variants.eventDate',
|
|
'variants.eventDates',
|
|
'variants.definitions.itemAttribute.attribute.options',
|
|
'bundleComponents.catalogItem',
|
|
'bundleComponents.variant.catalogItem',
|
|
]);
|
|
});
|
|
}
|
|
|
|
public function getDetail(CatalogItem $catalogItem, ?int $variantId = null): CatalogItem
|
|
{
|
|
$catalogItem->load([
|
|
'attachments',
|
|
'inventory',
|
|
'category',
|
|
'brand',
|
|
'validityTime',
|
|
'itemAttributes.attribute.options.validityTime',
|
|
'itemAttributes.attribute.eventDates.validityTime',
|
|
'variants' => fn ($query) => $query->orderBy('id'),
|
|
'variants.inventory',
|
|
'variants.attachments',
|
|
'variants.eventDate',
|
|
'variants.eventDates',
|
|
'variants.definitions' => fn ($query) => $query->orderBy('id'),
|
|
'variants.definitions.itemAttribute.attribute.options',
|
|
'bundleComponents.catalogItem.inventory',
|
|
'bundleComponents.variant.inventory',
|
|
'bundleComponents.variant.definitions.itemAttribute.attribute',
|
|
]);
|
|
|
|
$visibleVariants = $catalogItem->visibleVariants();
|
|
$selectedVariant = $variantId === null
|
|
? $visibleVariants->first()
|
|
: $visibleVariants->firstWhere('id', $variantId);
|
|
|
|
if ($variantId !== null && $selectedVariant === null) {
|
|
throw new NotFoundHttpException('Variant not found for catalog item.');
|
|
}
|
|
|
|
$catalogItem->setRelation('selectedVariant', $selectedVariant);
|
|
|
|
return $catalogItem;
|
|
}
|
|
|
|
/** @return LengthAwarePaginator<CatalogItem> */
|
|
public function search(
|
|
Tenant $tenant,
|
|
string $term,
|
|
int $perPage,
|
|
int $page,
|
|
): LengthAwarePaginator {
|
|
$normalizedTerm = mb_strtolower($term);
|
|
$containsPattern = "%{$normalizedTerm}%";
|
|
$startsWithPattern = "{$normalizedTerm}%";
|
|
|
|
$paginator = CatalogItem::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->whereVariantsAvailable()
|
|
->where(function (Builder $query) use ($containsPattern): void {
|
|
$query
|
|
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
|
|
->orWhereRaw('LOWER(descripcion) LIKE ?', [$containsPattern])
|
|
->orWhereHas(
|
|
'brand',
|
|
fn (Builder $brandQuery) => $brandQuery
|
|
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
|
|
)
|
|
->orWhereHas(
|
|
'category',
|
|
fn (Builder $categoryQuery) => $categoryQuery
|
|
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
|
|
);
|
|
})
|
|
->with([
|
|
'attachments',
|
|
'inventory',
|
|
'validityTime',
|
|
'itemAttributes.attribute',
|
|
'variants.inventory',
|
|
'variants.attachments',
|
|
'variants.eventDate',
|
|
'variants.eventDates',
|
|
'variants.definitions.itemAttribute.attribute.options',
|
|
'bundleComponents.catalogItem',
|
|
'bundleComponents.variant.catalogItem',
|
|
])
|
|
->orderByRaw(
|
|
'CASE WHEN LOWER(nombre) = ? THEN 0 WHEN LOWER(nombre) LIKE ? THEN 1 ELSE 2 END',
|
|
[$normalizedTerm, $startsWithPattern],
|
|
)
|
|
->orderBy('nombre')
|
|
->paginate(perPage: $perPage, pageName: 'page', page: $page);
|
|
|
|
return $paginator->withPath(route('catalog-items.index', [
|
|
'tenant' => $tenant->codigo,
|
|
]));
|
|
}
|
|
|
|
/** @return LengthAwarePaginator<CatalogItem> */
|
|
public function categoryItems(
|
|
Tenant $tenant,
|
|
Category $category,
|
|
int $perPage,
|
|
int $page,
|
|
): LengthAwarePaginator {
|
|
return CatalogItem::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->where('category_id', $category->id)
|
|
->whereVariantsAvailable()
|
|
->with([
|
|
'attachments',
|
|
'inventory',
|
|
'validityTime',
|
|
'itemAttributes.attribute',
|
|
'variants.inventory',
|
|
'variants.attachments',
|
|
'variants.eventDate',
|
|
'variants.eventDates',
|
|
'variants.definitions.itemAttribute.attribute.options',
|
|
'bundleComponents.catalogItem',
|
|
'bundleComponents.variant.catalogItem',
|
|
])
|
|
->orderBy('nombre')
|
|
->paginate(perPage: $perPage, pageName: 'page', page: $page);
|
|
}
|
|
|
|
public function delete(CatalogItem $catalogItem): void
|
|
{
|
|
DB::transaction(function () use ($catalogItem): void {
|
|
$catalogItem->load([
|
|
'attachments',
|
|
'variants.attachments',
|
|
]);
|
|
|
|
$attachments = $catalogItem->attachments
|
|
->merge($catalogItem->variants->flatMap->attachments)
|
|
->unique('id');
|
|
$inventoryIds = collect([$catalogItem->inventory_id])
|
|
->merge($catalogItem->variants->pluck('inventory_id'))
|
|
->filter()
|
|
->unique();
|
|
|
|
$catalogItem->attachments()->detach();
|
|
foreach ($catalogItem->variants as $variant) {
|
|
$variant->attachments()->detach();
|
|
}
|
|
|
|
$catalogItem->delete();
|
|
Inventory::query()->whereKey($inventoryIds)->delete();
|
|
|
|
foreach ($attachments as $attachment) {
|
|
if (! DB::table('catalog_items_attachments')->where('attachment_id', $attachment->id)->exists()) {
|
|
$this->attachmentService->delete($attachment);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
public function deleteVariant(Variant $variant): void
|
|
{
|
|
DB::transaction(function () use ($variant): void {
|
|
$variant = Variant::query()
|
|
->with('attachments')
|
|
->lockForUpdate()
|
|
->findOrFail($variant->getKey());
|
|
$catalogItem = CatalogItem::query()
|
|
->lockForUpdate()
|
|
->findOrFail($variant->catalog_item_id);
|
|
$attachments = $variant->attachments;
|
|
$inventoryId = $variant->inventory_id;
|
|
|
|
$variant->attachments()->detach();
|
|
$variant->delete();
|
|
Inventory::query()->whereKey($inventoryId)->delete();
|
|
|
|
$minimumPrice = $catalogItem->variants()->min('precio');
|
|
|
|
if ($minimumPrice === null) {
|
|
$this->delete($catalogItem);
|
|
} else {
|
|
$catalogItem->update(['precio' => $minimumPrice]);
|
|
}
|
|
|
|
foreach ($attachments as $attachment) {
|
|
if (! DB::table('catalog_items_attachments')->where('attachment_id', $attachment->id)->exists()) {
|
|
$this->attachmentService->delete($attachment);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private function createInventory(int $realStock): Inventory
|
|
{
|
|
return Inventory::query()->create([
|
|
'real_stock' => $realStock,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $components
|
|
*/
|
|
private function createBundleComponents(CatalogItem $bundle, array $components): void
|
|
{
|
|
$seen = [];
|
|
|
|
foreach (array_values($components) as $index => $componentData) {
|
|
$catalogItemId = (int) $componentData['catalog_item_id'];
|
|
$variantId = isset($componentData['variant_id'])
|
|
? (int) $componentData['variant_id']
|
|
: null;
|
|
$key = $catalogItemId.':'.($variantId ?? 'direct');
|
|
|
|
if (isset($seen[$key])) {
|
|
throw ValidationException::withMessages([
|
|
"components.{$index}" => [__('api.catalog.duplicate_component')],
|
|
]);
|
|
}
|
|
$seen[$key] = true;
|
|
|
|
$componentItem = CatalogItem::query()
|
|
->whereKey($catalogItemId)
|
|
->where('tenant_code', $bundle->tenant_code)
|
|
->first();
|
|
|
|
if ($componentItem === null) {
|
|
throw ValidationException::withMessages([
|
|
"components.{$index}.catalog_item_id" => [
|
|
__('api.catalog.component_wrong_tenant'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
if ($componentItem->is($bundle) || $componentItem->type !== CatalogItemType::Standard) {
|
|
throw ValidationException::withMessages([
|
|
"components.{$index}.catalog_item_id" => [
|
|
__('api.catalog.invalid_component'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$hasVariants = $componentItem->variants()->exists();
|
|
if ($hasVariants && $variantId === null) {
|
|
throw ValidationException::withMessages([
|
|
"components.{$index}.variant_id" => [
|
|
__('api.catalog.component_variant_required'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
if (! $hasVariants && $variantId !== null) {
|
|
throw ValidationException::withMessages([
|
|
"components.{$index}.variant_id" => [
|
|
__('api.catalog.component_variant_forbidden'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
if ($variantId !== null && ! $componentItem->variants()->whereKey($variantId)->exists()) {
|
|
throw ValidationException::withMessages([
|
|
"components.{$index}.variant_id" => [
|
|
__('api.catalog.component_variant_invalid'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$bundle->bundleComponents()->create([
|
|
'component_catalog_item_id' => $componentItem->id,
|
|
'component_variant_id' => $variantId,
|
|
'quantity' => (int) $componentData['quantity'],
|
|
]);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @param array<int, array<string, mixed>> $components
|
|
*/
|
|
private function validateBundleData(array $data, array $components): void
|
|
{
|
|
if ($components === []) {
|
|
throw ValidationException::withMessages([
|
|
'components' => [__('api.catalog.bundle_component_required')],
|
|
]);
|
|
}
|
|
|
|
foreach ([
|
|
'real_stock',
|
|
'inventory_policy',
|
|
'attribute_codes',
|
|
'variants',
|
|
'has_tickets',
|
|
'ticket_generation_policy',
|
|
'validity_time_id',
|
|
] as $field) {
|
|
if (array_key_exists($field, $data)) {
|
|
throw ValidationException::withMessages([
|
|
$field => [__('api.catalog.bundle_field_forbidden', ['field' => $field])],
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int, mixed> $images
|
|
*/
|
|
private function attachImages(
|
|
CatalogItem|Variant $owner,
|
|
array $images,
|
|
string $validationKey,
|
|
): void {
|
|
foreach (array_values($images) as $order => $image) {
|
|
$attachment = $this->resolveAttachment($image, "{$validationKey}.{$order}");
|
|
|
|
$owner->attachments()->attach($attachment->id, ['orden' => $order]);
|
|
}
|
|
}
|
|
|
|
private function resolveAttachment(mixed $image, string $validationKey): Attachment
|
|
{
|
|
if (is_string($image) && Str::isUuid($image)) {
|
|
$attachment = Attachment::query()->where('key', $image)->first();
|
|
|
|
if ($attachment === null) {
|
|
throw ValidationException::withMessages([
|
|
$validationKey => [__('api.catalog.attachment_not_found')],
|
|
]);
|
|
}
|
|
|
|
return $attachment;
|
|
}
|
|
|
|
return $this->attachmentService->store($image, 'catalog-items');
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $attributeCodes
|
|
* @param array<int, string> $multiSelectAttributeCodes
|
|
* @return array<string, ItemAttribute>
|
|
*/
|
|
private function createItemAttributes(
|
|
CatalogItem $catalogItem,
|
|
array $attributeCodes,
|
|
array $multiSelectAttributeCodes = [],
|
|
): array {
|
|
$itemAttributes = [];
|
|
$attributeCodes = array_values(array_unique($attributeCodes));
|
|
$attributes = Attribute::query()
|
|
->where('tenant_codigo', $catalogItem->tenant_code)
|
|
->whereIn('codigo', $attributeCodes)
|
|
->get()
|
|
->keyBy('codigo');
|
|
|
|
foreach ($attributeCodes as $attributeCode) {
|
|
$attribute = $attributes->get($attributeCode);
|
|
|
|
if ($attribute === null) {
|
|
throw ValidationException::withMessages([
|
|
'attribute_codes' => [
|
|
__('api.catalog.attribute_not_found', ['attribute' => $attributeCode]),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$itemAttribute = $catalogItem->itemAttributes()->create([
|
|
'attribute_id' => $attribute->id,
|
|
'allow_multi_select' => in_array($attributeCode, $multiSelectAttributeCodes, true),
|
|
]);
|
|
|
|
$itemAttributes[$attributeCode] = $itemAttribute;
|
|
}
|
|
|
|
return $itemAttributes;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @param array<string, ItemAttribute> $itemAttributes
|
|
*/
|
|
private function createVariant(
|
|
CatalogItem $catalogItem,
|
|
array $data,
|
|
array $itemAttributes,
|
|
int $index,
|
|
): Variant {
|
|
unset($data['images']);
|
|
|
|
if (
|
|
array_key_exists('inventory_id', $data)
|
|
|| array_key_exists('reserved_stock', $data)
|
|
|| array_key_exists('sold_units', $data)
|
|
) {
|
|
throw ValidationException::withMessages([
|
|
"variants.{$index}.inventory" => [
|
|
__('api.catalog.managed_inventory_fields'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$inventory = $this->createInventory((int) ($data['real_stock'] ?? 0));
|
|
$eventDateIds = collect($data['event_date_ids'] ?? [])
|
|
->when(
|
|
isset($data['event_date_id']),
|
|
fn ($ids) => $ids->push($data['event_date_id']),
|
|
)
|
|
->filter(fn ($id): bool => $id !== null)
|
|
->map(fn ($id): int => (int) $id)
|
|
->unique()
|
|
->sort()
|
|
->values();
|
|
|
|
$eventDateItemAttribute = $itemAttributes['event_date'] ?? null;
|
|
if ($eventDateItemAttribute !== null && (
|
|
$eventDateIds->isEmpty()
|
|
|| (! $eventDateItemAttribute->allow_multi_select && $eventDateIds->count() !== 1)
|
|
)) {
|
|
throw ValidationException::withMessages([
|
|
"variants.{$index}.event_date_ids" => [
|
|
$eventDateItemAttribute->allow_multi_select
|
|
? __('api.catalog.event_date_selection_required')
|
|
: __('api.catalog.single_event_date_required'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$validEventDateCount = EventDate::query()
|
|
->whereKey($eventDateIds)
|
|
->where('tenant_code', $catalogItem->tenant_code)
|
|
->count();
|
|
|
|
if ($validEventDateCount !== $eventDateIds->count()) {
|
|
throw ValidationException::withMessages([
|
|
"variants.{$index}.event_date_ids" => [
|
|
__('api.catalog.event_date_wrong_tenant'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$variant = $catalogItem->variants()->create([
|
|
'inventory_id' => $inventory->id,
|
|
'event_date_id' => $eventDateIds->count() === 1 ? $eventDateIds->first() : null,
|
|
'descripcion' => $data['descripcion'] ?? null,
|
|
'precio' => $data['precio'] ?? null,
|
|
]);
|
|
$variant->eventDates()->sync($eventDateIds->all());
|
|
$variant->setRelation('catalogItem', $catalogItem);
|
|
|
|
foreach ($data['values'] ?? [] as $attributeCode => $value) {
|
|
$itemAttribute = $itemAttributes[$attributeCode] ?? null;
|
|
|
|
if ($itemAttribute === null) {
|
|
throw ValidationException::withMessages([
|
|
"variants.{$index}.values.{$attributeCode}" => [
|
|
__('api.catalog.attribute_not_on_item'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
foreach ($this->validatedVariantValues(
|
|
$itemAttribute,
|
|
$value,
|
|
"variants.{$index}.values.{$attributeCode}",
|
|
) as $validatedValue) {
|
|
$variant->definitions()->create([
|
|
'item_attribute_id' => $itemAttribute->id,
|
|
'value' => $validatedValue,
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $variant;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $variants
|
|
* @param array<int, string> $attributeCodes
|
|
*/
|
|
private function validateUniqueVariantCombinations(array $variants, array $attributeCodes): void
|
|
{
|
|
$seen = [];
|
|
$attributeCodes = array_values(array_unique($attributeCodes));
|
|
sort($attributeCodes);
|
|
|
|
foreach (array_values($variants) as $index => $variant) {
|
|
$eventDateIds = collect($variant['event_date_ids'] ?? [])
|
|
->when(
|
|
isset($variant['event_date_id']),
|
|
fn ($ids) => $ids->push($variant['event_date_id']),
|
|
)
|
|
->map(fn ($id): int => (int) $id)
|
|
->unique()
|
|
->sort()
|
|
->values()
|
|
->implode(',');
|
|
$combination = [$eventDateIds];
|
|
|
|
foreach ($attributeCodes as $attributeCode) {
|
|
$values = $variant['values'][$attributeCode] ?? '';
|
|
$normalizedValues = collect(is_array($values) ? $values : [$values])
|
|
->map(fn ($value): string => $this->normalizeVariantValue((string) $value))
|
|
->unique()
|
|
->sort()
|
|
->values()
|
|
->implode(',');
|
|
$combination[] = $normalizedValues;
|
|
}
|
|
|
|
$key = implode('|', $combination);
|
|
if (isset($seen[$key])) {
|
|
throw ValidationException::withMessages([
|
|
"variants.{$index}" => [__('api.catalog.duplicate_variant_combination')],
|
|
]);
|
|
}
|
|
|
|
$seen[$key] = true;
|
|
}
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function validatedVariantValues(
|
|
ItemAttribute $itemAttribute,
|
|
mixed $value,
|
|
string $validationKey,
|
|
): array {
|
|
$values = is_array($value) ? array_values($value) : [$value];
|
|
|
|
if ($values === [] || (! $itemAttribute->allow_multi_select && count($values) !== 1)) {
|
|
throw ValidationException::withMessages([
|
|
$validationKey => [
|
|
$itemAttribute->allow_multi_select
|
|
? __('api.catalog.multi_value_required')
|
|
: __('api.catalog.single_value_required'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
if (collect($values)->contains(fn ($item): bool => ! is_string($item) || trim($item) === '')) {
|
|
throw ValidationException::withMessages([
|
|
$validationKey => [__('api.catalog.selected_values_non_empty')],
|
|
]);
|
|
}
|
|
|
|
$normalizedValues = collect($values)
|
|
->map(fn (string $item): string => $this->normalizeVariantValue($item));
|
|
|
|
if ($normalizedValues->unique()->count() !== count($values)) {
|
|
throw ValidationException::withMessages([
|
|
$validationKey => [__('api.catalog.selected_values_distinct')],
|
|
]);
|
|
}
|
|
|
|
$attribute = $itemAttribute->attribute;
|
|
if ($attribute->type->supportsOptions() && ! $attribute->type->usesDynamicOptions()) {
|
|
$optionsByNormalizedValue = $attribute->options
|
|
->keyBy(fn ($option): string => $this->normalizeVariantValue($option->value));
|
|
|
|
$resolvedOptions = $normalizedValues->map(fn (string $normalizedValue) => $optionsByNormalizedValue->get($normalizedValue));
|
|
if ($resolvedOptions->contains(null)) {
|
|
throw ValidationException::withMessages([
|
|
$validationKey => [__('api.catalog.invalid_attribute_options')],
|
|
]);
|
|
}
|
|
|
|
$validityTimeIds = $resolvedOptions
|
|
->pluck('validity_time_id')
|
|
->filter()
|
|
->unique();
|
|
if ($validityTimeIds->count() > 1) {
|
|
throw ValidationException::withMessages([
|
|
$validationKey => [__('api.catalog.incompatible_validity_windows')],
|
|
]);
|
|
}
|
|
|
|
return $resolvedOptions->pluck('value')->all();
|
|
}
|
|
|
|
return collect($values)->map(fn (string $item): string => trim($item))->all();
|
|
}
|
|
|
|
private function normalizeVariantValue(string $value): string
|
|
{
|
|
return Str::ascii(mb_strtolower(trim($value)));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @param array<int, array<string, mixed>> $variants
|
|
*/
|
|
private function validateInventoryStrategy(
|
|
array $data,
|
|
array $variants,
|
|
bool $hasVariants,
|
|
bool $hasDirectStock,
|
|
): void {
|
|
if ($hasVariants && $variants === []) {
|
|
throw ValidationException::withMessages([
|
|
'variants' => [
|
|
__('api.catalog.variants_required'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
if (! $hasVariants && $variants !== []) {
|
|
throw ValidationException::withMessages([
|
|
'variants' => [
|
|
__('api.catalog.variants_forbidden'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
if ($hasVariants && $hasDirectStock) {
|
|
throw ValidationException::withMessages([
|
|
'real_stock' => [
|
|
__('api.catalog.direct_inventory_forbidden'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
if (
|
|
array_key_exists('inventory_id', $data)
|
|
|| array_key_exists('reserved_stock', $data)
|
|
|| array_key_exists('sold_units', $data)
|
|
) {
|
|
throw ValidationException::withMessages([
|
|
'inventory' => [
|
|
__('api.catalog.managed_inventory_fields'),
|
|
],
|
|
]);
|
|
}
|
|
}
|
|
}
|