feat(brand): implement update functionality and validation for brand details

This commit is contained in:
2026-09-29 15:49:18 -03:00
parent a9d9854e5e
commit 3b6d6bf28b
6 changed files with 290 additions and 3 deletions

View File

@@ -2,17 +2,30 @@
namespace App\Domains\Core\Tenant\Controllers\AdminApp;
use App\Domains\Core\Tenant\Requests\AdminApp\UpdateBrandRequest;
use App\Domains\Core\Tenant\Resources\AdminApp\BrandResource;
use App\Domains\Core\Tenant\Services\BrandService;
use App\Domains\Core\Tenant\Services\TenantInformationService;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class BrandController extends Controller
{
public function __invoke(Request $request, TenantInformationService $tenantInformationService): BrandResource
public function show(Request $request, TenantInformationService $tenantInformationService): BrandResource
{
$tenant = $request->user()->tenant()->firstOrFail();
return BrandResource::make($tenantInformationService->load($tenant));
}
public function update(
UpdateBrandRequest $request,
BrandService $brandService,
TenantInformationService $tenantInformationService,
): BrandResource {
$tenant = $request->user()->tenant()->firstOrFail();
$brandService->update($tenant, $request->validated());
return BrandResource::make($tenantInformationService->load($tenant->fresh()));
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Domains\Core\Tenant\Requests\AdminApp;
use App\Shared\Rules\CroppedImageOrBase64Rule;
use App\Shared\Rules\ImageOrBase64Rule;
use Illuminate\Foundation\Http\FormRequest;
class UpdateBrandRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
$shopit = $this->user()->tenant()->firstOrFail()->storefront_website_type_code === 'shopit';
return [
'nombre' => ['required', 'string', 'max:255'],
'primary_color' => ['required', 'regex:/^#[0-9a-fA-F]{6}$/'],
'footer_bg_color' => ['required', 'regex:/^#[0-9a-fA-F]{6}$/'],
'header_logo' => ['present', 'nullable', new ImageOrBase64Rule],
'footer_logo' => ['present', 'nullable', new ImageOrBase64Rule],
'banner_images' => [$shopit ? 'present' : 'prohibited', 'array', 'max:10'],
'banner_images.*' => ['required', new CroppedImageOrBase64Rule],
];
}
}

View File

@@ -20,7 +20,10 @@ class BrandResource extends JsonResource
'header_bg_color' => $this->header_bg_color,
'footer_color' => $this->footerColor(),
'header_logo' => $this->headerLogo?->getTemporaryUrl(1440),
'header_logo_key' => $this->headerLogo?->key,
'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440),
'footer_logo_key' => $this->footerLogo?->key,
'can_edit_carousel' => $this->canEditCarousel(),
'banner_images' => $this->bannerImages(),
];
}
@@ -48,7 +51,7 @@ class BrandResource extends JsonResource
/** @return array<int, array<string, mixed>> */
private function bannerImages(): array
{
if ($this->storefront_website_type_code !== 'shopit') {
if (! $this->canEditCarousel()) {
return [];
}
@@ -73,6 +76,7 @@ class BrandResource extends JsonResource
->firstWhere('variant', AttachmentCrop::DESKTOP);
return [
'key' => $image->key,
'url' => ($desktopCrop?->croppedAttachment ?? $image)->getTemporaryUrl(1440),
'original_url' => $image->getTemporaryUrl(1440),
'crop' => $desktopCrop === null ? null : [
@@ -84,4 +88,17 @@ class BrandResource extends JsonResource
->values()
->all();
}
private function canEditCarousel(): bool
{
if ($this->storefront_website_type_code !== 'shopit') {
return false;
}
$extra = $this->websiteExtras->first(
fn ($extra) => $extra->websiteTypeExtra->codigo === 'carousel'
);
return $extra === null || $extra->is_enabled;
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Domains\Core\Tenant\Services;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Services\AttachmentService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Throwable;
class BrandService
{
public function __construct(
private readonly AttachmentService $attachmentService,
private readonly WebsiteExtraService $websiteExtraService,
) {}
/** @param array<string, mixed> $data */
public function update(Tenant $tenant, array $data): void
{
$carousel = $tenant->storefront_website_type_code === 'shopit'
? $tenant->websiteExtras()->whereHas('websiteTypeExtra',
fn ($query) => $query->where('codigo', 'carousel'))->first()
: null;
$allowedBannerIds = is_array($carousel?->config) ? $carousel->config : [];
if ($carousel !== null && ! $carousel->is_enabled && ($data['banner_images'] ?? []) !== []) {
throw ValidationException::withMessages(['banner_images' => 'El carrusel está deshabilitado.']);
}
$seen = [];
foreach (['header_logo' => $tenant->header_logo_id, 'footer_logo' => $tenant->footer_logo_id] as $field => $id) {
$this->existingImage($data[$field], $id === null ? [] : [$id], $field);
}
foreach ($data['banner_images'] ?? [] as $index => $item) {
$image = is_array($item) ? $item['image'] : $item;
$attachment = $this->existingImage($image, $allowedBannerIds, "banner_images.{$index}");
if ($attachment !== null) {
if (isset($seen[$attachment->id])) {
throw ValidationException::withMessages(["banner_images.{$index}" => 'La imagen está repetida.']);
}
$seen[$attachment->id] = true;
}
}
$created = [];
try {
DB::transaction(function () use ($tenant, $data, $carousel, &$created): void {
$header = $this->logo($data['header_logo'], $tenant->header_logo_id, $tenant, $created);
$footer = $this->logo($data['footer_logo'], $tenant->footer_logo_id, $tenant, $created);
$tenant->update([
'nombre' => trim($data['nombre']),
'primary_color' => $data['primary_color'],
'footer_bg_color' => $data['footer_bg_color'],
'header_logo_id' => $header?->id,
'footer_logo_id' => $footer?->id,
]);
if ($tenant->storefront_website_type_code !== 'shopit'
|| ($carousel !== null && ! $carousel->is_enabled)) {
return;
}
$definition = $this->websiteExtraService->definitionForTenant($tenant, 'carousel');
$ids = [];
$recrops = [];
foreach ($data['banner_images'] as $item) {
$image = is_array($item) ? $item['image'] : $item;
$attachment = $this->existingImage($image, $carousel?->config ?? [], 'banner_images');
$crops = $this->crops($item);
if ($attachment === null) {
$attachment = $crops === null
? $this->attachmentService->store($image, "tenants/{$tenant->codigo}/extras/carousel")
: $this->attachmentService->storeCroppedImageVariants(
$image, "tenants/{$tenant->codigo}/extras/carousel", $crops
);
$created[] = $attachment;
} elseif ($crops !== null) {
$recrops[] = [$attachment, $crops];
}
$ids[] = $attachment->id;
}
$tenant->websiteExtras()->updateOrCreate(
['website_type_extra_id' => $definition->id],
['config' => $ids]
);
foreach ($recrops as [$attachment, $crops]) {
$this->attachmentService->updateImageCropVariants($attachment, $crops);
}
});
} catch (Throwable $error) {
foreach ($created as $attachment) {
$paths = [$attachment->path];
foreach ($attachment->cropVariants as $crop) {
if ($crop->croppedAttachment !== null) {
$paths[] = $crop->croppedAttachment->path;
}
}
Storage::disk('s3')->delete($paths);
}
throw $error;
}
}
/** @param array<int, int> $allowedIds */
private function existingImage(mixed $value, array $allowedIds, string $field): ?Attachment
{
if (! is_string($value) || ! Str::isUuid($value)) {
return null;
}
$attachment = Attachment::query()->where('key', $value)->first();
if ($attachment === null || $attachment->type !== AttachmentType::Image
|| ! in_array($attachment->id, $allowedIds, true)) {
throw ValidationException::withMessages([$field => 'La imagen no pertenece a esta marca.']);
}
return $attachment;
}
/** @param array<int, Attachment> $created */
private function logo(mixed $value, ?int $currentId, Tenant $tenant, array &$created): ?Attachment
{
if ($value === null) {
return null;
}
$existing = $this->existingImage($value, $currentId === null ? [] : [$currentId], 'logo');
if ($existing !== null) {
return $existing;
}
$attachment = $this->attachmentService->store($value, "tenants/{$tenant->codigo}/logos");
$created[] = $attachment;
return $attachment;
}
/** @return array<string, array<string, mixed>>|null */
private function crops(mixed $item): ?array
{
if (! is_array($item)) {
return null;
}
if (! isset($item['crop_horizontal'], $item['crop_vertical'])) {
return null;
}
$crop = [
'crop_horizontal' => $item['crop_horizontal'],
'crop_vertical' => $item['crop_vertical'],
];
return ['desktop' => $crop, 'mobile' => $crop];
}
}

View File

@@ -7,7 +7,8 @@ use Illuminate\Support\Facades\Route;
Route::prefix('v1/adminapp/tenant')
->middleware(['auth:sanctum', 'adminapp.tenant'])
->group(function (): void {
Route::get('brand', BrandController::class);
Route::get('brand', [BrandController::class, 'show']);
Route::put('brand', [BrandController::class, 'update']);
Route::get('website-extras', [WebsiteExtraController::class, 'show']);
Route::get('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'showExtra']);
Route::put('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'update']);