Saltar al contenido principal

Communications Module

Multi-channel notification system for FlowPOS supporting Email (SendGrid), SMS (Twilio by default, LabsMobile opt-in), WhatsApp (YCloud / Meta), and PDF generation (Puppeteer).

Architecture​

The module follows Hexagonal Architecture (ports & adapters):

communications/
├── domain/ # Pure domain types & contracts
│ ├── communication.types.ts # SendCommunicationPayload, ICommunicationChannel, etc.
│ ├── communications-repository.domain.ts # ICommunicationsRepository port
│ ├── sms-transport.ts # SMS_PROVIDER resolver (twilio | labsmobile)
│ ├── ycloud-whatsapp.mappers.ts # Status + named→positional template body vars
│ └── html-utils.ts # Shared HTML-to-text utility
├── application/ # Business logic & orchestration
│ ├── communications.service.ts # Main orchestration service
│ ├── adapters/ # Channel adapter implementations
│ │ ├── email-adapter.service.ts
│ │ ├── sms-adapter.service.ts
│ │ └── whatsapp-adapter.service.ts
│ ├── services/ # Supporting services
│ │ ├── channel-factory.service.ts
│ │ ├── template-renderer.service.ts
│ │ ├── attachment-handler.service.ts
│ │ ├── recipient-resolver.service.ts
│ │ ├── invite-notification.service.ts
│ │ ├── rate-limiter.service.ts
│ │ ├── event-tracking.service.ts
│ │ ├── communication-helpers.service.ts
│ │ ├── communication-pdf.service.ts
│ │ └── public-document-link.service.ts
│ ├── events/ # Event handlers (event-driven)
│ │ ├── on-create-invite.handler.ts
│ │ ├── on-resend-invite.handler.ts
│ │ ├── on-create-low-stock-alert.handler.ts
│ │ ├── on-queue-job-processed.handler.ts
│ │ └── on-communication-status-changed.handler.ts
│ └── use-cases/
│ └── generate-communication-pdf.use-case.ts
├── infrastructure/ # External service integrations
│ ├── communications.repository.ts # Kysely database implementation
│ └── providers/
│ ├── twilio.provider.ts # Twilio SMS client
│ ├── labsmobile.provider.ts # LabsMobile SMS HTTP POST JSON client
│ ├── labsmobile-webhook-verifier.service.ts # Query-token for LabsMobile DLR
│ ├── ycloud.provider.ts # YCloud WhatsApp HTTP client (enqueue only)
│ └── ycloud-webhook-verifier.service.ts # HMAC for YCloud webhooks
├── interfaces/ # HTTP layer
│ ├── communications.controller.ts # CRUD + send + process endpoints
│ ├── webhooks.controller.ts # Provider webhook handlers
│ ├── dtos/ # Request validation
│ └── query/ # Pagination/filter queries
└── templates/ # HTML email templates

Dependency Rules​

  • Domain has zero framework dependencies
  • Application depends only on domain ports (interfaces)
  • Infrastructure implements domain ports
  • Interfaces map HTTP requests to application calls

The service depends on ICommunicationsRepository (injected via token "ICommunicationsRepository"), not the concrete CommunicationsRepository.

Key Flows​

Send Communication​

POST /communications/send
→ CommunicationsService.send()
→ Validate channel provider configured
→ Check recipient preferences (opt-out)
→ Render template if templateId provided
→ Validate attachments
→ Create communication record (status=PENDING)
→ Save attachments to DB
→ Enqueue to BullMQ (CommunicationQueueModule)
→ Return communication record

Queue Processing​

Queue processor picks job
→ CommunicationsService.executeSend()
→ Retrieve attachments from DB
→ ChannelFactory.getAdapter(channel)
→ adapter.send(payload) → SendGrid / Twilio or LabsMobile SMS / YCloud WhatsApp API
→ Update communication record (status, messageId, provider response)

Webhook Delivery Tracking​

POST /webhooks/sendgrid/events      (public, ECDSA signature)
POST /webhooks/twilio/sms-status (public, Twilio signature)
GET /webhooks/labsmobile/sms-status (public, query token)
POST /webhooks/ycloud/whatsapp (public, YCloud-Signature HMAC)

→ Map provider status → CommunicationStatus
→ Find communication by providerMessageId
→ Update status + timestamp
→ Trigger OnCommunicationStatusChangedHandler

Recipient Resolution (Low Stock Alerts)​

OnCreateLowStockAlertEvent triggered
→ RecipientResolverService.resolveRecipients()
→ Query active recipient rules (role / group / ad_hoc)
→ Resolve each rule to actual contacts
→ Deduplicate
→ Send notification to each resolved recipient

API Endpoints​

Communications Controller (/communications)​

MethodPathDescription
POST/communicationsCreate a communication record
POST/communications/sendSend (queue for async delivery)
GET/communicationsList with pagination, filters, search
GET/communications/statsGet statistics (by businessId)
GET/communications/pdf-templates/availableList PDF template types
GET/communications/:idGet by ID
PATCH/communications/:idUpdate
DELETE/communications/:idDelete
POST/communications/:id/resendResend a communication
POST/communications/:id/processManual queue bypass (for testing)
GET/communications/:id/attachmentsGet attachments

Webhooks Controller (/webhooks) — Public, no auth​

MethodPathDescription
POST/webhooks/sendgrid/eventsSendGrid delivery events
POST/webhooks/twilio/sms-statusTwilio SMS status callbacks
GET/webhooks/labsmobile/sms-statusLabsMobile DLR (token query param)
POST/webhooks/ycloud/whatsappYCloud WhatsApp status (whatsapp.message.updated)
POST/webhooks/twilio/messenger-statusMessenger status (stub)

Channel Adapters​

Each adapter implements ICommunicationChannel from domain/communication.types.ts:

ChannelProviderAdapterNotes
EmailSendGridEmailAdapterServiceHTML + text, attachments, tracking
SMSTwilio (default) or LabsMobileSmsAdapterServiceSMS_PROVIDER selects transport. Twilio: E.164. LabsMobile: HTTP POST JSON (long/nofilter; ucs2 only when the body is outside GSM-7), DLR via GET webhook
WhatsAppYCloudWhatsAppAdapterServiceMeta templates (enqueue) + freeform 24h window
MessengerTwilio—Not yet implemented

Environment Variables​

VariableRequiredDescription
SENDGRID_API_KEYYes (email)SendGrid API key
SENDGRID_FROM_EMAILNoFrom email (default: noreply@flowandgrow.tech)
TWILIO_ACCOUNT_SIDYes (Twilio SMS)Must start with "AC"
TWILIO_AUTH_TOKENYes (Twilio SMS)Twilio auth token
TWILIO_PHONE_NUMBERYes (Twilio SMS)E.164 format
SMS_PROVIDERNotwilio (default) or labsmobile. Unknown values fail closed
LABSMOBILE_USERNAMEYes (LabsMobile SMS)HTTP Basic username
LABSMOBILE_API_TOKENYes (LabsMobile SMS)HTTP Basic API token
LABSMOBILE_SENDERNoOptional tpoa, max 11 chars. Not guaranteed on all Guatemala routes
LABSMOBILE_TEST_MODENoReal SMS (test: 0) when 0 on localhost or production. Staging always uses test: 1. Unset defaults to test mode
LABSMOBILE_WEBHOOK_SECRETYes (LabsMobile, staging/prod)Query token for /webhooks/labsmobile/sms-status
LABSMOBILE_API_BASE_URLNoDefault https://api.labsmobile.com/json
YCLOUD_API_KEYYes (WA)YCloud API key (X-API-Key)
YCLOUD_WHATSAPP_FROMYes (WA)Platform WhatsApp number, E.164
YCLOUD_WEBHOOK_SECRETYes (WA, staging/prod)HMAC secret for /webhooks/ycloud/whatsapp
YCLOUD_WHATSAPP_TEMPLATE_LANGUAGENoMeta language code (default: es)
YCLOUD_API_BASE_URLNoDefault https://api.ycloud.com/v2
API_URLRecommendedTwilio SMS statusCallback and LabsMobile ackurl
FRONTEND_URLNoFor links in templates (default: localhost:5173)

Status Lifecycle​

PENDING → QUEUED → SENT → DELIVERED → OPENED → CLICKED
↘ FAILED
↘ BOUNCED

Status mapping is centralized in TwilioProvider.mapTwilioStatus(), TwilioProvider.mapSendgridEvent(), LabsMobileProvider.mapDeliveryStatus() (handset+ok / DELIVRD → delivered, operator+ok → sent, error/UNDELIV/REJECTD/EXPIRED/BLOCKED → failed), and mapYcloudStatus() (accepted/sent → sent, delivered → delivered, read → opened, failed → failed). LabsMobile getStatus() returns pending; delivery is webhook-only.

Key Design Decisions​

  1. Domain types decouple application from HTTP layer — SendCommunicationPayload (domain) vs SendCommunicationDto (interfaces)
  2. Repository injected via token — "ICommunicationsRepository" enables testing with mock implementations
  3. Attachments stored in DB, excluded from queue — Base64 content is too large for BullMQ payloads; stored in communicationAttachment table and retrieved during send
  4. Rate limiting is in-memory — Token bucket algorithm per provider:channel, not distributed (sufficient for single-instance)
  5. Webhook endpoints are public — Provider webhooks don't support auth tokens; signature verification should be added for production
  6. Invite notification logic is shared — InviteNotificationService eliminates duplication between create/resend handlers
  7. Twilio stays the SMS default — LabsMobile is opt-in via SMS_PROVIDER=labsmobile. Do not enable LabsMobile IP allowlisting (Cloud Run egress is dynamic). tpoa is not guaranteed on Guatemala routes. This change does not flip production traffic.