Communications Domain (schema: communications) โ
An independent Shared Core Domain providing omnichannel inbox and trigger-based automated messaging to Backoffice, Commerce, and Operations.
โ Hub: See domain-model.md for the bounded context map and cross-boundary integration surface.
Entity Relationship Diagram โ
Cross-context references (soft FKs, not shown above):
CHANNEL_ACCOUNTโ provisioned per BackofficeOPERATORCONTACTโ references BackofficePERSON_PROFILEvia soft FK (when the contact is a known person in the CRM)CONVERSATIONโ links to at most one of: CommerceBOOKING, OperationsSERVICE_LEG, or CommerceINVOICE(exclusive nullable FKs).
Entity Dictionary โ
- ChannelAccount: A configured communication endpoint provisioned per tenant via CPaaS providers (Meta Cloud API, Amazon SES, Amazon SNS). Carries
channel_type(WHATSAPP, EMAIL, WEBCHAT, SMS), typedprovider_configJSONB (shape varies by channel โ see schema-communications.md), and a lifecycle status (PENDING_VERIFICATION โ ACTIVE โ SUSPENDED โ REVOKED). Each tenant has at most one channel account per type. Tracks the operator's canonical sender identity (E.164 phone number, verified email domain) and an operator-editable display name. Sensitive provider secrets are encrypted at rest viapgsodium. See channel-provisioning-protocol.md for registration flows and lifecycle management. - Contact: Unified identity for an external person (customer, driver, vendor) merging cross-channel communication threads. Links to
PersonProfile(Backoffice) via soft FK when the contact is a known person in the CRM. Carriesidentifiers[](email addresses, phone numbers) for cross-channel de-duplication. When a booker is identified on a Booking (viabooker_profile_id), Communications auto-creates or matches aContactfor the booker'sPersonProfileusing email/phone de-duplication. This ensures the booker is reachable for transactional messages (confirmations, invoices, payment reminders). See ADR-037. - Conversation: The centralized container for a communication thread. Supports agent assignment (claim-based routing), snooze timers, and per-agent unread tracking. Uses exclusive nullable foreign keys instead of polymorphic associations to maintain Hasura GraphQL compatibility:
booking_id(Commerce),service_leg_id(Operations),invoice_id(Commerce)CHECK (num_nonnulls(booking_id, service_leg_id, invoice_id) <= 1)โ enforces that each conversation links to at most one business context- This permits the frontend to execute a single GraphQL query fetching an inbox thread with its deeply nested domain context (e.g., live telemetry for a service leg or line items for a booking).
- ConversationReadCursor: Per-agent read position tracking within a conversation. Enables unread count computation via a Hasura computed field. See inbox-protocol.md ยง6.
- Message: Individual communication payloads linked to a
Conversation, aChannelAccount, and aContact. Tracks the full delivery lifecycle from dispatch through provider confirmation, with idempotency dedup (via Hasuraevent_id) and fallback chain support. Carriesdirection(INBOUND, OUTBOUND),content_type(TEXT, TEMPLATE, MEDIA),rendered_content,template_id(soft FK to BackofficeNotificationTemplatefor automated messages),status(QUEUED, SENT, DELIVERED, READ, FAILED), and provider-confirmed delivery/read timestamps. Replaces the former Commerce-ownedCommunicationLogby centralizing all messaging records โ both automated and agent-initiated โ in a single audit trail. See notification-pipeline-protocol.md for the dispatch pipeline.
Trigger-Based Automation: The Communications schema does not poll domain pillars. Instead, Hasura Event Triggers fire webhooks to the Communications service when domain mutations occur (e.g., ServiceLeg status โ "Delayed"). Hasura Scheduled Triggers handle time-based workflows (e.g., 24-hour pre-departure reminders). Commerce and Operations remain completely ignorant of messaging protocols โ they only mutate their own state. See workflow-orchestration.md for the event trigger implementation patterns.
Booking Confirmation Workflow: When the Communications service receives a
BookingConfirmedevent trigger from Commerce, it generates the booking confirmation PDF and sends it to the booker (bookings.booker_profile_id) per the transactional routing split (ADR-037 ยง4). If the associatedtour_offering.is_pauschalreiseis true, the Notification pipeline must also fetch and attach BOTH the "Formblatt zur Unterrichtung des Reisenden" (auto-generated standard PDF Anlage 11) AND the operator'ssicherungsschein_urlPDF alongside the confirmation email/message.[planned โ Phase 1.x]When the booker and primary contact passenger differ, Communications should additionally send a separate operational "Your trip details" message to the primary contact (passengers WHERE is_primary_contact = true) containing only the travel-relevant subset (dates, boarding point, itinerary) โ no financial data.
Aggregates โ
Each aggregate defines a transactional consistency boundary. Cross-aggregate references follow ADR-036.
Conversation โ
| Role | Entity |
|---|---|
| Root | Conversation |
| Child | ConversationReadCursor |
Invariants: Status: OPEN โ RESOLVED / SNOOZED. At most one business context link (booking_id, service_leg_id, invoice_id) via CHECK constraint. last_message_at maintained by Postgres trigger (not aggregate logic). Cross-aggregate refs: contact_id is a soft FK (contact de-duplication/merge must not cascade to conversations).
Message is NOT a child of Conversation. Messages form an unbounded collection. CPaaS webhooks update individual messages by
external_message_idwithout loading the Conversation aggregate. Thelast_message_atdenormalization uses a Postgres trigger, deliberately decoupled from aggregate loading.
Message โ
Single-entity aggregate. Individual communication payload.
Invariants: Status: QUEUED โ PENDING_REVIEW โ SENT โ DELIVERED โ READ โ FAILED / SUPERSEDED. Idempotency via UNIQUE(correlation_id). external_message_id provides provider identity for webhook lookups. Cross-aggregate refs: conversation_id and channel_account_id are soft FKs (Message has own webhook-driven lifecycle; channel can be SUSPENDED while messages still receive delivery callbacks).
ChannelAccount โ
Single-entity aggregate. Operator's CPaaS endpoint.
Invariants: Status: PENDING_VERIFICATION โ ACTIVE โ SUSPENDED โ REVOKED. One channel account per type per tenant. Provider secrets encrypted via pgsodium.
Contact โ
Single-entity aggregate. Unified external person identity.
Invariants: De-duplicated by identifiers JSONB (email addresses, phone numbers).
Infrastructure Entities (not aggregates) โ
change_events.