5 Commits

19 changed files with 476 additions and 148 deletions

View File

@@ -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,14 +26,42 @@ 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->updateForTenant(
$tenant,
$websiteExtraCode,
$request->validated('config')
);
return WebsiteExtrasResource::make(
$this->loadTenant($request->user())
);
}
public function toggle(Request $request, string $websiteExtraCode): WebsiteExtrasResource
{
$tenant = $request->user()->tenant()->firstOrFail();
$this->websiteExtraService->replaceForTenant(
$tenant,
$request->validated('extras', [])
);
$this->websiteExtraService->toggleForTenant($tenant, $websiteExtraCode);
return WebsiteExtrasResource::make(
$this->loadTenant($request->user())

View File

@@ -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',
];
}

View File

@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'website_type_code',
'codigo',
'nombre',
'descripcion',
'is_required',

View File

@@ -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')
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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)
),

View File

@@ -41,11 +41,13 @@ class TenantResource extends JsonResource
),
'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),

View File

@@ -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,72 @@ 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()
->firstOrFail();
$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 +221,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 +255,8 @@ class WebsiteExtraService
$definition,
$path,
$value,
$transform
$transform,
$requestRoot
)
);
}
@@ -245,7 +310,8 @@ class WebsiteExtraService
WebsiteTypeExtra $definition,
string $path,
mixed $value,
array $transform
array $transform,
string $requestRoot
): mixed {
if ($value === null) {
return null;
@@ -253,7 +319,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 +328,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 +336,7 @@ class WebsiteExtraService
} else {
$attachment = $this->attachmentService->store(
$value,
"tenants/{$tenant->codigo}/extras/{$definition->nombre}"
"tenants/{$tenant->codigo}/extras/{$definition->codigo}"
);
}
@@ -282,7 +348,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 +356,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);
}
}
}

View File

@@ -6,8 +6,8 @@ use Illuminate\Support\Facades\Route;
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']);
});

View File

@@ -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');
});
}
};

View File

@@ -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.
}
};

View File

@@ -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');
});
}
};

View File

@@ -15,8 +15,9 @@ class WebsiteTypeSeeder extends Seeder
);
$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,10 +31,6 @@ class WebsiteTypeSeeder extends Seeder
'attachment_type' => 'image',
],
],
'database_rules' => [
'$' => 'required|array',
'$.*' => 'required|integer|distinct|exists:attachments,id',
],
],
],
);
@@ -44,8 +41,9 @@ class WebsiteTypeSeeder extends Seeder
);
$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 +61,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' => [
@@ -90,14 +81,6 @@ class WebsiteTypeSeeder extends Seeder
'dates.*' => 'required|date_format:Y-m-d|distinct',
],
'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',
],
],
],
);

View File

@@ -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,7 +82,7 @@ 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'],
$extras->keys()->all(),

View File

@@ -24,7 +24,8 @@ class WebsiteTypeSeederTest extends TestCase
->sole();
$this->assertSame('ShopIt', $shopIt->nombre);
$this->assertSame(['carousel'], $shopIt->extras->pluck('nombre')->all());
$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 +37,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()
@@ -50,10 +47,10 @@ class WebsiteTypeSeederTest extends TestCase
$this->assertSame('OnTicket', $onTicket->nombre);
$this->assertEqualsCanonicalizing(
['heroConfig', 'eventConfig'],
$onTicket->extras->pluck('nombre')->all(),
$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 +66,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',
@@ -90,14 +79,6 @@ class WebsiteTypeSeederTest extends TestCase
'dates.*' => 'required|date_format:Y-m-d|distinct',
],
'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);
}
}

View File

@@ -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,58 @@ 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('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_returns_not_found_for_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')
->assertNotFound();
}
private function createTenant(string $code): Tenant

View File

@@ -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,44 @@ 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()
->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');

View File

@@ -153,7 +153,7 @@ class StoreTenantWithExtrasTest extends TestCase
->websiteExtras()
->whereHas(
'websiteTypeExtra',
fn ($query) => $query->where('nombre', 'heroConfig')
fn ($query) => $query->where('codigo', 'heroConfig')
)
->sole()
->config;

View File

@@ -27,6 +27,7 @@ class WebsiteExtrasTest extends TestCase
$this->assertEqualsCanonicalizing([
'id',
'website_type_code',
'codigo',
'nombre',
'descripcion',
'is_required',
@@ -40,6 +41,7 @@ class WebsiteExtrasTest extends TestCase
'website_code',
'website_type_extra_id',
'config',
'is_enabled',
'created_at',
'updated_at',
], Schema::getColumnListing('websites_extras'));
@@ -52,6 +54,7 @@ class WebsiteExtrasTest extends TestCase
'nombre' => 'Tienda',
]);
$typeExtra = $type->extras()->create([
'codigo' => 'whatsapp',
'nombre' => 'WhatsApp',
'descripcion' => 'Configuracion del canal de WhatsApp',
'is_required' => true,
@@ -65,10 +68,12 @@ 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($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 +87,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 +97,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'],