feat(desfile): refactor entry management to use row-based configuration and expand seat options
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Domains\Desfile\Controllers;
|
namespace App\Domains\Desfile\Controllers;
|
||||||
|
|
||||||
use App\Domains\Desfile\Requests\ReplaceEntryImageRequest;
|
use App\Domains\Desfile\Requests\ReplaceEntryImageRequest;
|
||||||
use App\Domains\Desfile\Requests\SyncEntryVariantsRequest;
|
use App\Domains\Desfile\Requests\SyncEntryRowsRequest;
|
||||||
use App\Domains\Desfile\Requests\UpdateEntryImageRequest;
|
use App\Domains\Desfile\Requests\UpdateEntryImageRequest;
|
||||||
use App\Domains\Desfile\Resources\EntryResource;
|
use App\Domains\Desfile\Resources\EntryResource;
|
||||||
use App\Domains\Desfile\Services\EntryService;
|
use App\Domains\Desfile\Services\EntryService;
|
||||||
@@ -23,11 +23,11 @@ class EntryController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(SyncEntryVariantsRequest $request): EntryResource
|
public function update(SyncEntryRowsRequest $request): EntryResource
|
||||||
{
|
{
|
||||||
return new EntryResource($this->entryService->syncVariants(
|
return new EntryResource($this->entryService->syncRows(
|
||||||
$request->user()->tenant()->firstOrFail(),
|
$request->user()->tenant()->firstOrFail(),
|
||||||
$request->validated('variants'),
|
$request->validated('rows'),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
55
app/Domains/Desfile/Requests/SyncEntryRowsRequest.php
Normal file
55
app/Domains/Desfile/Requests/SyncEntryRowsRequest.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Desfile\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class SyncEntryRowsRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'rows' => ['required', 'array', 'min:1', 'max:1000'],
|
||||||
|
'rows.*' => ['required', 'array:type,sector,row,max_seat,price'],
|
||||||
|
'rows.*.type' => ['required', 'string', 'max:100'],
|
||||||
|
'rows.*.sector' => ['required', 'string', 'max:100'],
|
||||||
|
'rows.*.row' => ['required', 'integer', 'between:1,5'],
|
||||||
|
'rows.*.max_seat' => ['required', 'integer', 'between:1,100'],
|
||||||
|
'rows.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, callable> */
|
||||||
|
public function after(): array
|
||||||
|
{
|
||||||
|
return [function (Validator $validator): void {
|
||||||
|
$combinations = [];
|
||||||
|
|
||||||
|
foreach ($this->input('rows', []) as $index => $row) {
|
||||||
|
if (! is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$combination = collect(['type', 'sector', 'row'])
|
||||||
|
->map(fn (string $field): string => mb_strtolower(trim((string) ($row[$field] ?? ''))))
|
||||||
|
->implode('|');
|
||||||
|
|
||||||
|
if (isset($combinations[$combination])) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
"rows.{$index}",
|
||||||
|
'La combinación de tipo, sector y fila no puede repetirse.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$combinations[$combination] = true;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Desfile\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Validator;
|
|
||||||
|
|
||||||
class SyncEntryVariantsRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'variants' => ['required', 'array', 'min:1', 'max:1000'],
|
|
||||||
'variants.*' => ['required', 'array:id,type,sector,row,seat,price'],
|
|
||||||
'variants.*.id' => ['sometimes', 'integer', 'distinct'],
|
|
||||||
'variants.*.type' => ['required', 'string', 'max:100'],
|
|
||||||
'variants.*.sector' => ['required', 'string', 'max:100'],
|
|
||||||
'variants.*.row' => ['required', 'string', 'max:100'],
|
|
||||||
'variants.*.seat' => ['required', 'string', 'max:100'],
|
|
||||||
'variants.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<int, callable> */
|
|
||||||
public function after(): array
|
|
||||||
{
|
|
||||||
return [function (Validator $validator): void {
|
|
||||||
$combinations = [];
|
|
||||||
|
|
||||||
foreach ($this->input('variants', []) as $index => $variant) {
|
|
||||||
if (! is_array($variant)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$combination = collect(['type', 'sector', 'row', 'seat'])
|
|
||||||
->map(fn (string $field): string => mb_strtolower(trim((string) ($variant[$field] ?? ''))))
|
|
||||||
->implode('|');
|
|
||||||
|
|
||||||
if (isset($combinations[$combination])) {
|
|
||||||
$validator->errors()->add(
|
|
||||||
"variants.{$index}",
|
|
||||||
'La combinación de tipo, sector, fila y asiento no puede repetirse.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$combinations[$combination] = true;
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,18 +16,35 @@ class EntryResource extends JsonResource
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'variants' => $this->variants->map(function ($variant): array {
|
'rows' => $this->variants
|
||||||
$values = $variant->selectionValues();
|
->map(function ($variant): array {
|
||||||
|
$values = $variant->selectionValues();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $variant->id,
|
'type' => $values->get('tipo'),
|
||||||
'type' => $values->get('tipo'),
|
'sector' => $values->get('sector'),
|
||||||
'sector' => $values->get('sector'),
|
'row' => $values->get('fila'),
|
||||||
'row' => $values->get('fila'),
|
'seat' => (int) $values->get('asiento'),
|
||||||
'seat' => $values->get('asiento'),
|
'price' => $variant->getPrice(),
|
||||||
'price' => number_format($variant->getPrice(), 2, '.', ''),
|
];
|
||||||
];
|
})
|
||||||
})->values(),
|
->groupBy(fn (array $variant): string => implode('|', [
|
||||||
|
mb_strtolower(trim((string) $variant['type'])),
|
||||||
|
mb_strtolower(trim((string) $variant['sector'])),
|
||||||
|
mb_strtolower(trim((string) $variant['row'])),
|
||||||
|
]))
|
||||||
|
->map(function ($variants): array {
|
||||||
|
$first = $variants->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => $first['type'],
|
||||||
|
'sector' => $first['sector'],
|
||||||
|
'row' => $first['row'],
|
||||||
|
'max_seat' => $variants->max('seat'),
|
||||||
|
'price' => number_format($first['price'], 2, '.', ''),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values(),
|
||||||
'image' => $image === null ? null : [
|
'image' => $image === null ? null : [
|
||||||
'key' => $image->key,
|
'key' => $image->key,
|
||||||
'filename' => $image->filename,
|
'filename' => $image->filename,
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ class EntryService
|
|||||||
'seat' => 'asiento',
|
'seat' => 'asiento',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private const ROW_ATTRIBUTE_MAP = [
|
||||||
|
'type' => 'tipo',
|
||||||
|
'sector' => 'sector',
|
||||||
|
'row' => 'fila',
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(private readonly AttachmentService $attachmentService) {}
|
public function __construct(private readonly AttachmentService $attachmentService) {}
|
||||||
|
|
||||||
public function current(Tenant $tenant): CatalogItem
|
public function current(Tenant $tenant): CatalogItem
|
||||||
@@ -42,68 +48,70 @@ class EntryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $variants
|
* @param array<int, array<string, mixed>> $rows
|
||||||
*/
|
*/
|
||||||
public function syncVariants(Tenant $tenant, array $variants): CatalogItem
|
public function syncRows(Tenant $tenant, array $rows): CatalogItem
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($tenant, $variants): void {
|
DB::transaction(function () use ($tenant, $rows): void {
|
||||||
$entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
|
$entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
|
||||||
$itemAttributes = $this->itemAttributes($entry);
|
$itemAttributes = $this->itemAttributes($entry);
|
||||||
$existingVariants = $entry->variants()
|
$existingVariants = $entry->variants()
|
||||||
->with(['inventory', 'definitions.itemAttribute.attribute'])
|
->with(['inventory', 'definitions.itemAttribute.attribute'])
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->get();
|
->get();
|
||||||
$incomingIds = collect($variants)
|
$existingBySelection = $existingVariants->keyBy(
|
||||||
->pluck('id')
|
fn (Variant $variant): string => $this->selectionKey($variant->selectionValues()->all()),
|
||||||
->filter()
|
);
|
||||||
->map(fn ($id): int => (int) $id)
|
$desiredSelections = collect();
|
||||||
->values();
|
|
||||||
|
|
||||||
foreach ($existingVariants->whereNotIn('id', $incomingIds) as $variant) {
|
foreach (array_values($rows) as $index => $data) {
|
||||||
$this->assertVariantCanChangeIdentity($variant, 'variants');
|
$rowValues = $this->resolveRowValues($itemAttributes, $data, $index);
|
||||||
$variant->delete();
|
|
||||||
|
foreach (range(1, (int) $data['max_seat']) as $seat) {
|
||||||
|
$values = [
|
||||||
|
...$rowValues,
|
||||||
|
'asiento' => $this->resolveAttributeValue(
|
||||||
|
$itemAttributes,
|
||||||
|
'asiento',
|
||||||
|
(string) $seat,
|
||||||
|
"rows.{$index}.max_seat",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
$selectionKey = $this->selectionKey($values);
|
||||||
|
$desiredSelections->put($selectionKey, true);
|
||||||
|
$variant = $existingBySelection->get($selectionKey);
|
||||||
|
|
||||||
|
if ($variant === null) {
|
||||||
|
$variant = $entry->variants()->create([
|
||||||
|
'inventory_id' => Inventory::query()->create(['real_stock' => 1])->id,
|
||||||
|
'descripcion' => $this->description($values),
|
||||||
|
'precio' => $data['price'],
|
||||||
|
]);
|
||||||
|
$variant->definitions()->createMany(
|
||||||
|
collect($values)->map(
|
||||||
|
fn (string $value, string $code): array => [
|
||||||
|
'item_attribute_id' => $itemAttributes[$code]->id,
|
||||||
|
'value' => $value,
|
||||||
|
],
|
||||||
|
)->values()->all(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$variant->update([
|
||||||
|
'descripcion' => $this->description($values),
|
||||||
|
'precio' => $data['price'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (array_values($variants) as $index => $data) {
|
foreach ($existingVariants as $variant) {
|
||||||
$values = $this->resolveValues($itemAttributes, $data, $index);
|
$selectionKey = $this->selectionKey($variant->selectionValues()->all());
|
||||||
$variant = isset($data['id'])
|
if ($desiredSelections->has($selectionKey)) {
|
||||||
? $existingVariants->firstWhere('id', (int) $data['id'])
|
continue;
|
||||||
: 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) {
|
$this->assertVariantCanChangeIdentity($variant, 'rows');
|
||||||
$variant = $entry->variants()->create([
|
$variant->delete();
|
||||||
'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');
|
$minimumPrice = $entry->variants()->min('precio');
|
||||||
@@ -183,7 +191,7 @@ class EntryService
|
|||||||
|
|
||||||
if ($missing->isNotEmpty()) {
|
if ($missing->isNotEmpty()) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'variants' => [
|
'rows' => [
|
||||||
'Faltan atributos requeridos para las entradas del desfile: '.$missing->implode(', ').'.',
|
'Faltan atributos requeridos para las entradas del desfile: '.$missing->implode(', ').'.',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@@ -197,40 +205,49 @@ class EntryService
|
|||||||
* @param array<string, mixed> $data
|
* @param array<string, mixed> $data
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
private function resolveValues(Collection $itemAttributes, array $data, int $index): array
|
private function resolveRowValues(Collection $itemAttributes, array $data, int $index): array
|
||||||
{
|
{
|
||||||
$values = [];
|
$values = [];
|
||||||
|
|
||||||
foreach (self::ATTRIBUTE_MAP as $input => $code) {
|
foreach (self::ROW_ATTRIBUTE_MAP as $input => $code) {
|
||||||
$requestedValue = trim((string) $data[$input]);
|
$values[$code] = $this->resolveAttributeValue(
|
||||||
$option = $itemAttributes[$code]->attribute->options->first(
|
$itemAttributes,
|
||||||
fn ($candidate): bool => $this->normalize($candidate->value) === $this->normalize($requestedValue),
|
$code,
|
||||||
|
(string) $data[$input],
|
||||||
|
"rows.{$index}.{$input}",
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($option === null) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
"variants.{$index}.{$input}" => ['La opción seleccionada no es válida.'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$values[$code] = $option->value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $values;
|
return $values;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, string> $values */
|
/** @param Collection<string, ItemAttribute> $itemAttributes */
|
||||||
private function identityChanged(Variant $variant, array $values): bool
|
private function resolveAttributeValue(
|
||||||
{
|
Collection $itemAttributes,
|
||||||
$currentValues = $variant->definitions
|
string $code,
|
||||||
->mapWithKeys(fn ($definition): array => [
|
string $requestedValue,
|
||||||
$definition->itemAttribute?->attribute?->codigo => $definition->value,
|
string $errorKey,
|
||||||
]);
|
): string {
|
||||||
|
$requestedValue = trim($requestedValue);
|
||||||
return collect($values)->contains(
|
$option = $itemAttributes[$code]->attribute->options->first(
|
||||||
fn (string $value, string $code): bool => $this->normalize((string) $currentValues->get($code))
|
fn ($candidate): bool => $this->normalize($candidate->value) === $this->normalize($requestedValue),
|
||||||
!== $this->normalize($value),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($option === null) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
$errorKey => ['La opción seleccionada no es válida.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $option->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $values */
|
||||||
|
private function selectionKey(array $values): string
|
||||||
|
{
|
||||||
|
return collect(self::ATTRIBUTE_MAP)
|
||||||
|
->map(fn (string $code): string => $this->normalize((string) ($values[$code] ?? '')))
|
||||||
|
->implode('|');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertVariantCanChangeIdentity(Variant $variant, string $key): void
|
private function assertVariantCanChangeIdentity(Variant $variant, string $key): void
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'desfile_pura_tendencia';
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->replaceSeatOptions(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->replaceSeatOptions(17);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function replaceSeatOptions(int $maximum): void
|
||||||
|
{
|
||||||
|
$attributeId = DB::table('attribute')
|
||||||
|
->where('tenant_codigo', self::TENANT_CODE)
|
||||||
|
->where('codigo', 'asiento')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($attributeId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($attributeId, $maximum): void {
|
||||||
|
DB::table('attribute_options')->where('attribute_id', $attributeId)->delete();
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
DB::table('attribute_options')->insert(array_map(
|
||||||
|
fn (int $seat): array => [
|
||||||
|
'attribute_id' => $attributeId,
|
||||||
|
'validity_time_id' => null,
|
||||||
|
'value' => (string) $seat,
|
||||||
|
'label' => (string) $seat,
|
||||||
|
'sort_order' => $seat,
|
||||||
|
'metadata' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
range(1, $maximum),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -39,7 +39,7 @@ class EntryControllerTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_returns_and_synchronizes_the_single_entry_product_variants(): void
|
public function test_it_returns_row_configs_and_expands_them_into_seat_variants(): void
|
||||||
{
|
{
|
||||||
[$tenant, $entry] = $this->configuredEntry();
|
[$tenant, $entry] = $this->configuredEntry();
|
||||||
$existing = $this->createVariant($entry, 'NORMAL', 'A', '1', '1', 100000);
|
$existing = $this->createVariant($entry, 'NORMAL', 'A', '1', '1', 100000);
|
||||||
@@ -48,26 +48,25 @@ class EntryControllerTest extends TestCase
|
|||||||
$this->getJson('/api/v1/adminapp/tenant/desfile/entries')
|
$this->getJson('/api/v1/adminapp/tenant/desfile/entries')
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.id', $entry->id)
|
->assertJsonPath('data.id', $entry->id)
|
||||||
->assertJsonPath('data.variants.0.id', $existing->id)
|
->assertJsonPath('data.rows.0.type', 'NORMAL')
|
||||||
->assertJsonPath('data.variants.0.type', 'NORMAL')
|
->assertJsonPath('data.rows.0.row', '1')
|
||||||
->assertJsonPath('data.variants.0.seat', '1')
|
->assertJsonPath('data.rows.0.max_seat', 1)
|
||||||
->assertJsonPath('data.variants.0.price', '100000.00');
|
->assertJsonPath('data.rows.0.price', '100000.00');
|
||||||
|
|
||||||
$response = $this->putJson('/api/v1/adminapp/tenant/desfile/entries', [
|
$response = $this->putJson('/api/v1/adminapp/tenant/desfile/entries', [
|
||||||
'variants' => [
|
'rows' => [
|
||||||
[
|
[
|
||||||
'id' => $existing->id,
|
|
||||||
'type' => 'NORMAL',
|
'type' => 'NORMAL',
|
||||||
'sector' => 'A',
|
'sector' => 'A',
|
||||||
'row' => '1',
|
'row' => '1',
|
||||||
'seat' => '1',
|
'max_seat' => 2,
|
||||||
'price' => 120000,
|
'price' => 120000,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'type' => 'VIP + LUNCH',
|
'type' => 'VIP + LUNCH',
|
||||||
'sector' => 'B',
|
'sector' => 'B',
|
||||||
'row' => '2',
|
'row' => '2',
|
||||||
'seat' => '3',
|
'max_seat' => 3,
|
||||||
'price' => 250000,
|
'price' => 250000,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -75,21 +74,21 @@ class EntryControllerTest extends TestCase
|
|||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonCount(2, 'data.variants')
|
->assertJsonCount(2, 'data.rows')
|
||||||
->assertJsonPath('data.variants.0.price', '120000.00')
|
->assertJsonPath('data.rows.0.max_seat', 2)
|
||||||
->assertJsonPath('data.variants.1.type', 'VIP + LUNCH');
|
->assertJsonPath('data.rows.0.price', '120000.00')
|
||||||
|
->assertJsonPath('data.rows.1.max_seat', 3)
|
||||||
|
->assertJsonPath('data.rows.1.type', 'VIP + LUNCH');
|
||||||
|
|
||||||
$this->assertDatabaseCount('variantes', 2);
|
$this->assertDatabaseCount('variantes', 5);
|
||||||
$this->assertDatabaseHas('catalog_items', [
|
$this->assertDatabaseHas('catalog_items', [
|
||||||
'id' => $entry->id,
|
'id' => $entry->id,
|
||||||
'precio' => 120000,
|
'precio' => 120000,
|
||||||
]);
|
]);
|
||||||
$this->assertDatabaseHas('inventories', [
|
$this->assertSame('120000.00', $existing->fresh()->precio);
|
||||||
'id' => $response->json('data.variants.1.id') === null
|
$this->assertSame(2, $entry->variants()->where('precio', 120000)->count());
|
||||||
? 0
|
$this->assertSame(3, $entry->variants()->where('precio', 250000)->count());
|
||||||
: Variant::query()->findOrFail($response->json('data.variants.1.id'))->inventory_id,
|
$this->assertSame(5, Inventory::query()->where('real_stock', 1)->count());
|
||||||
'real_stock' => 1,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_replaces_and_toggles_the_entry_image(): void
|
public function test_it_replaces_and_toggles_the_entry_image(): void
|
||||||
@@ -129,32 +128,52 @@ class EntryControllerTest extends TestCase
|
|||||||
$this->assertCount(1, $entry->fresh()->attachments);
|
$this->assertCount(1, $entry->fresh()->attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_rejects_duplicate_seats_and_cross_tenant_access(): void
|
public function test_it_rejects_duplicate_rows(): void
|
||||||
{
|
{
|
||||||
[, $entry] = $this->configuredEntry();
|
[$tenant] = $this->configuredEntry();
|
||||||
$foreignVariant = $this->createVariant($entry, 'NORMAL', 'A', '1', '1', 100);
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
[$otherTenant] = $this->configuredEntry('other-desfile');
|
|
||||||
Sanctum::actingAs($this->createAdminAppUser($otherTenant));
|
|
||||||
|
|
||||||
$this->putJson('/api/v1/adminapp/tenant/desfile/entries', [
|
|
||||||
'variants' => [[
|
|
||||||
'id' => $foreignVariant->id,
|
|
||||||
...$this->variantPayload('NORMAL', 'A', '1', '1', 100),
|
|
||||||
]],
|
|
||||||
])
|
|
||||||
->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors('variants.0.id');
|
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'variants' => [
|
'rows' => [
|
||||||
$this->variantPayload('NORMAL', 'A', '1', '1', 100),
|
$this->rowPayload('NORMAL', 'A', '1', 10, 100),
|
||||||
$this->variantPayload('normal', 'A', '1', '1', 200),
|
$this->rowPayload('normal', 'A', '1', 20, 200),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->putJson('/api/v1/adminapp/tenant/desfile/entries', $payload)
|
$this->putJson('/api/v1/adminapp/tenant/desfile/entries', $payload)
|
||||||
->assertUnprocessable()
|
->assertUnprocessable()
|
||||||
->assertJsonValidationErrors('variants.1');
|
->assertJsonValidationErrors('rows.1');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_rows_and_seat_quantities_outside_the_configured_ranges(): void
|
||||||
|
{
|
||||||
|
[$tenant] = $this->configuredEntry();
|
||||||
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
|
|
||||||
|
$this->putJson('/api/v1/adminapp/tenant/desfile/entries', [
|
||||||
|
'rows' => [
|
||||||
|
$this->rowPayload('NORMAL', 'A', '6', 1, 100),
|
||||||
|
$this->rowPayload('NORMAL', 'A', '1', 101, 100),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors(['rows.0.row', 'rows.1.max_seat']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_cannot_reduce_the_maximum_below_a_reserved_seat(): void
|
||||||
|
{
|
||||||
|
[$tenant, $entry] = $this->configuredEntry();
|
||||||
|
$reserved = $this->createVariant($entry, 'NORMAL', 'A', '1', '3', 100);
|
||||||
|
$reserved->inventory()->update(['reserved_stock' => 1]);
|
||||||
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
|
|
||||||
|
$this->putJson('/api/v1/adminapp/tenant/desfile/entries', [
|
||||||
|
'rows' => [$this->rowPayload('NORMAL', 'A', '1', 2, 100)],
|
||||||
|
])
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors('rows');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('variantes', ['id' => $reserved->id, 'deleted_at' => null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array{Tenant, CatalogItem} */
|
/** @return array{Tenant, CatalogItem} */
|
||||||
@@ -184,8 +203,8 @@ class EntryControllerTest extends TestCase
|
|||||||
foreach ([
|
foreach ([
|
||||||
'tipo' => ['VIP + LUNCH', 'NORMAL'],
|
'tipo' => ['VIP + LUNCH', 'NORMAL'],
|
||||||
'sector' => ['A', 'B', 'C', 'D'],
|
'sector' => ['A', 'B', 'C', 'D'],
|
||||||
'fila' => ['1', '2'],
|
'fila' => array_map('strval', range(1, 5)),
|
||||||
'asiento' => ['1', '2', '3'],
|
'asiento' => array_map('strval', range(1, 100)),
|
||||||
] as $code => $options) {
|
] as $code => $options) {
|
||||||
$attribute = Attribute::query()->create([
|
$attribute = Attribute::query()->create([
|
||||||
'tenant_codigo' => $tenant->codigo,
|
'tenant_codigo' => $tenant->codigo,
|
||||||
@@ -238,14 +257,17 @@ class EntryControllerTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
private function variantPayload(
|
private function rowPayload(
|
||||||
string $type,
|
string $type,
|
||||||
string $sector,
|
string $sector,
|
||||||
string $row,
|
string $row,
|
||||||
string $seat,
|
int $maxSeat,
|
||||||
int $price,
|
int $price,
|
||||||
): array {
|
): array {
|
||||||
return compact('type', 'sector', 'row', 'seat', 'price');
|
return [
|
||||||
|
...compact('type', 'sector', 'row', 'price'),
|
||||||
|
'max_seat' => $maxSeat,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createAdminAppUser(Tenant $tenant): User
|
private function createAdminAppUser(Tenant $tenant): User
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ class CreateDesfilePuraTendenciaTenantTest extends TestCase
|
|||||||
);
|
);
|
||||||
$rowAndSeatMigration->up();
|
$rowAndSeatMigration->up();
|
||||||
|
|
||||||
|
$seatOptionsMigration = require database_path(
|
||||||
|
'migrations/2026_08_14_040000_expand_desfile_seat_options.php'
|
||||||
|
);
|
||||||
|
$seatOptionsMigration->up();
|
||||||
|
|
||||||
$footerBackgroundMigration = require database_path(
|
$footerBackgroundMigration = require database_path(
|
||||||
'migrations/2026_08_12_060000_set_desfile_footer_background_image.php'
|
'migrations/2026_08_12_060000_set_desfile_footer_background_image.php'
|
||||||
);
|
);
|
||||||
@@ -192,7 +197,7 @@ class CreateDesfilePuraTendenciaTenantTest extends TestCase
|
|||||||
'tipo' => ['VIP + LUNCH', 'NORMAL'],
|
'tipo' => ['VIP + LUNCH', 'NORMAL'],
|
||||||
'sector' => ['A', 'B', 'C', 'D'],
|
'sector' => ['A', 'B', 'C', 'D'],
|
||||||
'fila' => array_map('strval', range(1, 5)),
|
'fila' => array_map('strval', range(1, 5)),
|
||||||
'asiento' => array_map('strval', range(1, 17)),
|
'asiento' => array_map('strval', range(1, 100)),
|
||||||
], $attributes->map(fn (object $attribute): array => DB::table('attribute_options')
|
], $attributes->map(fn (object $attribute): array => DB::table('attribute_options')
|
||||||
->where('attribute_id', $attribute->id)
|
->where('attribute_id', $attribute->id)
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
|
|||||||
Reference in New Issue
Block a user