Add email integration for ticket system and IMAP inbound support

Email integration: IMAP polling via webklex/php-imap (M365/Zoho/MIAB
compatible), TicketEmailProcessor with 3-strategy ticket matching
(In-Reply-To, References, subject line [EZSCALE-{id}]), signature and
quote stripping, scheduled command every 2min.

Outbound: 4 notification classes (TicketCreated, CustomerReply,
StaffReply, StatusChanged) with email threading headers via
withSymfonyMessage(). Staff replies set Reply-To: support@ezscale.cloud.

Frontend: ticket reference chips on all pages, "Via Email" badges on
email-originated replies. 12 new tests (163 total, 816 assertions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-02-09 17:10:04 -05:00
parent a4cf7026bc
commit 015949a7f6
28 changed files with 1220 additions and 6 deletions

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -15,6 +16,8 @@ class SupportTicket extends Model
protected $fillable = [
'user_id',
'ticket_reference',
'customer_email',
'subject',
'status',
'priority',
@@ -30,6 +33,25 @@ class SupportTicket extends Model
];
}
protected static function booted(): void
{
static::created(function (SupportTicket $ticket): void {
if (empty($ticket->ticket_reference)) {
$ticket->updateQuietly([
'ticket_reference' => sprintf(
config('tickets.ticket_reference_format'),
$ticket->id
),
]);
}
});
}
public function scopeByReference(Builder $query, string $reference): Builder
{
return $query->where('ticket_reference', $reference);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);

View File

@@ -17,6 +17,9 @@ class TicketReply extends Model
'user_id',
'body',
'is_staff_reply',
'message_id',
'from_email',
'via_email',
];
/** @return array<string, string> */
@@ -24,6 +27,7 @@ class TicketReply extends Model
{
return [
'is_staff_reply' => 'boolean',
'via_email' => 'boolean',
];
}