feat(desfile): implement entry management with image handling and variant synchronization

This commit is contained in:
2026-08-14 11:24:17 -03:00
parent 7fbdc00c64
commit 21b0517e6c
10 changed files with 767 additions and 1 deletions

View File

@@ -0,0 +1,258 @@
<?php
namespace Tests\Feature\Desfile;
use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Auth\Models\User;
use App\Domains\Authorization\Enums\RoleCode;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Enums\InventorySubject;
use App\Domains\Catalog\Models\Attribute;
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\Menu\Models\Menu;
use App\Domains\Shared\Enums\FieldType;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Tenant\Models\WebsiteType;
use Database\Seeders\AuthorizationSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class EntryControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(AuthorizationSeeder::class);
WebsiteType::query()->create([
'codigo' => 'onticket',
'nombre' => 'OnTicket',
]);
}
public function test_it_returns_and_synchronizes_the_single_entry_product_variants(): void
{
[$tenant, $entry] = $this->configuredEntry();
$existing = $this->createVariant($entry, 'NORMAL', 'A', '1', '1', 100000);
Sanctum::actingAs($this->createAdminAppUser($tenant));
$this->getJson('/api/v1/adminapp/tenant/desfile/entries')
->assertOk()
->assertJsonPath('data.id', $entry->id)
->assertJsonPath('data.variants.0.id', $existing->id)
->assertJsonPath('data.variants.0.type', 'NORMAL')
->assertJsonPath('data.variants.0.seat', '1')
->assertJsonPath('data.variants.0.price', '100000.00');
$response = $this->putJson('/api/v1/adminapp/tenant/desfile/entries', [
'variants' => [
[
'id' => $existing->id,
'type' => 'NORMAL',
'sector' => 'A',
'row' => '1',
'seat' => '1',
'price' => 120000,
],
[
'type' => 'VIP + LUNCH',
'sector' => 'B',
'row' => '2',
'seat' => '3',
'price' => 250000,
],
],
]);
$response
->assertOk()
->assertJsonCount(2, 'data.variants')
->assertJsonPath('data.variants.0.price', '120000.00')
->assertJsonPath('data.variants.1.type', 'VIP + LUNCH');
$this->assertDatabaseCount('variantes', 2);
$this->assertDatabaseHas('catalog_items', [
'id' => $entry->id,
'precio' => 120000,
]);
$this->assertDatabaseHas('inventories', [
'id' => $response->json('data.variants.1.id') === null
? 0
: Variant::query()->findOrFail($response->json('data.variants.1.id'))->inventory_id,
'real_stock' => 1,
]);
}
public function test_it_replaces_and_toggles_the_entry_image(): void
{
Storage::fake('s3');
[$tenant, $entry] = $this->configuredEntry();
$oldImage = Attachment::query()->create([
'path' => 'catalog-items/old.png',
'filename' => 'old.png',
'type' => AttachmentType::Image,
'mime_type' => 'image/png',
'extension' => 'png',
'size' => 10,
]);
Storage::disk('s3')->put($oldImage->path, 'old');
$entry->allAttachments()->attach($oldImage->id, ['orden' => 0]);
Sanctum::actingAs($this->createAdminAppUser($tenant));
$this->post('/api/v1/adminapp/tenant/desfile/entries/image', [
'image' => UploadedFile::fake()->image('plano.png'),
'is_enabled' => false,
], ['Accept' => 'application/json'])
->assertOk()
->assertJsonPath('data.image.filename', 'plano.png')
->assertJsonPath('data.image.is_enabled', false);
$this->assertDatabaseMissing('attachments', ['id' => $oldImage->id]);
$this->assertCount(0, $entry->fresh()->attachments);
$this->assertCount(1, $entry->fresh()->allAttachments);
$this->patchJson('/api/v1/adminapp/tenant/desfile/entries/image', [
'is_enabled' => true,
])
->assertOk()
->assertJsonPath('data.image.is_enabled', true);
$this->assertCount(1, $entry->fresh()->attachments);
}
public function test_it_rejects_duplicate_seats_and_cross_tenant_access(): void
{
[, $entry] = $this->configuredEntry();
$foreignVariant = $this->createVariant($entry, 'NORMAL', 'A', '1', '1', 100);
[$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 = [
'variants' => [
$this->variantPayload('NORMAL', 'A', '1', '1', 100),
$this->variantPayload('normal', 'A', '1', '1', 200),
],
];
$this->putJson('/api/v1/adminapp/tenant/desfile/entries', $payload)
->assertUnprocessable()
->assertJsonValidationErrors('variants.1');
}
/** @return array{Tenant, CatalogItem} */
private function configuredEntry(string $tenantCode = 'desfile_pura_tendencia'): array
{
$tenant = Tenant::query()->create([
'codigo' => $tenantCode,
'nombre' => 'Desfile',
'dominio' => "{$tenantCode}.test",
'website_type_code' => 'onticket',
]);
$menu = Menu::query()->firstOrCreate(
['code' => 'adminapp.desfile.entradas'],
['label' => 'Entradas', 'route' => '/admin/desfile/entradas'],
);
$tenant->menues()->attach($menu->code);
$entry = CatalogItem::query()->create([
'tenant_code' => $tenant->codigo,
'slug' => 'entrada',
'nombre' => 'Entrada',
'precio' => 0,
'inventory_policy' => InventoryPolicy::Tracked,
'inventory_subject' => InventorySubject::Seat,
'has_tickets' => true,
]);
foreach ([
'tipo' => ['VIP + LUNCH', 'NORMAL'],
'sector' => ['A', 'B', 'C', 'D'],
'fila' => ['1', '2'],
'asiento' => ['1', '2', '3'],
] as $code => $options) {
$attribute = Attribute::query()->create([
'tenant_codigo' => $tenant->codigo,
'codigo' => $code,
'nombre' => ucfirst($code),
'type' => FieldType::Select,
'is_required' => true,
]);
foreach ($options as $order => $option) {
$attribute->options()->create([
'value' => $option,
'label' => $option,
'sort_order' => $order + 1,
]);
}
$entry->itemAttributes()->create([
'attribute_id' => $attribute->id,
'sort_order' => $entry->itemAttributes()->count() + 1,
]);
}
return [$tenant, $entry];
}
private function createVariant(
CatalogItem $entry,
string $type,
string $sector,
string $row,
string $seat,
int $price,
): Variant {
$variant = $entry->variants()->create([
'inventory_id' => Inventory::query()->create(['real_stock' => 1])->id,
'precio' => $price,
]);
$itemAttributes = $entry->itemAttributes()->with('attribute')->get()->keyBy(
fn (ItemAttribute $itemAttribute): string => $itemAttribute->attribute->codigo,
);
foreach (compact('type', 'sector', 'row', 'seat') as $input => $value) {
$code = ['type' => 'tipo', 'sector' => 'sector', 'row' => 'fila', 'seat' => 'asiento'][$input];
$variant->definitions()->create([
'item_attribute_id' => $itemAttributes[$code]->id,
'value' => $value,
]);
}
return $variant;
}
/** @return array<string, mixed> */
private function variantPayload(
string $type,
string $sector,
string $row,
string $seat,
int $price,
): array {
return compact('type', 'sector', 'row', 'seat', 'price');
}
private function createAdminAppUser(Tenant $tenant): User
{
return User::factory()->create([
'rol_codigo' => RoleCode::AdminApp->value,
'tenant_codigo' => $tenant->codigo,
]);
}
}