Files
hrynco-notification-service/docs/itemtracker-outbox-email-consumer.md
agrynco 81654c0d33 feat(templates): add service language template management
Add normalized service and template catalogs, scoped administration, migration, validation, and regression coverage.

Ref: IT-1601
2026-10-17 22:02:43 +03:00

8.4 KiB

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 worker first resolves a template by exact ServiceName, TemplateKey and the requested LanguageCode. If that exact version is missing, it makes one compatibility lookup using LanguageCode.Trim().ToLowerInvariant(), only when the result differs ordinally. Thus RU selects an existing exact RU version before ru, while RU or ru can resolve ru when their exact version is absent. There is no case-insensitive scan, regional substitution or fallback to another language; RU-ru does not search for ru-RU or ru. Blank codes are rejected. 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.

The template model now requires the transactional email template localization migration. Existing channel selection, SMTP settings, quota checks, and usage counters remain in place. Exact language codes, including regional variants, are preserved. API requests remain exact; worker sends retain only the bounded compatibility retry described above. Prefer the configured code to make the selected version explicit.

Local end-to-end setup

With Docker Desktop running, install and start the complete Notification Service development stack from the repository root:

.\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:

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:

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.

The template administration list preserves optional Service Name and Key filters when an administrator opens the create or edit screen. These links use explicit Razor expression boundaries so the filter query is appended as query parameters rather than rendered as a literal @filterQuery path segment. With no active filters, the create route is exactly /admin/templates/create.

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.