Squashed commit of the following:

commit 6ae625d4fdc11989950ef0678db62fdb52be7c8c
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Sep 2 09:46:18 2026 -0300

    refactor(tickets): enhance search functionality for tickets by client names and formatted amounts

commit 098e525d32be9cb6487bc01a3d120aa0f2386a15
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Sep 2 09:09:30 2026 -0300

    fix(tickets): protect referenced purchase items

commit 210db17f2f6a6a4a509297b773a2b5d3e3149a3d
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Sep 2 08:59:24 2026 -0300

    test(tickets): cover purchase item references

commit 6a004713c4bad622dfa5e26394458925db1cb5a7
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Sep 2 08:59:02 2026 -0300

    refactor(tickets): reference purchase items directly
This commit is contained in:
2026-09-02 09:47:04 -03:00
parent 836aa1afd7
commit ee906d2d2e
18 changed files with 504 additions and 85 deletions

View File

@@ -161,7 +161,7 @@ class NotificationMailServiceTest extends TestCase
'precio' => 25,
'has_tickets' => true,
]);
$purchase->items()->create([
$purchaseItem = $purchase->items()->create([
'source_catalog_item_id' => $catalogItem->id,
'nombre' => 'Entrada',
'descripcion' => 'Entrada general',
@@ -175,7 +175,7 @@ class NotificationMailServiceTest extends TestCase
$ticket = Ticket::query()->create([
'tenant_code' => $this->tenant->codigo,
'ticket' => fake()->uuid(),
'source_purchase_id' => $purchase->id,
'source_purchase_item_id' => $purchaseItem->id,
'source_catalog_item_id' => $catalogItem->id,
'user_id' => $this->user->id,
]);

View File

@@ -336,17 +336,28 @@ class AdminAppSaleControllerTest extends TestCase
'precio' => 20000,
'has_tickets' => true,
]);
$purchaseItem = $purchase->items()->create([
'source_catalog_item_id' => $catalogItem->id,
'nombre' => $catalogItem->nombre,
'descripcion' => $catalogItem->descripcion,
'slug' => $catalogItem->slug,
'item_nombre' => $catalogItem->nombre,
'variant_attributes' => [],
'cantidad' => 2,
'precio_unitario' => 10000,
'total' => 20000,
]);
$firstTicket = Ticket::query()->create([
'tenant_code' => $tenant->codigo,
'ticket' => '11111111-1111-4111-8111-111111111111',
'source_purchase_id' => $purchase->id,
'source_purchase_item_id' => $purchaseItem->id,
'source_catalog_item_id' => $catalogItem->id,
'user_id' => $admin->id,
]);
$usedTicket = Ticket::query()->create([
'tenant_code' => $tenant->codigo,
'ticket' => '22222222-2222-4222-8222-222222222222',
'source_purchase_id' => $purchase->id,
'source_purchase_item_id' => $purchaseItem->id,
'source_catalog_item_id' => $catalogItem->id,
'used_at' => now()->subMinute(),
'user_id' => $admin->id,

View File

@@ -297,7 +297,7 @@ class DesfilePuraTendenciaSeederTest extends TestCase
->where('total', 0)
->count());
$this->assertSame(49, DB::table('tickets')
->where('source_purchase_id', $purchase->id)
->whereIn('source_purchase_item_id', (clone $items)->pluck('id'))
->where('user_id', $user->id)
->where('source_catalog_item_id', $catalogItemId)
->count());

View File

@@ -79,7 +79,7 @@ class AdminAppTicketControllerTest extends TestCase
->assertJsonPath('meta.total', 1);
}
public function test_it_supports_id_and_uuid_search(): void
public function test_it_searches_by_id_and_does_not_search_by_uuid(): void
{
$tenant = $this->createTenant('fiesta_futbol_infantil');
$admin = $this->createAdminAppUser($tenant);
@@ -95,9 +95,98 @@ class AdminAppTicketControllerTest extends TestCase
->assertJsonPath('data.0.id', $matching->id);
$this->getJson('/api/v1/adminapp/tenant/tickets?q='.substr($matching->ticket, 0, 8))
->assertOk()
->assertJsonCount(0, 'data');
}
public function test_it_searches_by_visible_client_and_scanner_names(): void
{
$tenant = $this->createTenant('fiesta_futbol_infantil');
$admin = $this->createAdminAppUser($tenant);
$client = User::factory()->create([
'tenant_codigo' => $tenant->codigo,
'nombre_apellido' => 'María Cliente',
]);
$scanner = $this->createAdminAppUser($tenant);
$scanner->update(['nombre_apellido' => 'Carlos Inspector']);
$this->grantTicketsMenu($tenant);
Sanctum::actingAs($admin);
$clientTicket = $this->createTicket($tenant, $client);
$scannerTicket = $this->createTicket($tenant, $admin, [
'scanner_user_id' => $scanner->id,
'used_at' => now(),
]);
$this->createTicket($tenant, $admin);
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query(['q' => 'maría']))
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.ticket', $matching->ticket);
->assertJsonPath('data.0.id', $clientTicket->id);
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query(['q' => 'INSPECTOR']))
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $scannerTicket->id);
}
public function test_it_searches_by_order_number_and_formatted_unit_amount(): void
{
$tenant = $this->createTenant('fiesta_futbol_infantil');
$admin = $this->createAdminAppUser($tenant);
$this->grantTicketsMenu($tenant);
Sanctum::actingAs($admin);
foreach (range(1, 10) as $number) {
$this->createPurchase($tenant, $admin, now()->subMinutes($number)->toDateTimeString());
}
$item = CatalogItem::query()->create([
'tenant_code' => $tenant->codigo,
'slug' => 'entrada-busqueda',
'nombre' => 'Entrada búsqueda',
'precio' => '1234.50',
]);
$purchase = Purchase::query()->create([
'tenant_codigo' => $tenant->codigo,
'user_id' => $admin->id,
'status' => Purchase::STATUS_PAID,
'nombre_apellido' => 'Cliente de la compra',
'total' => '1234.50',
]);
$purchaseItem = PurchaseItem::query()->create([
'compra_id' => $purchase->id,
'source_catalog_item_id' => $item->id,
'nombre' => $item->nombre,
'descripcion' => '',
'slug' => $item->slug,
'item_nombre' => $item->nombre,
'cantidad' => 1,
'precio_unitario' => '1234.50',
'total' => '1234.50',
]);
$matching = $this->createTicket($tenant, $admin, [
'source_purchase_item_id' => $purchaseItem->id,
'source_catalog_item_id' => $item->id,
]);
$this->createTicket($tenant, $admin);
$this->getJson("/api/v1/adminapp/tenant/tickets?q={$purchase->id}")
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $matching->id);
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query(['q' => 'cliente de la compra']))
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $matching->id);
foreach (['1234,50', '$1.234,50', '1,234.50'] as $amount) {
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query(['q' => $amount]))
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $matching->id);
}
}
public function test_it_sorts_by_ticket_id_before_paginating(): void
@@ -219,7 +308,7 @@ class AdminAppTicketControllerTest extends TestCase
'nombre_apellido' => 'Nombre Apellido',
'total' => '8000.00',
]);
PurchaseItem::query()->create([
$purchaseItem = PurchaseItem::query()->create([
'compra_id' => $purchase->id,
'source_catalog_item_id' => $item->id,
'source_variant_id' => $variant->id,
@@ -232,7 +321,7 @@ class AdminAppTicketControllerTest extends TestCase
'total' => '8000.00',
]);
$ticket = $this->createTicket($tenant, $admin, [
'source_purchase_id' => $purchase->id,
'source_purchase_item_id' => $purchaseItem->id,
'source_catalog_item_id' => $item->id,
'source_variant_id' => $variant->id,
'scanner_user_id' => $admin->id,
@@ -242,6 +331,7 @@ class AdminAppTicketControllerTest extends TestCase
$this->getJson('/api/v1/adminapp/tenant/tickets')
->assertOk()
->assertJsonPath('data.0.id', $ticket->id)
->assertJsonPath('data.0.source_purchase_item_id', $purchaseItem->id)
->assertJsonPath('data.0.order_number', $purchase->id)
->assertJsonPath('data.0.product', 'Remera')
->assertJsonPath('data.0.amount', '8000.00')
@@ -289,20 +379,23 @@ class AdminAppTicketControllerTest extends TestCase
$matchingPurchase = $this->createPurchase($tenant, $admin, '2026-08-20 10:00:00');
$otherPurchase = $this->createPurchase($tenant, $admin, '2026-08-21 10:00:00');
$dinnerItem = $this->createPurchaseItem($matchingPurchase, $food, $dinner);
$lunchItem = $this->createPurchaseItem($matchingPurchase, $food, $lunch);
$otherDinnerItem = $this->createPurchaseItem($otherPurchase, $food, $otherDinner);
$matching = $this->createTicket($tenant, $admin, [
'source_purchase_id' => $matchingPurchase->id,
'source_purchase_item_id' => $dinnerItem->id,
'source_catalog_item_id' => $food->id,
'source_variant_id' => $dinner->id,
'used_at' => now(),
]);
$this->createTicket($tenant, $admin, [
'source_purchase_id' => $matchingPurchase->id,
'source_purchase_item_id' => $lunchItem->id,
'source_catalog_item_id' => $food->id,
'source_variant_id' => $lunch->id,
'used_at' => now(),
]);
$this->createTicket($tenant, $admin, [
'source_purchase_id' => $otherPurchase->id,
'source_purchase_item_id' => $otherDinnerItem->id,
'source_catalog_item_id' => $food->id,
'source_variant_id' => $otherDinner->id,
'used_at' => now(),
@@ -494,6 +587,22 @@ class AdminAppTicketControllerTest extends TestCase
return $purchase;
}
private function createPurchaseItem(Purchase $purchase, CatalogItem $item, Variant $variant): PurchaseItem
{
return $purchase->items()->create([
'source_catalog_item_id' => $item->id,
'source_variant_id' => $variant->id,
'nombre' => $item->nombre,
'descripcion' => $item->descripcion,
'slug' => $item->slug,
'item_nombre' => $item->nombre,
'variant_attributes' => [],
'cantidad' => 1,
'precio_unitario' => 0,
'total' => 0,
]);
}
private function grantTicketsMenu(Tenant $tenant): void
{
$menu = Menu::query()->create([

View File

@@ -19,6 +19,7 @@ use App\Domains\Ticket\Exceptions\TicketGenerationException;
use App\Domains\Ticket\Models\ValidityTime;
use App\Domains\Ticket\Services\TicketGeneratorService;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Queue;
@@ -246,14 +247,59 @@ class TicketGeneratorServiceTest extends TestCase
$purchase = $this->createPurchase($item, 1, $variant->id);
$purchase->markAsPaid();
$purchaseItem = $purchase->items()->sole();
$this->assertDatabaseHas('tickets', [
'source_purchase_id' => $purchase->id,
'source_purchase_item_id' => $purchaseItem->id,
'source_catalog_item_id' => $item->id,
'source_variant_id' => $variant->id,
]);
}
public function test_bundle_tickets_reference_the_purchase_item_that_generated_them(): void
{
$component = $this->createTicketableItem('bundle-component');
$bundle = $this->createBundle('purchased-bundle');
$bundle->bundleComponents()->create([
'component_catalog_item_id' => $component->id,
'quantity' => 2,
]);
$purchase = $this->createPurchase($bundle, 2);
$purchaseItem = $purchase->items()->sole();
$purchase->markAsPaid();
$this->assertCount(4, $purchaseItem->tickets);
$this->assertTrue($purchaseItem->tickets->every(
fn ($ticket): bool => $ticket->source_purchase_item_id === $purchaseItem->id
&& $ticket->source_catalog_item_id === $component->id
));
$this->assertCount(4, $purchase->tickets);
}
public function test_a_purchase_item_that_generated_tickets_cannot_be_deleted(): void
{
$item = $this->createTicketableItem('protected-purchase-item');
$purchase = $this->createPurchase($item, 1);
$purchase->markAsPaid();
$purchaseItem = $purchase->items()->sole();
$this->expectException(QueryException::class);
$purchaseItem->delete();
}
public function test_a_purchase_with_generated_tickets_cannot_be_deleted(): void
{
$item = $this->createTicketableItem('protected-purchase');
$purchase = $this->createPurchase($item, 1);
$purchase->markAsPaid();
$this->expectException(QueryException::class);
$purchase->delete();
}
public function test_event_date_and_time_window_are_combined_in_the_same_and_group(): void
{
$item = $this->createTicketableItem('scheduled-meal');