diff --git a/app/Domains/Sale/Controllers/AdminApp/SaleController.php b/app/Domains/Sale/Controllers/AdminApp/SaleController.php index be6d562..d02db6f 100644 --- a/app/Domains/Sale/Controllers/AdminApp/SaleController.php +++ b/app/Domains/Sale/Controllers/AdminApp/SaleController.php @@ -6,6 +6,7 @@ use App\Domains\Sale\Requests\AdminAppSaleIndexRequest; use App\Domains\Sale\Resources\AdminApp\SaleDetailResource; use App\Domains\Sale\Resources\AdminApp\SaleModificationResource; use App\Domains\Sale\Resources\AdminApp\SaleResource; +use App\Domains\Sale\Resources\AdminApp\SaleTicketResource; use App\Domains\Sale\Services\AdminAppSalePdfService; use App\Domains\Sale\Services\AdminAppSaleService; use App\Http\Controllers\Controller; @@ -38,6 +39,15 @@ class SaleController extends Controller return new SaleDetailResource($this->saleService->detail($tenant, $sale)); } + public function tickets(Request $request, int $sale): AnonymousResourceCollection + { + $tenant = $request->user()->tenant()->firstOrFail(); + + return SaleTicketResource::collection( + $this->saleService->tickets($tenant, $sale) + ); + } + public function confirm(Request $request, int $sale): SaleResource { $tenant = $request->user()->tenant()->firstOrFail(); diff --git a/app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php b/app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php new file mode 100644 index 0000000..5070b1a --- /dev/null +++ b/app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php @@ -0,0 +1,22 @@ + */ + public function toArray(Request $request): array + { + return [ + 'product' => $this->name, + 'id' => $this->id, + 'expires_at' => $this->getEffectiveExpiresAt(), + 'status' => $this->status, + ]; + } +} diff --git a/app/Domains/Sale/Services/AdminAppSaleService.php b/app/Domains/Sale/Services/AdminAppSaleService.php index 9c8918e..35c05e5 100644 --- a/app/Domains/Sale/Services/AdminAppSaleService.php +++ b/app/Domains/Sale/Services/AdminAppSaleService.php @@ -6,6 +6,7 @@ use App\Domains\Logging\Models\ValueChange; use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Services\CheckoutService; use App\Domains\Tenant\Models\Tenant; +use App\Domains\Ticket\Models\Ticket; use Illuminate\Database\Eloquent\Builder; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; @@ -52,6 +53,16 @@ class AdminAppSaleService ->findOrFail($saleId); } + /** @return Collection */ + public function tickets(Tenant $tenant, int $saleId): Collection + { + return $this->findForTenant($tenant, $saleId) + ->tickets() + ->with('validityTime') + ->orderBy('id') + ->get(); + } + public function confirm(Tenant $tenant, int $saleId): Purchase { $sale = $this->findForTenant($tenant, $saleId); diff --git a/app/Domains/Sale/routes/adminapp.php b/app/Domains/Sale/routes/adminapp.php index 5136ecb..b803f1e 100644 --- a/app/Domains/Sale/routes/adminapp.php +++ b/app/Domains/Sale/routes/adminapp.php @@ -12,5 +12,6 @@ Route::prefix('v1/adminapp/tenant') Route::get('sales/modifications/pdf', [SaleController::class, 'downloadModificationsPdf']); Route::post('sales/{sale}/confirm', [SaleController::class, 'confirm'])->whereNumber('sale'); Route::post('sales/{sale}/cancel', [SaleController::class, 'cancel'])->whereNumber('sale'); + Route::get('sales/{sale}/tickets', [SaleController::class, 'tickets'])->whereNumber('sale'); Route::get('sales/{sale}', [SaleController::class, 'show'])->whereNumber('sale'); }); diff --git a/app/Domains/Ticket/Models/Ticket.php b/app/Domains/Ticket/Models/Ticket.php index 2c19f00..5c370e0 100644 --- a/app/Domains/Ticket/Models/Ticket.php +++ b/app/Domains/Ticket/Models/Ticket.php @@ -30,12 +30,19 @@ class Ticket extends Model { use HasFactory; + public const STATUS_ACTIVE = 'active'; + + public const STATUS_EXPIRED = 'expired'; + + public const STATUS_USED = 'used'; + public $timestamps = false; protected $appends = [ 'is_valid', 'is_expired', 'is_used', + 'status', ]; protected function casts(): array @@ -121,6 +128,19 @@ class Ticket extends Model return $this->used_at !== null; } + public function getStatusAttribute(): string + { + if ($this->is_used) { + return self::STATUS_USED; + } + + if ($this->is_expired) { + return self::STATUS_EXPIRED; + } + + return self::STATUS_ACTIVE; + } + public function getEffectiveStartsAt(): ?CarbonInterface { return $this->validityTime?->startsAt(); diff --git a/tests/Feature/Sale/AdminAppSaleControllerTest.php b/tests/Feature/Sale/AdminAppSaleControllerTest.php index 98bf13b..bf22af0 100644 --- a/tests/Feature/Sale/AdminAppSaleControllerTest.php +++ b/tests/Feature/Sale/AdminAppSaleControllerTest.php @@ -10,6 +10,7 @@ use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\PurchaseItem; use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Ticket\Models\Ticket; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; @@ -85,6 +86,71 @@ class AdminAppSaleControllerTest extends TestCase ->assertNotFound(); } + public function test_an_adminapp_user_can_read_all_tickets_from_a_sale_in_its_tenant(): void + { + $tenant = $this->createTenant('acme'); + $admin = $this->createAdminAppUser($tenant); + Sanctum::actingAs($admin); + + $purchase = Purchase::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'status' => Purchase::STATUS_PAID, + 'total' => '20000.00', + ]); + $firstTicket = Ticket::query()->create([ + 'tenant_code' => $tenant->codigo, + 'ticket' => '11111111-1111-4111-8111-111111111111', + 'name' => 'Abono general', + 'description' => 'Acceso general', + 'source_purchase_id' => $purchase->id, + 'user_id' => $admin->id, + ]); + $usedTicket = Ticket::query()->create([ + 'tenant_code' => $tenant->codigo, + 'ticket' => '22222222-2222-4222-8222-222222222222', + 'name' => 'Abono general', + 'description' => 'Acceso general', + 'source_purchase_id' => $purchase->id, + 'used_at' => now()->subMinute(), + 'user_id' => $admin->id, + ]); + + $this->getJson("/api/v1/adminapp/tenant/sales/{$purchase->id}/tickets") + ->assertOk() + ->assertExactJson([ + 'data' => [ + [ + 'product' => 'Abono general', + 'id' => $firstTicket->id, + 'expires_at' => null, + 'status' => Ticket::STATUS_ACTIVE, + ], + [ + 'product' => 'Abono general', + 'id' => $usedTicket->id, + 'expires_at' => null, + 'status' => Ticket::STATUS_USED, + ], + ], + ]); + } + + public function test_an_adminapp_user_cannot_read_tickets_from_another_tenant_sale(): void + { + $tenant = $this->createTenant('acme'); + $otherTenant = $this->createTenant('other'); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $foreignPurchase = Purchase::query()->create([ + 'tenant_codigo' => $otherTenant->codigo, + 'status' => Purchase::STATUS_PAID, + 'total' => '10000.00', + ]); + + $this->getJson("/api/v1/adminapp/tenant/sales/{$foreignPurchase->id}/tickets") + ->assertNotFound(); + } + public function test_an_adminapp_user_can_confirm_a_pending_sale(): void { Event::fake([PurchasePaid::class]); diff --git a/tests/Unit/Ticket/TicketTest.php b/tests/Unit/Ticket/TicketTest.php index 28ab6d8..8db6f51 100644 --- a/tests/Unit/Ticket/TicketTest.php +++ b/tests/Unit/Ticket/TicketTest.php @@ -51,7 +51,10 @@ class TicketTest extends TestCase public function test_unused_ticket_without_validity_time_is_valid(): void { - $this->assertTrue((new Ticket)->isValid()); + $ticket = new Ticket; + + $this->assertTrue($ticket->isValid()); + $this->assertSame(Ticket::STATUS_ACTIVE, $ticket->status); } public function test_fixed_window_controls_ticket_validity(): void @@ -92,6 +95,7 @@ class TicketTest extends TestCase $this->assertFalse($ticket->isValid()); $this->assertTrue($ticket->is_expired); + $this->assertSame(Ticket::STATUS_EXPIRED, $ticket->status); } public function test_used_ticket_is_invalid_and_not_reported_as_expired(): void @@ -105,6 +109,7 @@ class TicketTest extends TestCase $this->assertFalse($ticket->is_valid); $this->assertFalse($ticket->is_expired); $this->assertTrue($ticket->is_used); + $this->assertSame(Ticket::STATUS_USED, $ticket->status); } private function ticketWithValidityTime(ValidityTime $validityTime): Ticket