286 lines
10 KiB
PHP
286 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Desfile\Services;
|
|
|
|
use App\Domains\Attachable\Models\Attachment;
|
|
use App\Domains\Attachable\Services\AttachmentService;
|
|
use App\Domains\Catalog\Models\CatalogItem;
|
|
use App\Domains\Catalog\Models\Inventory;
|
|
use App\Domains\Catalog\Models\ItemAttribute;
|
|
use App\Domains\Catalog\Models\Variant;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Throwable;
|
|
|
|
class EntryService
|
|
{
|
|
private const ATTRIBUTE_MAP = [
|
|
'type' => 'tipo',
|
|
'sector' => 'sector',
|
|
'row' => 'fila',
|
|
'seat' => 'asiento',
|
|
];
|
|
|
|
public function __construct(private readonly AttachmentService $attachmentService) {}
|
|
|
|
public function current(Tenant $tenant): CatalogItem
|
|
{
|
|
return $this->entryQuery($tenant)
|
|
->with([
|
|
'allAttachments',
|
|
'itemAttributes.attribute.options',
|
|
'variants' => fn ($query) => $query->orderBy('id'),
|
|
'variants.inventory',
|
|
'variants.definitions.itemAttribute.attribute',
|
|
])
|
|
->firstOrFail();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $variants
|
|
*/
|
|
public function syncVariants(Tenant $tenant, array $variants): CatalogItem
|
|
{
|
|
DB::transaction(function () use ($tenant, $variants): void {
|
|
$entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
|
|
$itemAttributes = $this->itemAttributes($entry);
|
|
$existingVariants = $entry->variants()
|
|
->with(['inventory', 'definitions.itemAttribute.attribute'])
|
|
->lockForUpdate()
|
|
->get();
|
|
$incomingIds = collect($variants)
|
|
->pluck('id')
|
|
->filter()
|
|
->map(fn ($id): int => (int) $id)
|
|
->values();
|
|
|
|
foreach ($existingVariants->whereNotIn('id', $incomingIds) as $variant) {
|
|
$this->assertVariantCanChangeIdentity($variant, 'variants');
|
|
$variant->delete();
|
|
}
|
|
|
|
foreach (array_values($variants) as $index => $data) {
|
|
$values = $this->resolveValues($itemAttributes, $data, $index);
|
|
$variant = isset($data['id'])
|
|
? $existingVariants->firstWhere('id', (int) $data['id'])
|
|
: null;
|
|
|
|
if (isset($data['id']) && $variant === null) {
|
|
throw ValidationException::withMessages([
|
|
"variants.{$index}.id" => [
|
|
'La variante no pertenece al producto Entrada del desfile.',
|
|
],
|
|
]);
|
|
}
|
|
|
|
if ($variant === null) {
|
|
$variant = $entry->variants()->create([
|
|
'inventory_id' => Inventory::query()->create(['real_stock' => 1])->id,
|
|
'descripcion' => $this->description($values),
|
|
'precio' => $data['price'],
|
|
]);
|
|
} else {
|
|
if ($this->identityChanged($variant, $values)) {
|
|
$this->assertVariantCanChangeIdentity($variant, "variants.{$index}");
|
|
}
|
|
|
|
$variant->update([
|
|
'descripcion' => $this->description($values),
|
|
'precio' => $data['price'],
|
|
]);
|
|
$variant->definitions()->delete();
|
|
}
|
|
|
|
$variant->definitions()->createMany(
|
|
collect($values)->map(
|
|
fn (string $value, string $code): array => [
|
|
'item_attribute_id' => $itemAttributes[$code]->id,
|
|
'value' => $value,
|
|
],
|
|
)->values()->all(),
|
|
);
|
|
}
|
|
|
|
$minimumPrice = $entry->variants()->min('precio');
|
|
if ($minimumPrice !== null) {
|
|
$entry->update(['precio' => $minimumPrice]);
|
|
}
|
|
});
|
|
|
|
return $this->current($tenant);
|
|
}
|
|
|
|
public function replaceImage(
|
|
Tenant $tenant,
|
|
UploadedFile $image,
|
|
bool $isEnabled,
|
|
): CatalogItem {
|
|
$attachment = $this->attachmentService->store($image, 'catalog-items');
|
|
$previousAttachments = collect();
|
|
|
|
try {
|
|
DB::transaction(function () use ($tenant, $attachment, $isEnabled, &$previousAttachments): void {
|
|
$entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
|
|
$previousAttachments = $entry->allAttachments()->get();
|
|
$entry->allAttachments()->sync([
|
|
$attachment->id => [
|
|
'orden' => 0,
|
|
'is_enabled' => $isEnabled,
|
|
],
|
|
]);
|
|
});
|
|
} catch (Throwable $throwable) {
|
|
$this->deleteAttachmentQuietly($attachment);
|
|
throw $throwable;
|
|
}
|
|
|
|
$previousAttachments->each(fn (Attachment $previous) => $this->deleteIfUnused($previous));
|
|
|
|
return $this->current($tenant);
|
|
}
|
|
|
|
public function updateImage(Tenant $tenant, bool $isEnabled): CatalogItem
|
|
{
|
|
DB::transaction(function () use ($tenant, $isEnabled): void {
|
|
$entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
|
|
$attachment = $entry->allAttachments()->lockForUpdate()->firstOrFail();
|
|
|
|
$entry->allAttachments()->updateExistingPivot($attachment->id, [
|
|
'is_enabled' => $isEnabled,
|
|
]);
|
|
});
|
|
|
|
return $this->current($tenant);
|
|
}
|
|
|
|
public function deleteImage(Tenant $tenant): void
|
|
{
|
|
$attachment = DB::transaction(function () use ($tenant): Attachment {
|
|
$entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
|
|
$attachment = $entry->allAttachments()->lockForUpdate()->firstOrFail();
|
|
$entry->allAttachments()->detach($attachment->id);
|
|
|
|
return $attachment;
|
|
});
|
|
|
|
$this->deleteIfUnused($attachment);
|
|
}
|
|
|
|
/** @return Collection<string, ItemAttribute> */
|
|
private function itemAttributes(CatalogItem $entry): Collection
|
|
{
|
|
$attributes = $entry->itemAttributes()
|
|
->with('attribute.options')
|
|
->get()
|
|
->filter(fn (ItemAttribute $itemAttribute): bool => $itemAttribute->attribute !== null)
|
|
->keyBy(fn (ItemAttribute $itemAttribute): string => $itemAttribute->attribute->codigo);
|
|
$missing = collect(self::ATTRIBUTE_MAP)->diff($attributes->keys());
|
|
|
|
if ($missing->isNotEmpty()) {
|
|
throw ValidationException::withMessages([
|
|
'variants' => [
|
|
'Faltan atributos requeridos para las entradas del desfile: '.$missing->implode(', ').'.',
|
|
],
|
|
]);
|
|
}
|
|
|
|
return $attributes;
|
|
}
|
|
|
|
/**
|
|
* @param Collection<string, ItemAttribute> $itemAttributes
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, string>
|
|
*/
|
|
private function resolveValues(Collection $itemAttributes, array $data, int $index): array
|
|
{
|
|
$values = [];
|
|
|
|
foreach (self::ATTRIBUTE_MAP as $input => $code) {
|
|
$requestedValue = trim((string) $data[$input]);
|
|
$option = $itemAttributes[$code]->attribute->options->first(
|
|
fn ($candidate): bool => $this->normalize($candidate->value) === $this->normalize($requestedValue),
|
|
);
|
|
|
|
if ($option === null) {
|
|
throw ValidationException::withMessages([
|
|
"variants.{$index}.{$input}" => ['La opción seleccionada no es válida.'],
|
|
]);
|
|
}
|
|
|
|
$values[$code] = $option->value;
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/** @param array<string, string> $values */
|
|
private function identityChanged(Variant $variant, array $values): bool
|
|
{
|
|
$currentValues = $variant->definitions
|
|
->mapWithKeys(fn ($definition): array => [
|
|
$definition->itemAttribute?->attribute?->codigo => $definition->value,
|
|
]);
|
|
|
|
return collect($values)->contains(
|
|
fn (string $value, string $code): bool => $this->normalize((string) $currentValues->get($code))
|
|
!== $this->normalize($value),
|
|
);
|
|
}
|
|
|
|
private function assertVariantCanChangeIdentity(Variant $variant, string $key): void
|
|
{
|
|
$inventory = $variant->inventory;
|
|
|
|
if (($inventory?->reserved_stock ?? 0) > 0 || ($inventory?->sold_units ?? 0) > 0) {
|
|
throw ValidationException::withMessages([
|
|
$key => [
|
|
'No se puede modificar ni eliminar un asiento con ventas o reservas.',
|
|
],
|
|
]);
|
|
}
|
|
}
|
|
|
|
/** @param array<string, string> $values */
|
|
private function description(array $values): string
|
|
{
|
|
return "Sector {$values['sector']} - Fila {$values['fila']} - Asiento {$values['asiento']} - {$values['tipo']}";
|
|
}
|
|
|
|
private function normalize(string $value): string
|
|
{
|
|
return Str::ascii(mb_strtolower(trim($value)));
|
|
}
|
|
|
|
/** @return Builder<CatalogItem> */
|
|
private function entryQuery(Tenant $tenant): Builder
|
|
{
|
|
return CatalogItem::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->where('slug', 'entrada');
|
|
}
|
|
|
|
private function deleteIfUnused(Attachment $attachment): void
|
|
{
|
|
if (DB::table('catalog_items_attachments')->where('attachment_id', $attachment->id)->exists()) {
|
|
return;
|
|
}
|
|
|
|
$this->deleteAttachmentQuietly($attachment);
|
|
}
|
|
|
|
private function deleteAttachmentQuietly(Attachment $attachment): void
|
|
{
|
|
try {
|
|
$this->attachmentService->delete($attachment);
|
|
} catch (Throwable $throwable) {
|
|
report($throwable);
|
|
}
|
|
}
|
|
}
|