feat(scan): implement scan attempt tracking and validation logic
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Domains\Authorization\Enums\RoleCode;
|
|||||||
use App\Domains\Authorization\Models\Role;
|
use App\Domains\Authorization\Models\Role;
|
||||||
use App\Domains\Catalog\Models\Category;
|
use App\Domains\Catalog\Models\Category;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Models\ScanAttempt;
|
||||||
use Database\Factories\UserFactory;
|
use Database\Factories\UserFactory;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
@@ -46,6 +47,12 @@ class User extends Authenticatable
|
|||||||
return $this->hasMany(LoginAttempt::class);
|
return $this->hasMany(LoginAttempt::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<ScanAttempt, $this> */
|
||||||
|
public function scanAttempts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ScanAttempt::class, 'scanner_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Role, $this>
|
* @return BelongsTo<Role, $this>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Domains\Event\Models\EventDate;
|
|||||||
use App\Domains\Menu\Models\Menu;
|
use App\Domains\Menu\Models\Menu;
|
||||||
use App\Domains\Menu\Models\TenantMenu;
|
use App\Domains\Menu\Models\TenantMenu;
|
||||||
use App\Domains\Tenant\Enums\CartEditingPolicy;
|
use App\Domains\Tenant\Enums\CartEditingPolicy;
|
||||||
|
use App\Domains\Ticket\Models\ScanAttempt;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -195,6 +196,12 @@ class Tenant extends Model
|
|||||||
return $this->hasMany(Category::class, 'tenant_code', 'codigo');
|
return $this->hasMany(Category::class, 'tenant_code', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<ScanAttempt, $this> */
|
||||||
|
public function scanAttempts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ScanAttempt::class, 'tenant_code', 'codigo');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<SocialMedia, $this>
|
* @return BelongsToMany<SocialMedia, $this>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Domains\Ticket\Controllers\Scanner;
|
|||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
use App\Domains\Auth\Models\User;
|
||||||
use App\Domains\Ticket\Requests\ScannerTicketIndexRequest;
|
use App\Domains\Ticket\Requests\ScannerTicketIndexRequest;
|
||||||
use App\Domains\Ticket\Requests\ScanTicketRequest;
|
|
||||||
use App\Domains\Ticket\Resources\Scanner\ScannedTicketResource;
|
use App\Domains\Ticket\Resources\Scanner\ScannedTicketResource;
|
||||||
use App\Domains\Ticket\Resources\TicketResource;
|
use App\Domains\Ticket\Resources\TicketResource;
|
||||||
use App\Domains\Ticket\Services\ScannerTicketService;
|
use App\Domains\Ticket\Services\ScannerTicketService;
|
||||||
@@ -36,13 +35,13 @@ class TicketController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scan(ScanTicketRequest $request): TicketResource
|
public function scan(Request $request): TicketResource
|
||||||
{
|
{
|
||||||
/** @var User $scanner */
|
/** @var User $scanner */
|
||||||
$scanner = $request->user();
|
$scanner = $request->user();
|
||||||
|
|
||||||
return TicketResource::make(
|
return TicketResource::make(
|
||||||
$this->ticketService->scan($scanner, $request->validated('data'))
|
$this->ticketService->scan($scanner, $request->input('data'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/Domains/Ticket/Enums/ScanAttemptResult.php
Normal file
16
app/Domains/Ticket/Enums/ScanAttemptResult.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticket\Enums;
|
||||||
|
|
||||||
|
enum ScanAttemptResult: string
|
||||||
|
{
|
||||||
|
case Processing = 'processing';
|
||||||
|
case Accepted = 'accepted';
|
||||||
|
case InvalidQr = 'invalid_qr';
|
||||||
|
case TicketNotFound = 'ticket_not_found';
|
||||||
|
case CategoryForbidden = 'category_forbidden';
|
||||||
|
case AlreadyScanned = 'already_scanned';
|
||||||
|
case Expired = 'expired';
|
||||||
|
case NotValid = 'not_valid';
|
||||||
|
case UnexpectedError = 'unexpected_error';
|
||||||
|
}
|
||||||
52
app/Domains/Ticket/Models/ScanAttempt.php
Normal file
52
app/Domains/Ticket/Models/ScanAttempt.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticket\Models;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Enums\ScanAttemptResult;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'tenant_code',
|
||||||
|
'scanner_user_id',
|
||||||
|
'ticket_id',
|
||||||
|
'data',
|
||||||
|
'result',
|
||||||
|
'resolved_at',
|
||||||
|
])]
|
||||||
|
class ScanAttempt extends Model
|
||||||
|
{
|
||||||
|
public const UPDATED_AT = null;
|
||||||
|
|
||||||
|
/** @return BelongsTo<User, $this> */
|
||||||
|
public function scanner(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'scanner_user_id')->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Ticket, $this> */
|
||||||
|
public function ticket(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Ticket::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Tenant, $this> */
|
||||||
|
public function tenant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'scanner_user_id' => 'integer',
|
||||||
|
'ticket_id' => 'integer',
|
||||||
|
'result' => ScanAttemptResult::class,
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'resolved_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
@@ -81,6 +82,12 @@ class Ticket extends Model
|
|||||||
return $this->belongsTo(User::class, 'scanner_user_id')->withTrashed();
|
return $this->belongsTo(User::class, 'scanner_user_id')->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<ScanAttempt, $this> */
|
||||||
|
public function scanAttempts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ScanAttempt::class);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<PurchaseItem, $this> */
|
/** @return BelongsTo<PurchaseItem, $this> */
|
||||||
public function sourcePurchaseItem(): BelongsTo
|
public function sourcePurchaseItem(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Ticket\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class ScanTicketRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, list<string>> */
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'data' => ['required', 'uuid'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, string> */
|
|
||||||
public function messages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'data.required' => 'El QR proporcionado es inválido.',
|
|
||||||
'data.uuid' => 'El QR proporcionado es inválido.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,14 +3,21 @@
|
|||||||
namespace App\Domains\Ticket\Services;
|
namespace App\Domains\Ticket\Services;
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Ticket\Enums\ScanAttemptResult;
|
||||||
|
use App\Domains\Ticket\Models\ScanAttempt;
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class ScannerTicketService
|
class ScannerTicketService
|
||||||
{
|
{
|
||||||
|
private const INVALID_QR_MESSAGE = 'El QR proporcionado es inválido.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{q?: string|null, page?: int, per_page?: int} $filters
|
* @param array{q?: string|null, page?: int, per_page?: int} $filters
|
||||||
* @return LengthAwarePaginator<Ticket>
|
* @return LengthAwarePaginator<Ticket>
|
||||||
@@ -90,42 +97,117 @@ class ScannerTicketService
|
|||||||
return $query->firstOrFail();
|
return $query->firstOrFail();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scan(User $scanner, string $ticketUuid): Ticket
|
public function scan(User $scanner, mixed $scannedData): Ticket
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($scanner, $ticketUuid): Ticket {
|
$scanAttempt = ScanAttempt::query()->create([
|
||||||
$ticket = $this->baseQuery()
|
'tenant_code' => $scanner->tenant_codigo,
|
||||||
->where('tenant_code', $scanner->tenant_codigo)
|
'scanner_user_id' => $scanner->getKey(),
|
||||||
->where('ticket', $ticketUuid)
|
'data' => $this->serializeScannedData($scannedData),
|
||||||
->lockForUpdate()
|
'result' => ScanAttemptResult::Processing,
|
||||||
->firstOrFail();
|
]);
|
||||||
|
|
||||||
if (! $this->scannerCanScan($scanner, $ticket)) {
|
if (! is_string($scannedData) || ! Str::isUuid($scannedData)) {
|
||||||
throw ValidationException::withMessages([
|
$this->resolveScanAttempt($scanAttempt, ScanAttemptResult::InvalidQr);
|
||||||
'ticket' => __('api.ticket.scanner_category_forbidden'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ticket->is_used) {
|
throw ValidationException::withMessages([
|
||||||
throw ValidationException::withMessages([
|
'data' => self::INVALID_QR_MESSAGE,
|
||||||
'ticket' => __('api.ticket.already_scanned'),
|
]);
|
||||||
]);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (! $ticket->is_valid) {
|
$ticketId = null;
|
||||||
throw ValidationException::withMessages([
|
$failureResult = null;
|
||||||
'ticket' => $ticket->is_expired
|
|
||||||
? __('api.ticket.expired_for_scan')
|
|
||||||
: __('api.ticket.not_valid_for_scan'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ticket->forceFill([
|
try {
|
||||||
'used_at' => now(),
|
return DB::transaction(function () use (
|
||||||
'scanner_user_id' => $scanner->getKey(),
|
$scanner,
|
||||||
])->save();
|
$scannedData,
|
||||||
|
$scanAttempt,
|
||||||
|
&$ticketId,
|
||||||
|
&$failureResult,
|
||||||
|
): Ticket {
|
||||||
|
$ticket = $this->baseQuery()
|
||||||
|
->where('tenant_code', $scanner->tenant_codigo)
|
||||||
|
->where('ticket', $scannedData)
|
||||||
|
->lockForUpdate()
|
||||||
|
->firstOrFail();
|
||||||
|
$ticketId = (int) $ticket->getKey();
|
||||||
|
|
||||||
return $ticket->refresh()->load($this->relations());
|
if (! $this->scannerCanScan($scanner, $ticket)) {
|
||||||
});
|
$failureResult = ScanAttemptResult::CategoryForbidden;
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'ticket' => __('api.ticket.scanner_category_forbidden'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ticket->is_used) {
|
||||||
|
$failureResult = ScanAttemptResult::AlreadyScanned;
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'ticket' => __('api.ticket.already_scanned'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $ticket->is_valid) {
|
||||||
|
$failureResult = $ticket->is_expired
|
||||||
|
? ScanAttemptResult::Expired
|
||||||
|
: ScanAttemptResult::NotValid;
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'ticket' => $ticket->is_expired
|
||||||
|
? __('api.ticket.expired_for_scan')
|
||||||
|
: __('api.ticket.not_valid_for_scan'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket->forceFill([
|
||||||
|
'used_at' => now(),
|
||||||
|
'scanner_user_id' => $scanner->getKey(),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$this->resolveScanAttempt(
|
||||||
|
$scanAttempt,
|
||||||
|
ScanAttemptResult::Accepted,
|
||||||
|
$ticketId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $ticket->refresh()->load($this->relations());
|
||||||
|
});
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$result = $exception instanceof ModelNotFoundException
|
||||||
|
? ScanAttemptResult::TicketNotFound
|
||||||
|
: ($failureResult ?? ScanAttemptResult::UnexpectedError);
|
||||||
|
|
||||||
|
$this->resolveScanAttempt($scanAttempt, $result, $ticketId);
|
||||||
|
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveScanAttempt(
|
||||||
|
ScanAttempt $scanAttempt,
|
||||||
|
ScanAttemptResult $result,
|
||||||
|
?int $ticketId = null,
|
||||||
|
): void {
|
||||||
|
$scanAttempt->forceFill([
|
||||||
|
'ticket_id' => $ticketId,
|
||||||
|
'result' => $result,
|
||||||
|
'resolved_at' => now(),
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function serializeScannedData(mixed $scannedData): ?string
|
||||||
|
{
|
||||||
|
if ($scannedData === null || is_string($scannedData)) {
|
||||||
|
return $scannedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encoded = json_encode(
|
||||||
|
$scannedData,
|
||||||
|
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $encoded === false ? get_debug_type($scannedData) : $encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return Builder<Ticket> */
|
/** @return Builder<Ticket> */
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('scan_attempts', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->foreignId('scanner_user_id')
|
||||||
|
->nullable()
|
||||||
|
->constrained('users')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->nullOnDelete();
|
||||||
|
$table->foreignId('ticket_id')
|
||||||
|
->nullable()
|
||||||
|
->constrained('tickets')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->nullOnDelete();
|
||||||
|
$table->text('data')->nullable();
|
||||||
|
$table->string('result', 32);
|
||||||
|
$table->timestamp('resolved_at')->nullable();
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
|
||||||
|
$table->foreign('tenant_code')
|
||||||
|
->references('codigo')
|
||||||
|
->on('tenants')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->restrictOnDelete();
|
||||||
|
|
||||||
|
$table->index(['scanner_user_id', 'created_at']);
|
||||||
|
$table->index(['tenant_code', 'created_at']);
|
||||||
|
$table->index(['ticket_id', 'created_at']);
|
||||||
|
$table->index(['tenant_code', 'result', 'created_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('scan_attempts');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,6 +10,8 @@ use App\Domains\Catalog\Models\CatalogItem;
|
|||||||
use App\Domains\Catalog\Models\Category;
|
use App\Domains\Catalog\Models\Category;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Tenant\Models\WebsiteType;
|
use App\Domains\Tenant\Models\WebsiteType;
|
||||||
|
use App\Domains\Ticket\Enums\ScanAttemptResult;
|
||||||
|
use App\Domains\Ticket\Models\ScanAttempt;
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use Database\Seeders\AuthorizationSeeder;
|
use Database\Seeders\AuthorizationSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@@ -215,6 +217,14 @@ class ScannerTicketControllerTest extends TestCase
|
|||||||
'scanner_user_id' => $this->scanner->id,
|
'scanner_user_id' => $this->scanner->id,
|
||||||
]);
|
]);
|
||||||
$this->assertNotNull($ticket->fresh()->used_at);
|
$this->assertNotNull($ticket->fresh()->used_at);
|
||||||
|
|
||||||
|
$scanAttempt = ScanAttempt::query()->sole();
|
||||||
|
$this->assertSame($ticket->ticket, $scanAttempt->data);
|
||||||
|
$this->assertSame(ScanAttemptResult::Accepted, $scanAttempt->result);
|
||||||
|
$this->assertTrue($scanAttempt->scanner->is($this->scanner));
|
||||||
|
$this->assertTrue($scanAttempt->ticket->is($ticket));
|
||||||
|
$this->assertTrue($scanAttempt->tenant->is($this->tenant));
|
||||||
|
$this->assertNotNull($scanAttempt->resolved_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_scan_requires_a_uuid_in_the_data_field(): void
|
public function test_scan_requires_a_uuid_in_the_data_field(): void
|
||||||
@@ -230,6 +240,16 @@ class ScannerTicketControllerTest extends TestCase
|
|||||||
->assertUnprocessable()
|
->assertUnprocessable()
|
||||||
->assertJsonPath('message', 'El QR proporcionado es inválido.')
|
->assertJsonPath('message', 'El QR proporcionado es inválido.')
|
||||||
->assertJsonPath('errors.data.0', 'El QR proporcionado es inválido.');
|
->assertJsonPath('errors.data.0', 'El QR proporcionado es inválido.');
|
||||||
|
|
||||||
|
$scanAttempts = ScanAttempt::query()->orderBy('id')->get();
|
||||||
|
$this->assertCount(2, $scanAttempts);
|
||||||
|
$this->assertNull($scanAttempts[0]->data);
|
||||||
|
$this->assertSame('not-a-uuid', $scanAttempts[1]->data);
|
||||||
|
$this->assertTrue($scanAttempts->every(
|
||||||
|
fn (ScanAttempt $scanAttempt): bool => $scanAttempt->result === ScanAttemptResult::InvalidQr
|
||||||
|
&& $scanAttempt->scanner_user_id === $this->scanner->id
|
||||||
|
&& $scanAttempt->resolved_at !== null
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_ticket_cannot_be_scanned_twice(): void
|
public function test_ticket_cannot_be_scanned_twice(): void
|
||||||
@@ -249,6 +269,11 @@ class ScannerTicketControllerTest extends TestCase
|
|||||||
->assertUnprocessable()
|
->assertUnprocessable()
|
||||||
->assertJsonValidationErrors('ticket');
|
->assertJsonValidationErrors('ticket');
|
||||||
$this->assertSame($this->scanner->id, $ticket->fresh()->scanner_user_id);
|
$this->assertSame($this->scanner->id, $ticket->fresh()->scanner_user_id);
|
||||||
|
$this->assertDatabaseHas('scan_attempts', [
|
||||||
|
'scanner_user_id' => $otherScanner->id,
|
||||||
|
'ticket_id' => $ticket->id,
|
||||||
|
'result' => ScanAttemptResult::AlreadyScanned->value,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_scanner_cannot_scan_a_ticket_from_an_unassigned_category(): void
|
public function test_scanner_cannot_scan_a_ticket_from_an_unassigned_category(): void
|
||||||
@@ -268,6 +293,11 @@ class ScannerTicketControllerTest extends TestCase
|
|||||||
->assertUnprocessable()
|
->assertUnprocessable()
|
||||||
->assertJsonValidationErrors('ticket');
|
->assertJsonValidationErrors('ticket');
|
||||||
$this->assertNull($ticket->fresh()->used_at);
|
$this->assertNull($ticket->fresh()->used_at);
|
||||||
|
$this->assertDatabaseHas('scan_attempts', [
|
||||||
|
'scanner_user_id' => $this->scanner->id,
|
||||||
|
'ticket_id' => $ticket->id,
|
||||||
|
'result' => ScanAttemptResult::CategoryForbidden->value,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_scanner_can_read_and_scan_any_category_when_tenant_disables_validation(): void
|
public function test_scanner_can_read_and_scan_any_category_when_tenant_disables_validation(): void
|
||||||
@@ -314,6 +344,13 @@ class ScannerTicketControllerTest extends TestCase
|
|||||||
->assertNotFound();
|
->assertNotFound();
|
||||||
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $foreignTicket->ticket])
|
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $foreignTicket->ticket])
|
||||||
->assertNotFound();
|
->assertNotFound();
|
||||||
|
$this->assertDatabaseHas('scan_attempts', [
|
||||||
|
'tenant_code' => $this->tenant->codigo,
|
||||||
|
'scanner_user_id' => $this->scanner->id,
|
||||||
|
'ticket_id' => null,
|
||||||
|
'data' => $foreignTicket->ticket,
|
||||||
|
'result' => ScanAttemptResult::TicketNotFound->value,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_adminapp_cannot_access_scanner_routes_even_with_scan_permission(): void
|
public function test_adminapp_cannot_access_scanner_routes_even_with_scan_permission(): void
|
||||||
@@ -330,6 +367,7 @@ class ScannerTicketControllerTest extends TestCase
|
|||||||
$this->getJson("/api/v1/scanner/tickets/{$ticket->ticket}")->assertForbidden();
|
$this->getJson("/api/v1/scanner/tickets/{$ticket->ticket}")->assertForbidden();
|
||||||
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket])->assertForbidden();
|
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket])->assertForbidden();
|
||||||
$this->assertNull($ticket->fresh()->used_at);
|
$this->assertNull($ticket->fresh()->used_at);
|
||||||
|
$this->assertDatabaseCount('scan_attempts', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $attributes */
|
/** @param array<string, mixed> $attributes */
|
||||||
|
|||||||
Reference in New Issue
Block a user