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)
| Method | Path | Description |
|---|---|---|
| POST | /communications | Create a communication record |
| POST | /communications/send | Send (queue for async delivery) |
| GET | /communications | List with pagination, filters, search |
| GET | /communications/stats | Get statistics (by businessId) |
| GET | /communications/pdf-templates/available | List PDF template types |
| GET | /communications/:id | Get by ID |
| PATCH | /communications/:id | Update |
| DELETE | /communications/:id | Delete |
| POST | /communications/:id/resend | Resend a communication |
| POST | /communications/:id/process | Manual queue bypass (for testing) |
| GET | /communications/:id/attachments | Get attachments |
Webhooks Controller (/webhooks) — Public, no auth
| Method | Path | Description |
|---|---|---|
| POST | /webhooks/sendgrid/events | SendGrid delivery events |
| POST | /webhooks/twilio/sms-status | Twilio SMS status callbacks |
| GET | /webhooks/labsmobile/sms-status | LabsMobile DLR (token query param) |
| POST | /webhooks/ycloud/whatsapp | YCloud WhatsApp status (whatsapp.message.updated) |
| POST | /webhooks/twilio/messenger-status | Messenger status (stub) |
Channel Adapters
Each adapter implements ICommunicationChannel from domain/communication.types.ts:
| Channel | Provider | Adapter | Notes |
|---|---|---|---|
| SendGrid | EmailAdapterService | HTML + text, attachments, tracking | |
| SMS | Twilio (default) or LabsMobile | SmsAdapterService | SMS_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 |
| YCloud | WhatsAppAdapterService | Meta templates (enqueue) + freeform 24h window | |
| Messenger | Twilio | — | Not yet implemented |
Environment Variables
| Variable | Required | Description |
|---|---|---|
SENDGRID_API_KEY | Yes (email) | SendGrid API key |
SENDGRID_FROM_EMAIL | No | From email (default: noreply@flowandgrow.tech) |
TWILIO_ACCOUNT_SID | Yes (Twilio SMS) | Must start with "AC" |
TWILIO_AUTH_TOKEN | Yes (Twilio SMS) | Twilio auth token |
TWILIO_PHONE_NUMBER | Yes (Twilio SMS) | E.164 format |
SMS_PROVIDER | No | twilio (default) or labsmobile. Unknown values fail closed |
LABSMOBILE_USERNAME | Yes (LabsMobile SMS) | HTTP Basic username |
LABSMOBILE_API_TOKEN | Yes (LabsMobile SMS) | HTTP Basic API token |
LABSMOBILE_SENDER | No | Optional tpoa, max 11 chars. Not guaranteed on all Guatemala routes |
LABSMOBILE_TEST_MODE | No | Real SMS (test: 0) when 0 on localhost or production. Staging always uses test: 1. Unset defaults to test mode |
LABSMOBILE_WEBHOOK_SECRET | Yes (LabsMobile, staging/prod) | Query token for /webhooks/labsmobile/sms-status |
LABSMOBILE_API_BASE_URL | No | Default https://api.labsmobile.com/json |
YCLOUD_API_KEY | Yes (WA) | YCloud API key (X-API-Key) |
YCLOUD_WHATSAPP_FROM | Yes (WA) | Platform WhatsApp number, E.164 |
YCLOUD_WEBHOOK_SECRET | Yes (WA, staging/prod) | HMAC secret for /webhooks/ycloud/whatsapp |
YCLOUD_WHATSAPP_TEMPLATE_LANGUAGE | No | Meta language code (default: es) |
YCLOUD_API_BASE_URL | No | Default https://api.ycloud.com/v2 |
API_URL | Recommended | Twilio SMS statusCallback and LabsMobile ackurl |
FRONTEND_URL | No | For 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
- Domain types decouple application from HTTP layer —
SendCommunicationPayload(domain) vsSendCommunicationDto(interfaces) - Repository injected via token —
"ICommunicationsRepository"enables testing with mock implementations - Attachments stored in DB, excluded from queue — Base64 content is too large for BullMQ payloads; stored in
communicationAttachmenttable and retrieved during send - Rate limiting is in-memory — Token bucket algorithm per provider:channel, not distributed (sufficient for single-instance)
- Webhook endpoints are public — Provider webhooks don't support auth tokens; signature verification should be added for production
- Invite notification logic is shared —
InviteNotificationServiceeliminates duplication between create/resend handlers - Twilio stays the SMS default — LabsMobile is opt-in via
SMS_PROVIDER=labsmobile. Do not enable LabsMobile IP allowlisting (Cloud Run egress is dynamic).tpoais not guaranteed on Guatemala routes. This change does not flip production traffic.