feat(desfile): enhance entry reservation form and service with tenant-specific data
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Domains\Commerce\Catalog\Models;
|
||||
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use App\Domains\Commerce\Catalog\Enums\CatalogItemType;
|
||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Commerce\Catalog\Enums\InventorySubject;
|
||||
@@ -10,6 +9,7 @@ use App\Domains\Commerce\Catalog\Services\CatalogInventoryService;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticketing\Event\Models\Event;
|
||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -264,9 +264,15 @@ class CatalogItem extends Model
|
||||
/** @return Collection<int, Variant> */
|
||||
public function visibleVariants(?int $includedVariantId = null): Collection
|
||||
{
|
||||
$this->variants
|
||||
->filter(fn (Variant $variant): bool => $variant->exists)
|
||||
->loadMissing('desfileEntryReservations');
|
||||
|
||||
return $this->variants
|
||||
->each(fn (Variant $variant) => $variant->setRelation('catalogItem', $this))
|
||||
->filter(fn (Variant $variant): bool => $variant->hasOnlyActiveEventDates()
|
||||
&& (! $variant->relationLoaded('desfileEntryReservations')
|
||||
|| $variant->desfileEntryReservations->isEmpty())
|
||||
&& (($includedVariantId !== null && $variant->id === $includedVariantId)
|
||||
|| ($variant->isSellable() && (
|
||||
$this->inventory_policy === InventoryPolicy::Unlimited
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Shared\Forms\Controllers\AdminApp;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Shared\Forms\Resources\DesfileEntryReservationFormResource;
|
||||
use App\Shared\Forms\Services\DesfileEntryReservationFormService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DesfileEntryReservationFormController extends Controller
|
||||
{
|
||||
@@ -12,8 +13,10 @@ class DesfileEntryReservationFormController extends Controller
|
||||
private readonly DesfileEntryReservationFormService $formService,
|
||||
) {}
|
||||
|
||||
public function __invoke(): DesfileEntryReservationFormResource
|
||||
public function __invoke(Request $request): DesfileEntryReservationFormResource
|
||||
{
|
||||
return DesfileEntryReservationFormResource::make($this->formService->get());
|
||||
return DesfileEntryReservationFormResource::make(
|
||||
$this->formService->get($request->user()->tenant()->firstOrFail()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ class DesfileEntryReservationFormResource extends JsonResource
|
||||
{
|
||||
return [
|
||||
'payment_types' => $this->resource['payment_types'],
|
||||
'fields' => $this->resource['fields'],
|
||||
'variants' => $this->resource['variants'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,48 @@
|
||||
|
||||
namespace App\Shared\Forms\Services;
|
||||
|
||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
|
||||
|
||||
class DesfileEntryReservationFormService
|
||||
{
|
||||
/** @return array{payment_types: list<array{value: string, label: string}>} */
|
||||
public function get(): array
|
||||
/** @return array<string, mixed> */
|
||||
public function get(Tenant $tenant): array
|
||||
{
|
||||
$entry = CatalogItem::query()->forTenantCatalog($tenant)->where('slug', 'entrada')
|
||||
->with([
|
||||
'variants' => fn ($query) => $query->orderBy('id'),
|
||||
'variants.inventory',
|
||||
'variants.definitions.itemAttribute.attribute',
|
||||
'variants.eventDates',
|
||||
'variants.eventDate',
|
||||
])->first();
|
||||
$variants = $entry?->visibleVariants()
|
||||
->map(function (Variant $variant): array {
|
||||
$values = $variant->selectionValues();
|
||||
|
||||
return [
|
||||
'id' => $variant->id,
|
||||
'tipo' => (string) $values->get('tipo'),
|
||||
'sector' => (string) $values->get('sector'),
|
||||
'fila' => (string) $values->get('fila'),
|
||||
'asiento' => (string) $values->get('asiento'),
|
||||
'price' => $variant->getPrice(),
|
||||
];
|
||||
})->values() ?? collect();
|
||||
|
||||
return [
|
||||
'payment_types' => EntryReservationPaymentType::options(),
|
||||
'fields' => collect(['tipo' => 'Tipo', 'sector' => 'Sector', 'fila' => 'Fila', 'asiento' => 'Asiento'])
|
||||
->map(fn (string $label, string $key): array => [
|
||||
'key' => $key,
|
||||
'label' => $label,
|
||||
'options' => $variants->pluck($key)->unique()->sort(SORT_NATURAL)->values()
|
||||
->map(fn (string $value): array => ['value' => $value, 'label' => $value])->all(),
|
||||
])->values()->all(),
|
||||
'variants' => $variants->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
namespace Tests\Feature\Forms;
|
||||
|
||||
use App\Domains\Commerce\Catalog\Models\Attribute;
|
||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||
use App\Domains\Core\Menu\Models\Menu;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Shared\Enums\FieldType;
|
||||
use Database\Seeders\AuthorizationSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
@@ -37,7 +41,7 @@ class AdminAppDesfileEntryReservationFormControllerTest extends TestCase
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_it_only_returns_the_available_payment_types(): void
|
||||
public function test_it_returns_payment_types_and_empty_variant_options_without_a_catalog(): void
|
||||
{
|
||||
$tenant = $this->createTenant('desfile_pura_tendencia');
|
||||
$this->grantReservationsMenu($tenant);
|
||||
@@ -51,10 +55,66 @@ class AdminAppDesfileEntryReservationFormControllerTest extends TestCase
|
||||
['value' => 'sin_cargo', 'label' => 'Sin cargo'],
|
||||
['value' => 'otro_metodo', 'label' => 'Otro método'],
|
||||
],
|
||||
'fields' => [
|
||||
['key' => 'tipo', 'label' => 'Tipo', 'options' => []],
|
||||
['key' => 'sector', 'label' => 'Sector', 'options' => []],
|
||||
['key' => 'fila', 'label' => 'Fila', 'options' => []],
|
||||
['key' => 'asiento', 'label' => 'Asiento', 'options' => []],
|
||||
],
|
||||
'variants' => [],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_returns_only_available_combinations_without_reserving_stock(): void
|
||||
{
|
||||
$tenant = $this->createTenant('desfile_pura_tendencia');
|
||||
$this->grantReservationsMenu($tenant);
|
||||
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||
$entry = CatalogItem::query()->create([
|
||||
'tenant_code' => $tenant->codigo, 'slug' => 'entrada', 'nombre' => 'Entrada', 'precio' => 100,
|
||||
]);
|
||||
$attributes = [];
|
||||
foreach (['tipo', 'sector', 'fila', 'asiento'] as $code) {
|
||||
$attribute = Attribute::query()->create([
|
||||
'tenant_codigo' => $tenant->codigo, 'codigo' => $code,
|
||||
'nombre' => $code, 'type' => FieldType::Select,
|
||||
]);
|
||||
$attributes[$code] = $entry->itemAttributes()->create(['attribute_id' => $attribute->id]);
|
||||
}
|
||||
$availableId = null;
|
||||
foreach (['available', 'reserved', 'sold', 'disabled', 'administrative'] as $index => $state) {
|
||||
$inventory = Inventory::query()->create([
|
||||
'real_stock' => $state === 'sold' ? 0 : 1,
|
||||
'reserved_stock' => $state === 'reserved' ? 1 : 0,
|
||||
]);
|
||||
$variant = $entry->variants()->create([
|
||||
'inventory_id' => $inventory->id, 'precio' => 100,
|
||||
'sales_disabled_at' => $state === 'disabled' ? now() : null,
|
||||
]);
|
||||
foreach ($attributes as $code => $attribute) {
|
||||
$variant->definitions()->create([
|
||||
'item_attribute_id' => $attribute->id,
|
||||
'value' => $code === 'asiento' ? (string) ($index + 1) : '1',
|
||||
]);
|
||||
}
|
||||
if ($state === 'administrative') {
|
||||
$variant->desfileEntryReservations()->create([
|
||||
'fecha_reserva' => now(), 'importe' => 0, 'tipo_pago' => 'sin_cargo',
|
||||
]);
|
||||
}
|
||||
if ($state === 'available') {
|
||||
$availableId = $variant->id;
|
||||
}
|
||||
}
|
||||
$this->getJson('/api/v1/adminapp/forms/desfile/entry-reservation')
|
||||
->assertOk()->assertJsonCount(1, 'data.variants')
|
||||
->assertJsonPath('data.variants.0.id', $availableId)
|
||||
->assertJsonPath('data.fields.3.options', [['value' => '1', 'label' => '1']]);
|
||||
$this->assertDatabaseCount('desfile_entry_reservations', 1);
|
||||
$this->assertSame(1, (int) Inventory::query()->sum('reserved_stock'));
|
||||
}
|
||||
|
||||
private function createTenant(string $code): Tenant
|
||||
{
|
||||
return Tenant::query()->create([
|
||||
|
||||
60
tests/Unit/Catalog/VariantAvailabilityTest.php
Normal file
60
tests/Unit/Catalog/VariantAvailabilityTest.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Catalog;
|
||||
|
||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class VariantAvailabilityTest extends TestCase
|
||||
{
|
||||
public function test_administrative_reservations_are_excluded_even_for_unlimited_stock_and_cart_exceptions(): void
|
||||
{
|
||||
$reserved = new Variant;
|
||||
$reserved->id = 1;
|
||||
$reserved->setRelation('desfileEntryReservations', new Collection([new EntryReservation]));
|
||||
$available = new Variant;
|
||||
$available->id = 2;
|
||||
$available->setRelation('desfileEntryReservations', new Collection);
|
||||
$item = new CatalogItem(['inventory_policy' => InventoryPolicy::Unlimited]);
|
||||
$item->setRelation('variants', new Collection([$reserved, $available]));
|
||||
|
||||
$this->assertSame([2], $item->visibleVariants()->modelKeys());
|
||||
$this->assertSame([2], $item->visibleVariants(1)->modelKeys());
|
||||
}
|
||||
|
||||
public function test_sale_closure_and_variant_status_apply_without_the_cart_exception(): void
|
||||
{
|
||||
$active = new Variant;
|
||||
$active->id = 1;
|
||||
$disabled = new Variant(['sales_disabled_at' => now()]);
|
||||
$disabled->id = 2;
|
||||
$replaced = new Variant(['replaced_by_variant_id' => 1]);
|
||||
$replaced->id = 3;
|
||||
$item = new CatalogItem(['inventory_policy' => InventoryPolicy::Unlimited]);
|
||||
$item->setRelation('variants', new Collection([$active, $disabled, $replaced]));
|
||||
|
||||
$this->assertSame([1], $item->visibleVariants()->modelKeys());
|
||||
$item->sales_end_at = now()->subMinute();
|
||||
$this->assertSame([], $item->visibleVariants()->modelKeys());
|
||||
$this->assertSame([1], $item->visibleVariants(1)->modelKeys());
|
||||
}
|
||||
|
||||
public function test_only_the_cart_can_include_its_own_out_of_stock_variant(): void
|
||||
{
|
||||
$variant = new Variant;
|
||||
$variant->id = 1;
|
||||
$variant->setRelation('inventory', new Inventory(['real_stock' => 1, 'reserved_stock' => 1]));
|
||||
$item = new CatalogItem(['inventory_policy' => InventoryPolicy::Tracked]);
|
||||
$item->setRelation('variants', new Collection([$variant]));
|
||||
|
||||
$this->assertSame([], $item->visibleVariants()->modelKeys());
|
||||
$this->assertSame([1], $item->visibleVariants(1)->modelKeys());
|
||||
$item->inventory_policy = InventoryPolicy::Unlimited;
|
||||
$this->assertSame([1], $item->visibleVariants()->modelKeys());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user