Busflow Docs

Internal documentation portal

Skip to content
Reviewed 19 May 2026

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 โ€‹

100%

Cross-context references (soft FKs, not shown above):

  • CHANNEL_ACCOUNT โ† provisioned per Backoffice OPERATOR
  • CONTACT โ†’ references Backoffice PERSON_PROFILE via soft FK (when the contact is a known person in the CRM)
  • CONVERSATION โ†’ links to at most one of: Commerce BOOKING, Operations SERVICE_LEG, or Commerce INVOICE (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), typed provider_config JSONB (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 via pgsodium. 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. Carries identifiers[] (email addresses, phone numbers) for cross-channel de-duplication. When a booker is identified on a Booking (via booker_profile_id), Communications auto-creates or matches a Contact for the booker's PersonProfile using 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, a ChannelAccount, and a Contact. Tracks the full delivery lifecycle from dispatch through provider confirmation, with idempotency dedup (via Hasura event_id) and fallback chain support. Carries direction (INBOUND, OUTBOUND), content_type (TEXT, TEMPLATE, MEDIA), rendered_content, template_id (soft FK to Backoffice NotificationTemplate for automated messages), status (QUEUED, SENT, DELIVERED, READ, FAILED), and provider-confirmed delivery/read timestamps. Replaces the former Commerce-owned CommunicationLog by 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 BookingConfirmed event 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 associated tour_offering.is_pauschalreise is 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's sicherungsschein_url PDF 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 โ€‹

RoleEntity
RootConversation
ChildConversationReadCursor

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_id without loading the Conversation aggregate. The last_message_at denormalization 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.

Internal documentation โ€” Busflow