Fall back to display name when a published-post email has no username (#325)

* Fall back to display name when a published-post email has no username.

Facebook Pages often have no vanity username, which left the email as "Facebook Page (@)". Use username, then display name, and omit the empty parentheses when both are missing.

* Move the notification account label onto PostPlatform.

The fallback belongs on the row that always has a platform, so mail and in-app notify can call one method instead of guarding a missing social account at every call site.

* Cover the in-app published/failed notification body for Facebook Pages.

The email tests were not enough — SendNotification body is what the in-app inbox shows, and that path also goes through notificationLabel().
This commit is contained in:
Paulo Castellano 2026-09-03 12:51:44 -03:00 committed by GitHub
parent 0db9e0706d
commit abe687d0fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 272 additions and 3 deletions

View file

@ -426,7 +426,7 @@ private function notify(Post $post, PostPlatformStatus $status): void
->enabled()
->where('status', $status)
->get()
->map(fn ($pp) => $pp->platform->label().' (@'.data_get($pp, 'socialAccount.username', '').')')
->map(fn ($pp) => $pp->notificationLabel())
->implode(', ');
SendNotification::dispatch(

View file

@ -36,7 +36,7 @@ public function content(): Content
->get()
->filter(fn ($pp) => $pp->status === Status::Failed)
->map(fn ($pp) => [
'name' => $pp->platform->label().' (@'.data_get($pp, 'socialAccount.username', data_get($pp, 'socialAccount.display_name', '')).')',
'name' => $pp->notificationLabel(),
'error' => $pp->error_message,
])
->values()

View file

@ -36,7 +36,7 @@ public function content(): Content
->get()
->filter(fn ($pp) => $pp->status === Status::Published)
->map(fn ($pp) => [
'name' => $pp->platform->label().' (@'.data_get($pp, 'socialAccount.username', '').')',
'name' => $pp->notificationLabel(),
'url' => $pp->platform_url,
])
->values()

View file

@ -89,6 +89,25 @@ public function getDisplayUsernameAttribute(): ?string
return $this->socialAccount?->username ?? $this->platform_username;
}
/**
* "Facebook Page (@handle)" for emails and in-app notifications.
* Username first, then display name (live account or the snapshot
* kept on this row). When neither is set — or the account is gone
* and there is no snapshot — just the platform name, never "(@)".
*/
public function notificationLabel(): string
{
$identifier = $this->display_username
?: $this->socialAccount?->display_name
?: $this->platform_name;
if (! filled($identifier) || $identifier === $this->platform->label()) {
return $this->platform->label();
}
return "{$this->platform->label()} (@{$identifier})";
}
/**
* Get avatar URL, falling back to snapshot if account was deleted.
*/

View file

@ -23,6 +23,7 @@
use App\Models\Workspace;
use App\Services\Media\MediaOptimizer;
use App\Services\Social\ConnectionVerifier;
use App\Services\Social\FacebookPublisher;
use App\Services\Social\LinkedInPagePublisher;
use App\Services\Social\LinkedInPublisher;
use App\Services\Social\PinterestPublisher;
@ -1275,6 +1276,77 @@
Queue::assertPushed(SendNotification::class);
});
test('in-app published notification falls back to the facebook page display name', function () {
Event::fake();
Queue::fake();
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $this->workspace->id,
'username' => null,
'display_name' => 'InboxPlacement.io',
]);
$post = Post::factory()->scheduled()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
]);
$postPlatform = PostPlatform::factory()->facebook()->create([
'post_id' => $post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
'enabled' => true,
]);
$publisher = Mockery::mock(FacebookPublisher::class);
$publisher->shouldReceive('publish')->andReturn([
'id' => 'fb-123',
'url' => 'https://www.facebook.com/permalink.php?story_fbid=pfbid0&id=61592851040951',
]);
$this->app->instance(FacebookPublisher::class, $publisher);
(new PublishToSocialPlatform($postPlatform))->handle();
Queue::assertPushed(SendNotification::class, function (SendNotification $job) use ($post) {
return $job->type === Type::PostPublished
&& $job->title === 'Post published successfully'
&& $job->body === 'Facebook Page (@InboxPlacement.io)'
&& data_get($job->data, 'post_id') === $post->id;
});
});
test('in-app failed notification falls back to the facebook page display name', function () {
Event::fake();
Queue::fake();
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $this->workspace->id,
'username' => null,
'display_name' => 'InboxPlacement.io',
]);
$post = Post::factory()->scheduled()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
]);
$postPlatform = PostPlatform::factory()->facebook()->create([
'post_id' => $post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
'enabled' => true,
]);
$publisher = Mockery::mock(FacebookPublisher::class);
$publisher->shouldReceive('publish')->andThrow(new Exception('API error'));
$this->app->instance(FacebookPublisher::class, $publisher);
(new PublishToSocialPlatform($postPlatform))->handle();
Queue::assertPushed(SendNotification::class, function (SendNotification $job) use ($post) {
return $job->type === Type::PostFailed
&& $job->title === 'Post failed to publish'
&& $job->body === 'Failed on: Facebook Page (@InboxPlacement.io)'
&& data_get($job->data, 'post_id') === $post->id;
});
});
test('it retries with token refresh when token expires during publish', function () {
Event::fake();

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use App\Mail\PostPublishFailed;
use App\Models\Post;
use App\Models\PostPlatform;
use App\Models\SocialAccount;
use App\Models\Workspace;
test('failed email falls back to the page display name when facebook has no username', function () {
$workspace = Workspace::factory()->create(['name' => 'InboxPlacement.io']);
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $workspace->id,
'username' => null,
'display_name' => 'InboxPlacement.io',
]);
$post = Post::factory()->failed()->create(['workspace_id' => $workspace->id]);
PostPlatform::factory()->facebook()->failed()->create([
'post_id' => $post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
]);
$mail = new PostPublishFailed($post);
$mail->assertSeeInHtml('Facebook Page (@InboxPlacement.io)');
$mail->assertDontSeeInHtml('Facebook Page (@)');
});

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
use App\Mail\PostPublished;
use App\Models\Post;
use App\Models\PostPlatform;
use App\Models\SocialAccount;
use App\Models\Workspace;
test('published email falls back to the page display name when facebook has no username', function () {
$workspace = Workspace::factory()->create(['name' => 'InboxPlacement.io']);
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $workspace->id,
'username' => null,
'display_name' => 'InboxPlacement.io',
]);
$post = Post::factory()->published()->create(['workspace_id' => $workspace->id]);
PostPlatform::factory()->facebook()->published()->create([
'post_id' => $post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
'platform_url' => 'https://www.facebook.com/permalink.php?story_fbid=pfbid0&id=61592851040951',
]);
$mail = new PostPublished($post);
$mail->assertSeeInHtml('Facebook Page (@InboxPlacement.io)');
$mail->assertDontSeeInHtml('Facebook Page (@)');
$mail->assertSeeInHtml('https://www.facebook.com/permalink.php?story_fbid=pfbid0&id=61592851040951');
});
test('published email uses the username when the display name is empty', function () {
$workspace = Workspace::factory()->create(['name' => 'InboxPlacement.io']);
$account = SocialAccount::factory()->bluesky()->create([
'workspace_id' => $workspace->id,
'username' => 'inboxplacementio.bsky.social',
'display_name' => '',
]);
$post = Post::factory()->published()->create(['workspace_id' => $workspace->id]);
PostPlatform::factory()->bluesky()->published()->create([
'post_id' => $post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
]);
$mail = new PostPublished($post);
$mail->assertSeeInHtml('Bluesky (@inboxplacementio.bsky.social)');
$mail->assertDontSeeInHtml('Bluesky (@)');
});
test('published email omits empty parentheses when both identifiers are missing', function () {
$workspace = Workspace::factory()->create(['name' => 'InboxPlacement.io']);
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $workspace->id,
'username' => null,
'display_name' => '',
]);
$post = Post::factory()->published()->create(['workspace_id' => $workspace->id]);
PostPlatform::factory()->facebook()->published()->create([
'post_id' => $post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
]);
$mail = new PostPublished($post);
$mail->assertSeeInHtml('Facebook Page');
$mail->assertDontSeeInHtml('Facebook Page (@)');
});

View file

@ -64,6 +64,84 @@
expect($this->postPlatform->fresh()->display_name)->toBe('Snapshot Name');
});
test('notificationLabel prefers username over display name', function () {
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $this->workspace->id,
'username' => 'myfbpage',
'display_name' => 'InboxPlacement.io',
]);
$postPlatform = PostPlatform::factory()->facebook()->create([
'post_id' => $this->post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
]);
expect($postPlatform->notificationLabel())->toBe('Facebook Page (@myfbpage)');
});
test('notificationLabel falls back to display name when username is empty', function () {
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $this->workspace->id,
'username' => null,
'display_name' => 'InboxPlacement.io',
]);
$postPlatform = PostPlatform::factory()->facebook()->create([
'post_id' => $this->post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
]);
expect($postPlatform->notificationLabel())->toBe('Facebook Page (@InboxPlacement.io)');
});
test('notificationLabel uses username when display name is empty', function () {
$account = SocialAccount::factory()->bluesky()->create([
'workspace_id' => $this->workspace->id,
'username' => 'inboxplacementio.bsky.social',
'display_name' => '',
]);
$postPlatform = PostPlatform::factory()->bluesky()->create([
'post_id' => $this->post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
]);
expect($postPlatform->notificationLabel())->toBe('Bluesky (@inboxplacementio.bsky.social)');
});
test('notificationLabel omits empty parentheses when both identifiers are missing', function () {
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $this->workspace->id,
'username' => null,
'display_name' => '',
]);
$postPlatform = PostPlatform::factory()->facebook()->create([
'post_id' => $this->post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
]);
expect($postPlatform->notificationLabel())->toBe('Facebook Page');
});
test('notificationLabel uses the snapshot when the account has been deleted', function () {
$account = SocialAccount::factory()->facebook()->create([
'workspace_id' => $this->workspace->id,
'username' => null,
'display_name' => 'InboxPlacement.io',
]);
$postPlatform = PostPlatform::factory()->facebook()->create([
'post_id' => $this->post->id,
'social_account_id' => $account->id,
'platform' => $account->platform,
'platform_name' => 'InboxPlacement.io',
]);
$account->delete();
expect($postPlatform->fresh()->notificationLabel())->toBe('Facebook Page (@InboxPlacement.io)');
});
test('markAsFailed clears stale platform_post_id and platform_url', function () {
$this->postPlatform->update([
'status' => Status::Published,