Skip to content

Commit f7e3926

Browse files
simonhampclaude
andcommitted
Improve support ticket email notifications
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a09c305 commit f7e3926

File tree

5 files changed

+86
-10
lines changed

5 files changed

+86
-10
lines changed

app/Filament/Resources/SupportTicketResource/RelationManagers/RepliesRelationManager.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ public function table(Table $table): Table
7373
}
7474

7575
$ticket = $this->getOwnerRecord();
76-
$ticket->user->notify(new SupportTicketReplied($ticket, $record));
76+
77+
if ($ticket->user_id !== auth()->id()) {
78+
$ticket->user->notify(new SupportTicketReplied($ticket, $record));
79+
}
7780
}),
7881
])
7982
->actions([

app/Filament/Resources/SupportTicketResource/Widgets/TicketRepliesWidget.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public function sendReply(): void
3636
'note' => $this->isNote,
3737
]);
3838

39-
if (! $this->isNote) {
39+
if (! $this->isNote && $this->record->user_id !== auth()->id()) {
4040
$this->record->user->notify(new SupportTicketReplied($this->record, $reply));
4141
}
4242

app/Notifications/SupportTicketReplied.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Illuminate\Contracts\Queue\ShouldQueue;
99
use Illuminate\Notifications\Messages\MailMessage;
1010
use Illuminate\Notifications\Notification;
11-
use Illuminate\Support\Str;
1211

1312
class SupportTicketReplied extends Notification implements ShouldQueue
1413
{
@@ -30,11 +29,11 @@ public function via(object $notifiable): array
3029
public function toMail(object $notifiable): MailMessage
3130
{
3231
return (new MailMessage)
33-
->subject('Update on your support request: '.$this->ticket->subject)
32+
->subject('Update on your support request: '.$this->ticket->mask)
3433
->greeting("Hi {$notifiable->first_name},")
35-
->line('Your support ticket has received a new reply.')
36-
->line('**'.e($this->ticket->subject).'**')
37-
->line(Str::limit($this->reply->message, 500))
38-
->action('View Ticket', route('customer.support.tickets.show', $this->ticket));
34+
->line('Your support ticket **'.e($this->ticket->subject).'** has received a new reply.')
35+
->line('Please log in to your dashboard to view the message and respond.')
36+
->action('View Ticket', route('customer.support.tickets.show', $this->ticket))
37+
->line('*Please do not reply to this email — responses must be submitted through the support portal.*');
3938
}
4039
}

app/Notifications/SupportTicketSubmitted.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,39 @@ public function via(object $notifiable): array
2626
public function toMail(object $notifiable): MailMessage
2727
{
2828
$ticket = $this->ticket->loadMissing('user');
29+
$user = $ticket->user;
2930

3031
return (new MailMessage)
3132
->subject('New Support Ticket: '.$ticket->subject)
32-
->replyTo($ticket->user->email, $ticket->user->name)
3333
->greeting('New support ticket received!')
34+
->line("**Customer:** {$user->name}")
35+
->line('**Email:** '.$this->obfuscateEmail($user->email))
3436
->line("**Product:** {$ticket->product}")
3537
->line('**Issue Type:** '.($ticket->issue_type ?? 'N/A'))
3638
->line("**Subject:** {$ticket->subject}")
3739
->line('**Message:**')
3840
->line(Str::limit($ticket->message, 500))
3941
->action('View Ticket', SupportTicketResource::getUrl('view', ['record' => $ticket]));
4042
}
43+
44+
private function obfuscateEmail(string $email): string
45+
{
46+
$parts = explode('@', $email);
47+
$local = $parts[0];
48+
$domain = $parts[1] ?? '';
49+
50+
$visibleLocal = Str::length($local) > 2
51+
? Str::substr($local, 0, 2).str_repeat('*', Str::length($local) - 2)
52+
: $local;
53+
54+
$domainParts = explode('.', $domain);
55+
$domainName = $domainParts[0] ?? '';
56+
$tld = implode('.', array_slice($domainParts, 1));
57+
58+
$visibleDomain = Str::length($domainName) > 2
59+
? Str::substr($domainName, 0, 2).str_repeat('*', Str::length($domainName) - 2)
60+
: $domainName;
61+
62+
return "{$visibleLocal}@{$visibleDomain}.{$tld}";
63+
}
4164
}

tests/Feature/SupportTicketTest.php

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,31 @@ function (SupportTicketSubmitted $notification, array $channels, object $notifia
656656
);
657657
}
658658

659+
#[Test]
660+
public function support_ticket_email_includes_customer_details_with_obfuscated_email(): void
661+
{
662+
$user = User::factory()->create([
663+
'name' => 'Jane Smith',
664+
'email' => 'jane@example.com',
665+
]);
666+
667+
$ticket = SupportTicket::factory()->create([
668+
'user_id' => $user->id,
669+
'subject' => 'Test ticket',
670+
'product' => 'bifrost',
671+
'issue_type' => 'bug',
672+
'message' => 'Test message',
673+
]);
674+
675+
$notification = new SupportTicketSubmitted($ticket);
676+
$mailMessage = $notification->toMail($user);
677+
$rendered = $mailMessage->render()->toHtml();
678+
679+
$this->assertStringContainsString('Jane Smith', $rendered);
680+
$this->assertStringContainsString('ja**@ex*****.com', $rendered);
681+
$this->assertStringNotContainsString('jane@example.com', $rendered);
682+
}
683+
659684
#[Test]
660685
public function authenticated_ultra_user_can_reply_to_their_open_ticket(): void
661686
{
@@ -877,6 +902,27 @@ public function internal_note_reply_does_not_send_notification_to_ticket_owner()
877902
Notification::assertNotSentTo($user, SupportTicketReplied::class);
878903
}
879904

905+
#[Test]
906+
public function ticket_owner_does_not_receive_notification_for_own_reply(): void
907+
{
908+
Notification::fake();
909+
910+
$admin = User::factory()->create(['is_admin' => true]);
911+
$ticket = SupportTicket::factory()->create(['user_id' => $admin->id]);
912+
913+
Livewire::actingAs($admin)
914+
->test(TicketRepliesWidget::class, ['record' => $ticket])
915+
->set('newMessage', 'Replying to my own ticket.')
916+
->call('sendReply');
917+
918+
$this->assertDatabaseHas('replies', [
919+
'support_ticket_id' => $ticket->id,
920+
'message' => 'Replying to my own ticket.',
921+
]);
922+
923+
Notification::assertNotSentTo($admin, SupportTicketReplied::class);
924+
}
925+
880926
#[Test]
881927
public function support_ticket_replied_notification_contains_correct_mail_content(): void
882928
{
@@ -892,9 +938,14 @@ public function support_ticket_replied_notification_contains_correct_mail_conten
892938

893939
$notification = new SupportTicketReplied($ticket, $reply);
894940
$mail = $notification->toMail($user);
941+
$rendered = $mail->render()->toHtml();
895942

896-
$this->assertStringContainsString('Login issue', $mail->subject);
943+
$this->assertStringContainsString($ticket->mask, $mail->subject);
944+
$this->assertStringNotContainsString('Login issue', $mail->subject);
897945
$this->assertStringContainsString('Hi Jane', $mail->greeting);
946+
$this->assertStringContainsString('log in to your dashboard', $rendered);
947+
$this->assertStringContainsString('do not reply to this email', $rendered);
948+
$this->assertStringNotContainsString('We have fixed the login issue', $rendered);
898949
}
899950

900951
#[Test]

0 commit comments

Comments
 (0)