Refactor notification event payloads and listener naming
linter / quality (pull_request) Successful in 1m17s
tests / ci (8.4) (pull_request) Failing after 1m35s

This commit is contained in:
2026-04-25 21:54:42 +00:00
parent 20f89952e7
commit 61505ace23
16 changed files with 286 additions and 115 deletions
@@ -2,7 +2,6 @@
namespace App\Actions\Notifications;
use App\Enums\NotificationEventType;
use App\Models\Project;
use App\Models\Ticket;
use App\Models\TicketStatus;
@@ -17,9 +16,8 @@ class BuildTicketCreatedPayload
TicketStatus $ticketStatus,
?User $actor,
CarbonImmutable $occurredAt,
): TicketStatusChangedPayload {
return new TicketStatusChangedPayload(
eventType: NotificationEventType::TicketCreate,
): TicketCreatedPayload {
return new TicketCreatedPayload(
projectId: $project->id,
projectKey: $project->key,
projectName: $project->name,
@@ -29,8 +27,6 @@ class BuildTicketCreatedPayload
ticketUrl: route('projects.tickets.show', [$project, $ticket]),
actorId: $actor?->id,
actorName: $actor?->name,
fromStatusId: null,
fromStatusLabel: null,
toStatusId: $ticketStatus->id,
toStatusLabel: $this->statusLabel($ticketStatus),
occurredAt: $occurredAt,
@@ -2,7 +2,6 @@
namespace App\Actions\Notifications;
use App\Enums\NotificationEventType;
use App\Models\Project;
use App\Models\Ticket;
use App\Models\TicketStatus;
@@ -22,7 +21,6 @@ class BuildTicketStatusChangedPayload
CarbonImmutable $occurredAt,
): TicketStatusChangedPayload {
return new TicketStatusChangedPayload(
eventType: NotificationEventType::TicketStatusChanged,
projectId: $project->id,
projectKey: $project->key,
projectName: $project->name,
@@ -28,7 +28,7 @@ class DeliverNotificationAction implements ShouldQueue
public function __construct(
public int $notificationActionId,
public TicketStatusChangedPayload $payload,
public NotificationEvent $event,
public ?int $notificationDeliveryLogId,
) {}
@@ -60,7 +60,7 @@ class DeliverNotificationAction implements ShouldQueue
if ($notificationAction->channel === NotificationChannel::Discord) {
$discordMessage = app(RenderDiscordMessageTemplate::class)->handle(
$notificationAction->message_template,
$this->payload,
$this->event,
);
$validationError = $this->discordMessageValidationError($discordMessage);
@@ -245,7 +245,7 @@ class DeliverNotificationAction implements ShouldQueue
'attempt_count' => 0,
'channel' => $notificationAction->channel,
'delivery_status' => NotificationDeliveryStatus::Pending,
'event_type' => $this->payload->eventType,
'event_type' => $this->event->payload()->eventType(),
'notification_action_id' => $notificationAction->id,
'payload_metadata' => $this->payloadMetadata(),
]);
@@ -334,7 +334,7 @@ class DeliverNotificationAction implements ShouldQueue
->asJson()
->connectTimeout(5)
->timeout(10)
->post($notificationAction->delivery_url, $this->payload->toArray());
->post($notificationAction->delivery_url, $this->event->payload()->toArray());
}
private function hasAttemptsRemaining(int $attemptCount): bool
@@ -398,14 +398,11 @@ class DeliverNotificationAction implements ShouldQueue
}
/**
* @return array{project_key: string, ticket_key: string}
* @return array<string, scalar|null>
*/
private function payloadMetadata(): array
{
return [
'project_key' => $this->payload->projectKey,
'ticket_key' => $this->payload->ticketKey,
];
return $this->event->payload()->deliveryMetadata();
}
private function excerpt(?string $value): ?string
@@ -0,0 +1,8 @@
<?php
namespace App\Actions\Notifications;
interface NotificationEvent
{
public function payload(): NotificationEventPayload;
}
@@ -0,0 +1,26 @@
<?php
namespace App\Actions\Notifications;
use App\Enums\NotificationEventType;
use Illuminate\Contracts\Support\Arrayable;
/**
* @extends Arrayable<string, mixed>
*/
interface NotificationEventPayload extends Arrayable
{
public function eventType(): NotificationEventType;
public function projectId(): int;
/**
* @return array<string, scalar|null>
*/
public function deliveryMetadata(): array;
/**
* @return array<string, string>
*/
public function discordTemplateVariables(): array;
}
@@ -7,16 +7,18 @@ use App\Enums\NotificationDeliveryStatus;
use App\Models\NotificationAction;
use App\Models\NotificationDeliveryLog;
class QueueTicketStatusChangedNotificationDeliveries
class QueueNotificationEventDeliveries
{
public function handle(TicketStatusChanged|TicketCreated $event): void
public function handle(NotificationEvent $event): void
{
$payload = $event->payload();
NotificationAction::query()
->enabled()
->where('project_id', $event->payload->projectId)
->where('event_type', $event->payload->eventType->value)
->where('project_id', $payload->projectId())
->where('event_type', $payload->eventType()->value)
->get(['channel', 'id'])
->each(function (NotificationAction $notificationAction) use ($event): void {
->each(function (NotificationAction $notificationAction) use ($event, $payload): void {
$notificationDeliveryLogId = null;
if (in_array($notificationAction->channel, [NotificationChannel::Discord, NotificationChannel::Webhook], true)) {
@@ -24,18 +26,15 @@ class QueueTicketStatusChangedNotificationDeliveries
'attempt_count' => 0,
'channel' => $notificationAction->channel,
'delivery_status' => NotificationDeliveryStatus::Pending,
'event_type' => $event->payload->eventType,
'event_type' => $payload->eventType(),
'notification_action_id' => $notificationAction->id,
'payload_metadata' => [
'project_key' => $event->payload->projectKey,
'ticket_key' => $event->payload->ticketKey,
],
'payload_metadata' => $payload->deliveryMetadata(),
])->id;
}
DeliverNotificationAction::dispatch(
notificationActionId: $notificationAction->id,
payload: $event->payload,
event: $event,
notificationDeliveryLogId: $notificationDeliveryLogId,
);
});
@@ -6,16 +6,8 @@ use Illuminate\Support\Str;
class RenderDiscordMessageTemplate
{
public function handle(?string $messageTemplate, TicketStatusChangedPayload $payload): string
public function handle(?string $messageTemplate, NotificationEvent $event): string
{
return strtr((string) Str::of((string) $messageTemplate), [
'$url' => $payload->ticketUrl,
'$ticket_key' => $payload->ticketKey,
'$ticket_title' => $payload->ticketTitle,
'$project_name' => $payload->projectName,
'$status_from' => $payload->fromStatusLabel ?? '',
'$status_to' => $payload->toStatusLabel,
'$actor_name' => $payload->actorName ?? '',
]);
return strtr((string) Str::of((string) $messageTemplate), $event->payload()->discordTemplateVariables());
}
}
+7 -2
View File
@@ -5,11 +5,16 @@ namespace App\Actions\Notifications;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
final readonly class TicketCreated implements ShouldDispatchAfterCommit
final readonly class TicketCreated implements NotificationEvent, ShouldDispatchAfterCommit
{
use Dispatchable;
public function __construct(
public TicketStatusChangedPayload $payload,
public TicketCreatedPayload $payload,
) {}
public function payload(): NotificationEventPayload
{
return $this->payload;
}
}
@@ -0,0 +1,113 @@
<?php
namespace App\Actions\Notifications;
use App\Enums\NotificationEventType;
use Carbon\CarbonImmutable;
final readonly class TicketCreatedPayload implements NotificationEventPayload
{
public NotificationEventType $eventType;
public ?int $fromStatusId;
public ?string $fromStatusLabel;
public function __construct(
public int $projectId,
public string $projectKey,
public string $projectName,
public int $ticketId,
public string $ticketKey,
public string $ticketTitle,
public string $ticketUrl,
public ?int $actorId,
public ?string $actorName,
public int $toStatusId,
public string $toStatusLabel,
public CarbonImmutable $occurredAt,
) {
$this->eventType = NotificationEventType::TicketCreate;
$this->fromStatusId = null;
$this->fromStatusLabel = null;
}
public function eventType(): NotificationEventType
{
return $this->eventType;
}
public function projectId(): int
{
return $this->projectId;
}
/**
* @return array<string, scalar|null>
*/
public function deliveryMetadata(): array
{
return [
'project_key' => $this->projectKey,
'ticket_key' => $this->ticketKey,
];
}
/**
* @return array<string, string>
*/
public function discordTemplateVariables(): array
{
return [
'$url' => $this->ticketUrl,
'$ticket_key' => $this->ticketKey,
'$ticket_title' => $this->ticketTitle,
'$project_name' => $this->projectName,
'$status_from' => '',
'$status_to' => $this->toStatusLabel,
'$actor_name' => $this->actorName ?? '',
];
}
/**
* @return array{
* event_type: string,
* project: array{id: int, key: string, name: string},
* ticket: array{id: int, key: string, title: string},
* actor: array{id: int, name: string}|null,
* from: null,
* to: array{id: int, label: string},
* ticket_url: string,
* occurred_at: string
* }
*/
public function toArray(): array
{
return [
'event_type' => $this->eventType->value,
'project' => [
'id' => $this->projectId,
'key' => $this->projectKey,
'name' => $this->projectName,
],
'ticket' => [
'id' => $this->ticketId,
'key' => $this->ticketKey,
'title' => $this->ticketTitle,
],
'actor' => $this->actorId === null || $this->actorName === null
? null
: [
'id' => $this->actorId,
'name' => $this->actorName,
],
'from' => null,
'to' => [
'id' => $this->toStatusId,
'label' => $this->toStatusLabel,
],
'ticket_url' => $this->ticketUrl,
'occurred_at' => $this->occurredAt->toISOString(),
];
}
}
@@ -5,11 +5,16 @@ namespace App\Actions\Notifications;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
final readonly class TicketStatusChanged implements ShouldDispatchAfterCommit
final readonly class TicketStatusChanged implements NotificationEvent, ShouldDispatchAfterCommit
{
use Dispatchable;
public function __construct(
public TicketStatusChangedPayload $payload,
) {}
public function payload(): NotificationEventPayload
{
return $this->payload;
}
}
@@ -4,15 +4,12 @@ namespace App\Actions\Notifications;
use App\Enums\NotificationEventType;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Support\Arrayable;
/**
* @implements Arrayable<string, mixed>
*/
final readonly class TicketStatusChangedPayload implements Arrayable
final readonly class TicketStatusChangedPayload implements NotificationEventPayload
{
public NotificationEventType $eventType;
public function __construct(
public NotificationEventType $eventType,
public int $projectId,
public string $projectKey,
public string $projectName,
@@ -22,12 +19,51 @@ final readonly class TicketStatusChangedPayload implements Arrayable
public string $ticketUrl,
public ?int $actorId,
public ?string $actorName,
public ?int $fromStatusId,
public ?string $fromStatusLabel,
public int $fromStatusId,
public string $fromStatusLabel,
public int $toStatusId,
public string $toStatusLabel,
public CarbonImmutable $occurredAt,
) {}
) {
$this->eventType = NotificationEventType::TicketStatusChanged;
}
public function eventType(): NotificationEventType
{
return $this->eventType;
}
public function projectId(): int
{
return $this->projectId;
}
/**
* @return array<string, scalar|null>
*/
public function deliveryMetadata(): array
{
return [
'project_key' => $this->projectKey,
'ticket_key' => $this->ticketKey,
];
}
/**
* @return array<string, string>
*/
public function discordTemplateVariables(): array
{
return [
'$url' => $this->ticketUrl,
'$ticket_key' => $this->ticketKey,
'$ticket_title' => $this->ticketTitle,
'$project_name' => $this->projectName,
'$status_from' => $this->fromStatusLabel,
'$status_to' => $this->toStatusLabel,
'$actor_name' => $this->actorName ?? '',
];
}
/**
* @return array{
@@ -35,7 +71,7 @@ final readonly class TicketStatusChangedPayload implements Arrayable
* project: array{id: int, key: string, name: string},
* ticket: array{id: int, key: string, title: string},
* actor: array{id: int, name: string}|null,
* from: array{id: int, label: string}|null,
* from: array{id: int, label: string},
* to: array{id: int, label: string},
* ticket_url: string,
* occurred_at: string
@@ -61,12 +97,10 @@ final readonly class TicketStatusChangedPayload implements Arrayable
'id' => $this->actorId,
'name' => $this->actorName,
],
'from' => $this->fromStatusId === null || $this->fromStatusLabel === null
? null
: [
'id' => $this->fromStatusId,
'label' => $this->fromStatusLabel,
],
'from' => [
'id' => $this->fromStatusId,
'label' => $this->fromStatusLabel,
],
'to' => [
'id' => $this->toStatusId,
'label' => $this->toStatusLabel,
+3 -3
View File
@@ -2,7 +2,7 @@
namespace App\Providers;
use App\Actions\Notifications\QueueTicketStatusChangedNotificationDeliveries;
use App\Actions\Notifications\QueueNotificationEventDeliveries;
use App\Actions\Notifications\TicketCreated;
use App\Actions\Notifications\TicketStatusChanged;
use App\Actions\Permissions\PermissionService;
@@ -98,12 +98,12 @@ class AppServiceProvider extends ServiceProvider
{
Event::listen(
TicketStatusChanged::class,
QueueTicketStatusChangedNotificationDeliveries::class,
QueueNotificationEventDeliveries::class,
);
Event::listen(
TicketCreated::class,
QueueTicketStatusChangedNotificationDeliveries::class,
QueueNotificationEventDeliveries::class,
);
}
}