Compare commits
17 Commits
authorizat
...
4a51f3afde
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a51f3afde | |||
| f1df6a5918 | |||
| dbeaf19513 | |||
| dd76d5d951 | |||
| f603a82b5e | |||
| b478c23f3b | |||
| 6858b387c3 | |||
| 893bea1492 | |||
| ec1d80dff2 | |||
| 3968a10f6a | |||
| cf2beb2ff5 | |||
| 484fa204b3 | |||
| 6dd057874a | |||
| 40cbac17c6 | |||
| 45e56c43a6 | |||
| cbb7b1c3a1 | |||
| 6b3a12b624 |
42
app/Domains/Auth/Controllers/AdminAppLoginController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Auth\Controllers;
|
||||
|
||||
use App\Domains\Auth\Requests\AdminAppLoginRequest;
|
||||
use App\Domains\Auth\Resources\UserResource;
|
||||
use App\Domains\Auth\Services\PasswordLoginService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class AdminAppLoginController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PasswordLoginService $passwordLoginService,
|
||||
) {}
|
||||
|
||||
public function __invoke(AdminAppLoginRequest $request): JsonResponse
|
||||
{
|
||||
$credentials = $request->validated();
|
||||
$user = $this->passwordLoginService->authenticateAdminApp(
|
||||
$credentials['email'],
|
||||
$credentials['password'],
|
||||
$request->ip(),
|
||||
$request->userAgent(),
|
||||
);
|
||||
|
||||
$expirationMinutes = (int) config('sanctum.expiration');
|
||||
$token = $user->createToken(
|
||||
'adminapp-token',
|
||||
['adminapp'],
|
||||
now()->addMinutes($expirationMinutes),
|
||||
)->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'code' => 'auth.login_success',
|
||||
'message' => __('api.auth.login_success'),
|
||||
'token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'user' => UserResource::make($user),
|
||||
]);
|
||||
}
|
||||
}
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
36
app/Domains/Auth/Requests/AdminAppLoginRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Auth\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminAppLoginRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$email = $this->input('email');
|
||||
|
||||
if (is_string($email)) {
|
||||
$this->merge([
|
||||
'email' => Str::lower(trim($email)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email', 'max:255'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Domains\Auth\Services;
|
||||
use App\Domains\Auth\Exceptions\AccountLockedException;
|
||||
use App\Domains\Auth\Models\LoginAttempt;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Authorization\Enums\RoleCode;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -22,6 +23,48 @@ class PasswordLoginService
|
||||
string $tenantCode,
|
||||
?string $ipAddress,
|
||||
?string $userAgent,
|
||||
): User {
|
||||
return $this->authenticateUser(
|
||||
$email,
|
||||
$password,
|
||||
$tenantCode,
|
||||
$ipAddress,
|
||||
$userAgent,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a tenant-bound AdminApp user without requiring the caller
|
||||
* to know their tenant code beforehand.
|
||||
*
|
||||
* @throws AccountLockedException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticateAdminApp(
|
||||
string $email,
|
||||
string $password,
|
||||
?string $ipAddress,
|
||||
?string $userAgent,
|
||||
): User {
|
||||
return $this->authenticateUser(
|
||||
$email,
|
||||
$password,
|
||||
null,
|
||||
$ipAddress,
|
||||
$userAgent,
|
||||
RoleCode::AdminApp,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private function authenticateUser(
|
||||
string $email,
|
||||
string $password,
|
||||
?string $tenantCode,
|
||||
?string $ipAddress,
|
||||
?string $userAgent,
|
||||
RoleCode $requiredRole = RoleCode::User,
|
||||
bool $requiresTenant = false,
|
||||
): User {
|
||||
$normalizedEmail = mb_strtolower(trim($email));
|
||||
$now = CarbonImmutable::now();
|
||||
@@ -34,17 +77,25 @@ class PasswordLoginService
|
||||
$ipAddress,
|
||||
$userAgent,
|
||||
$now,
|
||||
$requiredRole,
|
||||
$requiresTenant,
|
||||
): array {
|
||||
$user = User::query()
|
||||
->where('email', $normalizedEmail)
|
||||
->where('rol_codigo', $requiredRole->value)
|
||||
->when(
|
||||
$requiresTenant,
|
||||
fn ($query) => $query->whereNotNull('tenant_codigo'),
|
||||
)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
$attemptTenantCode = $tenantCode ?? $user?->tenant_codigo;
|
||||
|
||||
if ($user?->locked_until?->isFuture()) {
|
||||
$this->recordAttempt(
|
||||
$user,
|
||||
$normalizedEmail,
|
||||
$tenantCode,
|
||||
$attemptTenantCode,
|
||||
LoginAttempt::OUTCOME_ACCOUNT_LOCKED,
|
||||
$ipAddress,
|
||||
$userAgent,
|
||||
@@ -76,7 +127,7 @@ class PasswordLoginService
|
||||
$this->recordAttempt(
|
||||
$user,
|
||||
$normalizedEmail,
|
||||
$tenantCode,
|
||||
$attemptTenantCode,
|
||||
$outcome,
|
||||
$ipAddress,
|
||||
$userAgent,
|
||||
@@ -100,7 +151,7 @@ class PasswordLoginService
|
||||
$this->recordAttempt(
|
||||
$user,
|
||||
$normalizedEmail,
|
||||
$tenantCode,
|
||||
$attemptTenantCode,
|
||||
LoginAttempt::OUTCOME_SUCCESS,
|
||||
$ipAddress,
|
||||
$userAgent,
|
||||
@@ -150,7 +201,7 @@ class PasswordLoginService
|
||||
private function recordAttempt(
|
||||
?User $user,
|
||||
string $normalizedEmail,
|
||||
string $tenantCode,
|
||||
?string $tenantCode,
|
||||
string $outcome,
|
||||
?string $ipAddress,
|
||||
?string $userAgent,
|
||||
|
||||
11
app/Domains/Auth/routes/adminapp.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Auth\Controllers\AdminAppLoginController;
|
||||
use App\Domains\Auth\Controllers\AdminAppMeController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('v1/adminapp')->group(function (): void {
|
||||
Route::post('login', AdminAppLoginController::class)->middleware('throttle:login');
|
||||
Route::middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||
->get('me', AdminAppMeController::class);
|
||||
});
|
||||
@@ -23,3 +23,5 @@ Route::post('/auth/google/exchange', GoogleTokenExchangeController::class);
|
||||
Route::middleware('auth:sanctum')->post('/logout', LogoutController::class);
|
||||
Route::middleware('auth:sanctum')->get('/me', MeController::class);
|
||||
Route::middleware('auth:sanctum')->put('/me', UpdateProfileController::class);
|
||||
|
||||
require __DIR__.'/adminapp.php';
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Tenant\Controllers\AdminApp;
|
||||
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use App\Domains\Tenant\Requests\BootstrapAdminAppRequest;
|
||||
use App\Domains\Tenant\Resources\AdminApp\BootstrapAdminAppResource;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class BootstrapAdminAppController extends Controller
|
||||
{
|
||||
public function __invoke(
|
||||
BootstrapAdminAppRequest $request
|
||||
): BootstrapAdminAppResource {
|
||||
/** @var string $domain */
|
||||
$domain = $request->validated('dominio');
|
||||
|
||||
return BootstrapAdminAppResource::make(
|
||||
WebsiteType::query()
|
||||
->with(['siteLogo', 'footerLogo'])
|
||||
->where('dominio', $domain)
|
||||
->firstOrFail()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ namespace App\Domains\Tenant\Controllers\AdminApp;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Requests\AdminApp\UpdateWebsiteExtrasRequest;
|
||||
use App\Domains\Tenant\Requests\AdminApp\UpdateWebsiteExtraRequest;
|
||||
use App\Domains\Tenant\Resources\AdminApp\WebsiteExtraResource;
|
||||
use App\Domains\Tenant\Resources\AdminApp\WebsiteExtrasResource;
|
||||
use App\Domains\Tenant\Services\TenantInformationService;
|
||||
use App\Domains\Tenant\Services\WebsiteExtraService;
|
||||
@@ -25,13 +26,30 @@ class WebsiteExtraController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function update(UpdateWebsiteExtrasRequest $request): WebsiteExtrasResource
|
||||
public function showExtra(Request $request, string $websiteExtraCode): WebsiteExtraResource
|
||||
{
|
||||
$tenant = $this->loadTenant($request->user());
|
||||
$definition = $this->websiteExtraService->definitionForTenant($tenant, $websiteExtraCode);
|
||||
$websiteExtra = $tenant->websiteExtras
|
||||
->firstWhere('website_type_extra_id', $definition->id);
|
||||
|
||||
if (! $websiteExtra) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return WebsiteExtraResource::make($websiteExtra);
|
||||
}
|
||||
|
||||
public function update(
|
||||
UpdateWebsiteExtraRequest $request,
|
||||
string $websiteExtraCode
|
||||
): WebsiteExtrasResource {
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
$this->websiteExtraService->replaceForTenant(
|
||||
$this->websiteExtraService->updateForTenant(
|
||||
$tenant,
|
||||
$request->validated('extras', [])
|
||||
$websiteExtraCode,
|
||||
$request->validated('config')
|
||||
);
|
||||
|
||||
return WebsiteExtrasResource::make(
|
||||
@@ -39,6 +57,20 @@ class WebsiteExtraController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function toggle(Request $request, string $websiteExtraCode): WebsiteExtrasResource
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
$this->websiteExtraService->toggleForTenant($tenant, $websiteExtraCode);
|
||||
|
||||
return WebsiteExtrasResource::make(
|
||||
$this->loadTenant($request->user())
|
||||
)->additional([
|
||||
'code' => 'tenant.website_extra_toggled',
|
||||
'message' => __('api.tenant.website_extra_toggled'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function loadTenant(User $user): Tenant
|
||||
{
|
||||
$tenant = $user->tenant()->firstOrFail();
|
||||
|
||||
@@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
'website_code',
|
||||
'website_type_extra_id',
|
||||
'config',
|
||||
'is_enabled',
|
||||
])]
|
||||
class WebsiteExtra extends Model
|
||||
{
|
||||
@@ -30,6 +31,7 @@ class WebsiteExtra extends Model
|
||||
return [
|
||||
'website_type_extra_id' => 'integer',
|
||||
'config' => 'array',
|
||||
'is_enabled' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,30 @@
|
||||
|
||||
namespace App\Domains\Tenant\Models;
|
||||
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
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([
|
||||
'codigo',
|
||||
'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
|
||||
{
|
||||
@@ -17,6 +33,22 @@ class WebsiteType extends Model
|
||||
|
||||
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>
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable([
|
||||
'website_type_code',
|
||||
'codigo',
|
||||
'nombre',
|
||||
'descripcion',
|
||||
'is_required',
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Domains\Tenant\Requests\AdminApp;
|
||||
use App\Domains\Tenant\Services\WebsiteExtraService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateWebsiteExtrasRequest extends FormRequest
|
||||
class UpdateWebsiteExtraRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
@@ -17,8 +17,11 @@ class UpdateWebsiteExtrasRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return app(WebsiteExtraService::class)->requestRules(
|
||||
$this->user()?->tenant?->website_type_code
|
||||
$tenant = $this->user()->tenant()->firstOrFail();
|
||||
|
||||
return app(WebsiteExtraService::class)->requestRulesForExtra(
|
||||
$tenant,
|
||||
(string) $this->route('websiteExtraCode')
|
||||
);
|
||||
}
|
||||
}
|
||||
5
app/Domains/Tenant/Requests/BootstrapAdminAppRequest.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Tenant\Requests;
|
||||
|
||||
class BootstrapAdminAppRequest extends BootstrapTenantRequest {}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Tenant\Resources\AdminApp;
|
||||
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin WebsiteType
|
||||
*/
|
||||
class BootstrapAdminAppResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'website_type_code' => $this->codigo,
|
||||
'primary_color' => $this->primary_color,
|
||||
'secondary_color' => $this->secondary_color,
|
||||
'danger_color' => $this->danger_color,
|
||||
'success_color' => $this->success_color,
|
||||
'warning_color' => $this->warning_color,
|
||||
'body_color' => $this->body_color,
|
||||
'darker_body_color' => $this->darker_body_color,
|
||||
'surface_color' => $this->surface_color,
|
||||
'background_color' => $this->background_color,
|
||||
'border_color' => $this->border_color,
|
||||
'login_header_footer_color' => $this->login_header_footer_color,
|
||||
'site_logo' => $this->siteLogo?->getTemporaryUrl(1440),
|
||||
'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Tenant\Resources\AdminApp;
|
||||
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Tenant\Models\WebsiteExtra;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin WebsiteExtra
|
||||
*/
|
||||
class WebsiteExtraResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'codigo' => $this->websiteTypeExtra->codigo,
|
||||
'nombre' => $this->websiteTypeExtra->nombre,
|
||||
'descripcion' => $this->websiteTypeExtra->descripcion,
|
||||
'is_required' => $this->websiteTypeExtra->is_required,
|
||||
'is_enabled' => $this->is_enabled,
|
||||
'request_rules' => $this->websiteTypeExtra->config_schema['request_rules'] ?? [],
|
||||
'config' => $this->formatConfig(
|
||||
$this->resolvedConfig(),
|
||||
fn (Attachment $attachment): string => $attachment->key
|
||||
),
|
||||
'resolved_config' => $this->formatConfig(
|
||||
$this->resolvedConfig(),
|
||||
fn (Attachment $attachment): string => $attachment->getTemporaryUrl(1440)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatConfig(mixed $value, callable $formatAttachment): mixed
|
||||
{
|
||||
if ($value instanceof Attachment) {
|
||||
return $formatAttachment($value);
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return array_map(
|
||||
fn (mixed $item): mixed => $this->formatConfig($item, $formatAttachment),
|
||||
$value
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class WebsiteExtrasResource extends JsonResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$websiteExtras = $this->websiteExtras->keyBy(
|
||||
fn ($extra) => $extra->websiteTypeExtra->nombre
|
||||
fn ($extra) => $extra->websiteTypeExtra->codigo
|
||||
);
|
||||
|
||||
return [
|
||||
@@ -28,20 +28,24 @@ class WebsiteExtrasResource extends JsonResource
|
||||
] : null,
|
||||
'definitions' => $this->websiteType?->extras
|
||||
->mapWithKeys(fn ($definition) => [
|
||||
$definition->nombre => [
|
||||
$definition->codigo => [
|
||||
'codigo' => $definition->codigo,
|
||||
'nombre' => $definition->nombre,
|
||||
'descripcion' => $definition->descripcion,
|
||||
'is_required' => $definition->is_required,
|
||||
'is_enabled' => $websiteExtras
|
||||
->get($definition->codigo)?->is_enabled,
|
||||
'request_rules' => $definition->config_schema['request_rules'] ?? [],
|
||||
],
|
||||
]) ?? [],
|
||||
'extras' => $websiteExtras->mapWithKeys(fn ($extra) => [
|
||||
$extra->websiteTypeExtra->nombre => $this->formatConfig(
|
||||
$extra->websiteTypeExtra->codigo => $this->formatConfig(
|
||||
$extra->resolvedConfig(),
|
||||
fn (Attachment $attachment): string => $attachment->key
|
||||
),
|
||||
]),
|
||||
'resolved_extras' => $websiteExtras->mapWithKeys(fn ($extra) => [
|
||||
$extra->websiteTypeExtra->nombre => $this->formatConfig(
|
||||
$extra->websiteTypeExtra->codigo => $this->formatConfig(
|
||||
$extra->resolvedConfig(),
|
||||
fn (Attachment $attachment): string => $attachment->getTemporaryUrl(1440)
|
||||
),
|
||||
|
||||
@@ -32,20 +32,15 @@ class TenantResource extends JsonResource
|
||||
'header_bg_color' => $this->header_bg_color,
|
||||
'footer_bg_color' => $this->footer_bg_color,
|
||||
'website_type_code' => $this->website_type_code,
|
||||
'website_type' => $this->whenLoaded(
|
||||
'websiteType',
|
||||
fn () => $this->websiteType ? [
|
||||
'codigo' => $this->websiteType->codigo,
|
||||
'nombre' => $this->websiteType->nombre,
|
||||
] : null
|
||||
),
|
||||
'extras' => $this->whenLoaded(
|
||||
'websiteExtras',
|
||||
fn () => $this->websiteExtras->mapWithKeys(fn ($extra) => [
|
||||
$extra->websiteTypeExtra->nombre => $this->formatExtraConfig(
|
||||
$extra->resolvedConfig()
|
||||
),
|
||||
])
|
||||
fn () => $this->websiteExtras
|
||||
->filter(fn ($extra) => $extra->is_enabled)
|
||||
->mapWithKeys(fn ($extra) => [
|
||||
$extra->websiteTypeExtra->codigo => $this->formatExtraConfig(
|
||||
$extra->resolvedConfig()
|
||||
),
|
||||
])
|
||||
),
|
||||
// 1 day
|
||||
'header_logo' => $this->headerLogo?->getTemporaryUrl(1440),
|
||||
|
||||
@@ -14,7 +14,6 @@ class TenantInformationService
|
||||
'headerLogo',
|
||||
'footerLogo',
|
||||
'socialMedia',
|
||||
'websiteType',
|
||||
'websiteExtras.websiteTypeExtra',
|
||||
];
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Attachable\Services\AttachmentService;
|
||||
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Models\WebsiteExtra;
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use App\Domains\Tenant\Models\WebsiteTypeExtra;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use InvalidArgumentException;
|
||||
@@ -34,7 +34,7 @@ class WebsiteExtraService
|
||||
}
|
||||
|
||||
$definitions = $this->definitionsFor($websiteTypeCode);
|
||||
$allowedNames = $definitions->pluck('nombre')->all();
|
||||
$allowedCodes = $definitions->pluck('codigo')->all();
|
||||
$hasRequiredExtras = $definitions->contains(
|
||||
fn (WebsiteTypeExtra $definition): bool => $definition->is_required
|
||||
);
|
||||
@@ -43,17 +43,17 @@ class WebsiteExtraService
|
||||
'extras' => [
|
||||
$hasRequiredExtras ? 'required' : 'sometimes',
|
||||
'array',
|
||||
function (string $attribute, mixed $value, \Closure $fail) use ($allowedNames): void {
|
||||
function (string $attribute, mixed $value, \Closure $fail) use ($allowedCodes): void {
|
||||
if (! is_array($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unknownNames = array_diff(array_keys($value), $allowedNames);
|
||||
$unknownCodes = array_diff(array_keys($value), $allowedCodes);
|
||||
|
||||
if ($unknownNames !== []) {
|
||||
if ($unknownCodes !== []) {
|
||||
$fail(
|
||||
'The '.$attribute.' field contains extras not supported by the website type: '
|
||||
.implode(', ', $unknownNames).'.'
|
||||
.implode(', ', $unknownCodes).'.'
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -69,14 +69,14 @@ class WebsiteExtraService
|
||||
));
|
||||
array_unshift($rootRules, $definition->is_required ? 'required' : 'sometimes');
|
||||
|
||||
$rules["extras.{$definition->nombre}"] = $rootRules;
|
||||
$rules["extras.{$definition->codigo}"] = $rootRules;
|
||||
|
||||
foreach ($schemaRules as $path => $pathRules) {
|
||||
if ($path === '$') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rules[$this->requestAttribute($definition->nombre, $path)] = $this->compileRules($pathRules);
|
||||
$rules[$this->requestAttribute($definition->codigo, $path)] = $this->compileRules($pathRules);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class WebsiteExtraService
|
||||
}
|
||||
|
||||
$definitions = $this->definitionsFor((string) $tenant->website_type_code)
|
||||
->keyBy('nombre');
|
||||
->keyBy('codigo');
|
||||
|
||||
foreach ($extras as $name => $config) {
|
||||
/** @var WebsiteTypeExtra|null $definition */
|
||||
@@ -107,9 +107,13 @@ class WebsiteExtraService
|
||||
]);
|
||||
}
|
||||
|
||||
$transformedConfig = $this->applyTransforms($tenant, $definition, $config);
|
||||
$this->validateDatabaseConfig($definition, $transformedConfig);
|
||||
|
||||
$requestRoot = "extras.{$definition->codigo}";
|
||||
$transformedConfig = $this->applyTransforms(
|
||||
$tenant,
|
||||
$definition,
|
||||
$config,
|
||||
$requestRoot
|
||||
);
|
||||
$tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $definition->id,
|
||||
'config' => $transformedConfig,
|
||||
@@ -120,18 +124,80 @@ class WebsiteExtraService
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all configured extras for a tenant.
|
||||
* Build request rules for one extra addressed by its stable code.
|
||||
*
|
||||
* @param array<string, mixed> $extras
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function replaceForTenant(Tenant $tenant, array $extras): void
|
||||
public function requestRulesForExtra(Tenant $tenant, string $extraCode): array
|
||||
{
|
||||
DB::transaction(function () use ($tenant, $extras): void {
|
||||
$tenant->websiteExtras()->delete();
|
||||
$this->createForTenant($tenant, $extras);
|
||||
});
|
||||
$definition = $this->definitionForTenant($tenant, $extraCode);
|
||||
$schemaRules = $definition->config_schema['request_rules'] ?? [];
|
||||
$rootRules = $this->compileRules($schemaRules['$'] ?? []);
|
||||
$rootRules = array_values(array_filter(
|
||||
$rootRules,
|
||||
fn (mixed $rule): bool => ! in_array($rule, ['required', 'sometimes'], true)
|
||||
));
|
||||
array_unshift($rootRules, 'required');
|
||||
|
||||
$tenant->unsetRelation('websiteExtras');
|
||||
$rules = ['config' => $rootRules];
|
||||
|
||||
foreach ($schemaRules as $path => $pathRules) {
|
||||
if ($path === '$') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rules[$this->configAttribute('config', $path)] = $this->compileRules($pathRules);
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function updateForTenant(Tenant $tenant, string $extraCode, mixed $config): WebsiteExtra
|
||||
{
|
||||
$definition = $this->definitionForTenant($tenant, $extraCode);
|
||||
|
||||
return DB::transaction(function () use ($tenant, $definition, $config): WebsiteExtra {
|
||||
$transformedConfig = $this->applyTransforms($tenant, $definition, $config, 'config');
|
||||
|
||||
return $tenant->websiteExtras()->updateOrCreate(
|
||||
['website_type_extra_id' => $definition->id],
|
||||
['config' => $transformedConfig]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function toggleForTenant(Tenant $tenant, string $extraCode): WebsiteExtra
|
||||
{
|
||||
$definition = $this->definitionForTenant($tenant, $extraCode);
|
||||
|
||||
return DB::transaction(function () use ($tenant, $definition): WebsiteExtra {
|
||||
$websiteExtra = $tenant->websiteExtras()
|
||||
->where('website_type_extra_id', $definition->id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (! $websiteExtra) {
|
||||
return $tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $definition->id,
|
||||
'config' => [],
|
||||
'is_enabled' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$websiteExtra->update([
|
||||
'is_enabled' => ! $websiteExtra->is_enabled,
|
||||
]);
|
||||
|
||||
return $websiteExtra;
|
||||
});
|
||||
}
|
||||
|
||||
public function definitionForTenant(Tenant $tenant, string $extraCode): WebsiteTypeExtra
|
||||
{
|
||||
return WebsiteTypeExtra::query()
|
||||
->where('website_type_code', $tenant->website_type_code)
|
||||
->where('codigo', $extraCode)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,23 +229,29 @@ class WebsiteExtraService
|
||||
);
|
||||
}
|
||||
|
||||
private function requestAttribute(string $extraName, string $path): string
|
||||
private function requestAttribute(string $extraCode, string $path): string
|
||||
{
|
||||
return $this->configAttribute("extras.{$extraCode}", $path);
|
||||
}
|
||||
|
||||
private function configAttribute(string $root, string $path): string
|
||||
{
|
||||
if ($path === '$') {
|
||||
return "extras.{$extraName}";
|
||||
return $root;
|
||||
}
|
||||
|
||||
if (str_starts_with($path, '$.')) {
|
||||
$path = substr($path, 2);
|
||||
}
|
||||
|
||||
return "extras.{$extraName}.{$path}";
|
||||
return "{$root}.{$path}";
|
||||
}
|
||||
|
||||
private function applyTransforms(
|
||||
Tenant $tenant,
|
||||
WebsiteTypeExtra $definition,
|
||||
mixed $config
|
||||
mixed $config,
|
||||
string $requestRoot
|
||||
): mixed {
|
||||
foreach ($definition->config_schema['transforms'] ?? [] as $path => $transform) {
|
||||
$segments = $this->pathSegments($path);
|
||||
@@ -191,7 +263,8 @@ class WebsiteExtraService
|
||||
$definition,
|
||||
$path,
|
||||
$value,
|
||||
$transform
|
||||
$transform,
|
||||
$requestRoot
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -245,7 +318,8 @@ class WebsiteExtraService
|
||||
WebsiteTypeExtra $definition,
|
||||
string $path,
|
||||
mixed $value,
|
||||
array $transform
|
||||
array $transform,
|
||||
string $requestRoot
|
||||
): mixed {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
@@ -253,7 +327,7 @@ class WebsiteExtraService
|
||||
|
||||
if (($transform['handler'] ?? null) !== 'attachment') {
|
||||
throw new InvalidArgumentException(
|
||||
"Unsupported transform handler for {$definition->nombre}: ".($transform['handler'] ?? 'null')
|
||||
"Unsupported transform handler for {$definition->codigo}: ".($transform['handler'] ?? 'null')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -262,7 +336,7 @@ class WebsiteExtraService
|
||||
|
||||
if (! $attachment) {
|
||||
throw ValidationException::withMessages([
|
||||
$this->requestAttribute($definition->nombre, $path) => [
|
||||
$this->configAttribute($requestRoot, $path) => [
|
||||
'The selected attachment does not exist.',
|
||||
],
|
||||
]);
|
||||
@@ -270,7 +344,7 @@ class WebsiteExtraService
|
||||
} else {
|
||||
$attachment = $this->attachmentService->store(
|
||||
$value,
|
||||
"tenants/{$tenant->codigo}/extras/{$definition->nombre}"
|
||||
"tenants/{$tenant->codigo}/extras/{$definition->codigo}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -282,7 +356,7 @@ class WebsiteExtraService
|
||||
&& $attachment->type->value !== $expectedType
|
||||
) {
|
||||
throw ValidationException::withMessages([
|
||||
$this->requestAttribute($definition->nombre, $path) => [
|
||||
$this->configAttribute($requestRoot, $path) => [
|
||||
"The attachment must be of type {$expectedType}.",
|
||||
],
|
||||
]);
|
||||
@@ -290,32 +364,4 @@ class WebsiteExtraService
|
||||
|
||||
return $attachment->id;
|
||||
}
|
||||
|
||||
private function validateDatabaseConfig(
|
||||
WebsiteTypeExtra $definition,
|
||||
mixed $config
|
||||
): void {
|
||||
$schemaRules = $definition->config_schema['database_rules'] ?? [];
|
||||
$rules = [];
|
||||
|
||||
foreach ($schemaRules as $path => $pathRules) {
|
||||
$attribute = $path === '$'
|
||||
? 'config'
|
||||
: 'config.'.ltrim(str_starts_with($path, '$.') ? substr($path, 2) : $path, '.');
|
||||
$rules[$attribute] = $this->compileRules($pathRules);
|
||||
}
|
||||
|
||||
$validator = Validator::make(['config' => $config], $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$messages = [];
|
||||
|
||||
foreach ($validator->errors()->toArray() as $attribute => $errors) {
|
||||
$suffix = $attribute === 'config' ? '' : substr($attribute, strlen('config'));
|
||||
$messages["extras.{$definition->nombre}{$suffix}"] = $errors;
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages($messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,19 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Tenant\Controllers\AdminApp\BootstrapAdminAppController;
|
||||
use App\Domains\Tenant\Controllers\AdminApp\WebsiteExtraController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get(
|
||||
'v1/adminapp/bootstrap/{dominio}',
|
||||
BootstrapAdminAppController::class
|
||||
);
|
||||
|
||||
Route::prefix('v1/adminapp/tenant')
|
||||
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||
->group(function (): void {
|
||||
Route::get('website-extras', [WebsiteExtraController::class, 'show'])
|
||||
->name('adminapp.tenant.website-extras.show');
|
||||
Route::put('website-extras', [WebsiteExtraController::class, 'update'])
|
||||
->name('adminapp.tenant.website-extras.update');
|
||||
Route::get('website-extras', [WebsiteExtraController::class, 'show']);
|
||||
Route::get('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'showExtra']);
|
||||
Route::put('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'update']);
|
||||
Route::patch('website-extras/{websiteExtraCode}/toggle', [WebsiteExtraController::class, 'toggle']);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('website_type_extras', function (Blueprint $table): void {
|
||||
$table->string('codigo')->nullable()->after('website_type_code');
|
||||
});
|
||||
|
||||
Schema::table('websites_extras', function (Blueprint $table): void {
|
||||
$table->unique(
|
||||
['website_code', 'website_type_extra_id'],
|
||||
'websites_extras_website_definition_unique'
|
||||
);
|
||||
});
|
||||
|
||||
DB::table('website_type_extras')
|
||||
->select(['id', 'nombre'])
|
||||
->orderBy('id')
|
||||
->each(function (object $extra): void {
|
||||
DB::table('website_type_extras')
|
||||
->where('id', $extra->id)
|
||||
->update(['codigo' => $extra->nombre]);
|
||||
});
|
||||
|
||||
Schema::table('website_type_extras', function (Blueprint $table): void {
|
||||
$table->string('codigo')->nullable(false)->change();
|
||||
$table->unique(
|
||||
['website_type_code', 'codigo'],
|
||||
'website_type_extras_type_code_unique'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('websites_extras', function (Blueprint $table): void {
|
||||
$table->dropUnique('websites_extras_website_definition_unique');
|
||||
});
|
||||
|
||||
Schema::table('website_type_extras', function (Blueprint $table): void {
|
||||
$table->dropUnique('website_type_extras_type_code_unique');
|
||||
$table->dropColumn('codigo');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('website_type_extras')
|
||||
->select(['id', 'config_schema'])
|
||||
->orderBy('id')
|
||||
->each(function (object $definition): void {
|
||||
$schema = json_decode($definition->config_schema, true);
|
||||
|
||||
if (! is_array($schema) || ! array_key_exists('database_rules', $schema)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($schema['database_rules']);
|
||||
|
||||
DB::table('website_type_extras')
|
||||
->where('id', $definition->id)
|
||||
->update(['config_schema' => json_encode($schema)]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Removed rules cannot be reconstructed generically.
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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('websites_extras', function (Blueprint $table): void {
|
||||
$table->boolean('is_enabled')->nullable()->after('config');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('websites_extras', function (Blueprint $table): void {
|
||||
$table->dropColumn('is_enabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -23,16 +23,54 @@ class MenuSeeder extends Seeder
|
||||
'route' => '/product/:id',
|
||||
],
|
||||
['code' => 'checkout', 'label' => 'Finalizar compra', 'route' => '/checkout'],
|
||||
['code' => 'admin.event', 'label' => 'Eventos', 'route' => '/admin/event'],
|
||||
['code' => 'admin.catalog', 'label' => 'Catálogo', 'route' => '/admin/catalog'],
|
||||
['code' => 'admin.combos', 'label' => 'Combos', 'route' => '/admin/combos'],
|
||||
[
|
||||
'code' => 'admin.categories',
|
||||
'code' => 'main.adminapp',
|
||||
'label' => 'Administración',
|
||||
'content_type' => Menu::CONTENT_TYPE_DYNAMIC,
|
||||
'route' => '/',
|
||||
],
|
||||
[
|
||||
'code' => 'adminapp.event',
|
||||
'label' => 'Eventos',
|
||||
'parent_menu_code' => 'main.adminapp',
|
||||
'route' => '/admin/event',
|
||||
],
|
||||
[
|
||||
'code' => 'adminapp.inicio',
|
||||
'label' => 'Inicio',
|
||||
'parent_menu_code' => 'main.adminapp',
|
||||
'route' => '/admin/inicio',
|
||||
],
|
||||
[
|
||||
'code' => 'adminapp.catalog',
|
||||
'label' => 'Catálogo',
|
||||
'parent_menu_code' => 'main.adminapp',
|
||||
'route' => '/admin/catalog',
|
||||
],
|
||||
[
|
||||
'code' => 'adminapp.combos',
|
||||
'label' => 'Combos',
|
||||
'parent_menu_code' => 'main.adminapp',
|
||||
'route' => '/admin/combos',
|
||||
],
|
||||
[
|
||||
'code' => 'adminapp.categories',
|
||||
'label' => 'Categorías',
|
||||
'parent_menu_code' => 'main.adminapp',
|
||||
'route' => '/admin/categories',
|
||||
],
|
||||
['code' => 'admin.ventas', 'label' => 'Ventas', 'route' => '/admin/ventas'],
|
||||
['code' => 'admin.staff', 'label' => 'Staff', 'route' => '/admin/staff'],
|
||||
[
|
||||
'code' => 'adminapp.ventas',
|
||||
'label' => 'Ventas',
|
||||
'parent_menu_code' => 'main.adminapp',
|
||||
'route' => '/admin/ventas',
|
||||
],
|
||||
[
|
||||
'code' => 'adminapp.staff',
|
||||
'label' => 'Staff',
|
||||
'parent_menu_code' => 'main.adminapp',
|
||||
'route' => '/admin/staff',
|
||||
],
|
||||
[
|
||||
'code' => 'account',
|
||||
'label' => 'Mi cuenta',
|
||||
@@ -135,9 +173,30 @@ class MenuSeeder extends Seeder
|
||||
->whereIn('code', ['profile', 'purchases', 'tickets'])
|
||||
->delete();
|
||||
|
||||
Menu::query()
|
||||
->whereIn('code', [
|
||||
'admin.event',
|
||||
'admin.catalog',
|
||||
'admin.combos',
|
||||
'admin.categories',
|
||||
'admin.ventas',
|
||||
'admin.staff',
|
||||
'event',
|
||||
'catalog',
|
||||
'combos',
|
||||
'categories',
|
||||
'ventas',
|
||||
'staff',
|
||||
])
|
||||
->delete();
|
||||
|
||||
$allRoleMenuCodes = Menu::query()->pluck('code');
|
||||
$adminAppMenuCodes = Menu::query()
|
||||
->where('code', 'main.adminapp')
|
||||
->orWhere('parent_menu_code', 'main.adminapp')
|
||||
->pluck('code');
|
||||
$userMenuCodes = Menu::query()
|
||||
->where('code', 'not like', 'admin.%')
|
||||
->whereNotIn('code', $adminAppMenuCodes)
|
||||
->pluck('code');
|
||||
|
||||
Role::query()
|
||||
|
||||
@@ -80,7 +80,7 @@ class ProductCatalogFromImagesSeeder extends Seeder
|
||||
throw new RuntimeException("Tenant 'sonder' not found.");
|
||||
}
|
||||
|
||||
$catalogPath = storage_path('public/catalog');
|
||||
$catalogPath = public_path('images/catalog');
|
||||
|
||||
if (! is_dir($catalogPath)) {
|
||||
throw new RuntimeException("Catalog directory not found at path: {$catalogPath}");
|
||||
|
||||
@@ -56,34 +56,34 @@ class TenantSeeder extends Seeder
|
||||
'success_color' => '#198754',
|
||||
'header_bg_color' => '#ffffff',
|
||||
'footer_bg_color' => '#313131',
|
||||
'header_logo' => $this->uploadedImage('images/sonder_header.png', 'sonder_header.png'),
|
||||
'footer_logo' => $this->uploadedImage('images/sonder_footer.png', 'sonder_footer.png'),
|
||||
'header_logo' => $this->uploadedImage('images/tennants/sonder/sonder_header.png', 'sonder_header.png'),
|
||||
'footer_logo' => $this->uploadedImage('images/tennants/sonder/sonder_footer.png', 'sonder_footer.png'),
|
||||
'social_media' => self::SOCIAL_MEDIA,
|
||||
'website_type_code' => 'shopit',
|
||||
'extras' => [
|
||||
'carousel' => [
|
||||
$this->uploadedImage(
|
||||
'images/sonder-main-carousel/01-urban-team.png',
|
||||
'images/tennants/sonder/sonder-main-carousel/01-urban-team.png',
|
||||
'01-urban-team.png',
|
||||
),
|
||||
$this->uploadedImage(
|
||||
'images/sonder-main-carousel/02-running-shoes.png',
|
||||
'images/tennants/sonder/sonder-main-carousel/02-running-shoes.png',
|
||||
'02-running-shoes.png',
|
||||
),
|
||||
$this->uploadedImage(
|
||||
'images/sonder-main-carousel/03-streetwear.png',
|
||||
'images/tennants/sonder/sonder-main-carousel/03-streetwear.png',
|
||||
'03-streetwear.png',
|
||||
),
|
||||
$this->uploadedImage(
|
||||
'images/sonder-main-carousel/04-football-training.png',
|
||||
'images/tennants/sonder/sonder-main-carousel/04-football-training.png',
|
||||
'04-football-training.png',
|
||||
),
|
||||
$this->uploadedImage(
|
||||
'images/sonder-main-carousel/05-activewear-essentials.png',
|
||||
'images/tennants/sonder/sonder-main-carousel/05-activewear-essentials.png',
|
||||
'05-activewear-essentials.png',
|
||||
),
|
||||
$this->uploadedImage(
|
||||
'images/sonder-main-carousel/06-city-runners.png',
|
||||
'images/tennants/sonder/sonder-main-carousel/06-city-runners.png',
|
||||
'06-city-runners.png',
|
||||
),
|
||||
],
|
||||
@@ -108,11 +108,11 @@ class TenantSeeder extends Seeder
|
||||
'header_bg_color' => '#ffffff',
|
||||
'footer_bg_color' => '#015327',
|
||||
'header_logo' => $this->uploadedImage(
|
||||
'images/futbol_infantil_header.png',
|
||||
'images/tennants/fiesta_futbol_infantil/futbol_infantil_header.png',
|
||||
'futbol_infantil_header.png',
|
||||
),
|
||||
'footer_logo' => $this->uploadedImage(
|
||||
'images/futbol_infantil_footer.png',
|
||||
'images/tennants/fiesta_futbol_infantil/futbol_infantil_footer.png',
|
||||
'futbol_infantil_footer.png',
|
||||
),
|
||||
'social_media' => self::SOCIAL_MEDIA,
|
||||
@@ -120,11 +120,10 @@ class TenantSeeder extends Seeder
|
||||
'extras' => [
|
||||
'heroConfig' => [
|
||||
'title_html' => '<h1>Fiesta Fútbol Infantil</h1>',
|
||||
'description_html' => '<p>Viví una jornada inolvidable de fútbol infantil.</p>',
|
||||
'button_text' => 'Comprar entradas',
|
||||
'button_href' => '/tickets',
|
||||
'background_image_id' => $this->uploadedImage(
|
||||
'images/futbol_infantil_hero.jpg',
|
||||
'images/tennants/fiesta_futbol_infantil/futbol_infantil_hero.jpg',
|
||||
'futbol_infantil_hero.jpg',
|
||||
),
|
||||
],
|
||||
@@ -133,10 +132,21 @@ class TenantSeeder extends Seeder
|
||||
'location' => 'Rosario, Santa Fe',
|
||||
'dates_text' => '9, 10, 11 y 12 de Octubre 2026',
|
||||
'dates' => [
|
||||
'2026-12-05',
|
||||
'2026-12-06',
|
||||
[
|
||||
'date' => '2026-12-05',
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '18:00',
|
||||
],
|
||||
[
|
||||
'date' => '2026-12-06',
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '18:00',
|
||||
],
|
||||
],
|
||||
],
|
||||
'additionalInfoConfig' => [
|
||||
'description' => '<p>Viví una jornada inolvidable de fútbol infantil.</p>',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2,21 +2,45 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use App\Domains\Tenant\Services\WebsiteTypeService;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use RuntimeException;
|
||||
|
||||
class WebsiteTypeSeeder extends Seeder
|
||||
{
|
||||
private const PRESENTATION = [
|
||||
'primary_color' => '#FF7006',
|
||||
'secondary_color' => '#777777',
|
||||
'danger_color' => '#E04A4A',
|
||||
'success_color' => '#81BC73',
|
||||
'warning_color' => '#81BC73',
|
||||
'body_color' => '#666666',
|
||||
'darker_body_color' => '#333333',
|
||||
'surface_color' => '#ffffff',
|
||||
'background_color' => '#f8f8f8',
|
||||
'border_color' => '#EAEAEA',
|
||||
];
|
||||
|
||||
public function __construct(private readonly WebsiteTypeService $websiteTypeService) {}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$shopIt = WebsiteType::query()->updateOrCreate(
|
||||
$shopIt = $this->websiteTypeService->updateOrCreate(
|
||||
['codigo' => 'shopit'],
|
||||
['nombre' => 'ShopIt'],
|
||||
[
|
||||
'nombre' => 'ShopIt',
|
||||
'dominio' => 'localhost',
|
||||
...self::PRESENTATION,
|
||||
'site_logo' => $this->onTicketLogo(),
|
||||
'footer_logo' => $this->onTicketFooterLogo(),
|
||||
],
|
||||
);
|
||||
|
||||
$shopIt->extras()->updateOrCreate(
|
||||
['nombre' => 'carousel'],
|
||||
['codigo' => 'carousel'],
|
||||
[
|
||||
'nombre' => 'Carrusel principal',
|
||||
'descripcion' => 'Listado de attachments que se muestran en el carousel principal.',
|
||||
'is_required' => false,
|
||||
'config_schema' => [
|
||||
@@ -30,22 +54,25 @@ class WebsiteTypeSeeder extends Seeder
|
||||
'attachment_type' => 'image',
|
||||
],
|
||||
],
|
||||
'database_rules' => [
|
||||
'$' => 'required|array',
|
||||
'$.*' => 'required|integer|distinct|exists:attachments,id',
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$onTicket = WebsiteType::query()->updateOrCreate(
|
||||
$onTicket = $this->websiteTypeService->updateOrCreate(
|
||||
['codigo' => 'onticket'],
|
||||
['nombre' => 'OnTicket'],
|
||||
[
|
||||
'nombre' => 'OnTicket',
|
||||
'dominio' => 'onticket.localhost',
|
||||
...self::PRESENTATION,
|
||||
'site_logo' => $this->onTicketLogo(),
|
||||
'footer_logo' => $this->onTicketFooterLogo(),
|
||||
],
|
||||
);
|
||||
|
||||
$onTicket->extras()->updateOrCreate(
|
||||
['nombre' => 'heroConfig'],
|
||||
['codigo' => 'heroConfig'],
|
||||
[
|
||||
'nombre' => 'Configuración del hero',
|
||||
'descripcion' => 'Configuracion del hero principal del evento.',
|
||||
'is_required' => false,
|
||||
'config_schema' => [
|
||||
@@ -63,21 +90,14 @@ class WebsiteTypeSeeder extends Seeder
|
||||
'attachment_type' => 'image',
|
||||
],
|
||||
],
|
||||
'database_rules' => [
|
||||
'$' => 'required|array',
|
||||
'title_html' => 'nullable|string',
|
||||
'description_html' => 'nullable|string',
|
||||
'button_text' => 'nullable|string',
|
||||
'button_href' => 'nullable|string',
|
||||
'background_image_id' => 'nullable|integer|exists:attachments,id',
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$onTicket->extras()->updateOrCreate(
|
||||
['nombre' => 'eventConfig'],
|
||||
['codigo' => 'eventConfig'],
|
||||
[
|
||||
'nombre' => 'Información del evento',
|
||||
'descripcion' => 'Informacion principal del evento.',
|
||||
'is_required' => false,
|
||||
'config_schema' => [
|
||||
@@ -87,19 +107,68 @@ class WebsiteTypeSeeder extends Seeder
|
||||
'location' => 'nullable|string',
|
||||
'dates_text' => 'nullable|string|max:255',
|
||||
'dates' => 'nullable|array',
|
||||
'dates.*' => 'required|date_format:Y-m-d|distinct',
|
||||
'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',
|
||||
'contact' => 'nullable|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',
|
||||
],
|
||||
'transforms' => [],
|
||||
'database_rules' => [
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$onTicket->extras()->updateOrCreate(
|
||||
['codigo' => 'additionalInfoConfig'],
|
||||
[
|
||||
'nombre' => 'Información adicional',
|
||||
'descripcion' => 'Información adicional visible en la página del evento.',
|
||||
'is_required' => false,
|
||||
'config_schema' => [
|
||||
'request_rules' => [
|
||||
'$' => 'required|array',
|
||||
'title' => 'nullable|string',
|
||||
'location' => 'nullable|string',
|
||||
'dates_text' => 'nullable|string|max:255',
|
||||
'dates' => 'nullable|array',
|
||||
'dates.*' => 'required|date_format:Y-m-d|distinct',
|
||||
'description' => 'nullable|string',
|
||||
],
|
||||
'transforms' => [],
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function onTicketLogo(): UploadedFile
|
||||
{
|
||||
$path = public_path('images/website_types/onticket_logo.png');
|
||||
|
||||
if (! file_exists($path)) {
|
||||
throw new RuntimeException("OnTicket logo not found at path: {$path}");
|
||||
}
|
||||
|
||||
return new UploadedFile(
|
||||
$path,
|
||||
'onticket_logo.png',
|
||||
'image/png',
|
||||
null,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private function onTicketFooterLogo(): UploadedFile
|
||||
{
|
||||
$path = public_path('images/website_types/onticket_footer_logo.png');
|
||||
|
||||
if (! file_exists($path)) {
|
||||
throw new RuntimeException("OnTicket footer logo not found at path: {$path}");
|
||||
}
|
||||
|
||||
return new UploadedFile(
|
||||
$path,
|
||||
'onticket_footer_logo.png',
|
||||
'image/png',
|
||||
null,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,9 @@ return [
|
||||
'schema_required' => 'The schema is required for static menus.',
|
||||
'not_found' => 'The specified menu does not exist.',
|
||||
],
|
||||
'tenant' => [
|
||||
'website_extra_toggled' => 'The section status was updated successfully.',
|
||||
],
|
||||
'errors' => [
|
||||
'forbidden' => 'You do not have permission to perform this action.',
|
||||
'not_found' => 'The requested resource was not found.',
|
||||
|
||||
@@ -94,6 +94,9 @@ return [
|
||||
'schema_required' => 'El schema es obligatorio para los menús estáticos.',
|
||||
'not_found' => 'El menú indicado no existe.',
|
||||
],
|
||||
'tenant' => [
|
||||
'website_extra_toggled' => 'El estado de la sección se actualizó correctamente.',
|
||||
],
|
||||
'errors' => [
|
||||
'forbidden' => 'No tienes permiso para realizar esta acción.',
|
||||
'not_found' => 'El recurso solicitado no fue encontrado.',
|
||||
|
||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 458 KiB After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 386 KiB After Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
public/images/website_types/onticket_footer_logo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/website_types/onticket_logo.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
130
tests/Feature/Auth/AdminAppLoginControllerTest.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Domains\Auth\Models\LoginAttempt;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Authorization\Enums\RoleCode;
|
||||
use App\Domains\Authorization\Models\Role;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminAppLoginControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_logs_in_a_tenant_bound_adminapp_user(): void
|
||||
{
|
||||
$tenant = $this->createTenant();
|
||||
$this->createRole(RoleCode::AdminApp);
|
||||
$user = User::factory()->create([
|
||||
'email' => 'admin@example.com',
|
||||
'password' => Hash::make('secret123'),
|
||||
'rol_codigo' => RoleCode::AdminApp->value,
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/adminapp/login', [
|
||||
'email' => ' ADMIN@EXAMPLE.COM ',
|
||||
'password' => 'secret123',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 'auth.login_success')
|
||||
->assertJsonPath('token_type', 'Bearer')
|
||||
->assertJsonPath('user.id', $user->id)
|
||||
->assertJsonPath('user.rol_codigo', RoleCode::AdminApp->value)
|
||||
->assertJsonPath('user.tenant_codigo', $tenant->codigo);
|
||||
|
||||
$this->assertNotEmpty($response->json('token'));
|
||||
$this->assertSame(['adminapp'], $user->tokens()->sole()->abilities);
|
||||
$this->assertDatabaseHas('login_attempts', [
|
||||
'user_id' => $user->id,
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'outcome' => LoginAttempt::OUTCOME_SUCCESS,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_rejects_non_adminapp_users_as_invalid_credentials(): void
|
||||
{
|
||||
$this->createRole(RoleCode::User);
|
||||
$user = User::factory()->create([
|
||||
'email' => 'customer@example.com',
|
||||
'password' => Hash::make('secret123'),
|
||||
'rol_codigo' => RoleCode::User->value,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/adminapp/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'secret123',
|
||||
])->assertUnprocessable()->assertJsonValidationErrors(['email']);
|
||||
|
||||
$this->assertDatabaseCount('personal_access_tokens', 0);
|
||||
$this->assertSame(0, $user->refresh()->failed_login_attempts);
|
||||
}
|
||||
|
||||
public function test_the_storefront_login_rejects_adminapp_users(): void
|
||||
{
|
||||
$tenant = $this->createTenant();
|
||||
$this->createRole(RoleCode::AdminApp);
|
||||
$user = User::factory()->create([
|
||||
'email' => 'admin@example.com',
|
||||
'password' => Hash::make('secret123'),
|
||||
'rol_codigo' => RoleCode::AdminApp->value,
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'secret123',
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
])->assertUnprocessable()->assertJsonValidationErrors(['email']);
|
||||
|
||||
$this->assertDatabaseCount('personal_access_tokens', 0);
|
||||
}
|
||||
|
||||
public function test_it_rejects_an_adminapp_user_without_a_tenant(): void
|
||||
{
|
||||
$this->createRole(RoleCode::AdminApp);
|
||||
$user = User::factory()->create([
|
||||
'email' => 'unbound@example.com',
|
||||
'password' => Hash::make('secret123'),
|
||||
'rol_codigo' => RoleCode::AdminApp->value,
|
||||
'tenant_codigo' => null,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/adminapp/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'secret123',
|
||||
])->assertUnprocessable()->assertJsonValidationErrors(['email']);
|
||||
|
||||
$this->assertDatabaseCount('personal_access_tokens', 0);
|
||||
}
|
||||
|
||||
public function test_it_validates_required_fields(): void
|
||||
{
|
||||
$this->postJson('/api/v1/adminapp/login', [])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['email', 'password']);
|
||||
}
|
||||
|
||||
private function createRole(RoleCode $role): Role
|
||||
{
|
||||
return Role::query()->create([
|
||||
'codigo' => $role->value,
|
||||
'nombre' => $role->value,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createTenant(): Tenant
|
||||
{
|
||||
return Tenant::query()->create([
|
||||
'codigo' => 'acme',
|
||||
'nombre' => 'Acme',
|
||||
'dominio' => 'acme.test',
|
||||
]);
|
||||
}
|
||||
}
|
||||
95
tests/Feature/Auth/AdminAppMeControllerTest.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Authorization\Enums\RoleCode;
|
||||
use App\Domains\Authorization\Models\Role;
|
||||
use App\Domains\Menu\Models\Menu;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminAppMeControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_authentication_is_required(): void
|
||||
{
|
||||
$this->getJson('/api/v1/adminapp/me')->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_a_customer_cannot_access_the_adminapp_context(): void
|
||||
{
|
||||
$customer = User::factory()->create([
|
||||
'rol_codigo' => $this->createRole(RoleCode::User)->codigo,
|
||||
'tenant_codigo' => null,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($customer);
|
||||
|
||||
$this->getJson('/api/v1/adminapp/me')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_it_returns_the_user_tenant_and_authorized_admin_menus(): void
|
||||
{
|
||||
$adminAppRole = $this->createRole(RoleCode::AdminApp);
|
||||
$tenant = $this->createTenant('acme');
|
||||
$otherTenant = $this->createTenant('other');
|
||||
|
||||
$catalog = $this->createMenu('admin.catalog', 'Catálogo', '/admin/catalog');
|
||||
$staff = $this->createMenu('admin.staff', 'Staff', '/admin/staff');
|
||||
$storefront = $this->createMenu('index', 'Inicio', '/');
|
||||
|
||||
$adminAppRole->menus()->sync([$catalog->code, $storefront->code]);
|
||||
$tenant->menues()->sync([$catalog->code, $staff->code, $storefront->code]);
|
||||
$otherTenant->menues()->sync([$staff->code]);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'nombre_apellido' => 'Admin Acme',
|
||||
'email' => 'admin@acme.test',
|
||||
'rol_codigo' => $adminAppRole->codigo,
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
]);
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$this->getJson('/api/v1/adminapp/me')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.user.id', $user->id)
|
||||
->assertJsonPath('data.user.email', 'admin@acme.test')
|
||||
->assertJsonPath('data.tenant.codigo', 'acme')
|
||||
->assertJsonCount(1, 'data.tenant.menues')
|
||||
->assertJsonPath('data.tenant.menues.0.code', 'admin.catalog')
|
||||
->assertJsonPath('data.tenant.menues.0.label', 'Catálogo')
|
||||
->assertJsonPath('data.tenant.menues.0.route', '/admin/catalog')
|
||||
->assertJsonMissing(['code' => 'admin.staff'])
|
||||
->assertJsonMissing(['code' => 'index']);
|
||||
}
|
||||
|
||||
private function createRole(RoleCode $role): Role
|
||||
{
|
||||
return Role::query()->create([
|
||||
'codigo' => $role->value,
|
||||
'nombre' => $role->value,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createTenant(string $code): Tenant
|
||||
{
|
||||
return Tenant::query()->create([
|
||||
'codigo' => $code,
|
||||
'nombre' => ucfirst($code),
|
||||
'dominio' => "{$code}.test",
|
||||
]);
|
||||
}
|
||||
|
||||
private function createMenu(string $code, string $label, string $route): Menu
|
||||
{
|
||||
return Menu::query()->create([
|
||||
'code' => $code,
|
||||
'label' => $label,
|
||||
'route' => $route,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,30 @@ class MenuSeederTest extends TestCase
|
||||
$this->seed(MenuSeeder::class);
|
||||
|
||||
$expectedMenus = [
|
||||
'admin.catalog' => ['Catálogo', '/admin/catalog'],
|
||||
'admin.categories' => ['Categorías', '/admin/categories'],
|
||||
'admin.combos' => ['Combos', '/admin/combos'],
|
||||
'admin.event' => ['Eventos', '/admin/event'],
|
||||
'admin.staff' => ['Staff', '/admin/staff'],
|
||||
'admin.ventas' => ['Ventas', '/admin/ventas'],
|
||||
'adminapp.catalog' => ['Catálogo', '/admin/catalog'],
|
||||
'adminapp.categories' => ['Categorías', '/admin/categories'],
|
||||
'adminapp.combos' => ['Combos', '/admin/combos'],
|
||||
'adminapp.event' => ['Eventos', '/admin/event'],
|
||||
'adminapp.staff' => ['Staff', '/admin/staff'],
|
||||
'adminapp.ventas' => ['Ventas', '/admin/ventas'],
|
||||
];
|
||||
|
||||
$adminApp = Menu::query()
|
||||
->with('children')
|
||||
->where('code', 'main.adminapp')
|
||||
->firstOrFail();
|
||||
|
||||
$this->assertSame('Administración', $adminApp->label);
|
||||
$this->assertSame(Menu::CONTENT_TYPE_DYNAMIC, $adminApp->content_type);
|
||||
$this->assertSame('/', $adminApp->route);
|
||||
$this->assertSame(
|
||||
array_keys($expectedMenus),
|
||||
$adminApp->children->pluck('code')->sort()->values()->all()
|
||||
);
|
||||
$this->assertTrue(
|
||||
$tenant->menues()->where('menues.code', 'main.adminapp')->exists()
|
||||
);
|
||||
|
||||
$adminMenus = Menu::query()
|
||||
->whereIn('code', array_keys($expectedMenus))
|
||||
->get()
|
||||
@@ -42,10 +58,28 @@ class MenuSeederTest extends TestCase
|
||||
foreach ($expectedMenus as $code => [$label, $route]) {
|
||||
$this->assertSame($label, $adminMenus->get($code)?->label);
|
||||
$this->assertSame($route, $adminMenus->get($code)?->route);
|
||||
$this->assertSame('main.adminapp', $adminMenus->get($code)?->parent_menu_code);
|
||||
$this->assertTrue(
|
||||
$tenant->menues()->where('menues.code', $code)->exists()
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertFalse(
|
||||
Menu::query()->whereIn('code', [
|
||||
'admin.event',
|
||||
'admin.catalog',
|
||||
'admin.combos',
|
||||
'admin.categories',
|
||||
'admin.ventas',
|
||||
'admin.staff',
|
||||
'event',
|
||||
'catalog',
|
||||
'combos',
|
||||
'categories',
|
||||
'ventas',
|
||||
'staff',
|
||||
])->exists()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_it_assigns_menus_to_the_expected_roles(): void
|
||||
@@ -57,8 +91,12 @@ class MenuSeederTest extends TestCase
|
||||
->orderBy('code')
|
||||
->pluck('code')
|
||||
->all();
|
||||
$adminAppMenuCodes = Menu::query()
|
||||
->where('code', 'main.adminapp')
|
||||
->orWhere('parent_menu_code', 'main.adminapp')
|
||||
->pluck('code');
|
||||
$userMenuCodes = Menu::query()
|
||||
->where('code', 'not like', 'admin.%')
|
||||
->whereNotIn('code', $adminAppMenuCodes)
|
||||
->orderBy('code')
|
||||
->pluck('code')
|
||||
->all();
|
||||
@@ -84,6 +122,8 @@ class MenuSeederTest extends TestCase
|
||||
->all();
|
||||
|
||||
$this->assertSame($userMenuCodes, $userRoleMenuCodes);
|
||||
$this->assertNotContains('main.adminapp', $userRoleMenuCodes);
|
||||
$this->assertNotContains('adminapp.catalog', $userRoleMenuCodes);
|
||||
$this->assertDatabaseCount(
|
||||
'roles_menues',
|
||||
(count($allMenuCodes) * 2) + count($userMenuCodes)
|
||||
|
||||
@@ -63,7 +63,7 @@ class TenantSeederTest extends TestCase
|
||||
$this->assertSame('shopit', $sonder->website_type_code);
|
||||
|
||||
$carousel = $sonder->websiteExtras
|
||||
->firstWhere('websiteTypeExtra.nombre', 'carousel');
|
||||
->firstWhere('websiteTypeExtra.codigo', 'carousel');
|
||||
|
||||
$this->assertNotNull($carousel);
|
||||
$this->assertCount(6, $carousel->config);
|
||||
@@ -82,14 +82,15 @@ class TenantSeederTest extends TestCase
|
||||
|
||||
$this->assertSame('onticket', $fiesta->website_type_code);
|
||||
|
||||
$extras = $fiesta->websiteExtras->keyBy('websiteTypeExtra.nombre');
|
||||
$extras = $fiesta->websiteExtras->keyBy('websiteTypeExtra.codigo');
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['heroConfig', 'eventConfig'],
|
||||
['heroConfig', 'eventConfig', 'additionalInfoConfig'],
|
||||
$extras->keys()->all(),
|
||||
);
|
||||
|
||||
$heroConfig = $extras->get('heroConfig')->config;
|
||||
$this->assertSame('<h1>Fiesta Fútbol Infantil</h1>', $heroConfig['title_html']);
|
||||
$this->assertArrayNotHasKey('description_html', $heroConfig);
|
||||
$this->assertIsInt($heroConfig['background_image_id']);
|
||||
$this->assertDatabaseHas('attachments', [
|
||||
'id' => $heroConfig['background_image_id'],
|
||||
@@ -101,9 +102,21 @@ class TenantSeederTest extends TestCase
|
||||
'location' => 'Rosario, Santa Fe',
|
||||
'dates_text' => '9, 10, 11 y 12 de Octubre 2026',
|
||||
'dates' => [
|
||||
'2026-12-05',
|
||||
'2026-12-06',
|
||||
[
|
||||
'date' => '2026-12-05',
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '18:00',
|
||||
],
|
||||
[
|
||||
'date' => '2026-12-06',
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '18:00',
|
||||
],
|
||||
],
|
||||
], $extras->get('eventConfig')->config);
|
||||
|
||||
$this->assertSame([
|
||||
'description' => '<p>Viví una jornada inolvidable de fútbol infantil.</p>',
|
||||
], $extras->get('additionalInfoConfig')->config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
namespace Tests\Feature\Seeders;
|
||||
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use Database\Seeders\WebsiteTypeSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WebsiteTypeSeederTest extends TestCase
|
||||
@@ -13,10 +15,26 @@ class WebsiteTypeSeederTest extends TestCase
|
||||
|
||||
public function test_it_seeds_website_types_and_their_config_schemas_idempotently(): void
|
||||
{
|
||||
Storage::fake('s3');
|
||||
|
||||
$this->seed(WebsiteTypeSeeder::class);
|
||||
$this->seed(WebsiteTypeSeeder::class);
|
||||
|
||||
$this->assertSame(2, WebsiteType::query()->count());
|
||||
$this->assertSame(4, Attachment::query()->count());
|
||||
|
||||
$expectedPresentation = [
|
||||
'primary_color' => '#FF7006',
|
||||
'secondary_color' => '#777777',
|
||||
'danger_color' => '#E04A4A',
|
||||
'success_color' => '#81BC73',
|
||||
'warning_color' => '#81BC73',
|
||||
'body_color' => '#666666',
|
||||
'darker_body_color' => '#333333',
|
||||
'surface_color' => '#ffffff',
|
||||
'background_color' => '#f8f8f8',
|
||||
'border_color' => '#EAEAEA',
|
||||
];
|
||||
|
||||
$shopIt = WebsiteType::query()
|
||||
->where('codigo', 'shopit')
|
||||
@@ -24,7 +42,14 @@ class WebsiteTypeSeederTest extends TestCase
|
||||
->sole();
|
||||
|
||||
$this->assertSame('ShopIt', $shopIt->nombre);
|
||||
$this->assertSame(['carousel'], $shopIt->extras->pluck('nombre')->all());
|
||||
$this->assertSame('localhost', $shopIt->dominio);
|
||||
$this->assertSame($expectedPresentation, $shopIt->only(array_keys($expectedPresentation)));
|
||||
$this->assertSame('onticket_logo.png', $shopIt->siteLogo->filename);
|
||||
Storage::disk('s3')->assertExists($shopIt->siteLogo->path);
|
||||
$this->assertSame('onticket_footer_logo.png', $shopIt->footerLogo->filename);
|
||||
Storage::disk('s3')->assertExists($shopIt->footerLogo->path);
|
||||
$this->assertSame(['carousel'], $shopIt->extras->pluck('codigo')->all());
|
||||
$this->assertSame('Carrusel principal', $shopIt->extras->sole()->nombre);
|
||||
$this->assertSame([
|
||||
'request_rules' => [
|
||||
'$' => 'required|array|max:10',
|
||||
@@ -36,10 +61,6 @@ class WebsiteTypeSeederTest extends TestCase
|
||||
'attachment_type' => 'image',
|
||||
],
|
||||
],
|
||||
'database_rules' => [
|
||||
'$' => 'required|array',
|
||||
'$.*' => 'required|integer|distinct|exists:attachments,id',
|
||||
],
|
||||
], $shopIt->extras->sole()->config_schema);
|
||||
|
||||
$onTicket = WebsiteType::query()
|
||||
@@ -48,12 +69,20 @@ class WebsiteTypeSeederTest extends TestCase
|
||||
->sole();
|
||||
|
||||
$this->assertSame('OnTicket', $onTicket->nombre);
|
||||
$this->assertSame('onticket.localhost', $onTicket->dominio);
|
||||
$this->assertSame($expectedPresentation, $onTicket->only(array_keys($expectedPresentation)));
|
||||
$this->assertSame('onticket_logo.png', $onTicket->siteLogo->filename);
|
||||
Storage::disk('s3')->assertExists($onTicket->siteLogo->path);
|
||||
$this->assertSame('onticket_footer_logo.png', $onTicket->footerLogo->filename);
|
||||
Storage::disk('s3')->assertExists($onTicket->footerLogo->path);
|
||||
$this->assertNotSame($shopIt->site_logo, $onTicket->site_logo);
|
||||
$this->assertNotSame($shopIt->footer_logo, $onTicket->footer_logo);
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['heroConfig', 'eventConfig'],
|
||||
$onTicket->extras->pluck('nombre')->all(),
|
||||
['heroConfig', 'eventConfig', 'additionalInfoConfig'],
|
||||
$onTicket->extras->pluck('codigo')->all(),
|
||||
);
|
||||
|
||||
$heroSchema = $onTicket->extras->firstWhere('nombre', 'heroConfig')->config_schema;
|
||||
$heroSchema = $onTicket->extras->firstWhere('codigo', 'heroConfig')->config_schema;
|
||||
$this->assertSame([
|
||||
'request_rules' => [
|
||||
'$' => 'required|array',
|
||||
@@ -69,17 +98,9 @@ class WebsiteTypeSeederTest extends TestCase
|
||||
'attachment_type' => 'image',
|
||||
],
|
||||
],
|
||||
'database_rules' => [
|
||||
'$' => 'required|array',
|
||||
'title_html' => 'nullable|string',
|
||||
'description_html' => 'nullable|string',
|
||||
'button_text' => 'nullable|string',
|
||||
'button_href' => 'nullable|string',
|
||||
'background_image_id' => 'nullable|integer|exists:attachments,id',
|
||||
],
|
||||
], $heroSchema);
|
||||
|
||||
$eventSchema = $onTicket->extras->firstWhere('nombre', 'eventConfig')->config_schema;
|
||||
$eventSchema = $onTicket->extras->firstWhere('codigo', 'eventConfig')->config_schema;
|
||||
$this->assertSame([
|
||||
'request_rules' => [
|
||||
'$' => 'required|array',
|
||||
@@ -87,17 +108,27 @@ class WebsiteTypeSeederTest extends TestCase
|
||||
'location' => 'nullable|string',
|
||||
'dates_text' => 'nullable|string|max:255',
|
||||
'dates' => 'nullable|array',
|
||||
'dates.*' => 'required|date_format:Y-m-d|distinct',
|
||||
'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',
|
||||
'contact' => 'nullable|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',
|
||||
],
|
||||
'transforms' => [],
|
||||
'database_rules' => [
|
||||
'$' => 'required|array',
|
||||
'title' => 'nullable|string',
|
||||
'location' => 'nullable|string',
|
||||
'dates_text' => 'nullable|string|max:255',
|
||||
'dates' => 'nullable|array',
|
||||
'dates.*' => 'required|date_format:Y-m-d|distinct',
|
||||
],
|
||||
], $eventSchema);
|
||||
|
||||
$additionalInfoSchema = $onTicket->extras
|
||||
->firstWhere('codigo', 'additionalInfoConfig')
|
||||
->config_schema;
|
||||
$this->assertSame([
|
||||
'request_rules' => [
|
||||
'$' => 'required|array',
|
||||
'description' => 'nullable|string',
|
||||
],
|
||||
'transforms' => [],
|
||||
], $additionalInfoSchema);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ class AdminAppWebsiteExtraControllerTest extends TestCase
|
||||
]);
|
||||
|
||||
$this->websiteType->extras()->create([
|
||||
'nombre' => 'contactConfig',
|
||||
'codigo' => 'contactConfig',
|
||||
'nombre' => 'Configuración de contacto',
|
||||
'descripcion' => 'Datos de contacto visibles en la tienda.',
|
||||
'is_required' => false,
|
||||
'config_schema' => [
|
||||
@@ -38,10 +39,6 @@ class AdminAppWebsiteExtraControllerTest extends TestCase
|
||||
'phone' => 'required|string|max:30',
|
||||
],
|
||||
'transforms' => [],
|
||||
'database_rules' => [
|
||||
'$' => 'required|array',
|
||||
'phone' => 'required|string|max:30',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -79,16 +76,59 @@ class AdminAppWebsiteExtraControllerTest extends TestCase
|
||||
$this->getJson('/api/v1/adminapp/tenant/website-extras')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.website_type.codigo', 'test-store')
|
||||
->assertJsonPath('data.definitions.contactConfig.codigo', 'contactConfig')
|
||||
->assertJsonPath('data.definitions.contactConfig.nombre', 'Configuración de contacto')
|
||||
->assertJsonPath('data.definitions.contactConfig.is_required', false)
|
||||
->assertJsonPath('data.definitions.contactConfig.is_enabled', null)
|
||||
->assertJsonPath('data.extras.contactConfig.phone', '+54 341 555 0101')
|
||||
->assertJsonPath('data.resolved_extras.contactConfig.phone', '+54 341 555 0101');
|
||||
}
|
||||
|
||||
public function test_adminapp_user_replaces_only_its_tenant_extras(): void
|
||||
public function test_adminapp_user_can_read_one_website_extra(): void
|
||||
{
|
||||
$tenant = $this->createTenant('acme');
|
||||
$definition = $this->websiteType->extras()->firstOrFail();
|
||||
$tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $definition->id,
|
||||
'config' => ['phone' => '+54 341 555 0101'],
|
||||
'is_enabled' => true,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||
|
||||
$this->getJson('/api/v1/adminapp/tenant/website-extras/contactConfig')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.codigo', 'contactConfig')
|
||||
->assertJsonPath('data.nombre', 'Configuración de contacto')
|
||||
->assertJsonPath('data.is_enabled', true)
|
||||
->assertJsonPath('data.config.phone', '+54 341 555 0101')
|
||||
->assertJsonPath('data.resolved_config.phone', '+54 341 555 0101');
|
||||
}
|
||||
|
||||
public function test_reading_an_unconfigured_website_extra_returns_not_found(): void
|
||||
{
|
||||
$tenant = $this->createTenant('acme');
|
||||
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||
|
||||
$this->getJson('/api/v1/adminapp/tenant/website-extras/contactConfig')
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_adminapp_user_updates_one_extra_without_touching_other_tenants(): void
|
||||
{
|
||||
$tenant = $this->createTenant('acme');
|
||||
$otherTenant = $this->createTenant('other');
|
||||
$definition = $this->websiteType->extras()->firstOrFail();
|
||||
$secondaryDefinition = $this->websiteType->extras()->create([
|
||||
'codigo' => 'footerConfig',
|
||||
'nombre' => 'Configuración del pie',
|
||||
'descripcion' => 'Configuración adicional del pie.',
|
||||
'is_required' => false,
|
||||
'config_schema' => [
|
||||
'request_rules' => ['$' => 'required|array'],
|
||||
'transforms' => [],
|
||||
],
|
||||
]);
|
||||
|
||||
$tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $definition->id,
|
||||
@@ -98,14 +138,16 @@ class AdminAppWebsiteExtraControllerTest extends TestCase
|
||||
'website_type_extra_id' => $definition->id,
|
||||
'config' => ['phone' => 'untouched'],
|
||||
]);
|
||||
$tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $secondaryDefinition->id,
|
||||
'config' => ['text' => 'also untouched'],
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||
|
||||
$this->putJson('/api/v1/adminapp/tenant/website-extras', [
|
||||
'extras' => [
|
||||
'contactConfig' => [
|
||||
'phone' => '+54 341 555 9999',
|
||||
],
|
||||
$this->putJson('/api/v1/adminapp/tenant/website-extras/contactConfig', [
|
||||
'config' => [
|
||||
'phone' => '+54 341 555 9999',
|
||||
],
|
||||
])
|
||||
->assertOk()
|
||||
@@ -119,20 +161,68 @@ class AdminAppWebsiteExtraControllerTest extends TestCase
|
||||
['phone' => 'untouched'],
|
||||
$otherTenant->websiteExtras()->firstOrFail()->config
|
||||
);
|
||||
$this->assertSame(
|
||||
['text' => 'also untouched'],
|
||||
$tenant->websiteExtras()
|
||||
->where('website_type_extra_id', $secondaryDefinition->id)
|
||||
->firstOrFail()
|
||||
->config
|
||||
);
|
||||
}
|
||||
|
||||
public function test_update_rejects_extras_not_supported_by_the_website_type(): void
|
||||
public function test_update_returns_not_found_for_an_unsupported_extra_code(): void
|
||||
{
|
||||
$tenant = $this->createTenant('acme');
|
||||
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||
|
||||
$this->putJson('/api/v1/adminapp/tenant/website-extras', [
|
||||
'extras' => [
|
||||
'unknown' => ['enabled' => true],
|
||||
],
|
||||
$this->putJson('/api/v1/adminapp/tenant/website-extras/unknown', [
|
||||
'config' => ['enabled' => true],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('extras');
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_adminapp_user_can_toggle_an_existing_website_extra(): void
|
||||
{
|
||||
$tenant = $this->createTenant('acme');
|
||||
$definition = $this->websiteType->extras()->firstOrFail();
|
||||
$websiteExtra = $tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $definition->id,
|
||||
'config' => ['phone' => '+54 341 555 0101'],
|
||||
'is_enabled' => false,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||
|
||||
$this->patchJson('/api/v1/adminapp/tenant/website-extras/contactConfig/toggle')
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 'tenant.website_extra_toggled')
|
||||
->assertJsonPath('message', 'El estado de la sección se actualizó correctamente.')
|
||||
->assertJsonPath('data.definitions.contactConfig.is_enabled', true);
|
||||
|
||||
$this->assertTrue($websiteExtra->refresh()->is_enabled);
|
||||
|
||||
$this->patchJson('/api/v1/adminapp/tenant/website-extras/contactConfig/toggle')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.definitions.contactConfig.is_enabled', false);
|
||||
|
||||
$this->assertFalse($websiteExtra->refresh()->is_enabled);
|
||||
}
|
||||
|
||||
public function test_toggle_enables_an_extra_without_a_tenant_value(): void
|
||||
{
|
||||
$tenant = $this->createTenant('acme');
|
||||
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||
|
||||
$this->patchJson('/api/v1/adminapp/tenant/website-extras/contactConfig/toggle')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.definitions.contactConfig.is_enabled', true)
|
||||
->assertJsonPath('data.extras.contactConfig', []);
|
||||
|
||||
$this->assertDatabaseHas('websites_extras', [
|
||||
'website_code' => $tenant->codigo,
|
||||
'website_type_extra_id' => $this->websiteType->extras()->firstOrFail()->id,
|
||||
'is_enabled' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createTenant(string $code): Tenant
|
||||
|
||||
63
tests/Feature/Tenant/BootstrapAdminAppControllerTest.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BootstrapAdminAppControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_publicly_bootstraps_the_admin_app_by_domain(): void
|
||||
{
|
||||
$footerLogo = Attachment::factory()->create();
|
||||
|
||||
WebsiteType::query()->create([
|
||||
'codigo' => 'shopit',
|
||||
'nombre' => 'ShopIt',
|
||||
'dominio' => 'admin.shopit.test',
|
||||
'primary_color' => '#112233',
|
||||
'secondary_color' => '#445566',
|
||||
'danger_color' => '#aa0000',
|
||||
'success_color' => '#00aa00',
|
||||
'warning_color' => '#ffaa00',
|
||||
'body_color' => '#666666',
|
||||
'darker_body_color' => '#333333',
|
||||
'surface_color' => '#ffffff',
|
||||
'background_color' => '#f8f8f8',
|
||||
'border_color' => '#eaeaea',
|
||||
'login_header_footer_color' => '#313131',
|
||||
'footer_logo' => $footerLogo->id,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/adminapp/bootstrap/ADMIN.SHOPIT.TEST')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.website_type_code', 'shopit')
|
||||
->assertJsonPath('data.primary_color', '#112233')
|
||||
->assertJsonPath('data.warning_color', '#ffaa00')
|
||||
->assertJsonPath('data.login_header_footer_color', '#313131')
|
||||
->assertJsonPath('data.site_logo', null)
|
||||
->assertJsonPath('data.footer_logo', $footerLogo->getTemporaryUrl(1440))
|
||||
->assertJsonMissingPath('data.codigo')
|
||||
->assertJsonMissingPath('data.nombre')
|
||||
->assertJsonMissingPath('data.dominio');
|
||||
}
|
||||
|
||||
public function test_it_returns_not_found_for_an_unknown_domain(): void
|
||||
{
|
||||
$this->getJson('/api/v1/adminapp/bootstrap/unknown.test')
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_it_rejects_an_invalid_domain(): void
|
||||
{
|
||||
$invalidDomain = str_repeat('a', 256);
|
||||
|
||||
$this->getJson("/api/v1/adminapp/bootstrap/{$invalidDomain}")
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['dominio']);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Domains\Authorization\Models\Role;
|
||||
use App\Domains\Catalog\Models\Category;
|
||||
use App\Domains\Menu\Models\Menu;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -137,6 +138,45 @@ class BootstrapTenantControllerTest extends TestCase
|
||||
->assertJsonMissing(['nombre' => 'Global']);
|
||||
}
|
||||
|
||||
public function test_it_returns_only_enabled_website_extras_in_the_bootstrap(): void
|
||||
{
|
||||
$tenant = $this->createTenant();
|
||||
$websiteType = WebsiteType::query()->create([
|
||||
'codigo' => 'store',
|
||||
'nombre' => 'Tienda',
|
||||
]);
|
||||
$tenant->update(['website_type_code' => $websiteType->codigo]);
|
||||
|
||||
$enabledExtra = $websiteType->extras()->create([
|
||||
'codigo' => 'contact',
|
||||
'nombre' => 'Contacto',
|
||||
'descripcion' => 'Datos de contacto.',
|
||||
'config_schema' => [],
|
||||
]);
|
||||
$disabledExtra = $websiteType->extras()->create([
|
||||
'codigo' => 'banner',
|
||||
'nombre' => 'Banner',
|
||||
'descripcion' => 'Banner promocional.',
|
||||
'config_schema' => [],
|
||||
]);
|
||||
$tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $enabledExtra->id,
|
||||
'config' => ['phone' => '+54 341 555 0101'],
|
||||
'is_enabled' => true,
|
||||
]);
|
||||
$tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $disabledExtra->id,
|
||||
'config' => ['title' => 'No mostrar'],
|
||||
'is_enabled' => false,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/tenants/bootstrap/acme.com')
|
||||
->assertOk()
|
||||
->assertJsonMissingPath('data.website_type')
|
||||
->assertJsonPath('data.extras.contact.phone', '+54 341 555 0101')
|
||||
->assertJsonMissingPath('data.extras.banner');
|
||||
}
|
||||
|
||||
public function test_it_returns_not_found_when_the_domain_does_not_exist(): void
|
||||
{
|
||||
$response = $this->getJson('/api/tenants/bootstrap/missing.example');
|
||||
|
||||
@@ -31,7 +31,18 @@ class StoreTenantWithExtrasTest extends TestCase
|
||||
'title' => 'Festival',
|
||||
'location' => 'Buenos Aires',
|
||||
'dates_text' => '10 y 11 de octubre de 2026',
|
||||
'dates' => ['2026-10-10', '2026-10-11'],
|
||||
'dates' => [
|
||||
[
|
||||
'date' => '2026-10-10',
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '18:00',
|
||||
],
|
||||
[
|
||||
'date' => '2026-10-11',
|
||||
'start_time' => '10:00',
|
||||
'end_time' => '17:00',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]));
|
||||
@@ -39,7 +50,7 @@ class StoreTenantWithExtrasTest extends TestCase
|
||||
$response
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.website_type_code', 'onticket')
|
||||
->assertJsonPath('data.website_type.codigo', 'onticket')
|
||||
->assertJsonMissingPath('data.website_type')
|
||||
->assertJsonPath('data.extras.eventConfig.title', 'Festival');
|
||||
|
||||
$tenant = Tenant::query()->where('codigo', 'festival')->sole();
|
||||
@@ -52,7 +63,18 @@ class StoreTenantWithExtrasTest extends TestCase
|
||||
'title' => 'Festival',
|
||||
'location' => 'Buenos Aires',
|
||||
'dates_text' => '10 y 11 de octubre de 2026',
|
||||
'dates' => ['2026-10-10', '2026-10-11'],
|
||||
'dates' => [
|
||||
[
|
||||
'date' => '2026-10-10',
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '18:00',
|
||||
],
|
||||
[
|
||||
'date' => '2026-10-11',
|
||||
'start_time' => '10:00',
|
||||
'end_time' => '17:00',
|
||||
],
|
||||
],
|
||||
],
|
||||
$tenant->websiteExtras()->sole()->config
|
||||
);
|
||||
@@ -80,12 +102,16 @@ class StoreTenantWithExtrasTest extends TestCase
|
||||
'website_type_code' => 'onticket',
|
||||
'extras' => [
|
||||
'eventConfig' => [
|
||||
'dates' => ['not-a-date'],
|
||||
'dates' => [[
|
||||
'date' => 'not-a-date',
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '18:00',
|
||||
]],
|
||||
],
|
||||
],
|
||||
]))
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['extras.eventConfig.dates.0']);
|
||||
->assertJsonValidationErrors(['extras.eventConfig.dates.0.date']);
|
||||
|
||||
$this->assertDatabaseMissing('tenants', ['codigo' => 'festival']);
|
||||
}
|
||||
@@ -153,7 +179,7 @@ class StoreTenantWithExtrasTest extends TestCase
|
||||
->websiteExtras()
|
||||
->whereHas(
|
||||
'websiteTypeExtra',
|
||||
fn ($query) => $query->where('nombre', 'heroConfig')
|
||||
fn ($query) => $query->where('codigo', 'heroConfig')
|
||||
)
|
||||
->sole()
|
||||
->config;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Domains\Attachable\Enums\AttachmentType;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -20,6 +22,19 @@ class WebsiteExtrasTest extends TestCase
|
||||
'id',
|
||||
'codigo',
|
||||
'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',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
], Schema::getColumnListing('website_type'));
|
||||
@@ -27,6 +42,7 @@ class WebsiteExtrasTest extends TestCase
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'id',
|
||||
'website_type_code',
|
||||
'codigo',
|
||||
'nombre',
|
||||
'descripcion',
|
||||
'is_required',
|
||||
@@ -40,6 +56,7 @@ class WebsiteExtrasTest extends TestCase
|
||||
'website_code',
|
||||
'website_type_extra_id',
|
||||
'config',
|
||||
'is_enabled',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
], Schema::getColumnListing('websites_extras'));
|
||||
@@ -47,11 +64,32 @@ class WebsiteExtrasTest extends TestCase
|
||||
|
||||
public function test_models_map_the_diagram_relations_and_casts(): void
|
||||
{
|
||||
$siteLogo = Attachment::query()->create([
|
||||
'key' => '92bb3986-fb22-4871-b98c-fc5663d78aa6',
|
||||
'path' => 'website-types/site-logo.png',
|
||||
'filename' => 'site-logo.png',
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
$type = WebsiteType::query()->create([
|
||||
'codigo' => 'store',
|
||||
'nombre' => 'Tienda',
|
||||
'dominio' => 'store.test',
|
||||
'primary_color' => '#111111',
|
||||
'secondary_color' => '#222222',
|
||||
'danger_color' => '#ff0000',
|
||||
'success_color' => '#00aa55',
|
||||
'warning_color' => '#ffaa00',
|
||||
'body_color' => '#666666',
|
||||
'darker_body_color' => '#333333',
|
||||
'surface_color' => '#ffffff',
|
||||
'background_color' => '#f8f8f8',
|
||||
'border_color' => '#dddddd',
|
||||
'login_header_footer_color' => '#333333',
|
||||
'site_logo' => $siteLogo->id,
|
||||
]);
|
||||
$typeExtra = $type->extras()->create([
|
||||
'codigo' => 'whatsapp',
|
||||
'nombre' => 'WhatsApp',
|
||||
'descripcion' => 'Configuracion del canal de WhatsApp',
|
||||
'is_required' => true,
|
||||
@@ -65,10 +103,25 @@ class WebsiteExtrasTest extends TestCase
|
||||
$websiteExtra = $tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $typeExtra->id,
|
||||
'config' => ['phone' => '+5491112345678'],
|
||||
'is_enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertTrue($type->extras()->firstOrFail()->is($typeExtra));
|
||||
$this->assertSame('store.test', $type->dominio);
|
||||
$this->assertSame('#111111', $type->primary_color);
|
||||
$this->assertSame('#222222', $type->secondary_color);
|
||||
$this->assertSame('#ff0000', $type->danger_color);
|
||||
$this->assertSame('#00aa55', $type->success_color);
|
||||
$this->assertSame('#ffaa00', $type->warning_color);
|
||||
$this->assertSame('#666666', $type->body_color);
|
||||
$this->assertSame('#333333', $type->darker_body_color);
|
||||
$this->assertSame('#ffffff', $type->surface_color);
|
||||
$this->assertSame('#f8f8f8', $type->background_color);
|
||||
$this->assertSame('#dddddd', $type->border_color);
|
||||
$this->assertSame('#333333', $type->login_header_footer_color);
|
||||
$this->assertTrue($type->siteLogo()->firstOrFail()->is($siteLogo));
|
||||
$this->assertSame($type->codigo, $typeExtra->website_type_code);
|
||||
$this->assertSame('whatsapp', $typeExtra->codigo);
|
||||
$this->assertTrue($type->tenants()->firstOrFail()->is($tenant));
|
||||
$this->assertTrue($tenant->websiteType()->firstOrFail()->is($type));
|
||||
$this->assertSame($type->codigo, $tenant->website_type_code);
|
||||
@@ -82,6 +135,7 @@ class WebsiteExtrasTest extends TestCase
|
||||
'required' => ['phone'],
|
||||
], $typeExtra->config_schema);
|
||||
$this->assertSame(['phone' => '+5491112345678'], $websiteExtra->config);
|
||||
$this->assertTrue($websiteExtra->is_enabled);
|
||||
}
|
||||
|
||||
public function test_deleting_a_type_cascades_its_definitions_and_website_values(): void
|
||||
@@ -91,6 +145,7 @@ class WebsiteExtrasTest extends TestCase
|
||||
'nombre' => 'Tienda',
|
||||
]);
|
||||
$typeExtra = $type->extras()->create([
|
||||
'codigo' => 'whatsapp',
|
||||
'nombre' => 'WhatsApp',
|
||||
'descripcion' => 'Configuracion del canal de WhatsApp',
|
||||
'config_schema' => ['type' => 'object'],
|
||||
|
||||
62
tests/Feature/Tenant/WebsiteTypeServiceTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Domains\Tenant\Services\WebsiteTypeService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WebsiteTypeServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_creates_a_website_type_and_uploads_its_logos_to_s3(): void
|
||||
{
|
||||
Storage::fake('s3');
|
||||
|
||||
$websiteType = app(WebsiteTypeService::class)->create([
|
||||
'codigo' => 'marketplace',
|
||||
'nombre' => 'Marketplace',
|
||||
'dominio' => 'marketplace.test',
|
||||
'primary_color' => '#111111',
|
||||
'secondary_color' => '#222222',
|
||||
'danger_color' => '#ff0000',
|
||||
'success_color' => '#00aa55',
|
||||
'warning_color' => '#ffaa00',
|
||||
'body_color' => '#666666',
|
||||
'darker_body_color' => '#333333',
|
||||
'surface_color' => '#ffffff',
|
||||
'background_color' => '#f8f8f8',
|
||||
'border_color' => '#dddddd',
|
||||
'login_header_footer_color' => '#333333',
|
||||
'site_logo' => UploadedFile::fake()->image('site-logo.png'),
|
||||
'footer_logo' => UploadedFile::fake()->image('footer-logo.png'),
|
||||
]);
|
||||
|
||||
$siteLogo = $websiteType->siteLogo()->firstOrFail();
|
||||
$footerLogo = $websiteType->footerLogo()->firstOrFail();
|
||||
|
||||
$this->assertSame('marketplace', $websiteType->codigo);
|
||||
$this->assertSame($siteLogo->id, $websiteType->site_logo);
|
||||
$this->assertSame($footerLogo->id, $websiteType->footer_logo);
|
||||
$this->assertStringStartsWith('website-types/', $siteLogo->path);
|
||||
$this->assertStringStartsWith('website-types/', $footerLogo->path);
|
||||
Storage::disk('s3')->assertExists($siteLogo->path);
|
||||
Storage::disk('s3')->assertExists($footerLogo->path);
|
||||
$this->assertDatabaseHas('website_type', [
|
||||
'codigo' => 'marketplace',
|
||||
'dominio' => 'marketplace.test',
|
||||
'warning_color' => '#ffaa00',
|
||||
'body_color' => '#666666',
|
||||
'darker_body_color' => '#333333',
|
||||
'surface_color' => '#ffffff',
|
||||
'background_color' => '#f8f8f8',
|
||||
'border_color' => '#dddddd',
|
||||
'login_header_footer_color' => '#333333',
|
||||
'site_logo' => $siteLogo->id,
|
||||
'footer_logo' => $footerLogo->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
25
tests/Unit/Tenant/BootstrapAdminAppResourceTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Tenant;
|
||||
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use App\Domains\Tenant\Resources\AdminApp\BootstrapAdminAppResource;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BootstrapAdminAppResourceTest extends TestCase
|
||||
{
|
||||
public function test_it_exposes_the_website_type_code(): void
|
||||
{
|
||||
$websiteType = new WebsiteType([
|
||||
'codigo' => 'shopit',
|
||||
'nombre' => 'ShopIt',
|
||||
'dominio' => 'admin.shopit.test',
|
||||
]);
|
||||
$websiteType->setRelation('siteLogo', null);
|
||||
|
||||
$data = BootstrapAdminAppResource::make($websiteType)->resolve(request());
|
||||
|
||||
$this->assertSame('shopit', $data['codigo']);
|
||||
$this->assertSame('shopit', $data['website_type_code']);
|
||||
}
|
||||
}
|
||||