Files
hrynco-notification-service/docs/itemtracker-outbox-email-consumer.md
T
agrynco 2757869176 feat: consume transactional email notifications
Add contract validation, SMTP delivery results, terminal failure context, neutral development seeding, and local Docker setup.

Ref: IT-1033
2026-08-04 12:32:28 +03:00

116 lines
7.3 KiB
Markdown

# ItemTracker Outbox Email Consumer
## Contract and routing
The Worker consumes durable messages from queue `notification.send-email`. ItemTracker publishes through RabbitMQ's default exchange, so the queue name is also the routing key. No custom exchange binding is required.
`HrynCo.RabbitMq` owns the transport lifecycle: connection, queue declaration, deserialization, prefetch, structured delivery context, retry, terminal-failure notification, and manual ACK/NACK. Notification Service only supplies its queue, contract validation, email handler, and result publisher through the package's generic extension points. This integration requires package version `1.0.17` or newer.
Accepted messages must have all of the following:
- AMQP `Type`: `Notification.SendEmail.v1`
- AMQP `MessageId`: the non-empty stable ItemTracker Outbox ID
- JSON content using the PascalCase `SendEmailMessage` envelope
- non-empty `CorrelationContext.CorrelationId`
- non-empty `ServiceName`, `TemplateKey`, `RecipientEmail`, `RecipientName`, and `LanguageCode`
- a valid recipient address and a non-null `Variables` object
The service resolves a template by the exact `ServiceName`, `TemplateKey`, and normalized lower-case `LanguageCode`. It does not silently fall back to another language. Every variable marked `Required` by the selected template must have a non-empty value before rendering.
## Delivery and acknowledgement policy
The consumer uses manual acknowledgements and prefetches one message at a time.
1. Unsupported contract metadata, malformed JSON, or invalid required fields are logged and nacked without requeue.
2. Template, provider, quota, SMTP, or usage-accounting failures are retried in-process three times with a five-second delay.
3. The message is acknowledged only after SMTP accepts the email and provider usage is incremented.
4. If `CorrelationContext.ReplyTo` is present, result publication is attempted after delivery. It is best-effort: a reply-queue failure is logged but does not make the SMTP delivery fail, because retrying after SMTP success could send a duplicate email.
5. After the final processing retry fails, the shared terminal-failure hook publishes one
negative result to the same `ReplyTo`. It retains the original correlation and contains
a normalized diagnostic string limited to 2000 characters. Common SMTP DNS,
connection-refused, timeout, and recipient-rejection failures receive safe explanatory
context while retaining the low-level provider reason. Hostnames, recipient addresses,
and credentials are not added. Client outboxes store this as a neutral delivery error
rather than interpreting SMTP-specific exception types.
Success and terminal failure result publication remain best-effort. If the result queue
cannot be reached, the internal exception is logged and the original delivery follows its
normal ACK/NACK policy. Clients therefore must not interpret the absence of a result as a
confirmed failure.
Delivery remains at least once. There is no inbox/deduplication store in this MVP, so a broker redelivery or a process failure after SMTP acceptance but before acknowledgement can produce a duplicate. `MessageId`, `CorrelationId`, and `MessageType` are added to the structured log scope for correlation. Recipient addresses are masked, and template variable names/values and rendered links are not logged.
No database migration is required. Existing channel selection, SMTP settings, quota checks, and usage counters remain in place.
## Local end-to-end setup
With Docker Desktop running, install and start the complete Notification Service
development stack from the repository root:
```powershell
.\docker\environments\install-development.cmd
```
The script uses the tracked `docker/environments/.env.Development` defaults. The
following manual setup is only needed when overriding those values locally.
Create a local environment file outside source control, for example `docker/environments/.env.local`, with these values:
```dotenv
DB_NAME=notification_service
DB_USER=postgres
DB_PASS=postgres
VOLUME_PREFIX=ns-dev
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_VIRTUAL_HOST=/
RABBITMQ_AMQP_PORT=5672
RABBITMQ_MANAGEMENT_PORT=15672
DEVELOPMENT_EMAIL_SERVICE_NAME=TestService
```
Start the Notification Service stack from the repository root:
```powershell
docker compose --env-file docker/environments/.env.local `
-f docker/environments/docker-compose.yml `
-f docker/environments/docker-compose.Development.yml `
up --build -d
```
Development compose exposes:
- Notification Service admin: `http://localhost:5200/admin/channels` and `/admin/templates`
- RabbitMQ management: `http://localhost:15672`
- Mailpit inbox: `http://localhost:8025`
- Seq: `http://localhost:5342`
The development migrator idempotently creates an active SMTP channel for
`DEVELOPMENT_EMAIL_SERVICE_NAME` using host `mailpit`, port `1025`, SSL disabled, and
blank credentials. It also creates a neutral English `TestEmail` template with
`RecipientName` and `Message` variables. Existing channels and templates are left
unchanged. Use the admin UI to inspect or customize them and to add client-specific
service names, template keys, or languages.
Configure ItemTracker to use the same RabbitMQ host, port, credentials, virtual host, queue `notification.send-email`, and service name. When ItemTracker runs outside Docker against the development stack, the broker is `localhost:5672`; from a Docker container it is the reachable host or shared-network name. Start the ItemTracker Outbox Worker in `RabbitMq` mode, trigger a production-safe notification to an owned test address, then verify:
1. the ItemTracker Outbox row becomes published;
2. RabbitMQ delivers and removes the message from `notification.send-email`;
3. Notification Service logs show the same `MessageId` and `CorrelationId` without the full address;
4. Mailpit shows exactly one rendered email;
5. the Notification Service channel usage counter increments;
6. the client result queue is consumed and the matching outbox row records successful delivery.
## Production configuration and smoke test
Production requires the same `App__RabbitMq__Host`, `Port`, `User`, `Password`, and `VirtualHost` values as ItemTracker's publisher, plus the Notification Service database connection. The production compose file obtains these from deployment environment variables; secrets must stay in the deployment secret store. The queue is declared durable by both producer and consumer.
This Notification Service revision restores the published immutable
`HrynCo.RabbitMq` version `1.0.17` from NuGet.org. Build and deployment environments
therefore require NuGet.org access or a trusted package mirror containing that exact
version.
Before enabling the Worker, verify that the target service has an active SMTP channel and exact-language templates for every queued ItemTracker template key. Inspect any delayed ItemTracker backlog for expired password-reset, verification, or invitation messages before draining it.
For the smoke test, use an owned test account and a non-sensitive notification template. Record the Outbox `Id` and `CorrelationId`, trigger only one message, follow those identifiers through Outbox publication and Notification Service logs, and confirm receipt with the configured SMTP provider. Do not copy payloads, tokens, credentials, full recipient addresses, or rendered URLs into tickets or logs.