Compare commits

..

7 Commits

752 changed files with 6308 additions and 28582 deletions

View File

@@ -8,7 +8,6 @@ PURCHASE_CHECKOUT_EXPIRATION_MINUTES=30
PURCHASE_QR_EXPIRATION_MINUTES=15 PURCHASE_QR_EXPIRATION_MINUTES=15
PURCHASE_TELEPAGOS_EXPIRATION_MINUTES=30 PURCHASE_TELEPAGOS_EXPIRATION_MINUTES=30
PURCHASE_TRANSFER_EXPIRATION_MINUTES=1440 PURCHASE_TRANSFER_EXPIRATION_MINUTES=1440
PURCHASE_TRANSFER_CANDIDATE_AMOUNT_TOLERANCE_PERCENTAGE=5
STOCK_RESERVATION_EXPIRATION_MINUTES=30 STOCK_RESERVATION_EXPIRATION_MINUTES=30
FRONTEND_URLS=http://localhost:4200 FRONTEND_URLS=http://localhost:4200
@@ -32,7 +31,6 @@ AUTH_LOGIN_ATTEMPT_WINDOW_MINUTES=30
AUTH_LOGIN_LOCK_MINUTES=15 AUTH_LOGIN_LOCK_MINUTES=15
AUTH_LOGIN_RATE_LIMIT_PER_MINUTE=10 AUTH_LOGIN_RATE_LIMIT_PER_MINUTE=10
AUTH_LOGIN_IP_RATE_LIMIT_PER_MINUTE=30 AUTH_LOGIN_IP_RATE_LIMIT_PER_MINUTE=30
AUTH_PASSWORD_RESET_EXPIRATION_MINUTES=60
LOG_CHANNEL=daily LOG_CHANNEL=daily
LOG_STACK=single LOG_STACK=single
@@ -43,9 +41,6 @@ TELEPAGOS_LOG_LEVEL=info
TELEPAGOS_LOG_DAYS=30 TELEPAGOS_LOG_DAYS=30
COMMANDS_LOG_LEVEL=info COMMANDS_LOG_LEVEL=info
COMMANDS_LOG_DAYS=30 COMMANDS_LOG_DAYS=30
EMAILS_LOG_LEVEL=info
EMAILS_LOG_DAYS=30
EMAIL_DELIVERY_LEASE_SECONDS=300
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1

View File

@@ -1,29 +1,19 @@
# Project Conventions # Project Conventions
## Test database safety
- Tests must use SQLite `:memory:` through `tests/bootstrap.php` and `Tests\TestCase`.
- Never run tests, `migrate:fresh`, `migrate:refresh`, or `db:wipe` against a persistent database, including the developer's `shopit` database.
- Never bypass the connection safety guard to resolve test failures. Use `php tests/verify-database-safety.php` to verify isolation without queries or migrations.
## Architecture ## Architecture
This project uses a domain-oriented structure grouped by business area under This project uses a domain-oriented structure under `app/Domains`.
`app/Domains`. Cross-cutting modules live under `app/Shared`.
Business domains must live in: Each domain must live in:
```text ```text
app/Domains/<Area>/<DomainName>/ app/Domains/<DomainName>/
``` ```
The current areas are `Core`, `Commerce`, and `Ticketing`. Cross-cutting
modules live in `app/Shared/<ModuleName>/`.
Supported folders inside each domain: Supported folders inside each domain:
```text ```text
app/Domains/<Area>/<DomainName>/ app/Domains/<DomainName>/
├── Models/ ├── Models/
├── Controllers/ ├── Controllers/
├── Requests/ ├── Requests/
@@ -38,28 +28,22 @@ app/Domains/<Area>/<DomainName>/
- Root API routes live in `routes/api.php`. - Root API routes live in `routes/api.php`.
- `routes/api.php` should act as an aggregator only. - `routes/api.php` should act as an aggregator only.
- Each business domain defines its own routes in - Each domain defines its own routes in `app/Domains/<DomainName>/routes/api.php`.
`app/Domains/<Area>/<DomainName>/routes/api.php`.
- Shared modules that expose routes define them in
`app/Shared/<ModuleName>/routes/api.php`.
## Namespaces ## Namespaces
Use this namespace pattern: Use this namespace pattern:
```php ```php
App\Domains\<Area>\<DomainName>\Models App\Domains\<DomainName>\Models
App\Domains\<Area>\<DomainName>\Controllers App\Domains\<DomainName>\Controllers
App\Domains\<Area>\<DomainName>\Requests App\Domains\<DomainName>\Requests
App\Domains\<Area>\<DomainName>\Resources App\Domains\<DomainName>\Resources
App\Domains\<Area>\<DomainName>\Services App\Domains\<DomainName>\Services
App\Domains\<Area>\<DomainName>\Policies App\Domains\<DomainName>\Policies
App\Domains\<Area>\<DomainName>\Exceptions App\Domains\<DomainName>\Exceptions
``` ```
For cross-cutting modules, use `App\Shared\<ModuleName>\...`. Reusable enums
and rules live directly under `App\Shared`.
## Responsibilities ## Responsibilities
- `Models/`: Eloquent models and model relationships. - `Models/`: Eloquent models and model relationships.
@@ -90,19 +74,13 @@ and rules live directly under `App\Shared`.
- Namespace, folder, and class names must stay aligned. - Namespace, folder, and class names must stay aligned.
- Route files should be named `api.php`. - Route files should be named `api.php`.
## Current Areas and Modules ## Current Domains
- `Core`: `Administrator`, `Auth`, `Authorization`, `Bootstrap`, `Client`, - `Tenant`
`Menu`, `Staff`, `Tenant`. - `Product`
- `Commerce`: `Cart`, `Catalog`, `Purchase`, `Sale`.
- `Ticketing`: `Desfile`, `Event`, `FiestaFutbolInfantil`, `Ticket`.
- `Shared`: `Attachable`, `Forms`, `Integration`, `Logging`, `MailTest`,
`Notification`, `StorageTest`, plus shared enums and rules.
## Notes For Future Changes ## Notes For Future Changes
- New features should be added inside the corresponding domain first. - New features should be added inside the corresponding domain first.
- Add new business features to the appropriate area under `app/Domains`. - If a concern does not belong to an existing domain, create a new domain under `app/Domains`.
- Keep generic reusable code in `app/Shared`; do not use it as a catch-all for
business features.
- Avoid placing new business code in `app/Http/Controllers` or `app/Models`. - Avoid placing new business code in `app/Http/Controllers` or `app/Models`.

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Shared\Attachable\Enums; namespace App\Domains\Attachable\Enums;
enum AttachmentType: string enum AttachmentType: string
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Shared\Attachable\Exceptions; namespace App\Domains\Attachable\Exceptions;
use RuntimeException; use RuntimeException;

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Shared\Attachable\Models; namespace App\Domains\Attachable\Models;
use App\Shared\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Enums\AttachmentType;
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;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Shared\Attachable\Models; namespace App\Domains\Attachable\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;

View File

@@ -1,11 +1,11 @@
<?php <?php
namespace App\Shared\Attachable\Services; namespace App\Domains\Attachable\Services;
use App\Shared\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Exceptions\AttachmentStorageException; use App\Domains\Attachable\Exceptions\AttachmentStorageException;
use App\Shared\Attachable\Models\Attachment; use App\Domains\Attachable\Models\Attachment;
use App\Shared\Attachable\Models\AttachmentCrop; use App\Domains\Attachable\Models\AttachmentCrop;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Requests\AdminAppLoginRequest; use App\Domains\Auth\Requests\AdminAppLoginRequest;
use App\Domains\Core\Auth\Resources\UserResource; use App\Domains\Auth\Resources\UserResource;
use App\Domains\Core\Auth\Services\PasswordLoginService; use App\Domains\Auth\Services\PasswordLoginService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Auth\Resources\AdminAppMeResource; use App\Domains\Auth\Resources\AdminAppMeResource;
use App\Domains\Core\Auth\Services\AdminAppContextService; use App\Domains\Auth\Services\AdminAppContextService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\ResetPasswordAttempt; use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Core\Auth\Requests\AdminAppCreateResetPasswordAttemptRequest; use App\Domains\Auth\Requests\AdminAppCreateResetPasswordAttemptRequest;
use App\Domains\Core\Auth\Services\ResetPasswordAttemptService; use App\Domains\Auth\Services\ResetPasswordAttemptService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\ResetPasswordAttempt; use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Core\Auth\Requests\CreateResetPasswordAttemptRequest; use App\Domains\Auth\Requests\CreateResetPasswordAttemptRequest;
use App\Domains\Core\Auth\Services\ResetPasswordAttemptService; use App\Domains\Auth\Services\ResetPasswordAttemptService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\ResetPasswordAttempt; use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Core\Auth\Requests\ScannerCreateResetPasswordAttemptRequest; use App\Domains\Auth\Requests\ScannerCreateResetPasswordAttemptRequest;
use App\Domains\Core\Auth\Services\ResetPasswordAttemptService; use App\Domains\Auth\Services\ResetPasswordAttemptService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Services\GoogleAuthService; use App\Domains\Auth\Services\GoogleAuthService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View File

@@ -1,11 +1,11 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Requests\GoogleTokenExchangeRequest; use App\Domains\Auth\Requests\GoogleTokenExchangeRequest;
use App\Domains\Core\Auth\Resources\UserResource; use App\Domains\Auth\Resources\UserResource;
use App\Domains\Core\Auth\Services\GoogleAuthService; use App\Domains\Auth\Services\GoogleAuthService;
use App\Domains\Commerce\Cart\Services\GuestCartMergeService; use App\Domains\Cart\Services\GuestCartMergeService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Cookie;

View File

@@ -1,11 +1,11 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Requests\LoginUserRequest; use App\Domains\Auth\Requests\LoginUserRequest;
use App\Domains\Core\Auth\Resources\UserResource; use App\Domains\Auth\Resources\UserResource;
use App\Domains\Core\Auth\Services\PasswordLoginService; use App\Domains\Auth\Services\PasswordLoginService;
use App\Domains\Commerce\Cart\Services\GuestCartMergeService; use App\Domains\Cart\Services\GuestCartMergeService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Cookie;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Resources\UserResource; use App\Domains\Auth\Resources\UserResource;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Requests\RegisterUserRequest; use App\Domains\Auth\Requests\RegisterUserRequest;
use App\Domains\Core\Auth\Resources\UserResource; use App\Domains\Auth\Resources\UserResource;
use App\Domains\Core\Auth\Services\RegisterUserService; use App\Domains\Auth\Services\RegisterUserService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;

View File

@@ -1,11 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\ResetPasswordAttempt; use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Core\Auth\Requests\ResetPasswordRequest; use App\Domains\Auth\Requests\ResetPasswordRequest;
use App\Domains\Core\Auth\Services\ResetPasswordAttemptService; use App\Domains\Auth\Services\ResetPasswordAttemptService;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@@ -27,7 +26,6 @@ class ResetPasswordController extends Controller
$data['email'], $data['email'],
$data['codigo'], $data['codigo'],
$data['password'], $data['password'],
RoleCode::from($request->route('reset_role', RoleCode::User->value)),
)) { )) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'codigo' => __('api.auth.password_reset_invalid'), 'codigo' => __('api.auth.password_reset_invalid'),

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Requests\ScannerLoginRequest; use App\Domains\Auth\Requests\ScannerLoginRequest;
use App\Domains\Core\Auth\Resources\UserResource; use App\Domains\Auth\Resources\UserResource;
use App\Domains\Core\Auth\Services\PasswordLoginService; use App\Domains\Auth\Services\PasswordLoginService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Auth\Resources\ScannerMeResource; use App\Domains\Auth\Resources\ScannerMeResource;
use App\Domains\Core\Auth\Services\ScannerContextService; use App\Domains\Auth\Services\ScannerContextService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Requests\UpdateProfileRequest; use App\Domains\Auth\Requests\UpdateProfileRequest;
use App\Domains\Core\Auth\Resources\UserResource; use App\Domains\Auth\Resources\UserResource;
use App\Domains\Core\Auth\Services\ProfileService; use App\Domains\Auth\Services\ProfileService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
class UpdateProfileController class UpdateProfileController

View File

@@ -1,11 +1,10 @@
<?php <?php
namespace App\Domains\Core\Auth\Controllers; namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\ResetPasswordAttempt; use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Core\Auth\Requests\ValidateResetPasswordAttemptRequest; use App\Domains\Auth\Requests\ValidateResetPasswordAttemptRequest;
use App\Domains\Core\Auth\Services\ResetPasswordAttemptService; use App\Domains\Auth\Services\ResetPasswordAttemptService;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@@ -23,19 +22,10 @@ class ValidateResetPasswordAttemptController extends Controller
{ {
$data = $request->validated(); $data = $request->validated();
$result = $this->resetPasswordAttemptService->validateCode( if (! $this->resetPasswordAttemptService->validateCode(
$data['email'], $data['email'],
$data['codigo'], $data['codigo'],
RoleCode::from($request->route('reset_role', RoleCode::User->value)), )) {
);
if ($result === ResetPasswordAttemptService::CODE_EXPIRED) {
throw ValidationException::withMessages([
'codigo' => __('api.auth.reset_code_expired'),
]);
}
if ($result !== ResetPasswordAttemptService::CODE_VALID) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'codigo' => __('api.auth.reset_code_invalid'), 'codigo' => __('api.auth.reset_code_invalid'),
]); ]);

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Exceptions; namespace App\Domains\Auth\Exceptions;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use RuntimeException; use RuntimeException;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Models; namespace App\Domains\Auth\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;

View File

@@ -1,13 +1,13 @@
<?php <?php
namespace App\Domains\Core\Auth\Models; namespace App\Domains\Auth\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable(['user_id', 'codigo', 'reason', 'status', 'expires_at'])] #[Fillable(['user_id', 'codigo', 'reason', 'status'])]
#[Hidden(['codigo'])] #[Hidden(['codigo'])]
class ResetPasswordAttempt extends Model class ResetPasswordAttempt extends Model
{ {
@@ -17,8 +17,6 @@ class ResetPasswordAttempt extends Model
public const REASON_STAFF_CREATED = 'staff_created'; public const REASON_STAFF_CREATED = 'staff_created';
public const REASON_ADMINISTRATOR_CREATED = 'administrator_created';
public const STATUS_PENDING = 'pending'; public const STATUS_PENDING = 'pending';
public const STATUS_VALIDATED = 'validated'; public const STATUS_VALIDATED = 'validated';
@@ -33,7 +31,6 @@ class ResetPasswordAttempt extends Model
{ {
return [ return [
'user_id' => 'integer', 'user_id' => 'integer',
'expires_at' => 'datetime',
]; ];
} }

View File

@@ -1,13 +1,11 @@
<?php <?php
namespace App\Domains\Core\Auth\Models; namespace App\Domains\Auth\Models;
use App\Domains\Core\Authorization\Enums\RoleCode; use App\Domains\Authorization\Enums\RoleCode;
use App\Domains\Core\Authorization\Models\Role; use App\Domains\Authorization\Models\Role;
use App\Domains\Commerce\Catalog\Models\Category; use App\Domains\Catalog\Models\Category;
use App\Domains\Ticketing\Event\Models\EventDateChangeView; use App\Domains\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Ticket\Models\ScanAttempt;
use Database\Factories\UserFactory; use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\Hidden;
@@ -15,17 +13,16 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
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\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
#[Fillable(['nombre_apellido', 'email', 'password', 'dni', 'telefono', 'google_id', 'rol_codigo', 'tenant_codigo'])] #[Fillable(['nombre_apellido', 'email', 'password', 'dni', 'telefono', 'google_id', 'rol_codigo', 'tenant_codigo'])]
#[Hidden(['password', 'remember_token', 'active_email', 'active_google_id'])] #[Hidden(['password', 'remember_token'])]
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<UserFactory> */ /** @use HasFactory<UserFactory> */
use HasApiTokens, HasFactory, Notifiable, SoftDeletes; use HasApiTokens, HasFactory, Notifiable;
protected $attributes = [ protected $attributes = [
'rol_codigo' => RoleCode::User->value, 'rol_codigo' => RoleCode::User->value,
@@ -48,18 +45,6 @@ class User extends Authenticatable
return $this->hasMany(LoginAttempt::class); return $this->hasMany(LoginAttempt::class);
} }
/** @return HasMany<ScanAttempt, $this> */
public function scanAttempts(): HasMany
{
return $this->hasMany(ScanAttempt::class, 'scanner_user_id');
}
/** @return HasMany<EventDateChangeView, $this> */
public function eventDateChangeViews(): HasMany
{
return $this->hasMany(EventDateChangeView::class);
}
/** /**
* @return BelongsTo<Role, $this> * @return BelongsTo<Role, $this>
*/ */

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str; use Illuminate\Support\Str;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str; use Illuminate\Support\Str;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str; use Illuminate\Support\Str;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str; use Illuminate\Support\Str;

View File

@@ -1,8 +1,7 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use App\Domains\Core\Authorization\Enums\RoleCode;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password; use Illuminate\Validation\Rules\Password;
@@ -14,26 +13,15 @@ class RegisterUserRequest extends FormRequest
return true; return true;
} }
protected function prepareForValidation(): void /**
{ * @return array<string, mixed>
if (is_string($this->input('email'))) { */
$this->merge(['email' => mb_strtolower(trim($this->input('email')))]);
}
}
/** @return array<string, mixed> */
public function rules(): array public function rules(): array
{ {
return [ return [
'tenant_codigo' => ['nullable', 'string', Rule::exists('tenants', 'codigo')], 'tenant_codigo' => ['nullable', 'string', Rule::exists('tenants', 'codigo')],
'nombre_apellido' => ['required', 'string', 'max:255'], 'nombre_apellido' => ['required', 'string', 'max:255'],
'email' => [ 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')],
'required',
'string',
'email',
'max:255',
Rule::unique('users', 'active_email')->where('rol_codigo', RoleCode::User->value)->whereNull('deleted_at'),
],
'password' => ['required', 'string', 'confirmed', Password::min(8)->mixedCase()->symbols()], 'password' => ['required', 'string', 'confirmed', Password::min(8)->mixedCase()->symbols()],
'dni' => ['nullable', 'string', 'max:255'], 'dni' => ['nullable', 'string', 'max:255'],
'telefono' => ['nullable', 'string', 'max:255'], 'telefono' => ['nullable', 'string', 'max:255'],

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str; use Illuminate\Support\Str;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str; use Illuminate\Support\Str;

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
class ScannerLoginRequest extends AdminAppLoginRequest {} class ScannerLoginRequest extends AdminAppLoginRequest {}

View File

@@ -1,10 +1,9 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
class UpdateProfileRequest extends FormRequest class UpdateProfileRequest extends FormRequest
{ {
@@ -13,13 +12,6 @@ class UpdateProfileRequest extends FormRequest
return true; return true;
} }
protected function prepareForValidation(): void
{
if (is_string($this->input('email'))) {
$this->merge(['email' => mb_strtolower(trim($this->input('email')))]);
}
}
public function rules(): array public function rules(): array
{ {
return [ return [
@@ -27,13 +19,11 @@ class UpdateProfileRequest extends FormRequest
'email' => [ 'email' => [
'required', 'required',
'email', 'email',
Rule::unique('users', 'active_email')->where('rol_codigo', $this->user()->rol_codigo) Rule::unique('users', 'email')->ignore($this->user()->id),
->whereNull('deleted_at')
->ignore($this->user()->id),
], ],
'dni' => ['nullable', 'string', 'regex:/^[0-9]{7,8}$/'], 'dni' => ['nullable', 'string', 'regex:/^[0-9]{7,8}$/'],
'telefono' => ['nullable', 'string', 'regex:/^\+?[0-9\s\-]+$/'], 'telefono' => ['nullable', 'string', 'regex:/^\+?[0-9\s\-]+$/'],
'password' => ['nullable', 'string', Password::min(8)->mixedCase()->symbols()], 'password' => ['nullable', 'string', \Illuminate\Validation\Rules\Password::min(8)->mixedCase()->symbols()],
]; ];
} }
} }

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Auth\Requests; namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str; use Illuminate\Support\Str;

View File

@@ -1,9 +1,9 @@
<?php <?php
namespace App\Domains\Core\Auth\Resources; namespace App\Domains\Auth\Resources;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Tenant\Resources\TenantResource; use App\Domains\Tenant\Resources\TenantResource;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;

View File

@@ -1,9 +1,9 @@
<?php <?php
namespace App\Domains\Core\Auth\Resources; namespace App\Domains\Auth\Resources;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Tenant\Resources\TenantResource; use App\Domains\Tenant\Resources\TenantResource;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Domains\Core\Auth\Resources; namespace App\Domains\Auth\Resources;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@@ -24,11 +24,6 @@ class UserResource extends JsonResource
'telefono' => $this->telefono, 'telefono' => $this->telefono,
'rol_codigo' => $this->rol_codigo, 'rol_codigo' => $this->rol_codigo,
'tenant_codigo' => $this->tenant_codigo, 'tenant_codigo' => $this->tenant_codigo,
'categories' => $this->whenLoaded('scanCategories', fn () => $this->scanCategories
->map(fn ($category) => [
'id' => $category->id,
'nombre' => $category->nombre,
])->values()),
]; ];
} }
} }

View File

@@ -1,9 +1,9 @@
<?php <?php
namespace App\Domains\Core\Auth\Services; namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode; use App\Domains\Authorization\Enums\RoleCode;
class AdminAppContextService class AdminAppContextService
{ {

View File

@@ -1,9 +1,9 @@
<?php <?php
namespace App\Domains\Core\Auth\Services; namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode; use App\Domains\Authorization\Enums\RoleCode;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
class AdminCredentialVerifier class AdminCredentialVerifier
@@ -11,7 +11,7 @@ class AdminCredentialVerifier
public function verify(string $email, string $password): bool public function verify(string $email, string $password): bool
{ {
$admin = User::query() $admin = User::query()
->where('active_email', mb_strtolower(trim($email))) ->where('email', mb_strtolower(trim($email)))
->where('rol_codigo', RoleCode::Admin->value) ->where('rol_codigo', RoleCode::Admin->value)
->first(); ->first();

View File

@@ -1,12 +1,11 @@
<?php <?php
namespace App\Domains\Core\Auth\Services; namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode; use App\Domains\Notification\Events\UserRegistered;
use App\Shared\Notification\Events\UserRegistered; use App\Domains\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Tenant\Support\TenantDomainNormalizer;
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@@ -125,12 +124,12 @@ class GoogleAuthService
]); ]);
} }
$user = User::query()->where('rol_codigo', RoleCode::User->value)->where('google_id', $googleId)->first(); $user = User::query()->where('google_id', $googleId)->first();
if ($user) { if ($user) {
return $user; return $user;
} }
$user = User::query()->where('rol_codigo', RoleCode::User->value)->where('active_email', mb_strtolower(trim($email)))->first(); $user = User::query()->where('email', $email)->first();
if ($user) { if ($user) {
$user->forceFill(['google_id' => $googleId])->save(); $user->forceFill(['google_id' => $googleId])->save();

View File

@@ -1,14 +1,14 @@
<?php <?php
namespace App\Domains\Core\Auth\Services; namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Exceptions\AccountLockedException; use App\Domains\Auth\Exceptions\AccountLockedException;
use App\Domains\Core\Auth\Models\LoginAttempt; use App\Domains\Auth\Models\LoginAttempt;
use App\Domains\Core\Auth\Models\ResetPasswordAttempt; use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\PermissionCode; use App\Domains\Authorization\Enums\PermissionCode;
use App\Domains\Core\Authorization\Enums\RoleCode; use App\Domains\Authorization\Enums\RoleCode;
use App\Shared\Notification\Events\PasswordResetRequested; use App\Domains\Notification\Events\PasswordResetRequested;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@@ -85,7 +85,7 @@ class PasswordLoginService
null, null,
$ipAddress, $ipAddress,
$userAgent, $userAgent,
RoleCode::Scanner, null,
true, true,
PermissionCode::ScanTickets->value, PermissionCode::ScanTickets->value,
PasswordResetRequested::CHANNEL_SCANNER, PasswordResetRequested::CHANNEL_SCANNER,
@@ -120,7 +120,7 @@ class PasswordLoginService
$passwordResetChannel, $passwordResetChannel,
): array { ): array {
$user = User::query() $user = User::query()
->where('active_email', $normalizedEmail) ->where('email', $normalizedEmail)
->when( ->when(
$requiredRole !== null, $requiredRole !== null,
fn ($query) => $query->where('rol_codigo', $requiredRole->value), fn ($query) => $query->where('rol_codigo', $requiredRole->value),

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Domains\Core\Auth\Services; namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
class ProfileService class ProfileService

View File

@@ -1,9 +1,9 @@
<?php <?php
namespace App\Domains\Core\Auth\Services; namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Shared\Notification\Events\UserRegistered; use App\Domains\Notification\Events\UserRegistered;
class RegisterUserService class RegisterUserService
{ {

View File

@@ -1,23 +1,17 @@
<?php <?php
namespace App\Domains\Core\Auth\Services; namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\ResetPasswordAttempt; use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode; use App\Domains\Authorization\Enums\RoleCode;
use App\Shared\Notification\Events\PasswordResetRequested; use App\Domains\Notification\Events\PasswordResetRequested;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Throwable; use Throwable;
class ResetPasswordAttemptService class ResetPasswordAttemptService
{ {
public const CODE_VALID = 'valid';
public const CODE_INVALID = 'invalid';
public const CODE_EXPIRED = 'expired';
public function createForEmail( public function createForEmail(
string $email, string $email,
string $tenantCode, string $tenantCode,
@@ -28,8 +22,7 @@ class ResetPasswordAttemptService
try { try {
$attemptId = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?int { $attemptId = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?int {
$user = User::query() $user = User::query()
->where('active_email', mb_strtolower(trim($email))) ->where('email', $email)
->where('rol_codigo', RoleCode::User->value)
->lockForUpdate() ->lockForUpdate()
->first(); ->first();
@@ -66,7 +59,7 @@ class ResetPasswordAttemptService
try { try {
$result = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?array { $result = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?array {
$user = User::query() $user = User::query()
->where('active_email', mb_strtolower(trim($email))) ->where('email', $email)
->where('rol_codigo', RoleCode::AdminApp->value) ->where('rol_codigo', RoleCode::AdminApp->value)
->whereNotNull('tenant_codigo') ->whereNotNull('tenant_codigo')
->lockForUpdate() ->lockForUpdate()
@@ -114,7 +107,7 @@ class ResetPasswordAttemptService
try { try {
$result = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?array { $result = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?array {
$user = User::query() $user = User::query()
->where('active_email', mb_strtolower(trim($email))) ->where('email', $email)
->where('rol_codigo', RoleCode::Scanner->value) ->where('rol_codigo', RoleCode::Scanner->value)
->whereNotNull('tenant_codigo') ->whereNotNull('tenant_codigo')
->lockForUpdate() ->lockForUpdate()
@@ -153,15 +146,14 @@ class ResetPasswordAttemptService
); );
} }
public function validateCode(string $email, string $code, RoleCode $role = RoleCode::User): string public function validateCode(string $email, string $code): bool
{ {
$emailFingerprint = $this->emailFingerprint($email); $emailFingerprint = $this->emailFingerprint($email);
try { try {
return DB::transaction(function () use ($email, $code, $emailFingerprint, $role): string { return DB::transaction(function () use ($email, $code, $emailFingerprint): bool {
$user = User::query() $user = User::query()
->where('active_email', mb_strtolower(trim($email))) ->where('email', $email)
->where('rol_codigo', $role->value)
->lockForUpdate() ->lockForUpdate()
->first(); ->first();
@@ -177,27 +169,14 @@ class ResetPasswordAttemptService
'email_fingerprint' => $emailFingerprint, 'email_fingerprint' => $emailFingerprint,
]); ]);
return self::CODE_INVALID; return false;
}
if ($attempt->expires_at?->isPast()) {
$attempt->update([
'status' => ResetPasswordAttempt::STATUS_EXPIRED,
]);
Log::info('Password reset code validation failed: attempt expired.', [
'email_fingerprint' => $emailFingerprint,
'attempt_id' => $attempt->getKey(),
]);
return self::CODE_EXPIRED;
} }
$attempt->update([ $attempt->update([
'status' => ResetPasswordAttempt::STATUS_VALIDATED, 'status' => ResetPasswordAttempt::STATUS_VALIDATED,
]); ]);
return self::CODE_VALID; return true;
}); });
} catch (Throwable $exception) { } catch (Throwable $exception) {
Log::error('Failed to validate password reset code.', [ Log::error('Failed to validate password reset code.', [
@@ -209,15 +188,14 @@ class ResetPasswordAttemptService
} }
} }
public function resetPassword(string $email, string $code, string $password, RoleCode $role = RoleCode::User): bool public function resetPassword(string $email, string $code, string $password): bool
{ {
$emailFingerprint = $this->emailFingerprint($email); $emailFingerprint = $this->emailFingerprint($email);
try { try {
return DB::transaction(function () use ($email, $code, $password, $emailFingerprint, $role): bool { return DB::transaction(function () use ($email, $code, $password, $emailFingerprint): bool {
$user = User::query() $user = User::query()
->where('active_email', mb_strtolower(trim($email))) ->where('email', $email)
->where('rol_codigo', $role->value)
->lockForUpdate() ->lockForUpdate()
->first(); ->first();
@@ -236,19 +214,6 @@ class ResetPasswordAttemptService
return false; return false;
} }
if ($attempt->expires_at?->isPast()) {
$attempt->update([
'status' => ResetPasswordAttempt::STATUS_EXPIRED,
]);
Log::info('Password reset failed: attempt expired.', [
'email_fingerprint' => $emailFingerprint,
'attempt_id' => $attempt->getKey(),
]);
return false;
}
$user->password = $password; $user->password = $password;
$user->failed_login_attempts = 0; $user->failed_login_attempts = 0;
$user->last_failed_login_at = null; $user->last_failed_login_at = null;
@@ -305,7 +270,6 @@ class ResetPasswordAttemptService
'codigo' => $this->generateCode(), 'codigo' => $this->generateCode(),
'reason' => $reason, 'reason' => $reason,
'status' => ResetPasswordAttempt::STATUS_PENDING, 'status' => ResetPasswordAttempt::STATUS_PENDING,
'expires_at' => now()->addMinutes((int) config('auth.passwords.users.expire')),
]); ]);
return $attempt->getKey(); return $attempt->getKey();

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Domains\Core\Auth\Services; namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
class ScannerContextService class ScannerContextService
{ {
@@ -19,16 +19,6 @@ class ScannerContextService
$user->setRelation('tenant', $tenant); $user->setRelation('tenant', $tenant);
if ($tenant->requiresScannerCategoryValidation()) {
$categories = $user->scanCategories()
->orderBy('nombre')
->get();
if ($categories->isNotEmpty()) {
$user->setRelation('scanCategories', $categories);
}
}
return $user; return $user;
} }
} }

View File

@@ -1,10 +1,10 @@
<?php <?php
use App\Domains\Core\Auth\Controllers\AdminAppLoginController; use App\Domains\Auth\Controllers\AdminAppLoginController;
use App\Domains\Core\Auth\Controllers\AdminAppMeController; use App\Domains\Auth\Controllers\AdminAppMeController;
use App\Domains\Core\Auth\Controllers\CreateAdminAppResetPasswordAttemptController; use App\Domains\Auth\Controllers\CreateAdminAppResetPasswordAttemptController;
use App\Domains\Core\Auth\Controllers\ResetPasswordController; use App\Domains\Auth\Controllers\ResetPasswordController;
use App\Domains\Core\Auth\Controllers\ValidateResetPasswordAttemptController; use App\Domains\Auth\Controllers\ValidateResetPasswordAttemptController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::prefix('v1/adminapp')->group(function (): void { Route::prefix('v1/adminapp')->group(function (): void {
@@ -12,10 +12,8 @@ Route::prefix('v1/adminapp')->group(function (): void {
Route::post('password/reset-attempts', CreateAdminAppResetPasswordAttemptController::class) Route::post('password/reset-attempts', CreateAdminAppResetPasswordAttemptController::class)
->middleware('throttle:5,1'); ->middleware('throttle:5,1');
Route::post('password/reset-attempts/validate', ValidateResetPasswordAttemptController::class) Route::post('password/reset-attempts/validate', ValidateResetPasswordAttemptController::class)
->defaults('reset_role', 'adminapp')
->middleware('throttle:10,1'); ->middleware('throttle:10,1');
Route::post('password/reset', ResetPasswordController::class) Route::post('password/reset', ResetPasswordController::class)
->defaults('reset_role', 'adminapp')
->middleware('throttle:5,1'); ->middleware('throttle:5,1');
Route::middleware(['auth:sanctum', 'adminapp.tenant']) Route::middleware(['auth:sanctum', 'adminapp.tenant'])
->get('me', AdminAppMeController::class); ->get('me', AdminAppMeController::class);

View File

@@ -1,14 +1,14 @@
<?php <?php
use App\Domains\Core\Auth\Controllers\CreateResetPasswordAttemptController; use App\Domains\Auth\Controllers\CreateResetPasswordAttemptController;
use App\Domains\Core\Auth\Controllers\GoogleTokenExchangeController; use App\Domains\Auth\Controllers\GoogleTokenExchangeController;
use App\Domains\Core\Auth\Controllers\LoginController; use App\Domains\Auth\Controllers\LoginController;
use App\Domains\Core\Auth\Controllers\LogoutController; use App\Domains\Auth\Controllers\LogoutController;
use App\Domains\Core\Auth\Controllers\MeController; use App\Domains\Auth\Controllers\MeController;
use App\Domains\Core\Auth\Controllers\RegisterController; use App\Domains\Auth\Controllers\RegisterController;
use App\Domains\Core\Auth\Controllers\ResetPasswordController; use App\Domains\Auth\Controllers\ResetPasswordController;
use App\Domains\Core\Auth\Controllers\UpdateProfileController; use App\Domains\Auth\Controllers\UpdateProfileController;
use App\Domains\Core\Auth\Controllers\ValidateResetPasswordAttemptController; use App\Domains\Auth\Controllers\ValidateResetPasswordAttemptController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::post('/register', RegisterController::class); Route::post('/register', RegisterController::class);

View File

@@ -1,10 +1,10 @@
<?php <?php
use App\Domains\Core\Auth\Controllers\CreateScannerResetPasswordAttemptController; use App\Domains\Auth\Controllers\CreateScannerResetPasswordAttemptController;
use App\Domains\Core\Auth\Controllers\ResetPasswordController; use App\Domains\Auth\Controllers\ResetPasswordController;
use App\Domains\Core\Auth\Controllers\ScannerLoginController; use App\Domains\Auth\Controllers\ScannerLoginController;
use App\Domains\Core\Auth\Controllers\ScannerMeController; use App\Domains\Auth\Controllers\ScannerMeController;
use App\Domains\Core\Auth\Controllers\ValidateResetPasswordAttemptController; use App\Domains\Auth\Controllers\ValidateResetPasswordAttemptController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::prefix('v1/scanner')->group(function (): void { Route::prefix('v1/scanner')->group(function (): void {
@@ -12,10 +12,8 @@ Route::prefix('v1/scanner')->group(function (): void {
Route::post('password/reset-attempts', CreateScannerResetPasswordAttemptController::class) Route::post('password/reset-attempts', CreateScannerResetPasswordAttemptController::class)
->middleware('throttle:5,1'); ->middleware('throttle:5,1');
Route::post('password/reset-attempts/validate', ValidateResetPasswordAttemptController::class) Route::post('password/reset-attempts/validate', ValidateResetPasswordAttemptController::class)
->defaults('reset_role', 'scanner')
->middleware('throttle:10,1'); ->middleware('throttle:10,1');
Route::post('password/reset', ResetPasswordController::class) Route::post('password/reset', ResetPasswordController::class)
->defaults('reset_role', 'scanner')
->middleware('throttle:5,1'); ->middleware('throttle:5,1');
Route::middleware(['auth:sanctum', 'scanner.tenant']) Route::middleware(['auth:sanctum', 'scanner.tenant'])
->get('me', ScannerMeController::class); ->get('me', ScannerMeController::class);

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Authorization\Enums; namespace App\Domains\Authorization\Enums;
enum PermissionCode: string enum PermissionCode: string
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Authorization\Enums; namespace App\Domains\Authorization\Enums;
enum RoleCode: string enum RoleCode: string
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Authorization\Models; namespace App\Domains\Authorization\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Authorization\Models; namespace App\Domains\Authorization\Models;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Core\Menu\Models\Menu; use App\Domains\Menu\Models\Menu;
use App\Domains\Core\Menu\Models\MenuRole; use App\Domains\Menu\Models\MenuRole;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Core\Authorization\Models; namespace App\Domains\Authorization\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Controllers; namespace App\Domains\Bootstrap\Controllers;
use App\Domains\Core\Bootstrap\Requests\AdminAppBootstrapRequest; use App\Domains\Bootstrap\Requests\AdminAppBootstrapRequest;
use App\Domains\Core\Bootstrap\Resources\AdminAppBootstrapResource; use App\Domains\Bootstrap\Resources\AdminAppBootstrapResource;
use App\Domains\Core\Bootstrap\Services\AdminAppBootstrapService; use App\Domains\Bootstrap\Services\AdminAppBootstrapService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
class AdminAppBootstrapController extends Controller class AdminAppBootstrapController extends Controller

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Controllers; namespace App\Domains\Bootstrap\Controllers;
use App\Domains\Core\Bootstrap\Requests\ScannerBootstrapRequest; use App\Domains\Bootstrap\Requests\ScannerBootstrapRequest;
use App\Domains\Core\Bootstrap\Resources\ScannerBootstrapResource; use App\Domains\Bootstrap\Resources\ScannerBootstrapResource;
use App\Domains\Core\Bootstrap\Services\ScannerBootstrapService; use App\Domains\Bootstrap\Services\ScannerBootstrapService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
class ScannerBootstrapController extends Controller class ScannerBootstrapController extends Controller

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Controllers; namespace App\Domains\Bootstrap\Controllers;
use App\Domains\Core\Bootstrap\Requests\TenantBootstrapRequest; use App\Domains\Bootstrap\Requests\TenantBootstrapRequest;
use App\Domains\Core\Bootstrap\Services\TenantBootstrapService; use App\Domains\Bootstrap\Services\TenantBootstrapService;
use App\Domains\Core\Tenant\Resources\TenantResource; use App\Domains\Tenant\Resources\TenantResource;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
class TenantBootstrapController extends Controller class TenantBootstrapController extends Controller

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Requests; namespace App\Domains\Bootstrap\Requests;
class AdminAppBootstrapRequest extends TenantBootstrapRequest {} class AdminAppBootstrapRequest extends TenantBootstrapRequest {}

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Requests; namespace App\Domains\Bootstrap\Requests;
class ScannerBootstrapRequest extends TenantBootstrapRequest {} class ScannerBootstrapRequest extends TenantBootstrapRequest {}

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Requests; namespace App\Domains\Bootstrap\Requests;
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer; use App\Domains\Tenant\Support\TenantDomainNormalizer;
use Closure; use Closure;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;

View File

@@ -1,23 +1,22 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Resources; namespace App\Domains\Bootstrap\Resources;
use App\Domains\Core\Tenant\Models\AdminWebsiteType; use App\Domains\Tenant\Models\WebsiteType;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin array{website_type: AdminWebsiteType} */ /** @mixin array{website_type: WebsiteType} */
class AdminAppBootstrapResource extends JsonResource class AdminAppBootstrapResource extends JsonResource
{ {
/** @return array<string, mixed> */ /** @return array<string, mixed> */
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
/** @var AdminWebsiteType $websiteType */ /** @var WebsiteType $websiteType */
$websiteType = $this->resource['website_type']; $websiteType = $this->resource['website_type'];
return [ return [
'admin_website_type_code' => $websiteType->codigo, 'website_type_code' => $websiteType->codigo,
'site_title' => $websiteType->site_title,
'primary_color' => $websiteType->primary_color, 'primary_color' => $websiteType->primary_color,
'secondary_color' => $websiteType->secondary_color, 'secondary_color' => $websiteType->secondary_color,
'danger_color' => $websiteType->danger_color, 'danger_color' => $websiteType->danger_color,
@@ -31,7 +30,6 @@ class AdminAppBootstrapResource extends JsonResource
'login_header_footer_color' => $websiteType->login_header_footer_color, 'login_header_footer_color' => $websiteType->login_header_footer_color,
'site_logo' => $websiteType->siteLogo?->getTemporaryUrl(1440), 'site_logo' => $websiteType->siteLogo?->getTemporaryUrl(1440),
'footer_logo' => $websiteType->footerLogo?->getTemporaryUrl(1440), 'footer_logo' => $websiteType->footerLogo?->getTemporaryUrl(1440),
'favicon' => $websiteType->favicon?->getTemporaryUrl(1440),
]; ];
} }
} }

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Resources; namespace App\Domains\Bootstrap\Resources;
class ScannerBootstrapResource extends AdminAppBootstrapResource {} class ScannerBootstrapResource extends AdminAppBootstrapResource {}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Domains\Bootstrap\Services;
use App\Domains\Tenant\Models\WebsiteType;
class AdminAppBootstrapService
{
/** @return array{website_type: WebsiteType} */
public function get(string $domain): array
{
return [
'website_type' => WebsiteType::query()
->with(['siteLogo', 'footerLogo'])
->where('dominio', $domain)
->firstOrFail(),
];
}
}

View 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(),
];
}
}

View File

@@ -1,11 +1,11 @@
<?php <?php
namespace App\Domains\Core\Bootstrap\Services; namespace App\Domains\Bootstrap\Services;
use App\Domains\Core\Authorization\Enums\RoleCode; use App\Domains\Authorization\Enums\RoleCode;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Services\TenantInformationService; use App\Domains\Tenant\Services\TenantInformationService;
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer; use App\Domains\Tenant\Support\TenantDomainNormalizer;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
class TenantBootstrapService class TenantBootstrapService
@@ -32,13 +32,11 @@ class TenantBootstrapService
return $this->tenantInformationService->load( return $this->tenantInformationService->load(
$tenant, $tenant,
[ [
'eventDateChanges',
'menues' => fn ($query) => $query->whereHas( 'menues' => fn ($query) => $query->whereHas(
'roles', 'roles',
fn ($query) => $query->where('codigo', RoleCode::User->value) fn ($query) => $query->where('codigo', RoleCode::User->value)
), ),
$tenant->storefront_website_type_code === 'onticket_multi_event' 'categories' => fn ($query) => $query->orderBy('nombre'),
? 'eventCategories' : 'categories' => fn ($query) => $query->orderBy('nombre'),
] ]
); );
} }

View File

@@ -1,6 +1,6 @@
<?php <?php
use App\Domains\Core\Bootstrap\Controllers\AdminAppBootstrapController; use App\Domains\Bootstrap\Controllers\AdminAppBootstrapController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get( Route::get(

View File

@@ -1,6 +1,6 @@
<?php <?php
use App\Domains\Core\Bootstrap\Controllers\TenantBootstrapController; use App\Domains\Bootstrap\Controllers\TenantBootstrapController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('tenants/bootstrap', TenantBootstrapController::class); Route::get('tenants/bootstrap', TenantBootstrapController::class);

View File

@@ -1,6 +1,6 @@
<?php <?php
use App\Domains\Core\Bootstrap\Controllers\ScannerBootstrapController; use App\Domains\Bootstrap\Controllers\ScannerBootstrapController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get( Route::get(

View File

@@ -1,13 +1,13 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Controllers; namespace App\Domains\Cart\Controllers;
use App\Domains\Commerce\Cart\Models\CartItem; use App\Domains\Cart\Models\CartItem;
use App\Domains\Commerce\Cart\Requests\AddCartItemRequest; use App\Domains\Cart\Requests\AddCartItemRequest;
use App\Domains\Commerce\Cart\Requests\UpdateCartItemQuantityRequest; use App\Domains\Cart\Requests\UpdateCartItemQuantityRequest;
use App\Domains\Commerce\Cart\Resources\CartResource; use App\Domains\Cart\Resources\CartResource;
use App\Domains\Commerce\Cart\Services\CartService; use App\Domains\Cart\Services\CartService;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View File

@@ -1,17 +1,16 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Models; namespace App\Domains\Cart\Models;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Inventory; use App\Domains\Catalog\Models\Inventory;
use App\Domains\Commerce\Catalog\Models\StockReservation; use App\Domains\Catalog\Models\Variant;
use App\Domains\Commerce\Catalog\Models\Variant; use App\Domains\Catalog\Services\CatalogInventoryService;
use App\Domains\Commerce\Catalog\Services\CatalogInventoryService; use App\Domains\Catalog\Services\StockReservationService;
use App\Domains\Commerce\Catalog\Services\StockReservationService; use App\Domains\Purchase\Models\Purchase;
use App\Domains\Commerce\Purchase\Models\Purchase; use App\Domains\Purchase\Services\UserPurchaseLimitService;
use App\Domains\Commerce\Purchase\Services\UserPurchaseLimitService; use App\Domains\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\Tenant;
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;
@@ -29,7 +28,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
'status', 'status',
'origin', 'origin',
'current_purchase_id', 'current_purchase_id',
'current_stock_reservation_id',
])] ])]
class Cart extends Model class Cart extends Model
{ {
@@ -38,16 +36,6 @@ class Cart extends Model
protected $table = 'carritos'; protected $table = 'carritos';
public const STATUS_ACTIVE = 'active';
public const STATUS_CHECKOUT = 'checkout';
public const STATUS_CONVERTED = 'converted';
public const STATUS_EXPIRED = 'expired';
public const STATUS_ABANDONED = 'abandoned';
public const ORIGIN_USER = 'user'; public const ORIGIN_USER = 'user';
public const ORIGIN_DIRECT_CHECKOUT = 'direct_checkout'; public const ORIGIN_DIRECT_CHECKOUT = 'direct_checkout';
@@ -57,7 +45,6 @@ class Cart extends Model
return [ return [
'user_id' => 'integer', 'user_id' => 'integer',
'current_purchase_id' => 'integer', 'current_purchase_id' => 'integer',
'current_stock_reservation_id' => 'integer',
]; ];
} }
@@ -97,15 +84,6 @@ class Cart extends Model
return $this->belongsTo(Purchase::class, 'current_purchase_id'); return $this->belongsTo(Purchase::class, 'current_purchase_id');
} }
/** @return BelongsTo<StockReservation, $this> */
public function currentStockReservation(): BelongsTo
{
return $this->belongsTo(
StockReservation::class,
'current_stock_reservation_id',
);
}
public function getTotalAmount(): float public function getTotalAmount(): float
{ {
$items = $this->relationLoaded('items') $items = $this->relationLoaded('items')
@@ -129,7 +107,6 @@ class Cart extends Model
return DB::transaction(function () use ($catalogItemId, $variantId, $quantity): CartItem { return DB::transaction(function () use ($catalogItemId, $variantId, $quantity): CartItem {
$this->invalidateCurrentCheckout(); $this->invalidateCurrentCheckout();
app(StockReservationService::class)->assertCartReservationUsable($this);
$selectedItem = $this->resolveScopedItem($catalogItemId, $variantId, true); $selectedItem = $this->resolveScopedItem($catalogItemId, $variantId, true);
$cartQuantity = (int) $this->items() $cartQuantity = (int) $this->items()
->where('catalog_item_id', $catalogItemId) ->where('catalog_item_id', $catalogItemId)
@@ -163,11 +140,12 @@ class Cart extends Model
'cantidad' => $quantity, 'cantidad' => $quantity,
]); ]);
} else { } else {
app(StockReservationService::class)->ensure($item, $selectedItem);
$item->cantidad += $quantity; $item->cantidad += $quantity;
$item->save(); $item->save();
} }
app(StockReservationService::class)->syncCart($this); app(StockReservationService::class)->reserve($item, $selectedItem, $quantity);
return $item->fresh(); return $item->fresh();
}); });
@@ -194,7 +172,6 @@ class Cart extends Model
$excludedPurchaseId, $excludedPurchaseId,
): CartItem { ): CartItem {
$this->invalidateCurrentCheckout(); $this->invalidateCurrentCheckout();
app(StockReservationService::class)->assertCartReservationUsable($this);
/** @var CartItem $item */ /** @var CartItem $item */
$item = $this->items() $item = $this->items()
@@ -228,6 +205,7 @@ class Cart extends Model
$nextAvailableQuantity, $nextAvailableQuantity,
); );
app(StockReservationService::class)->release($item, $currentSelection, $item->cantidad);
$availableQuantity = $inventoryService->availableQuantity($nextSelection); $availableQuantity = $inventoryService->availableQuantity($nextSelection);
if ($availableQuantity !== null && $availableQuantity < $quantity) { if ($availableQuantity !== null && $availableQuantity < $quantity) {
@@ -244,10 +222,11 @@ class Cart extends Model
->first(); ->first();
if ($targetItem !== null) { if ($targetItem !== null) {
app(StockReservationService::class)->ensure($targetItem, $nextSelection);
$targetItem->cantidad += $quantity; $targetItem->cantidad += $quantity;
$targetItem->save(); $targetItem->save();
app(StockReservationService::class)->reserve($targetItem, $nextSelection, $quantity);
$item->delete(); $item->delete();
app(StockReservationService::class)->syncCart($this);
return $targetItem->fresh(); return $targetItem->fresh();
} }
@@ -255,7 +234,7 @@ class Cart extends Model
$item->variant_id = $variantId; $item->variant_id = $variantId;
$item->cantidad = $quantity; $item->cantidad = $quantity;
$item->save(); $item->save();
app(StockReservationService::class)->syncCart($this); app(StockReservationService::class)->reserve($item, $nextSelection, $quantity);
return $item->fresh(); return $item->fresh();
} }
@@ -284,9 +263,16 @@ class Cart extends Model
]); ]);
} }
if ($delta > 0) {
app(StockReservationService::class)->reserve($item, $currentSelection, $delta);
}
if ($delta < 0) {
app(StockReservationService::class)->release($item, $currentSelection, abs($delta));
}
$item->cantidad = $quantity; $item->cantidad = $quantity;
$item->save(); $item->save();
app(StockReservationService::class)->syncCart($this);
return $item->fresh(); return $item->fresh();
}); });
@@ -296,7 +282,6 @@ class Cart extends Model
{ {
DB::transaction(function () use ($cartItemId): void { DB::transaction(function () use ($cartItemId): void {
$this->invalidateCurrentCheckout(); $this->invalidateCurrentCheckout();
app(StockReservationService::class)->assertCartReservationUsable($this);
/** @var CartItem $item */ /** @var CartItem $item */
$item = $this->items() $item = $this->items()
@@ -304,8 +289,17 @@ class Cart extends Model
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
$selectedItem = $this->resolveScopedItem(
$item->catalog_item_id,
$item->variant_id,
true,
);
app(StockReservationService::class)->release(
$item,
$selectedItem,
$item->cantidad,
);
$item->delete(); $item->delete();
app(StockReservationService::class)->syncCart($this);
}); });
} }
@@ -347,20 +341,13 @@ class Cart extends Model
Purchase::STATUS_CREATED, Purchase::STATUS_CREATED,
Purchase::STATUS_PENDING_PAYMENT, Purchase::STATUS_PENDING_PAYMENT,
], true)) { ], true)) {
app(StockReservationService::class)->returnToCart($currentPurchase, $cart);
$currentPurchase->update([ $currentPurchase->update([
'status' => Purchase::STATUS_SUPERSEDED, 'status' => Purchase::STATUS_SUPERSEDED,
'expires_at' => null,
]); ]);
$this->current_purchase_id = null;
return;
} }
app(StockReservationService::class)->releaseForPurchase( app(StockReservationService::class)->detachFromPurchase($currentPurchase);
$currentPurchase,
reason: StockReservationService::REASON_PURCHASE_SUPERSEDED,
);
self::query() self::query()
->whereKey($cart->getKey()) ->whereKey($cart->getKey())
->where('current_purchase_id', $currentPurchase->getKey()) ->where('current_purchase_id', $currentPurchase->getKey())
@@ -375,7 +362,7 @@ class Cart extends Model
): CatalogItem|Variant { ): CatalogItem|Variant {
$catalogItemQuery = CatalogItem::query() $catalogItemQuery = CatalogItem::query()
->whereKey($catalogItemId) ->whereKey($catalogItemId)
->forTenantCatalog($this->tenant()->firstOrFail()); ->where('tenant_code', $this->tenant_codigo);
if ($lockForUpdate) { if ($lockForUpdate) {
$catalogItemQuery->lockForUpdate(); $catalogItemQuery->lockForUpdate();
@@ -400,17 +387,6 @@ class Cart extends Model
]); ]);
} }
if ($catalogItem->bundleComponents()
->whereNotNull('component_variant_id')
->whereHas('variant', fn ($query) => $query
->whereNotNull('sales_disabled_at')
->orWhereNotNull('replaced_by_variant_id'))
->exists()) {
throw ValidationException::withMessages([
'catalog_item_id' => [__('api.cart.bundle_component_unavailable')],
]);
}
return $catalogItem; return $catalogItem;
} }
@@ -441,12 +417,6 @@ class Cart extends Model
throw new NotFoundHttpException('Variant not found for catalog item.'); throw new NotFoundHttpException('Variant not found for catalog item.');
} }
if (! $variant->isSellable()) {
throw ValidationException::withMessages([
'variant_id' => [__('api.cart.variant_unavailable')],
]);
}
$inventory = $this->resolveInventory($variant->inventory_id, $lockForUpdate); $inventory = $this->resolveInventory($variant->inventory_id, $lockForUpdate);
$variant->setRelation('catalogItem', $catalogItem); $variant->setRelation('catalogItem', $catalogItem);
$variant->setRelation('inventory', $inventory); $variant->setRelation('inventory', $inventory);

View File

@@ -1,13 +1,15 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Models; namespace App\Domains\Cart\Models;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Variant; use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Catalog\Models\Variant;
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\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([ #[Fillable([
'cart_id', 'cart_id',
@@ -55,4 +57,10 @@ class CartItem extends Model
{ {
return $this->variant ?? $this->catalogItem; return $this->variant ?? $this->catalogItem;
} }
/** @return HasMany<StockReservation, $this> */
public function stockReservations(): HasMany
{
return $this->hasMany(StockReservation::class);
}
} }

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Requests; namespace App\Domains\Cart\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Requests; namespace App\Domains\Cart\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;

View File

@@ -1,11 +1,11 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Resources; namespace App\Domains\Cart\Resources;
use App\Domains\Commerce\Cart\Models\CartItem; use App\Domains\Cart\Models\CartItem;
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Commerce\Catalog\Models\Variant; use App\Domains\Catalog\Models\Variant;
use App\Domains\Core\Tenant\Models\Tenant; 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;

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Resources; namespace App\Domains\Cart\Resources;
use App\Domains\Commerce\Cart\Models\Cart; use App\Domains\Cart\Models\Cart;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;

View File

@@ -1,15 +1,12 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Services; namespace App\Domains\Cart\Services;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Commerce\Cart\Models\Cart; use App\Domains\Cart\Models\Cart;
use App\Domains\Commerce\Catalog\Exceptions\StockReservationExpiredException; use App\Domains\Tenant\Models\Tenant;
use App\Domains\Commerce\Catalog\Services\ExpireStockReservationsService;
use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Cookie;
@@ -25,7 +22,7 @@ class CartService
return $this->makeEmptyCart($tenant); return $this->makeEmptyCart($tenant);
} }
$cart = $this->resolveCart($tenant, $resolvedIdentity['identity']); $cart = $this->findCart($tenant, $resolvedIdentity['identity']);
if ($cart === null) { if ($cart === null) {
return $this->makeEmptyCart($tenant); return $this->makeEmptyCart($tenant);
@@ -122,7 +119,7 @@ class CartService
{ {
$cart = new Cart([ $cart = new Cart([
'tenant_codigo' => $tenant->codigo, 'tenant_codigo' => $tenant->codigo,
'status' => Cart::STATUS_ACTIVE, 'status' => 'active',
]); ]);
$cart->setRelation('items', collect()); $cart->setRelation('items', collect());
@@ -223,14 +220,12 @@ class CartService
{ {
return Cart::query() return Cart::query()
->where('tenant_codigo', $tenant->codigo) ->where('tenant_codigo', $tenant->codigo)
->where('origin', Cart::ORIGIN_USER) ->where('status', 'active')
->whereIn('status', [Cart::STATUS_ACTIVE, Cart::STATUS_EXPIRED])
->when( ->when(
$identity['user_id'] !== null, $identity['user_id'] !== null,
fn ($query) => $query->where('user_id', $identity['user_id']), fn ($query) => $query->where('user_id', $identity['user_id']),
fn ($query) => $query->where('guest_token', $identity['guest_token']), fn ($query) => $query->where('guest_token', $identity['guest_token']),
) )
->orderByRaw('CASE WHEN status = ? THEN 0 ELSE 1 END', [Cart::STATUS_ACTIVE])
->first(); ->first();
} }
@@ -239,7 +234,7 @@ class CartService
*/ */
protected function findCartOrFail(Tenant $tenant, array $identity): Cart protected function findCartOrFail(Tenant $tenant, array $identity): Cart
{ {
$cart = $this->resolveCart($tenant, $identity, replaceExpired: false); $cart = $this->findCart($tenant, $identity);
if ($cart === null) { if ($cart === null) {
throw new NotFoundHttpException('Cart not found.'); throw new NotFoundHttpException('Cart not found.');
@@ -252,73 +247,10 @@ class CartService
* @param array{user_id: ?int, guest_token: ?string} $identity * @param array{user_id: ?int, guest_token: ?string} $identity
*/ */
protected function findOrCreateCart(Tenant $tenant, array $identity): Cart protected function findOrCreateCart(Tenant $tenant, array $identity): Cart
{
return $this->resolveCart($tenant, $identity)
?? $this->createCart($tenant, $identity);
}
/**
* @param array{user_id: ?int, guest_token: ?string} $identity
*/
protected function resolveCart(
Tenant $tenant,
array $identity,
bool $replaceExpired = true,
): ?Cart {
$cart = $this->findCart($tenant, $identity);
if ($cart?->status === Cart::STATUS_ACTIVE
&& $cart->current_stock_reservation_id !== null
&& app(ExpireStockReservationsService::class)
->expireIfOverdue($cart->current_stock_reservation_id)) {
$cart = $this->findCart($tenant, $identity);
}
if ($cart?->status === Cart::STATUS_EXPIRED) {
if (! $replaceExpired) {
throw new StockReservationExpiredException;
}
return $this->replaceExpiredCart($cart, $tenant, $identity);
}
if ($cart !== null) {
return $cart;
}
return null;
}
/**
* @param array{user_id: ?int, guest_token: ?string} $identity
*/
protected function replaceExpiredCart(Cart $expiredCart, Tenant $tenant, array $identity): Cart
{
return DB::transaction(function () use ($expiredCart, $tenant, $identity): Cart {
/** @var Cart|null $lockedCart */
$lockedCart = Cart::query()->lockForUpdate()->find($expiredCart->getKey());
if ($lockedCart?->status === Cart::STATUS_EXPIRED) {
$lockedCart->update([
'status' => Cart::STATUS_ABANDONED,
'current_purchase_id' => null,
]);
}
return $this->findCart($tenant, $identity)
?? $this->createCart($tenant, $identity);
});
}
/**
* @param array{user_id: ?int, guest_token: ?string} $identity
*/
protected function createCart(Tenant $tenant, array $identity): Cart
{ {
$attributes = [ $attributes = [
'tenant_codigo' => $tenant->codigo, 'tenant_codigo' => $tenant->codigo,
'status' => Cart::STATUS_ACTIVE, 'status' => 'active',
'origin' => Cart::ORIGIN_USER,
]; ];
if ($identity['user_id'] !== null) { if ($identity['user_id'] !== null) {

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Domains\Cart\Services;
use App\Domains\Cart\Models\Cart;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\StockReservation;
use Illuminate\Support\Facades\DB;
class ExpireCartReservationsService
{
public function expireOverdue(): int
{
$expiredItems = 0;
$lastCartItemId = 0;
do {
$cartItemIds = StockReservation::query()
->where('status', StockReservation::STATUS_ACTIVE)
->whereNull('purchase_id')
->whereNotNull('cart_item_id')
->whereNotNull('expires_at')
->where('expires_at', '<=', now())
->where('cart_item_id', '>', $lastCartItemId)
->whereHas('cartItem.cart', fn ($query) => $query->where('status', 'active'))
->select('cart_item_id')
->distinct()
->orderBy('cart_item_id')
->limit(500)
->pluck('cart_item_id');
foreach ($cartItemIds as $cartItemId) {
$lastCartItemId = (int) $cartItemId;
if ($this->expireCartItem($lastCartItemId)) {
$expiredItems++;
}
}
} while ($cartItemIds->count() === 500);
return $expiredItems;
}
private function expireCartItem(int $cartItemId): bool
{
/** @var CartItem|null $candidate */
$candidate = CartItem::query()->select(['id', 'cart_id'])->find($cartItemId);
if ($candidate === null) {
return false;
}
return DB::transaction(function () use ($candidate, $cartItemId): bool {
/** @var Cart|null $cart */
$cart = Cart::query()
->whereKey($candidate->cart_id)
->where('status', 'active')
->lockForUpdate()
->first();
if ($cart === null) {
return false;
}
/** @var CartItem|null $cartItem */
$cartItem = $cart->items()
->whereKey($cartItemId)
->lockForUpdate()
->first();
if ($cartItem === null) {
return false;
}
$reservations = StockReservation::query()
->where('cart_item_id', $cartItem->getKey())
->where('status', StockReservation::STATUS_ACTIVE)
->orderBy('inventory_id')
->lockForUpdate()
->get();
if (
$reservations->isEmpty()
|| $reservations->contains(
fn (StockReservation $reservation): bool => $reservation->purchase_id !== null
|| $reservation->expires_at === null
|| $reservation->expires_at->isFuture(),
)
) {
return false;
}
$inventories = Inventory::query()
->whereKey($reservations->pluck('inventory_id'))
->orderBy('id')
->lockForUpdate()
->get()
->keyBy('id');
foreach ($reservations as $reservation) {
$inventory = $inventories->get($reservation->inventory_id)
?? throw new \InvalidArgumentException('No se encontro el inventario reservado.');
$inventory->release((int) $reservation->quantity);
$reservation->update([
'quantity' => 0,
'status' => StockReservation::STATUS_EXPIRED,
'expires_at' => null,
'released_at' => now(),
]);
}
$cartItem->delete();
if (! $cart->items()->exists()) {
$cart->update(['status' => 'expired']);
$cart->delete();
}
return true;
});
}
}

View File

@@ -1,9 +1,9 @@
<?php <?php
namespace App\Domains\Commerce\Cart\Services; namespace App\Domains\Cart\Services;
use App\Domains\Core\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Commerce\Cart\Models\Cart; use App\Domains\Cart\Models\Cart;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class GuestCartMergeService class GuestCartMergeService

View File

@@ -6,12 +6,13 @@ Gestiona el carrito activo de un tenant tanto para visitantes como para usuarios
## Modelo ## Modelo
- `Cart`: pertenece a un tenant y opcionalmente a un usuario; calcula el total, permite agregar, actualizar o quitar ítems y apunta a su reserva de stock vigente mediante `current_stock_reservation_id`. - `Cart`: pertenece a un tenant y opcionalmente a un usuario; calcula el total y permite agregar, actualizar o quitar ítems.
- `CartItem`: referencia un `CatalogItem` y, opcionalmente, una `Variant`; sólo persiste la selección y cantidad, y expone siempre los datos vigentes del catálogo. - `CartItem`: referencia un `CatalogItem` y, opcionalmente, una `Variant`; sólo persiste la selección y cantidad, y expone siempre los datos vigentes del catálogo.
## Servicios ## Servicios
- `CartService`: obtiene el carrito, modifica ítems y administra la cookie del token invitado. - `CartService`: obtiene el carrito, modifica ítems y administra la cookie del token invitado.
- `ExpireCartReservationsService`: libera las reservas vencidas de carritos activos y elimina los carritos que quedan vacíos.
- `GuestCartMergeService`: incorpora el carrito invitado al usuario cuando este se autentica. - `GuestCartMergeService`: incorpora el carrito invitado al usuario cuando este se autentica.
## Endpoints ## Endpoints
@@ -33,6 +34,4 @@ Depende de `Catalog` para productos y variantes, de `Tenant` para aislar datos y
Un carrito puede pasar a `checkout`. Las compras directas usan un carrito técnico con `origin=direct_checkout`; los carritos normales conservan `origin=user` y pueden restaurarse al cancelar o vencer la compra. Un carrito puede pasar a `checkout`. Las compras directas usan un carrito técnico con `origin=direct_checkout`; los carritos normales conservan `origin=user` y pueden restaurarse al cancelar o vencer la compra.
Cada edición sincroniza una única reserva para el carrito completo. Si varios ítems o bundles consumen el mismo inventario, se persiste una sola línea con la cantidad agregada. Al editar durante checkout, la compra anterior queda `superseded`, se desvincula y el carrito conserva la misma reserva activa con sus líneas actualizadas. El comando unificado `php artisan reservations:expire` procesa primero las compras vencidas y luego las reservas activas sin compra cuyo `expires_at` haya vencido. Se ejecuta cada minuto mediante el scheduler, conserva la fila de reserva con estado `expired`, elimina el ítem abandonado y elimina lógicamente el carrito cuando queda vacío. Cada intento registra sus resultados o su error en el log diario `storage/logs/commands/commands-AAAA-MM-DD.log`.
El comando unificado `php artisan reservations:expire` recorre una sola vez las reservas activas cuyo `expires_at` haya vencido. Cuando pertenecen a un carrito, conserva la reserva y sus líneas como historial, libera el stock como conjunto y cambia el carrito asociado a `expired` sin eliminar sus ítems. Al volver a resolver ese carrito desde la API, el anterior pasa automáticamente a `abandoned` y se crea uno activo y vacío para la misma identidad. El cliente nunca necesita reiniciarlo explícitamente. La API también materializa este vencimiento al acceder al carrito aunque el comando programado todavía no haya corrido.

View File

@@ -1,6 +1,6 @@
<?php <?php
use App\Domains\Commerce\Cart\Controllers\CartController; use App\Domains\Cart\Controllers\CartController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::prefix('tenants/{tenant:codigo}') Route::prefix('tenants/{tenant:codigo}')

View File

@@ -1,12 +1,12 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Controllers\AdminApp; namespace App\Domains\Catalog\Controllers\AdminApp;
use App\Domains\Commerce\Catalog\Models\FeaturedGroup; use App\Domains\Catalog\Models\FeaturedGroup;
use App\Domains\Commerce\Catalog\Requests\AdminApp\UpsertOnTicketFeaturedGroupRequest; use App\Domains\Catalog\Requests\AdminApp\UpsertOnTicketFeaturedGroupRequest;
use App\Domains\Commerce\Catalog\Resources\AdminApp\OnTicketFeaturedGroupResource; use App\Domains\Catalog\Resources\AdminApp\OnTicketFeaturedGroupResource;
use App\Domains\Commerce\Catalog\Services\OnTicketFeaturedGroupService; use App\Domains\Catalog\Services\OnTicketFeaturedGroupService;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -58,7 +58,7 @@ class OnTicketFeaturedGroupController extends Controller
{ {
$tenant = $request->user()->tenant()->firstOrFail(); $tenant = $request->user()->tenant()->firstOrFail();
abort_unless(in_array($tenant->storefront_website_type_code, ['onticket', 'onticket_multi_event'], true), 404); abort_unless($tenant->website_type_code === 'onticket', 404);
return $tenant; return $tenant;
} }

View File

@@ -1,25 +1,27 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Controllers; namespace App\Domains\Catalog\Controllers;
use App\Domains\Commerce\Cart\Services\CartService; use App\Domains\Cart\Services\CartService;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Category; use App\Domains\Catalog\Models\Category;
use App\Domains\Commerce\Catalog\Models\FeaturedGroup; use App\Domains\Catalog\Models\FeaturedGroup;
use App\Domains\Commerce\Catalog\Requests\CatalogItemDetailRequest; use App\Domains\Catalog\Requests\CatalogItemDetailRequest;
use App\Domains\Commerce\Catalog\Requests\CatalogVariantOptionsRequest; use App\Domains\Catalog\Requests\CatalogVariantOptionsRequest;
use App\Domains\Commerce\Catalog\Requests\CategoryPageRequest; use App\Domains\Catalog\Requests\CategoryPageRequest;
use App\Domains\Commerce\Catalog\Requests\FeaturedGroupPageRequest; use App\Domains\Catalog\Requests\FeaturedGroupPageRequest;
use App\Domains\Commerce\Catalog\Requests\SearchCatalogItemsRequest; use App\Domains\Catalog\Requests\SearchCatalogItemsRequest;
use App\Domains\Commerce\Catalog\Resources\CatalogFeaturedGroupResource; use App\Domains\Catalog\Requests\StoreCatalogItemRequest;
use App\Domains\Commerce\Catalog\Resources\CatalogItemDetailResource; use App\Domains\Catalog\Resources\CatalogFeaturedGroupResource;
use App\Domains\Commerce\Catalog\Resources\CatalogSearchItemResource; use App\Domains\Catalog\Resources\CatalogItemDetailResource;
use App\Domains\Commerce\Catalog\Resources\CatalogVariantOptionsResource; use App\Domains\Catalog\Resources\CatalogItemResource;
use App\Domains\Commerce\Catalog\Services\CatalogItemAllowanceService; use App\Domains\Catalog\Resources\CatalogSearchItemResource;
use App\Domains\Commerce\Catalog\Services\CatalogService; use App\Domains\Catalog\Resources\CatalogVariantOptionsResource;
use App\Domains\Commerce\Catalog\Services\FeaturedGroupService; use App\Domains\Catalog\Services\CatalogItemAllowanceService;
use App\Domains\Commerce\Catalog\Services\VariantSelectionService; use App\Domains\Catalog\Services\CatalogService;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Catalog\Services\FeaturedGroupService;
use App\Domains\Catalog\Services\VariantSelectionService;
use App\Domains\Tenant\Models\Tenant;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -38,7 +40,7 @@ class CatalogController extends Controller
return response()->json($featuredGroups->map( return response()->json($featuredGroups->map(
fn (FeaturedGroup $featuredGroup): array => (new CatalogFeaturedGroupResource( fn (FeaturedGroup $featuredGroup): array => (new CatalogFeaturedGroupResource(
$featuredGroup, $featuredGroup,
$featuredGroupService->itemsResponse($featuredGroup, $tenant, 1, $this->userId($request)), $featuredGroupService->itemsResponse($featuredGroup, 1, $this->userId($request)),
))->resolve() ))->resolve()
)); ));
} }
@@ -100,7 +102,6 @@ class CatalogController extends Controller
return response()->json($featuredGroupService->itemsResponse( return response()->json($featuredGroupService->itemsResponse(
$featuredGroup, $featuredGroup,
$tenant,
$page, $page,
$this->userId($request), $this->userId($request),
)); ));
@@ -113,8 +114,7 @@ class CatalogController extends Controller
CatalogService $catalogService, CatalogService $catalogService,
CatalogItemAllowanceService $allowances, CatalogItemAllowanceService $allowances,
): CatalogItemDetailResource { ): CatalogItemDetailResource {
abort_unless($catalogItem->tenant_code === $tenant->codigo abort_unless($catalogItem->tenant_code === $tenant->codigo, 404);
&& ($tenant->active_event_id === null || $catalogItem->event_id === $tenant->active_event_id), 404);
$variantId = $request->validated('variant_id'); $variantId = $request->validated('variant_id');
@@ -123,6 +123,10 @@ class CatalogController extends Controller
$variantId === null ? null : (int) $variantId, $variantId === null ? null : (int) $variantId,
); );
$allowances->attach(collect([$item]), $this->userId($request)); $allowances->attach(collect([$item]), $this->userId($request));
abort_unless($allowances->availability(
$item->availableStock(),
$item->getAttribute('remaining_user_quota'),
)->isVisible(), 404);
return CatalogItemDetailResource::make($item); return CatalogItemDetailResource::make($item);
} }
@@ -134,8 +138,7 @@ class CatalogController extends Controller
VariantSelectionService $variantSelectionService, VariantSelectionService $variantSelectionService,
CartService $cartService, CartService $cartService,
): CatalogVariantOptionsResource { ): CatalogVariantOptionsResource {
abort_unless($catalogItem->tenant_code === $tenant->codigo abort_unless($catalogItem->tenant_code === $tenant->codigo, 404);
&& ($tenant->active_event_id === null || $catalogItem->event_id === $tenant->active_event_id), 404);
$includedVariantId = null; $includedVariantId = null;
$cartItemId = $request->validated('cart_item_id'); $cartItemId = $request->validated('cart_item_id');
@@ -164,6 +167,21 @@ class CatalogController extends Controller
); );
} }
public function store(
StoreCatalogItemRequest $request,
Tenant $tenant,
CatalogService $catalogService,
): JsonResponse {
$catalogItem = $catalogService->create([
...$request->validated(),
'tenant_code' => $tenant->codigo,
]);
return CatalogItemResource::make($catalogItem)
->response()
->setStatusCode(201);
}
private function userId(Request $request): ?int private function userId(Request $request): ?int
{ {
$userId = $request->user()?->getAuthIdentifier() ?? Auth::guard('sanctum')->id(); $userId = $request->user()?->getAuthIdentifier() ?? Auth::guard('sanctum')->id();

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Domains\Catalog\Enums;
enum AvailabilityEffect: string
{
case Hide = 'hide';
case Restrict = 'restrict';
case Notice = 'notice';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Catalog\Enums;
enum CatalogAction: string
{
case SelectVariant = 'select_variant';
case ChangeQuantity = 'change_quantity';
case AddToCart = 'add_to_cart';
case BuyNow = 'buy_now';
}

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Enums; namespace App\Domains\Catalog\Enums;
enum CatalogItemType: string enum CatalogItemType: string
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Enums; namespace App\Domains\Catalog\Enums;
enum FeaturedGroupSource: string enum FeaturedGroupSource: string
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Enums; namespace App\Domains\Catalog\Enums;
enum GroupLayout: string enum GroupLayout: string
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Enums; namespace App\Domains\Catalog\Enums;
enum InventoryPolicy: string enum InventoryPolicy: string
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Enums; namespace App\Domains\Catalog\Enums;
enum InventorySubject: string enum InventorySubject: string
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Enums; namespace App\Domains\Catalog\Enums;
enum ProductLayout: string enum ProductLayout: string
{ {

View File

@@ -1,10 +1,10 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Models; namespace App\Domains\Catalog\Models;
use App\Domains\Ticketing\Event\Models\EventDate; use App\Domains\Event\Models\EventDate;
use App\Shared\Enums\FieldType; use App\Domains\Shared\Enums\FieldType;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
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;
@@ -59,8 +59,6 @@ class Attribute extends Model
public function eventDates(): HasMany public function eventDates(): HasMany
{ {
return $this->hasMany(EventDate::class, 'tenant_code', 'tenant_codigo') return $this->hasMany(EventDate::class, 'tenant_code', 'tenant_codigo')
->whereNull('rescheduled_to_event_date_id')
->whereNull('suspended_at')
->orderBy('date') ->orderBy('date')
->orderBy('time_start'); ->orderBy('time_start');
} }

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace App\Domains\Commerce\Catalog\Models; namespace App\Domains\Catalog\Models;
use App\Domains\Ticketing\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\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;

Some files were not shown because too many files have changed in this diff Show More