Delivery Tracking Resolver Registry
Contract for registering inbound delivery-tracking payload resolvers that map provider webhooks onto communication_log updates.
Overview
App\Services\Notifications\DeliveryTrackingResolverRegistry is the inbound delivery-tracking seam (P7B-12). It maps a provider key to an App\Contracts\Notifications\DeliveryTrackingResolver, then lets the signed inbound webhook endpoint resolve provider payloads into DeliveryTrackingUpdateData for queued application against communication_log.
Core ships no production resolvers. A FakeDeliveryTrackingResolver (provider() → fake) exists for tests and local fixtures only. Plugins register resolvers through PluginRegistrar::deliveryTrackingResolver().
Public surface
| Method | Purpose |
|---|---|
register() |
Warehouse one resolver instance under its provider() key |
get() |
Return the resolver for a provider or throw if it is missing |
has() |
Check whether a provider key is registered |
providers() |
Return all registered provider keys, sorted alphabetically |
Accepted contract and identity
Every resolver implements App\Contracts\Notifications\DeliveryTrackingResolver:
public function provider(): string;
public function resolve(array $payload): ?DeliveryTrackingUpdateData;
provider() is the stable registry identity and the inbound URL segment {provider} consumed by NotificationInboundController.
resolve() maps a provider-specific payload array to App\Data\Notifications\DeliveryTrackingUpdateData, or returns null when the payload is unrecognised.
Current core registrations
Core does not seed production providers into this registry. The only reference implementation in the codebase is App\Services\Notifications\Resolvers\FakeDeliveryTrackingResolver for tests:
| Provider | Class | Notes |
|---|---|---|
fake |
FakeDeliveryTrackingResolver |
Expects status plus communication_log_id or message_id; optional occurred_at and meta |
Ordering and lookup behavior
providers() returns registered provider keys sorted alphabetically. This is a catalogue/discovery order only.
register() stores resolvers in an associative array keyed by provider(). Registering the same provider again replaces the earlier resolver instance. The registry does not reject duplicates.
get($provider) throws InvalidArgumentException with Unknown delivery tracking provider [{$provider}]. when the provider is missing.
has($provider) is the optional guard used by the inbound controller before signature verification.
Runtime consumption
App\Http\Controllers\Api\V1\NotificationInboundController is the primary consumer:
- Abort
404whenhas($provider)is false - Verify the signed inbound request using per-provider secrets from settings (
notifications.inbound_{provider}_secret, with shared header/tolerance fallbacks) - Call
get($provider)->resolve($payload) - Return
422withUnrecognised payload.when resolve returnsnull - Dispatch
ApplyDeliveryTrackingUpdateon thenotificationsqueue and return202{ "status": "accepted" }
Auth is signature-only (no Sanctum) on that endpoint.
Worked example
This is the current registration and lookup shape exercised in tests:
$registry = app(DeliveryTrackingResolverRegistry::class);
$registry->register(new FakeDeliveryTrackingResolver);
$registry->has('fake'); // true
$registry->providers(); // ['fake']
$update = $registry->get('fake')->resolve([
'communication_log_id' => 1,
'status' => 'delivered',
'occurred_at' => '2026-07-16T12:00:00Z',
'meta' => ['reason' => 'smtp-250'],
]);
An unknown provider fails get() with InvalidArgumentException. An unrecognised payload returns null from resolve() and is rejected by the inbound controller with HTTP 422.