Maps the 31-module plan onto the registry's 19 leaves as packages; the two edges the registry forbids (provider->httpclient, inbox->messaging) are replaced by application-owned ports. See docs/notification/module-mapping.md. Acceptance is not delivery: ProviderSubmissionResult refuses to carry a delivery outcome, and AMBIGUOUS is a first-class terminal state that blocks automatic retry and fallback until reconciliation resolves it. Providers: SES (SigV4 + SNS callback), Twilio (X-Twilio-Signature + reconciliation), FCM (FID-primary batch), APNs, Web Push (RFC 8030/8291/8292), SMTP and webhook. Contact points are AES-256-GCM encrypted with a separate HMAC lookup fingerprint; nothing raw reaches a log, metric tag or exception. Dispatch commits the attempt row, calls the provider with no transaction open, then records the outcome; the durable queue uses FOR UPDATE SKIP LOCKED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
38 lines
1.5 KiB
Markdown
38 lines
1.5 KiB
Markdown
# NOTIF-ADR-003 — ambiguous submission is a first-class state
|
|
|
|
## Status
|
|
|
|
Accepted.
|
|
|
|
## Context
|
|
|
|
The most common serious failure is not a rejection. It is a request whose body reached the provider
|
|
and whose response never came back. The platform has no provider request id, and the user may or may
|
|
not have received the notification.
|
|
|
|
Treating that as a failure produces duplicates: a retry sends a second message, and a cross-channel
|
|
fallback sends the SMS next to the push that already arrived. Treating it as a success loses real
|
|
failures.
|
|
|
|
## Decision
|
|
|
|
`AMBIGUOUS` is a stored `SubmissionOutcome` and `AttemptConfirmation`. Attempts record
|
|
`requestStarted`, `requestBodyCommitted` and `providerResponseReceived`, each with an
|
|
`EvidenceCertainty` of `PROVEN`, `INFERRED` or `UNKNOWN`, so an adapter that does not know is not
|
|
forced to answer `false`.
|
|
|
|
While an ambiguous attempt exists on a recipient delivery:
|
|
|
|
- automatic retry is blocked unless the provider proves per-request idempotency
|
|
- automatic cross-channel fallback is blocked unconditionally
|
|
- reconciliation runs where the provider supports a status query
|
|
- otherwise the delivery stops and waits for an operator
|
|
|
|
Operator redrive of an ambiguous attempt requires explicit duplicate-risk approval.
|
|
|
|
## Consequences
|
|
|
|
Some notifications stop in a state that needs a human or a reconciliation pass. That is the intended
|
|
trade: an unresolved unknown is cheaper than a guaranteed duplicate, and the state is visible rather
|
|
than silently resolved in either direction.
|