Compare commits

..

7 Commits

765 changed files with 8010 additions and 29513 deletions

View File

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

View File

@@ -1,29 +1,19 @@
# 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
This project uses a domain-oriented structure grouped by business area under
`app/Domains`. Cross-cutting modules live under `app/Shared`.
This project uses a domain-oriented structure under `app/Domains`.
Business domains must live in:
Each domain must live in:
```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:
```text
app/Domains/<Area>/<DomainName>/
app/Domains/<DomainName>/
├── Models/
├── Controllers/
├── Requests/
@@ -38,28 +28,22 @@ app/Domains/<Area>/<DomainName>/
- Root API routes live in `routes/api.php`.
- `routes/api.php` should act as an aggregator only.
- Each business domain defines its own routes in
`app/Domains/<Area>/<DomainName>/routes/api.php`.
- Shared modules that expose routes define them in
`app/Shared/<ModuleName>/routes/api.php`.
- Each domain defines its own routes in `app/Domains/<DomainName>/routes/api.php`.
## Namespaces
Use this namespace pattern:
```php
App\Domains\<Area>\<DomainName>\Models
App\Domains\<Area>\<DomainName>\Controllers
App\Domains\<Area>\<DomainName>\Requests
App\Domains\<Area>\<DomainName>\Resources
App\Domains\<Area>\<DomainName>\Services
App\Domains\<Area>\<DomainName>\Policies
App\Domains\<Area>\<DomainName>\Exceptions
App\Domains\<DomainName>\Models
App\Domains\<DomainName>\Controllers
App\Domains\<DomainName>\Requests
App\Domains\<DomainName>\Resources
App\Domains\<DomainName>\Services
App\Domains\<DomainName>\Policies
App\Domains\<DomainName>\Exceptions
```
For cross-cutting modules, use `App\Shared\<ModuleName>\...`. Reusable enums
and rules live directly under `App\Shared`.
## Responsibilities
- `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.
- Route files should be named `api.php`.
## Current Areas and Modules
## Current Domains
- `Core`: `Administrator`, `Auth`, `Authorization`, `Bootstrap`, `Client`,
`Menu`, `Staff`, `Tenant`.
- `Commerce`: `Cart`, `Catalog`, `Purchase`, `Sale`.
- `Ticketing`: `Desfile`, `Event`, `FiestaFutbolInfantil`, `Ticket`.
- `Shared`: `Attachable`, `Forms`, `Integration`, `Logging`, `MailTest`,
`Notification`, `StorageTest`, plus shared enums and rules.
- `Tenant`
- `Product`
## Notes For Future Changes
- New features should be added inside the corresponding domain first.
- Add new business features to the appropriate area under `app/Domains`.
- Keep generic reusable code in `app/Shared`; do not use it as a catch-all for
business features.
- If a concern does not belong to an existing domain, create a new domain under `app/Domains`.
- 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
namespace App\Shared\Attachable\Enums;
namespace App\Domains\Attachable\Enums;
enum AttachmentType: string
{

View File

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

View File

@@ -1,8 +1,8 @@
<?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\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?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 Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?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\Request;

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
<?php
namespace App\Domains\Core\Auth\Controllers;
namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Requests\ScannerLoginRequest;
use App\Domains\Core\Auth\Resources\UserResource;
use App\Domains\Core\Auth\Services\PasswordLoginService;
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;

View File

@@ -1,10 +1,10 @@
<?php
namespace App\Domains\Core\Auth\Controllers;
namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Auth\Resources\ScannerMeResource;
use App\Domains\Core\Auth\Services\ScannerContextService;
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;

View File

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

View File

@@ -1,11 +1,10 @@
<?php
namespace App\Domains\Core\Auth\Controllers;
namespace App\Domains\Auth\Controllers;
use App\Domains\Core\Auth\Models\ResetPasswordAttempt;
use App\Domains\Core\Auth\Requests\ValidateResetPasswordAttemptRequest;
use App\Domains\Core\Auth\Services\ResetPasswordAttemptService;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Auth\Requests\ValidateResetPasswordAttemptRequest;
use App\Domains\Auth\Services\ResetPasswordAttemptService;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
@@ -23,19 +22,10 @@ class ValidateResetPasswordAttemptController extends Controller
{
$data = $request->validated();
$result = $this->resetPasswordAttemptService->validateCode(
if (! $this->resetPasswordAttemptService->validateCode(
$data['email'],
$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([
'codigo' => __('api.auth.reset_code_invalid'),
]);

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
<?php
namespace App\Domains\Core\Auth\Models;
namespace App\Domains\Auth\Models;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Core\Authorization\Models\Role;
use App\Domains\Commerce\Catalog\Models\Category;
use App\Domains\Ticketing\Event\Models\EventDateChangeView;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Ticket\Models\ScanAttempt;
use App\Domains\Authorization\Enums\RoleCode;
use App\Domains\Authorization\Models\Role;
use App\Domains\Catalog\Models\Category;
use App\Domains\Tenant\Models\Tenant;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
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\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
#[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
{
/** @use HasFactory<UserFactory> */
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
use HasApiTokens, HasFactory, Notifiable;
protected $attributes = [
'rol_codigo' => RoleCode::User->value,
@@ -48,18 +45,6 @@ class User extends Authenticatable
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>
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
<?php
namespace App\Domains\Core\Auth\Requests;
namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
class UpdateProfileRequest extends FormRequest
{
@@ -13,13 +12,6 @@ class UpdateProfileRequest extends FormRequest
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
{
return [
@@ -27,13 +19,11 @@ class UpdateProfileRequest extends FormRequest
'email' => [
'required',
'email',
Rule::unique('users', 'active_email')->where('rol_codigo', $this->user()->rol_codigo)
->whereNull('deleted_at')
->ignore($this->user()->id),
Rule::unique('users', 'email')->ignore($this->user()->id),
],
'dni' => ['nullable', 'string', 'regex:/^[0-9]{7,8}$/'],
'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
namespace App\Domains\Core\Auth\Requests;
namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?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\Resources\Json\JsonResource;
@@ -24,11 +24,6 @@ class UserResource extends JsonResource
'telefono' => $this->telefono,
'rol_codigo' => $this->rol_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
namespace App\Domains\Core\Auth\Services;
namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Auth\Models\User;
use App\Domains\Authorization\Enums\RoleCode;
class AdminAppContextService
{

View File

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

View File

@@ -1,12 +1,11 @@
<?php
namespace App\Domains\Core\Auth\Services;
namespace App\Domains\Auth\Services;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Shared\Notification\Events\UserRegistered;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer;
use App\Domains\Auth\Models\User;
use App\Domains\Notification\Events\UserRegistered;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Tenant\Support\TenantDomainNormalizer;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
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) {
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) {
$user->forceFill(['google_id' => $googleId])->save();

View File

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

View File

@@ -1,8 +1,8 @@
<?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;
class ProfileService

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?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
{
@@ -19,16 +19,6 @@ class ScannerContextService
$user->setRelation('tenant', $tenant);
if ($tenant->requiresScannerCategoryValidation()) {
$categories = $user->scanCategories()
->orderBy('nombre')
->get();
if ($categories->isNotEmpty()) {
$user->setRelation('scanCategories', $categories);
}
}
return $user;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
<?php
namespace App\Domains\Core\Bootstrap\Controllers;
namespace App\Domains\Bootstrap\Controllers;
use App\Domains\Core\Bootstrap\Requests\ScannerBootstrapRequest;
use App\Domains\Core\Bootstrap\Resources\ScannerBootstrapResource;
use App\Domains\Core\Bootstrap\Services\ScannerBootstrapService;
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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?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 Illuminate\Foundation\Http\FormRequest;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,16 @@
<?php
namespace App\Domains\Commerce\Cart\Models;
namespace App\Domains\Cart\Models;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Inventory;
use App\Domains\Commerce\Catalog\Models\StockReservation;
use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Commerce\Catalog\Services\CatalogInventoryService;
use App\Domains\Commerce\Catalog\Services\StockReservationService;
use App\Domains\Commerce\Purchase\Models\Purchase;
use App\Domains\Commerce\Purchase\Services\UserPurchaseLimitService;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Auth\Models\User;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Catalog\Services\CatalogInventoryService;
use App\Domains\Catalog\Services\StockReservationService;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Services\UserPurchaseLimitService;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -29,7 +28,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
'status',
'origin',
'current_purchase_id',
'current_stock_reservation_id',
])]
class Cart extends Model
{
@@ -38,16 +36,6 @@ class Cart extends Model
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_DIRECT_CHECKOUT = 'direct_checkout';
@@ -57,7 +45,6 @@ class Cart extends Model
return [
'user_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 BelongsTo<StockReservation, $this> */
public function currentStockReservation(): BelongsTo
{
return $this->belongsTo(
StockReservation::class,
'current_stock_reservation_id',
);
}
public function getTotalAmount(): float
{
$items = $this->relationLoaded('items')
@@ -129,7 +107,6 @@ class Cart extends Model
return DB::transaction(function () use ($catalogItemId, $variantId, $quantity): CartItem {
$this->invalidateCurrentCheckout();
app(StockReservationService::class)->assertCartReservationUsable($this);
$selectedItem = $this->resolveScopedItem($catalogItemId, $variantId, true);
$cartQuantity = (int) $this->items()
->where('catalog_item_id', $catalogItemId)
@@ -163,11 +140,12 @@ class Cart extends Model
'cantidad' => $quantity,
]);
} else {
app(StockReservationService::class)->ensure($item, $selectedItem);
$item->cantidad += $quantity;
$item->save();
}
app(StockReservationService::class)->syncCart($this);
app(StockReservationService::class)->reserve($item, $selectedItem, $quantity);
return $item->fresh();
});
@@ -194,7 +172,6 @@ class Cart extends Model
$excludedPurchaseId,
): CartItem {
$this->invalidateCurrentCheckout();
app(StockReservationService::class)->assertCartReservationUsable($this);
/** @var CartItem $item */
$item = $this->items()
@@ -228,6 +205,7 @@ class Cart extends Model
$nextAvailableQuantity,
);
app(StockReservationService::class)->release($item, $currentSelection, $item->cantidad);
$availableQuantity = $inventoryService->availableQuantity($nextSelection);
if ($availableQuantity !== null && $availableQuantity < $quantity) {
@@ -244,10 +222,11 @@ class Cart extends Model
->first();
if ($targetItem !== null) {
app(StockReservationService::class)->ensure($targetItem, $nextSelection);
$targetItem->cantidad += $quantity;
$targetItem->save();
app(StockReservationService::class)->reserve($targetItem, $nextSelection, $quantity);
$item->delete();
app(StockReservationService::class)->syncCart($this);
return $targetItem->fresh();
}
@@ -255,7 +234,7 @@ class Cart extends Model
$item->variant_id = $variantId;
$item->cantidad = $quantity;
$item->save();
app(StockReservationService::class)->syncCart($this);
app(StockReservationService::class)->reserve($item, $nextSelection, $quantity);
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->save();
app(StockReservationService::class)->syncCart($this);
return $item->fresh();
});
@@ -296,7 +282,6 @@ class Cart extends Model
{
DB::transaction(function () use ($cartItemId): void {
$this->invalidateCurrentCheckout();
app(StockReservationService::class)->assertCartReservationUsable($this);
/** @var CartItem $item */
$item = $this->items()
@@ -304,8 +289,17 @@ class Cart extends Model
->lockForUpdate()
->firstOrFail();
$selectedItem = $this->resolveScopedItem(
$item->catalog_item_id,
$item->variant_id,
true,
);
app(StockReservationService::class)->release(
$item,
$selectedItem,
$item->cantidad,
);
$item->delete();
app(StockReservationService::class)->syncCart($this);
});
}
@@ -347,20 +341,13 @@ class Cart extends Model
Purchase::STATUS_CREATED,
Purchase::STATUS_PENDING_PAYMENT,
], true)) {
app(StockReservationService::class)->returnToCart($currentPurchase, $cart);
$currentPurchase->update([
'status' => Purchase::STATUS_SUPERSEDED,
'expires_at' => null,
]);
$this->current_purchase_id = null;
return;
}
app(StockReservationService::class)->releaseForPurchase(
$currentPurchase,
reason: StockReservationService::REASON_PURCHASE_SUPERSEDED,
);
app(StockReservationService::class)->detachFromPurchase($currentPurchase);
self::query()
->whereKey($cart->getKey())
->where('current_purchase_id', $currentPurchase->getKey())
@@ -375,7 +362,7 @@ class Cart extends Model
): CatalogItem|Variant {
$catalogItemQuery = CatalogItem::query()
->whereKey($catalogItemId)
->forTenantCatalog($this->tenant()->firstOrFail());
->where('tenant_code', $this->tenant_codigo);
if ($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;
}
@@ -441,12 +417,6 @@ class Cart extends Model
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);
$variant->setRelation('catalogItem', $catalogItem);
$variant->setRelation('inventory', $inventory);

View File

@@ -1,13 +1,15 @@
<?php
namespace App\Domains\Commerce\Cart\Models;
namespace App\Domains\Cart\Models;
use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Catalog\Models\Variant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'cart_id',
@@ -55,4 +57,10 @@ class CartItem extends Model
{
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
namespace App\Domains\Commerce\Cart\Requests;
namespace App\Domains\Cart\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

View File

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

View File

@@ -1,11 +1,11 @@
<?php
namespace App\Domains\Commerce\Cart\Resources;
namespace App\Domains\Cart\Resources;
use App\Domains\Commerce\Cart\Models\CartItem;
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

View File

@@ -1,8 +1,8 @@
<?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\Resources\Json\JsonResource;

View File

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

View File

@@ -6,12 +6,13 @@ Gestiona el carrito activo de un tenant tanto para visitantes como para usuarios
## 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.
## Servicios
- `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.
## 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.
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` 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.
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`.

View File

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

View File

@@ -1,12 +1,12 @@
<?php
namespace App\Domains\Commerce\Catalog\Controllers\AdminApp;
namespace App\Domains\Catalog\Controllers\AdminApp;
use App\Domains\Commerce\Catalog\Models\FeaturedGroup;
use App\Domains\Commerce\Catalog\Requests\AdminApp\UpsertOnTicketFeaturedGroupRequest;
use App\Domains\Commerce\Catalog\Resources\AdminApp\OnTicketFeaturedGroupResource;
use App\Domains\Commerce\Catalog\Services\OnTicketFeaturedGroupService;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Catalog\Models\FeaturedGroup;
use App\Domains\Catalog\Requests\AdminApp\UpsertOnTicketFeaturedGroupRequest;
use App\Domains\Catalog\Resources\AdminApp\OnTicketFeaturedGroupResource;
use App\Domains\Catalog\Services\OnTicketFeaturedGroupService;
use App\Domains\Tenant\Models\Tenant;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -58,7 +58,7 @@ class OnTicketFeaturedGroupController extends Controller
{
$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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?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\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

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