Compare commits
42 Commits
1d7c484510
...
adaptacion
| Author | SHA1 | Date | |
|---|---|---|---|
| 5197862a62 | |||
| 2115d2384d | |||
| a458c564ca | |||
| 68dd373d61 | |||
| 8d8e49fcfb | |||
| 19b506a465 | |||
| 6f63a87af5 | |||
| a59b96dd22 | |||
| 6354e26620 | |||
| 69320c54d4 | |||
| 5bfc8b9f70 | |||
| 17f4cb6502 | |||
| 6844ab7af9 | |||
| 8542c03466 | |||
| e7c8807a67 | |||
| c6351d706f | |||
| 2693df2cbf | |||
| 4aaa66dc38 | |||
| 1cd60f7021 | |||
| ca605127d0 | |||
| 00489c14d3 | |||
| 63f98b5da2 | |||
| b05314b1dd | |||
| feed3205eb | |||
| 11776f0734 | |||
| e1ad27ecf0 | |||
| 6a6d3690cc | |||
| ed4502425a | |||
| 6318a99328 | |||
| 21b70777f2 | |||
| 91af233941 | |||
| 717ee5d194 | |||
| 14d53d1e0b | |||
| 7b7efeb4cc | |||
| 60b8415c59 | |||
| f578c6e24a | |||
| 5ad61eb217 | |||
| 20dd246cd8 | |||
| 7561ba756c | |||
| b224dd8650 | |||
| 448ffb4102 | |||
| 7f01adab73 |
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Auth\Controllers;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Requests\ScannerLoginRequest;
|
|
||||||
use App\Domains\Auth\Resources\UserResource;
|
|
||||||
use App\Domains\Auth\Services\PasswordLoginService;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
|
|
||||||
class ScannerLoginController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly PasswordLoginService $passwordLoginService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function __invoke(ScannerLoginRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$credentials = $request->validated();
|
|
||||||
$user = $this->passwordLoginService->authenticateScanner(
|
|
||||||
$credentials['email'],
|
|
||||||
$credentials['password'],
|
|
||||||
$request->ip(),
|
|
||||||
$request->userAgent(),
|
|
||||||
);
|
|
||||||
|
|
||||||
$expirationMinutes = (int) config('sanctum.expiration');
|
|
||||||
$token = $user->createToken(
|
|
||||||
'scanner-token',
|
|
||||||
['scanner'],
|
|
||||||
now()->addMinutes($expirationMinutes),
|
|
||||||
)->plainTextToken;
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'code' => 'auth.login_success',
|
|
||||||
'message' => __('api.auth.login_success'),
|
|
||||||
'token' => $token,
|
|
||||||
'token_type' => 'Bearer',
|
|
||||||
'user' => UserResource::make($user),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Auth\Controllers;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
use App\Domains\Auth\Resources\ScannerMeResource;
|
|
||||||
use App\Domains\Auth\Services\ScannerContextService;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ScannerMeController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly ScannerContextService $scannerContextService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function __invoke(Request $request): ScannerMeResource
|
|
||||||
{
|
|
||||||
/** @var User $user */
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
return ScannerMeResource::make($this->scannerContextService->load($user));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -53,16 +53,6 @@ class User extends Authenticatable
|
|||||||
return $this->belongsTo(Role::class, 'rol_codigo', 'codigo');
|
return $this->belongsTo(Role::class, 'rol_codigo', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasPermission(string $permissionCode): bool
|
|
||||||
{
|
|
||||||
return $this->role()
|
|
||||||
->whereHas(
|
|
||||||
'permissions',
|
|
||||||
fn ($query) => $query->where('permisos.codigo', $permissionCode)
|
|
||||||
)
|
|
||||||
->exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Tenant, $this>
|
* @return BelongsTo<Tenant, $this>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Auth\Requests;
|
|
||||||
|
|
||||||
class ScannerLoginRequest extends AdminAppLoginRequest {}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Auth\Resources;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
use App\Domains\Tenant\Resources\TenantResource;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
/** @mixin User */
|
|
||||||
class ScannerMeResource extends JsonResource
|
|
||||||
{
|
|
||||||
/** @return array<string, mixed> */
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'user' => UserResource::make($this->resource),
|
|
||||||
'tenant' => TenantResource::make($this->tenant),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ namespace App\Domains\Auth\Services;
|
|||||||
use App\Domains\Auth\Exceptions\AccountLockedException;
|
use App\Domains\Auth\Exceptions\AccountLockedException;
|
||||||
use App\Domains\Auth\Models\LoginAttempt;
|
use App\Domains\Auth\Models\LoginAttempt;
|
||||||
use App\Domains\Auth\Models\User;
|
use App\Domains\Auth\Models\User;
|
||||||
use App\Domains\Authorization\Enums\PermissionCode;
|
|
||||||
use App\Domains\Authorization\Enums\RoleCode;
|
use App\Domains\Authorization\Enums\RoleCode;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -63,39 +62,14 @@ class PasswordLoginService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate a tenant-bound user authorized to scan tickets.
|
|
||||||
*
|
|
||||||
* @throws AccountLockedException
|
|
||||||
* @throws ValidationException
|
|
||||||
*/
|
|
||||||
public function authenticateScanner(
|
|
||||||
string $email,
|
|
||||||
string $password,
|
|
||||||
?string $ipAddress,
|
|
||||||
?string $userAgent,
|
|
||||||
): User {
|
|
||||||
return $this->authenticateUser(
|
|
||||||
$email,
|
|
||||||
$password,
|
|
||||||
null,
|
|
||||||
$ipAddress,
|
|
||||||
$userAgent,
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
PermissionCode::ScanTickets->value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function authenticateUser(
|
private function authenticateUser(
|
||||||
string $email,
|
string $email,
|
||||||
string $password,
|
string $password,
|
||||||
?string $tenantCode,
|
?string $tenantCode,
|
||||||
?string $ipAddress,
|
?string $ipAddress,
|
||||||
?string $userAgent,
|
?string $userAgent,
|
||||||
?RoleCode $requiredRole = RoleCode::User,
|
RoleCode $requiredRole = RoleCode::User,
|
||||||
bool $requiresTenant = false,
|
bool $requiresTenant = false,
|
||||||
?string $requiredPermission = null,
|
|
||||||
): User {
|
): User {
|
||||||
$normalizedEmail = mb_strtolower(trim($email));
|
$normalizedEmail = mb_strtolower(trim($email));
|
||||||
$now = CarbonImmutable::now();
|
$now = CarbonImmutable::now();
|
||||||
@@ -110,21 +84,10 @@ class PasswordLoginService
|
|||||||
$now,
|
$now,
|
||||||
$requiredRole,
|
$requiredRole,
|
||||||
$requiresTenant,
|
$requiresTenant,
|
||||||
$requiredPermission,
|
|
||||||
): array {
|
): array {
|
||||||
$user = User::query()
|
$user = User::query()
|
||||||
->where('email', $normalizedEmail)
|
->where('email', $normalizedEmail)
|
||||||
->when(
|
->where('rol_codigo', $requiredRole->value)
|
||||||
$requiredRole !== null,
|
|
||||||
fn ($query) => $query->where('rol_codigo', $requiredRole->value),
|
|
||||||
)
|
|
||||||
->when(
|
|
||||||
$requiredPermission !== null,
|
|
||||||
fn ($query) => $query->whereHas(
|
|
||||||
'role.permissions',
|
|
||||||
fn ($query) => $query->where('permisos.codigo', $requiredPermission)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
->when(
|
->when(
|
||||||
$requiresTenant,
|
$requiresTenant,
|
||||||
fn ($query) => $query->whereNotNull('tenant_codigo'),
|
fn ($query) => $query->whereNotNull('tenant_codigo'),
|
||||||
@@ -159,8 +122,8 @@ class PasswordLoginService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($user === null || ! Hash::check($password, $user->password)) {
|
if ($user === null || ! Hash::check($password, $user->password)) {
|
||||||
if ($user !== null && $attemptTenantCode !== null) {
|
if ($user !== null) {
|
||||||
$this->registerFailure($user, $now, $attemptTenantCode);
|
$this->registerFailure($user, $now, $tenantCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
$outcome = $user?->locked_until?->isFuture()
|
$outcome = $user?->locked_until?->isFuture()
|
||||||
@@ -247,7 +210,7 @@ class PasswordLoginService
|
|||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::error('Failed to trigger reset password on account lock', [
|
Log::error('Failed to trigger reset password on account lock', [
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'exception' => $e,
|
'exception' => $e
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Auth\Services;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
|
|
||||||
class ScannerContextService
|
|
||||||
{
|
|
||||||
public function load(User $user): User
|
|
||||||
{
|
|
||||||
$tenant = $user->tenant()
|
|
||||||
->with([
|
|
||||||
'menues' => fn ($query) => $query->whereHas(
|
|
||||||
'roles',
|
|
||||||
fn ($query) => $query->where('codigo', $user->rol_codigo)
|
|
||||||
),
|
|
||||||
])
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
$user->setRelation('tenant', $tenant);
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,4 +25,3 @@ Route::middleware('auth:sanctum')->get('/me', MeController::class);
|
|||||||
Route::middleware('auth:sanctum')->put('/me', UpdateProfileController::class);
|
Route::middleware('auth:sanctum')->put('/me', UpdateProfileController::class);
|
||||||
|
|
||||||
require __DIR__.'/adminapp.php';
|
require __DIR__.'/adminapp.php';
|
||||||
require __DIR__.'/scanner.php';
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Domains\Auth\Controllers\ScannerLoginController;
|
|
||||||
use App\Domains\Auth\Controllers\ScannerMeController;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
Route::prefix('v1/scanner')->group(function (): void {
|
|
||||||
Route::post('login', ScannerLoginController::class)->middleware('throttle:login');
|
|
||||||
Route::middleware(['auth:sanctum', 'scanner.tenant'])
|
|
||||||
->get('me', ScannerMeController::class);
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Authorization\Enums;
|
|
||||||
|
|
||||||
enum PermissionCode: string
|
|
||||||
{
|
|
||||||
case ScanTickets = 'tickets.escanear';
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Bootstrap\Controllers;
|
|
||||||
|
|
||||||
use App\Domains\Bootstrap\Requests\ScannerBootstrapRequest;
|
|
||||||
use App\Domains\Bootstrap\Resources\ScannerBootstrapResource;
|
|
||||||
use App\Domains\Bootstrap\Services\ScannerBootstrapService;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
class ScannerBootstrapController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(protected ScannerBootstrapService $bootstrapService) {}
|
|
||||||
|
|
||||||
public function __invoke(ScannerBootstrapRequest $request): ScannerBootstrapResource
|
|
||||||
{
|
|
||||||
return ScannerBootstrapResource::make(
|
|
||||||
$this->bootstrapService->get((string) $request->validated('dominio'))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Bootstrap\Requests;
|
|
||||||
|
|
||||||
class ScannerBootstrapRequest extends TenantBootstrapRequest {}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Bootstrap\Resources;
|
|
||||||
|
|
||||||
class ScannerBootstrapResource extends AdminAppBootstrapResource {}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Bootstrap\Services;
|
|
||||||
|
|
||||||
use App\Domains\Tenant\Models\WebsiteType;
|
|
||||||
|
|
||||||
class ScannerBootstrapService
|
|
||||||
{
|
|
||||||
/** @return array{website_type: WebsiteType} */
|
|
||||||
public function get(string $domain): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'website_type' => WebsiteType::query()
|
|
||||||
->with(['siteLogo', 'footerLogo'])
|
|
||||||
->where('scanner_domain', $domain)
|
|
||||||
->firstOrFail(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,4 +7,3 @@ Route::get('tenants/bootstrap/{dominio}', TenantBootstrapController::class)
|
|||||||
->where('dominio', '.*');
|
->where('dominio', '.*');
|
||||||
|
|
||||||
require __DIR__.'/adminapp.php';
|
require __DIR__.'/adminapp.php';
|
||||||
require __DIR__.'/scanner.php';
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Domains\Bootstrap\Controllers\ScannerBootstrapController;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
Route::get(
|
|
||||||
'v1/scanner/bootstrap/{dominio}',
|
|
||||||
ScannerBootstrapController::class
|
|
||||||
);
|
|
||||||
@@ -13,7 +13,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
'codigo',
|
'codigo',
|
||||||
'nombre',
|
'nombre',
|
||||||
'dominio',
|
'dominio',
|
||||||
'scanner_domain',
|
|
||||||
'primary_color',
|
'primary_color',
|
||||||
'secondary_color',
|
'secondary_color',
|
||||||
'danger_color',
|
'danger_color',
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Ticket\Controllers\Scanner;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
use App\Domains\Ticket\Requests\ScannerTicketIndexRequest;
|
|
||||||
use App\Domains\Ticket\Resources\Scanner\ScannedTicketResource;
|
|
||||||
use App\Domains\Ticket\Resources\TicketResource;
|
|
||||||
use App\Domains\Ticket\Services\ScannerTicketService;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
|
||||||
|
|
||||||
class TicketController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(private readonly ScannerTicketService $ticketService) {}
|
|
||||||
|
|
||||||
public function index(ScannerTicketIndexRequest $request): AnonymousResourceCollection
|
|
||||||
{
|
|
||||||
/** @var User $scanner */
|
|
||||||
$scanner = $request->user();
|
|
||||||
|
|
||||||
return ScannedTicketResource::collection(
|
|
||||||
$this->ticketService->scannedBy($scanner, $request->validated())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(Request $request, string $ticketUuid): TicketResource
|
|
||||||
{
|
|
||||||
/** @var User $scanner */
|
|
||||||
$scanner = $request->user();
|
|
||||||
|
|
||||||
return TicketResource::make(
|
|
||||||
$this->ticketService->detail($scanner, $ticketUuid)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scan(Request $request, string $ticketUuid): TicketResource
|
|
||||||
{
|
|
||||||
/** @var User $scanner */
|
|
||||||
$scanner = $request->user();
|
|
||||||
|
|
||||||
return TicketResource::make(
|
|
||||||
$this->ticketService->scan($scanner, $ticketUuid)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Ticket\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class ScannerTicketIndexRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, list<string>> */
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'q' => ['sometimes', 'nullable', 'string', 'max:255'],
|
|
||||||
'page' => ['sometimes', 'integer', 'min:1'],
|
|
||||||
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Ticket\Resources\Scanner;
|
|
||||||
|
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
/** @mixin Ticket */
|
|
||||||
class ScannedTicketResource extends JsonResource
|
|
||||||
{
|
|
||||||
/** @return array<string, mixed> */
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'product' => $this->name,
|
|
||||||
'id' => $this->id,
|
|
||||||
'ticket' => $this->ticket,
|
|
||||||
'used_at' => $this->used_at,
|
|
||||||
'expires_at' => $this->getEffectiveExpiresAt(),
|
|
||||||
'status' => $this->status,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,6 @@ class TicketResource extends JsonResource
|
|||||||
'ticket' => $this->ticket,
|
'ticket' => $this->ticket,
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'client' => $this->user?->nombre_apellido,
|
|
||||||
'category' => $this->sourceCatalogItem?->category?->nombre,
|
|
||||||
'source_catalog_item_id' => $this->source_catalog_item_id,
|
'source_catalog_item_id' => $this->source_catalog_item_id,
|
||||||
'source_variant_id' => $this->source_variant_id,
|
'source_variant_id' => $this->source_variant_id,
|
||||||
'validity_times' => ValidityTimeResource::collection($this->allValidityTimes()),
|
'validity_times' => ValidityTimeResource::collection($this->allValidityTimes()),
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Ticket\Services;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
|
|
||||||
class ScannerTicketService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param array{q?: string|null, page?: int, per_page?: int} $filters
|
|
||||||
* @return LengthAwarePaginator<Ticket>
|
|
||||||
*/
|
|
||||||
public function scannedBy(User $scanner, array $filters = []): LengthAwarePaginator
|
|
||||||
{
|
|
||||||
$search = trim((string) ($filters['q'] ?? ''));
|
|
||||||
|
|
||||||
return $this->baseQuery()
|
|
||||||
->where('tenant_code', $scanner->tenant_codigo)
|
|
||||||
->where('scanner_user_id', $scanner->getKey())
|
|
||||||
->when($search !== '', function (Builder $query) use ($search): void {
|
|
||||||
$usedAtDate = $this->parseSearchDate($search);
|
|
||||||
|
|
||||||
$query->where(function (Builder $searchQuery) use ($search, $usedAtDate): void {
|
|
||||||
$searchQuery->where('ticket', 'like', "%{$search}%");
|
|
||||||
|
|
||||||
if (ctype_digit($search)) {
|
|
||||||
$searchQuery->orWhere('id', (int) $search);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($usedAtDate !== null) {
|
|
||||||
$searchQuery->orWhereDate('used_at', $usedAtDate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
->orderByDesc('used_at')
|
|
||||||
->orderByDesc('id')
|
|
||||||
->paginateFromRequest()
|
|
||||||
->withQueryString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseSearchDate(string $search): ?string
|
|
||||||
{
|
|
||||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $search, $matches) === 1) {
|
|
||||||
[$year, $month, $day] = array_map('intval', array_slice($matches, 1));
|
|
||||||
|
|
||||||
if (checkdate($month, $day, $year)) {
|
|
||||||
return sprintf('%04d-%02d-%02d', $year, $month, $day);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^(\d{2})\/(\d{2})\/(\d{2}|\d{4})$/', $search, $matches) === 1) {
|
|
||||||
$day = (int) $matches[1];
|
|
||||||
$month = (int) $matches[2];
|
|
||||||
$year = (int) $matches[3];
|
|
||||||
$year = strlen($matches[3]) === 2 ? 2000 + $year : $year;
|
|
||||||
|
|
||||||
if (checkdate($month, $day, $year)) {
|
|
||||||
return sprintf('%04d-%02d-%02d', $year, $month, $day);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function detail(User $scanner, string $ticketUuid): Ticket
|
|
||||||
{
|
|
||||||
$categoryIds = $this->scannerCategoryIds($scanner);
|
|
||||||
|
|
||||||
return $this->baseQuery()
|
|
||||||
->where('tenant_code', $scanner->tenant_codigo)
|
|
||||||
->where('ticket', $ticketUuid)
|
|
||||||
->where(function (Builder $query) use ($scanner, $categoryIds): void {
|
|
||||||
$query
|
|
||||||
->where('scanner_user_id', $scanner->getKey())
|
|
||||||
->orWhereHas(
|
|
||||||
'sourceCatalogItem',
|
|
||||||
fn (Builder $catalogItemQuery): Builder => $catalogItemQuery
|
|
||||||
->whereIn('category_id', $categoryIds)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
->firstOrFail();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scan(User $scanner, string $ticketUuid): Ticket
|
|
||||||
{
|
|
||||||
return DB::transaction(function () use ($scanner, $ticketUuid): Ticket {
|
|
||||||
$ticket = $this->baseQuery()
|
|
||||||
->where('tenant_code', $scanner->tenant_codigo)
|
|
||||||
->where('ticket', $ticketUuid)
|
|
||||||
->lockForUpdate()
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
if (! $this->scannerCanScan($scanner, $ticket)) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'ticket' => __('api.ticket.scanner_category_forbidden'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ticket->is_used) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'ticket' => __('api.ticket.already_scanned'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $ticket->is_valid) {
|
|
||||||
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();
|
|
||||||
|
|
||||||
return $ticket->refresh()->load($this->relations());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return Builder<Ticket> */
|
|
||||||
private function baseQuery(): Builder
|
|
||||||
{
|
|
||||||
return Ticket::query()->with($this->relations());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<int, string> */
|
|
||||||
private function relations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'validityGroups.validityTimes',
|
|
||||||
'sourceCatalogItem.category',
|
|
||||||
'sourceVariant.eventDate',
|
|
||||||
'sourceVariant.catalogItem',
|
|
||||||
'user',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<int, int> */
|
|
||||||
private function scannerCategoryIds(User $scanner): array
|
|
||||||
{
|
|
||||||
return $scanner->scanCategories()
|
|
||||||
->pluck('categorias.id')
|
|
||||||
->map(fn (mixed $id): int => (int) $id)
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function scannerCanScan(User $scanner, Ticket $ticket): bool
|
|
||||||
{
|
|
||||||
$categoryId = $ticket->sourceCatalogItem?->category_id;
|
|
||||||
|
|
||||||
return $categoryId !== null
|
|
||||||
&& $scanner->scanCategories()
|
|
||||||
->where('categorias.id', $categoryId)
|
|
||||||
->exists();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,5 +9,3 @@ Route::prefix('tenants/{tenant:codigo}')
|
|||||||
Route::get('tickets', [TicketController::class, 'index']);
|
Route::get('tickets', [TicketController::class, 'index']);
|
||||||
Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']);
|
Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']);
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/scanner.php';
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Domains\Ticket\Controllers\Scanner\TicketController;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
Route::prefix('v1/scanner/tickets')
|
|
||||||
->middleware(['auth:sanctum', 'scanner.tenant'])
|
|
||||||
->group(function (): void {
|
|
||||||
Route::get('/', [TicketController::class, 'index']);
|
|
||||||
Route::get('{ticketUuid}', [TicketController::class, 'show'])
|
|
||||||
->whereUuid('ticketUuid');
|
|
||||||
Route::post('{ticketUuid}/scan', [TicketController::class, 'scan'])
|
|
||||||
->whereUuid('ticketUuid');
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use App\Domains\Authorization\Enums\PermissionCode;
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
class EnsureScannerTenant
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Ensure the authenticated user can scan tickets for a tenant.
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next): Response
|
|
||||||
{
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
if (
|
|
||||||
! $user
|
|
||||||
|| ! $user->tenant_codigo
|
|
||||||
|| ! $user->hasPermission(PermissionCode::ScanTickets->value)
|
|
||||||
) {
|
|
||||||
throw new AuthorizationException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
use App\Domains\Auth\Exceptions\AccountLockedException;
|
use App\Domains\Auth\Exceptions\AccountLockedException;
|
||||||
use App\Domains\Ticket\Exceptions\TicketNotAvailableException;
|
use App\Domains\Ticket\Exceptions\TicketNotAvailableException;
|
||||||
use App\Http\Middleware\EnsureAdminAppTenant;
|
use App\Http\Middleware\EnsureAdminAppTenant;
|
||||||
use App\Http\Middleware\EnsureScannerTenant;
|
|
||||||
use App\Http\Middleware\EnsureTenantHasMenu;
|
use App\Http\Middleware\EnsureTenantHasMenu;
|
||||||
use App\Http\Middleware\SetApiLocale;
|
use App\Http\Middleware\SetApiLocale;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
@@ -26,7 +25,6 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'adminapp.tenant' => EnsureAdminAppTenant::class,
|
'adminapp.tenant' => EnsureAdminAppTenant::class,
|
||||||
'scanner.tenant' => EnsureScannerTenant::class,
|
|
||||||
'tenant.menu' => EnsureTenantHasMenu::class,
|
'tenant.menu' => EnsureTenantHasMenu::class,
|
||||||
]);
|
]);
|
||||||
$middleware->encryptCookies(except: [
|
$middleware->encryptCookies(except: [
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<?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::table('website_type', function (Blueprint $table): void {
|
|
||||||
$table->string('scanner_domain')->nullable()->unique()->after('dominio');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('website_type', function (Blueprint $table): void {
|
|
||||||
$table->dropUnique(['scanner_domain']);
|
|
||||||
$table->dropColumn('scanner_domain');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Domains\Authorization\Enums\PermissionCode;
|
|
||||||
use App\Domains\Authorization\Enums\RoleCode;
|
use App\Domains\Authorization\Enums\RoleCode;
|
||||||
use App\Domains\Authorization\Models\Permission;
|
use App\Domains\Authorization\Models\Permission;
|
||||||
use App\Domains\Authorization\Models\Role;
|
use App\Domains\Authorization\Models\Role;
|
||||||
@@ -82,7 +81,7 @@ class AuthorizationSeeder extends Seeder
|
|||||||
'nombre' => 'Gestionar tickets',
|
'nombre' => 'Gestionar tickets',
|
||||||
'descripcion' => 'Permite emitir, invalidar o regenerar tickets.',
|
'descripcion' => 'Permite emitir, invalidar o regenerar tickets.',
|
||||||
],
|
],
|
||||||
PermissionCode::ScanTickets->value => [
|
'tickets.escanear' => [
|
||||||
'nombre' => 'Escanear tickets',
|
'nombre' => 'Escanear tickets',
|
||||||
'descripcion' => 'Permite validar y consumir tickets de las categorías asignadas al usuario.',
|
'descripcion' => 'Permite validar y consumir tickets de las categorías asignadas al usuario.',
|
||||||
],
|
],
|
||||||
@@ -120,12 +119,12 @@ class AuthorizationSeeder extends Seeder
|
|||||||
RoleCode::AdminApp->value => [
|
RoleCode::AdminApp->value => [
|
||||||
'nombre' => 'Administrador de la aplicación',
|
'nombre' => 'Administrador de la aplicación',
|
||||||
'descripcion' => 'Accede a los menús administrativos de la aplicación.',
|
'descripcion' => 'Accede a los menús administrativos de la aplicación.',
|
||||||
'permisos' => [PermissionCode::ScanTickets->value],
|
'permisos' => [],
|
||||||
],
|
],
|
||||||
RoleCode::Scanner->value => [
|
RoleCode::Scanner->value => [
|
||||||
'nombre' => 'Scanner',
|
'nombre' => 'Scanner',
|
||||||
'descripcion' => 'Valida y consume tickets de las categorías que tiene asignadas.',
|
'descripcion' => 'Valida y consume tickets de las categorías que tiene asignadas.',
|
||||||
'permisos' => [PermissionCode::ScanTickets->value],
|
'permisos' => ['tickets.escanear'],
|
||||||
],
|
],
|
||||||
RoleCode::User->value => [
|
RoleCode::User->value => [
|
||||||
'nombre' => 'Usuario',
|
'nombre' => 'Usuario',
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ use App\Domains\Catalog\Models\CatalogItem;
|
|||||||
use App\Domains\Catalog\Models\Category;
|
use App\Domains\Catalog\Models\Category;
|
||||||
use App\Domains\Catalog\Models\FeaturedGroup;
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
use App\Domains\Catalog\Services\CatalogService;
|
use App\Domains\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Event\Models\EventDate;
|
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use App\Domains\Ticket\Enums\ValidityTimeType;
|
|
||||||
use App\Domains\Ticket\Models\ValidityTime;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
@@ -50,30 +47,13 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
|||||||
'event_location' => 'Sunchales, Santa Fe',
|
'event_location' => 'Sunchales, Santa Fe',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$existingValidityTimeIds = $tenant->eventDates()->pluck('validity_time_id');
|
|
||||||
$tenant->eventDates()->delete();
|
$tenant->eventDates()->delete();
|
||||||
ValidityTime::query()->whereKey($existingValidityTimeIds)->delete();
|
|
||||||
|
|
||||||
$eventDates = collect(['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12'])
|
$eventDates = collect(['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12'])
|
||||||
->map(function (string $date) use ($tenant): EventDate {
|
->map(fn (string $date) => $tenant->eventDates()->create([
|
||||||
$validityTime = ValidityTime::query()->create([
|
|
||||||
'type' => ValidityTimeType::FixedWindow,
|
|
||||||
'start_time' => null,
|
|
||||||
'end_time' => null,
|
|
||||||
'fixed_starts_at' => $date.' 00:00:00',
|
|
||||||
'fixed_expires_at' => $date.' 23:59:59',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$eventDate = new EventDate;
|
|
||||||
$eventDate->forceFill([
|
|
||||||
'date' => $date,
|
'date' => $date,
|
||||||
'time_start' => '00:00:00',
|
'time_start' => '00:00:00',
|
||||||
'time_end' => '23:59:59',
|
'time_end' => '23:59:59',
|
||||||
'validity_time_id' => $validityTime->id,
|
]));
|
||||||
]);
|
|
||||||
|
|
||||||
return $tenant->eventDates()->save($eventDate);
|
|
||||||
});
|
|
||||||
$dateIds = $eventDates->pluck('id')->map(fn ($id): int => (int) $id)->values();
|
$dateIds = $eventDates->pluck('id')->map(fn ($id): int => (int) $id)->values();
|
||||||
|
|
||||||
$this->createProduct($tenant, [
|
$this->createProduct($tenant, [
|
||||||
|
|||||||
@@ -29,13 +29,6 @@ class MenuSeeder extends Seeder
|
|||||||
'content_type' => Menu::CONTENT_TYPE_DYNAMIC,
|
'content_type' => Menu::CONTENT_TYPE_DYNAMIC,
|
||||||
'route' => '/',
|
'route' => '/',
|
||||||
],
|
],
|
||||||
['code' => 'scanner.inicio', 'label' => 'Inicio', 'route' => '/scanner/inicio'],
|
|
||||||
['code' => 'scanner.scan', 'label' => 'Escanear', 'route' => '/scanner/scan'],
|
|
||||||
[
|
|
||||||
'code' => 'scanner.detail',
|
|
||||||
'label' => 'Detalle',
|
|
||||||
'route' => '/scanner/detail/:id',
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.event',
|
'code' => 'adminapp.event',
|
||||||
'label' => 'Eventos',
|
'label' => 'Eventos',
|
||||||
@@ -226,11 +219,8 @@ class MenuSeeder extends Seeder
|
|||||||
->where('code', 'main.adminapp')
|
->where('code', 'main.adminapp')
|
||||||
->orWhere('parent_menu_code', 'main.adminapp')
|
->orWhere('parent_menu_code', 'main.adminapp')
|
||||||
->pluck('code');
|
->pluck('code');
|
||||||
$scannerMenuCodes = Menu::query()
|
|
||||||
->where('code', 'like', 'scanner.%')
|
|
||||||
->pluck('code');
|
|
||||||
$userMenuCodes = Menu::query()
|
$userMenuCodes = Menu::query()
|
||||||
->whereNotIn('code', $adminAppMenuCodes->merge($scannerMenuCodes))
|
->whereNotIn('code', $adminAppMenuCodes)
|
||||||
->pluck('code');
|
->pluck('code');
|
||||||
|
|
||||||
Role::query()
|
Role::query()
|
||||||
@@ -241,10 +231,6 @@ class MenuSeeder extends Seeder
|
|||||||
->where('codigo', RoleCode::User->value)
|
->where('codigo', RoleCode::User->value)
|
||||||
->each(fn (Role $role) => $role->menus()->sync($userMenuCodes));
|
->each(fn (Role $role) => $role->menus()->sync($userMenuCodes));
|
||||||
|
|
||||||
Role::query()
|
|
||||||
->where('codigo', RoleCode::Scanner->value)
|
|
||||||
->each(fn (Role $role) => $role->menus()->sync($scannerMenuCodes));
|
|
||||||
|
|
||||||
$tenants = Tenant::all();
|
$tenants = Tenant::all();
|
||||||
|
|
||||||
$allMenus = Menu::pluck('code')->toArray();
|
$allMenus = Menu::pluck('code')->toArray();
|
||||||
|
|||||||
@@ -128,8 +128,8 @@ class TenantSeeder extends Seeder
|
|||||||
'button_text' => 'Comprar entradas',
|
'button_text' => 'Comprar entradas',
|
||||||
'button_href' => '/tickets',
|
'button_href' => '/tickets',
|
||||||
'background_image_id' => $this->uploadedImage(
|
'background_image_id' => $this->uploadedImage(
|
||||||
'images/tennants/fiesta_futbol_infantil/futbol_infantil_hero.png',
|
'images/tennants/fiesta_futbol_infantil/futbol_infantil_hero.jpg',
|
||||||
'futbol_infantil_hero.png',
|
'futbol_infantil_hero.jpg',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
'eventConfig' => [
|
'eventConfig' => [
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'nombre' => 'ShopIt',
|
'nombre' => 'ShopIt',
|
||||||
'dominio' => 'localhost',
|
'dominio' => 'localhost',
|
||||||
'scanner_domain' => 'scanner.localhost',
|
|
||||||
...self::PRESENTATION,
|
...self::PRESENTATION,
|
||||||
'site_logo' => $this->onTicketLogo(),
|
'site_logo' => $this->onTicketLogo(),
|
||||||
'footer_logo' => $this->onTicketFooterLogo(),
|
'footer_logo' => $this->onTicketFooterLogo(),
|
||||||
@@ -64,7 +63,6 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'nombre' => 'OnTicket',
|
'nombre' => 'OnTicket',
|
||||||
'dominio' => 'onticket.localhost',
|
'dominio' => 'onticket.localhost',
|
||||||
'scanner_domain' => 'scanner.onticket.localhost',
|
|
||||||
...self::PRESENTATION,
|
...self::PRESENTATION,
|
||||||
'site_logo' => $this->onTicketLogo(),
|
'site_logo' => $this->onTicketLogo(),
|
||||||
'footer_logo' => $this->onTicketFooterLogo(),
|
'footer_logo' => $this->onTicketFooterLogo(),
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ return [
|
|||||||
'invalid_variant' => 'Variant :variant does not belong to product :product.',
|
'invalid_variant' => 'Variant :variant does not belong to product :product.',
|
||||||
'purchase_without_user' => 'Purchase :purchase does not have an associated user.',
|
'purchase_without_user' => 'Purchase :purchase does not have an associated user.',
|
||||||
'product_not_found' => 'The product for purchase item :purchase_item was not found.',
|
'product_not_found' => 'The product for purchase item :purchase_item was not found.',
|
||||||
'already_scanned' => 'The ticket has already been scanned.',
|
|
||||||
'expired_for_scan' => 'The ticket has expired.',
|
|
||||||
'not_valid_for_scan' => 'The ticket is not currently valid.',
|
|
||||||
'scanner_category_forbidden' => 'The scanner is not assigned to the ticket category.',
|
|
||||||
],
|
],
|
||||||
'integration' => [
|
'integration' => [
|
||||||
'not_configured' => 'The integration is not configured for this tenant.',
|
'not_configured' => 'The integration is not configured for this tenant.',
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ return [
|
|||||||
'invalid_variant' => 'La variante :variant no pertenece al producto :product.',
|
'invalid_variant' => 'La variante :variant no pertenece al producto :product.',
|
||||||
'purchase_without_user' => 'La compra :purchase no tiene un usuario asociado.',
|
'purchase_without_user' => 'La compra :purchase no tiene un usuario asociado.',
|
||||||
'product_not_found' => 'No se encontró el producto de la línea de compra :purchase_item.',
|
'product_not_found' => 'No se encontró el producto de la línea de compra :purchase_item.',
|
||||||
'already_scanned' => 'El ticket ya fue escaneado.',
|
|
||||||
'expired_for_scan' => 'El ticket está vencido.',
|
|
||||||
'not_valid_for_scan' => 'El ticket no es válido en este momento.',
|
|
||||||
'scanner_category_forbidden' => 'El scanner no está asignado a la categoría del ticket.',
|
|
||||||
],
|
],
|
||||||
'integration' => [
|
'integration' => [
|
||||||
'not_configured' => 'La integración no está configurada para este tenant.',
|
'not_configured' => 'La integración no está configurada para este tenant.',
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Models\LoginAttempt;
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
use App\Domains\Authorization\Enums\PermissionCode;
|
|
||||||
use App\Domains\Authorization\Enums\RoleCode;
|
|
||||||
use App\Domains\Authorization\Models\Permission;
|
|
||||||
use App\Domains\Authorization\Models\Role;
|
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ScannerLoginControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_it_logs_in_a_tenant_bound_user_with_scan_permission(): void
|
|
||||||
{
|
|
||||||
$role = Role::query()->create([
|
|
||||||
'codigo' => RoleCode::AdminApp->value,
|
|
||||||
'nombre' => 'Operador',
|
|
||||||
]);
|
|
||||||
$permission = Permission::query()->create([
|
|
||||||
'codigo' => PermissionCode::ScanTickets->value,
|
|
||||||
'nombre' => 'Escanear tickets',
|
|
||||||
]);
|
|
||||||
$role->permissions()->attach($permission->codigo);
|
|
||||||
$tenant = Tenant::query()->create([
|
|
||||||
'codigo' => 'acme',
|
|
||||||
'nombre' => 'Acme',
|
|
||||||
'dominio' => 'acme.test',
|
|
||||||
]);
|
|
||||||
$user = User::factory()->create([
|
|
||||||
'email' => 'scanner@example.com',
|
|
||||||
'password' => Hash::make('secret123'),
|
|
||||||
'rol_codigo' => $role->codigo,
|
|
||||||
'tenant_codigo' => $tenant->codigo,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/scanner/login', [
|
|
||||||
'email' => ' SCANNER@EXAMPLE.COM ',
|
|
||||||
'password' => 'secret123',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonPath('user.id', $user->id)
|
|
||||||
->assertJsonPath('user.rol_codigo', RoleCode::AdminApp->value)
|
|
||||||
->assertJsonPath('token_type', 'Bearer');
|
|
||||||
|
|
||||||
$this->assertSame(['scanner'], $user->tokens()->sole()->abilities);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_rejects_a_user_without_scan_permission(): void
|
|
||||||
{
|
|
||||||
$role = Role::query()->create([
|
|
||||||
'codigo' => RoleCode::Scanner->value,
|
|
||||||
'nombre' => 'Scanner sin permiso',
|
|
||||||
]);
|
|
||||||
$tenant = Tenant::query()->create([
|
|
||||||
'codigo' => 'acme',
|
|
||||||
'nombre' => 'Acme',
|
|
||||||
'dominio' => 'acme.test',
|
|
||||||
]);
|
|
||||||
$user = User::factory()->create([
|
|
||||||
'email' => 'customer@example.com',
|
|
||||||
'password' => Hash::make('secret123'),
|
|
||||||
'rol_codigo' => $role->codigo,
|
|
||||||
'tenant_codigo' => $tenant->codigo,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->postJson('/api/v1/scanner/login', [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'secret123',
|
|
||||||
])->assertUnprocessable()->assertJsonValidationErrors(['email']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_an_invalid_password_is_recorded_with_the_users_tenant(): void
|
|
||||||
{
|
|
||||||
$role = Role::query()->create([
|
|
||||||
'codigo' => RoleCode::Scanner->value,
|
|
||||||
'nombre' => 'Scanner',
|
|
||||||
]);
|
|
||||||
$permission = Permission::query()->create([
|
|
||||||
'codigo' => PermissionCode::ScanTickets->value,
|
|
||||||
'nombre' => 'Escanear tickets',
|
|
||||||
]);
|
|
||||||
$role->permissions()->attach($permission->codigo);
|
|
||||||
$tenant = Tenant::query()->create([
|
|
||||||
'codigo' => 'acme',
|
|
||||||
'nombre' => 'Acme',
|
|
||||||
'dominio' => 'acme.test',
|
|
||||||
]);
|
|
||||||
$user = User::factory()->create([
|
|
||||||
'email' => 'scanner@example.com',
|
|
||||||
'password' => Hash::make('correct-password'),
|
|
||||||
'rol_codigo' => $role->codigo,
|
|
||||||
'tenant_codigo' => $tenant->codigo,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->postJson('/api/v1/scanner/login', [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'wrong-password',
|
|
||||||
])->assertUnprocessable()->assertJsonValidationErrors(['email']);
|
|
||||||
|
|
||||||
$this->assertSame(1, $user->refresh()->failed_login_attempts);
|
|
||||||
$this->assertDatabaseHas('login_attempts', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'tenant_codigo' => $tenant->codigo,
|
|
||||||
'outcome' => LoginAttempt::OUTCOME_INVALID_CREDENTIALS,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
use App\Domains\Authorization\Enums\PermissionCode;
|
|
||||||
use App\Domains\Authorization\Enums\RoleCode;
|
|
||||||
use App\Domains\Authorization\Models\Permission;
|
|
||||||
use App\Domains\Authorization\Models\Role;
|
|
||||||
use App\Domains\Menu\Models\Menu;
|
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Laravel\Sanctum\Sanctum;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ScannerMeControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_it_returns_only_scanner_menus_assigned_to_the_tenant(): void
|
|
||||||
{
|
|
||||||
$scannerRole = Role::query()->create([
|
|
||||||
'codigo' => RoleCode::Scanner->value,
|
|
||||||
'nombre' => 'Scanner',
|
|
||||||
]);
|
|
||||||
$permission = Permission::query()->create([
|
|
||||||
'codigo' => PermissionCode::ScanTickets->value,
|
|
||||||
'nombre' => 'Escanear tickets',
|
|
||||||
]);
|
|
||||||
$scannerRole->permissions()->attach($permission->codigo);
|
|
||||||
$tenant = Tenant::query()->create([
|
|
||||||
'codigo' => 'acme',
|
|
||||||
'nombre' => 'Acme',
|
|
||||||
'dominio' => 'acme.test',
|
|
||||||
]);
|
|
||||||
$home = Menu::query()->create([
|
|
||||||
'code' => 'scanner.inicio',
|
|
||||||
'label' => 'Inicio',
|
|
||||||
'route' => '/scanner/inicio',
|
|
||||||
]);
|
|
||||||
$scan = Menu::query()->create([
|
|
||||||
'code' => 'scanner.scan',
|
|
||||||
'label' => 'Escanear',
|
|
||||||
'route' => '/scanner/scan',
|
|
||||||
]);
|
|
||||||
$foreign = Menu::query()->create([
|
|
||||||
'code' => 'adminapp.inicio',
|
|
||||||
'label' => 'Administración',
|
|
||||||
'route' => '/admin/inicio',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$scannerRole->menus()->sync([$home->code, $scan->code]);
|
|
||||||
$tenant->menues()->sync([$home->code, $scan->code, $foreign->code]);
|
|
||||||
|
|
||||||
$user = User::factory()->create([
|
|
||||||
'rol_codigo' => $scannerRole->codigo,
|
|
||||||
'tenant_codigo' => $tenant->codigo,
|
|
||||||
]);
|
|
||||||
Sanctum::actingAs($user);
|
|
||||||
|
|
||||||
$this->getJson('/api/v1/scanner/me')
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonPath('data.user.id', $user->id)
|
|
||||||
->assertJsonPath('data.tenant.codigo', $tenant->codigo)
|
|
||||||
->assertJsonCount(2, 'data.tenant.menues')
|
|
||||||
->assertJsonMissing(['code' => $foreign->code]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Seeders;
|
namespace Tests\Feature\Seeders;
|
||||||
|
|
||||||
use App\Domains\Authorization\Enums\PermissionCode;
|
|
||||||
use App\Domains\Authorization\Enums\RoleCode;
|
use App\Domains\Authorization\Enums\RoleCode;
|
||||||
use App\Domains\Authorization\Models\Permission;
|
use App\Domains\Authorization\Models\Permission;
|
||||||
use App\Domains\Authorization\Models\Role;
|
use App\Domains\Authorization\Models\Role;
|
||||||
@@ -22,7 +21,6 @@ class AuthorizationSeederTest extends TestCase
|
|||||||
[
|
[
|
||||||
RoleCode::Admin->value,
|
RoleCode::Admin->value,
|
||||||
RoleCode::AdminApp->value,
|
RoleCode::AdminApp->value,
|
||||||
RoleCode::Scanner->value,
|
|
||||||
RoleCode::User->value,
|
RoleCode::User->value,
|
||||||
],
|
],
|
||||||
Role::query()->orderBy('codigo')->pluck('codigo')->all()
|
Role::query()->orderBy('codigo')->pluck('codigo')->all()
|
||||||
@@ -36,15 +34,10 @@ class AuthorizationSeederTest extends TestCase
|
|||||||
|
|
||||||
$admin = Role::query()->where('codigo', RoleCode::Admin->value)->firstOrFail();
|
$admin = Role::query()->where('codigo', RoleCode::Admin->value)->firstOrFail();
|
||||||
$appAdmin = Role::query()->where('codigo', RoleCode::AdminApp->value)->firstOrFail();
|
$appAdmin = Role::query()->where('codigo', RoleCode::AdminApp->value)->firstOrFail();
|
||||||
$scanner = Role::query()->where('codigo', RoleCode::Scanner->value)->firstOrFail();
|
|
||||||
$user = Role::query()->where('codigo', RoleCode::User->value)->firstOrFail();
|
$user = Role::query()->where('codigo', RoleCode::User->value)->firstOrFail();
|
||||||
|
|
||||||
$this->assertCount(22, $admin->permissions);
|
$this->assertCount(22, $admin->permissions);
|
||||||
$this->assertCount(0, $appAdmin->permissions);
|
$this->assertCount(0, $appAdmin->permissions);
|
||||||
$this->assertSame(
|
|
||||||
[PermissionCode::ScanTickets->value],
|
|
||||||
$scanner->permissions->pluck('codigo')->all()
|
|
||||||
);
|
|
||||||
$this->assertCount(0, $user->permissions);
|
$this->assertCount(0, $user->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +46,8 @@ class AuthorizationSeederTest extends TestCase
|
|||||||
$this->seed(AuthorizationSeeder::class);
|
$this->seed(AuthorizationSeeder::class);
|
||||||
$this->seed(AuthorizationSeeder::class);
|
$this->seed(AuthorizationSeeder::class);
|
||||||
|
|
||||||
$this->assertCount(4, Role::query()->get());
|
$this->assertCount(3, Role::query()->get());
|
||||||
$this->assertCount(22, Permission::query()->get());
|
$this->assertCount(22, Permission::query()->get());
|
||||||
$this->assertDatabaseCount('roles_permisos', 23);
|
$this->assertDatabaseCount('roles_permisos', 22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ use App\Domains\Catalog\Models\Category;
|
|||||||
use App\Domains\Catalog\Models\FeaturedGroup;
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use App\Domains\Ticket\Enums\ValidityTimeType;
|
|
||||||
use App\Domains\Ticket\Models\ValidityTime;
|
|
||||||
use Database\Seeders\AttributeSeeder;
|
use Database\Seeders\AttributeSeeder;
|
||||||
use Database\Seeders\FiestaFutbolInfantilProductSeeder;
|
use Database\Seeders\FiestaFutbolInfantilProductSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@@ -46,14 +44,6 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase
|
|||||||
$this->seed([AttributeSeeder::class, FiestaFutbolInfantilProductSeeder::class]);
|
$this->seed([AttributeSeeder::class, FiestaFutbolInfantilProductSeeder::class]);
|
||||||
|
|
||||||
$this->assertSame('9, 10, 11 y 12 de Octubre 2026', $tenant->fresh()->event_date_text);
|
$this->assertSame('9, 10, 11 y 12 de Octubre 2026', $tenant->fresh()->event_date_text);
|
||||||
$this->assertCount(4, $tenant->eventDates);
|
|
||||||
$this->assertCount(4, ValidityTime::query()->whereHas('eventDate', fn ($query) => $query
|
|
||||||
->where('tenant_code', $tenant->codigo))->get());
|
|
||||||
$this->assertTrue($tenant->eventDates()->with('validityTime')->get()->every(
|
|
||||||
fn ($eventDate): bool => $eventDate->validityTime->type === ValidityTimeType::FixedWindow
|
|
||||||
&& $eventDate->validityTime->fixed_starts_at->format('Y-m-d H:i:s') === $eventDate->date->format('Y-m-d').' 00:00:00'
|
|
||||||
&& $eventDate->validityTime->fixed_expires_at->format('Y-m-d H:i:s') === $eventDate->date->format('Y-m-d').' 23:59:59'
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
['color', 'event_date', 'horario', 'servicio', 'talle', 'tipo_alojamiento'],
|
['color', 'event_date', 'horario', 'servicio', 'talle', 'tipo_alojamiento'],
|
||||||
|
|||||||
@@ -124,13 +124,8 @@ class MenuSeederTest extends TestCase
|
|||||||
->where('code', 'main.adminapp')
|
->where('code', 'main.adminapp')
|
||||||
->orWhere('parent_menu_code', 'main.adminapp')
|
->orWhere('parent_menu_code', 'main.adminapp')
|
||||||
->pluck('code');
|
->pluck('code');
|
||||||
$scannerMenuCodes = Menu::query()
|
|
||||||
->where('code', 'like', 'scanner.%')
|
|
||||||
->orderBy('code')
|
|
||||||
->pluck('code')
|
|
||||||
->all();
|
|
||||||
$userMenuCodes = Menu::query()
|
$userMenuCodes = Menu::query()
|
||||||
->whereNotIn('code', [...$adminAppMenuCodes, ...$scannerMenuCodes])
|
->whereNotIn('code', $adminAppMenuCodes)
|
||||||
->orderBy('code')
|
->orderBy('code')
|
||||||
->pluck('code')
|
->pluck('code')
|
||||||
->all();
|
->all();
|
||||||
@@ -158,23 +153,9 @@ class MenuSeederTest extends TestCase
|
|||||||
$this->assertSame($userMenuCodes, $userRoleMenuCodes);
|
$this->assertSame($userMenuCodes, $userRoleMenuCodes);
|
||||||
$this->assertNotContains('main.adminapp', $userRoleMenuCodes);
|
$this->assertNotContains('main.adminapp', $userRoleMenuCodes);
|
||||||
$this->assertNotContains('adminapp.catalog', $userRoleMenuCodes);
|
$this->assertNotContains('adminapp.catalog', $userRoleMenuCodes);
|
||||||
|
|
||||||
$scannerRoleMenuCodes = Role::query()
|
|
||||||
->where('codigo', RoleCode::Scanner->value)
|
|
||||||
->firstOrFail()
|
|
||||||
->menus()
|
|
||||||
->orderBy('menues.code')
|
|
||||||
->pluck('menues.code')
|
|
||||||
->all();
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
'scanner.detail',
|
|
||||||
'scanner.inicio',
|
|
||||||
'scanner.scan',
|
|
||||||
], $scannerRoleMenuCodes);
|
|
||||||
$this->assertDatabaseCount(
|
$this->assertDatabaseCount(
|
||||||
'roles_menues',
|
'roles_menues',
|
||||||
(count($allMenuCodes) * 2) + count($userMenuCodes) + count($scannerMenuCodes)
|
(count($allMenuCodes) * 2) + count($userMenuCodes)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ class WebsiteTypeSeederTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame('ShopIt', $shopIt->nombre);
|
$this->assertSame('ShopIt', $shopIt->nombre);
|
||||||
$this->assertSame('localhost', $shopIt->dominio);
|
$this->assertSame('localhost', $shopIt->dominio);
|
||||||
$this->assertSame('scanner.localhost', $shopIt->scanner_domain);
|
|
||||||
$this->assertSame($expectedPresentation, $shopIt->only(array_keys($expectedPresentation)));
|
$this->assertSame($expectedPresentation, $shopIt->only(array_keys($expectedPresentation)));
|
||||||
$this->assertSame('onticket_logo.png', $shopIt->siteLogo->filename);
|
$this->assertSame('onticket_logo.png', $shopIt->siteLogo->filename);
|
||||||
Storage::disk('s3')->assertExists($shopIt->siteLogo->path);
|
Storage::disk('s3')->assertExists($shopIt->siteLogo->path);
|
||||||
@@ -71,7 +70,6 @@ class WebsiteTypeSeederTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame('OnTicket', $onTicket->nombre);
|
$this->assertSame('OnTicket', $onTicket->nombre);
|
||||||
$this->assertSame('onticket.localhost', $onTicket->dominio);
|
$this->assertSame('onticket.localhost', $onTicket->dominio);
|
||||||
$this->assertSame('scanner.onticket.localhost', $onTicket->scanner_domain);
|
|
||||||
$this->assertSame($expectedPresentation, $onTicket->only(array_keys($expectedPresentation)));
|
$this->assertSame($expectedPresentation, $onTicket->only(array_keys($expectedPresentation)));
|
||||||
$this->assertSame('onticket_logo.png', $onTicket->siteLogo->filename);
|
$this->assertSame('onticket_logo.png', $onTicket->siteLogo->filename);
|
||||||
Storage::disk('s3')->assertExists($onTicket->siteLogo->path);
|
Storage::disk('s3')->assertExists($onTicket->siteLogo->path);
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Tenant;
|
|
||||||
|
|
||||||
use App\Domains\Attachable\Models\Attachment;
|
|
||||||
use App\Domains\Tenant\Models\WebsiteType;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class BootstrapScannerControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_it_publicly_bootstraps_the_scanner_by_scanner_domain(): void
|
|
||||||
{
|
|
||||||
$siteLogo = Attachment::factory()->create();
|
|
||||||
|
|
||||||
WebsiteType::query()->create([
|
|
||||||
'codigo' => 'shopit',
|
|
||||||
'nombre' => 'ShopIt',
|
|
||||||
'dominio' => 'admin.shopit.test',
|
|
||||||
'scanner_domain' => 'scanner.shopit.test',
|
|
||||||
'primary_color' => '#112233',
|
|
||||||
'secondary_color' => '#445566',
|
|
||||||
'danger_color' => '#aa0000',
|
|
||||||
'success_color' => '#00aa00',
|
|
||||||
'warning_color' => '#ffaa00',
|
|
||||||
'body_color' => '#666666',
|
|
||||||
'darker_body_color' => '#333333',
|
|
||||||
'surface_color' => '#ffffff',
|
|
||||||
'background_color' => '#f8f8f8',
|
|
||||||
'border_color' => '#eaeaea',
|
|
||||||
'login_header_footer_color' => '#313131',
|
|
||||||
'site_logo' => $siteLogo->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->getJson('/api/v1/scanner/bootstrap/SCANNER.SHOPIT.TEST')
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonPath('data.website_type_code', 'shopit')
|
|
||||||
->assertJsonPath('data.primary_color', '#112233')
|
|
||||||
->assertJsonPath('data.site_logo', $siteLogo->getTemporaryUrl(1440))
|
|
||||||
->assertJsonPath('data.footer_logo', null)
|
|
||||||
->assertJsonMissingPath('data.codigo')
|
|
||||||
->assertJsonMissingPath('data.nombre')
|
|
||||||
->assertJsonMissingPath('data.dominio')
|
|
||||||
->assertJsonMissingPath('data.scanner_domain');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_does_not_match_the_admin_app_domain(): void
|
|
||||||
{
|
|
||||||
WebsiteType::query()->create([
|
|
||||||
'codigo' => 'shopit',
|
|
||||||
'nombre' => 'ShopIt',
|
|
||||||
'dominio' => 'admin.shopit.test',
|
|
||||||
'scanner_domain' => 'scanner.shopit.test',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->getJson('/api/v1/scanner/bootstrap/admin.shopit.test')
|
|
||||||
->assertNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_not_found_for_an_unknown_scanner_domain(): void
|
|
||||||
{
|
|
||||||
$this->getJson('/api/v1/scanner/bootstrap/unknown.test')
|
|
||||||
->assertNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_rejects_an_invalid_domain(): void
|
|
||||||
{
|
|
||||||
$invalidDomain = str_repeat('a', 256);
|
|
||||||
|
|
||||||
$this->getJson("/api/v1/scanner/bootstrap/{$invalidDomain}")
|
|
||||||
->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors(['dominio']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,6 @@ class WebsiteTypeServiceTest extends TestCase
|
|||||||
'codigo' => 'marketplace',
|
'codigo' => 'marketplace',
|
||||||
'nombre' => 'Marketplace',
|
'nombre' => 'Marketplace',
|
||||||
'dominio' => 'marketplace.test',
|
'dominio' => 'marketplace.test',
|
||||||
'scanner_domain' => 'scanner.marketplace.test',
|
|
||||||
'primary_color' => '#111111',
|
'primary_color' => '#111111',
|
||||||
'secondary_color' => '#222222',
|
'secondary_color' => '#222222',
|
||||||
'danger_color' => '#ff0000',
|
'danger_color' => '#ff0000',
|
||||||
@@ -49,7 +48,6 @@ class WebsiteTypeServiceTest extends TestCase
|
|||||||
$this->assertDatabaseHas('website_type', [
|
$this->assertDatabaseHas('website_type', [
|
||||||
'codigo' => 'marketplace',
|
'codigo' => 'marketplace',
|
||||||
'dominio' => 'marketplace.test',
|
'dominio' => 'marketplace.test',
|
||||||
'scanner_domain' => 'scanner.marketplace.test',
|
|
||||||
'warning_color' => '#ffaa00',
|
'warning_color' => '#ffaa00',
|
||||||
'body_color' => '#666666',
|
'body_color' => '#666666',
|
||||||
'darker_body_color' => '#333333',
|
'darker_body_color' => '#333333',
|
||||||
|
|||||||
@@ -1,313 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Ticket;
|
|
||||||
|
|
||||||
use App\Domains\Attachable\Models\Attachment;
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
use App\Domains\Authorization\Enums\RoleCode;
|
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
|
||||||
use App\Domains\Catalog\Models\Category;
|
|
||||||
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\Str;
|
|
||||||
use Laravel\Sanctum\Sanctum;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ScannerTicketControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
private Tenant $tenant;
|
|
||||||
|
|
||||||
private User $scanner;
|
|
||||||
|
|
||||||
private User $ticketOwner;
|
|
||||||
|
|
||||||
private Category $category;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->seed(AuthorizationSeeder::class);
|
|
||||||
WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']);
|
|
||||||
$this->tenant = $this->createTenant('acme');
|
|
||||||
$this->scanner = User::factory()->create([
|
|
||||||
'rol_codigo' => RoleCode::Scanner->value,
|
|
||||||
'tenant_codigo' => $this->tenant->codigo,
|
|
||||||
]);
|
|
||||||
$this->ticketOwner = User::factory()->create();
|
|
||||||
$this->category = Category::query()->create([
|
|
||||||
'tenant_code' => $this->tenant->codigo,
|
|
||||||
'nombre' => 'Entradas',
|
|
||||||
]);
|
|
||||||
$this->scanner->scanCategories()->attach($this->category);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_routes_require_authentication_and_scan_permission(): void
|
|
||||||
{
|
|
||||||
$this->getJson('/api/v1/scanner/tickets')->assertUnauthorized();
|
|
||||||
|
|
||||||
Sanctum::actingAs(User::factory()->create([
|
|
||||||
'rol_codigo' => RoleCode::AdminApp->value,
|
|
||||||
'tenant_codigo' => $this->tenant->codigo,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$this->getJson('/api/v1/scanner/tickets')->assertForbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_can_list_only_its_scanned_tickets_using_adminapp_format(): void
|
|
||||||
{
|
|
||||||
$older = $this->createTicket('11111111-1111-4111-8111-111111111111', [
|
|
||||||
'used_at' => now()->subMinutes(2),
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
$newer = $this->createTicket('22222222-2222-4222-8222-222222222222', [
|
|
||||||
'used_at' => now()->subMinute(),
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
$otherScanner = User::factory()->create([
|
|
||||||
'rol_codigo' => RoleCode::Scanner->value,
|
|
||||||
'tenant_codigo' => $this->tenant->codigo,
|
|
||||||
]);
|
|
||||||
$this->createTicket('33333333-3333-4333-8333-333333333333', [
|
|
||||||
'used_at' => now(),
|
|
||||||
'scanner_user_id' => $otherScanner->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
|
|
||||||
$this->getJson('/api/v1/scanner/tickets')
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonCount(2, 'data')
|
|
||||||
->assertJsonPath('data.0.id', $newer->id)
|
|
||||||
->assertJsonPath('data.0.ticket', $newer->ticket)
|
|
||||||
->assertJsonPath('data.0.status', Ticket::STATUS_USED)
|
|
||||||
->assertJsonPath('data.1.id', $older->id)
|
|
||||||
->assertJsonPath('meta.current_page', 1)
|
|
||||||
->assertJsonPath('meta.total', 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_ticket_history_supports_id_search_and_pagination(): void
|
|
||||||
{
|
|
||||||
$matching = $this->createTicket('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', [
|
|
||||||
'used_at' => now(),
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
$this->createTicket('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', [
|
|
||||||
'used_at' => now()->subMinute(),
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
|
|
||||||
$this->getJson('/api/v1/scanner/tickets?q=aaaaaaaa&per_page=1')
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonCount(1, 'data')
|
|
||||||
->assertJsonPath('data.0.ticket', $matching->ticket)
|
|
||||||
->assertJsonPath('meta.current_page', 1)
|
|
||||||
->assertJsonPath('meta.per_page', 1)
|
|
||||||
->assertJsonPath('meta.total', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_ticket_history_can_be_searched_by_database_id(): void
|
|
||||||
{
|
|
||||||
$matching = $this->createTicket('cccccccc-cccc-4ccc-8ccc-cccccccccccc', [
|
|
||||||
'used_at' => now(),
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
$this->createTicket('dddddddd-dddd-4ddd-8ddd-dddddddddddd', [
|
|
||||||
'used_at' => now()->subMinute(),
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
|
|
||||||
$this->getJson("/api/v1/scanner/tickets?q={$matching->id}")
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonCount(1, 'data')
|
|
||||||
->assertJsonPath('data.0.id', $matching->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_ticket_history_can_be_searched_by_used_date(): void
|
|
||||||
{
|
|
||||||
$matching = $this->createTicket('eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee', [
|
|
||||||
'used_at' => '2026-08-11 14:30:00',
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
$this->createTicket('ffffffff-ffff-4fff-8fff-ffffffffffff', [
|
|
||||||
'used_at' => '2026-08-10 14:30:00',
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
|
|
||||||
$this->getJson('/api/v1/scanner/tickets?q=11%2F08%2F26')
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonCount(1, 'data')
|
|
||||||
->assertJsonPath('data.0.id', $matching->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_can_read_an_authorized_ticket_detail_by_uuid(): void
|
|
||||||
{
|
|
||||||
$ticket = $this->createTicket('44444444-4444-4444-8444-444444444444');
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
|
|
||||||
$this->getJson("/api/v1/scanner/tickets/{$ticket->ticket}")
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonPath('data.id', $ticket->id)
|
|
||||||
->assertJsonPath('data.ticket', $ticket->ticket)
|
|
||||||
->assertJsonPath('data.name', $ticket->name)
|
|
||||||
->assertJsonPath('data.client', $this->ticketOwner->nombre_apellido)
|
|
||||||
->assertJsonPath('data.category', $this->category->nombre)
|
|
||||||
->assertJsonPath('data.is_valid', true)
|
|
||||||
->assertJsonPath('data.is_used', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_cannot_read_a_ticket_from_an_unassigned_category_or_tenant(): void
|
|
||||||
{
|
|
||||||
$otherCategory = Category::query()->create([
|
|
||||||
'tenant_code' => $this->tenant->codigo,
|
|
||||||
'nombre' => 'Comidas',
|
|
||||||
]);
|
|
||||||
$unassignedTicket = $this->createTicket(
|
|
||||||
'55555555-5555-4555-8555-555555555555',
|
|
||||||
[],
|
|
||||||
$otherCategory,
|
|
||||||
);
|
|
||||||
$otherTenant = $this->createTenant('other');
|
|
||||||
$foreignCategory = Category::query()->create([
|
|
||||||
'tenant_code' => $otherTenant->codigo,
|
|
||||||
'nombre' => 'Externas',
|
|
||||||
]);
|
|
||||||
$foreignTicket = $this->createTicket(
|
|
||||||
'66666666-6666-4666-8666-666666666666',
|
|
||||||
[],
|
|
||||||
$foreignCategory,
|
|
||||||
$otherTenant,
|
|
||||||
);
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
|
|
||||||
$this->getJson("/api/v1/scanner/tickets/{$unassignedTicket->ticket}")
|
|
||||||
->assertNotFound();
|
|
||||||
$this->getJson("/api/v1/scanner/tickets/{$foreignTicket->ticket}")
|
|
||||||
->assertNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_can_scan_an_authorized_ticket_by_uuid(): void
|
|
||||||
{
|
|
||||||
$ticket = $this->createTicket('77777777-7777-4777-8777-777777777777');
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
|
|
||||||
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonPath('data.ticket', $ticket->ticket)
|
|
||||||
->assertJsonPath('data.scanner_user_id', $this->scanner->id)
|
|
||||||
->assertJsonPath('data.is_valid', false)
|
|
||||||
->assertJsonPath('data.is_used', true);
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('tickets', [
|
|
||||||
'id' => $ticket->id,
|
|
||||||
'scanner_user_id' => $this->scanner->id,
|
|
||||||
]);
|
|
||||||
$this->assertNotNull($ticket->fresh()->used_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_ticket_cannot_be_scanned_twice(): void
|
|
||||||
{
|
|
||||||
$ticket = $this->createTicket('88888888-8888-4888-8888-888888888888');
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")->assertOk();
|
|
||||||
|
|
||||||
$otherScanner = User::factory()->create([
|
|
||||||
'rol_codigo' => RoleCode::Scanner->value,
|
|
||||||
'tenant_codigo' => $this->tenant->codigo,
|
|
||||||
]);
|
|
||||||
$otherScanner->scanCategories()->attach($this->category);
|
|
||||||
Sanctum::actingAs($otherScanner);
|
|
||||||
|
|
||||||
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")
|
|
||||||
->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors('ticket');
|
|
||||||
$this->assertSame($this->scanner->id, $ticket->fresh()->scanner_user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_scanner_cannot_scan_a_ticket_from_an_unassigned_category(): void
|
|
||||||
{
|
|
||||||
$otherCategory = Category::query()->create([
|
|
||||||
'tenant_code' => $this->tenant->codigo,
|
|
||||||
'nombre' => 'Comidas',
|
|
||||||
]);
|
|
||||||
$ticket = $this->createTicket(
|
|
||||||
'99999999-9999-4999-8999-999999999999',
|
|
||||||
[],
|
|
||||||
$otherCategory,
|
|
||||||
);
|
|
||||||
Sanctum::actingAs($this->scanner);
|
|
||||||
|
|
||||||
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")
|
|
||||||
->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors('ticket');
|
|
||||||
$this->assertNull($ticket->fresh()->used_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param array<string, mixed> $attributes */
|
|
||||||
private function createTicket(
|
|
||||||
string $uuid,
|
|
||||||
array $attributes = [],
|
|
||||||
?Category $category = null,
|
|
||||||
?Tenant $tenant = null,
|
|
||||||
): Ticket {
|
|
||||||
$tenant ??= $this->tenant;
|
|
||||||
$category ??= $this->category;
|
|
||||||
$catalogItem = CatalogItem::query()->create([
|
|
||||||
'tenant_code' => $tenant->codigo,
|
|
||||||
'category_id' => $category->id,
|
|
||||||
'slug' => "ticket-{$uuid}",
|
|
||||||
'nombre' => 'Entrada general',
|
|
||||||
'descripcion' => 'Acceso general',
|
|
||||||
'precio' => 100,
|
|
||||||
'has_tickets' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return Ticket::query()->create(array_merge([
|
|
||||||
'tenant_code' => $tenant->codigo,
|
|
||||||
'ticket' => $uuid,
|
|
||||||
'name' => 'Entrada general',
|
|
||||||
'description' => 'Acceso general',
|
|
||||||
'source_catalog_item_id' => $catalogItem->id,
|
|
||||||
'user_id' => $this->ticketOwner->id,
|
|
||||||
], $attributes));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createTenant(string $code): Tenant
|
|
||||||
{
|
|
||||||
$logo = Attachment::query()->create([
|
|
||||||
'key' => (string) Str::uuid(),
|
|
||||||
'path' => "tests/{$code}-logo.png",
|
|
||||||
'filename' => "{$code}-logo.png",
|
|
||||||
'type' => 'image',
|
|
||||||
'mime_type' => 'image/png',
|
|
||||||
'extension' => 'png',
|
|
||||||
'size' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return Tenant::query()->create([
|
|
||||||
'codigo' => $code,
|
|
||||||
'nombre' => ucfirst($code),
|
|
||||||
'dominio' => "{$code}.test",
|
|
||||||
'primary_color' => '#ff7006',
|
|
||||||
'secondary_color' => '#777777',
|
|
||||||
'danger_color' => '#e04a4a',
|
|
||||||
'success_color' => '#81bc73',
|
|
||||||
'header_bg_color' => '#313131',
|
|
||||||
'footer_bg_color' => '#313131',
|
|
||||||
'header_logo_id' => $logo->id,
|
|
||||||
'footer_logo_id' => $logo->id,
|
|
||||||
'website_type_code' => 'onticket',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user