feat(tenant): add social media functionality with validation, synchronization, and tests
This commit is contained in:
@@ -16,7 +16,7 @@ class BootstrapTenantController extends Controller
|
|||||||
|
|
||||||
return TenantResource::make(
|
return TenantResource::make(
|
||||||
Tenant::query()
|
Tenant::query()
|
||||||
->with(['headerLogo', 'footerLogo', 'mainCarouselImages', 'menues'])
|
->with(['headerLogo', 'footerLogo', 'mainCarouselImages', 'menues', 'socialMedia'])
|
||||||
->where('dominio', $dominio)
|
->where('dominio', $dominio)
|
||||||
->firstOrFail()
|
->firstOrFail()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class TenantController extends Controller
|
|||||||
{
|
{
|
||||||
return TenantResource::collection(
|
return TenantResource::collection(
|
||||||
Tenant::query()
|
Tenant::query()
|
||||||
->with(['headerLogo', 'footerLogo', 'mainCarouselImages'])
|
->with(['headerLogo', 'footerLogo', 'mainCarouselImages', 'socialMedia'])
|
||||||
->latest()
|
->latest()
|
||||||
->paginateFromRequest()
|
->paginateFromRequest()
|
||||||
)->response();
|
)->response();
|
||||||
@@ -30,14 +30,14 @@ class TenantController extends Controller
|
|||||||
$tenant = $this->tenantService->create($request->validated());
|
$tenant = $this->tenantService->create($request->validated());
|
||||||
|
|
||||||
return TenantResource::make(
|
return TenantResource::make(
|
||||||
$tenant->loadMissing(['headerLogo', 'footerLogo', 'mainCarouselImages'])
|
$tenant->loadMissing(['headerLogo', 'footerLogo', 'mainCarouselImages', 'socialMedia'])
|
||||||
)->response()->setStatusCode(201);
|
)->response()->setStatusCode(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(Tenant $tenant): TenantResource
|
public function show(Tenant $tenant): TenantResource
|
||||||
{
|
{
|
||||||
return TenantResource::make(
|
return TenantResource::make(
|
||||||
$tenant->loadMissing(['headerLogo', 'footerLogo', 'mainCarouselImages'])
|
$tenant->loadMissing(['headerLogo', 'footerLogo', 'mainCarouselImages', 'socialMedia'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ class TenantController extends Controller
|
|||||||
$tenant = $this->tenantService->update($tenant, $request->validated());
|
$tenant = $this->tenantService->update($tenant, $request->validated());
|
||||||
|
|
||||||
return TenantResource::make(
|
return TenantResource::make(
|
||||||
$tenant->loadMissing(['headerLogo', 'footerLogo', 'mainCarouselImages'])
|
$tenant->loadMissing(['headerLogo', 'footerLogo', 'mainCarouselImages', 'socialMedia'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ class StoreTenantRequest extends FormRequest
|
|||||||
'hero_bg_image' => ['nullable', new ImageOrBase64Rule],
|
'hero_bg_image' => ['nullable', new ImageOrBase64Rule],
|
||||||
'main_carousel_images' => ['sometimes', 'array'],
|
'main_carousel_images' => ['sometimes', 'array'],
|
||||||
'main_carousel_images.*' => ['required', 'distinct', new ImageOrBase64Rule],
|
'main_carousel_images.*' => ['required', 'distinct', new ImageOrBase64Rule],
|
||||||
|
'social_media' => ['sometimes', 'array'],
|
||||||
|
'social_media.*.code' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'distinct',
|
||||||
|
Rule::exists('social_media', 'code'),
|
||||||
|
],
|
||||||
|
'social_media.*.url' => ['required', 'url', 'max:2048'],
|
||||||
'hero_config' => ['nullable', 'array'],
|
'hero_config' => ['nullable', 'array'],
|
||||||
'hero_config.title_html' => ['nullable', 'string'],
|
'hero_config.title_html' => ['nullable', 'string'],
|
||||||
'hero_config.description_html' => ['nullable', 'string'],
|
'hero_config.description_html' => ['nullable', 'string'],
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ class UpdateTenantRequest extends FormRequest
|
|||||||
'hero_bg_image' => ['nullable', new ImageOrBase64Rule],
|
'hero_bg_image' => ['nullable', new ImageOrBase64Rule],
|
||||||
'main_carousel_images' => ['sometimes', 'array'],
|
'main_carousel_images' => ['sometimes', 'array'],
|
||||||
'main_carousel_images.*' => ['required', 'distinct', new ImageOrBase64Rule],
|
'main_carousel_images.*' => ['required', 'distinct', new ImageOrBase64Rule],
|
||||||
|
'social_media' => ['sometimes', 'array'],
|
||||||
|
'social_media.*.code' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'distinct',
|
||||||
|
Rule::exists('social_media', 'code'),
|
||||||
|
],
|
||||||
|
'social_media.*.url' => ['required', 'url', 'max:2048'],
|
||||||
'hero_config' => ['nullable', 'array'],
|
'hero_config' => ['nullable', 'array'],
|
||||||
'hero_config.title_html' => ['nullable', 'string'],
|
'hero_config.title_html' => ['nullable', 'string'],
|
||||||
'hero_config.description_html' => ['nullable', 'string'],
|
'hero_config.description_html' => ['nullable', 'string'],
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ class TenantResource extends JsonResource
|
|||||||
->map(fn ($attachment) => $attachment->getTemporaryUrl(1440))
|
->map(fn ($attachment) => $attachment->getTemporaryUrl(1440))
|
||||||
->values()
|
->values()
|
||||||
),
|
),
|
||||||
|
'social_media' => $this->whenLoaded(
|
||||||
|
'socialMedia',
|
||||||
|
fn () => $this->socialMedia
|
||||||
|
->map(fn ($socialMedia) => [
|
||||||
|
'code' => $socialMedia->code,
|
||||||
|
'icon' => $socialMedia->icon,
|
||||||
|
'name' => $socialMedia->name,
|
||||||
|
'url' => $socialMedia->pivot->url,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
),
|
||||||
'menues' => $this->whenLoaded('menues'),
|
'menues' => $this->whenLoaded('menues'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ class TenantService
|
|||||||
$footerLogo = $data['footer_logo'] ?? null;
|
$footerLogo = $data['footer_logo'] ?? null;
|
||||||
$heroBgImage = $data['hero_bg_image'] ?? null;
|
$heroBgImage = $data['hero_bg_image'] ?? null;
|
||||||
$mainCarouselImages = $data['main_carousel_images'] ?? [];
|
$mainCarouselImages = $data['main_carousel_images'] ?? [];
|
||||||
|
$socialMedia = $data['social_media'] ?? [];
|
||||||
|
|
||||||
unset(
|
unset(
|
||||||
$data['header_logo'],
|
$data['header_logo'],
|
||||||
$data['footer_logo'],
|
$data['footer_logo'],
|
||||||
$data['hero_bg_image'],
|
$data['hero_bg_image'],
|
||||||
$data['main_carousel_images']
|
$data['main_carousel_images'],
|
||||||
|
$data['social_media']
|
||||||
);
|
);
|
||||||
|
|
||||||
$headerAttachmentId = null;
|
$headerAttachmentId = null;
|
||||||
@@ -74,6 +76,7 @@ class TenantService
|
|||||||
/** @var Tenant $tenant */
|
/** @var Tenant $tenant */
|
||||||
$tenant = Tenant::query()->create($data);
|
$tenant = Tenant::query()->create($data);
|
||||||
$this->syncMainCarouselImages($tenant, $mainCarouselImages);
|
$this->syncMainCarouselImages($tenant, $mainCarouselImages);
|
||||||
|
$this->syncSocialMedia($tenant, $socialMedia);
|
||||||
|
|
||||||
return $tenant;
|
return $tenant;
|
||||||
});
|
});
|
||||||
@@ -91,16 +94,19 @@ class TenantService
|
|||||||
$hasFooterLogoKey = array_key_exists('footer_logo', $data);
|
$hasFooterLogoKey = array_key_exists('footer_logo', $data);
|
||||||
$hasHeroBgImageKey = array_key_exists('hero_bg_image', $data);
|
$hasHeroBgImageKey = array_key_exists('hero_bg_image', $data);
|
||||||
$hasMainCarouselImagesKey = array_key_exists('main_carousel_images', $data);
|
$hasMainCarouselImagesKey = array_key_exists('main_carousel_images', $data);
|
||||||
|
$hasSocialMediaKey = array_key_exists('social_media', $data);
|
||||||
$headerLogo = $data['header_logo'] ?? null;
|
$headerLogo = $data['header_logo'] ?? null;
|
||||||
$footerLogo = $data['footer_logo'] ?? null;
|
$footerLogo = $data['footer_logo'] ?? null;
|
||||||
$heroBgImage = $data['hero_bg_image'] ?? null;
|
$heroBgImage = $data['hero_bg_image'] ?? null;
|
||||||
$mainCarouselImages = $data['main_carousel_images'] ?? [];
|
$mainCarouselImages = $data['main_carousel_images'] ?? [];
|
||||||
|
$socialMedia = $data['social_media'] ?? [];
|
||||||
|
|
||||||
unset(
|
unset(
|
||||||
$data['header_logo'],
|
$data['header_logo'],
|
||||||
$data['footer_logo'],
|
$data['footer_logo'],
|
||||||
$data['hero_bg_image'],
|
$data['hero_bg_image'],
|
||||||
$data['main_carousel_images']
|
$data['main_carousel_images'],
|
||||||
|
$data['social_media']
|
||||||
);
|
);
|
||||||
|
|
||||||
$oldHeroBgId = $tenant->hero_bg_image_id;
|
$oldHeroBgId = $tenant->hero_bg_image_id;
|
||||||
@@ -167,6 +173,10 @@ class TenantService
|
|||||||
$this->syncMainCarouselImages($tenant, $mainCarouselImages);
|
$this->syncMainCarouselImages($tenant, $mainCarouselImages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($hasSocialMediaKey) {
|
||||||
|
$this->syncSocialMedia($tenant, $socialMedia);
|
||||||
|
}
|
||||||
|
|
||||||
return $tenant;
|
return $tenant;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -186,6 +196,21 @@ class TenantService
|
|||||||
$tenant->mainCarouselImages()->sync($attachments);
|
$tenant->mainCarouselImages()->sync($attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{code: string, url: string}> $socialMedia
|
||||||
|
*/
|
||||||
|
private function syncSocialMedia(Tenant $tenant, array $socialMedia): void
|
||||||
|
{
|
||||||
|
$associations = [];
|
||||||
|
|
||||||
|
foreach ($socialMedia as $item) {
|
||||||
|
$associations[$item['code']] = ['url' => $item['url']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->socialMedia()->sync($associations);
|
||||||
|
$tenant->unsetRelation('socialMedia');
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveMainCarouselImage(mixed $image, int $order): Attachment
|
private function resolveMainCarouselImage(mixed $image, int $order): Attachment
|
||||||
{
|
{
|
||||||
if (is_string($image) && Str::isUuid($image)) {
|
if (is_string($image) && Str::isUuid($image)) {
|
||||||
|
|||||||
@@ -76,6 +76,116 @@ class TenantSocialMediaTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_tenant_update_synchronizes_social_media_and_returns_pivot_urls(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant();
|
||||||
|
$instagram = $this->createSocialMedia('instagram', 'Instagram');
|
||||||
|
$facebook = $this->createSocialMedia('facebook', 'Facebook');
|
||||||
|
|
||||||
|
$tenant->socialMedia()->attach($facebook->code, [
|
||||||
|
'url' => 'https://facebook.com/old-acme',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->putJson("/api/tenants/{$tenant->codigo}", [
|
||||||
|
'social_media' => [
|
||||||
|
[
|
||||||
|
'code' => $instagram->code,
|
||||||
|
'url' => 'https://instagram.com/acme',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.social_media.0.code', 'instagram')
|
||||||
|
->assertJsonPath('data.social_media.0.icon', 'instagram')
|
||||||
|
->assertJsonPath('data.social_media.0.name', 'Instagram')
|
||||||
|
->assertJsonPath('data.social_media.0.url', 'https://instagram.com/acme')
|
||||||
|
->assertJsonCount(1, 'data.social_media');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('tenant_social_media', [
|
||||||
|
'tenant_code' => $tenant->codigo,
|
||||||
|
'social_media_code' => $instagram->code,
|
||||||
|
'url' => 'https://instagram.com/acme',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('tenant_social_media', [
|
||||||
|
'tenant_code' => $tenant->codigo,
|
||||||
|
'social_media_code' => $facebook->code,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_tenant_store_synchronizes_social_media(): void
|
||||||
|
{
|
||||||
|
$instagram = $this->createSocialMedia('instagram', 'Instagram');
|
||||||
|
$headerLogo = $this->createAttachment('new-header.png');
|
||||||
|
$footerLogo = $this->createAttachment('new-footer.png');
|
||||||
|
|
||||||
|
$this->postJson('/api/tenants', [
|
||||||
|
'codigo' => 'new-acme',
|
||||||
|
'nombre' => 'New Acme',
|
||||||
|
'dominio' => 'new-acme.com',
|
||||||
|
'primary_color' => '#111111',
|
||||||
|
'secondary_color' => '#222222',
|
||||||
|
'danger_color' => '#333333',
|
||||||
|
'success_color' => '#444444',
|
||||||
|
'header_bg_color' => '#ffffff',
|
||||||
|
'footer_bg_color' => '#ffffff',
|
||||||
|
'header_logo' => $headerLogo->key,
|
||||||
|
'footer_logo' => $footerLogo->key,
|
||||||
|
'social_media' => [
|
||||||
|
[
|
||||||
|
'code' => $instagram->code,
|
||||||
|
'url' => 'https://instagram.com/new-acme',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertCreated()
|
||||||
|
->assertJsonPath('data.social_media.0.code', 'instagram')
|
||||||
|
->assertJsonPath('data.social_media.0.url', 'https://instagram.com/new-acme');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('tenant_social_media', [
|
||||||
|
'tenant_code' => 'new-acme',
|
||||||
|
'social_media_code' => $instagram->code,
|
||||||
|
'url' => 'https://instagram.com/new-acme',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_tenant_update_can_clear_social_media(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant();
|
||||||
|
$instagram = $this->createSocialMedia('instagram', 'Instagram');
|
||||||
|
$tenant->socialMedia()->attach($instagram->code, [
|
||||||
|
'url' => 'https://instagram.com/acme',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->putJson("/api/tenants/{$tenant->codigo}", [
|
||||||
|
'social_media' => [],
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.social_media', []);
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('tenant_social_media', [
|
||||||
|
'tenant_code' => $tenant->codigo,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_tenant_update_validates_social_media_codes_and_urls(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant();
|
||||||
|
|
||||||
|
$this->putJson("/api/tenants/{$tenant->codigo}", [
|
||||||
|
'social_media' => [
|
||||||
|
[
|
||||||
|
'code' => 'unknown',
|
||||||
|
'url' => 'not-a-url',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors([
|
||||||
|
'social_media.0.code',
|
||||||
|
'social_media.0.url',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function createTenant(): Tenant
|
private function createTenant(): Tenant
|
||||||
{
|
{
|
||||||
$headerLogo = $this->createAttachment('header.png');
|
$headerLogo = $this->createAttachment('header.png');
|
||||||
@@ -105,4 +215,13 @@ class TenantSocialMediaTest extends TestCase
|
|||||||
'mime_type' => 'image/png',
|
'mime_type' => 'image/png',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createSocialMedia(string $code, string $name): SocialMedia
|
||||||
|
{
|
||||||
|
return SocialMedia::query()->create([
|
||||||
|
'code' => $code,
|
||||||
|
'icon' => $code,
|
||||||
|
'name' => $name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user