diff --git a/app/Domains/Purchase/Controllers/PurchaseController.php b/app/Domains/Purchase/Controllers/PurchaseController.php index a92e6af..ef5611f 100644 --- a/app/Domains/Purchase/Controllers/PurchaseController.php +++ b/app/Domains/Purchase/Controllers/PurchaseController.php @@ -55,7 +55,7 @@ class PurchaseController extends Controller { $compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra); - $compra->loadMissing('items'); + $compra->loadMissing('items')->loadCount('tickets'); $compra->items->load('imageAttachment'); return PurchaseResource::make($compra); diff --git a/app/Domains/Purchase/Models/Purchase.php b/app/Domains/Purchase/Models/Purchase.php index 70a2842..57a8d73 100644 --- a/app/Domains/Purchase/Models/Purchase.php +++ b/app/Domains/Purchase/Models/Purchase.php @@ -6,6 +6,7 @@ use App\Domains\Auth\Models\User; use App\Domains\Cart\Models\Cart; use App\Domains\Purchase\Events\PurchasePaid; use App\Domains\Tenant\Models\Tenant; +use App\Domains\Ticket\Models\Ticket; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -88,6 +89,14 @@ class Purchase extends Model return $this->hasMany(PurchaseItem::class, 'compra_id'); } + /** + * @return HasMany + */ + public function tickets(): HasMany + { + return $this->hasMany(Ticket::class, 'source_purchase_id'); + } + /** * @return HasOne */ diff --git a/app/Domains/Purchase/Resources/PurchaseResource.php b/app/Domains/Purchase/Resources/PurchaseResource.php index 13696c0..b383adc 100644 --- a/app/Domains/Purchase/Resources/PurchaseResource.php +++ b/app/Domains/Purchase/Resources/PurchaseResource.php @@ -20,6 +20,9 @@ class PurchaseResource extends JsonResource $items = $this->resource->relationLoaded('items') ? $this->resource->getRelation('items') : collect(); + $ticketsCount = array_key_exists('tickets_count', $this->resource->getAttributes()) + ? (int) $this->resource->getAttribute('tickets_count') + : null; $subtotal = $items->isNotEmpty() ? $items->reduce( @@ -51,6 +54,8 @@ class PurchaseResource extends JsonResource 'email' => $this->email, 'items_source' => $items->isNotEmpty() ? 'purchase' : null, 'items' => PurchaseItemResource::collection($items), + 'tickets_count' => $this->when($ticketsCount !== null, $ticketsCount), + 'has_generated_tickets' => $this->when($ticketsCount !== null, $ticketsCount > 0), 'subtotal' => $this->formatMoney($subtotal), 'total' => $this->formatMoney($total), ]; diff --git a/app/Domains/Ticket/Listeners/GenerateTicketsForPaidPurchase.php b/app/Domains/Ticket/Listeners/GenerateTicketsForPaidPurchase.php index 0befca2..29899d2 100644 --- a/app/Domains/Ticket/Listeners/GenerateTicketsForPaidPurchase.php +++ b/app/Domains/Ticket/Listeners/GenerateTicketsForPaidPurchase.php @@ -45,6 +45,7 @@ class GenerateTicketsForPaidPurchase $user, $purchaseItem->cantidad, $purchaseItem->source_variant_id, + $purchase->getKey(), ); array_push($ticketIds, ...$generatedTickets->pluck('id')->all()); diff --git a/app/Domains/Ticket/Models/Ticket.php b/app/Domains/Ticket/Models/Ticket.php index e0aaa74..17319da 100644 --- a/app/Domains/Ticket/Models/Ticket.php +++ b/app/Domains/Ticket/Models/Ticket.php @@ -3,6 +3,7 @@ namespace App\Domains\Ticket\Models; use App\Domains\Auth\Models\User; +use App\Domains\Purchase\Models\Purchase; use App\Domains\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -14,6 +15,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; 'ticket', 'name', 'description', + 'source_purchase_id', 'source_catalog_item_id', 'source_variant_id', 'starts_at', @@ -38,6 +40,7 @@ class Ticket extends Model return [ 'source_catalog_item_id' => 'integer', 'source_variant_id' => 'integer', + 'source_purchase_id' => 'integer', 'starts_at' => 'datetime', 'expires_at' => 'datetime', 'used_at' => 'datetime', @@ -57,6 +60,12 @@ class Ticket extends Model return $this->belongsTo(User::class); } + /** @return BelongsTo */ + public function sourcePurchase(): BelongsTo + { + return $this->belongsTo(Purchase::class, 'source_purchase_id'); + } + public function isValid(): bool { $now = now(); diff --git a/app/Domains/Ticket/Services/TicketGeneratorService.php b/app/Domains/Ticket/Services/TicketGeneratorService.php index 7c8d770..c65b1e1 100644 --- a/app/Domains/Ticket/Services/TicketGeneratorService.php +++ b/app/Domains/Ticket/Services/TicketGeneratorService.php @@ -21,12 +21,13 @@ class TicketGeneratorService User $user, int $quantity = 1, ?int $sourceVariantId = null, + ?int $sourcePurchaseId = null, ): Collection { if ($quantity < 1) { throw TicketGenerationException::invalidQuantity(); } - return DB::transaction(function () use ($catalogItem, $user, $quantity, $sourceVariantId): Collection { + return DB::transaction(function () use ($catalogItem, $user, $quantity, $sourceVariantId, $sourcePurchaseId): Collection { $targets = $this->resolveTargets( $catalogItem, $quantity, @@ -36,6 +37,7 @@ class TicketGeneratorService return $targets->map(function (array $target) use ( $catalogItem, $sourceVariantId, + $sourcePurchaseId, $user, ): Ticket { $item = $target['catalog_item']; @@ -46,6 +48,7 @@ class TicketGeneratorService 'ticket' => (string) Str::uuid(), 'name' => $item->nombre, 'description' => (string) ($item->descripcion ?? ''), + 'source_purchase_id' => $sourcePurchaseId, 'source_catalog_item_id' => $catalogItem->getKey(), 'source_variant_id' => $sourceVariantId, 'starts_at' => $selectedItem->getMinimumUseDate(), diff --git a/database/migrations/2026_07_28_000000_add_source_purchase_id_to_tickets_table.php b/database/migrations/2026_07_28_000000_add_source_purchase_id_to_tickets_table.php new file mode 100644 index 0000000..75f29b3 --- /dev/null +++ b/database/migrations/2026_07_28_000000_add_source_purchase_id_to_tickets_table.php @@ -0,0 +1,27 @@ +foreignId('source_purchase_id') + ->nullable() + ->after('description') + ->constrained('compras') + ->cascadeOnUpdate() + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('tickets', function (Blueprint $table): void { + $table->dropConstrainedForeignId('source_purchase_id'); + }); + } +}; diff --git a/tests/Feature/Ticket/TicketGeneratorServiceTest.php b/tests/Feature/Ticket/TicketGeneratorServiceTest.php index e0912ff..0dc7d89 100644 --- a/tests/Feature/Ticket/TicketGeneratorServiceTest.php +++ b/tests/Feature/Ticket/TicketGeneratorServiceTest.php @@ -193,6 +193,12 @@ class TicketGeneratorServiceTest extends TestCase $this->assertDatabaseCount('tickets', 2); Event::assertDispatchedTimes(TicketsAvailable::class, 1); + $this->actingAs($this->user, 'sanctum') + ->getJson("/api/tenants/{$this->tenant->codigo}/compras/{$purchase->id}") + ->assertOk() + ->assertJsonPath('data.tickets_count', 2) + ->assertJsonPath('data.has_generated_tickets', true); + $purchase->markAsPaid(); $this->assertDatabaseCount('tickets', 2); @@ -209,6 +215,7 @@ class TicketGeneratorServiceTest extends TestCase $purchase->markAsPaid(); $this->assertDatabaseHas('tickets', [ + 'source_purchase_id' => $purchase->id, 'source_catalog_item_id' => $item->id, 'source_variant_id' => $variant->id, ]); @@ -226,6 +233,12 @@ class TicketGeneratorServiceTest extends TestCase $this->assertSame(Purchase::STATUS_PAID, $purchase->status); $this->assertDatabaseCount('tickets', 0); Event::assertNotDispatched(TicketsAvailable::class); + + $this->actingAs($this->user, 'sanctum') + ->getJson("/api/tenants/{$this->tenant->codigo}/compras/{$purchase->id}") + ->assertOk() + ->assertJsonPath('data.tickets_count', 0) + ->assertJsonPath('data.has_generated_tickets', false); } public function test_paid_status_is_confirmed_when_ticket_maximum_use_date_was_reached(): void