Compare commits
27 Commits
cf2beb2ff5
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| b294e5c46e | |||
| d02b0c5551 | |||
| ee911171be | |||
| e17d1fa15e | |||
| 2a15dc92be | |||
| 3a039a6055 | |||
| 84e45bb964 | |||
| 8aa3a26ee7 | |||
| 1de08c1ca4 | |||
| 3f9bcd84c4 | |||
| 96f26e2e9d | |||
| c4b317d5fc | |||
| 3b2977a330 | |||
| 3b36abd46d | |||
| 12697c2268 | |||
| f4638bc984 | |||
| c3713c62a6 | |||
| 4a51f3afde | |||
| f1df6a5918 | |||
| dbeaf19513 | |||
| dd76d5d951 | |||
| f603a82b5e | |||
| b478c23f3b | |||
| 6858b387c3 | |||
| 893bea1492 | |||
| ec1d80dff2 | |||
| 3968a10f6a |
26
app/Domains/Auth/Controllers/AdminAppMeController.php
Normal file
26
app/Domains/Auth/Controllers/AdminAppMeController.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Auth\Controllers;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class AdminAppMeController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AdminAppContextService $adminAppContextService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): AdminAppMeResource
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
return AdminAppMeResource::make(
|
||||||
|
$this->adminAppContextService->load($user)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ 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', 'status'])]
|
#[Fillable(['user_id', 'codigo', 'reason', 'status'])]
|
||||||
#[Hidden(['codigo'])]
|
#[Hidden(['codigo'])]
|
||||||
class ResetPasswordAttempt extends Model
|
class ResetPasswordAttempt extends Model
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ namespace App\Domains\Auth\Models;
|
|||||||
|
|
||||||
use App\Domains\Authorization\Enums\RoleCode;
|
use App\Domains\Authorization\Enums\RoleCode;
|
||||||
use App\Domains\Authorization\Models\Role;
|
use App\Domains\Authorization\Models\Role;
|
||||||
|
use App\Domains\Catalog\Models\Category;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
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;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
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\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
@@ -59,6 +61,17 @@ class User extends Authenticatable
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsToMany<Category, $this> */
|
||||||
|
public function scanCategories(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(
|
||||||
|
Category::class,
|
||||||
|
'category_scanners',
|
||||||
|
'user_id',
|
||||||
|
'categoria_id',
|
||||||
|
)->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
|
|||||||
25
app/Domains/Auth/Resources/AdminAppMeResource.php
Normal file
25
app/Domains/Auth/Resources/AdminAppMeResource.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Auth\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Tenant\Resources\TenantResource;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin User
|
||||||
|
*/
|
||||||
|
class AdminAppMeResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user' => UserResource::make($this->resource),
|
||||||
|
'tenant' => TenantResource::make($this->tenant),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Domains/Auth/Services/AdminAppContextService.php
Normal file
26
app/Domains/Auth/Services/AdminAppContextService.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Auth\Services;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Authorization\Enums\RoleCode;
|
||||||
|
|
||||||
|
class AdminAppContextService
|
||||||
|
{
|
||||||
|
public function load(User $user): User
|
||||||
|
{
|
||||||
|
$tenant = $user->tenant()
|
||||||
|
->with([
|
||||||
|
'menues' => fn ($query) => $query
|
||||||
|
->whereHas(
|
||||||
|
'roles',
|
||||||
|
fn ($query) => $query->where('codigo', RoleCode::AdminApp->value)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$user->setRelation('tenant', $tenant);
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,15 @@ use App\Domains\Authorization\Enums\RoleCode;
|
|||||||
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;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class PasswordLoginService
|
class PasswordLoginService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ResetPasswordAttemptService $resetPasswordAttemptService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws AccountLockedException
|
* @throws AccountLockedException
|
||||||
* @throws ValidationException
|
* @throws ValidationException
|
||||||
@@ -118,7 +123,7 @@ class PasswordLoginService
|
|||||||
|
|
||||||
if ($user === null || ! Hash::check($password, $user->password)) {
|
if ($user === null || ! Hash::check($password, $user->password)) {
|
||||||
if ($user !== null) {
|
if ($user !== null) {
|
||||||
$this->registerFailure($user, $now);
|
$this->registerFailure($user, $now, $tenantCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
$outcome = $user?->locked_until?->isFuture()
|
$outcome = $user?->locked_until?->isFuture()
|
||||||
@@ -177,7 +182,7 @@ class PasswordLoginService
|
|||||||
return $result['user'];
|
return $result['user'];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function registerFailure(User $user, CarbonImmutable $now): void
|
private function registerFailure(User $user, CarbonImmutable $now, string $tenantCode): void
|
||||||
{
|
{
|
||||||
$windowMinutes = max(1, (int) config('login-security.attempt_window_minutes'));
|
$windowMinutes = max(1, (int) config('login-security.attempt_window_minutes'));
|
||||||
$maxAttempts = max(1, (int) config('login-security.max_attempts'));
|
$maxAttempts = max(1, (int) config('login-security.max_attempts'));
|
||||||
@@ -189,6 +194,8 @@ class PasswordLoginService
|
|||||||
? $user->failed_login_attempts + 1
|
? $user->failed_login_attempts + 1
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
|
$previousAttempts = $user->failed_login_attempts;
|
||||||
|
|
||||||
$user->forceFill([
|
$user->forceFill([
|
||||||
'failed_login_attempts' => $attempts,
|
'failed_login_attempts' => $attempts,
|
||||||
'last_failed_login_at' => $now,
|
'last_failed_login_at' => $now,
|
||||||
@@ -196,6 +203,17 @@ class PasswordLoginService
|
|||||||
? $now->addMinutes($lockMinutes)
|
? $now->addMinutes($lockMinutes)
|
||||||
: null,
|
: null,
|
||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
|
if ($attempts >= $maxAttempts && $previousAttempts < $maxAttempts) {
|
||||||
|
try {
|
||||||
|
$this->resetPasswordAttemptService->createForEmail($user->email, $tenantCode, 'account_locked');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Failed to trigger reset password on account lock', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'exception' => $e
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function recordAttempt(
|
private function recordAttempt(
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ use Throwable;
|
|||||||
|
|
||||||
class ResetPasswordAttemptService
|
class ResetPasswordAttemptService
|
||||||
{
|
{
|
||||||
public function createForEmail(string $email, string $tenantCode): void
|
public function createForEmail(string $email, string $tenantCode, string $reason = 'manual'): void
|
||||||
{
|
{
|
||||||
$emailFingerprint = $this->emailFingerprint($email);
|
$emailFingerprint = $this->emailFingerprint($email);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$attemptId = DB::transaction(function () use ($email, $emailFingerprint): ?int {
|
$attemptId = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?int {
|
||||||
$user = User::query()
|
$user = User::query()
|
||||||
->where('email', $email)
|
->where('email', $email)
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
@@ -39,6 +39,7 @@ class ResetPasswordAttemptService
|
|||||||
|
|
||||||
$attempt = $user->resetPasswordAttempts()->create([
|
$attempt = $user->resetPasswordAttempts()->create([
|
||||||
'codigo' => $this->generateCode(),
|
'codigo' => $this->generateCode(),
|
||||||
|
'reason' => $reason,
|
||||||
'status' => ResetPasswordAttempt::STATUS_PENDING,
|
'status' => ResetPasswordAttempt::STATUS_PENDING,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Domains\Auth\Controllers\AdminAppLoginController;
|
use App\Domains\Auth\Controllers\AdminAppLoginController;
|
||||||
|
use App\Domains\Auth\Controllers\AdminAppMeController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::prefix('v1/adminapp')->group(function (): void {
|
Route::prefix('v1/adminapp')->group(function (): void {
|
||||||
Route::post('login', AdminAppLoginController::class)->middleware('throttle:login');
|
Route::post('login', AdminAppLoginController::class)->middleware('throttle:login');
|
||||||
|
Route::middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
|
->get('me', AdminAppMeController::class);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ enum RoleCode: string
|
|||||||
{
|
{
|
||||||
case Admin = 'admin';
|
case Admin = 'admin';
|
||||||
case AdminApp = 'adminapp';
|
case AdminApp = 'adminapp';
|
||||||
|
case Scanner = 'scanner';
|
||||||
case User = 'user';
|
case User = 'user';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Bootstrap\Controllers;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
public function __construct(protected AdminAppBootstrapService $bootstrapService) {}
|
||||||
|
|
||||||
|
public function __invoke(AdminAppBootstrapRequest $request): AdminAppBootstrapResource
|
||||||
|
{
|
||||||
|
return AdminAppBootstrapResource::make(
|
||||||
|
$this->bootstrapService->get((string) $request->validated('dominio'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Bootstrap\Controllers;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
public function __construct(protected TenantBootstrapService $bootstrapService) {}
|
||||||
|
|
||||||
|
public function __invoke(TenantBootstrapRequest $request): TenantResource
|
||||||
|
{
|
||||||
|
return TenantResource::make(
|
||||||
|
$this->bootstrapService->get((string) $request->validated('dominio'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Bootstrap\Requests;
|
||||||
|
|
||||||
|
class AdminAppBootstrapRequest extends TenantBootstrapRequest {}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Requests;
|
namespace App\Domains\Bootstrap\Requests;
|
||||||
|
|
||||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class BootstrapTenantRequest extends FormRequest
|
class TenantBootstrapRequest extends FormRequest
|
||||||
{
|
{
|
||||||
protected bool $hasInvalidDomain = false;
|
protected bool $hasInvalidDomain = false;
|
||||||
|
|
||||||
@@ -23,14 +23,10 @@ class BootstrapTenantRequest extends FormRequest
|
|||||||
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
|
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
|
||||||
&& $normalizedDomain === null;
|
&& $normalizedDomain === null;
|
||||||
|
|
||||||
$this->merge([
|
$this->merge(['dominio' => $normalizedDomain]);
|
||||||
'dominio' => $normalizedDomain,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @return array<string, mixed> */
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Bootstrap\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Tenant\Models\WebsiteType;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin array{website_type: WebsiteType} */
|
||||||
|
class AdminAppBootstrapResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
/** @var WebsiteType $websiteType */
|
||||||
|
$websiteType = $this->resource['website_type'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'website_type_code' => $websiteType->codigo,
|
||||||
|
'primary_color' => $websiteType->primary_color,
|
||||||
|
'secondary_color' => $websiteType->secondary_color,
|
||||||
|
'danger_color' => $websiteType->danger_color,
|
||||||
|
'success_color' => $websiteType->success_color,
|
||||||
|
'warning_color' => $websiteType->warning_color,
|
||||||
|
'body_color' => $websiteType->body_color,
|
||||||
|
'darker_body_color' => $websiteType->darker_body_color,
|
||||||
|
'surface_color' => $websiteType->surface_color,
|
||||||
|
'background_color' => $websiteType->background_color,
|
||||||
|
'border_color' => $websiteType->border_color,
|
||||||
|
'login_header_footer_color' => $websiteType->login_header_footer_color,
|
||||||
|
'site_logo' => $websiteType->siteLogo?->getTemporaryUrl(1440),
|
||||||
|
'footer_logo' => $websiteType->footerLogo?->getTemporaryUrl(1440),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Domains/Bootstrap/Services/AdminAppBootstrapService.php
Normal file
19
app/Domains/Bootstrap/Services/AdminAppBootstrapService.php
Normal 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Domains/Bootstrap/Services/TenantBootstrapService.php
Normal file
26
app/Domains/Bootstrap/Services/TenantBootstrapService.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Bootstrap\Services;
|
||||||
|
|
||||||
|
use App\Domains\Authorization\Enums\RoleCode;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Tenant\Services\TenantInformationService;
|
||||||
|
|
||||||
|
class TenantBootstrapService
|
||||||
|
{
|
||||||
|
public function __construct(protected TenantInformationService $tenantInformationService) {}
|
||||||
|
|
||||||
|
public function get(string $domain): Tenant
|
||||||
|
{
|
||||||
|
return $this->tenantInformationService->load(
|
||||||
|
Tenant::query()->where('dominio', $domain)->firstOrFail(),
|
||||||
|
[
|
||||||
|
'menues' => fn ($query) => $query->whereHas(
|
||||||
|
'roles',
|
||||||
|
fn ($query) => $query->where('codigo', RoleCode::User->value)
|
||||||
|
),
|
||||||
|
'categories' => fn ($query) => $query->orderBy('nombre'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
app/Domains/Bootstrap/routes/adminapp.php
Normal file
9
app/Domains/Bootstrap/routes/adminapp.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Bootstrap\Controllers\AdminAppBootstrapController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get(
|
||||||
|
'v1/adminapp/bootstrap/{dominio}',
|
||||||
|
AdminAppBootstrapController::class
|
||||||
|
);
|
||||||
9
app/Domains/Bootstrap/routes/api.php
Normal file
9
app/Domains/Bootstrap/routes/api.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Bootstrap\Controllers\TenantBootstrapController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('tenants/bootstrap/{dominio}', TenantBootstrapController::class)
|
||||||
|
->where('dominio', '.*');
|
||||||
|
|
||||||
|
require __DIR__.'/adminapp.php';
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Controllers\AdminApp;
|
||||||
|
|
||||||
|
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;
|
||||||
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
|
||||||
|
class OnTicketFeaturedGroupController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly OnTicketFeaturedGroupService $featuredGroupService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function index(Request $request): AnonymousResourceCollection
|
||||||
|
{
|
||||||
|
return OnTicketFeaturedGroupResource::collection(
|
||||||
|
$this->featuredGroupService->forTenant($this->onTicketTenant($request))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(UpsertOnTicketFeaturedGroupRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$featuredGroup = $this->featuredGroupService->create(
|
||||||
|
$this->onTicketTenant($request),
|
||||||
|
$request->validated(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return OnTicketFeaturedGroupResource::make($featuredGroup)
|
||||||
|
->response()
|
||||||
|
->setStatusCode(201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(
|
||||||
|
UpsertOnTicketFeaturedGroupRequest $request,
|
||||||
|
FeaturedGroup $featuredGroup,
|
||||||
|
): OnTicketFeaturedGroupResource {
|
||||||
|
$tenant = $this->onTicketTenant($request);
|
||||||
|
|
||||||
|
abort_unless($featuredGroup->tenant_code === $tenant->codigo, 404);
|
||||||
|
|
||||||
|
return OnTicketFeaturedGroupResource::make(
|
||||||
|
$this->featuredGroupService->update(
|
||||||
|
$tenant,
|
||||||
|
$featuredGroup,
|
||||||
|
$request->validated(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function onTicketTenant(Request $request): Tenant
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
abort_unless($tenant->website_type_code === 'onticket', 404);
|
||||||
|
|
||||||
|
return $tenant;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,34 +2,28 @@
|
|||||||
|
|
||||||
namespace App\Domains\Catalog\Controllers;
|
namespace App\Domains\Catalog\Controllers;
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\GroupLayout;
|
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\Category;
|
use App\Domains\Catalog\Models\Category;
|
||||||
use App\Domains\Catalog\Models\FeaturedGroup;
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
use App\Domains\Catalog\Models\FeaturedItem;
|
|
||||||
use App\Domains\Catalog\Requests\CatalogItemDetailRequest;
|
use App\Domains\Catalog\Requests\CatalogItemDetailRequest;
|
||||||
use App\Domains\Catalog\Requests\CategoryPageRequest;
|
use App\Domains\Catalog\Requests\CategoryPageRequest;
|
||||||
use App\Domains\Catalog\Requests\FeaturedGroupPageRequest;
|
use App\Domains\Catalog\Requests\FeaturedGroupPageRequest;
|
||||||
use App\Domains\Catalog\Requests\SearchCatalogItemsRequest;
|
use App\Domains\Catalog\Requests\SearchCatalogItemsRequest;
|
||||||
use App\Domains\Catalog\Requests\StoreCatalogItemRequest;
|
use App\Domains\Catalog\Requests\StoreCatalogItemRequest;
|
||||||
use App\Domains\Catalog\Resources\CatalogFeaturedGroupResource;
|
use App\Domains\Catalog\Resources\CatalogFeaturedGroupResource;
|
||||||
use App\Domains\Catalog\Resources\CatalogFeaturedItemResource;
|
|
||||||
use App\Domains\Catalog\Resources\CatalogItemDetailResource;
|
use App\Domains\Catalog\Resources\CatalogItemDetailResource;
|
||||||
use App\Domains\Catalog\Resources\CatalogItemResource;
|
use App\Domains\Catalog\Resources\CatalogItemResource;
|
||||||
use App\Domains\Catalog\Resources\CatalogSearchItemResource;
|
use App\Domains\Catalog\Resources\CatalogSearchItemResource;
|
||||||
use App\Domains\Catalog\Services\CatalogService;
|
use App\Domains\Catalog\Services\CatalogService;
|
||||||
|
use App\Domains\Catalog\Services\FeaturedGroupService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
class CatalogController extends Controller
|
class CatalogController extends Controller
|
||||||
{
|
{
|
||||||
private const ITEMS_PER_PAGE = 12;
|
public function index(Tenant $tenant, FeaturedGroupService $featuredGroupService): JsonResponse
|
||||||
|
|
||||||
public function index(Tenant $tenant): JsonResponse
|
|
||||||
{
|
{
|
||||||
$featuredGroups = FeaturedGroup::query()
|
$featuredGroups = FeaturedGroup::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
@@ -39,7 +33,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,
|
||||||
$this->featuredItemsResponse($featuredGroup, 1),
|
$featuredGroupService->itemsResponse($featuredGroup, 1),
|
||||||
))->resolve()
|
))->resolve()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -89,12 +83,13 @@ class CatalogController extends Controller
|
|||||||
FeaturedGroupPageRequest $request,
|
FeaturedGroupPageRequest $request,
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
FeaturedGroup $featuredGroup,
|
FeaturedGroup $featuredGroup,
|
||||||
|
FeaturedGroupService $featuredGroupService,
|
||||||
): JsonResponse {
|
): JsonResponse {
|
||||||
abort_unless($featuredGroup->tenant_code === $tenant->codigo, 404);
|
abort_unless($featuredGroup->tenant_code === $tenant->codigo, 404);
|
||||||
|
|
||||||
$page = (int) $request->validated('page', 1);
|
$page = (int) $request->validated('page', 1);
|
||||||
|
|
||||||
return response()->json($this->featuredItemsResponse($featuredGroup, $page));
|
return response()->json($featuredGroupService->itemsResponse($featuredGroup, $page));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(
|
public function show(
|
||||||
@@ -129,58 +124,4 @@ class CatalogController extends Controller
|
|||||||
->response()
|
->response()
|
||||||
->setStatusCode(201);
|
->setStatusCode(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<array-key, mixed> */
|
|
||||||
private function featuredItemsResponse(FeaturedGroup $featuredGroup, int $page): array
|
|
||||||
{
|
|
||||||
if ($featuredGroup->group_layout !== GroupLayout::Paginated) {
|
|
||||||
$featuredItems = $this->featuredItemsQuery($featuredGroup)->get();
|
|
||||||
|
|
||||||
$featuredItems->each(
|
|
||||||
fn ($featuredItem) => $featuredItem->setRelation('featuredGroup', $featuredGroup)
|
|
||||||
);
|
|
||||||
|
|
||||||
return CatalogFeaturedItemResource::collection($featuredItems)->resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
$paginator = $this->paginateFeaturedItems($featuredGroup, $page);
|
|
||||||
|
|
||||||
$paginator->getCollection()->each(
|
|
||||||
fn ($featuredItem) => $featuredItem->setRelation('featuredGroup', $featuredGroup)
|
|
||||||
);
|
|
||||||
|
|
||||||
return CatalogFeaturedItemResource::collection($paginator)
|
|
||||||
->response()
|
|
||||||
->getData(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function paginateFeaturedItems(
|
|
||||||
FeaturedGroup $featuredGroup,
|
|
||||||
int $page,
|
|
||||||
): LengthAwarePaginator {
|
|
||||||
$paginator = $this->featuredItemsQuery($featuredGroup)->paginate(
|
|
||||||
perPage: self::ITEMS_PER_PAGE,
|
|
||||||
pageName: 'page',
|
|
||||||
page: $page,
|
|
||||||
);
|
|
||||||
|
|
||||||
return $paginator->withPath(route('catalog.featured-groups.items.index', [
|
|
||||||
'tenant' => $featuredGroup->tenant_code,
|
|
||||||
'featuredGroup' => $featuredGroup->id,
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return HasMany<FeaturedItem, FeaturedGroup> */
|
|
||||||
private function featuredItemsQuery(FeaturedGroup $featuredGroup): HasMany
|
|
||||||
{
|
|
||||||
return $featuredGroup->featuredItems()->with([
|
|
||||||
'catalogItem.inventory',
|
|
||||||
'catalogItem.attachments',
|
|
||||||
'catalogItem.variants.inventory',
|
|
||||||
'catalogItem.variants.attachments',
|
|
||||||
'catalogItem.variants.definitions.itemAttribute.attribute',
|
|
||||||
'catalogItem.bundleComponents.catalogItem',
|
|
||||||
'catalogItem.bundleComponents.variant.catalogItem',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
app/Domains/Catalog/Enums/EventProductType.php
Normal file
15
app/Domains/Catalog/Enums/EventProductType.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Enums;
|
||||||
|
|
||||||
|
enum EventProductType: string
|
||||||
|
{
|
||||||
|
case Entry = 'entrada';
|
||||||
|
case Product = 'producto';
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(self::cases(), 'value');
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/Domains/Catalog/Enums/FeaturedGroupSource.php
Normal file
18
app/Domains/Catalog/Enums/FeaturedGroupSource.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Enums;
|
||||||
|
|
||||||
|
enum FeaturedGroupSource: string
|
||||||
|
{
|
||||||
|
case Manual = 'manual';
|
||||||
|
case Category = 'category';
|
||||||
|
case All = 'all';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(self::cases(), 'value');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,12 @@ namespace App\Domains\Catalog\Models;
|
|||||||
|
|
||||||
use App\Domains\Attachable\Models\Attachment;
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||||
|
use App\Domains\Catalog\Enums\EventProductType;
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Services\CatalogInventoryService;
|
use App\Domains\Catalog\Services\CatalogInventoryService;
|
||||||
|
use App\Domains\Event\Models\Event;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -17,6 +20,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'tenant_code',
|
'tenant_code',
|
||||||
|
'event_id',
|
||||||
|
'event_product_type',
|
||||||
'category_id',
|
'category_id',
|
||||||
'brand_id',
|
'brand_id',
|
||||||
'inventory_id',
|
'inventory_id',
|
||||||
@@ -50,6 +55,8 @@ class CatalogItem extends Model
|
|||||||
'category_id' => 'integer',
|
'category_id' => 'integer',
|
||||||
'brand_id' => 'integer',
|
'brand_id' => 'integer',
|
||||||
'inventory_id' => 'integer',
|
'inventory_id' => 'integer',
|
||||||
|
'event_id' => 'integer',
|
||||||
|
'event_product_type' => EventProductType::class,
|
||||||
'type' => CatalogItemType::class,
|
'type' => CatalogItemType::class,
|
||||||
'precio' => 'decimal:2',
|
'precio' => 'decimal:2',
|
||||||
'inventory_policy' => InventoryPolicy::class,
|
'inventory_policy' => InventoryPolicy::class,
|
||||||
@@ -65,6 +72,12 @@ class CatalogItem extends Model
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Event, $this> */
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<Category, $this> */
|
/** @return BelongsTo<Category, $this> */
|
||||||
public function category(): BelongsTo
|
public function category(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -101,6 +114,12 @@ class CatalogItem extends Model
|
|||||||
return $this->hasMany(Variant::class);
|
return $this->hasMany(Variant::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<Ticket, $this> */
|
||||||
|
public function sourceTickets(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Ticket::class, 'source_catalog_item_id');
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsToMany<Attribute, $this> */
|
/** @return BelongsToMany<Attribute, $this> */
|
||||||
public function attributes(): BelongsToMany
|
public function attributes(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Domains\Catalog\Models;
|
namespace App\Domains\Catalog\Models;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
use App\Domains\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;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
@@ -64,4 +66,15 @@ class Category extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(CatalogItem::class);
|
return $this->hasMany(CatalogItem::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsToMany<User, $this> */
|
||||||
|
public function scanners(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(
|
||||||
|
User::class,
|
||||||
|
'category_scanners',
|
||||||
|
'categoria_id',
|
||||||
|
'user_id',
|
||||||
|
)->withTimestamps();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domains\Catalog\Models;
|
namespace App\Domains\Catalog\Models;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Enums\FeaturedGroupSource;
|
||||||
use App\Domains\Catalog\Enums\GroupLayout;
|
use App\Domains\Catalog\Enums\GroupLayout;
|
||||||
use App\Domains\Catalog\Enums\ProductLayout;
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
@@ -13,6 +14,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'tenant_code',
|
'tenant_code',
|
||||||
|
'source_type',
|
||||||
|
'category_id',
|
||||||
'product_layout',
|
'product_layout',
|
||||||
'group_layout',
|
'group_layout',
|
||||||
'group_name',
|
'group_name',
|
||||||
@@ -26,9 +29,15 @@ class FeaturedGroup extends Model
|
|||||||
|
|
||||||
protected $table = 'featured_groups';
|
protected $table = 'featured_groups';
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'source_type' => FeaturedGroupSource::Manual->value,
|
||||||
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'source_type' => FeaturedGroupSource::class,
|
||||||
|
'category_id' => 'integer',
|
||||||
'product_layout' => ProductLayout::class,
|
'product_layout' => ProductLayout::class,
|
||||||
'group_layout' => GroupLayout::class,
|
'group_layout' => GroupLayout::class,
|
||||||
'group_order' => 'integer',
|
'group_order' => 'integer',
|
||||||
@@ -41,6 +50,12 @@ class FeaturedGroup extends Model
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Category, $this> */
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return HasMany<FeaturedItem, $this> */
|
/** @return HasMany<FeaturedItem, $this> */
|
||||||
public function featuredItems(): HasMany
|
public function featuredItems(): HasMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Domains\Catalog\Models;
|
namespace App\Domains\Catalog\Models;
|
||||||
|
|
||||||
use App\Domains\Attachable\Models\Attachment;
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
|
use App\Domains\Event\Models\EventDate;
|
||||||
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -13,6 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'catalog_item_id',
|
'catalog_item_id',
|
||||||
|
'event_date_id',
|
||||||
'inventory_id',
|
'inventory_id',
|
||||||
'minimum_use_date',
|
'minimum_use_date',
|
||||||
'maximum_use_date',
|
'maximum_use_date',
|
||||||
@@ -29,6 +32,7 @@ class Variant extends Model
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'catalog_item_id' => 'integer',
|
'catalog_item_id' => 'integer',
|
||||||
|
'event_date_id' => 'integer',
|
||||||
'inventory_id' => 'integer',
|
'inventory_id' => 'integer',
|
||||||
'minimum_use_date' => 'datetime',
|
'minimum_use_date' => 'datetime',
|
||||||
'maximum_use_date' => 'datetime',
|
'maximum_use_date' => 'datetime',
|
||||||
@@ -41,6 +45,18 @@ class Variant extends Model
|
|||||||
return $this->belongsTo(CatalogItem::class);
|
return $this->belongsTo(CatalogItem::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<EventDate, $this> */
|
||||||
|
public function eventDate(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(EventDate::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<Ticket, $this> */
|
||||||
|
public function sourceTickets(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Ticket::class, 'source_variant_id');
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<Inventory, $this> */
|
/** @return BelongsTo<Inventory, $this> */
|
||||||
public function inventory(): BelongsTo
|
public function inventory(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -79,31 +95,20 @@ class Variant extends Model
|
|||||||
|
|
||||||
public function getName(): string
|
public function getName(): string
|
||||||
{
|
{
|
||||||
$name = $this->catalogItem->nombre;
|
return $this->catalogItem->nombre;
|
||||||
$this->loadMissing('definitions.itemAttribute.attribute');
|
|
||||||
$definitions = $this->definitions
|
|
||||||
->map(function (VariantDefinition $definition): ?string {
|
|
||||||
$attributeName = $definition->itemAttribute?->attribute?->nombre;
|
|
||||||
|
|
||||||
return $attributeName
|
|
||||||
? "{$attributeName}: {$definition->value}"
|
|
||||||
: $definition->value;
|
|
||||||
})
|
|
||||||
->filter()
|
|
||||||
->implode(', ');
|
|
||||||
|
|
||||||
return $definitions === '' ? $name : "{$name} ({$definitions})";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMinimumUseDate(): ?CarbonInterface
|
public function getMinimumUseDate(): ?CarbonInterface
|
||||||
{
|
{
|
||||||
return $this->minimum_use_date
|
return $this->eventDate?->startsAt()
|
||||||
|
?? $this->minimum_use_date
|
||||||
?? $this->catalogItem->getMinimumUseDate();
|
?? $this->catalogItem->getMinimumUseDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMaximumUseDate(): ?CarbonInterface
|
public function getMaximumUseDate(): ?CarbonInterface
|
||||||
{
|
{
|
||||||
return $this->maximum_use_date
|
return $this->eventDate?->endsAt()
|
||||||
|
?? $this->maximum_use_date
|
||||||
?? $this->catalogItem->getMaximumUseDate();
|
?? $this->catalogItem->getMaximumUseDate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Requests\AdminApp;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpsertOnTicketFeaturedGroupRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'category_name' => ['required', 'string', 'max:255'],
|
||||||
|
'is_featured' => ['required', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Domains\Catalog\Requests;
|
namespace App\Domains\Catalog\Requests;
|
||||||
|
|
||||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||||
|
use App\Domains\Catalog\Enums\EventProductType;
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
@@ -25,6 +26,20 @@ class StoreCatalogItemRequest extends FormRequest
|
|||||||
return [
|
return [
|
||||||
'tenant_code' => ['prohibited'],
|
'tenant_code' => ['prohibited'],
|
||||||
'type' => ['sometimes', Rule::enum(CatalogItemType::class)],
|
'type' => ['sometimes', Rule::enum(CatalogItemType::class)],
|
||||||
|
'event_id' => [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
'required_with:event_product_type',
|
||||||
|
Rule::exists('events', 'id')->where(
|
||||||
|
fn ($query) => $query->where('tenant_code', $tenantCode)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'event_product_type' => [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
'required_with:event_id',
|
||||||
|
Rule::enum(EventProductType::class),
|
||||||
|
],
|
||||||
'category_id' => [
|
'category_id' => [
|
||||||
'sometimes',
|
'sometimes',
|
||||||
'nullable',
|
'nullable',
|
||||||
@@ -73,6 +88,14 @@ class StoreCatalogItemRequest extends FormRequest
|
|||||||
'images.*' => ['required', new ImageOrBase64Rule],
|
'images.*' => ['required', new ImageOrBase64Rule],
|
||||||
'variants' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'],
|
'variants' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'],
|
||||||
'variants.*.real_stock' => ['sometimes', 'integer', 'min:0'],
|
'variants.*.real_stock' => ['sometimes', 'integer', 'min:0'],
|
||||||
|
'variants.*.event_date_id' => [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('event_dates', 'id')->where(
|
||||||
|
fn ($query) => $query->where('event_id', $this->input('event_id'))
|
||||||
|
),
|
||||||
|
],
|
||||||
'variants.*.inventory_id' => ['prohibited'],
|
'variants.*.inventory_id' => ['prohibited'],
|
||||||
'variants.*.reserved_stock' => ['prohibited'],
|
'variants.*.reserved_stock' => ['prohibited'],
|
||||||
'variants.*.sold_units' => ['prohibited'],
|
'variants.*.sold_units' => ['prohibited'],
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Resources\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin FeaturedGroup */
|
||||||
|
class OnTicketFeaturedGroupResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'category_id' => $this->category_id,
|
||||||
|
'category_name' => $this->category->nombre,
|
||||||
|
'group_name' => $this->group_name,
|
||||||
|
'is_featured' => $this->product_layout === ProductLayout::Row,
|
||||||
|
'type' => $this->source_type->value,
|
||||||
|
'product_layout' => $this->product_layout->value,
|
||||||
|
'group_layout' => $this->group_layout->value,
|
||||||
|
'order' => $this->group_order,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,20 +5,22 @@ namespace App\Domains\Catalog\Resources;
|
|||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Enums\ProductLayout;
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\FeaturedItem;
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
/** @mixin FeaturedItem */
|
/** @mixin CatalogItem */
|
||||||
class CatalogFeaturedItemResource extends JsonResource
|
class CatalogFeaturedItemResource extends JsonResource
|
||||||
{
|
{
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
$catalogItem = $this->catalogItem;
|
$catalogItem = $this->resource;
|
||||||
|
/** @var FeaturedGroup $featuredGroup */
|
||||||
|
$featuredGroup = $catalogItem->getRelation('featuredGroup');
|
||||||
|
|
||||||
if ($this->featuredGroup->product_layout === ProductLayout::ColumnWithImage) {
|
if ($featuredGroup->product_layout === ProductLayout::ColumnWithImage) {
|
||||||
return $this->columnWithImageData($catalogItem);
|
return $this->columnWithImageData($catalogItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +34,8 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'variants' => $catalogItem->variants
|
'variants' => $catalogItem->variants
|
||||||
->map(fn (Variant $variant): array => [
|
->map(fn (Variant $variant): array => [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
|
'event_date_id' => $variant->event_date_id,
|
||||||
|
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
|
||||||
'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
||||||
? null
|
? null
|
||||||
: $variant->inventory->availableStock(),
|
: $variant->inventory->availableStock(),
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'type' => $this->type->value,
|
'type' => $this->type->value,
|
||||||
|
'event_id' => $this->event_id,
|
||||||
|
'event_product_type' => $this->event_product_type?->value,
|
||||||
'category_id' => $this->category_id,
|
'category_id' => $this->category_id,
|
||||||
'brand_id' => $this->brand_id,
|
'brand_id' => $this->brand_id,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
@@ -110,12 +112,16 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
|
'event_date_id' => $variant->event_date_id,
|
||||||
|
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
|
||||||
'stock_tecnico' => $this->variantStock($variant),
|
'stock_tecnico' => $this->variantStock($variant),
|
||||||
'minimum_use_date' => $variant->minimum_use_date,
|
'minimum_use_date' => $variant->minimum_use_date,
|
||||||
'maximum_use_date' => $variant->maximum_use_date,
|
'maximum_use_date' => $variant->maximum_use_date,
|
||||||
'effective_minimum_use_date' => $variant->minimum_use_date
|
'effective_minimum_use_date' => $variant->eventDate?->startsAt()
|
||||||
|
?? $variant->minimum_use_date
|
||||||
?? $this->minimum_use_date,
|
?? $this->minimum_use_date,
|
||||||
'effective_maximum_use_date' => $variant->maximum_use_date
|
'effective_maximum_use_date' => $variant->eventDate?->endsAt()
|
||||||
|
?? $variant->maximum_use_date
|
||||||
?? $this->maximum_use_date,
|
?? $this->maximum_use_date,
|
||||||
'values' => $variant->definitions
|
'values' => $variant->definitions
|
||||||
->mapWithKeys(fn ($definition) => [
|
->mapWithKeys(fn ($definition) => [
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class CatalogItemResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'type' => $this->type->value,
|
'type' => $this->type->value,
|
||||||
|
'event_id' => $this->event_id,
|
||||||
|
'event_product_type' => $this->event_product_type?->value,
|
||||||
'category_id' => $this->category_id,
|
'category_id' => $this->category_id,
|
||||||
'brand_id' => $this->brand_id,
|
'brand_id' => $this->brand_id,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
@@ -32,12 +34,16 @@ class CatalogItemResource extends JsonResource
|
|||||||
'variants' => $this->whenLoaded('variants', fn () => $this->variants
|
'variants' => $this->whenLoaded('variants', fn () => $this->variants
|
||||||
->map(fn ($variant) => [
|
->map(fn ($variant) => [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
|
'event_date_id' => $variant->event_date_id,
|
||||||
|
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
|
||||||
'real_stock' => $variant->inventory?->real_stock,
|
'real_stock' => $variant->inventory?->real_stock,
|
||||||
'minimum_use_date' => $variant->minimum_use_date,
|
'minimum_use_date' => $variant->minimum_use_date,
|
||||||
'maximum_use_date' => $variant->maximum_use_date,
|
'maximum_use_date' => $variant->maximum_use_date,
|
||||||
'effective_minimum_use_date' => $variant->minimum_use_date
|
'effective_minimum_use_date' => $variant->eventDate?->startsAt()
|
||||||
|
?? $variant->minimum_use_date
|
||||||
?? $this->minimum_use_date,
|
?? $this->minimum_use_date,
|
||||||
'effective_maximum_use_date' => $variant->maximum_use_date
|
'effective_maximum_use_date' => $variant->eventDate?->endsAt()
|
||||||
|
?? $variant->maximum_use_date
|
||||||
?? $this->maximum_use_date,
|
?? $this->maximum_use_date,
|
||||||
'values' => $variant->definitions
|
'values' => $variant->definitions
|
||||||
->mapWithKeys(fn ($definition) => [
|
->mapWithKeys(fn ($definition) => [
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class CatalogSearchItemResource extends JsonResource
|
|||||||
'variants' => $this->variants
|
'variants' => $this->variants
|
||||||
->map(fn (Variant $variant): array => [
|
->map(fn (Variant $variant): array => [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
|
'event_date_id' => $variant->event_date_id,
|
||||||
|
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
|
||||||
'stock_tecnico' => $this->inventory_policy === InventoryPolicy::Unlimited
|
'stock_tecnico' => $this->inventory_policy === InventoryPolicy::Unlimited
|
||||||
? null
|
? null
|
||||||
: $variant->inventory?->availableStock(),
|
: $variant->inventory?->availableStock(),
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ namespace App\Domains\Catalog\Services;
|
|||||||
use App\Domains\Attachable\Models\Attachment;
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
use App\Domains\Attachable\Services\AttachmentService;
|
use App\Domains\Attachable\Services\AttachmentService;
|
||||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||||
|
use App\Domains\Catalog\Enums\EventProductType;
|
||||||
use App\Domains\Catalog\Models\Attribute;
|
use App\Domains\Catalog\Models\Attribute;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\Category;
|
use App\Domains\Catalog\Models\Category;
|
||||||
use App\Domains\Catalog\Models\Inventory;
|
use App\Domains\Catalog\Models\Inventory;
|
||||||
use App\Domains\Catalog\Models\ItemAttribute;
|
use App\Domains\Catalog\Models\ItemAttribute;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Event\Models\Event;
|
||||||
|
use App\Domains\Event\Models\EventDate;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
@@ -39,7 +42,12 @@ class CatalogService
|
|||||||
$hasDirectStock = array_key_exists('real_stock', $data);
|
$hasDirectStock = array_key_exists('real_stock', $data);
|
||||||
$realStock = (int) ($data['real_stock'] ?? 0);
|
$realStock = (int) ($data['real_stock'] ?? 0);
|
||||||
|
|
||||||
$hasVariants = $attributeCodes !== [];
|
$hasEventDateVariants = $variants !== [] && collect($variants)->every(
|
||||||
|
fn (array $variant): bool => ! empty($variant['event_date_id'])
|
||||||
|
);
|
||||||
|
$hasVariants = $attributeCodes !== [] || $hasEventDateVariants;
|
||||||
|
|
||||||
|
$this->validateEventData($data);
|
||||||
|
|
||||||
if ($type === CatalogItemType::Bundle) {
|
if ($type === CatalogItemType::Bundle) {
|
||||||
$this->validateBundleData($data, $components);
|
$this->validateBundleData($data, $components);
|
||||||
@@ -121,9 +129,11 @@ class CatalogService
|
|||||||
'inventory',
|
'inventory',
|
||||||
'category',
|
'category',
|
||||||
'brand',
|
'brand',
|
||||||
|
'event',
|
||||||
'itemAttributes.attribute',
|
'itemAttributes.attribute',
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
|
'variants.eventDate',
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute',
|
||||||
'bundleComponents.catalogItem',
|
'bundleComponents.catalogItem',
|
||||||
'bundleComponents.variant.catalogItem',
|
'bundleComponents.variant.catalogItem',
|
||||||
@@ -138,10 +148,12 @@ class CatalogService
|
|||||||
'inventory',
|
'inventory',
|
||||||
'category',
|
'category',
|
||||||
'brand',
|
'brand',
|
||||||
|
'event',
|
||||||
'itemAttributes.attribute.options',
|
'itemAttributes.attribute.options',
|
||||||
'variants' => fn ($query) => $query->orderBy('id'),
|
'variants' => fn ($query) => $query->orderBy('id'),
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
|
'variants.eventDate',
|
||||||
'variants.definitions' => fn ($query) => $query->orderBy('id'),
|
'variants.definitions' => fn ($query) => $query->orderBy('id'),
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute',
|
||||||
'bundleComponents.catalogItem.inventory',
|
'bundleComponents.catalogItem.inventory',
|
||||||
@@ -195,6 +207,7 @@ class CatalogService
|
|||||||
'inventory',
|
'inventory',
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
|
'variants.eventDate',
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute',
|
||||||
'bundleComponents.catalogItem',
|
'bundleComponents.catalogItem',
|
||||||
'bundleComponents.variant.catalogItem',
|
'bundleComponents.variant.catalogItem',
|
||||||
@@ -226,6 +239,7 @@ class CatalogService
|
|||||||
'inventory',
|
'inventory',
|
||||||
'variants.inventory',
|
'variants.inventory',
|
||||||
'variants.attachments',
|
'variants.attachments',
|
||||||
|
'variants.eventDate',
|
||||||
'variants.definitions.itemAttribute.attribute',
|
'variants.definitions.itemAttribute.attribute',
|
||||||
'bundleComponents.catalogItem',
|
'bundleComponents.catalogItem',
|
||||||
'bundleComponents.variant.catalogItem',
|
'bundleComponents.variant.catalogItem',
|
||||||
@@ -472,8 +486,28 @@ class CatalogService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$inventory = $this->createInventory((int) ($data['real_stock'] ?? 0));
|
$inventory = $this->createInventory((int) ($data['real_stock'] ?? 0));
|
||||||
|
$eventDateId = $data['event_date_id'] ?? null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
$eventDateId !== null
|
||||||
|
&& (
|
||||||
|
$catalogItem->event_id === null
|
||||||
|
|| ! EventDate::query()
|
||||||
|
->whereKey($eventDateId)
|
||||||
|
->where('event_id', $catalogItem->event_id)
|
||||||
|
->exists()
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
"variants.{$index}.event_date_id" => [
|
||||||
|
'The event date must belong to the catalog item event.',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$variant = $catalogItem->variants()->create([
|
$variant = $catalogItem->variants()->create([
|
||||||
'inventory_id' => $inventory->id,
|
'inventory_id' => $inventory->id,
|
||||||
|
'event_date_id' => $eventDateId,
|
||||||
'minimum_use_date' => $data['minimum_use_date'] ?? null,
|
'minimum_use_date' => $data['minimum_use_date'] ?? null,
|
||||||
'maximum_use_date' => $data['maximum_use_date'] ?? null,
|
'maximum_use_date' => $data['maximum_use_date'] ?? null,
|
||||||
]);
|
]);
|
||||||
@@ -501,6 +535,38 @@ class CatalogService
|
|||||||
return $variant;
|
return $variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $data */
|
||||||
|
private function validateEventData(array $data): void
|
||||||
|
{
|
||||||
|
$eventId = $data['event_id'] ?? null;
|
||||||
|
$eventProductType = $data['event_product_type'] ?? null;
|
||||||
|
|
||||||
|
if (($eventId === null) !== ($eventProductType === null)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'event_id' => ['Event and event product type must be provided together.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($eventId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Event::query()
|
||||||
|
->whereKey($eventId)
|
||||||
|
->where('tenant_code', $data['tenant_code'] ?? null)
|
||||||
|
->exists()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'event_id' => ['The event must belong to the catalog item tenant.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($eventProductType, EventProductType::values(), true)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'event_product_type' => ['The event product type is invalid.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function validateVariantUseDates(Variant $variant, int $index): void
|
private function validateVariantUseDates(Variant $variant, int $index): void
|
||||||
{
|
{
|
||||||
$minimumUseDate = $variant->getMinimumUseDate();
|
$minimumUseDate = $variant->getMinimumUseDate();
|
||||||
|
|||||||
87
app/Domains/Catalog/Services/FeaturedGroupService.php
Normal file
87
app/Domains/Catalog/Services/FeaturedGroupService.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Services;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Enums\FeaturedGroupSource;
|
||||||
|
use App\Domains\Catalog\Enums\GroupLayout;
|
||||||
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
|
use App\Domains\Catalog\Resources\CatalogFeaturedItemResource;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class FeaturedGroupService
|
||||||
|
{
|
||||||
|
private const ITEMS_PER_PAGE = 12;
|
||||||
|
|
||||||
|
/** @return array<array-key, mixed> */
|
||||||
|
public function itemsResponse(FeaturedGroup $featuredGroup, int $page): array
|
||||||
|
{
|
||||||
|
if ($featuredGroup->group_layout !== GroupLayout::Paginated) {
|
||||||
|
$items = $this->itemsQuery($featuredGroup)->get();
|
||||||
|
$this->attachGroup($items, $featuredGroup);
|
||||||
|
|
||||||
|
return CatalogFeaturedItemResource::collection($items)->resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
$paginator = $this->paginateItems($featuredGroup, $page);
|
||||||
|
$this->attachGroup($paginator->getCollection(), $featuredGroup);
|
||||||
|
|
||||||
|
return CatalogFeaturedItemResource::collection($paginator)
|
||||||
|
->response()
|
||||||
|
->getData(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Builder<CatalogItem> */
|
||||||
|
private function itemsQuery(FeaturedGroup $featuredGroup): Builder
|
||||||
|
{
|
||||||
|
$query = CatalogItem::query()
|
||||||
|
->where('catalog_items.tenant_code', $featuredGroup->tenant_code)
|
||||||
|
->with([
|
||||||
|
'inventory',
|
||||||
|
'attachments',
|
||||||
|
'variants.inventory',
|
||||||
|
'variants.attachments',
|
||||||
|
'variants.eventDate',
|
||||||
|
'variants.definitions.itemAttribute.attribute',
|
||||||
|
'bundleComponents.catalogItem',
|
||||||
|
'bundleComponents.variant.catalogItem',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return match ($featuredGroup->source_type) {
|
||||||
|
FeaturedGroupSource::Manual => $query
|
||||||
|
->select('catalog_items.*')
|
||||||
|
->join('featured_items', 'featured_items.catalog_item_id', '=', 'catalog_items.id')
|
||||||
|
->where('featured_items.featured_group_id', $featuredGroup->id)
|
||||||
|
->orderBy('featured_items.order')
|
||||||
|
->orderBy('featured_items.id'),
|
||||||
|
FeaturedGroupSource::Category => $query
|
||||||
|
->where('catalog_items.category_id', $featuredGroup->category_id)
|
||||||
|
->orderBy('catalog_items.id'),
|
||||||
|
FeaturedGroupSource::All => $query->orderBy('catalog_items.id'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function paginateItems(
|
||||||
|
FeaturedGroup $featuredGroup,
|
||||||
|
int $page,
|
||||||
|
): LengthAwarePaginator {
|
||||||
|
$paginator = $this->itemsQuery($featuredGroup)->paginate(
|
||||||
|
perPage: self::ITEMS_PER_PAGE,
|
||||||
|
pageName: 'page',
|
||||||
|
page: $page,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $paginator->withPath(route('catalog.featured-groups.items.index', [
|
||||||
|
'tenant' => $featuredGroup->tenant_code,
|
||||||
|
'featuredGroup' => $featuredGroup->id,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function attachGroup(iterable $items, FeaturedGroup $featuredGroup): void
|
||||||
|
{
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$item->setRelation('featuredGroup', $featuredGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Services;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Enums\FeaturedGroupSource;
|
||||||
|
use App\Domains\Catalog\Enums\GroupLayout;
|
||||||
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
|
use App\Domains\Catalog\Models\Category;
|
||||||
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class OnTicketFeaturedGroupService
|
||||||
|
{
|
||||||
|
/** @return Collection<int, FeaturedGroup> */
|
||||||
|
public function forTenant(Tenant $tenant): Collection
|
||||||
|
{
|
||||||
|
return FeaturedGroup::query()
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where('source_type', FeaturedGroupSource::Category)
|
||||||
|
->whereHas('category', fn ($query) => $query->where('tenant_code', $tenant->codigo))
|
||||||
|
->with('category')
|
||||||
|
->orderBy('group_order')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array{category_name: string, is_featured: bool} $data */
|
||||||
|
public function create(Tenant $tenant, array $data): FeaturedGroup
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($tenant, $data): FeaturedGroup {
|
||||||
|
$category = $tenant->categories()->create([
|
||||||
|
'nombre' => $data['category_name'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$featuredGroup = FeaturedGroup::query()->create([
|
||||||
|
'tenant_code' => $tenant->codigo,
|
||||||
|
'source_type' => FeaturedGroupSource::Category,
|
||||||
|
'category_id' => $category->id,
|
||||||
|
'product_layout' => $this->productLayout($data['is_featured']),
|
||||||
|
'group_layout' => GroupLayout::Paginated,
|
||||||
|
'group_name' => $data['category_name'],
|
||||||
|
'group_order' => $this->nextOrder($tenant),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $featuredGroup->setRelation('category', $category);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array{category_name: string, is_featured: bool} $data */
|
||||||
|
public function update(
|
||||||
|
Tenant $tenant,
|
||||||
|
FeaturedGroup $featuredGroup,
|
||||||
|
array $data,
|
||||||
|
): FeaturedGroup {
|
||||||
|
return DB::transaction(function () use ($tenant, $featuredGroup, $data): FeaturedGroup {
|
||||||
|
$featuredGroup = FeaturedGroup::query()
|
||||||
|
->whereKey($featuredGroup->getKey())
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where('source_type', FeaturedGroupSource::Category)
|
||||||
|
->lockForUpdate()
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$category = Category::query()
|
||||||
|
->whereKey($featuredGroup->category_id)
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->lockForUpdate()
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$category->update(['nombre' => $data['category_name']]);
|
||||||
|
$featuredGroup->update([
|
||||||
|
'group_name' => $data['category_name'],
|
||||||
|
'product_layout' => $this->productLayout($data['is_featured']),
|
||||||
|
'group_layout' => GroupLayout::Paginated,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $featuredGroup->setRelation('category', $category);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function productLayout(bool $isFeatured): ProductLayout
|
||||||
|
{
|
||||||
|
return $isFeatured ? ProductLayout::Row : ProductLayout::ColumnWithCart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nextOrder(Tenant $tenant): int
|
||||||
|
{
|
||||||
|
$maximumOrder = FeaturedGroup::query()
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->max('group_order');
|
||||||
|
|
||||||
|
return $maximumOrder === null ? 0 : ((int) $maximumOrder) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/Domains/Catalog/routes/adminapp.php
Normal file
15
app/Domains/Catalog/routes/adminapp.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Controllers\AdminApp\OnTicketFeaturedGroupController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::prefix('v1/adminapp/tenant')
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
|
->group(function (): void {
|
||||||
|
Route::get('featured-groups', [OnTicketFeaturedGroupController::class, 'index'])
|
||||||
|
->name('adminapp.featured-groups.index');
|
||||||
|
Route::post('featured-groups', [OnTicketFeaturedGroupController::class, 'store'])
|
||||||
|
->name('adminapp.featured-groups.store');
|
||||||
|
Route::put('featured-groups/{featuredGroup}', [OnTicketFeaturedGroupController::class, 'update'])
|
||||||
|
->name('adminapp.featured-groups.update');
|
||||||
|
});
|
||||||
@@ -14,3 +14,5 @@ Route::prefix('tenants/{tenant:codigo}')->group(function (): void {
|
|||||||
Route::get('catalog-items/{catalogItem}', [CatalogController::class, 'show']);
|
Route::get('catalog-items/{catalogItem}', [CatalogController::class, 'show']);
|
||||||
Route::post('catalog-items', [CatalogController::class, 'store']);
|
Route::post('catalog-items', [CatalogController::class, 'store']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
require __DIR__.'/adminapp.php';
|
||||||
|
|||||||
31
app/Domains/Event/Controllers/AdminApp/EventController.php
Normal file
31
app/Domains/Event/Controllers/AdminApp/EventController.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Controllers\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Event\Requests\UpdateEventRequest;
|
||||||
|
use App\Domains\Event\Resources\EventResource;
|
||||||
|
use App\Domains\Event\Services\EventService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EventController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected EventService $eventService) {}
|
||||||
|
|
||||||
|
public function show(Request $request): EventResource
|
||||||
|
{
|
||||||
|
return EventResource::make(
|
||||||
|
$this->eventService->activeForTenant($request->user()->tenant()->firstOrFail())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateEventRequest $request): EventResource
|
||||||
|
{
|
||||||
|
return EventResource::make(
|
||||||
|
$this->eventService->updateActiveForTenant(
|
||||||
|
$request->user()->tenant()->firstOrFail(),
|
||||||
|
$request->validated()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Domains/Event/Models/Event.php
Normal file
48
app/Domains/Event/Models/Event.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Models;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
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([
|
||||||
|
'tenant_code',
|
||||||
|
'name',
|
||||||
|
'address',
|
||||||
|
])]
|
||||||
|
class Event extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
/** @return BelongsTo<Tenant, $this> */
|
||||||
|
public function tenant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<EventDate, $this> */
|
||||||
|
public function dates(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(EventDate::class)->orderBy('date')->orderBy('time_start');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<CatalogItem, $this> */
|
||||||
|
public function catalogItems(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CatalogItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<Purchase, $this> */
|
||||||
|
public function purchases(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Purchase::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/Domains/Event/Models/EventDate.php
Normal file
55
app/Domains/Event/Models/EventDate.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Models;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
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;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'event_id',
|
||||||
|
'date',
|
||||||
|
'time_start',
|
||||||
|
'time_end',
|
||||||
|
])]
|
||||||
|
class EventDate extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'event_id' => 'integer',
|
||||||
|
'date' => 'date:Y-m-d',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Event, $this> */
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<Variant, $this> */
|
||||||
|
public function variants(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Variant::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startsAt(): CarbonInterface
|
||||||
|
{
|
||||||
|
return Carbon::parse($this->date->format('Y-m-d').' '.$this->time_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function endsAt(): CarbonInterface
|
||||||
|
{
|
||||||
|
return Carbon::parse($this->date->format('Y-m-d').' '.$this->time_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
app/Domains/Event/Requests/UpdateEventRequest.php
Normal file
60
app/Domains/Event/Requests/UpdateEventRequest.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class UpdateEventRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => ['required', 'string', 'max:255'],
|
||||||
|
'location' => ['required', 'string', 'max:255'],
|
||||||
|
'dates' => ['required', 'array', 'min:1'],
|
||||||
|
'dates.*' => ['required', 'array:date,start_time,end_time'],
|
||||||
|
'dates.*.date' => ['required', 'date_format:Y-m-d', 'distinct'],
|
||||||
|
'dates.*.start_time' => ['required', 'date_format:H:i'],
|
||||||
|
'dates.*.end_time' => ['required', 'date_format:H:i'],
|
||||||
|
'social_media' => ['sometimes', 'array'],
|
||||||
|
'social_media.*' => ['required', 'array:code,url,orden'],
|
||||||
|
'social_media.*.code' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'distinct',
|
||||||
|
Rule::exists('social_media', 'code'),
|
||||||
|
],
|
||||||
|
'social_media.*.url' => ['required', 'url', 'max:2048'],
|
||||||
|
'social_media.*.orden' => ['sometimes', 'integer', 'min:0', 'distinct'],
|
||||||
|
'contact' => ['sometimes', 'array:whatsapp_url,instagram_url,facebook_url'],
|
||||||
|
'contact.whatsapp_url' => ['nullable', 'url', 'max:2048'],
|
||||||
|
'contact.instagram_url' => ['nullable', 'url', 'max:2048'],
|
||||||
|
'contact.facebook_url' => ['nullable', 'url', 'max:2048'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, callable> */
|
||||||
|
public function after(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
function (Validator $validator): void {
|
||||||
|
$input = $this->all();
|
||||||
|
|
||||||
|
if (! array_key_exists('social_media', $input) && ! array_key_exists('contact', $input)) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
'social_media',
|
||||||
|
'The social media field is required.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/Domains/Event/Resources/EventResource.php
Normal file
39
app/Domains/Event/Resources/EventResource.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Event\Models\Event;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin Event */
|
||||||
|
class EventResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
$socialMedia = $this->tenant->socialMedia->keyBy('code');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'title' => $this->name,
|
||||||
|
'location' => $this->address,
|
||||||
|
'dates' => $this->dates->map(fn ($eventDate): array => [
|
||||||
|
'id' => $eventDate->id,
|
||||||
|
'date' => $eventDate->date->format('Y-m-d'),
|
||||||
|
'start_time' => substr($eventDate->time_start, 0, 5),
|
||||||
|
'end_time' => substr($eventDate->time_end, 0, 5),
|
||||||
|
])->values(),
|
||||||
|
'social_media' => $this->tenant->socialMedia->map(fn ($item): array => [
|
||||||
|
'code' => $item->code,
|
||||||
|
'url' => $item->pivot->url,
|
||||||
|
'orden' => $item->pivot->orden,
|
||||||
|
])->values(),
|
||||||
|
'contact' => [
|
||||||
|
'whatsapp_url' => $socialMedia->get('whatsapp')?->pivot->url,
|
||||||
|
'instagram_url' => $socialMedia->get('instagram')?->pivot->url,
|
||||||
|
'facebook_url' => $socialMedia->get('facebook')?->pivot->url,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
120
app/Domains/Event/Services/EventService.php
Normal file
120
app/Domains/Event/Services/EventService.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Services;
|
||||||
|
|
||||||
|
use App\Domains\Event\Models\Event;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class EventService
|
||||||
|
{
|
||||||
|
private const CONTACT_CODES = [
|
||||||
|
'whatsapp_url' => 'whatsapp',
|
||||||
|
'instagram_url' => 'instagram',
|
||||||
|
'facebook_url' => 'facebook',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function activeForTenant(Tenant $tenant): Event
|
||||||
|
{
|
||||||
|
return $tenant->events()
|
||||||
|
->whereKey($tenant->active_event_id)
|
||||||
|
->with(['dates', 'tenant.socialMedia'])
|
||||||
|
->firstOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $data */
|
||||||
|
public function updateActiveForTenant(Tenant $tenant, array $data): Event
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($tenant, $data): Event {
|
||||||
|
$tenant = Tenant::query()->whereKey($tenant->getKey())->lockForUpdate()->firstOrFail();
|
||||||
|
$event = $tenant->active_event_id === null
|
||||||
|
? $tenant->events()->create([
|
||||||
|
'name' => $data['title'],
|
||||||
|
'address' => $data['location'],
|
||||||
|
])
|
||||||
|
: $tenant->events()
|
||||||
|
->whereKey($tenant->active_event_id)
|
||||||
|
->lockForUpdate()
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$event->update([
|
||||||
|
'name' => $data['title'],
|
||||||
|
'address' => $data['location'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($tenant->active_event_id === null) {
|
||||||
|
$tenant->update(['active_event_id' => $event->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->syncDates($event, $data['dates']);
|
||||||
|
if (array_key_exists('social_media', $data)) {
|
||||||
|
$this->syncSocialMedia($tenant, $data['social_media']);
|
||||||
|
} else {
|
||||||
|
$this->syncLegacyContact($tenant, $data['contact']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $event->load(['dates', 'tenant.socialMedia']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, array{date: string, start_time: string, end_time: string}> $dates */
|
||||||
|
private function syncDates(Event $event, array $dates): void
|
||||||
|
{
|
||||||
|
$existingDates = $event->dates()->get()->values();
|
||||||
|
|
||||||
|
foreach (array_values($dates) as $index => $date) {
|
||||||
|
$attributes = [
|
||||||
|
'date' => $date['date'],
|
||||||
|
'time_start' => $date['start_time'],
|
||||||
|
'time_end' => $date['end_time'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$existingDate = $existingDates->get($index);
|
||||||
|
|
||||||
|
if ($existingDate) {
|
||||||
|
$existingDate->update($attributes);
|
||||||
|
} else {
|
||||||
|
$event->dates()->create($attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingDates->slice(count($dates))->each->delete();
|
||||||
|
$event->unsetRelation('dates');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string|null> $contact */
|
||||||
|
private function syncLegacyContact(Tenant $tenant, array $contact): void
|
||||||
|
{
|
||||||
|
foreach (self::CONTACT_CODES as $field => $code) {
|
||||||
|
$url = $contact[$field] ?? null;
|
||||||
|
|
||||||
|
if ($url === null || $url === '') {
|
||||||
|
$tenant->socialMedia()->detach($code);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->socialMedia()->syncWithoutDetaching([
|
||||||
|
$code => ['url' => $url],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->unsetRelation('socialMedia');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, array{code: string, url: string, orden?: int}> $socialMedia */
|
||||||
|
private function syncSocialMedia(Tenant $tenant, array $socialMedia): void
|
||||||
|
{
|
||||||
|
$associations = [];
|
||||||
|
|
||||||
|
foreach (array_values($socialMedia) as $index => $item) {
|
||||||
|
$associations[$item['code']] = [
|
||||||
|
'url' => $item['url'],
|
||||||
|
'orden' => $item['orden'] ?? $index,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->socialMedia()->sync($associations);
|
||||||
|
$tenant->unsetRelation('socialMedia');
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/Domains/Event/routes/adminapp.php
Normal file
11
app/Domains/Event/routes/adminapp.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Event\Controllers\AdminApp\EventController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::prefix('v1/adminapp/tenant')
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
|
->group(function (): void {
|
||||||
|
Route::get('event', [EventController::class, 'show']);
|
||||||
|
Route::put('event', [EventController::class, 'update']);
|
||||||
|
});
|
||||||
3
app/Domains/Event/routes/api.php
Normal file
3
app/Domains/Event/routes/api.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/adminapp.php';
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Controllers\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Forms\Resources\EventFormResource;
|
||||||
|
use App\Domains\Forms\Services\EventFormService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EventFormController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected EventFormService $eventFormService) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): EventFormResource
|
||||||
|
{
|
||||||
|
return EventFormResource::make(
|
||||||
|
$this->eventFormService->get(
|
||||||
|
$request->user('sanctum')->tenant()->firstOrFail()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Controllers\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Forms\Resources\SaleFormResource;
|
||||||
|
use App\Domains\Forms\Services\SaleFormService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class SaleFormController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected SaleFormService $saleFormService) {}
|
||||||
|
|
||||||
|
public function __invoke(): SaleFormResource
|
||||||
|
{
|
||||||
|
return SaleFormResource::make($this->saleFormService->get());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Controllers\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Forms\Resources\StaffFormResource;
|
||||||
|
use App\Domains\Forms\Services\StaffFormService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class StaffFormController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected StaffFormService $staffFormService) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): StaffFormResource
|
||||||
|
{
|
||||||
|
return StaffFormResource::make(
|
||||||
|
$this->staffFormService->get(
|
||||||
|
$request->user('sanctum')->tenant()->firstOrFail()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Domains/Forms/Resources/EventFormResource.php
Normal file
19
app/Domains/Forms/Resources/EventFormResource.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class EventFormResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'social_media' => SocialMediaOptionResource::collection(
|
||||||
|
$this->resource['social_media']
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/Domains/Forms/Resources/SaleFormResource.php
Normal file
17
app/Domains/Forms/Resources/SaleFormResource.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class SaleFormResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'statuses' => $this->resource['statuses'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Domains/Forms/Resources/SocialMediaOptionResource.php
Normal file
22
app/Domains/Forms/Resources/SocialMediaOptionResource.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Tenant\Models\SocialMedia;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin SocialMedia */
|
||||||
|
class SocialMediaOptionResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => $this->code,
|
||||||
|
'name' => $this->name,
|
||||||
|
'icon' => $this->icon,
|
||||||
|
'url' => $this->url,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/Domains/Forms/Resources/StaffFormResource.php
Normal file
20
app/Domains/Forms/Resources/StaffFormResource.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class StaffFormResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'categories' => $this->resource['categories']->map(fn ($category) => [
|
||||||
|
'id' => $category->id,
|
||||||
|
'nombre' => $category->nombre,
|
||||||
|
])->values(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Domains/Forms/Services/EventFormService.php
Normal file
27
app/Domains/Forms/Services/EventFormService.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Services;
|
||||||
|
|
||||||
|
use App\Domains\Tenant\Models\SocialMedia;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
|
class EventFormService
|
||||||
|
{
|
||||||
|
/** @return array{social_media: Collection<int, SocialMedia>} */
|
||||||
|
public function get(Tenant $tenant): array
|
||||||
|
{
|
||||||
|
$urls = $tenant->socialMedia()
|
||||||
|
->pluck('tenant_social_media.url', 'social_media.code');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'social_media' => SocialMedia::query()
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->each(fn (SocialMedia $item) => $item->setAttribute(
|
||||||
|
'url',
|
||||||
|
$urls->get($item->code)
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Domains/Forms/Services/SaleFormService.php
Normal file
31
app/Domains/Forms/Services/SaleFormService.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Services;
|
||||||
|
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
|
||||||
|
class SaleFormService
|
||||||
|
{
|
||||||
|
/** @return array{statuses: list<array{code: string, name: string}>} */
|
||||||
|
public function get(): array
|
||||||
|
{
|
||||||
|
$names = [
|
||||||
|
Purchase::STATUS_CREATED => 'Creada',
|
||||||
|
Purchase::STATUS_PENDING_PAYMENT => 'Esperando pago',
|
||||||
|
Purchase::STATUS_PAID => 'Confirmada',
|
||||||
|
Purchase::STATUS_CANCELLED => 'Cancelada',
|
||||||
|
Purchase::STATUS_REJECTED => 'Rechazada',
|
||||||
|
Purchase::STATUS_EXPIRED => 'Vencida',
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'statuses' => array_map(
|
||||||
|
fn (string $status): array => [
|
||||||
|
'code' => $status,
|
||||||
|
'name' => $names[$status],
|
||||||
|
],
|
||||||
|
Purchase::statuses(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Domains/Forms/Services/StaffFormService.php
Normal file
27
app/Domains/Forms/Services/StaffFormService.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Forms\Services;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Models\Category;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
|
class StaffFormService
|
||||||
|
{
|
||||||
|
/** @return array{categories: Collection<int, Category>} */
|
||||||
|
public function get(Tenant $tenant): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'categories' => Category::query()
|
||||||
|
->whereNull('categoria_id')
|
||||||
|
->where(function (Builder $query) use ($tenant): void {
|
||||||
|
$query->where('tenant_code', $tenant->codigo)
|
||||||
|
->orWhereHas('catalogItems', fn (Builder $items) => $items
|
||||||
|
->where('tenant_code', $tenant->codigo));
|
||||||
|
})
|
||||||
|
->orderBy('nombre')
|
||||||
|
->get(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/Domains/Forms/routes/adminapp.php
Normal file
14
app/Domains/Forms/routes/adminapp.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Forms\Controllers\AdminApp\EventFormController;
|
||||||
|
use App\Domains\Forms\Controllers\AdminApp\SaleFormController;
|
||||||
|
use App\Domains\Forms\Controllers\AdminApp\StaffFormController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::prefix('v1/adminapp/forms')
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
|
->group(function (): void {
|
||||||
|
Route::get('event', EventFormController::class);
|
||||||
|
Route::get('sale', SaleFormController::class);
|
||||||
|
Route::get('staff', StaffFormController::class);
|
||||||
|
});
|
||||||
3
app/Domains/Forms/routes/api.php
Normal file
3
app/Domains/Forms/routes/api.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/adminapp.php';
|
||||||
@@ -57,13 +57,9 @@ class TelepagosWebhookService
|
|||||||
->whereIn('status', [
|
->whereIn('status', [
|
||||||
Purchase::STATUS_CREATED,
|
Purchase::STATUS_CREATED,
|
||||||
Purchase::STATUS_PENDING_PAYMENT,
|
Purchase::STATUS_PENDING_PAYMENT,
|
||||||
Purchase::STATUS_IN_REVIEW,
|
|
||||||
])
|
])
|
||||||
->where('payment_method', 'transfer')
|
->where('payment_method', 'transfer')
|
||||||
->where('total', $amount)
|
->where('total', $amount)
|
||||||
->orderByRaw('CASE WHEN status = ? THEN 0 ELSE 1 END', [
|
|
||||||
Purchase::STATUS_IN_REVIEW,
|
|
||||||
])
|
|
||||||
->latest()
|
->latest()
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -97,7 +93,6 @@ class TelepagosWebhookService
|
|||||||
|
|
||||||
if (! in_array($compra->status, [
|
if (! in_array($compra->status, [
|
||||||
Purchase::STATUS_PENDING_PAYMENT,
|
Purchase::STATUS_PENDING_PAYMENT,
|
||||||
Purchase::STATUS_IN_REVIEW,
|
|
||||||
], true)) {
|
], true)) {
|
||||||
Log::warning("Telepagos webhook: Purchase {$compra->id} is not awaiting payment confirmation");
|
Log::warning("Telepagos webhook: Purchase {$compra->id} is not awaiting payment confirmation");
|
||||||
|
|
||||||
|
|||||||
9
app/Domains/Logging/Enums/ValueChangeActorType.php
Normal file
9
app/Domains/Logging/Enums/ValueChangeActorType.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Logging\Enums;
|
||||||
|
|
||||||
|
enum ValueChangeActorType: string
|
||||||
|
{
|
||||||
|
case User = 'user';
|
||||||
|
case System = 'system';
|
||||||
|
}
|
||||||
65
app/Domains/Logging/Models/Concerns/LogsValueChanges.php
Normal file
65
app/Domains/Logging/Models/Concerns/LogsValueChanges.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Logging\Models\Concerns;
|
||||||
|
|
||||||
|
use App\Domains\Logging\Enums\ValueChangeActorType;
|
||||||
|
use App\Domains\Logging\Models\ValueChange;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use LogicException;
|
||||||
|
|
||||||
|
trait LogsValueChanges
|
||||||
|
{
|
||||||
|
abstract protected function valueChangeTenantCode(): string;
|
||||||
|
|
||||||
|
public static function bootLogsValueChanges(): void
|
||||||
|
{
|
||||||
|
static::updated(function (Model $model): void {
|
||||||
|
$changedAttributes = array_values(array_intersect(
|
||||||
|
$model->getLoggedAttributes(),
|
||||||
|
array_keys($model->getChanges()),
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($changedAttributes === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = Auth::id();
|
||||||
|
$actorType = $userId === null
|
||||||
|
? ValueChangeActorType::System
|
||||||
|
: ValueChangeActorType::User;
|
||||||
|
|
||||||
|
foreach ($changedAttributes as $attribute) {
|
||||||
|
$model->valueChanges()->create([
|
||||||
|
'tenant_code' => $model->valueChangeTenantCode(),
|
||||||
|
'attribute' => $attribute,
|
||||||
|
'old_value' => $model->getRawOriginal($attribute),
|
||||||
|
'new_value' => $model->getAttributes()[$attribute] ?? null,
|
||||||
|
'changed_at' => now(),
|
||||||
|
'actor_type' => $actorType,
|
||||||
|
'user_id' => $userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, string> */
|
||||||
|
public function getLoggedAttributes(): array
|
||||||
|
{
|
||||||
|
if (! property_exists($this, 'loggedAttributes')) {
|
||||||
|
throw new LogicException(sprintf(
|
||||||
|
'The [%s] model must define a $loggedAttributes property.',
|
||||||
|
static::class,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($this->loggedAttributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return MorphMany<ValueChange, $this> */
|
||||||
|
public function valueChanges(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(ValueChange::class, 'trackable');
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/Domains/Logging/Models/ValueChange.php
Normal file
55
app/Domains/Logging/Models/ValueChange.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Logging\Models;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Logging\Enums\ValueChangeActorType;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'tenant_code',
|
||||||
|
'trackable_type',
|
||||||
|
'trackable_id',
|
||||||
|
'attribute',
|
||||||
|
'old_value',
|
||||||
|
'new_value',
|
||||||
|
'changed_at',
|
||||||
|
'actor_type',
|
||||||
|
'user_id',
|
||||||
|
])]
|
||||||
|
class ValueChange extends Model
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
/** @return MorphTo<Model, $this> */
|
||||||
|
public function trackable(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<User, $this> */
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Tenant, $this> */
|
||||||
|
public function tenant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'trackable_id' => 'integer',
|
||||||
|
'changed_at' => 'datetime',
|
||||||
|
'actor_type' => ValueChangeActorType::class,
|
||||||
|
'user_id' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ 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;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
@@ -124,14 +125,28 @@ class PurchaseController extends Controller
|
|||||||
$purchaseUpdate['transfer_payer_dni'] = preg_replace('/\D+/', '', (string) $request->validated('transfer_payer_dni'));
|
$purchaseUpdate['transfer_payer_dni'] = preg_replace('/\D+/', '', (string) $request->validated('transfer_payer_dni'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$updated = Purchase::query()
|
$updated = DB::transaction(function () use ($compra, $purchaseUpdate): bool {
|
||||||
->whereKey($compra->getKey())
|
/** @var Purchase|null $purchase */
|
||||||
->whereIn('status', [Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT])
|
$purchase = Purchase::query()
|
||||||
->where(function ($query): void {
|
->whereKey($compra->getKey())
|
||||||
$query->whereNull('expires_at')
|
->lockForUpdate()
|
||||||
->orWhere('expires_at', '>', now());
|
->first();
|
||||||
})
|
|
||||||
->update($purchaseUpdate);
|
if (
|
||||||
|
$purchase === null
|
||||||
|
|| ! in_array($purchase->status, [
|
||||||
|
Purchase::STATUS_CREATED,
|
||||||
|
Purchase::STATUS_PENDING_PAYMENT,
|
||||||
|
], true)
|
||||||
|
|| ($purchase->expires_at !== null && $purchase->expires_at->isPast())
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$purchase->update($purchaseUpdate);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
if ($updated === 0) {
|
if ($updated === 0) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Domains\Purchase\Models;
|
|||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
use App\Domains\Auth\Models\User;
|
||||||
use App\Domains\Cart\Models\Cart;
|
use App\Domains\Cart\Models\Cart;
|
||||||
|
use App\Domains\Event\Models\Event;
|
||||||
|
use App\Domains\Logging\Models\Concerns\LogsValueChanges;
|
||||||
use App\Domains\Purchase\Events\PurchasePaid;
|
use App\Domains\Purchase\Events\PurchasePaid;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
@@ -18,6 +20,7 @@ use Illuminate\Support\Facades\DB;
|
|||||||
#[Fillable([
|
#[Fillable([
|
||||||
'cart_id',
|
'cart_id',
|
||||||
'tenant_codigo',
|
'tenant_codigo',
|
||||||
|
'event_id',
|
||||||
'user_id',
|
'user_id',
|
||||||
'status',
|
'status',
|
||||||
'payment_method',
|
'payment_method',
|
||||||
@@ -31,14 +34,12 @@ use Illuminate\Support\Facades\DB;
|
|||||||
])]
|
])]
|
||||||
class Purchase extends Model
|
class Purchase extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory, LogsValueChanges;
|
||||||
|
|
||||||
public const STATUS_CREATED = 'created';
|
public const STATUS_CREATED = 'created';
|
||||||
|
|
||||||
public const STATUS_PENDING_PAYMENT = 'pending_payment';
|
public const STATUS_PENDING_PAYMENT = 'pending_payment';
|
||||||
|
|
||||||
public const STATUS_IN_REVIEW = 'in_review';
|
|
||||||
|
|
||||||
public const STATUS_PAID = 'paid';
|
public const STATUS_PAID = 'paid';
|
||||||
|
|
||||||
public const STATUS_CANCELLED = 'cancelled';
|
public const STATUS_CANCELLED = 'cancelled';
|
||||||
@@ -47,12 +48,31 @@ class Purchase extends Model
|
|||||||
|
|
||||||
public const STATUS_EXPIRED = 'expired';
|
public const STATUS_EXPIRED = 'expired';
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
public static function statuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STATUS_CREATED,
|
||||||
|
self::STATUS_PENDING_PAYMENT,
|
||||||
|
self::STATUS_PAID,
|
||||||
|
self::STATUS_CANCELLED,
|
||||||
|
self::STATUS_REJECTED,
|
||||||
|
self::STATUS_EXPIRED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected $table = 'compras';
|
protected $table = 'compras';
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected array $loggedAttributes = [
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'cart_id' => 'integer',
|
'cart_id' => 'integer',
|
||||||
|
'event_id' => 'integer',
|
||||||
'user_id' => 'integer',
|
'user_id' => 'integer',
|
||||||
'expires_at' => 'datetime',
|
'expires_at' => 'datetime',
|
||||||
'total' => 'decimal:2',
|
'total' => 'decimal:2',
|
||||||
@@ -67,6 +87,12 @@ class Purchase extends Model
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Event, $this> */
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<User, $this>
|
* @return BelongsTo<User, $this>
|
||||||
*/
|
*/
|
||||||
@@ -133,6 +159,11 @@ class Purchase extends Model
|
|||||||
return (float) $this->items()->sum('total');
|
return (float) $this->items()->sum('total');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function valueChangeTenantCode(): string
|
||||||
|
{
|
||||||
|
return $this->tenant_codigo;
|
||||||
|
}
|
||||||
|
|
||||||
public function markAsPendingPayment(): void
|
public function markAsPendingPayment(): void
|
||||||
{
|
{
|
||||||
$this->update([
|
$this->update([
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class PurchaseResource extends JsonResource
|
|||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'cart_id' => $this->cart_id,
|
'cart_id' => $this->cart_id,
|
||||||
'tenant_codigo' => $this->tenant_codigo,
|
'tenant_codigo' => $this->tenant_codigo,
|
||||||
|
'event_id' => $this->event_id,
|
||||||
'user_id' => $this->user_id,
|
'user_id' => $this->user_id,
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ class CheckoutService
|
|||||||
public function startCheckout(Tenant $tenant, int $userId, array $purchaseData): Purchase
|
public function startCheckout(Tenant $tenant, int $userId, array $purchaseData): Purchase
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($tenant, $userId, $purchaseData): Purchase {
|
return DB::transaction(function () use ($tenant, $userId, $purchaseData): Purchase {
|
||||||
|
/** @var Tenant $tenant */
|
||||||
|
$tenant = Tenant::query()
|
||||||
|
->lockForUpdate()
|
||||||
|
->findOrFail($tenant->getKey());
|
||||||
|
|
||||||
$directItem = $purchaseData['direct_item'] ?? null;
|
$directItem = $purchaseData['direct_item'] ?? null;
|
||||||
$cartId = isset($purchaseData['cart_id']) ? (int) $purchaseData['cart_id'] : null;
|
$cartId = isset($purchaseData['cart_id']) ? (int) $purchaseData['cart_id'] : null;
|
||||||
unset($purchaseData['direct_item'], $purchaseData['cart_id']);
|
unset($purchaseData['direct_item'], $purchaseData['cart_id']);
|
||||||
@@ -70,7 +75,6 @@ class CheckoutService
|
|||||||
|
|
||||||
if (in_array($purchase->status, [
|
if (in_array($purchase->status, [
|
||||||
Purchase::STATUS_PAID,
|
Purchase::STATUS_PAID,
|
||||||
Purchase::STATUS_IN_REVIEW,
|
|
||||||
Purchase::STATUS_CANCELLED,
|
Purchase::STATUS_CANCELLED,
|
||||||
Purchase::STATUS_REJECTED,
|
Purchase::STATUS_REJECTED,
|
||||||
Purchase::STATUS_EXPIRED,
|
Purchase::STATUS_EXPIRED,
|
||||||
@@ -96,7 +100,6 @@ class CheckoutService
|
|||||||
->findOrFail($purchase->getKey());
|
->findOrFail($purchase->getKey());
|
||||||
|
|
||||||
if (in_array($purchase->status, [
|
if (in_array($purchase->status, [
|
||||||
Purchase::STATUS_IN_REVIEW,
|
|
||||||
Purchase::STATUS_PAID,
|
Purchase::STATUS_PAID,
|
||||||
], true)) {
|
], true)) {
|
||||||
return $this->loadPurchase($purchase);
|
return $this->loadPurchase($purchase);
|
||||||
@@ -112,7 +115,6 @@ class CheckoutService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$purchase->update([
|
$purchase->update([
|
||||||
'status' => Purchase::STATUS_IN_REVIEW,
|
|
||||||
'expires_at' => null,
|
'expires_at' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -618,6 +620,7 @@ class CheckoutService
|
|||||||
...$purchaseData,
|
...$purchaseData,
|
||||||
'cart_id' => $cartId,
|
'cart_id' => $cartId,
|
||||||
'tenant_codigo' => $tenant->codigo,
|
'tenant_codigo' => $tenant->codigo,
|
||||||
|
'event_id' => $tenant->active_event_id,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'status' => Purchase::STATUS_CREATED,
|
'status' => Purchase::STATUS_CREATED,
|
||||||
'payment_method' => null,
|
'payment_method' => null,
|
||||||
|
|||||||
61
app/Domains/Sale/Controllers/AdminApp/SaleController.php
Normal file
61
app/Domains/Sale/Controllers/AdminApp/SaleController.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Controllers\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Sale\Requests\AdminAppSaleIndexRequest;
|
||||||
|
use App\Domains\Sale\Resources\AdminApp\SaleModificationResource;
|
||||||
|
use App\Domains\Sale\Resources\AdminApp\SaleResource;
|
||||||
|
use App\Domains\Sale\Services\AdminAppSalePdfService;
|
||||||
|
use App\Domains\Sale\Services\AdminAppSaleService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
class SaleController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AdminAppSaleService $saleService,
|
||||||
|
protected AdminAppSalePdfService $salePdfService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function index(AdminAppSaleIndexRequest $request): AnonymousResourceCollection
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return SaleResource::collection(
|
||||||
|
$this->saleService->sales($tenant, $request->validated())
|
||||||
|
)->additional([
|
||||||
|
'confirmed_sales_total' => $this->saleService->confirmedSalesTotal($tenant),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function modifications(Request $request): AnonymousResourceCollection
|
||||||
|
{
|
||||||
|
return SaleModificationResource::collection(
|
||||||
|
$this->saleService->modifications(
|
||||||
|
$request->user()->tenant()->firstOrFail()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadPdf(AdminAppSaleIndexRequest $request): Response
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return $this->salePdfService->downloadSales(
|
||||||
|
$tenant,
|
||||||
|
$this->saleService->salesForExport($tenant, $request->validated()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadModificationsPdf(Request $request): Response
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return $this->salePdfService->downloadModifications(
|
||||||
|
$tenant,
|
||||||
|
$this->saleService->modificationsForExport($tenant),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Domains/Sale/Requests/AdminAppSaleIndexRequest.php
Normal file
30
app/Domains/Sale/Requests/AdminAppSaleIndexRequest.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Requests;
|
||||||
|
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class AdminAppSaleIndexRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, list<string>> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'q' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||||
|
'id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
|
'sale_date' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||||
|
'status' => ['sometimes', 'nullable', 'string', Rule::in(Purchase::statuses())],
|
||||||
|
'sort_by' => ['sometimes', 'string', 'in:id,date,customer_name,quantity,status,total'],
|
||||||
|
'sort_direction' => ['sometimes', 'string', 'in:asc,desc'],
|
||||||
|
'page' => ['sometimes', 'integer', 'min:1'],
|
||||||
|
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Resources\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Logging\Models\ValueChange;
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin ValueChange */
|
||||||
|
class SaleModificationResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
/** @var Purchase|null $sale */
|
||||||
|
$sale = $this->whenLoaded('trackable');
|
||||||
|
$user = $this->whenLoaded('user');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'sale_id' => $this->trackable_id,
|
||||||
|
'attribute' => $this->attribute,
|
||||||
|
'old_value' => $this->old_value,
|
||||||
|
'new_value' => $this->new_value,
|
||||||
|
'date' => $this->changed_at->format('Y-m-d'),
|
||||||
|
'time' => $this->changed_at->format('H:i:s'),
|
||||||
|
'actor_type' => $this->actor_type->value,
|
||||||
|
'sale' => $sale instanceof Purchase ? [
|
||||||
|
'id' => $sale->id,
|
||||||
|
'customer_name' => $sale->nombre_apellido,
|
||||||
|
'status' => $sale->status,
|
||||||
|
] : null,
|
||||||
|
'modified_by' => $user ? [
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => $user->nombre_apellido,
|
||||||
|
'email' => $user->email,
|
||||||
|
] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Domains/Sale/Resources/AdminApp/SaleResource.php
Normal file
28
app/Domains/Sale/Resources/AdminApp/SaleResource.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Resources\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin Purchase */
|
||||||
|
class SaleResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
$ticketsCount = (int) ($this->tickets_count ?? 0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'customer_name' => $this->nombre_apellido,
|
||||||
|
'quantity' => (int) ($this->quantity ?? 0),
|
||||||
|
'status' => $this->status,
|
||||||
|
'total' => number_format((float) $this->total, 2, '.', ''),
|
||||||
|
'tickets_count' => $ticketsCount,
|
||||||
|
'has_generated_tickets' => $ticketsCount > 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
68
app/Domains/Sale/Services/AdminAppSalePdfService.php
Normal file
68
app/Domains/Sale/Services/AdminAppSalePdfService.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Services;
|
||||||
|
|
||||||
|
use App\Domains\Logging\Models\ValueChange;
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Barryvdh\DomPDF\PDF as DomPdf;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class AdminAppSalePdfService
|
||||||
|
{
|
||||||
|
/** @param Collection<int, Purchase> $sales */
|
||||||
|
public function downloadSales(Tenant $tenant, Collection $sales): Response
|
||||||
|
{
|
||||||
|
$pdf = Pdf::loadView('pdf.adminapp.sales', [
|
||||||
|
'tenant' => $tenant,
|
||||||
|
'sales' => $sales,
|
||||||
|
'generatedAt' => now(),
|
||||||
|
'confirmedSalesTotal' => number_format(
|
||||||
|
(float) $sales->where('status', Purchase::STATUS_PAID)->sum('total'),
|
||||||
|
2,
|
||||||
|
'.',
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
])->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
|
$this->addPageNumbers($pdf);
|
||||||
|
|
||||||
|
return $pdf->download(
|
||||||
|
'ventas_'.$tenant->codigo.'_'.now()->format('Ymd_His').'.pdf'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Collection<int, ValueChange> $modifications */
|
||||||
|
public function downloadModifications(Tenant $tenant, Collection $modifications): Response
|
||||||
|
{
|
||||||
|
$pdf = Pdf::loadView('pdf.adminapp.sale-modifications', [
|
||||||
|
'tenant' => $tenant,
|
||||||
|
'modifications' => $modifications,
|
||||||
|
'generatedAt' => now(),
|
||||||
|
])->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
|
$this->addPageNumbers($pdf);
|
||||||
|
|
||||||
|
return $pdf->download(
|
||||||
|
'historial_modificaciones_'.$tenant->codigo.'_'.now()->format('Ymd_His').'.pdf'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addPageNumbers(DomPdf $pdf): void
|
||||||
|
{
|
||||||
|
$pdf->render();
|
||||||
|
$domPdf = $pdf->getDomPDF();
|
||||||
|
$font = $domPdf->getFontMetrics()->getFont('DejaVu Sans');
|
||||||
|
|
||||||
|
$domPdf->getCanvas()->page_text(
|
||||||
|
385,
|
||||||
|
575,
|
||||||
|
'Página {PAGE_NUM} de {PAGE_COUNT}',
|
||||||
|
$font,
|
||||||
|
7,
|
||||||
|
[0.48, 0.52, 0.49],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
120
app/Domains/Sale/Services/AdminAppSaleService.php
Normal file
120
app/Domains/Sale/Services/AdminAppSaleService.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Services;
|
||||||
|
|
||||||
|
use App\Domains\Logging\Models\ValueChange;
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class AdminAppSaleService
|
||||||
|
{
|
||||||
|
public function confirmedSalesTotal(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
$total = Purchase::query()
|
||||||
|
->where('tenant_codigo', $tenant->codigo)
|
||||||
|
->where('status', Purchase::STATUS_PAID)
|
||||||
|
->sum('total');
|
||||||
|
|
||||||
|
return number_format((float) $total, 2, '.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{
|
||||||
|
* q?: string|null,
|
||||||
|
* id?: int|null,
|
||||||
|
* sale_date?: string|null,
|
||||||
|
* status?: string|null,
|
||||||
|
* sort_by?: string,
|
||||||
|
* sort_direction?: string
|
||||||
|
* } $filters
|
||||||
|
* @return LengthAwarePaginator<Purchase>
|
||||||
|
*/
|
||||||
|
public function sales(Tenant $tenant, array $filters = []): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
return $this->salesQuery($tenant, $filters)
|
||||||
|
->paginateFromRequest()
|
||||||
|
->withQueryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $filters
|
||||||
|
* @return Collection<int, Purchase>
|
||||||
|
*/
|
||||||
|
public function salesForExport(Tenant $tenant, array $filters = []): Collection
|
||||||
|
{
|
||||||
|
return $this->salesQuery($tenant, $filters)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return LengthAwarePaginator<ValueChange> */
|
||||||
|
public function modifications(Tenant $tenant): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
return $this->modificationsQuery($tenant)
|
||||||
|
->paginateFromRequest()
|
||||||
|
->withQueryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, ValueChange> */
|
||||||
|
public function modificationsForExport(Tenant $tenant): Collection
|
||||||
|
{
|
||||||
|
return $this->modificationsQuery($tenant)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $filters */
|
||||||
|
protected function salesQuery(Tenant $tenant, array $filters): Builder
|
||||||
|
{
|
||||||
|
$sortColumns = [
|
||||||
|
'id' => 'id',
|
||||||
|
'date' => 'created_at',
|
||||||
|
'customer_name' => 'nombre_apellido',
|
||||||
|
'quantity' => 'quantity',
|
||||||
|
'status' => 'status',
|
||||||
|
'total' => 'total',
|
||||||
|
];
|
||||||
|
$requestedSort = $filters['sort_by'] ?? 'date';
|
||||||
|
$sortBy = array_key_exists($requestedSort, $sortColumns) ? $requestedSort : 'date';
|
||||||
|
$requestedDirection = $filters['sort_direction'] ?? 'desc';
|
||||||
|
$sortDirection = in_array($requestedDirection, ['asc', 'desc'], true)
|
||||||
|
? $requestedDirection
|
||||||
|
: 'desc';
|
||||||
|
|
||||||
|
return Purchase::query()
|
||||||
|
->where('tenant_codigo', $tenant->codigo)
|
||||||
|
->when($filters['q'] ?? null, function (Builder $query, string $search): void {
|
||||||
|
$term = trim($search);
|
||||||
|
|
||||||
|
$query->where(function (Builder $query) use ($term): void {
|
||||||
|
$query
|
||||||
|
->where('id', 'like', "%{$term}%")
|
||||||
|
->orWhere('nombre_apellido', 'like', "%{$term}%")
|
||||||
|
->orWhere('created_at', 'like', "%{$term}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($filters['id'] ?? null, fn (Builder $query, int $id): Builder => $query->whereKey($id))
|
||||||
|
->when(
|
||||||
|
$filters['sale_date'] ?? null,
|
||||||
|
fn (Builder $query, string $date): Builder => $query->whereDate('created_at', $date)
|
||||||
|
)
|
||||||
|
->when(
|
||||||
|
$filters['status'] ?? null,
|
||||||
|
fn (Builder $query, string $status): Builder => $query->where('status', $status)
|
||||||
|
)
|
||||||
|
->withSum('items as quantity', 'cantidad')
|
||||||
|
->withCount('tickets')
|
||||||
|
->orderBy($sortColumns[$sortBy], $sortDirection)
|
||||||
|
->when($sortBy !== 'id', fn (Builder $query): Builder => $query->orderByDesc('id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Builder<ValueChange> */
|
||||||
|
protected function modificationsQuery(Tenant $tenant): Builder
|
||||||
|
{
|
||||||
|
return ValueChange::query()
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where('trackable_type', (new Purchase)->getMorphClass())
|
||||||
|
->with(['trackable', 'user'])
|
||||||
|
->orderByDesc('changed_at')
|
||||||
|
->orderByDesc('id');
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/Domains/Sale/routes/adminapp.php
Normal file
13
app/Domains/Sale/routes/adminapp.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Sale\Controllers\AdminApp\SaleController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::prefix('v1/adminapp/tenant')
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
|
->group(function (): void {
|
||||||
|
Route::get('sales', [SaleController::class, 'index']);
|
||||||
|
Route::get('sales/pdf', [SaleController::class, 'downloadPdf']);
|
||||||
|
Route::get('sales/modifications', [SaleController::class, 'modifications']);
|
||||||
|
Route::get('sales/modifications/pdf', [SaleController::class, 'downloadModificationsPdf']);
|
||||||
|
});
|
||||||
3
app/Domains/Sale/routes/api.php
Normal file
3
app/Domains/Sale/routes/api.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/adminapp.php';
|
||||||
49
app/Domains/Staff/Controllers/AdminAppStaffController.php
Normal file
49
app/Domains/Staff/Controllers/AdminAppStaffController.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Staff\Controllers;
|
||||||
|
|
||||||
|
use App\Domains\Staff\Requests\StoreStaffRequest;
|
||||||
|
use App\Domains\Staff\Requests\UpdateStaffRequest;
|
||||||
|
use App\Domains\Staff\Resources\StaffResource;
|
||||||
|
use App\Domains\Staff\Services\StaffService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class AdminAppStaffController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private readonly StaffService $staffService) {}
|
||||||
|
|
||||||
|
public function index(Request $request): AnonymousResourceCollection
|
||||||
|
{
|
||||||
|
return StaffResource::collection($this->staffService->list(
|
||||||
|
$request->user()->tenant()->firstOrFail(),
|
||||||
|
$request->string('search')->trim()->toString() ?: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreStaffRequest $request): StaffResource
|
||||||
|
{
|
||||||
|
return StaffResource::make($this->staffService->create(
|
||||||
|
$request->user()->tenant()->firstOrFail(),
|
||||||
|
$request->validated(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateStaffRequest $request, int $staff): StaffResource
|
||||||
|
{
|
||||||
|
return StaffResource::make($this->staffService->update(
|
||||||
|
$request->user()->tenant()->firstOrFail(),
|
||||||
|
$staff,
|
||||||
|
$request->validated(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, int $staff): Response
|
||||||
|
{
|
||||||
|
$this->staffService->delete($request->user()->tenant()->firstOrFail(), $staff);
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Domains/Staff/Requests/StoreStaffRequest.php
Normal file
26
app/Domains/Staff/Requests/StoreStaffRequest.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Staff\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreStaffRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nombre_apellido' => ['required', 'string', 'max:255'],
|
||||||
|
'dni' => ['required', 'string', 'max:50'],
|
||||||
|
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||||
|
'category_ids' => ['required', 'array', 'min:1'],
|
||||||
|
'category_ids.*' => ['required', 'integer', 'distinct', Rule::exists('categorias', 'id')],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Domains/Staff/Requests/UpdateStaffRequest.php
Normal file
33
app/Domains/Staff/Requests/UpdateStaffRequest.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Staff\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class UpdateStaffRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$staffId = (int) $this->route('staff');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nombre_apellido' => ['required', 'string', 'max:255'],
|
||||||
|
'dni' => ['required', 'string', 'max:50'],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('users', 'email')->ignore($staffId),
|
||||||
|
],
|
||||||
|
'category_ids' => ['required', 'array', 'min:1'],
|
||||||
|
'category_ids.*' => ['required', 'integer', 'distinct', Rule::exists('categorias', 'id')],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Domains/Staff/Resources/StaffResource.php
Normal file
32
app/Domains/Staff/Resources/StaffResource.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Staff\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin User */
|
||||||
|
class StaffResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'nombre_apellido' => $this->nombre_apellido,
|
||||||
|
'dni' => $this->dni,
|
||||||
|
'email' => $this->email,
|
||||||
|
'rol_codigo' => $this->rol_codigo,
|
||||||
|
'role' => $this->whenLoaded('role', fn () => [
|
||||||
|
'codigo' => $this->role?->codigo,
|
||||||
|
'nombre' => $this->role?->nombre,
|
||||||
|
]),
|
||||||
|
'categories' => $this->whenLoaded('scanCategories', fn () => $this->scanCategories
|
||||||
|
->map(fn ($category) => [
|
||||||
|
'id' => $category->id,
|
||||||
|
'nombre' => $category->nombre,
|
||||||
|
])->values()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
113
app/Domains/Staff/Services/StaffService.php
Normal file
113
app/Domains/Staff/Services/StaffService.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Staff\Services;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Authorization\Enums\RoleCode;
|
||||||
|
use App\Domains\Catalog\Models\Category;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class StaffService
|
||||||
|
{
|
||||||
|
/** @return Collection<int, User> */
|
||||||
|
public function list(Tenant $tenant, ?string $search = null): Collection
|
||||||
|
{
|
||||||
|
return $this->staffQuery($tenant)
|
||||||
|
->with(['role', 'scanCategories' => fn ($query) => $query->orderBy('nombre')])
|
||||||
|
->when($search, function (Builder $query, string $search): void {
|
||||||
|
$query->where(function (Builder $query) use ($search): void {
|
||||||
|
$query->where('nombre_apellido', 'like', "%{$search}%")
|
||||||
|
->orWhere('dni', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy('nombre_apellido')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, Category> */
|
||||||
|
private function assignableCategories(Tenant $tenant): Collection
|
||||||
|
{
|
||||||
|
return Category::query()
|
||||||
|
->whereNull('categoria_id')
|
||||||
|
->where(function (Builder $query) use ($tenant): void {
|
||||||
|
$query->where('tenant_code', $tenant->codigo)
|
||||||
|
->orWhereHas('catalogItems', fn (Builder $items) => $items
|
||||||
|
->where('tenant_code', $tenant->codigo));
|
||||||
|
})
|
||||||
|
->orderBy('nombre')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $data */
|
||||||
|
public function create(Tenant $tenant, array $data): User
|
||||||
|
{
|
||||||
|
$this->assertCategoriesBelongToTenant($tenant, $data['category_ids']);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($tenant, $data): User {
|
||||||
|
$staff = User::query()->create([
|
||||||
|
...Arr::only($data, ['nombre_apellido', 'dni', 'email']),
|
||||||
|
'email' => mb_strtolower(trim((string) $data['email'])),
|
||||||
|
'password' => Str::random(64),
|
||||||
|
'rol_codigo' => RoleCode::Scanner->value,
|
||||||
|
'tenant_codigo' => $tenant->codigo,
|
||||||
|
]);
|
||||||
|
$staff->scanCategories()->sync($data['category_ids']);
|
||||||
|
|
||||||
|
return $staff->load('role', 'scanCategories');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $data */
|
||||||
|
public function update(Tenant $tenant, int $staffId, array $data): User
|
||||||
|
{
|
||||||
|
$staff = $this->find($tenant, $staffId);
|
||||||
|
$this->assertCategoriesBelongToTenant($tenant, $data['category_ids']);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($staff, $data): User {
|
||||||
|
$attributes = Arr::only($data, ['nombre_apellido', 'dni', 'email']);
|
||||||
|
$attributes['email'] = mb_strtolower(trim((string) $data['email']));
|
||||||
|
$staff->update($attributes);
|
||||||
|
$staff->scanCategories()->sync($data['category_ids']);
|
||||||
|
|
||||||
|
return $staff->load('role', 'scanCategories');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Tenant $tenant, int $staffId): void
|
||||||
|
{
|
||||||
|
$this->find($tenant, $staffId)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(Tenant $tenant, int $staffId): User
|
||||||
|
{
|
||||||
|
return $this->staffQuery($tenant)->findOrFail($staffId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function staffQuery(Tenant $tenant): Builder
|
||||||
|
{
|
||||||
|
return User::query()
|
||||||
|
->where('tenant_codigo', $tenant->codigo)
|
||||||
|
->where('rol_codigo', RoleCode::Scanner->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, int> $categoryIds */
|
||||||
|
private function assertCategoriesBelongToTenant(Tenant $tenant, array $categoryIds): void
|
||||||
|
{
|
||||||
|
$validIds = $this->assignableCategories($tenant)
|
||||||
|
->whereIn('id', $categoryIds)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
if ($validIds->count() !== count($categoryIds)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'category_ids' => 'Una o más categorías no pertenecen al tenant.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/Domains/Staff/routes/api.php
Normal file
10
app/Domains/Staff/routes/api.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Staff\Controllers\AdminAppStaffController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::prefix('v1/adminapp/tenant')
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
|
->group(function (): void {
|
||||||
|
Route::apiResource('staff', AdminAppStaffController::class)->except('show');
|
||||||
|
});
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Controllers\AdminApp;
|
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
|
||||||
use App\Domains\Tenant\Resources\TenantResource;
|
|
||||||
use App\Domains\Tenant\Services\TenantInformationService;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class BootstrapTenantController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected TenantInformationService $tenantInformationService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function __invoke(Request $request): TenantResource
|
|
||||||
{
|
|
||||||
/** @var User $user */
|
|
||||||
$user = $request->user();
|
|
||||||
$tenant = $user->tenant()->firstOrFail();
|
|
||||||
|
|
||||||
return TenantResource::make(
|
|
||||||
$this->tenantInformationService->load($tenant, [
|
|
||||||
'menues' => fn ($query) => $query
|
|
||||||
->where('code', 'like', 'admin.%')
|
|
||||||
->whereHas(
|
|
||||||
'roles',
|
|
||||||
fn ($query) => $query->where('codigo', $user->rol_codigo)
|
|
||||||
),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -65,7 +65,10 @@ class WebsiteExtraController extends Controller
|
|||||||
|
|
||||||
return WebsiteExtrasResource::make(
|
return WebsiteExtrasResource::make(
|
||||||
$this->loadTenant($request->user())
|
$this->loadTenant($request->user())
|
||||||
);
|
)->additional([
|
||||||
|
'code' => 'tenant.website_extra_toggled',
|
||||||
|
'message' => __('api.tenant.website_extra_toggled'),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadTenant(User $user): Tenant
|
private function loadTenant(User $user): Tenant
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Controllers;
|
|
||||||
|
|
||||||
use App\Domains\Authorization\Enums\RoleCode;
|
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
|
||||||
use App\Domains\Tenant\Requests\BootstrapTenantRequest;
|
|
||||||
use App\Domains\Tenant\Resources\TenantResource;
|
|
||||||
use App\Domains\Tenant\Services\TenantInformationService;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
class BootstrapTenantController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected TenantInformationService $tenantInformationService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function __invoke(BootstrapTenantRequest $request): TenantResource
|
|
||||||
{
|
|
||||||
/** @var string $dominio */
|
|
||||||
$dominio = $request->validated('dominio');
|
|
||||||
|
|
||||||
return TenantResource::make(
|
|
||||||
$this->tenantInformationService->load(
|
|
||||||
Tenant::query()
|
|
||||||
->where('dominio', $dominio)
|
|
||||||
->firstOrFail(),
|
|
||||||
[
|
|
||||||
'menues' => fn ($query) => $query->whereHas(
|
|
||||||
'roles',
|
|
||||||
fn ($query) => $query->where('codigo', RoleCode::User->value)
|
|
||||||
),
|
|
||||||
'categories' => fn ($query) => $query->orderBy('nombre'),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ use App\Domains\Catalog\Enums\GroupLayout;
|
|||||||
use App\Domains\Catalog\Enums\ProductLayout;
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\Category;
|
use App\Domains\Catalog\Models\Category;
|
||||||
|
use App\Domains\Event\Models\Event;
|
||||||
use App\Domains\Menu\Models\Menu;
|
use App\Domains\Menu\Models\Menu;
|
||||||
use App\Domains\Menu\Models\TenantMenu;
|
use App\Domains\Menu\Models\TenantMenu;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
@@ -32,6 +33,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
'search_product_layout',
|
'search_product_layout',
|
||||||
'search_group_layout',
|
'search_group_layout',
|
||||||
'search_items_per_page',
|
'search_items_per_page',
|
||||||
|
'active_event_id',
|
||||||
])]
|
])]
|
||||||
class Tenant extends Model
|
class Tenant extends Model
|
||||||
{
|
{
|
||||||
@@ -59,6 +61,7 @@ class Tenant extends Model
|
|||||||
'search_product_layout' => ProductLayout::class,
|
'search_product_layout' => ProductLayout::class,
|
||||||
'search_group_layout' => GroupLayout::class,
|
'search_group_layout' => GroupLayout::class,
|
||||||
'search_items_per_page' => 'integer',
|
'search_items_per_page' => 'integer',
|
||||||
|
'active_event_id' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +94,18 @@ class Tenant extends Model
|
|||||||
return $this->hasMany(CatalogItem::class, 'tenant_code', 'codigo');
|
return $this->hasMany(CatalogItem::class, 'tenant_code', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<Event, $this> */
|
||||||
|
public function events(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Event::class, 'tenant_code', 'codigo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Event, $this> */
|
||||||
|
public function activeEvent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class, 'active_event_id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<Category, $this>
|
* @return HasMany<Category, $this>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,14 +2,30 @@
|
|||||||
|
|
||||||
namespace App\Domains\Tenant\Models;
|
namespace App\Domains\Tenant\Models;
|
||||||
|
|
||||||
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
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\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'codigo',
|
'codigo',
|
||||||
'nombre',
|
'nombre',
|
||||||
|
'dominio',
|
||||||
|
'primary_color',
|
||||||
|
'secondary_color',
|
||||||
|
'danger_color',
|
||||||
|
'success_color',
|
||||||
|
'warning_color',
|
||||||
|
'body_color',
|
||||||
|
'darker_body_color',
|
||||||
|
'surface_color',
|
||||||
|
'background_color',
|
||||||
|
'border_color',
|
||||||
|
'login_header_footer_color',
|
||||||
|
'site_logo',
|
||||||
|
'footer_logo',
|
||||||
])]
|
])]
|
||||||
class WebsiteType extends Model
|
class WebsiteType extends Model
|
||||||
{
|
{
|
||||||
@@ -17,6 +33,22 @@ class WebsiteType extends Model
|
|||||||
|
|
||||||
protected $table = 'website_type';
|
protected $table = 'website_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Attachment, $this>
|
||||||
|
*/
|
||||||
|
public function siteLogo(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Attachment::class, 'site_logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Attachment, $this>
|
||||||
|
*/
|
||||||
|
public function footerLogo(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Attachment::class, 'footer_logo');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<WebsiteTypeExtra, $this>
|
* @return HasMany<WebsiteTypeExtra, $this>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,13 +32,20 @@ class TenantResource extends JsonResource
|
|||||||
'header_bg_color' => $this->header_bg_color,
|
'header_bg_color' => $this->header_bg_color,
|
||||||
'footer_bg_color' => $this->footer_bg_color,
|
'footer_bg_color' => $this->footer_bg_color,
|
||||||
'website_type_code' => $this->website_type_code,
|
'website_type_code' => $this->website_type_code,
|
||||||
'website_type' => $this->whenLoaded(
|
'active_event_id' => $this->active_event_id,
|
||||||
'websiteType',
|
'active_event' => $this->whenLoaded('activeEvent', fn () => $this->activeEvent === null
|
||||||
fn () => $this->websiteType ? [
|
? null
|
||||||
'codigo' => $this->websiteType->codigo,
|
: [
|
||||||
'nombre' => $this->websiteType->nombre,
|
'id' => $this->activeEvent->id,
|
||||||
] : null
|
'name' => $this->activeEvent->name,
|
||||||
),
|
'address' => $this->activeEvent->address,
|
||||||
|
'dates' => $this->activeEvent->dates->map(fn ($eventDate): array => [
|
||||||
|
'id' => $eventDate->id,
|
||||||
|
'date' => $eventDate->date->format('Y-m-d'),
|
||||||
|
'time_start' => $eventDate->time_start,
|
||||||
|
'time_end' => $eventDate->time_end,
|
||||||
|
])->values(),
|
||||||
|
]),
|
||||||
'extras' => $this->whenLoaded(
|
'extras' => $this->whenLoaded(
|
||||||
'websiteExtras',
|
'websiteExtras',
|
||||||
fn () => $this->websiteExtras
|
fn () => $this->websiteExtras
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class TenantInformationService
|
|||||||
'headerLogo',
|
'headerLogo',
|
||||||
'footerLogo',
|
'footerLogo',
|
||||||
'socialMedia',
|
'socialMedia',
|
||||||
'websiteType',
|
|
||||||
'websiteExtras.websiteTypeExtra',
|
'websiteExtras.websiteTypeExtra',
|
||||||
|
'activeEvent.dates',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -174,7 +174,15 @@ class WebsiteExtraService
|
|||||||
$websiteExtra = $tenant->websiteExtras()
|
$websiteExtra = $tenant->websiteExtras()
|
||||||
->where('website_type_extra_id', $definition->id)
|
->where('website_type_extra_id', $definition->id)
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->firstOrFail();
|
->first();
|
||||||
|
|
||||||
|
if (! $websiteExtra) {
|
||||||
|
return $tenant->websiteExtras()->create([
|
||||||
|
'website_type_extra_id' => $definition->id,
|
||||||
|
'config' => [],
|
||||||
|
'is_enabled' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$websiteExtra->update([
|
$websiteExtra->update([
|
||||||
'is_enabled' => ! $websiteExtra->is_enabled,
|
'is_enabled' => ! $websiteExtra->is_enabled,
|
||||||
|
|||||||
87
app/Domains/Tenant/Services/WebsiteTypeService.php
Normal file
87
app/Domains/Tenant/Services/WebsiteTypeService.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Tenant\Services;
|
||||||
|
|
||||||
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
|
use App\Domains\Attachable\Services\AttachmentService;
|
||||||
|
use App\Domains\Tenant\Models\WebsiteType;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class WebsiteTypeService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AttachmentService $attachmentService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a website type and store its logos.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*/
|
||||||
|
public function create(array $data): WebsiteType
|
||||||
|
{
|
||||||
|
return $this->save(new WebsiteType, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update a website type and replace its logos when provided.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attributes
|
||||||
|
* @param array<string, mixed> $values
|
||||||
|
*/
|
||||||
|
public function updateOrCreate(array $attributes, array $values = []): WebsiteType
|
||||||
|
{
|
||||||
|
/** @var WebsiteType $websiteType */
|
||||||
|
$websiteType = WebsiteType::query()->firstOrNew($attributes);
|
||||||
|
|
||||||
|
return $this->save($websiteType, [...$attributes, ...$values]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*/
|
||||||
|
private function save(WebsiteType $websiteType, array $data): WebsiteType
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($websiteType, $data): WebsiteType {
|
||||||
|
$previousLogos = [];
|
||||||
|
|
||||||
|
foreach (['site_logo' => 'siteLogo', 'footer_logo' => 'footerLogo'] as $field => $relation) {
|
||||||
|
if (! array_key_exists($field, $data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logo = $data[$field];
|
||||||
|
$previousLogos[$field] = $websiteType->exists
|
||||||
|
? $websiteType->{$relation}()->first()
|
||||||
|
: null;
|
||||||
|
unset($data[$field]);
|
||||||
|
|
||||||
|
$attachment = null;
|
||||||
|
|
||||||
|
if ($logo) {
|
||||||
|
$attachment = is_string($logo) && Str::isUuid($logo)
|
||||||
|
? Attachment::query()->where('key', $logo)->first()
|
||||||
|
: $this->attachmentService->store($logo, 'website-types');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[$field] = $attachment?->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$websiteType->fill($data)->save();
|
||||||
|
|
||||||
|
foreach ($previousLogos as $field => $previousLogo) {
|
||||||
|
if (
|
||||||
|
$previousLogo instanceof Attachment
|
||||||
|
&& $previousLogo->id !== $websiteType->{$field}
|
||||||
|
&& $previousLogo->id !== $websiteType->site_logo
|
||||||
|
&& $previousLogo->id !== $websiteType->footer_logo
|
||||||
|
) {
|
||||||
|
$this->attachmentService->delete($previousLogo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $websiteType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Domains\Tenant\Controllers\AdminApp\BootstrapTenantController;
|
|
||||||
use App\Domains\Tenant\Controllers\AdminApp\WebsiteExtraController;
|
use App\Domains\Tenant\Controllers\AdminApp\WebsiteExtraController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::prefix('v1/adminapp/tenant')
|
Route::prefix('v1/adminapp/tenant')
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::get('bootstrap', BootstrapTenantController::class);
|
|
||||||
Route::get('website-extras', [WebsiteExtraController::class, 'show']);
|
Route::get('website-extras', [WebsiteExtraController::class, 'show']);
|
||||||
Route::get('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'showExtra']);
|
Route::get('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'showExtra']);
|
||||||
Route::put('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'update']);
|
Route::put('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'update']);
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Domains\Tenant\Controllers\BootstrapTenantController;
|
|
||||||
use App\Domains\Tenant\Controllers\TenantController;
|
use App\Domains\Tenant\Controllers\TenantController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('tenants/bootstrap/{dominio}', BootstrapTenantController::class)
|
|
||||||
->where('dominio', '.*');
|
|
||||||
|
|
||||||
Route::apiResource('tenants', TenantController::class);
|
Route::apiResource('tenants', TenantController::class);
|
||||||
|
|
||||||
require __DIR__.'/adminapp.php';
|
require __DIR__.'/adminapp.php';
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class TicketController extends Controller
|
|||||||
$tickets = Ticket::query()
|
$tickets = Ticket::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('user_id', $request->user()->getKey())
|
->where('user_id', $request->user()->getKey())
|
||||||
|
->with('sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ class TicketController extends Controller
|
|||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->where('user_id', $request->user()->getKey())
|
->where('user_id', $request->user()->getKey())
|
||||||
->whereIn('id', $ticketIds)
|
->whereIn('id', $ticketIds)
|
||||||
|
->with('sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
namespace App\Domains\Ticket\Models;
|
namespace App\Domains\Ticket\Models;
|
||||||
|
|
||||||
use App\Domains\Auth\Models\User;
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use App\Domains\Purchase\Models\Purchase;
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -21,6 +24,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
'starts_at',
|
'starts_at',
|
||||||
'expires_at',
|
'expires_at',
|
||||||
'used_at',
|
'used_at',
|
||||||
|
'scanner_user_id',
|
||||||
'user_id',
|
'user_id',
|
||||||
])]
|
])]
|
||||||
class Ticket extends Model
|
class Ticket extends Model
|
||||||
@@ -44,6 +48,7 @@ class Ticket extends Model
|
|||||||
'starts_at' => 'datetime',
|
'starts_at' => 'datetime',
|
||||||
'expires_at' => 'datetime',
|
'expires_at' => 'datetime',
|
||||||
'used_at' => 'datetime',
|
'used_at' => 'datetime',
|
||||||
|
'scanner_user_id' => 'integer',
|
||||||
'user_id' => 'integer',
|
'user_id' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -60,19 +65,39 @@ class Ticket extends Model
|
|||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<User, $this> */
|
||||||
|
public function scannerUser(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'scanner_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<Purchase, $this> */
|
/** @return BelongsTo<Purchase, $this> */
|
||||||
public function sourcePurchase(): BelongsTo
|
public function sourcePurchase(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Purchase::class, 'source_purchase_id');
|
return $this->belongsTo(Purchase::class, 'source_purchase_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<CatalogItem, $this> */
|
||||||
|
public function sourceCatalogItem(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CatalogItem::class, 'source_catalog_item_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Variant, $this> */
|
||||||
|
public function sourceVariant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Variant::class, 'source_variant_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function isValid(): bool
|
public function isValid(): bool
|
||||||
{
|
{
|
||||||
$now = now();
|
$now = now();
|
||||||
|
$startsAt = $this->getEffectiveStartsAt();
|
||||||
|
$expiresAt = $this->getEffectiveExpiresAt();
|
||||||
|
|
||||||
return $this->used_at === null
|
return $this->used_at === null
|
||||||
&& ($this->starts_at === null || $this->starts_at->lessThanOrEqualTo($now))
|
&& ($startsAt === null || $startsAt->lessThanOrEqualTo($now))
|
||||||
&& ($this->expires_at === null || $this->expires_at->greaterThan($now));
|
&& ($expiresAt === null || $expiresAt->greaterThan($now));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getIsValidAttribute(): bool
|
public function getIsValidAttribute(): bool
|
||||||
@@ -82,13 +107,27 @@ class Ticket extends Model
|
|||||||
|
|
||||||
public function getIsExpiredAttribute(): bool
|
public function getIsExpiredAttribute(): bool
|
||||||
{
|
{
|
||||||
|
$expiresAt = $this->getEffectiveExpiresAt();
|
||||||
|
|
||||||
return $this->used_at === null
|
return $this->used_at === null
|
||||||
&& $this->expires_at !== null
|
&& $expiresAt !== null
|
||||||
&& $this->expires_at->lessThanOrEqualTo(now());
|
&& $expiresAt->lessThanOrEqualTo(now());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getIsUsedAttribute(): bool
|
public function getIsUsedAttribute(): bool
|
||||||
{
|
{
|
||||||
return $this->used_at !== null;
|
return $this->used_at !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEffectiveStartsAt(): ?CarbonInterface
|
||||||
|
{
|
||||||
|
return $this->sourceVariant?->getMinimumUseDate()
|
||||||
|
?? $this->starts_at;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEffectiveExpiresAt(): ?CarbonInterface
|
||||||
|
{
|
||||||
|
return $this->sourceVariant?->getMaximumUseDate()
|
||||||
|
?? $this->expires_at;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ class TicketResource extends JsonResource
|
|||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'source_catalog_item_id' => $this->source_catalog_item_id,
|
'source_catalog_item_id' => $this->source_catalog_item_id,
|
||||||
'source_variant_id' => $this->source_variant_id,
|
'source_variant_id' => $this->source_variant_id,
|
||||||
'starts_at' => $this->starts_at,
|
'starts_at' => $this->getEffectiveStartsAt(),
|
||||||
'expires_at' => $this->expires_at,
|
'expires_at' => $this->getEffectiveExpiresAt(),
|
||||||
'used_at' => $this->used_at,
|
'used_at' => $this->used_at,
|
||||||
|
'scanner_user_id' => $this->scanner_user_id,
|
||||||
'is_valid' => $this->is_valid,
|
'is_valid' => $this->is_valid,
|
||||||
'is_expired' => $this->is_expired,
|
'is_expired' => $this->is_expired,
|
||||||
'is_used' => $this->is_used,
|
'is_used' => $this->is_used,
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ class TicketGeneratorService
|
|||||||
);
|
);
|
||||||
|
|
||||||
return $targets->map(function (array $target) use (
|
return $targets->map(function (array $target) use (
|
||||||
$catalogItem,
|
|
||||||
$sourceVariantId,
|
|
||||||
$sourcePurchaseId,
|
$sourcePurchaseId,
|
||||||
$user,
|
$user,
|
||||||
): Ticket {
|
): Ticket {
|
||||||
@@ -49,8 +47,8 @@ class TicketGeneratorService
|
|||||||
'name' => $item->nombre,
|
'name' => $item->nombre,
|
||||||
'description' => (string) ($item->descripcion ?? ''),
|
'description' => (string) ($item->descripcion ?? ''),
|
||||||
'source_purchase_id' => $sourcePurchaseId,
|
'source_purchase_id' => $sourcePurchaseId,
|
||||||
'source_catalog_item_id' => $catalogItem->getKey(),
|
'source_catalog_item_id' => $item->getKey(),
|
||||||
'source_variant_id' => $sourceVariantId,
|
'source_variant_id' => $target['variant']?->getKey(),
|
||||||
'starts_at' => $selectedItem->getMinimumUseDate(),
|
'starts_at' => $selectedItem->getMinimumUseDate(),
|
||||||
'expires_at' => $selectedItem->getMaximumUseDate(),
|
'expires_at' => $selectedItem->getMaximumUseDate(),
|
||||||
'used_at' => null,
|
'used_at' => null,
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('website_type', function (Blueprint $table): void {
|
||||||
|
$table->string('dominio')->nullable()->unique()->after('nombre');
|
||||||
|
$table->string('primary_color')->nullable()->after('dominio');
|
||||||
|
$table->string('secondary_color')->nullable()->after('primary_color');
|
||||||
|
$table->string('danger_color')->nullable()->after('secondary_color');
|
||||||
|
$table->string('success_color')->nullable()->after('danger_color');
|
||||||
|
$table->string('warning_color')->nullable()->after('success_color');
|
||||||
|
$table->string('body_color')->nullable()->after('warning_color');
|
||||||
|
$table->string('darker_body_color')->nullable()->after('body_color');
|
||||||
|
$table->string('surface_color')->nullable()->after('darker_body_color');
|
||||||
|
$table->string('background_color')->nullable()->after('surface_color');
|
||||||
|
$table->string('border_color')->nullable()->after('background_color');
|
||||||
|
$table->string('login_header_footer_color')->nullable()->after('border_color');
|
||||||
|
$table->unsignedBigInteger('site_logo')->nullable()->after('login_header_footer_color');
|
||||||
|
|
||||||
|
$table->foreign('site_logo')
|
||||||
|
->references('id')
|
||||||
|
->on('attachments')
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('website_type', function (Blueprint $table): void {
|
||||||
|
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
|
||||||
|
$table->dropForeign(['site_logo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->dropColumn([
|
||||||
|
'dominio',
|
||||||
|
'primary_color',
|
||||||
|
'secondary_color',
|
||||||
|
'danger_color',
|
||||||
|
'success_color',
|
||||||
|
'warning_color',
|
||||||
|
'body_color',
|
||||||
|
'darker_body_color',
|
||||||
|
'surface_color',
|
||||||
|
'background_color',
|
||||||
|
'border_color',
|
||||||
|
'login_header_footer_color',
|
||||||
|
'site_logo',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('website_type', function (Blueprint $table): void {
|
||||||
|
$table->unsignedBigInteger('footer_logo')->nullable()->after('site_logo');
|
||||||
|
|
||||||
|
$table->foreign('footer_logo')
|
||||||
|
->references('id')
|
||||||
|
->on('attachments')
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('website_type', function (Blueprint $table): void {
|
||||||
|
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
|
||||||
|
$table->dropForeign(['footer_logo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->dropColumn('footer_logo');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Enums\EventProductType;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('events', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('address');
|
||||||
|
|
||||||
|
$table->foreign('tenant_code')
|
||||||
|
->references('codigo')
|
||||||
|
->on('tenants')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('event_dates', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('event_id')->constrained('events')->cascadeOnDelete();
|
||||||
|
$table->date('date');
|
||||||
|
$table->time('time_start');
|
||||||
|
$table->time('time_end');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('tenants', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('active_event_id')
|
||||||
|
->nullable()
|
||||||
|
->constrained('events')
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('catalog_items', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('event_id')
|
||||||
|
->nullable()
|
||||||
|
->after('tenant_code')
|
||||||
|
->constrained('events')
|
||||||
|
->nullOnDelete();
|
||||||
|
$table->enum('event_product_type', EventProductType::values())
|
||||||
|
->nullable()
|
||||||
|
->after('event_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('variantes', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('event_date_id')
|
||||||
|
->nullable()
|
||||||
|
->after('catalog_item_id')
|
||||||
|
->constrained('event_dates')
|
||||||
|
->nullOnDelete();
|
||||||
|
$table->unique(['catalog_item_id', 'event_date_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('active_event_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('variantes', function (Blueprint $table): void {
|
||||||
|
$table->dropUnique(['catalog_item_id', 'event_date_id']);
|
||||||
|
$table->dropConstrainedForeignId('event_date_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('catalog_items', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('event_id');
|
||||||
|
$table->dropColumn('event_product_type');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('event_dates');
|
||||||
|
Schema::dropIfExists('events');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('tickets')
|
||||||
|
->whereNotNull('source_variant_id')
|
||||||
|
->whereNotIn('source_variant_id', DB::table('variantes')->select('id'))
|
||||||
|
->update(['source_variant_id' => null]);
|
||||||
|
|
||||||
|
DB::table('tickets')
|
||||||
|
->whereNotNull('source_catalog_item_id')
|
||||||
|
->whereNotIn('source_catalog_item_id', DB::table('catalog_items')->select('id'))
|
||||||
|
->update(['source_catalog_item_id' => null]);
|
||||||
|
|
||||||
|
Schema::table('tickets', function (Blueprint $table): void {
|
||||||
|
$table->foreign('source_catalog_item_id')
|
||||||
|
->references('id')
|
||||||
|
->on('catalog_items')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->nullOnDelete();
|
||||||
|
$table->foreign('source_variant_id')
|
||||||
|
->references('id')
|
||||||
|
->on('variantes')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tickets', function (Blueprint $table): void {
|
||||||
|
$table->dropForeign(['source_catalog_item_id']);
|
||||||
|
$table->dropForeign(['source_variant_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('value_changes', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('trackable');
|
||||||
|
$table->string('attribute');
|
||||||
|
$table->text('old_value')->nullable();
|
||||||
|
$table->text('new_value')->nullable();
|
||||||
|
$table->timestamp('changed_at');
|
||||||
|
$table->string('actor_type');
|
||||||
|
$table->foreignId('user_id')
|
||||||
|
->nullable()
|
||||||
|
->constrained('users')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('value_changes');
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user