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(
|
||||
Tenant::query()
|
||||
->with(['headerLogo', 'footerLogo', 'mainCarouselImages', 'menues'])
|
||||
->with(['headerLogo', 'footerLogo', 'mainCarouselImages', 'menues', 'socialMedia'])
|
||||
->where('dominio', $dominio)
|
||||
->firstOrFail()
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ class TenantController extends Controller
|
||||
{
|
||||
return TenantResource::collection(
|
||||
Tenant::query()
|
||||
->with(['headerLogo', 'footerLogo', 'mainCarouselImages'])
|
||||
->with(['headerLogo', 'footerLogo', 'mainCarouselImages', 'socialMedia'])
|
||||
->latest()
|
||||
->paginateFromRequest()
|
||||
)->response();
|
||||
@@ -30,14 +30,14 @@ class TenantController extends Controller
|
||||
$tenant = $this->tenantService->create($request->validated());
|
||||
|
||||
return TenantResource::make(
|
||||
$tenant->loadMissing(['headerLogo', 'footerLogo', 'mainCarouselImages'])
|
||||
$tenant->loadMissing(['headerLogo', 'footerLogo', 'mainCarouselImages', 'socialMedia'])
|
||||
)->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Tenant $tenant): TenantResource
|
||||
{
|
||||
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());
|
||||
|
||||
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],
|
||||
'main_carousel_images' => ['sometimes', 'array'],
|
||||
'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.title_html' => ['nullable', 'string'],
|
||||
'hero_config.description_html' => ['nullable', 'string'],
|
||||
|
||||
@@ -74,6 +74,14 @@ class UpdateTenantRequest extends FormRequest
|
||||
'hero_bg_image' => ['nullable', new ImageOrBase64Rule],
|
||||
'main_carousel_images' => ['sometimes', 'array'],
|
||||
'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.title_html' => ['nullable', 'string'],
|
||||
'hero_config.description_html' => ['nullable', 'string'],
|
||||
|
||||
@@ -44,6 +44,17 @@ class TenantResource extends JsonResource
|
||||
->map(fn ($attachment) => $attachment->getTemporaryUrl(1440))
|
||||
->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'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,12 +26,14 @@ class TenantService
|
||||
$footerLogo = $data['footer_logo'] ?? null;
|
||||
$heroBgImage = $data['hero_bg_image'] ?? null;
|
||||
$mainCarouselImages = $data['main_carousel_images'] ?? [];
|
||||
$socialMedia = $data['social_media'] ?? [];
|
||||
|
||||
unset(
|
||||
$data['header_logo'],
|
||||
$data['footer_logo'],
|
||||
$data['hero_bg_image'],
|
||||
$data['main_carousel_images']
|
||||
$data['main_carousel_images'],
|
||||
$data['social_media']
|
||||
);
|
||||
|
||||
$headerAttachmentId = null;
|
||||
@@ -74,6 +76,7 @@ class TenantService
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::query()->create($data);
|
||||
$this->syncMainCarouselImages($tenant, $mainCarouselImages);
|
||||
$this->syncSocialMedia($tenant, $socialMedia);
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
@@ -91,16 +94,19 @@ class TenantService
|
||||
$hasFooterLogoKey = array_key_exists('footer_logo', $data);
|
||||
$hasHeroBgImageKey = array_key_exists('hero_bg_image', $data);
|
||||
$hasMainCarouselImagesKey = array_key_exists('main_carousel_images', $data);
|
||||
$hasSocialMediaKey = array_key_exists('social_media', $data);
|
||||
$headerLogo = $data['header_logo'] ?? null;
|
||||
$footerLogo = $data['footer_logo'] ?? null;
|
||||
$heroBgImage = $data['hero_bg_image'] ?? null;
|
||||
$mainCarouselImages = $data['main_carousel_images'] ?? [];
|
||||
$socialMedia = $data['social_media'] ?? [];
|
||||
|
||||
unset(
|
||||
$data['header_logo'],
|
||||
$data['footer_logo'],
|
||||
$data['hero_bg_image'],
|
||||
$data['main_carousel_images']
|
||||
$data['main_carousel_images'],
|
||||
$data['social_media']
|
||||
);
|
||||
|
||||
$oldHeroBgId = $tenant->hero_bg_image_id;
|
||||
@@ -167,6 +173,10 @@ class TenantService
|
||||
$this->syncMainCarouselImages($tenant, $mainCarouselImages);
|
||||
}
|
||||
|
||||
if ($hasSocialMediaKey) {
|
||||
$this->syncSocialMedia($tenant, $socialMedia);
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
}
|
||||
@@ -186,6 +196,21 @@ class TenantService
|
||||
$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
|
||||
{
|
||||
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
|
||||
{
|
||||
$headerLogo = $this->createAttachment('header.png');
|
||||
@@ -105,4 +215,13 @@ class TenantSocialMediaTest extends TestCase
|
||||
'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