Compare commits
9 Commits
4aaa66dc38
...
feature/sc
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d7c484510 | |||
| cb98652e2f | |||
| 622a3eef86 | |||
| 498b4d9eac | |||
| 7d9489169d | |||
| 3eaa3f8202 | |||
| 254faedf2c | |||
| e0f0fb1a72 | |||
| 02cf3f3773 |
42
app/Domains/Auth/Controllers/ScannerLoginController.php
Normal file
42
app/Domains/Auth/Controllers/ScannerLoginController.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?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),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Domains/Auth/Controllers/ScannerMeController.php
Normal file
24
app/Domains/Auth/Controllers/ScannerMeController.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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,6 +53,16 @@ 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>
|
||||||
*/
|
*/
|
||||||
|
|||||||
5
app/Domains/Auth/Requests/ScannerLoginRequest.php
Normal file
5
app/Domains/Auth/Requests/ScannerLoginRequest.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Auth\Requests;
|
||||||
|
|
||||||
|
class ScannerLoginRequest extends AdminAppLoginRequest {}
|
||||||
21
app/Domains/Auth/Resources/ScannerMeResource.php
Normal file
21
app/Domains/Auth/Resources/ScannerMeResource.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?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,6 +5,7 @@ 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;
|
||||||
@@ -62,14 +63,39 @@ 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();
|
||||||
@@ -84,10 +110,21 @@ class PasswordLoginService
|
|||||||
$now,
|
$now,
|
||||||
$requiredRole,
|
$requiredRole,
|
||||||
$requiresTenant,
|
$requiresTenant,
|
||||||
|
$requiredPermission,
|
||||||
): array {
|
): array {
|
||||||
$user = User::query()
|
$user = User::query()
|
||||||
->where('email', $normalizedEmail)
|
->where('email', $normalizedEmail)
|
||||||
->where('rol_codigo', $requiredRole->value)
|
->when(
|
||||||
|
$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'),
|
||||||
@@ -122,8 +159,8 @@ class PasswordLoginService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($user === null || ! Hash::check($password, $user->password)) {
|
if ($user === null || ! Hash::check($password, $user->password)) {
|
||||||
if ($user !== null) {
|
if ($user !== null && $attemptTenantCode !== null) {
|
||||||
$this->registerFailure($user, $now, $tenantCode);
|
$this->registerFailure($user, $now, $attemptTenantCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
$outcome = $user?->locked_until?->isFuture()
|
$outcome = $user?->locked_until?->isFuture()
|
||||||
@@ -210,7 +247,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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
app/Domains/Auth/Services/ScannerContextService.php
Normal file
24
app/Domains/Auth/Services/ScannerContextService.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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,3 +25,4 @@ 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';
|
||||||
|
|||||||
11
app/Domains/Auth/routes/scanner.php
Normal file
11
app/Domains/Auth/routes/scanner.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?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);
|
||||||
|
});
|
||||||
8
app/Domains/Authorization/Enums/PermissionCode.php
Normal file
8
app/Domains/Authorization/Enums/PermissionCode.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Authorization\Enums;
|
||||||
|
|
||||||
|
enum PermissionCode: string
|
||||||
|
{
|
||||||
|
case ScanTickets = 'tickets.escanear';
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?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'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Bootstrap\Requests;
|
||||||
|
|
||||||
|
class ScannerBootstrapRequest extends TenantBootstrapRequest {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Bootstrap\Resources;
|
||||||
|
|
||||||
|
class ScannerBootstrapResource extends AdminAppBootstrapResource {}
|
||||||
19
app/Domains/Bootstrap/Services/ScannerBootstrapService.php
Normal file
19
app/Domains/Bootstrap/Services/ScannerBootstrapService.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?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,3 +7,4 @@ Route::get('tenants/bootstrap/{dominio}', TenantBootstrapController::class)
|
|||||||
->where('dominio', '.*');
|
->where('dominio', '.*');
|
||||||
|
|
||||||
require __DIR__.'/adminapp.php';
|
require __DIR__.'/adminapp.php';
|
||||||
|
require __DIR__.'/scanner.php';
|
||||||
|
|||||||
9
app/Domains/Bootstrap/routes/scanner.php
Normal file
9
app/Domains/Bootstrap/routes/scanner.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Bootstrap\Controllers\ScannerBootstrapController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get(
|
||||||
|
'v1/scanner/bootstrap/{dominio}',
|
||||||
|
ScannerBootstrapController::class
|
||||||
|
);
|
||||||
@@ -54,16 +54,26 @@ class CartController extends Controller
|
|||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
CartItem $cartItem,
|
CartItem $cartItem,
|
||||||
): CartResource {
|
): CartResource {
|
||||||
|
$updatesVariant = $request->exists('variant_id');
|
||||||
|
|
||||||
return CartResource::make(
|
return CartResource::make(
|
||||||
$this->cartService->updateItemQuantity(
|
$this->cartService->updateItem(
|
||||||
$tenant,
|
$tenant,
|
||||||
$request,
|
$request,
|
||||||
$cartItem->getKey(),
|
$cartItem->getKey(),
|
||||||
(int) $request->validated('cantidad'),
|
(int) $request->validated('cantidad'),
|
||||||
|
$updatesVariant
|
||||||
|
? ($request->validated('variant_id') !== null
|
||||||
|
? (int) $request->validated('variant_id')
|
||||||
|
: null)
|
||||||
|
: $cartItem->variant_id,
|
||||||
|
$updatesVariant,
|
||||||
)
|
)
|
||||||
)->additional([
|
)->additional([
|
||||||
'code' => 'cart.quantity_updated',
|
'code' => $updatesVariant ? 'cart.item_updated' : 'cart.quantity_updated',
|
||||||
'message' => __('api.cart.quantity_updated'),
|
'message' => $updatesVariant
|
||||||
|
? __('api.cart.item_updated')
|
||||||
|
: __('api.cart.quantity_updated'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,27 +124,85 @@ class Cart extends Model
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateItem(int $cartItemId, int $quantity): CartItem
|
public function updateItem(
|
||||||
{
|
int $cartItemId,
|
||||||
|
int $quantity,
|
||||||
|
?int $variantId = null,
|
||||||
|
bool $updateVariant = false,
|
||||||
|
): CartItem {
|
||||||
if ($quantity <= 0) {
|
if ($quantity <= 0) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'cantidad' => __('api.cart.positive_quantity'),
|
'cantidad' => __('api.cart.positive_quantity'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($cartItemId, $quantity): CartItem {
|
return DB::transaction(function () use (
|
||||||
|
$cartItemId,
|
||||||
|
$quantity,
|
||||||
|
$variantId,
|
||||||
|
$updateVariant,
|
||||||
|
): CartItem {
|
||||||
/** @var CartItem $item */
|
/** @var CartItem $item */
|
||||||
$item = $this->items()
|
$item = $this->items()
|
||||||
->where('id', $cartItemId)
|
->where('id', $cartItemId)
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$selectedItem = $this->resolveScopedItem(
|
$currentSelection = $this->resolveScopedItem(
|
||||||
$item->catalog_item_id,
|
$item->catalog_item_id,
|
||||||
$item->variant_id,
|
$item->variant_id,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
$inventoryService = app(CatalogInventoryService::class);
|
$inventoryService = app(CatalogInventoryService::class);
|
||||||
|
|
||||||
|
if ($updateVariant && $variantId !== $item->variant_id) {
|
||||||
|
$nextSelection = $this->resolveScopedItem(
|
||||||
|
$item->catalog_item_id,
|
||||||
|
$variantId,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
$otherVariantsQuantity = (int) $this->items()
|
||||||
|
->where('catalog_item_id', $item->catalog_item_id)
|
||||||
|
->whereKeyNot($item->getKey())
|
||||||
|
->sum('cantidad');
|
||||||
|
$this->assertUserPurchaseLimit(
|
||||||
|
$nextSelection,
|
||||||
|
$otherVariantsQuantity + $quantity,
|
||||||
|
);
|
||||||
|
|
||||||
|
$inventoryService->release($currentSelection, $item->cantidad);
|
||||||
|
$availableQuantity = $inventoryService->availableQuantity($nextSelection);
|
||||||
|
|
||||||
|
if ($availableQuantity !== null && $availableQuantity < $quantity) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'variant_id' => __('api.cart.insufficient_stock', ['max' => $availableQuantity]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetItem = $this->items()
|
||||||
|
->where('catalog_item_id', $item->catalog_item_id)
|
||||||
|
->where('variant_id', $variantId)
|
||||||
|
->whereKeyNot($item->getKey())
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$inventoryService->reserve($nextSelection, $quantity);
|
||||||
|
|
||||||
|
if ($targetItem !== null) {
|
||||||
|
$targetItem->cantidad += $quantity;
|
||||||
|
$targetItem->save();
|
||||||
|
$item->delete();
|
||||||
|
|
||||||
|
return $targetItem->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
$item->variant_id = $variantId;
|
||||||
|
$item->cantidad = $quantity;
|
||||||
|
$item->save();
|
||||||
|
|
||||||
|
return $item->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
$delta = $quantity - $item->cantidad;
|
$delta = $quantity - $item->cantidad;
|
||||||
|
|
||||||
if ($delta > 0) {
|
if ($delta > 0) {
|
||||||
@@ -153,12 +211,12 @@ class Cart extends Model
|
|||||||
->whereKeyNot($item->getKey())
|
->whereKeyNot($item->getKey())
|
||||||
->sum('cantidad');
|
->sum('cantidad');
|
||||||
$this->assertUserPurchaseLimit(
|
$this->assertUserPurchaseLimit(
|
||||||
$selectedItem,
|
$currentSelection,
|
||||||
$otherVariantsQuantity + $quantity,
|
$otherVariantsQuantity + $quantity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
|
$availableQuantity = $inventoryService->availableQuantity($currentSelection);
|
||||||
|
|
||||||
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
|
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
|
||||||
$maxAvailable = $availableQuantity + $item->cantidad;
|
$maxAvailable = $availableQuantity + $item->cantidad;
|
||||||
@@ -171,11 +229,11 @@ class Cart extends Model
|
|||||||
$item->save();
|
$item->save();
|
||||||
|
|
||||||
if ($delta > 0) {
|
if ($delta > 0) {
|
||||||
$inventoryService->reserve($selectedItem, $delta);
|
$inventoryService->reserve($currentSelection, $delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($delta < 0) {
|
if ($delta < 0) {
|
||||||
$inventoryService->release($selectedItem, abs($delta));
|
$inventoryService->release($currentSelection, abs($delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $item->fresh();
|
return $item->fresh();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class UpdateCartItemQuantityRequest extends FormRequest
|
|||||||
return [
|
return [
|
||||||
'cantidad' => ['required', 'integer', 'min:1'],
|
'cantidad' => ['required', 'integer', 'min:1'],
|
||||||
'catalog_item_id' => ['prohibited'],
|
'catalog_item_id' => ['prohibited'],
|
||||||
'variant_id' => ['prohibited'],
|
'variant_id' => ['sometimes', 'nullable', 'integer'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Domains\Cart\Resources;
|
namespace App\Domains\Cart\Resources;
|
||||||
|
|
||||||
use App\Domains\Cart\Models\CartItem;
|
use App\Domains\Cart\Models\CartItem;
|
||||||
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -36,6 +38,16 @@ class CartItemResource extends JsonResource
|
|||||||
'product' => $selectedItem === null ? null : [
|
'product' => $selectedItem === null ? null : [
|
||||||
'nombre' => $selectedItem->getName(),
|
'nombre' => $selectedItem->getName(),
|
||||||
'imagen' => $imageUrl,
|
'imagen' => $imageUrl,
|
||||||
|
'variants' => $this->catalogItem->visibleVariants($this->variant_id)
|
||||||
|
->map(fn (Variant $variant): array => [
|
||||||
|
'id' => $variant->id,
|
||||||
|
'precio' => $this->formatMoney($variant->getPrice()),
|
||||||
|
'stock_tecnico' => $this->catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
||||||
|
? null
|
||||||
|
: $variant->inventory->availableStock(),
|
||||||
|
'values' => $variant->selectionOptions($this->catalogItem->itemAttributes),
|
||||||
|
])
|
||||||
|
->values(),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,11 +51,17 @@ class CartService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateItemQuantity(Tenant $tenant, Request $request, int $cartItemId, int $quantity): Cart
|
public function updateItem(
|
||||||
{
|
Tenant $tenant,
|
||||||
|
Request $request,
|
||||||
|
int $cartItemId,
|
||||||
|
int $quantity,
|
||||||
|
?int $variantId,
|
||||||
|
bool $updateVariant,
|
||||||
|
): Cart {
|
||||||
$identity = $this->requireIdentity($request);
|
$identity = $this->requireIdentity($request);
|
||||||
$cart = $this->findCartOrFail($tenant, $identity);
|
$cart = $this->findCartOrFail($tenant, $identity);
|
||||||
$cart->updateItem($cartItemId, $quantity);
|
$cart->updateItem($cartItemId, $quantity, $variantId, $updateVariant);
|
||||||
|
|
||||||
return $this->loadCart($cart);
|
return $this->loadCart($cart);
|
||||||
}
|
}
|
||||||
@@ -101,9 +107,14 @@ class CartService
|
|||||||
return $cart->fresh()->load([
|
return $cart->fresh()->load([
|
||||||
'items.catalogItem.attachments',
|
'items.catalogItem.attachments',
|
||||||
'items.catalogItem.inventory',
|
'items.catalogItem.inventory',
|
||||||
|
'items.catalogItem.itemAttributes.attribute',
|
||||||
|
'items.catalogItem.variants.inventory',
|
||||||
|
'items.catalogItem.variants.definitions.itemAttribute.attribute.options',
|
||||||
|
'items.catalogItem.variants.eventDates',
|
||||||
|
'items.catalogItem.variants.eventDate',
|
||||||
'items.variant.attachments',
|
'items.variant.attachments',
|
||||||
'items.variant.inventory',
|
'items.variant.inventory',
|
||||||
'items.variant.definitions.itemAttribute.attribute',
|
'items.variant.definitions.itemAttribute.attribute.options',
|
||||||
'items.variant.eventDates',
|
'items.variant.eventDates',
|
||||||
'items.variant.eventDate',
|
'items.variant.eventDate',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -4,22 +4,23 @@ namespace App\Domains\Catalog\Models;
|
|||||||
|
|
||||||
use App\Domains\Attachable\Models\Attachment;
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Services\CatalogInventoryService;
|
use App\Domains\Catalog\Services\CatalogInventoryService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use App\Domains\Ticket\Models\ValidityTime;
|
use App\Domains\Ticket\Models\ValidityTime;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
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\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'tenant_code',
|
'tenant_code',
|
||||||
'event_product_type',
|
|
||||||
'category_id',
|
'category_id',
|
||||||
'brand_id',
|
'brand_id',
|
||||||
'inventory_id',
|
'inventory_id',
|
||||||
@@ -31,6 +32,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
'inventory_policy',
|
'inventory_policy',
|
||||||
'max_units_per_user',
|
'max_units_per_user',
|
||||||
'has_tickets',
|
'has_tickets',
|
||||||
|
'ticket_generation_policy',
|
||||||
'validity_time_id',
|
'validity_time_id',
|
||||||
])]
|
])]
|
||||||
class CatalogItem extends Model
|
class CatalogItem extends Model
|
||||||
@@ -45,6 +47,7 @@ class CatalogItem extends Model
|
|||||||
'type' => CatalogItemType::Standard->value,
|
'type' => CatalogItemType::Standard->value,
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
'has_tickets' => false,
|
'has_tickets' => false,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::PerEventDate->value,
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
@@ -53,12 +56,12 @@ class CatalogItem extends Model
|
|||||||
'category_id' => 'integer',
|
'category_id' => 'integer',
|
||||||
'brand_id' => 'integer',
|
'brand_id' => 'integer',
|
||||||
'inventory_id' => 'integer',
|
'inventory_id' => 'integer',
|
||||||
'event_product_type' => EventProductType::class,
|
|
||||||
'type' => CatalogItemType::class,
|
'type' => CatalogItemType::class,
|
||||||
'precio' => 'decimal:2',
|
'precio' => 'decimal:2',
|
||||||
'inventory_policy' => InventoryPolicy::class,
|
'inventory_policy' => InventoryPolicy::class,
|
||||||
'max_units_per_user' => 'integer',
|
'max_units_per_user' => 'integer',
|
||||||
'has_tickets' => 'boolean',
|
'has_tickets' => 'boolean',
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::class,
|
||||||
'validity_time_id' => 'integer',
|
'validity_time_id' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -170,6 +173,31 @@ class CatalogItem extends Model
|
|||||||
return ($this->availableStock() ?? 0) > 0;
|
return ($this->availableStock() ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param Builder<CatalogItem> $query */
|
||||||
|
public function scopeWhereVariantsAvailable(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where(function (Builder $query): void {
|
||||||
|
$query
|
||||||
|
->whereDoesntHave('variants')
|
||||||
|
->orWhere('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
|
||||||
|
->orWhereHas(
|
||||||
|
'variants.inventory',
|
||||||
|
fn (Builder $inventoryQuery): Builder => $inventoryQuery
|
||||||
|
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, Variant> */
|
||||||
|
public function visibleVariants(?int $includedVariantId = null): Collection
|
||||||
|
{
|
||||||
|
return $this->variants
|
||||||
|
->filter(fn (Variant $variant): bool => ($includedVariantId !== null && $variant->id === $includedVariantId)
|
||||||
|
|| $this->inventory_policy === InventoryPolicy::Unlimited
|
||||||
|
|| ($variant->inventory?->availableStock() ?? 0) > 0)
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
public function getPrice(): float
|
public function getPrice(): float
|
||||||
{
|
{
|
||||||
return (float) $this->precio;
|
return (float) $this->precio;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
'catalog_item_id',
|
'catalog_item_id',
|
||||||
'attribute_id',
|
'attribute_id',
|
||||||
'allow_multi_select',
|
'allow_multi_select',
|
||||||
|
'sort_order',
|
||||||
])]
|
])]
|
||||||
class ItemAttribute extends Model
|
class ItemAttribute extends Model
|
||||||
{
|
{
|
||||||
@@ -25,6 +26,7 @@ class ItemAttribute extends Model
|
|||||||
'catalog_item_id' => 'integer',
|
'catalog_item_id' => 'integer',
|
||||||
'attribute_id' => 'integer',
|
'attribute_id' => 'integer',
|
||||||
'allow_multi_select' => 'boolean',
|
'allow_multi_select' => 'boolean',
|
||||||
|
'sort_order' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,87 @@ class Variant extends Model
|
|||||||
return $values;
|
return $values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<string, array{value: string, label: string}|array<int, array{value: string, label: string}>>
|
||||||
|
*/
|
||||||
|
public function selectionOptions(?Collection $itemAttributes = null): Collection
|
||||||
|
{
|
||||||
|
$options = $this->definitions
|
||||||
|
->groupBy('item_attribute_id')
|
||||||
|
->mapWithKeys(function (Collection $definitions): array {
|
||||||
|
$itemAttribute = $definitions->first()?->itemAttribute;
|
||||||
|
$attribute = $itemAttribute?->attribute;
|
||||||
|
$attributeCode = $attribute?->codigo;
|
||||||
|
|
||||||
|
if ($attributeCode === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = $definitions
|
||||||
|
->pluck('value')
|
||||||
|
->values()
|
||||||
|
->map(function (string $value) use ($attribute): array {
|
||||||
|
$attributeOption = $attribute->options->firstWhere('value', $value);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'value' => $value,
|
||||||
|
'label' => $attributeOption?->label ?? $value,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
$attributeCode => $itemAttribute->allow_multi_select
|
||||||
|
? $values->all()
|
||||||
|
: $values->first(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$eventDateOptions = $this->selectedEventDates()
|
||||||
|
->map(fn (EventDate $eventDate): array => [
|
||||||
|
'value' => (string) $eventDate->id,
|
||||||
|
'label' => $eventDate->date->format('d/m/Y'),
|
||||||
|
])
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if ($eventDateOptions->count() === 1) {
|
||||||
|
$options->put('event_date', $eventDateOptions->first());
|
||||||
|
} elseif ($eventDateOptions->isNotEmpty()) {
|
||||||
|
$options->put('event_date', $eventDateOptions->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($itemAttributes === null) {
|
||||||
|
$itemAttributes = $this->definitions
|
||||||
|
->map(fn (VariantDefinition $definition) => $definition->itemAttribute)
|
||||||
|
->filter()
|
||||||
|
->unique('id')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if ($this->catalogItem !== null) {
|
||||||
|
$itemAttributes = $itemAttributes
|
||||||
|
->merge($this->catalogItem->itemAttributes)
|
||||||
|
->unique('id')
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ordering = $itemAttributes->mapWithKeys(function (ItemAttribute $itemAttribute): array {
|
||||||
|
$attribute = $itemAttribute->attribute;
|
||||||
|
|
||||||
|
return $attribute === null
|
||||||
|
? []
|
||||||
|
: [$attribute->codigo => [$itemAttribute->sort_order, mb_strtolower($attribute->nombre)]];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $options->sortKeysUsing(function (string $left, string $right) use ($ordering): int {
|
||||||
|
[$leftOrder, $leftLabel] = $ordering->get($left, [0, mb_strtolower($left)]);
|
||||||
|
[$rightOrder, $rightLabel] = $ordering->get($right, [0, mb_strtolower($right)]);
|
||||||
|
|
||||||
|
return $leftOrder <=> $rightOrder
|
||||||
|
?: $leftLabel <=> $rightLabel
|
||||||
|
?: $left <=> $right;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** @return Collection<int, EventDate> */
|
/** @return Collection<int, EventDate> */
|
||||||
public function selectedEventDates(): Collection
|
public function selectedEventDates(): Collection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
namespace App\Domains\Catalog\Requests;
|
namespace App\Domains\Catalog\Requests;
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
||||||
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
@@ -26,11 +26,6 @@ class StoreCatalogItemRequest extends FormRequest
|
|||||||
return [
|
return [
|
||||||
'tenant_code' => ['prohibited'],
|
'tenant_code' => ['prohibited'],
|
||||||
'type' => ['sometimes', Rule::enum(CatalogItemType::class)],
|
'type' => ['sometimes', Rule::enum(CatalogItemType::class)],
|
||||||
'event_product_type' => [
|
|
||||||
'sometimes',
|
|
||||||
'nullable',
|
|
||||||
Rule::enum(EventProductType::class),
|
|
||||||
],
|
|
||||||
'category_id' => [
|
'category_id' => [
|
||||||
'sometimes',
|
'sometimes',
|
||||||
'nullable',
|
'nullable',
|
||||||
@@ -61,6 +56,7 @@ class StoreCatalogItemRequest extends FormRequest
|
|||||||
'inventory_policy' => [Rule::prohibitedIf($isBundle), 'sometimes', Rule::enum(InventoryPolicy::class)],
|
'inventory_policy' => [Rule::prohibitedIf($isBundle), 'sometimes', Rule::enum(InventoryPolicy::class)],
|
||||||
'max_units_per_user' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
'max_units_per_user' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'has_tickets' => [Rule::prohibitedIf($isBundle), 'sometimes', 'boolean'],
|
'has_tickets' => [Rule::prohibitedIf($isBundle), 'sometimes', 'boolean'],
|
||||||
|
'ticket_generation_policy' => [Rule::prohibitedIf($isBundle), 'sometimes', Rule::enum(TicketGenerationPolicy::class)],
|
||||||
'validity_time_id' => [Rule::prohibitedIf($isBundle), 'sometimes', 'nullable', 'integer', Rule::exists('validity_times', 'id')],
|
'validity_time_id' => [Rule::prohibitedIf($isBundle), 'sometimes', 'nullable', 'integer', Rule::exists('validity_times', 'id')],
|
||||||
'real_stock' => [Rule::prohibitedIf($isBundle), 'sometimes', 'integer', 'min:0'],
|
'real_stock' => [Rule::prohibitedIf($isBundle), 'sometimes', 'integer', 'min:0'],
|
||||||
'inventory_id' => ['prohibited'],
|
'inventory_id' => ['prohibited'],
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'nombre' => $catalogItem->nombre,
|
'nombre' => $catalogItem->nombre,
|
||||||
'descripcion' => $catalogItem->descripcion,
|
'descripcion' => $catalogItem->descripcion,
|
||||||
'precio' => $catalogItem->precio,
|
'precio' => $catalogItem->precio,
|
||||||
|
'ticket_generation_policy' => $catalogItem->ticket_generation_policy->value,
|
||||||
'validity_time_id' => $catalogItem->validity_time_id,
|
'validity_time_id' => $catalogItem->validity_time_id,
|
||||||
'validity_time' => ValidityTimeResource::make($catalogItem->validityTime),
|
'validity_time' => ValidityTimeResource::make($catalogItem->validityTime),
|
||||||
'stock_tecnico' => $catalogItem->availableStock(),
|
'stock_tecnico' => $catalogItem->availableStock(),
|
||||||
'variants' => $catalogItem->variants
|
'variants' => $catalogItem->visibleVariants()
|
||||||
->map(fn (Variant $variant): array => [
|
->map(fn (Variant $variant): array => [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
'event_date_id' => $variant->event_date_id,
|
'event_date_id' => $variant->event_date_id,
|
||||||
@@ -46,7 +47,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
||||||
? null
|
? null
|
||||||
: $variant->inventory->availableStock(),
|
: $variant->inventory->availableStock(),
|
||||||
'values' => $variant->selectionValues(),
|
'values' => $variant->selectionOptions($catalogItem->itemAttributes),
|
||||||
])
|
])
|
||||||
->values(),
|
->values(),
|
||||||
];
|
];
|
||||||
@@ -65,6 +66,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'type' => $catalogItem->type->value,
|
'type' => $catalogItem->type->value,
|
||||||
'nombre' => $catalogItem->nombre,
|
'nombre' => $catalogItem->nombre,
|
||||||
'precio' => $catalogItem->precio,
|
'precio' => $catalogItem->precio,
|
||||||
|
'ticket_generation_policy' => $catalogItem->ticket_generation_policy->value,
|
||||||
'validity_time_id' => $catalogItem->validity_time_id,
|
'validity_time_id' => $catalogItem->validity_time_id,
|
||||||
'validity_time' => ValidityTimeResource::make($catalogItem->validityTime),
|
'validity_time' => ValidityTimeResource::make($catalogItem->validityTime),
|
||||||
'image' => $attachment?->getTemporaryUrl(1440),
|
'image' => $attachment?->getTemporaryUrl(1440),
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'type' => $this->type->value,
|
'type' => $this->type->value,
|
||||||
'event_product_type' => $this->event_product_type?->value,
|
|
||||||
'category_id' => $this->category_id,
|
'category_id' => $this->category_id,
|
||||||
'brand_id' => $this->brand_id,
|
'brand_id' => $this->brand_id,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
@@ -36,6 +35,7 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
'inventory_policy' => $this->inventory_policy?->value,
|
'inventory_policy' => $this->inventory_policy?->value,
|
||||||
'max_units_per_user' => $this->max_units_per_user,
|
'max_units_per_user' => $this->max_units_per_user,
|
||||||
'has_tickets' => $this->has_tickets,
|
'has_tickets' => $this->has_tickets,
|
||||||
|
'ticket_generation_policy' => $this->ticket_generation_policy->value,
|
||||||
'validity_time_id' => $this->validity_time_id,
|
'validity_time_id' => $this->validity_time_id,
|
||||||
'validity_time' => $this->validityTime === null
|
'validity_time' => $this->validityTime === null
|
||||||
? null
|
? null
|
||||||
@@ -49,7 +49,7 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
$selectedVariant === null,
|
$selectedVariant === null,
|
||||||
fn () => $this->imageUrls($this->attachments),
|
fn () => $this->imageUrls($this->attachments),
|
||||||
),
|
),
|
||||||
'variants' => $this->variants
|
'variants' => $this->visibleVariants()
|
||||||
->map(fn (Variant $variant): array => $this->variantData($variant))
|
->map(fn (Variant $variant): array => $this->variantData($variant))
|
||||||
->values(),
|
->values(),
|
||||||
'selected_variant' => $this->when(
|
'selected_variant' => $this->when(
|
||||||
@@ -87,6 +87,7 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
'id' => $attribute->id,
|
'id' => $attribute->id,
|
||||||
'codigo' => $attribute->codigo,
|
'codigo' => $attribute->codigo,
|
||||||
'nombre' => $attribute->nombre,
|
'nombre' => $attribute->nombre,
|
||||||
|
'sort_order' => $itemAttribute->sort_order,
|
||||||
'is_required' => $attribute->is_required,
|
'is_required' => $attribute->is_required,
|
||||||
'allow_multi_select' => $itemAttribute->allow_multi_select,
|
'allow_multi_select' => $itemAttribute->allow_multi_select,
|
||||||
'metadata_schema' => $attribute->metadata_schema,
|
'metadata_schema' => $attribute->metadata_schema,
|
||||||
@@ -104,12 +105,10 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
->map(fn ($eventDate, int $index): array => [
|
->map(fn ($eventDate, int $index): array => [
|
||||||
'id' => $eventDate->id,
|
'id' => $eventDate->id,
|
||||||
'value' => (string) $eventDate->id,
|
'value' => (string) $eventDate->id,
|
||||||
'label' => $eventDate->date->format('d/m/Y').' · '
|
'label' => $eventDate->date->format('d/m/Y'),
|
||||||
.substr($eventDate->time_start, 0, 5).' a '
|
|
||||||
.substr($eventDate->time_end, 0, 5),
|
|
||||||
'sort_order' => $index,
|
'sort_order' => $index,
|
||||||
'validity_time_id' => null,
|
'validity_time_id' => $eventDate->validity_time_id,
|
||||||
'validity_time' => null,
|
'validity_time' => ValidityTimeResource::make($eventDate->validityTime),
|
||||||
'metadata' => [
|
'metadata' => [
|
||||||
'date' => $eventDate->date->format('Y-m-d'),
|
'date' => $eventDate->date->format('Y-m-d'),
|
||||||
'time_start' => $eventDate->time_start,
|
'time_start' => $eventDate->time_start,
|
||||||
@@ -148,7 +147,10 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
private function attributesData(): Collection
|
private function attributesData(): Collection
|
||||||
{
|
{
|
||||||
return $this->itemAttributes
|
return $this->itemAttributes
|
||||||
->sortBy(fn (ItemAttribute $itemAttribute): int => $itemAttribute->attribute->type === FieldType::EventDate ? 0 : 1)
|
->sort(function (ItemAttribute $left, ItemAttribute $right): int {
|
||||||
|
return $left->sort_order <=> $right->sort_order
|
||||||
|
?: mb_strtolower($left->attribute->nombre) <=> mb_strtolower($right->attribute->nombre);
|
||||||
|
})
|
||||||
->map(fn (ItemAttribute $itemAttribute): array => $this->attributeData($itemAttribute))
|
->map(fn (ItemAttribute $itemAttribute): array => $this->attributeData($itemAttribute))
|
||||||
->values();
|
->values();
|
||||||
}
|
}
|
||||||
@@ -156,7 +158,7 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
private function variantData(Variant $variant): array
|
private function variantData(Variant $variant): array
|
||||||
{
|
{
|
||||||
$values = $variant->selectionValues();
|
$values = $variant->selectionOptions($this->itemAttributes);
|
||||||
$eventDates = $variant->selectedEventDates();
|
$eventDates = $variant->selectedEventDates();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class CatalogItemResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'type' => $this->type->value,
|
'type' => $this->type->value,
|
||||||
'event_product_type' => $this->event_product_type?->value,
|
|
||||||
'category_id' => $this->category_id,
|
'category_id' => $this->category_id,
|
||||||
'brand_id' => $this->brand_id,
|
'brand_id' => $this->brand_id,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
@@ -26,6 +25,7 @@ class CatalogItemResource extends JsonResource
|
|||||||
'inventory_policy' => $this->inventory_policy?->value,
|
'inventory_policy' => $this->inventory_policy?->value,
|
||||||
'max_units_per_user' => $this->max_units_per_user,
|
'max_units_per_user' => $this->max_units_per_user,
|
||||||
'has_tickets' => $this->has_tickets,
|
'has_tickets' => $this->has_tickets,
|
||||||
|
'ticket_generation_policy' => $this->ticket_generation_policy->value,
|
||||||
'validity_time_id' => $this->validity_time_id,
|
'validity_time_id' => $this->validity_time_id,
|
||||||
'validity_time' => $this->whenLoaded(
|
'validity_time' => $this->whenLoaded(
|
||||||
'validityTime',
|
'validityTime',
|
||||||
@@ -45,7 +45,7 @@ class CatalogItemResource extends JsonResource
|
|||||||
'descripcion' => $variant->getDescription(),
|
'descripcion' => $variant->getDescription(),
|
||||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||||
'real_stock' => $variant->inventory?->real_stock,
|
'real_stock' => $variant->inventory?->real_stock,
|
||||||
'values' => $variant->selectionValues(),
|
'values' => $variant->selectionOptions($this->itemAttributes),
|
||||||
'images' => $variant->attachments
|
'images' => $variant->attachments
|
||||||
->map(fn ($attachment) => $attachment->getTemporaryUrl(1440))
|
->map(fn ($attachment) => $attachment->getTemporaryUrl(1440))
|
||||||
->values(),
|
->values(),
|
||||||
|
|||||||
@@ -26,11 +26,12 @@ class CatalogSearchItemResource extends JsonResource
|
|||||||
'nombre' => $this->nombre,
|
'nombre' => $this->nombre,
|
||||||
'descripcion' => $this->descripcion,
|
'descripcion' => $this->descripcion,
|
||||||
'precio' => $this->precio,
|
'precio' => $this->precio,
|
||||||
|
'ticket_generation_policy' => $this->ticket_generation_policy->value,
|
||||||
'validity_time_id' => $this->validity_time_id,
|
'validity_time_id' => $this->validity_time_id,
|
||||||
'validity_time' => ValidityTimeResource::make($this->validityTime),
|
'validity_time' => ValidityTimeResource::make($this->validityTime),
|
||||||
'image' => $attachment?->getTemporaryUrl(1440),
|
'image' => $attachment?->getTemporaryUrl(1440),
|
||||||
'stock_tecnico' => $this->availableStock(),
|
'stock_tecnico' => $this->availableStock(),
|
||||||
'variants' => $this->variants
|
'variants' => $this->visibleVariants()
|
||||||
->map(fn (Variant $variant): array => [
|
->map(fn (Variant $variant): array => [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
'event_date_id' => $variant->event_date_id,
|
'event_date_id' => $variant->event_date_id,
|
||||||
@@ -42,7 +43,7 @@ class CatalogSearchItemResource extends JsonResource
|
|||||||
'stock_tecnico' => $this->inventory_policy === InventoryPolicy::Unlimited
|
'stock_tecnico' => $this->inventory_policy === InventoryPolicy::Unlimited
|
||||||
? null
|
? null
|
||||||
: $variant->inventory?->availableStock(),
|
: $variant->inventory?->availableStock(),
|
||||||
'values' => $variant->selectionValues(),
|
'values' => $variant->selectionOptions($this->itemAttributes),
|
||||||
])
|
])
|
||||||
->values(),
|
->values(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Domains\Catalog\Services;
|
|||||||
use App\Domains\Attachable\Models\Attachment;
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
use App\Domains\Attachable\Services\AttachmentService;
|
use App\Domains\Attachable\Services\AttachmentService;
|
||||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use App\Domains\Catalog\Models\Attribute;
|
use App\Domains\Catalog\Models\Attribute;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\Category;
|
use App\Domains\Catalog\Models\Category;
|
||||||
@@ -65,7 +64,6 @@ class CatalogService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validateEventProductType($data);
|
|
||||||
$this->validateUniqueVariantCombinations($variants, $attributeCodes);
|
$this->validateUniqueVariantCombinations($variants, $attributeCodes);
|
||||||
|
|
||||||
if ($type === CatalogItemType::Bundle) {
|
if ($type === CatalogItemType::Bundle) {
|
||||||
@@ -153,7 +151,7 @@ class CatalogService
|
|||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
'variants.eventDate',
|
'variants.eventDate',
|
||||||
'variants.eventDates',
|
'variants.eventDates',
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute.options',
|
||||||
'bundleComponents.catalogItem',
|
'bundleComponents.catalogItem',
|
||||||
'bundleComponents.variant.catalogItem',
|
'bundleComponents.variant.catalogItem',
|
||||||
]);
|
]);
|
||||||
@@ -169,22 +167,23 @@ class CatalogService
|
|||||||
'brand',
|
'brand',
|
||||||
'validityTime',
|
'validityTime',
|
||||||
'itemAttributes.attribute.options.validityTime',
|
'itemAttributes.attribute.options.validityTime',
|
||||||
'itemAttributes.attribute.eventDates',
|
'itemAttributes.attribute.eventDates.validityTime',
|
||||||
'variants' => fn ($query) => $query->orderBy('id'),
|
'variants' => fn ($query) => $query->orderBy('id'),
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
'variants.eventDate',
|
'variants.eventDate',
|
||||||
'variants.eventDates',
|
'variants.eventDates',
|
||||||
'variants.definitions' => fn ($query) => $query->orderBy('id'),
|
'variants.definitions' => fn ($query) => $query->orderBy('id'),
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute.options',
|
||||||
'bundleComponents.catalogItem.inventory',
|
'bundleComponents.catalogItem.inventory',
|
||||||
'bundleComponents.variant.inventory',
|
'bundleComponents.variant.inventory',
|
||||||
'bundleComponents.variant.definitions.itemAttribute.attribute',
|
'bundleComponents.variant.definitions.itemAttribute.attribute',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$visibleVariants = $catalogItem->visibleVariants();
|
||||||
$selectedVariant = $variantId === null
|
$selectedVariant = $variantId === null
|
||||||
? $catalogItem->variants->first()
|
? $visibleVariants->first()
|
||||||
: $catalogItem->variants->firstWhere('id', $variantId);
|
: $visibleVariants->firstWhere('id', $variantId);
|
||||||
|
|
||||||
if ($variantId !== null && $selectedVariant === null) {
|
if ($variantId !== null && $selectedVariant === null) {
|
||||||
throw new NotFoundHttpException('Variant not found for catalog item.');
|
throw new NotFoundHttpException('Variant not found for catalog item.');
|
||||||
@@ -208,6 +207,7 @@ class CatalogService
|
|||||||
|
|
||||||
$paginator = CatalogItem::query()
|
$paginator = CatalogItem::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->whereVariantsAvailable()
|
||||||
->where(function (Builder $query) use ($containsPattern): void {
|
->where(function (Builder $query) use ($containsPattern): void {
|
||||||
$query
|
$query
|
||||||
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
|
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
|
||||||
@@ -227,11 +227,12 @@ class CatalogService
|
|||||||
'attachments',
|
'attachments',
|
||||||
'inventory',
|
'inventory',
|
||||||
'validityTime',
|
'validityTime',
|
||||||
|
'itemAttributes.attribute',
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
'variants.eventDate',
|
'variants.eventDate',
|
||||||
'variants.eventDates',
|
'variants.eventDates',
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute.options',
|
||||||
'bundleComponents.catalogItem',
|
'bundleComponents.catalogItem',
|
||||||
'bundleComponents.variant.catalogItem',
|
'bundleComponents.variant.catalogItem',
|
||||||
])
|
])
|
||||||
@@ -257,15 +258,17 @@ class CatalogService
|
|||||||
return CatalogItem::query()
|
return CatalogItem::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('category_id', $category->id)
|
->where('category_id', $category->id)
|
||||||
|
->whereVariantsAvailable()
|
||||||
->with([
|
->with([
|
||||||
'attachments',
|
'attachments',
|
||||||
'inventory',
|
'inventory',
|
||||||
'validityTime',
|
'validityTime',
|
||||||
|
'itemAttributes.attribute',
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
'variants.eventDate',
|
'variants.eventDate',
|
||||||
'variants.eventDates',
|
'variants.eventDates',
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute.options',
|
||||||
'bundleComponents.catalogItem',
|
'bundleComponents.catalogItem',
|
||||||
'bundleComponents.variant.catalogItem',
|
'bundleComponents.variant.catalogItem',
|
||||||
])
|
])
|
||||||
@@ -305,6 +308,39 @@ class CatalogService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteVariant(Variant $variant): void
|
||||||
|
{
|
||||||
|
DB::transaction(function () use ($variant): void {
|
||||||
|
$variant = Variant::query()
|
||||||
|
->with('attachments')
|
||||||
|
->lockForUpdate()
|
||||||
|
->findOrFail($variant->getKey());
|
||||||
|
$catalogItem = CatalogItem::query()
|
||||||
|
->lockForUpdate()
|
||||||
|
->findOrFail($variant->catalog_item_id);
|
||||||
|
$attachments = $variant->attachments;
|
||||||
|
$inventoryId = $variant->inventory_id;
|
||||||
|
|
||||||
|
$variant->attachments()->detach();
|
||||||
|
$variant->delete();
|
||||||
|
Inventory::query()->whereKey($inventoryId)->delete();
|
||||||
|
|
||||||
|
$minimumPrice = $catalogItem->variants()->min('precio');
|
||||||
|
|
||||||
|
if ($minimumPrice === null) {
|
||||||
|
$this->delete($catalogItem);
|
||||||
|
} else {
|
||||||
|
$catalogItem->update(['precio' => $minimumPrice]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($attachments as $attachment) {
|
||||||
|
if (! DB::table('catalog_items_attachments')->where('attachment_id', $attachment->id)->exists()) {
|
||||||
|
$this->attachmentService->delete($attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private function createInventory(int $realStock): Inventory
|
private function createInventory(int $realStock): Inventory
|
||||||
{
|
{
|
||||||
return Inventory::query()->create([
|
return Inventory::query()->create([
|
||||||
@@ -406,6 +442,7 @@ class CatalogService
|
|||||||
'attribute_codes',
|
'attribute_codes',
|
||||||
'variants',
|
'variants',
|
||||||
'has_tickets',
|
'has_tickets',
|
||||||
|
'ticket_generation_policy',
|
||||||
'validity_time_id',
|
'validity_time_id',
|
||||||
] as $field) {
|
] as $field) {
|
||||||
if (array_key_exists($field, $data)) {
|
if (array_key_exists($field, $data)) {
|
||||||
@@ -586,22 +623,6 @@ class CatalogService
|
|||||||
return $variant;
|
return $variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $data */
|
|
||||||
private function validateEventProductType(array $data): void
|
|
||||||
{
|
|
||||||
$eventProductType = $data['event_product_type'] ?? null;
|
|
||||||
|
|
||||||
if ($eventProductType === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! in_array($eventProductType, EventProductType::values(), true)) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'event_product_type' => [__('api.catalog.invalid_event_product_type')],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $variants
|
* @param array<int, array<string, mixed>> $variants
|
||||||
* @param array<int, string> $attributeCodes
|
* @param array<int, string> $attributeCodes
|
||||||
|
|||||||
@@ -37,15 +37,17 @@ class FeaturedGroupService
|
|||||||
{
|
{
|
||||||
$query = CatalogItem::query()
|
$query = CatalogItem::query()
|
||||||
->where('catalog_items.tenant_code', $featuredGroup->tenant_code)
|
->where('catalog_items.tenant_code', $featuredGroup->tenant_code)
|
||||||
|
->whereVariantsAvailable()
|
||||||
->with([
|
->with([
|
||||||
'inventory',
|
'inventory',
|
||||||
'attachments',
|
'attachments',
|
||||||
'validityTime',
|
'validityTime',
|
||||||
|
'itemAttributes.attribute',
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
'variants.eventDate',
|
'variants.eventDate',
|
||||||
'variants.eventDates',
|
'variants.eventDates',
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute.options',
|
||||||
'bundleComponents.catalogItem',
|
'bundleComponents.catalogItem',
|
||||||
'bundleComponents.variant.catalogItem',
|
'bundleComponents.variant.catalogItem',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
namespace App\Domains\Event\Models;
|
namespace App\Domains\Event\Models;
|
||||||
|
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Event\Services\EventDateTextFormatter;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Enums\ValidityTimeType;
|
||||||
|
use App\Domains\Ticket\Models\ValidityTime;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -25,10 +28,31 @@ class EventDate extends Model
|
|||||||
|
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
static::creating(fn (self $eventDate) => $eventDate->syncValidityTime());
|
||||||
|
static::created(fn (self $eventDate) => $eventDate->syncTenantDateText());
|
||||||
|
static::updated(function (self $eventDate): void {
|
||||||
|
if ($eventDate->wasChanged(['date', 'time_start', 'time_end'])) {
|
||||||
|
$eventDate->syncValidityTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventDate->syncTenantDateText();
|
||||||
|
});
|
||||||
|
static::deleted(function (self $eventDate): void {
|
||||||
|
$eventDate->syncTenantDateText();
|
||||||
|
ValidityTime::query()
|
||||||
|
->whereKey($eventDate->validity_time_id)
|
||||||
|
->whereDoesntHave('ticketValidityGroups')
|
||||||
|
->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'date' => 'date:Y-m-d',
|
'date' => 'date:Y-m-d',
|
||||||
|
'validity_time_id' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +62,12 @@ class EventDate extends Model
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<ValidityTime, $this> */
|
||||||
|
public function validityTime(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ValidityTime::class);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return HasMany<Variant, $this> */
|
/** @return HasMany<Variant, $this> */
|
||||||
public function variants(): HasMany
|
public function variants(): HasMany
|
||||||
{
|
{
|
||||||
@@ -64,4 +94,48 @@ class EventDate extends Model
|
|||||||
{
|
{
|
||||||
return Carbon::parse($this->date->format('Y-m-d').' '.$this->time_end);
|
return Carbon::parse($this->date->format('Y-m-d').' '.$this->time_end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function syncTenantDateText(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->tenant()->first();
|
||||||
|
|
||||||
|
if (! $tenant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->update([
|
||||||
|
'event_date_text' => app(EventDateTextFormatter::class)->format(
|
||||||
|
$tenant->eventDates()->pluck('date')
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncValidityTime(): void
|
||||||
|
{
|
||||||
|
$startsAt = $this->startsAt();
|
||||||
|
$expiresAt = $this->endsAt();
|
||||||
|
|
||||||
|
if ($expiresAt->lessThanOrEqualTo($startsAt)) {
|
||||||
|
$expiresAt = $expiresAt->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributes = [
|
||||||
|
'type' => ValidityTimeType::FixedWindow,
|
||||||
|
'start_time' => null,
|
||||||
|
'end_time' => null,
|
||||||
|
'fixed_starts_at' => $startsAt,
|
||||||
|
'fixed_expires_at' => $expiresAt,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->validity_time_id === null) {
|
||||||
|
$validityTime = ValidityTime::query()->create($attributes);
|
||||||
|
$this->validity_time_id = $validityTime->getKey();
|
||||||
|
$this->setRelation('validityTime', $validityTime);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validityTime()->update($attributes);
|
||||||
|
$this->unsetRelation('validityTime');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Domains\Event\Resources;
|
namespace App\Domains\Event\Resources;
|
||||||
|
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Resources\ValidityTimeResource;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ class EventResource extends JsonResource
|
|||||||
'location' => $this->event_location,
|
'location' => $this->event_location,
|
||||||
'dates' => $this->eventDates->map(fn ($eventDate): array => [
|
'dates' => $this->eventDates->map(fn ($eventDate): array => [
|
||||||
'id' => $eventDate->id,
|
'id' => $eventDate->id,
|
||||||
|
'validity_time_id' => $eventDate->validity_time_id,
|
||||||
|
'validity_time' => ValidityTimeResource::make($eventDate->validityTime),
|
||||||
'date' => $eventDate->date->format('Y-m-d'),
|
'date' => $eventDate->date->format('Y-m-d'),
|
||||||
'start_time' => substr($eventDate->time_start, 0, 5),
|
'start_time' => substr($eventDate->time_start, 0, 5),
|
||||||
'end_time' => substr($eventDate->time_end, 0, 5),
|
'end_time' => substr($eventDate->time_end, 0, 5),
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ class EventService
|
|||||||
'facebook_url' => 'facebook',
|
'facebook_url' => 'facebook',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(protected EventDateTextFormatter $eventDateTextFormatter) {}
|
|
||||||
|
|
||||||
public function forTenant(Tenant $tenant): Tenant
|
public function forTenant(Tenant $tenant): Tenant
|
||||||
{
|
{
|
||||||
return $tenant->load(['eventDates', 'socialMedia']);
|
return $tenant->load(['eventDates.validityTime', 'socialMedia']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $data */
|
/** @param array<string, mixed> $data */
|
||||||
@@ -28,9 +26,6 @@ class EventService
|
|||||||
$tenant->update([
|
$tenant->update([
|
||||||
'event_title' => $data['title'],
|
'event_title' => $data['title'],
|
||||||
'event_location' => $data['location'],
|
'event_location' => $data['location'],
|
||||||
'event_date_text' => $this->eventDateTextFormatter->format(
|
|
||||||
array_column($data['dates'], 'date')
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->syncDates($tenant, $data['dates']);
|
$this->syncDates($tenant, $data['dates']);
|
||||||
@@ -40,7 +35,7 @@ class EventService
|
|||||||
$this->syncLegacyContact($tenant, $data['contact']);
|
$this->syncLegacyContact($tenant, $data['contact']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $tenant->load(['eventDates', 'socialMedia']);
|
return $tenant->load(['eventDates.validityTime', 'socialMedia']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Domains\FiestaFutbolInfantil\Resources\AccommodationResource;
|
|||||||
use App\Domains\FiestaFutbolInfantil\Services\AccommodationService;
|
use App\Domains\FiestaFutbolInfantil\Services\AccommodationService;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
class AccommodationController extends Controller
|
class AccommodationController extends Controller
|
||||||
{
|
{
|
||||||
@@ -30,4 +31,14 @@ class AccommodationController extends Controller
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, int $accommodation): Response
|
||||||
|
{
|
||||||
|
$this->accommodationService->delete(
|
||||||
|
$request->user()->tenant()->firstOrFail(),
|
||||||
|
$accommodation,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
class EntryController extends Controller
|
class EntryController extends Controller
|
||||||
{
|
{
|
||||||
@@ -34,4 +35,14 @@ class EntryController extends Controller
|
|||||||
->response()
|
->response()
|
||||||
->setStatusCode(200);
|
->setStatusCode(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, int $entry): Response
|
||||||
|
{
|
||||||
|
$this->entryService->delete(
|
||||||
|
$request->user()->tenant()->firstOrFail(),
|
||||||
|
$entry,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Domains\FiestaFutbolInfantil\Resources\FoodResource;
|
|||||||
use App\Domains\FiestaFutbolInfantil\Services\FoodService;
|
use App\Domains\FiestaFutbolInfantil\Services\FoodService;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
class FoodController extends Controller
|
class FoodController extends Controller
|
||||||
{
|
{
|
||||||
@@ -30,4 +31,14 @@ class FoodController extends Controller
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, int $food): Response
|
||||||
|
{
|
||||||
|
$this->foodService->delete(
|
||||||
|
$request->user()->tenant()->firstOrFail(),
|
||||||
|
$food,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
class MerchandiseController extends Controller
|
class MerchandiseController extends Controller
|
||||||
{
|
{
|
||||||
@@ -33,4 +34,14 @@ class MerchandiseController extends Controller
|
|||||||
->response()
|
->response()
|
||||||
->setStatusCode(200);
|
->setStatusCode(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, int $merchandise): Response
|
||||||
|
{
|
||||||
|
$this->merchandiseService->delete(
|
||||||
|
$request->user()->tenant()->firstOrFail(),
|
||||||
|
$merchandise,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ class UpsertEntriesRequest extends FormRequest
|
|||||||
Rule::exists('catalog_items', 'id')->where(
|
Rule::exists('catalog_items', 'id')->where(
|
||||||
fn ($query) => $query
|
fn ($query) => $query
|
||||||
->where('tenant_code', $tenantCode)
|
->where('tenant_code', $tenantCode)
|
||||||
->where('event_product_type', 'entrada')
|
->whereIn('category_id', fn ($categoryQuery) => $categoryQuery
|
||||||
|
->select('id')
|
||||||
|
->from('categorias')
|
||||||
|
->where('tenant_code', $tenantCode)
|
||||||
|
->where('nombre', 'Entradas'))
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
'entries.*.title' => ['required', 'string', 'max:255'],
|
'entries.*.title' => ['required', 'string', 'max:255'],
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ class UpsertMerchandiseRequest extends FormRequest
|
|||||||
Rule::exists('catalog_items', 'id')->where(
|
Rule::exists('catalog_items', 'id')->where(
|
||||||
fn ($query) => $query
|
fn ($query) => $query
|
||||||
->where('tenant_code', $tenantCode)
|
->where('tenant_code', $tenantCode)
|
||||||
->where('event_product_type', 'producto')
|
->whereIn('category_id', fn ($categoryQuery) => $categoryQuery
|
||||||
|
->select('id')
|
||||||
|
->from('categorias')
|
||||||
|
->where('tenant_code', $tenantCode)
|
||||||
|
->where('nombre', 'Merchandising'))
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
'items.*.title' => ['required', 'string', 'max:255'],
|
'items.*.title' => ['required', 'string', 'max:255'],
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Domains\FiestaFutbolInfantil\Services;
|
namespace App\Domains\FiestaFutbolInfantil\Services;
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Models\Attribute;
|
use App\Domains\Catalog\Models\Attribute;
|
||||||
use App\Domains\Catalog\Models\AttributeOption;
|
use App\Domains\Catalog\Models\AttributeOption;
|
||||||
@@ -11,7 +10,9 @@ use App\Domains\Catalog\Models\Category;
|
|||||||
use App\Domains\Catalog\Models\Inventory;
|
use App\Domains\Catalog\Models\Inventory;
|
||||||
use App\Domains\Catalog\Models\ItemAttribute;
|
use App\Domains\Catalog\Models\ItemAttribute;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@@ -20,6 +21,8 @@ class AccommodationService
|
|||||||
{
|
{
|
||||||
private const ATTRIBUTE_CODE = 'tipo_alojamiento';
|
private const ATTRIBUTE_CODE = 'tipo_alojamiento';
|
||||||
|
|
||||||
|
public function __construct(private readonly CatalogService $catalogService) {}
|
||||||
|
|
||||||
public function current(Tenant $tenant): ?CatalogItem
|
public function current(Tenant $tenant): ?CatalogItem
|
||||||
{
|
{
|
||||||
return CatalogItem::query()
|
return CatalogItem::query()
|
||||||
@@ -86,6 +89,18 @@ class AccommodationService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete(Tenant $tenant, int $accommodationId): void
|
||||||
|
{
|
||||||
|
$variant = Variant::query()
|
||||||
|
->whereKey($accommodationId)
|
||||||
|
->whereHas('catalogItem', fn ($query) => $query
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where('slug', 'alojamiento'))
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$this->catalogService->deleteVariant($variant);
|
||||||
|
}
|
||||||
|
|
||||||
private function attribute(Tenant $tenant): Attribute
|
private function attribute(Tenant $tenant): Attribute
|
||||||
{
|
{
|
||||||
$attribute = Attribute::query()
|
$attribute = Attribute::query()
|
||||||
@@ -120,9 +135,9 @@ class AccommodationService
|
|||||||
if ($accommodation !== null) {
|
if ($accommodation !== null) {
|
||||||
$accommodation->update([
|
$accommodation->update([
|
||||||
'category_id' => $category->id,
|
'category_id' => $category->id,
|
||||||
'event_product_type' => EventProductType::Product->value,
|
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
'has_tickets' => false,
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $accommodation;
|
return $accommodation;
|
||||||
@@ -135,9 +150,9 @@ class AccommodationService
|
|||||||
'descripcion' => 'Alojamiento',
|
'descripcion' => 'Alojamiento',
|
||||||
'category_id' => $category->id,
|
'category_id' => $category->id,
|
||||||
'precio' => collect($variants)->min('price') ?? 0,
|
'precio' => collect($variants)->min('price') ?? 0,
|
||||||
'event_product_type' => EventProductType::Product->value,
|
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
'has_tickets' => false,
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
'inventory_id' => null,
|
'inventory_id' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Domains\FiestaFutbolInfantil\Services;
|
namespace App\Domains\FiestaFutbolInfantil\Services;
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\Category;
|
use App\Domains\Catalog\Models\Category;
|
||||||
@@ -10,6 +9,7 @@ use App\Domains\Catalog\Models\Inventory;
|
|||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use App\Domains\Catalog\Services\CatalogService;
|
use App\Domains\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -24,7 +24,7 @@ class EntryService
|
|||||||
{
|
{
|
||||||
return CatalogItem::query()
|
return CatalogItem::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('event_product_type', EventProductType::Entry->value)
|
->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas'))
|
||||||
->with([
|
->with([
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.eventDate',
|
'variants.eventDate',
|
||||||
@@ -62,8 +62,8 @@ class EntryService
|
|||||||
'descripcion' => $entry['description'] ?? null,
|
'descripcion' => $entry['description'] ?? null,
|
||||||
'category_id' => $category->id,
|
'category_id' => $category->id,
|
||||||
'precio' => $entry['price'],
|
'precio' => $entry['price'],
|
||||||
'event_product_type' => EventProductType::Entry->value,
|
|
||||||
'has_tickets' => true,
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
'attribute_codes' => ['event_date'],
|
'attribute_codes' => ['event_date'],
|
||||||
'multi_select_attribute_codes' => ['event_date'],
|
'multi_select_attribute_codes' => ['event_date'],
|
||||||
@@ -76,13 +76,24 @@ class EntryService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete(Tenant $tenant, int $entryId): void
|
||||||
|
{
|
||||||
|
$entry = CatalogItem::query()
|
||||||
|
->whereKey($entryId)
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas'))
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$this->catalogService->delete($entry);
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $entry */
|
/** @param array<string, mixed> $entry */
|
||||||
private function update(Tenant $tenant, Category $category, array $entry, int $index): CatalogItem
|
private function update(Tenant $tenant, Category $category, array $entry, int $index): CatalogItem
|
||||||
{
|
{
|
||||||
$catalogItem = CatalogItem::query()
|
$catalogItem = CatalogItem::query()
|
||||||
->whereKey($entry['id'])
|
->whereKey($entry['id'])
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('event_product_type', EventProductType::Entry->value)
|
->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas'))
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
@@ -124,6 +135,7 @@ class EntryService
|
|||||||
'category_id' => $category->id,
|
'category_id' => $category->id,
|
||||||
'precio' => $entry['price'],
|
'precio' => $entry['price'],
|
||||||
'has_tickets' => true,
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
]);
|
]);
|
||||||
$variant->update([
|
$variant->update([
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Domains\FiestaFutbolInfantil\Services;
|
namespace App\Domains\FiestaFutbolInfantil\Services;
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Models\Attribute;
|
use App\Domains\Catalog\Models\Attribute;
|
||||||
use App\Domains\Catalog\Models\AttributeOption;
|
use App\Domains\Catalog\Models\AttributeOption;
|
||||||
@@ -11,7 +10,9 @@ use App\Domains\Catalog\Models\Category;
|
|||||||
use App\Domains\Catalog\Models\Inventory;
|
use App\Domains\Catalog\Models\Inventory;
|
||||||
use App\Domains\Catalog\Models\ItemAttribute;
|
use App\Domains\Catalog\Models\ItemAttribute;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@@ -20,6 +21,14 @@ class FoodService
|
|||||||
{
|
{
|
||||||
private const ATTRIBUTE_CODES = ['event_date', 'horario', 'servicio'];
|
private const ATTRIBUTE_CODES = ['event_date', 'horario', 'servicio'];
|
||||||
|
|
||||||
|
private const ATTRIBUTE_SORT_ORDERS = [
|
||||||
|
'event_date' => 1,
|
||||||
|
'horario' => 2,
|
||||||
|
'servicio' => 3,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(private readonly CatalogService $catalogService) {}
|
||||||
|
|
||||||
public function current(Tenant $tenant): ?CatalogItem
|
public function current(Tenant $tenant): ?CatalogItem
|
||||||
{
|
{
|
||||||
return CatalogItem::query()
|
return CatalogItem::query()
|
||||||
@@ -87,6 +96,18 @@ class FoodService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete(Tenant $tenant, int $foodId): void
|
||||||
|
{
|
||||||
|
$variant = Variant::query()
|
||||||
|
->whereKey($foodId)
|
||||||
|
->whereHas('catalogItem', fn ($query) => $query
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where('slug', 'comida'))
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$this->catalogService->deleteVariant($variant);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return Collection<string, Attribute> */
|
/** @return Collection<string, Attribute> */
|
||||||
private function attributes(Tenant $tenant): Collection
|
private function attributes(Tenant $tenant): Collection
|
||||||
{
|
{
|
||||||
@@ -125,9 +146,9 @@ class FoodService
|
|||||||
if ($food !== null) {
|
if ($food !== null) {
|
||||||
$food->update([
|
$food->update([
|
||||||
'category_id' => $category->id,
|
'category_id' => $category->id,
|
||||||
'event_product_type' => EventProductType::Product->value,
|
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
'has_tickets' => false,
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $food;
|
return $food;
|
||||||
@@ -140,9 +161,9 @@ class FoodService
|
|||||||
'descripcion' => 'Comida',
|
'descripcion' => 'Comida',
|
||||||
'category_id' => $category->id,
|
'category_id' => $category->id,
|
||||||
'precio' => collect($variants)->min('price') ?? 0,
|
'precio' => collect($variants)->min('price') ?? 0,
|
||||||
'event_product_type' => EventProductType::Product->value,
|
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
'has_tickets' => false,
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
'inventory_id' => null,
|
'inventory_id' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -154,9 +175,12 @@ class FoodService
|
|||||||
private function itemAttributes(CatalogItem $food, Collection $attributes): Collection
|
private function itemAttributes(CatalogItem $food, Collection $attributes): Collection
|
||||||
{
|
{
|
||||||
return $attributes->mapWithKeys(function (Attribute $attribute, string $code) use ($food): array {
|
return $attributes->mapWithKeys(function (Attribute $attribute, string $code) use ($food): array {
|
||||||
$itemAttribute = $food->itemAttributes()->firstOrCreate(
|
$itemAttribute = $food->itemAttributes()->updateOrCreate(
|
||||||
['attribute_id' => $attribute->id],
|
['attribute_id' => $attribute->id],
|
||||||
['allow_multi_select' => false],
|
[
|
||||||
|
'allow_multi_select' => false,
|
||||||
|
'sort_order' => self::ATTRIBUTE_SORT_ORDERS[$code],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [$code => $itemAttribute];
|
return [$code => $itemAttribute];
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Domains\FiestaFutbolInfantil\Services;
|
namespace App\Domains\FiestaFutbolInfantil\Services;
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Models\Attribute;
|
use App\Domains\Catalog\Models\Attribute;
|
||||||
use App\Domains\Catalog\Models\AttributeOption;
|
use App\Domains\Catalog\Models\AttributeOption;
|
||||||
@@ -11,7 +10,9 @@ use App\Domains\Catalog\Models\Category;
|
|||||||
use App\Domains\Catalog\Models\Inventory;
|
use App\Domains\Catalog\Models\Inventory;
|
||||||
use App\Domains\Catalog\Models\ItemAttribute;
|
use App\Domains\Catalog\Models\ItemAttribute;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -21,12 +22,13 @@ class MerchandiseService
|
|||||||
{
|
{
|
||||||
private const ATTRIBUTE_CODES = ['color', 'talle'];
|
private const ATTRIBUTE_CODES = ['color', 'talle'];
|
||||||
|
|
||||||
|
public function __construct(private readonly CatalogService $catalogService) {}
|
||||||
|
|
||||||
/** @return Collection<int, CatalogItem> */
|
/** @return Collection<int, CatalogItem> */
|
||||||
public function all(Tenant $tenant): Collection
|
public function all(Tenant $tenant): Collection
|
||||||
{
|
{
|
||||||
return CatalogItem::query()
|
return CatalogItem::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('event_product_type', EventProductType::Product->value)
|
|
||||||
->whereHas('category', fn ($query) => $query->where('nombre', 'Merchandising'))
|
->whereHas('category', fn ($query) => $query->where('nombre', 'Merchandising'))
|
||||||
->with([
|
->with([
|
||||||
'itemAttributes.attribute.options',
|
'itemAttributes.attribute.options',
|
||||||
@@ -71,9 +73,9 @@ class MerchandiseService
|
|||||||
'descripcion' => $data['description'] ?? null,
|
'descripcion' => $data['description'] ?? null,
|
||||||
'category_id' => $category->id,
|
'category_id' => $category->id,
|
||||||
'max_units_per_user' => (int) $data['max_units_per_user'],
|
'max_units_per_user' => (int) $data['max_units_per_user'],
|
||||||
'event_product_type' => EventProductType::Product->value,
|
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
'has_tickets' => false,
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$itemAttributes = $this->itemAttributes($item, $attributes);
|
$itemAttributes = $this->itemAttributes($item, $attributes);
|
||||||
@@ -120,6 +122,19 @@ class MerchandiseService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete(Tenant $tenant, int $merchandiseId): void
|
||||||
|
{
|
||||||
|
$variant = Variant::query()
|
||||||
|
->whereKey($merchandiseId)
|
||||||
|
->whereHas('catalogItem', fn ($query) => $query
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->whereHas('category', fn ($categoryQuery) => $categoryQuery
|
||||||
|
->where('nombre', 'Merchandising')))
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$this->catalogService->deleteVariant($variant);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return Collection<string, Attribute> */
|
/** @return Collection<string, Attribute> */
|
||||||
private function attributes(Tenant $tenant): Collection
|
private function attributes(Tenant $tenant): Collection
|
||||||
{
|
{
|
||||||
@@ -153,7 +168,6 @@ class MerchandiseService
|
|||||||
->whereKey($itemId)
|
->whereKey($itemId)
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('category_id', $category->id)
|
->where('category_id', $category->id)
|
||||||
->where('event_product_type', EventProductType::Product->value)
|
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -184,9 +198,9 @@ class MerchandiseService
|
|||||||
'category_id' => $category->id,
|
'category_id' => $category->id,
|
||||||
'precio' => collect($data['variants'])->min('price') ?? 0,
|
'precio' => collect($data['variants'])->min('price') ?? 0,
|
||||||
'max_units_per_user' => (int) $data['max_units_per_user'],
|
'max_units_per_user' => (int) $data['max_units_per_user'],
|
||||||
'event_product_type' => EventProductType::Product->value,
|
|
||||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
'has_tickets' => false,
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
'inventory_id' => null,
|
'inventory_id' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,22 +15,34 @@ Route::prefix('v1/adminapp/tenant')
|
|||||||
Route::post('entries', [EntryController::class, 'store'])
|
Route::post('entries', [EntryController::class, 'store'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas')
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas')
|
||||||
->name('adminapp.fiesta-futbol-infantil.entries.store');
|
->name('adminapp.fiesta-futbol-infantil.entries.store');
|
||||||
|
Route::delete('entries/{entry}', [EntryController::class, 'destroy'])
|
||||||
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas')
|
||||||
|
->name('adminapp.fiesta-futbol-infantil.entries.destroy');
|
||||||
Route::get('foods', [FoodController::class, 'index'])
|
Route::get('foods', [FoodController::class, 'index'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
||||||
->name('adminapp.fiesta-futbol-infantil.foods.index');
|
->name('adminapp.fiesta-futbol-infantil.foods.index');
|
||||||
Route::post('foods', [FoodController::class, 'store'])
|
Route::post('foods', [FoodController::class, 'store'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
||||||
->name('adminapp.fiesta-futbol-infantil.foods.store');
|
->name('adminapp.fiesta-futbol-infantil.foods.store');
|
||||||
|
Route::delete('foods/{food}', [FoodController::class, 'destroy'])
|
||||||
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
||||||
|
->name('adminapp.fiesta-futbol-infantil.foods.destroy');
|
||||||
Route::get('accommodations', [AccommodationController::class, 'index'])
|
Route::get('accommodations', [AccommodationController::class, 'index'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos')
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos')
|
||||||
->name('adminapp.fiesta-futbol-infantil.accommodations.index');
|
->name('adminapp.fiesta-futbol-infantil.accommodations.index');
|
||||||
Route::post('accommodations', [AccommodationController::class, 'store'])
|
Route::post('accommodations', [AccommodationController::class, 'store'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos')
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos')
|
||||||
->name('adminapp.fiesta-futbol-infantil.accommodations.store');
|
->name('adminapp.fiesta-futbol-infantil.accommodations.store');
|
||||||
|
Route::delete('accommodations/{accommodation}', [AccommodationController::class, 'destroy'])
|
||||||
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos')
|
||||||
|
->name('adminapp.fiesta-futbol-infantil.accommodations.destroy');
|
||||||
Route::get('merchandise', [MerchandiseController::class, 'index'])
|
Route::get('merchandise', [MerchandiseController::class, 'index'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising')
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising')
|
||||||
->name('adminapp.fiesta-futbol-infantil.merchandise.index');
|
->name('adminapp.fiesta-futbol-infantil.merchandise.index');
|
||||||
Route::post('merchandise', [MerchandiseController::class, 'store'])
|
Route::post('merchandise', [MerchandiseController::class, 'store'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising')
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising')
|
||||||
->name('adminapp.fiesta-futbol-infantil.merchandise.store');
|
->name('adminapp.fiesta-futbol-infantil.merchandise.store');
|
||||||
|
Route::delete('merchandise/{merchandise}', [MerchandiseController::class, 'destroy'])
|
||||||
|
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising')
|
||||||
|
->name('adminapp.fiesta-futbol-infantil.merchandise.destroy');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Domains\Forms\Resources;
|
|||||||
|
|
||||||
use App\Domains\Catalog\Models\AttributeOption;
|
use App\Domains\Catalog\Models\AttributeOption;
|
||||||
use App\Domains\Event\Models\EventDate;
|
use App\Domains\Event\Models\EventDate;
|
||||||
|
use App\Domains\Ticket\Resources\ValidityTimeResource;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -17,6 +18,8 @@ class FoodFormResource extends JsonResource
|
|||||||
'event_dates' => $this->resource['event_dates']->map(
|
'event_dates' => $this->resource['event_dates']->map(
|
||||||
fn (EventDate $eventDate): array => [
|
fn (EventDate $eventDate): array => [
|
||||||
'id' => $eventDate->id,
|
'id' => $eventDate->id,
|
||||||
|
'validity_time_id' => $eventDate->validity_time_id,
|
||||||
|
'validity_time' => ValidityTimeResource::make($eventDate->validityTime),
|
||||||
'date' => $eventDate->date->format('Y-m-d'),
|
'date' => $eventDate->date->format('Y-m-d'),
|
||||||
]
|
]
|
||||||
)->values(),
|
)->values(),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class FoodFormService
|
|||||||
->keyBy('codigo');
|
->keyBy('codigo');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'event_dates' => $tenant->eventDates()->get(),
|
'event_dates' => $tenant->eventDates()->with('validityTime')->get(),
|
||||||
'schedules' => $attributes->get('horario')?->options ?? new Collection,
|
'schedules' => $attributes->get('horario')?->options ?? new Collection,
|
||||||
'services' => $attributes->get('servicio')?->options ?? new Collection,
|
'services' => $attributes->get('servicio')?->options ?? new Collection,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -127,8 +127,7 @@ class TelepagosWebhookService
|
|||||||
|
|
||||||
DB::transaction(function () use ($compra, $paymentData) {
|
DB::transaction(function () use ($compra, $paymentData) {
|
||||||
TelepagosPayment::create($paymentData);
|
TelepagosPayment::create($paymentData);
|
||||||
$this->checkoutService->confirmPurchase($compra);
|
$this->checkoutService->confirmPaidPurchase($compra);
|
||||||
$compra->markAsPaid();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Log::info("Telepagos webhook: Successfully processed cashin {$cashinId} for purchase {$compra->id}");
|
Log::info("Telepagos webhook: Successfully processed cashin {$cashinId} for purchase {$compra->id}");
|
||||||
|
|||||||
@@ -18,12 +18,17 @@ class ReleaseCheckoutService
|
|||||||
|
|
||||||
public function cancel(Purchase $purchase): Purchase
|
public function cancel(Purchase $purchase): Purchase
|
||||||
{
|
{
|
||||||
return $this->release($purchase, Purchase::STATUS_CANCELLED);
|
return $this->release($purchase, Purchase::STATUS_CANCELLED, restoreCart: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelWithoutRestoringCart(Purchase $purchase): Purchase
|
||||||
|
{
|
||||||
|
return $this->release($purchase, Purchase::STATUS_CANCELLED, restoreCart: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function expire(Purchase $purchase): Purchase
|
public function expire(Purchase $purchase): Purchase
|
||||||
{
|
{
|
||||||
return $this->release($purchase, Purchase::STATUS_EXPIRED);
|
return $this->release($purchase, Purchase::STATUS_EXPIRED, restoreCart: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function expireOverdue(): int
|
public function expireOverdue(): int
|
||||||
@@ -46,9 +51,9 @@ class ReleaseCheckoutService
|
|||||||
return $expiredCount;
|
return $expiredCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function release(Purchase $purchase, string $targetStatus): Purchase
|
private function release(Purchase $purchase, string $targetStatus, bool $restoreCart): Purchase
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($purchase, $targetStatus): Purchase {
|
return DB::transaction(function () use ($purchase, $targetStatus, $restoreCart): Purchase {
|
||||||
$purchase = $this->lockPurchase($purchase);
|
$purchase = $this->lockPurchase($purchase);
|
||||||
|
|
||||||
if ($purchase->status === Purchase::STATUS_PAID) {
|
if ($purchase->status === Purchase::STATUS_PAID) {
|
||||||
@@ -76,7 +81,7 @@ class ReleaseCheckoutService
|
|||||||
->where('reservation_status', PurchaseItem::RESERVATION_ACTIVE)
|
->where('reservation_status', PurchaseItem::RESERVATION_ACTIVE)
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->get();
|
->get();
|
||||||
$reservationReturnedToCart = $this->sourceCart->restore($purchase);
|
$reservationReturnedToCart = $restoreCart && $this->sourceCart->restore($purchase);
|
||||||
|
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if (! $reservationReturnedToCart) {
|
if (! $reservationReturnedToCart) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Domains\Purchase\Services\Checkout\EditCheckoutService;
|
|||||||
use App\Domains\Purchase\Services\Checkout\ReleaseCheckoutService;
|
use App\Domains\Purchase\Services\Checkout\ReleaseCheckoutService;
|
||||||
use App\Domains\Purchase\Services\Checkout\StartCheckoutService;
|
use App\Domains\Purchase\Services\Checkout\StartCheckoutService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stable checkout API used by controllers, commands and integrations.
|
* Stable checkout API used by controllers, commands and integrations.
|
||||||
@@ -64,11 +65,26 @@ class CheckoutService
|
|||||||
$this->completer->confirm($purchase);
|
$this->completer->confirm($purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function confirmPaidPurchase(Purchase $purchase): Purchase
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($purchase): Purchase {
|
||||||
|
$this->completer->confirm($purchase);
|
||||||
|
$purchase->markAsPaid();
|
||||||
|
|
||||||
|
return $purchase->refresh()->load(['items.imageAttachment']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function cancelPurchase(Purchase $purchase): Purchase
|
public function cancelPurchase(Purchase $purchase): Purchase
|
||||||
{
|
{
|
||||||
return $this->releaser->cancel($purchase);
|
return $this->releaser->cancel($purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function cancelPurchaseWithoutRestoringCart(Purchase $purchase): Purchase
|
||||||
|
{
|
||||||
|
return $this->releaser->cancelWithoutRestoringCart($purchase);
|
||||||
|
}
|
||||||
|
|
||||||
public function expirePurchase(Purchase $purchase): Purchase
|
public function expirePurchase(Purchase $purchase): Purchase
|
||||||
{
|
{
|
||||||
return $this->releaser->expire($purchase);
|
return $this->releaser->expire($purchase);
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
namespace App\Domains\Sale\Controllers\AdminApp;
|
namespace App\Domains\Sale\Controllers\AdminApp;
|
||||||
|
|
||||||
use App\Domains\Sale\Requests\AdminAppSaleIndexRequest;
|
use App\Domains\Sale\Requests\AdminAppSaleIndexRequest;
|
||||||
|
use App\Domains\Sale\Resources\AdminApp\SaleDetailResource;
|
||||||
use App\Domains\Sale\Resources\AdminApp\SaleModificationResource;
|
use App\Domains\Sale\Resources\AdminApp\SaleModificationResource;
|
||||||
use App\Domains\Sale\Resources\AdminApp\SaleResource;
|
use App\Domains\Sale\Resources\AdminApp\SaleResource;
|
||||||
|
use App\Domains\Sale\Resources\AdminApp\SaleTicketResource;
|
||||||
use App\Domains\Sale\Services\AdminAppSalePdfService;
|
use App\Domains\Sale\Services\AdminAppSalePdfService;
|
||||||
use App\Domains\Sale\Services\AdminAppSaleService;
|
use App\Domains\Sale\Services\AdminAppSaleService;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@@ -30,6 +32,36 @@ class SaleController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, int $sale): SaleDetailResource
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return new SaleDetailResource($this->saleService->detail($tenant, $sale));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tickets(Request $request, int $sale): AnonymousResourceCollection
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return SaleTicketResource::collection(
|
||||||
|
$this->saleService->tickets($tenant, $sale)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function confirm(Request $request, int $sale): SaleResource
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return new SaleResource($this->saleService->confirm($tenant, $sale));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(Request $request, int $sale): SaleResource
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return new SaleResource($this->saleService->cancel($tenant, $sale));
|
||||||
|
}
|
||||||
|
|
||||||
public function modifications(Request $request): AnonymousResourceCollection
|
public function modifications(Request $request): AnonymousResourceCollection
|
||||||
{
|
{
|
||||||
return SaleModificationResource::collection(
|
return SaleModificationResource::collection(
|
||||||
|
|||||||
50
app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php
Normal file
50
app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Resources\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Purchase\Models\PurchaseItem;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin Purchase */
|
||||||
|
class SaleDetailResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'items' => $this->items->map(fn (PurchaseItem $item): array => [
|
||||||
|
'id' => $item->id,
|
||||||
|
'product' => $item->item_nombre,
|
||||||
|
'event_dates' => $this->eventDates($item),
|
||||||
|
'quantity' => (int) $item->cantidad,
|
||||||
|
'unit_price' => $this->formatMoney($item->precio_unitario),
|
||||||
|
'total' => $this->formatMoney($item->total),
|
||||||
|
])->values(),
|
||||||
|
'total' => $this->formatMoney($this->total),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
private function eventDates(PurchaseItem $item): array
|
||||||
|
{
|
||||||
|
return collect($item->variant_attributes ?? [])
|
||||||
|
->filter(fn (mixed $attribute): bool => is_array($attribute)
|
||||||
|
&& mb_strtolower(trim((string) ($attribute['name'] ?? ''))) === 'fecha')
|
||||||
|
->flatMap(function (array $attribute): array {
|
||||||
|
$value = $attribute['value'] ?? [];
|
||||||
|
|
||||||
|
return is_array($value) ? $value : [$value];
|
||||||
|
})
|
||||||
|
->filter(fn (mixed $date): bool => is_string($date) && $date !== '')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatMoney(float|int|string|null $amount): string
|
||||||
|
{
|
||||||
|
return number_format((float) ($amount ?? 0), 2, '.', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php
Normal file
22
app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Resources\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin Ticket */
|
||||||
|
class SaleTicketResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'product' => $this->name,
|
||||||
|
'id' => $this->id,
|
||||||
|
'expires_at' => $this->getEffectiveExpiresAt(),
|
||||||
|
'status' => $this->status,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,19 @@ namespace App\Domains\Sale\Services;
|
|||||||
|
|
||||||
use App\Domains\Logging\Models\ValueChange;
|
use App\Domains\Logging\Models\ValueChange;
|
||||||
use App\Domains\Purchase\Models\Purchase;
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Purchase\Services\CheckoutService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class AdminAppSaleService
|
class AdminAppSaleService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected CheckoutService $checkoutService,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function confirmedSalesTotal(Tenant $tenant): string
|
public function confirmedSalesTotal(Tenant $tenant): string
|
||||||
{
|
{
|
||||||
$total = Purchase::query()
|
$total = Purchase::query()
|
||||||
@@ -39,6 +45,42 @@ class AdminAppSaleService
|
|||||||
->withQueryString();
|
->withQueryString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function detail(Tenant $tenant, int $saleId): Purchase
|
||||||
|
{
|
||||||
|
return Purchase::query()
|
||||||
|
->where('tenant_codigo', $tenant->codigo)
|
||||||
|
->with('items')
|
||||||
|
->findOrFail($saleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, Ticket> */
|
||||||
|
public function tickets(Tenant $tenant, int $saleId): Collection
|
||||||
|
{
|
||||||
|
return $this->findForTenant($tenant, $saleId)
|
||||||
|
->tickets()
|
||||||
|
->with('validityGroups.validityTimes')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function confirm(Tenant $tenant, int $saleId): Purchase
|
||||||
|
{
|
||||||
|
$sale = $this->findForTenant($tenant, $saleId);
|
||||||
|
|
||||||
|
return $this->saleForResponse(
|
||||||
|
$this->checkoutService->confirmPaidPurchase($sale)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(Tenant $tenant, int $saleId): Purchase
|
||||||
|
{
|
||||||
|
$sale = $this->findForTenant($tenant, $saleId);
|
||||||
|
|
||||||
|
return $this->saleForResponse(
|
||||||
|
$this->checkoutService->cancelPurchaseWithoutRestoringCart($sale)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $filters
|
* @param array<string, mixed> $filters
|
||||||
* @return Collection<int, Purchase>
|
* @return Collection<int, Purchase>
|
||||||
@@ -117,4 +159,18 @@ class AdminAppSaleService
|
|||||||
->orderByDesc('changed_at')
|
->orderByDesc('changed_at')
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function findForTenant(Tenant $tenant, int $saleId): Purchase
|
||||||
|
{
|
||||||
|
return Purchase::query()
|
||||||
|
->where('tenant_codigo', $tenant->codigo)
|
||||||
|
->findOrFail($saleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function saleForResponse(Purchase $sale): Purchase
|
||||||
|
{
|
||||||
|
return $sale->refresh()
|
||||||
|
->loadSum('items as quantity', 'cantidad')
|
||||||
|
->loadCount('tickets');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,8 @@ Route::prefix('v1/adminapp/tenant')
|
|||||||
Route::get('sales/pdf', [SaleController::class, 'downloadPdf']);
|
Route::get('sales/pdf', [SaleController::class, 'downloadPdf']);
|
||||||
Route::get('sales/modifications', [SaleController::class, 'modifications']);
|
Route::get('sales/modifications', [SaleController::class, 'modifications']);
|
||||||
Route::get('sales/modifications/pdf', [SaleController::class, 'downloadModificationsPdf']);
|
Route::get('sales/modifications/pdf', [SaleController::class, 'downloadModificationsPdf']);
|
||||||
|
Route::post('sales/{sale}/confirm', [SaleController::class, 'confirm'])->whereNumber('sale');
|
||||||
|
Route::post('sales/{sale}/cancel', [SaleController::class, 'cancel'])->whereNumber('sale');
|
||||||
|
Route::get('sales/{sale}/tickets', [SaleController::class, 'tickets'])->whereNumber('sale');
|
||||||
|
Route::get('sales/{sale}', [SaleController::class, 'show'])->whereNumber('sale');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
'search_product_layout',
|
'search_product_layout',
|
||||||
'search_group_layout',
|
'search_group_layout',
|
||||||
'search_items_per_page',
|
'search_items_per_page',
|
||||||
|
'display_categories',
|
||||||
|
'display_seach_bar',
|
||||||
'event_title',
|
'event_title',
|
||||||
'event_location',
|
'event_location',
|
||||||
'event_date_text',
|
'event_date_text',
|
||||||
@@ -45,6 +47,8 @@ class Tenant extends Model
|
|||||||
'search_product_layout' => ProductLayout::ColumnWithImage->value,
|
'search_product_layout' => ProductLayout::ColumnWithImage->value,
|
||||||
'search_group_layout' => GroupLayout::Paginated->value,
|
'search_group_layout' => GroupLayout::Paginated->value,
|
||||||
'search_items_per_page' => 12,
|
'search_items_per_page' => 12,
|
||||||
|
'display_categories' => true,
|
||||||
|
'display_seach_bar' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
public function getRouteKeyName(): string
|
public function getRouteKeyName(): string
|
||||||
@@ -63,6 +67,8 @@ class Tenant extends Model
|
|||||||
'search_product_layout' => ProductLayout::class,
|
'search_product_layout' => ProductLayout::class,
|
||||||
'search_group_layout' => GroupLayout::class,
|
'search_group_layout' => GroupLayout::class,
|
||||||
'search_items_per_page' => 'integer',
|
'search_items_per_page' => 'integer',
|
||||||
|
'display_categories' => 'boolean',
|
||||||
|
'display_seach_bar' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ class WebsiteExtra extends Model
|
|||||||
|
|
||||||
protected $table = 'websites_extras';
|
protected $table = 'websites_extras';
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'is_enabled' => true,
|
||||||
|
];
|
||||||
|
|
||||||
private mixed $resolvedConfig = null;
|
private mixed $resolvedConfig = null;
|
||||||
|
|
||||||
private bool $hasResolvedConfig = false;
|
private bool $hasResolvedConfig = false;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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',
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ class StoreTenantRequest extends FormRequest
|
|||||||
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
|
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
|
||||||
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
|
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
|
||||||
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
|
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
|
||||||
|
'display_categories' => ['sometimes', 'boolean'],
|
||||||
|
'display_seach_bar' => ['sometimes', 'boolean'],
|
||||||
'website_type_code' => [
|
'website_type_code' => [
|
||||||
'required_with:extras',
|
'required_with:extras',
|
||||||
'sometimes',
|
'sometimes',
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ class UpdateTenantRequest extends FormRequest
|
|||||||
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
|
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
|
||||||
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
|
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
|
||||||
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
|
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
|
||||||
|
'display_categories' => ['sometimes', 'boolean'],
|
||||||
|
'display_seach_bar' => ['sometimes', 'boolean'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Domains\Tenant\Models\Tenant;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @mixin Tenant
|
* @mixin Tenant
|
||||||
@@ -61,6 +62,8 @@ class TenantResource extends JsonResource
|
|||||||
'search_product_layout' => $this->search_product_layout->value,
|
'search_product_layout' => $this->search_product_layout->value,
|
||||||
'search_group_layout' => $this->search_group_layout->value,
|
'search_group_layout' => $this->search_group_layout->value,
|
||||||
'search_items_per_page' => $this->search_items_per_page,
|
'search_items_per_page' => $this->search_items_per_page,
|
||||||
|
'display_categories' => $this->display_categories,
|
||||||
|
'display_seach_bar' => $this->display_seach_bar,
|
||||||
'social_media' => $this->whenLoaded(
|
'social_media' => $this->whenLoaded(
|
||||||
'socialMedia',
|
'socialMedia',
|
||||||
fn () => $this->socialMedia
|
fn () => $this->socialMedia
|
||||||
@@ -157,6 +160,10 @@ class TenantResource extends JsonResource
|
|||||||
|
|
||||||
$formatted['submenues'] = $childrenByParent
|
$formatted['submenues'] = $childrenByParent
|
||||||
->get($menu->code, collect())
|
->get($menu->code, collect())
|
||||||
|
->sortBy(
|
||||||
|
fn (Menu $submenu) => Str::lower(Str::ascii($submenu->label)),
|
||||||
|
SORT_NATURAL
|
||||||
|
)
|
||||||
->map($formatMenu)
|
->map($formatMenu)
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
@@ -166,6 +173,10 @@ class TenantResource extends JsonResource
|
|||||||
return $menus
|
return $menus
|
||||||
->filter(fn (Menu $menu) => $menu->parent_menu_code === null
|
->filter(fn (Menu $menu) => $menu->parent_menu_code === null
|
||||||
|| ! $menuCodes->has($menu->parent_menu_code))
|
|| ! $menuCodes->has($menu->parent_menu_code))
|
||||||
|
->sortBy(
|
||||||
|
fn (Menu $menu) => Str::lower(Str::ascii($menu->label)),
|
||||||
|
SORT_NATURAL
|
||||||
|
)
|
||||||
->map($formatMenu)
|
->map($formatMenu)
|
||||||
->values();
|
->values();
|
||||||
}
|
}
|
||||||
|
|||||||
47
app/Domains/Ticket/Controllers/Scanner/TicketController.php
Normal file
47
app/Domains/Ticket/Controllers/Scanner/TicketController.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ class TicketController extends Controller
|
|||||||
$tickets = Ticket::query()
|
$tickets = Ticket::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('user_id', $request->user()->getKey())
|
->where('user_id', $request->user()->getKey())
|
||||||
->with('validityTime', 'sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
->with('validityGroups.validityTimes', 'sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class TicketController extends Controller
|
|||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('user_id', $request->user()->getKey())
|
->where('user_id', $request->user()->getKey())
|
||||||
->whereIn('id', $ticketIds)
|
->whereIn('id', $ticketIds)
|
||||||
->with('validityTime', 'sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
->with('validityGroups.validityTimes', 'sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domains\Catalog\Enums;
|
namespace App\Domains\Ticket\Enums;
|
||||||
|
|
||||||
enum EventProductType: string
|
enum TicketGenerationPolicy: string
|
||||||
{
|
{
|
||||||
case Entry = 'entrada';
|
case PerEventDate = 'per_event_date';
|
||||||
case Product = 'producto';
|
case OnePerUnit = 'one_per_unit';
|
||||||
|
|
||||||
/** @return list<string> */
|
/** @return list<string> */
|
||||||
public static function values(): array
|
public static function values(): array
|
||||||
@@ -9,9 +9,12 @@ use App\Domains\Purchase\Models\Purchase;
|
|||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||||
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;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'tenant_code',
|
'tenant_code',
|
||||||
@@ -21,7 +24,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
'source_purchase_id',
|
'source_purchase_id',
|
||||||
'source_catalog_item_id',
|
'source_catalog_item_id',
|
||||||
'source_variant_id',
|
'source_variant_id',
|
||||||
'validity_time_id',
|
|
||||||
'used_at',
|
'used_at',
|
||||||
'scanner_user_id',
|
'scanner_user_id',
|
||||||
'user_id',
|
'user_id',
|
||||||
@@ -30,12 +32,19 @@ class Ticket extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
public const STATUS_ACTIVE = 'active';
|
||||||
|
|
||||||
|
public const STATUS_EXPIRED = 'expired';
|
||||||
|
|
||||||
|
public const STATUS_USED = 'used';
|
||||||
|
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $appends = [
|
protected $appends = [
|
||||||
'is_valid',
|
'is_valid',
|
||||||
'is_expired',
|
'is_expired',
|
||||||
'is_used',
|
'is_used',
|
||||||
|
'status',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
@@ -44,7 +53,6 @@ class Ticket extends Model
|
|||||||
'source_catalog_item_id' => 'integer',
|
'source_catalog_item_id' => 'integer',
|
||||||
'source_variant_id' => 'integer',
|
'source_variant_id' => 'integer',
|
||||||
'source_purchase_id' => 'integer',
|
'source_purchase_id' => 'integer',
|
||||||
'validity_time_id' => 'integer',
|
|
||||||
'used_at' => 'datetime',
|
'used_at' => 'datetime',
|
||||||
'scanner_user_id' => 'integer',
|
'scanner_user_id' => 'integer',
|
||||||
'user_id' => 'integer',
|
'user_id' => 'integer',
|
||||||
@@ -87,18 +95,23 @@ class Ticket extends Model
|
|||||||
return $this->belongsTo(Variant::class, 'source_variant_id');
|
return $this->belongsTo(Variant::class, 'source_variant_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<ValidityTime, $this> */
|
/** @return HasMany<TicketValidityGroup, $this> */
|
||||||
public function validityTime(): BelongsTo
|
public function validityGroups(): HasMany
|
||||||
{
|
{
|
||||||
return $this->belongsTo(ValidityTime::class);
|
return $this->hasMany(TicketValidityGroup::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isValid(): bool
|
public function isValid(): bool
|
||||||
{
|
{
|
||||||
return $this->used_at === null
|
if ($this->used_at !== null) {
|
||||||
&& (
|
return false;
|
||||||
$this->validityTime === null
|
}
|
||||||
|| $this->validityTime->isValid()
|
|
||||||
|
$validityGroups = $this->resolvedValidityGroups();
|
||||||
|
|
||||||
|
return $validityGroups->isEmpty()
|
||||||
|
|| $validityGroups->contains(
|
||||||
|
fn (TicketValidityGroup $group): bool => $group->isValid()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +122,13 @@ class Ticket extends Model
|
|||||||
|
|
||||||
public function getIsExpiredAttribute(): bool
|
public function getIsExpiredAttribute(): bool
|
||||||
{
|
{
|
||||||
$expiresAt = $this->getEffectiveExpiresAt();
|
$validityGroups = $this->resolvedValidityGroups();
|
||||||
|
|
||||||
return $this->used_at === null
|
return $this->used_at === null
|
||||||
&& $expiresAt !== null
|
&& $validityGroups->isNotEmpty()
|
||||||
&& $expiresAt->lessThanOrEqualTo(now());
|
&& $validityGroups->every(
|
||||||
|
fn (TicketValidityGroup $group): bool => $group->isExpired()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getIsUsedAttribute(): bool
|
public function getIsUsedAttribute(): bool
|
||||||
@@ -121,13 +136,65 @@ class Ticket extends Model
|
|||||||
return $this->used_at !== null;
|
return $this->used_at !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getStatusAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->is_used) {
|
||||||
|
return self::STATUS_USED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->is_expired) {
|
||||||
|
return self::STATUS_EXPIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STATUS_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
public function getEffectiveStartsAt(): ?CarbonInterface
|
public function getEffectiveStartsAt(): ?CarbonInterface
|
||||||
{
|
{
|
||||||
return $this->validityTime?->startsAt();
|
return $this->resolvedValidityGroups()
|
||||||
|
->map(fn (TicketValidityGroup $group): ?CarbonInterface => $group->effectiveStartsAt())
|
||||||
|
->filter()
|
||||||
|
->sortBy(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
|
||||||
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getEffectiveExpiresAt(): ?CarbonInterface
|
public function getEffectiveExpiresAt(): ?CarbonInterface
|
||||||
{
|
{
|
||||||
return $this->validityTime?->expiresAt();
|
return $this->resolvedValidityGroups()
|
||||||
|
->map(fn (TicketValidityGroup $group): ?CarbonInterface => $group->effectiveExpiresAt())
|
||||||
|
->filter()
|
||||||
|
->sortByDesc(fn (CarbonInterface $expiresAt): int => $expiresAt->getTimestamp())
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, ValidityTime> */
|
||||||
|
public function allValidityTimes(): Collection
|
||||||
|
{
|
||||||
|
return $this->resolvedValidityGroups()
|
||||||
|
->flatMap(fn (TicketValidityGroup $group): EloquentCollection => $group->resolvedValidityTimes())
|
||||||
|
->unique(
|
||||||
|
fn (ValidityTime $validityTime): int => $validityTime->getKey()
|
||||||
|
?? spl_object_id($validityTime)
|
||||||
|
)
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return EloquentCollection<int, TicketValidityGroup> */
|
||||||
|
public function resolvedValidityGroups(): EloquentCollection
|
||||||
|
{
|
||||||
|
if ($this->relationLoaded('validityGroups')) {
|
||||||
|
return $this->getRelation('validityGroups');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->exists) {
|
||||||
|
return new EloquentCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validityGroups = $this->validityGroups()
|
||||||
|
->with('validityTimes')
|
||||||
|
->get();
|
||||||
|
$this->setRelation('validityGroups', $validityGroups);
|
||||||
|
|
||||||
|
return $validityGroups;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
app/Domains/Ticket/Models/TicketValidityGroup.php
Normal file
117
app/Domains/Ticket/Models/TicketValidityGroup.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticket\Models;
|
||||||
|
|
||||||
|
use App\Domains\Ticket\Enums\ValidityTimeType;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
#[Fillable(['ticket_id'])]
|
||||||
|
class TicketValidityGroup extends Model
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
/** @return BelongsTo<Ticket, $this> */
|
||||||
|
public function ticket(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Ticket::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsToMany<ValidityTime, $this> */
|
||||||
|
public function validityTimes(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(
|
||||||
|
ValidityTime::class,
|
||||||
|
'ticket_validity_group_times',
|
||||||
|
'ticket_validity_group_id',
|
||||||
|
'validity_time_id',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid(?CarbonInterface $at = null): bool
|
||||||
|
{
|
||||||
|
$at ??= now();
|
||||||
|
$validityTimes = $this->resolvedValidityTimes();
|
||||||
|
|
||||||
|
return $validityTimes->isNotEmpty()
|
||||||
|
&& $validityTimes->every(
|
||||||
|
fn (ValidityTime $validityTime): bool => $validityTime->isValid($at)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isExpired(?CarbonInterface $at = null): bool
|
||||||
|
{
|
||||||
|
$at ??= now();
|
||||||
|
$expiresAt = $this->effectiveExpiresAt($at);
|
||||||
|
|
||||||
|
return $expiresAt !== null && $expiresAt->lessThanOrEqualTo($at);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function effectiveStartsAt(?CarbonInterface $at = null): ?CarbonInterface
|
||||||
|
{
|
||||||
|
$at ??= now();
|
||||||
|
$anchor = $this->dateAnchor() ?? $at;
|
||||||
|
|
||||||
|
return $this->resolvedValidityTimes()
|
||||||
|
->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->startsAt($anchor))
|
||||||
|
->filter()
|
||||||
|
->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function effectiveExpiresAt(?CarbonInterface $at = null): ?CarbonInterface
|
||||||
|
{
|
||||||
|
$at ??= now();
|
||||||
|
$anchor = $this->dateAnchor() ?? $at;
|
||||||
|
|
||||||
|
return $this->resolvedValidityTimes()
|
||||||
|
->map(function (ValidityTime $validityTime) use ($anchor): ?CarbonInterface {
|
||||||
|
$startsAt = $validityTime->startsAt($anchor);
|
||||||
|
$expiresAt = $validityTime->expiresAt($anchor);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$startsAt !== null
|
||||||
|
&& $expiresAt !== null
|
||||||
|
&& $expiresAt->lessThanOrEqualTo($startsAt)
|
||||||
|
) {
|
||||||
|
return $expiresAt->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $expiresAt;
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->sortBy(fn (CarbonInterface $expiresAt): int => $expiresAt->getTimestamp())
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return EloquentCollection<int, ValidityTime> */
|
||||||
|
public function resolvedValidityTimes(): EloquentCollection
|
||||||
|
{
|
||||||
|
if ($this->relationLoaded('validityTimes')) {
|
||||||
|
return $this->getRelation('validityTimes');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->exists) {
|
||||||
|
return new EloquentCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validityTimes = $this->validityTimes()->get();
|
||||||
|
$this->setRelation('validityTimes', $validityTimes);
|
||||||
|
|
||||||
|
return $validityTimes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dateAnchor(): ?CarbonInterface
|
||||||
|
{
|
||||||
|
return $this->resolvedValidityTimes()
|
||||||
|
->filter(fn (ValidityTime $validityTime): bool => $validityTime->type === ValidityTimeType::FixedWindow)
|
||||||
|
->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->fixed_starts_at)
|
||||||
|
->filter()
|
||||||
|
->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@ namespace App\Domains\Ticket\Models;
|
|||||||
|
|
||||||
use App\Domains\Catalog\Models\AttributeOption;
|
use App\Domains\Catalog\Models\AttributeOption;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Event\Models\EventDate;
|
||||||
use App\Domains\Ticket\Enums\ValidityTimeType;
|
use App\Domains\Ticket\Enums\ValidityTimeType;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
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;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'type',
|
'type',
|
||||||
@@ -44,10 +47,21 @@ class ValidityTime extends Model
|
|||||||
return $this->hasMany(AttributeOption::class);
|
return $this->hasMany(AttributeOption::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return HasMany<Ticket, $this> */
|
/** @return HasOne<EventDate, $this> */
|
||||||
public function tickets(): HasMany
|
public function eventDate(): HasOne
|
||||||
{
|
{
|
||||||
return $this->hasMany(Ticket::class);
|
return $this->hasOne(EventDate::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsToMany<TicketValidityGroup, $this> */
|
||||||
|
public function ticketValidityGroups(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(
|
||||||
|
TicketValidityGroup::class,
|
||||||
|
'ticket_validity_group_times',
|
||||||
|
'validity_time_id',
|
||||||
|
'ticket_validity_group_id',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function startsAt(
|
public function startsAt(
|
||||||
|
|||||||
23
app/Domains/Ticket/Requests/ScannerTicketIndexRequest.php
Normal file
23
app/Domains/Ticket/Requests/ScannerTicketIndexRequest.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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,10 +18,14 @@ 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_time_id' => $this->validity_time_id,
|
'validity_times' => ValidityTimeResource::collection($this->allValidityTimes()),
|
||||||
'validity_time' => ValidityTimeResource::make($this->validityTime),
|
'validity_groups' => TicketValidityGroupResource::collection(
|
||||||
|
$this->resolvedValidityGroups()
|
||||||
|
),
|
||||||
'starts_at' => $this->getEffectiveStartsAt(),
|
'starts_at' => $this->getEffectiveStartsAt(),
|
||||||
'expires_at' => $this->getEffectiveExpiresAt(),
|
'expires_at' => $this->getEffectiveExpiresAt(),
|
||||||
'used_at' => $this->used_at,
|
'used_at' => $this->used_at,
|
||||||
|
|||||||
24
app/Domains/Ticket/Resources/TicketValidityGroupResource.php
Normal file
24
app/Domains/Ticket/Resources/TicketValidityGroupResource.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticket\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Ticket\Models\TicketValidityGroup;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin TicketValidityGroup */
|
||||||
|
class TicketValidityGroupResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'validity_times' => ValidityTimeResource::collection($this->resolvedValidityTimes()),
|
||||||
|
'starts_at' => $this->effectiveStartsAt(),
|
||||||
|
'expires_at' => $this->effectiveExpiresAt(),
|
||||||
|
'is_valid' => $this->isValid(),
|
||||||
|
'is_expired' => $this->isExpired(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
163
app/Domains/Ticket/Services/ScannerTicketService.php
Normal file
163
app/Domains/Ticket/Services/ScannerTicketService.php
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,12 @@ use App\Domains\Auth\Models\User;
|
|||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use App\Domains\Event\Models\EventDate;
|
use App\Domains\Event\Models\EventDate;
|
||||||
use App\Domains\Ticket\Enums\ValidityTimeType;
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
use App\Domains\Ticket\Exceptions\TicketGenerationException;
|
use App\Domains\Ticket\Exceptions\TicketGenerationException;
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
|
use App\Domains\Ticket\Models\TicketValidityGroup;
|
||||||
use App\Domains\Ticket\Models\ValidityTime;
|
use App\Domains\Ticket\Models\ValidityTime;
|
||||||
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -36,36 +38,51 @@ class TicketGeneratorService
|
|||||||
$quantity,
|
$quantity,
|
||||||
$sourceVariantId,
|
$sourceVariantId,
|
||||||
);
|
);
|
||||||
$fixedValidityTimes = [];
|
|
||||||
|
|
||||||
return $targets->map(function (array $target) use (
|
return $targets->map(function (array $target) use (
|
||||||
&$fixedValidityTimes,
|
|
||||||
$sourcePurchaseId,
|
$sourcePurchaseId,
|
||||||
$user,
|
$user,
|
||||||
): Ticket {
|
): Ticket {
|
||||||
$item = $target['catalog_item'];
|
$item = $target['catalog_item'];
|
||||||
$variant = $target['variant'];
|
$variant = $target['variant'];
|
||||||
$eventDate = $target['event_date'];
|
$eventDate = $target['event_date'];
|
||||||
$validityTime = $this->resolveValidityTime($item, $variant);
|
$validityGroups = $this->buildTicketValidityGroups(
|
||||||
$validityTime = $this->materializeEventDateValidityTime(
|
$item,
|
||||||
$variant,
|
$variant,
|
||||||
$eventDate,
|
$eventDate,
|
||||||
$validityTime,
|
$this->resolveValidityTime($item, $variant),
|
||||||
$fixedValidityTimes,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Ticket::query()->create([
|
$ticket = Ticket::query()->create([
|
||||||
'tenant_code' => $item->tenant_code,
|
'tenant_code' => $item->tenant_code,
|
||||||
'ticket' => (string) Str::uuid(),
|
'ticket' => (string) Str::uuid(),
|
||||||
'name' => $item->nombre,
|
'name' => $this->ticketName($item, $variant, $eventDate),
|
||||||
'description' => (string) ($item->descripcion ?? ''),
|
'description' => (string) ($item->descripcion ?? ''),
|
||||||
'source_purchase_id' => $sourcePurchaseId,
|
'source_purchase_id' => $sourcePurchaseId,
|
||||||
'source_catalog_item_id' => $item->getKey(),
|
'source_catalog_item_id' => $item->getKey(),
|
||||||
'source_variant_id' => $variant?->getKey(),
|
'source_variant_id' => $variant?->getKey(),
|
||||||
'validity_time_id' => $validityTime?->getKey(),
|
|
||||||
'used_at' => null,
|
'used_at' => null,
|
||||||
'user_id' => $user->getKey(),
|
'user_id' => $user->getKey(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$groups = $validityGroups->map(function (Collection $validityTimes) use ($ticket): TicketValidityGroup {
|
||||||
|
$group = $ticket->validityGroups()->create();
|
||||||
|
$group->validityTimes()->attach(
|
||||||
|
$validityTimes
|
||||||
|
->map(fn (ValidityTime $validityTime): int => $validityTime->getKey())
|
||||||
|
->all()
|
||||||
|
);
|
||||||
|
$group->setRelation(
|
||||||
|
'validityTimes',
|
||||||
|
new EloquentCollection($validityTimes->all()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $group;
|
||||||
|
});
|
||||||
|
|
||||||
|
$ticket->setRelation('validityGroups', new EloquentCollection($groups->all()));
|
||||||
|
|
||||||
|
return $ticket;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,8 +142,21 @@ class TicketGeneratorService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$variant->loadMissing(['eventDates', 'eventDate']);
|
$variant->loadMissing(['eventDates.validityTime', 'eventDate.validityTime']);
|
||||||
$selectedEventDates = $variant->selectedEventDates();
|
$selectedEventDates = $variant->selectedEventDates();
|
||||||
|
|
||||||
|
if ($catalogItem->ticket_generation_policy === TicketGenerationPolicy::OnePerUnit) {
|
||||||
|
$eventDate = $selectedEventDates->count() === 1
|
||||||
|
? $selectedEventDates->first()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return Collection::times($quantity, fn (): array => [
|
||||||
|
'catalog_item' => $catalogItem,
|
||||||
|
'variant' => $variant,
|
||||||
|
'event_date' => $eventDate,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$eventDates = $selectedEventDates->isEmpty()
|
$eventDates = $selectedEventDates->isEmpty()
|
||||||
? collect([null])
|
? collect([null])
|
||||||
: $selectedEventDates;
|
: $selectedEventDates;
|
||||||
@@ -206,52 +236,77 @@ class TicketGeneratorService
|
|||||||
return $validityTimes->first();
|
return $validityTimes->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function ticketName(
|
||||||
* Convert a recurring time window into the fixed date and time purchased by
|
CatalogItem $catalogItem,
|
||||||
* the customer. Equal tickets generated together share the same snapshot.
|
?Variant $variant,
|
||||||
*
|
?EventDate $eventDate,
|
||||||
* @param array<string, ValidityTime> $fixedValidityTimes
|
): string {
|
||||||
*/
|
if ($variant === null) {
|
||||||
private function materializeEventDateValidityTime(
|
return $catalogItem->nombre;
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = $variant->selectionOptions();
|
||||||
|
|
||||||
|
if ($eventDate !== null) {
|
||||||
|
$options->put('event_date', [
|
||||||
|
'value' => (string) $eventDate->getKey(),
|
||||||
|
'label' => $eventDate->date->format('d/m/Y'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$properties = $options
|
||||||
|
->flatMap(function (array $option): array {
|
||||||
|
if (array_is_list($option)) {
|
||||||
|
return collect($option)
|
||||||
|
->pluck('label')
|
||||||
|
->filter(fn ($label): bool => is_string($label) && $label !== '')
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $option['label'] ?? null;
|
||||||
|
|
||||||
|
return is_string($label) && $label !== '' ? [$label] : [];
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if ($properties->isEmpty()) {
|
||||||
|
return $catalogItem->nombre;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $catalogItem->nombre.' ('.$properties->implode(', ').')';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, Collection<int, ValidityTime>> */
|
||||||
|
private function buildTicketValidityGroups(
|
||||||
|
CatalogItem $catalogItem,
|
||||||
?Variant $variant,
|
?Variant $variant,
|
||||||
?EventDate $eventDate,
|
?EventDate $eventDate,
|
||||||
?ValidityTime $validityTime,
|
?ValidityTime $validityTime,
|
||||||
array &$fixedValidityTimes,
|
): Collection {
|
||||||
): ?ValidityTime {
|
$eventDates = collect([$eventDate]);
|
||||||
if (
|
|
||||||
$variant === null
|
|
||||||
|| $eventDate === null
|
|
||||||
) {
|
|
||||||
return $validityTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($validityTime !== null && $validityTime->type !== ValidityTimeType::TimeWindow) {
|
|
||||||
return $validityTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheKey = $eventDate->getKey().':'.($validityTime?->getKey() ?? 'event');
|
|
||||||
|
|
||||||
if (isset($fixedValidityTimes[$cacheKey])) {
|
|
||||||
return $fixedValidityTimes[$cacheKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
$startsAt = $validityTime?->startsAt($eventDate->date) ?? $eventDate->startsAt();
|
|
||||||
$expiresAt = $validityTime?->expiresAt($eventDate->date) ?? $eventDate->endsAt();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$startsAt !== null
|
$catalogItem->ticket_generation_policy === TicketGenerationPolicy::OnePerUnit
|
||||||
&& $expiresAt !== null
|
&& $variant !== null
|
||||||
&& $expiresAt->lessThanOrEqualTo($startsAt)
|
|
||||||
) {
|
) {
|
||||||
$expiresAt = $expiresAt->addDay();
|
$variant->loadMissing(['eventDates.validityTime', 'eventDate.validityTime']);
|
||||||
|
$selectedEventDates = $variant->selectedEventDates();
|
||||||
|
|
||||||
|
if ($selectedEventDates->isNotEmpty()) {
|
||||||
|
$eventDates = $selectedEventDates;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $fixedValidityTimes[$cacheKey] = ValidityTime::query()->create([
|
return $eventDates
|
||||||
'type' => ValidityTimeType::FixedWindow,
|
->map(function (?EventDate $date) use ($validityTime): Collection {
|
||||||
'start_time' => null,
|
$date?->loadMissing('validityTime');
|
||||||
'end_time' => null,
|
|
||||||
'fixed_starts_at' => $startsAt,
|
return collect([$date?->validityTime, $validityTime])
|
||||||
'fixed_expires_at' => $expiresAt,
|
->filter()
|
||||||
]);
|
->unique(fn (ValidityTime $time): int => $time->getKey())
|
||||||
|
->values();
|
||||||
|
})
|
||||||
|
->filter(fn (Collection $group): bool => $group->isNotEmpty())
|
||||||
|
->values();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ 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';
|
||||||
|
|||||||
14
app/Domains/Ticket/routes/scanner.php
Normal file
14
app/Domains/Ticket/routes/scanner.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?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');
|
||||||
|
});
|
||||||
30
app/Http/Middleware/EnsureScannerTenant.php
Normal file
30
app/Http/Middleware/EnsureScannerTenant.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?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,6 +3,7 @@
|
|||||||
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;
|
||||||
@@ -25,6 +26,7 @@ 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,6 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -43,9 +42,6 @@ return new class extends Migration
|
|||||||
->after('tenant_code')
|
->after('tenant_code')
|
||||||
->constrained('events')
|
->constrained('events')
|
||||||
->nullOnDelete();
|
->nullOnDelete();
|
||||||
$table->enum('event_product_type', EventProductType::values())
|
|
||||||
->nullable()
|
|
||||||
->after('event_id');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::table('variantes', function (Blueprint $table): void {
|
Schema::table('variantes', function (Blueprint $table): void {
|
||||||
@@ -71,7 +67,6 @@ return new class extends Migration
|
|||||||
|
|
||||||
Schema::table('catalog_items', function (Blueprint $table): void {
|
Schema::table('catalog_items', function (Blueprint $table): void {
|
||||||
$table->dropConstrainedForeignId('event_id');
|
$table->dropConstrainedForeignId('event_id');
|
||||||
$table->dropColumn('event_product_type');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::dropIfExists('event_dates');
|
Schema::dropIfExists('event_dates');
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('item_attributes', function (Blueprint $table): void {
|
||||||
|
$table->unsignedInteger('sort_order')->default(0)->after('allow_multi_select');
|
||||||
|
});
|
||||||
|
|
||||||
|
$foodItemAttributes = DB::table('item_attributes')
|
||||||
|
->join('catalog_items', 'catalog_items.id', '=', 'item_attributes.catalog_item_id')
|
||||||
|
->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id')
|
||||||
|
->where('catalog_items.slug', 'comida')
|
||||||
|
->whereIn('attribute.codigo', ['event_date', 'horario', 'servicio'])
|
||||||
|
->select('item_attributes.id', 'attribute.codigo')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$sortOrders = [
|
||||||
|
'event_date' => 1,
|
||||||
|
'horario' => 2,
|
||||||
|
'servicio' => 3,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($foodItemAttributes as $itemAttribute) {
|
||||||
|
DB::table('item_attributes')
|
||||||
|
->where('id', $itemAttribute->id)
|
||||||
|
->update(['sort_order' => $sortOrders[$itemAttribute->codigo]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('item_attributes', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('sort_order');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('catalog_items')
|
||||||
|
->where('tenant_code', 'fiesta_futbol_infantil')
|
||||||
|
->update(['has_tickets' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$entryCategoryIds = DB::table('categorias')
|
||||||
|
->select('id')
|
||||||
|
->where('tenant_code', 'fiesta_futbol_infantil')
|
||||||
|
->where('nombre', 'Entradas');
|
||||||
|
|
||||||
|
DB::table('catalog_items')
|
||||||
|
->where('tenant_code', 'fiesta_futbol_infantil')
|
||||||
|
->whereNotIn('category_id', $entryCategoryIds)
|
||||||
|
->update(['has_tickets' => false]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (! Schema::hasColumn('catalog_items', 'event_product_type')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('catalog_items', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('event_product_type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasColumn('catalog_items', 'event_product_type')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('catalog_items', function (Blueprint $table): void {
|
||||||
|
$table->enum('event_product_type', ['entrada', 'producto'])
|
||||||
|
->nullable()
|
||||||
|
->after('tenant_code');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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('tenants', function (Blueprint $table): void {
|
||||||
|
$table->boolean('display_categories')->default(true)->after('search_items_per_page');
|
||||||
|
$table->boolean('display_seach_bar')->default(true)->after('display_categories');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn(['display_categories', 'display_seach_bar']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('tenants')
|
||||||
|
->where('codigo', 'fiesta_futbol_infantil')
|
||||||
|
->update([
|
||||||
|
'display_categories' => false,
|
||||||
|
'display_seach_bar' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('tenants')
|
||||||
|
->where('codigo', 'sonder')
|
||||||
|
->update([
|
||||||
|
'display_categories' => true,
|
||||||
|
'display_seach_bar' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void {}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('websites_extras')
|
||||||
|
->whereNull('is_enabled')
|
||||||
|
->update(['is_enabled' => true]);
|
||||||
|
|
||||||
|
Schema::table('websites_extras', function (Blueprint $table): void {
|
||||||
|
$table->boolean('is_enabled')->default(true)->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('websites_extras', function (Blueprint $table): void {
|
||||||
|
$table->boolean('is_enabled')->nullable()->default(null)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('catalog_items', function (Blueprint $table): void {
|
||||||
|
$table->enum('ticket_generation_policy', TicketGenerationPolicy::values())
|
||||||
|
->default(TicketGenerationPolicy::PerEventDate->value)
|
||||||
|
->after('has_tickets');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('catalog_items')
|
||||||
|
->where('tenant_code', 'fiesta_futbol_infantil')
|
||||||
|
->update([
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('catalog_items', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('ticket_generation_policy');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('ticket_validity_times', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('ticket_id')
|
||||||
|
->constrained('tickets')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->cascadeOnDelete();
|
||||||
|
$table->foreignId('validity_time_id')
|
||||||
|
->constrained('validity_times')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->restrictOnDelete();
|
||||||
|
|
||||||
|
$table->primary(['ticket_id', 'validity_time_id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('ticket_validity_times')->insertUsing(
|
||||||
|
['ticket_id', 'validity_time_id'],
|
||||||
|
DB::table('tickets')
|
||||||
|
->select(['id', 'validity_time_id'])
|
||||||
|
->whereNotNull('validity_time_id'),
|
||||||
|
);
|
||||||
|
|
||||||
|
Schema::table('tickets', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('validity_time_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tickets', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('validity_time_id')
|
||||||
|
->nullable()
|
||||||
|
->after('source_variant_id')
|
||||||
|
->constrained('validity_times')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->restrictOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('ticket_validity_times')
|
||||||
|
->orderBy('ticket_id')
|
||||||
|
->orderBy('validity_time_id')
|
||||||
|
->get()
|
||||||
|
->groupBy('ticket_id')
|
||||||
|
->each(function ($validityTimes, int|string $ticketId): void {
|
||||||
|
DB::table('tickets')
|
||||||
|
->where('id', $ticketId)
|
||||||
|
->update([
|
||||||
|
'validity_time_id' => $validityTimes->first()->validity_time_id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('ticket_validity_times');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('event_dates', function (Blueprint $table): void {
|
||||||
|
$table->unsignedBigInteger('validity_time_id')
|
||||||
|
->nullable()
|
||||||
|
->unique()
|
||||||
|
->after('id');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('event_dates')
|
||||||
|
->orderBy('id')
|
||||||
|
->each(function (object $eventDate): void {
|
||||||
|
$startsAt = $eventDate->date.' '.$eventDate->time_start;
|
||||||
|
$expiresAt = $eventDate->date.' '.$eventDate->time_end;
|
||||||
|
|
||||||
|
if (strtotime($expiresAt) <= strtotime($startsAt)) {
|
||||||
|
$expiresAt = date('Y-m-d H:i:s', strtotime($expiresAt.' +1 day'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$validityTimeId = DB::table('validity_times')->insertGetId([
|
||||||
|
'type' => 'fixed_window',
|
||||||
|
'start_time' => null,
|
||||||
|
'end_time' => null,
|
||||||
|
'fixed_starts_at' => $startsAt,
|
||||||
|
'fixed_expires_at' => $expiresAt,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('event_dates')
|
||||||
|
->where('id', $eventDate->id)
|
||||||
|
->update(['validity_time_id' => $validityTimeId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('event_dates', function (Blueprint $table): void {
|
||||||
|
$table->unsignedBigInteger('validity_time_id')->nullable(false)->change();
|
||||||
|
$table->foreign('validity_time_id')
|
||||||
|
->references('id')
|
||||||
|
->on('validity_times')
|
||||||
|
->restrictOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$validityTimeIds = DB::table('event_dates')
|
||||||
|
->pluck('validity_time_id')
|
||||||
|
->filter()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
Schema::table('event_dates', function (Blueprint $table): void {
|
||||||
|
$table->dropForeign(['validity_time_id']);
|
||||||
|
$table->dropUnique(['validity_time_id']);
|
||||||
|
$table->dropColumn('validity_time_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('validity_times')->whereIn('id', $validityTimeIds)->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('ticket_validity_groups', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('ticket_id')
|
||||||
|
->constrained('tickets')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('ticket_validity_group_times', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('ticket_validity_group_id')
|
||||||
|
->constrained('ticket_validity_groups')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->cascadeOnDelete();
|
||||||
|
$table->foreignId('validity_time_id')
|
||||||
|
->constrained('validity_times')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->restrictOnDelete();
|
||||||
|
|
||||||
|
$table->primary(['ticket_validity_group_id', 'validity_time_id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Every former pivot row was an OR alternative, so each one becomes
|
||||||
|
// an independent group to preserve existing ticket behavior.
|
||||||
|
DB::table('ticket_validity_times')
|
||||||
|
->orderBy('ticket_id')
|
||||||
|
->orderBy('validity_time_id')
|
||||||
|
->each(function (object $association): void {
|
||||||
|
$groupId = DB::table('ticket_validity_groups')->insertGetId([
|
||||||
|
'ticket_id' => $association->ticket_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('ticket_validity_group_times')->insert([
|
||||||
|
'ticket_validity_group_id' => $groupId,
|
||||||
|
'validity_time_id' => $association->validity_time_id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('ticket_validity_times');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::create('ticket_validity_times', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('ticket_id')
|
||||||
|
->constrained('tickets')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->cascadeOnDelete();
|
||||||
|
$table->foreignId('validity_time_id')
|
||||||
|
->constrained('validity_times')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->restrictOnDelete();
|
||||||
|
|
||||||
|
$table->primary(['ticket_id', 'validity_time_id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('ticket_validity_group_times')
|
||||||
|
->join(
|
||||||
|
'ticket_validity_groups',
|
||||||
|
'ticket_validity_groups.id',
|
||||||
|
'=',
|
||||||
|
'ticket_validity_group_times.ticket_validity_group_id',
|
||||||
|
)
|
||||||
|
->select([
|
||||||
|
'ticket_validity_groups.ticket_id',
|
||||||
|
'ticket_validity_group_times.validity_time_id',
|
||||||
|
])
|
||||||
|
->distinct()
|
||||||
|
->orderBy('ticket_validity_groups.ticket_id')
|
||||||
|
->each(fn (object $association) => DB::table('ticket_validity_times')->insert([
|
||||||
|
'ticket_id' => $association->ticket_id,
|
||||||
|
'validity_time_id' => $association->validity_time_id,
|
||||||
|
]));
|
||||||
|
|
||||||
|
Schema::dropIfExists('ticket_validity_group_times');
|
||||||
|
Schema::dropIfExists('ticket_validity_groups');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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,6 +2,7 @@
|
|||||||
|
|
||||||
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;
|
||||||
@@ -81,7 +82,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.',
|
||||||
],
|
],
|
||||||
'tickets.escanear' => [
|
PermissionCode::ScanTickets->value => [
|
||||||
'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.',
|
||||||
],
|
],
|
||||||
@@ -119,12 +120,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' => [],
|
'permisos' => [PermissionCode::ScanTickets->value],
|
||||||
],
|
],
|
||||||
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' => ['tickets.escanear'],
|
'permisos' => [PermissionCode::ScanTickets->value],
|
||||||
],
|
],
|
||||||
RoleCode::User->value => [
|
RoleCode::User->value => [
|
||||||
'nombre' => 'Usuario',
|
'nombre' => 'Usuario',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\EventProductType;
|
|
||||||
use App\Domains\Catalog\Enums\FeaturedGroupSource;
|
use App\Domains\Catalog\Enums\FeaturedGroupSource;
|
||||||
use App\Domains\Catalog\Enums\GroupLayout;
|
use App\Domains\Catalog\Enums\GroupLayout;
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
@@ -11,7 +10,11 @@ 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\ValidityTimeType;
|
||||||
|
use App\Domains\Ticket\Models\ValidityTime;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
@@ -47,13 +50,30 @@ 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(fn (string $date) => $tenant->eventDates()->create([
|
->map(function (string $date) use ($tenant): EventDate {
|
||||||
|
$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, [
|
||||||
@@ -64,8 +84,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
|||||||
'attribute_codes' => ['color', 'talle'],
|
'attribute_codes' => ['color', 'talle'],
|
||||||
'variants' => collect(['Verde', 'Blanco'])
|
'variants' => collect(['Verde', 'Blanco'])
|
||||||
->crossJoin(['14', 'S', 'M', 'L', 'XL', 'XXL'])
|
->crossJoin(['14', 'S', 'M', 'L', 'XL', 'XXL'])
|
||||||
->map(fn (array $values): array => [
|
->map(fn (array $values, int $index): array => [
|
||||||
'real_stock' => 0,
|
'real_stock' => [24, 3, 18, 0, 12, 2, 25, 0, 14, 1, 19, 8][$index],
|
||||||
'values' => ['color' => $values[0], 'talle' => $values[1]],
|
'values' => ['color' => $values[0], 'talle' => $values[1]],
|
||||||
])->all(),
|
])->all(),
|
||||||
]);
|
]);
|
||||||
@@ -76,8 +96,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
|||||||
'category_id' => $categories['alojamientos']->id,
|
'category_id' => $categories['alojamientos']->id,
|
||||||
'precio' => 35000,
|
'precio' => 35000,
|
||||||
'attribute_codes' => ['tipo_alojamiento'],
|
'attribute_codes' => ['tipo_alojamiento'],
|
||||||
'variants' => collect(['Carpa', 'Motorhome'])->map(fn (string $type): array => [
|
'variants' => collect(['Carpa', 'Motorhome'])->map(fn (string $type, int $index): array => [
|
||||||
'real_stock' => 0,
|
'real_stock' => [0, 3][$index],
|
||||||
'values' => ['tipo_alojamiento' => $type],
|
'values' => ['tipo_alojamiento' => $type],
|
||||||
])->all(),
|
])->all(),
|
||||||
]);
|
]);
|
||||||
@@ -90,8 +110,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
|||||||
'attribute_codes' => ['event_date', 'horario', 'servicio'],
|
'attribute_codes' => ['event_date', 'horario', 'servicio'],
|
||||||
'variants' => $eventDates
|
'variants' => $eventDates
|
||||||
->crossJoin(['Desayuno', 'Almuerzo', 'Cena'], ['Comedor', 'Vianda'])
|
->crossJoin(['Desayuno', 'Almuerzo', 'Cena'], ['Comedor', 'Vianda'])
|
||||||
->map(fn (array $values): array => [
|
->map(fn (array $values, int $index): array => [
|
||||||
'real_stock' => 0,
|
'real_stock' => [80, 3, 65, 0, 42, 2, 70, 18, 0, 5, 55, 40, 90, 1, 35, 0, 60, 8, 75, 22, 0, 4, 50, 30][$index],
|
||||||
'event_date_ids' => [(int) $values[0]->id],
|
'event_date_ids' => [(int) $values[0]->id],
|
||||||
'descripcion' => sprintf(
|
'descripcion' => sprintf(
|
||||||
'%s del %s - %s',
|
'%s del %s - %s',
|
||||||
@@ -109,12 +129,11 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
|||||||
'nombre' => 'Abono',
|
'nombre' => 'Abono',
|
||||||
'category_id' => $categories['entradas']->id,
|
'category_id' => $categories['entradas']->id,
|
||||||
'precio' => 40000,
|
'precio' => 40000,
|
||||||
'event_product_type' => EventProductType::Entry->value,
|
|
||||||
'has_tickets' => true,
|
'has_tickets' => true,
|
||||||
'attribute_codes' => ['event_date'],
|
'attribute_codes' => ['event_date'],
|
||||||
'multi_select_attribute_codes' => ['event_date'],
|
'multi_select_attribute_codes' => ['event_date'],
|
||||||
'variants' => [[
|
'variants' => [[
|
||||||
'real_stock' => 0,
|
'real_stock' => 120,
|
||||||
'event_date_ids' => $dateIds->all(),
|
'event_date_ids' => $dateIds->all(),
|
||||||
]],
|
]],
|
||||||
]);
|
]);
|
||||||
@@ -123,8 +142,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
|||||||
'tenant_code' => $tenant->codigo,
|
'tenant_code' => $tenant->codigo,
|
||||||
'source_type' => FeaturedGroupSource::All,
|
'source_type' => FeaturedGroupSource::All,
|
||||||
'category_id' => null,
|
'category_id' => null,
|
||||||
'product_layout' => ProductLayout::ColumnWithCart,
|
'product_layout' => ProductLayout::Row,
|
||||||
'group_layout' => GroupLayout::Simple,
|
'group_layout' => GroupLayout::SimpleVertical,
|
||||||
'group_name' => 'Productos',
|
'group_name' => 'Productos',
|
||||||
'group_order' => 0,
|
'group_order' => 0,
|
||||||
]);
|
]);
|
||||||
@@ -143,10 +162,11 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
return $this->catalogService->create([
|
return $this->catalogService->create([
|
||||||
'tenant_code' => $tenant->codigo,
|
'tenant_code' => $tenant->codigo,
|
||||||
'event_product_type' => EventProductType::Product->value,
|
|
||||||
'descripcion' => $data['nombre'],
|
'descripcion' => $data['nombre'],
|
||||||
'inventory_policy' => InventoryPolicy::Unlimited->value,
|
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||||
...$data,
|
...$data,
|
||||||
|
'has_tickets' => true,
|
||||||
|
'ticket_generation_policy' => TicketGenerationPolicy::OnePerUnit->value,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ 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',
|
||||||
@@ -219,8 +226,11 @@ 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)
|
->whereNotIn('code', $adminAppMenuCodes->merge($scannerMenuCodes))
|
||||||
->pluck('code');
|
->pluck('code');
|
||||||
|
|
||||||
Role::query()
|
Role::query()
|
||||||
@@ -231,6 +241,10 @@ 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();
|
||||||
@@ -252,6 +266,12 @@ class MenuSeeder extends Seeder
|
|||||||
'adminapp.fiesta-futbol-infantil.merchandising',
|
'adminapp.fiesta-futbol-infantil.merchandising',
|
||||||
'adminapp.fiesta-futbol-infantil.comida',
|
'adminapp.fiesta-futbol-infantil.comida',
|
||||||
];
|
];
|
||||||
|
$fiestaExcludedAdminMenuCodes = [
|
||||||
|
'adminapp.inicio',
|
||||||
|
'adminapp.catalog',
|
||||||
|
'adminapp.categories',
|
||||||
|
'adminapp.combos',
|
||||||
|
];
|
||||||
$frequentlyAskedQuestions = [
|
$frequentlyAskedQuestions = [
|
||||||
[
|
[
|
||||||
'pregunta' => '¿Hay algún límite de compra?',
|
'pregunta' => '¿Hay algún límite de compra?',
|
||||||
@@ -311,6 +331,8 @@ class MenuSeeder extends Seeder
|
|||||||
|
|
||||||
if ($tenant->codigo !== 'fiesta_futbol_infantil') {
|
if ($tenant->codigo !== 'fiesta_futbol_infantil') {
|
||||||
$menuCodes = array_diff($menuCodes, $fiestaCategoryMenuCodes);
|
$menuCodes = array_diff($menuCodes, $fiestaCategoryMenuCodes);
|
||||||
|
} else {
|
||||||
|
$menuCodes = array_diff($menuCodes, $fiestaExcludedAdminMenuCodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usar sync para asociar los menues al tenant
|
// Usar sync para asociar los menues al tenant
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ class TenantSeeder extends Seeder
|
|||||||
'success_color' => '#198754',
|
'success_color' => '#198754',
|
||||||
'header_bg_color' => '#ffffff',
|
'header_bg_color' => '#ffffff',
|
||||||
'footer_bg_color' => '#313131',
|
'footer_bg_color' => '#313131',
|
||||||
|
'display_categories' => true,
|
||||||
|
'display_seach_bar' => true,
|
||||||
'header_logo' => $this->uploadedImage('images/tennants/sonder/sonder_header.png', 'sonder_header.png'),
|
'header_logo' => $this->uploadedImage('images/tennants/sonder/sonder_header.png', 'sonder_header.png'),
|
||||||
'footer_logo' => $this->uploadedImage('images/tennants/sonder/sonder_footer.png', 'sonder_footer.png'),
|
'footer_logo' => $this->uploadedImage('images/tennants/sonder/sonder_footer.png', 'sonder_footer.png'),
|
||||||
'social_media' => self::SOCIAL_MEDIA,
|
'social_media' => self::SOCIAL_MEDIA,
|
||||||
@@ -107,6 +109,8 @@ class TenantSeeder extends Seeder
|
|||||||
'success_color' => '#198754',
|
'success_color' => '#198754',
|
||||||
'header_bg_color' => '#ffffff',
|
'header_bg_color' => '#ffffff',
|
||||||
'footer_bg_color' => '#015327',
|
'footer_bg_color' => '#015327',
|
||||||
|
'display_categories' => false,
|
||||||
|
'display_seach_bar' => false,
|
||||||
'header_logo' => $this->uploadedImage(
|
'header_logo' => $this->uploadedImage(
|
||||||
'images/tennants/fiesta_futbol_infantil/futbol_infantil_header.png',
|
'images/tennants/fiesta_futbol_infantil/futbol_infantil_header.png',
|
||||||
'futbol_infantil_header.png',
|
'futbol_infantil_header.png',
|
||||||
@@ -124,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.jpg',
|
'images/tennants/fiesta_futbol_infantil/futbol_infantil_hero.png',
|
||||||
'futbol_infantil_hero.jpg',
|
'futbol_infantil_hero.png',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
'eventConfig' => [
|
'eventConfig' => [
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ 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(),
|
||||||
@@ -63,6 +64,7 @@ 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(),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ return [
|
|||||||
'cart' => [
|
'cart' => [
|
||||||
'item_added' => 'Product added to cart.',
|
'item_added' => 'Product added to cart.',
|
||||||
'quantity_updated' => 'Product quantity updated.',
|
'quantity_updated' => 'Product quantity updated.',
|
||||||
|
'item_updated' => 'Product updated.',
|
||||||
'item_removed' => 'Product removed from cart.',
|
'item_removed' => 'Product removed from cart.',
|
||||||
'positive_quantity' => 'The quantity must be greater than zero.',
|
'positive_quantity' => 'The quantity must be greater than zero.',
|
||||||
'insufficient_stock' => 'There is not enough stock for the requested product. Maximum available: :max.',
|
'insufficient_stock' => 'There is not enough stock for the requested product. Maximum available: :max.',
|
||||||
@@ -61,6 +62,10 @@ 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.',
|
||||||
@@ -97,7 +102,6 @@ return [
|
|||||||
'event_date_selection_required' => 'At least one event date must be selected.',
|
'event_date_selection_required' => 'At least one event date must be selected.',
|
||||||
'single_event_date_required' => 'Exactly one event date must be selected.',
|
'single_event_date_required' => 'Exactly one event date must be selected.',
|
||||||
'event_date_wrong_tenant' => 'Every event date must belong to the catalog item tenant.',
|
'event_date_wrong_tenant' => 'Every event date must belong to the catalog item tenant.',
|
||||||
'invalid_event_product_type' => 'The event product type is invalid.',
|
|
||||||
'duplicate_variant_combination' => 'The variant combination must be unique.',
|
'duplicate_variant_combination' => 'The variant combination must be unique.',
|
||||||
'multi_value_required' => 'At least one value must be selected.',
|
'multi_value_required' => 'At least one value must be selected.',
|
||||||
'single_value_required' => 'Exactly one value must be selected.',
|
'single_value_required' => 'Exactly one value must be selected.',
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ return [
|
|||||||
'cart' => [
|
'cart' => [
|
||||||
'item_added' => 'Producto agregado al carrito.',
|
'item_added' => 'Producto agregado al carrito.',
|
||||||
'quantity_updated' => 'Cantidad de producto actualizada.',
|
'quantity_updated' => 'Cantidad de producto actualizada.',
|
||||||
|
'item_updated' => 'Producto actualizado.',
|
||||||
'item_removed' => 'Producto eliminado del carrito.',
|
'item_removed' => 'Producto eliminado del carrito.',
|
||||||
'positive_quantity' => 'La cantidad debe ser mayor a cero.',
|
'positive_quantity' => 'La cantidad debe ser mayor a cero.',
|
||||||
'insufficient_stock' => 'Stock insuficiente para el producto solicitado. Máximo disponible: :max.',
|
'insufficient_stock' => 'Stock insuficiente para el producto solicitado. Máximo disponible: :max.',
|
||||||
@@ -61,6 +62,10 @@ 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.',
|
||||||
@@ -97,7 +102,6 @@ return [
|
|||||||
'event_date_selection_required' => 'Debe seleccionar al menos una fecha de evento.',
|
'event_date_selection_required' => 'Debe seleccionar al menos una fecha de evento.',
|
||||||
'single_event_date_required' => 'Debe seleccionar exactamente una fecha de evento.',
|
'single_event_date_required' => 'Debe seleccionar exactamente una fecha de evento.',
|
||||||
'event_date_wrong_tenant' => 'Todas las fechas del evento deben pertenecer al tenant del ítem de catálogo.',
|
'event_date_wrong_tenant' => 'Todas las fechas del evento deben pertenecer al tenant del ítem de catálogo.',
|
||||||
'invalid_event_product_type' => 'El tipo de producto de evento no es válido.',
|
|
||||||
'duplicate_variant_combination' => 'La combinación de la variante debe ser única.',
|
'duplicate_variant_combination' => 'La combinación de la variante debe ser única.',
|
||||||
'multi_value_required' => 'Debe seleccionar al menos un valor.',
|
'multi_value_required' => 'Debe seleccionar al menos un valor.',
|
||||||
'single_value_required' => 'Debe seleccionar exactamente un valor.',
|
'single_value_required' => 'Debe seleccionar exactamente un valor.',
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
116
tests/Feature/Auth/ScannerLoginControllerTest.php
Normal file
116
tests/Feature/Auth/ScannerLoginControllerTest.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
tests/Feature/Auth/ScannerMeControllerTest.php
Normal file
68
tests/Feature/Auth/ScannerMeControllerTest.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user