Skip to content

Chapter 30 — Communication Centre

1. Purpose

The Communication Centre is Bizwiz's outbound messaging hub. From one menu, staff can blast Bulk SMS to suppliers/employees/customers, send Bulk WhatsApp notices and marketing to route customers, and compose Emails (primarily to suppliers) with attachments. Each channel has a Create screen, a Test Message screen, and a log / delivery report screen so operators can prove what was sent and whether it landed.

The most important thing to understand about this chapter is that it is two things wearing one hat:

  1. A human-operated blast tool (the screens under this menu) for one-off campaigns and manual notices.
  2. The application-wide notification backbone — the same underlying SmsService and WhatsAppService classes that these screens use are injected into ~146 files across the whole ERP to fire transactional messages: OTPs, approval-queue alerts, delivery notices, payment confirmations, order booking reminders, and more.

So Communication Centre is not a walled garden. It is the visible surface of the shared messaging plumbing that every other book quietly relies on.


Technically, the menu is gated by permission root communication-center___view and is defined in resources/views/admin/includes/sidebar_includes/communication_centre.blade.php (101 lines). It links to three route groups declared in routes/web.php:

  • bulk-sms.* → BulkSmsController (routes/web.php:4793)
  • bulk-whatsapp.* → BulkWhatsappController (routes/web.php:4808)
  • communications.emails.* → EmailController (routes/web.php:4820)

The SMS channel is provider-pluggable: the concrete sender is bound in app/Providers/AppServiceProvider.php:209 off config('app.sms_provider') (default airtouch, config/app.php:284). WhatsApp uses the Meta WhatsApp Cloud API (config/whatsapp.php). Email uses Laravel's standard mailer (SMTP/Mailgun-style, config/mail.php).

2. Users & Roles

Access is permission-driven. The chairman/superadmin (role_id == 1) always sees everything; everyone else needs the granular permissions that mirror the menu tree.


Permission checks come from two places: the blade sidebar ($my_permissions[...]) and the can() helper inside each controller action.

Area View gate Sub-action permissions
Menu root communication-center___view —
Bulk SMS bulk-sms___view bulk-sms___create, bulk-sms___test-message, bulk-sms___message-log
Bulk WhatsApp bulk-whatsapp___view bulk-whatsapp___create, bulk-whatsapp___test-message, bulk-whatsapp___delivery-report
Emails (shown under communication-center___view) none enforced in blade; EmailController has no can() guards

Controller-level enforcement examples: BulkSmsController::create_bulk_message() calls can('create','bulk-sms') (BulkSmsController.php:35); BulkWhatsappController::create() calls can('create','bulk-whatsapp') (BulkWhatsappController.php:35). Notably, EmailController performs no permission checks at all — any authenticated user who can reach the routes can send supplier emails (see §7).

3. Processes

Plain-language: to send a bulk SMS you pick a contact group (Suppliers, Employees, or Customers), narrow it by branch/route/role, type a title + message, choose a sender ID, and submit. The system resolves the phone list, queues a job per number, hits the SMS provider's HTTP API, and writes one log row per recipient with a send status. WhatsApp and Email follow the same "resolve audience → queue → send → record status" shape.


3.1 Bulk SMS send pipeline

flowchart TD
    A[Operator: bulk-sms.create] --> B[save_bulk_message POST]
    B --> C{contact_group?}
    C -->|Suppliers| D[query wa_suppliers via location/stock joins]
    C -->|Employees| E[query users by branch/role]
    C -->|Customers| F[query wa_route_customers by branch/route]
    D & E & F --> G[normalise + dedupe to 0XXXXXXXXX]
    G --> H[SendBulkSms::dispatch]
    H --> I[create bulk_sms parent row]
    I --> J[loop: dispatch SendSms per number]
    J --> K{issn switch}
    K -->|KANINI ids| L[InfoSkySmsService]
    K -->|AIRTOUCH_ISSN| M[AirTouchSmsService]
    K -->|SMS_GATEWAY_SENDER_ID| N[GatewaySmsService]
    L & M & N --> O[provider HTTP API]
    O --> P[insert bulk_sms_messages row + send_status]

Audience resolution lives entirely in BulkSmsController::save_bulk_message() (BulkSmsController.php:52-165): - Suppliers — pulled from wa_suppliers joined through wa_location_and_stores → wa_inventory_location_stock_status → wa_inventory_item_suppliers, filtered by branch (:63-92). - Employees — users filtered by restaurant_id (branch), optional role_id, status='1' (:94-114). - Customers — wa_route_customers joined to routes, filtered by restaurant_id and route ids, supporting all-in-branch / multi-route / hand-picked-via-DataTable selection (:116-141, DataTable at :170-215).

Numbers are deduped and coerced to a local 0XXXXXXXXX form (:145-149), then handed to the queued SendBulkSms job (:157). That job (app/Jobs/SendBulkSms.php) creates the bulk_sms parent, then fans out one SendSms job per number (SendBulkSms.php:50-54). SendSms::handle() switches on the chosen issn (sender ID) to pick the provider by direct instantiation and records the result in bulk_sms_messages (app/Jobs/SendSms.php).

Test Message (bulk-sms.test-message) is a single-recipient version doing the same provider switch inline and logging with category = 'Test SMS' (BulkSmsController.php:239-298).

3.2 WhatsApp & Email pipelines

flowchart LR
    subgraph WhatsApp
    WA1[bulk-whatsapp.create] --> WA2[save: message_type notice/marketing]
    WA2 --> WA3[insert whatsapp_bulk_messages + recipients]
    WA3 --> WA4[ProcessWhatsappBulkMessageJob queue=whatsapp]
    WA4 --> WA5[MetaWhatsAppCloudService.sendNamedTemplate]
    WA5 --> WA6[Meta Graph API graph.facebook.com]
    WA6 --> WA7[store wamid + status on recipient]
    WH[Meta webhook api/whatsapp/messaging] --> WA7
    end
    subgraph Email
    E1[emails.create] --> E2[store: Email + EmailRecipient + attachments]
    E2 --> E3[DeliverEmailJob]
    E3 --> E4[Mail::send SupplierEmailMail via SMTP]
    E4 --> E5[recipients status sent/failed, recalculateStatus]
    end

WhatsApp: BulkWhatsappController::save() validates a Customers-only contact group over route_ids, distinguishes notice vs marketing (marketing requires a start/end date and an image), writes a WhatsappBulkMessage parent + WhatsappBulkMessageRecipient rows, then dispatches ProcessWhatsappBulkMessageJob on the whatsapp queue (BulkWhatsappController.php:94-306). The job chunks pending recipients and calls the Meta service per recipient (app/Jobs/ProcessWhatsappBulkMessageJob.php). Delivery status is updated both by send-time responses (wamid) and asynchronously via the Meta webhook POST /api/whatsapp/messaging (routes/api.php:143-144, WhatsAppWebhookController).

Email: EmailController::store() writes an Email row (status pending), EmailRecipient rows for to/cc/bcc (resolving supplier ids to addresses), and EmailAttachment rows via UploadService, then dispatches DeliverEmailJob (EmailController.php:81-118). The job loads recipients, downloads attachment bytes over HTTP, sends via Mail::to()->cc()->bcc()->send(new SupplierEmailMail(...)), marks each recipient sent/failed, and recomputes the parent status (app/Jobs/DeliverEmailJob.php).

4. Tables Touched & Key Data

Plain-language: SMS logs live in a parent/child pair (bulk_sms campaign → bulk_sms_messages per-recipient rows). WhatsApp mirrors that shape (whatsapp_bulk_messages → whatsapp_bulk_message_recipients). Email uses emails → email_recipients (+ email_attachments). A separate allowed_sms_notifications table is the global on/off switch for each scenario of transactional messaging.


Table Key columns Migration
bulk_sms title, send_group, branch_id (nullable, default 10) 2024_07_03_180133..., 2024_08_26_114226...
bulk_sms_messages bulk_sms_id, created_by, issn, phone_number, message, category (Bulk SMS / Test SMS), send_status, sms_length, sms_response 2024_07_03_185451...
allowed_sms_notifications scenario (text), enabled (bool), is_whatsapp_enabled (bool) 2025_03_31_113629..., 2026_04_07_120000...
whatsapp_bulk_messages created_by, target_group, branch_id, template_name, template_language, message_text, total_recipients, sent_count, failed_count, status, queued_at, completed_at 2026_04_20_100000...
whatsapp_bulk_message_recipients wabm_id, reference_id/reference_type (wa_route_customer), phone_e164, customer_name, status, wamid, payload (json), error_message 2026_04_20_100001...
emails subject, body, from_name, from_email, status (pending/sending/sent/failed/partial), error_message, sent_at, created_by 2026_07_13_132640...
email_recipients email_id, supplier_id, type (to/cc/bcc), recipient_email, recipient_name, status, delivered_at 2026_07_13_132656...
email_attachments email_id, original_filename, url 2026_07_13_132710...

Notes: - No credits/cost table. There is no SMS-credit ledger or per-message pricing table. Cost tracking is effectively "count rows in bulk_sms_messages." An InfoSkySmsService::checkSmsBalance() method exists but only logs the provider's balance response (InfoSkySmsService.php:109). - No opt-out / unsubscribe table was found for SMS or WhatsApp. The only suppression mechanism is the per-scenario allowed_sms_notifications.enabled / is_whatsapp_enabled global toggle. - Message content is stored verbatim in the log tables (bulk_sms_messages.message, emails.body), so the logs double as an audit trail.

5. Interactions with Other Modules

This is the headline section. Communication Centre owns the shared messaging classes, but the transactional traffic that flows through them is generated everywhere else in the ERP.


5.1 Verdict — is this the app-wide messaging backbone?

Yes, decisively — but with an important nuance. There is a single provider-agnostic contract, App\Interfaces\SmsService (two methods: sendMessage, sendOtp), bound once in AppServiceProvider.php:209-239 to whichever concrete provider the tenant configured. Modules obtain it via app(SmsService::class) and never care which gateway is live.

Proof of breadth: ~146 files reference SmsService, and app(SmsService::class)->sendMessage(...) / ->sendOtp(...) is called from across the whole system, including:

  • OTP / auth — app/Support/PettyCashOtpGuard.php:87, app/Model/User.php:455, app/User.php:285.
  • POS & cash — PosCashSalesController.php (multiple), PosBankingController.php:7914, CasheirManagementController.php:578,791.
  • Approvals / requisitions — InternalRequisitionController.php:2753, IssueFullfillRequisitionController.php:843, ReceivePurchasedOrderController.php:2038.
  • Deliveries & transfers — InboundDeliveryController.php:2207, NInventoryLocationTransferController.php (multiple), DeliveryScheduleController.php.
  • Invoices / payments — SalesInvoiceController.php:4051, PropertyInvoiceController.php:786,1112, SendCreditInvoiceNotification.php:167.
  • Cases / help desk — CaseTicketController.php:1877.
  • Queued jobs — ProcessBasketStockUpdatesAndNotifications.php, BalanceUnbalancedInvoicesForShift.php:134, SendTenantAnnouncement.php:133.

There is also a legacy procedural layer in app/helpers.php: a global sendMessage()/sendOtp() that hard-codes new InfoSkySmsService() (helpers.php:855-884) plus an old send_sms() (:812). These bypass the container binding, so some older call sites are pinned to InfoSky regardless of the configured provider — a partial fragmentation of the backbone.

The nuance: the Bulk SMS screens do not use app(SmsService::class). SendBulkSms/SendSms and BulkSmsController::test_message_save() select the provider by a manual switch on the sender-ID (issn) and new up the class directly (SendSms.php:42-61, BulkSmsController.php:249-262). So: transactional messaging = the DI-bound SmsService backbone; bulk blasts = a parallel, sender-ID-routed path that reuses the same provider classes but not the same binding. Same providers, two dispatch mechanisms.

5.2 Scenario governance (the cross-cutting switch)

App\Enums\SmsScenarios enumerates 103 scenarios (e.g. NEW_ORDER_NOTIFICATION_TO_CUSTOMER, PETTY_CASH_NOTIFICATION_TO_FINAL_APPROVERS, PENDING_GRN_CONFIRMATION_NOTIFICATION_TO_AUTHORIZERS). Every provider's sendMessage() first checks AllowedSmsNotification::where('scenario',$scenario) and silently returns if disabled (GatewaySmsService.php:44-49, InfoSkySmsService.php:50-55, AirTouchSmsService.php:16-21). ScenarioNotificationDispatcher (app/Services/ScenarioNotificationDispatcher.php) is a higher-level orchestrator that fans a single business event out to both SMS and WhatsApp based on the scenario's enabled / is_whatsapp_enabled flags — the clearest evidence that this menu's channels are the shared notification substrate for other books (orders, approvals, payments, attendance).

5.3 Email & WhatsApp reuse

WhatsApp: the WhatsAppService / WhatsappNamedTemplateSender interfaces are bound to MetaWhatsAppCloudService (AppServiceProvider.php:195-207) and reused for transactional invoice/order templates (config/whatsapp.php defines new_order_invoice_with_document, issue_continuity_update_with_document, etc.). Email is comparatively siloed: EmailController targets wa_suppliers and there is no evidence of general transactional reuse of the Email model by other modules (system emails elsewhere use Laravel Mail:: / Mailables directly).

6. Alternatives & Variants

Plain-language: SMS is transactional-first and provider-swappable per tenant; WhatsApp is template-driven and customer-facing; Email is a lightweight supplier mailbox. The big variant axis is which SMS gateway a tenant runs.


SMS providers (chosen by config('app.sms_provider'), AppServiceProvider.php:211)

Provider value Class Gateway / endpoint Env keys
airtouch (default) AirTouchSmsService https://client.airtouch.co.ke:9012/sms/api/ AIRTOUCH_SMS_API_KEY, AIRTOUCH_ISSN, AIRTOUCH_USERNAME
infosky InfoSkySmsService https://bulk.infosky.co.ke/api/v1/send-sms (Bearer token) KANINI_SMS_SENDER_ID, KANINI_SMS_SENDER_ID_2, KANINI_SMS_TOKEN
olivetree OliveTreeSmsService OliveTree Kenya SMS API (see OliveTreeSmsService.php)
gateway GatewaySmsService http://getapi.smsgateway.co.ke/send (SMS Gateway Kenya) SMS_GATEWAY_ACCESS_TOKEN, SMS_GATEWAY_SENDER_ID, SMS_GATEWAY_ENABLED (config/sms_gateway.php)
log LogSmsService none — writes to log only (dev/test) —

All providers are Kenyan SMS aggregators (AirTouch, InfoSky, OliveTree, SMS Gateway Kenya); there is no Africa's Talking or Twilio SMS integration. Each provider self-heals: if its own credentials are missing it falls back to app(SmsService::class) (e.g. GatewaySmsService.php:54-60).

WhatsApp — Meta WhatsApp Cloud API (direct, no BSP)

MetaWhatsAppCloudService (app/Services/MetaWhatsAppCloudService.php, 714 lines) posts to the Meta Graph API (config/whatsapp.php: WHATSAPP_GRAPH_VERSION=v22.0, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_ACCESS_TOKEN). It is template-based (Meta pre-approved named templates), supports notice vs marketing message types with an image header, and has an inbound side (WhatsAppInbound* services + /api/whatsapp/messaging webhook) for two-way/session messaging and delivery receipts. This is a direct Meta Cloud integration, not a BSP like Twilio/360dialog.

Email — Laravel mailer, "Mailbox" = sent-items table

Transport is Laravel's standard mailer (config/mail.php: MAIL_DRIVER default smtp, Mailgun-compatible host defaults). The "Mailbox" (communications.emails.index) is not an inbound IMAP mailbox — it is a paginated view of the emails table (sent/outbound records only). There is no inbound email ingestion.

7. Open Questions

Code-proven facts: - SMS backbone confirmed: single SmsService interface, container-bound in AppServiceProvider.php:209, consumed by ~146 files via app(SmsService::class). - 103 governed scenarios in App\Enums\SmsScenarios; global on/off via allowed_sms_notifications. - Bulk SMS uses a separate sender-ID switch dispatch (SendSms.php), not the DI binding. - WhatsApp = direct Meta Cloud API; Email = Laravel mailer, outbound-only "mailbox." - EmailController has no can() permission guards (EmailController.php — verified all methods). - config/sms_gateway.php:14 ships a hard-coded default access token in source — a real credential-leak concern if .env is unset.

Inference / unverified (needs runtime or deeper read): - Whether any tenant actually runs sms_provider=infosky/olivetree in production, or all default to airtouch — not determinable from static config. - Exact SMS delivery-report handling: bulk_sms_messages.send_status is set from the synchronous HTTP response only; I found no async DLR webhook for SMS (unlike WhatsApp), so "delivered" vs "accepted by gateway" is likely conflated. - The legacy helpers.php sendMessage() pins to InfoSky (:858); how many live call sites still route through it (vs the DI binding) was not exhaustively counted. - No scheduling table for Bulk SMS was found; WhatsApp marketing has campaign start/end dates but whether sends are actually deferred to those dates vs recorded-only was not traced into the job. - Per-tenant WhatsApp credentials: MetaWhatsAppCloudService::resolveCredentials() referenced but its multi-tenant resolution logic not fully read.

8. Source References

  • resources/views/admin/includes/sidebar_includes/communication_centre.blade.php:1-101 — nav, permissions.
  • routes/web.php:4793 (bulk-sms), :4808 (bulk-whatsapp), :4820 (communications.emails); routes/api.php:143-144 (WhatsApp webhook).
  • app/Http/Controllers/Admin/BulkSmsController.php:29,35,52-165,239-298,300-383 — audience resolution, test, logs.
  • app/Http/Controllers/Admin/BulkWhatsappController.php:27-306 — WhatsApp create/save/dispatch.
  • app/Http/Controllers/EmailController.php:1-141 — email compose/store (no can() guards).
  • app/Jobs/SendBulkSms.php, app/Jobs/SendSms.php — bulk SMS fan-out + sender-ID switch.
  • app/Jobs/ProcessWhatsappBulkMessageJob.php, app/Jobs/DeliverEmailJob.php — WhatsApp & email delivery.
  • app/Providers/AppServiceProvider.php:195-239 — WhatsApp + SMS provider bindings.
  • app/Interfaces/SmsService.php — the backbone contract.
  • app/Services/GatewaySmsService.php, InfoSkySmsService.php, AirTouchSmsService.php, OliveTreeSmsService.php, LogSmsService.php — SMS providers.
  • app/Services/MetaWhatsAppCloudService.php:1-70 — Meta Cloud sender.
  • app/Services/ScenarioNotificationDispatcher.php:1-40 — SMS+WhatsApp fan-out orchestrator.
  • app/Enums/SmsScenarios.php — 103 notification scenarios.
  • app/Models/AllowedSmsNotification.php — scenario toggle.
  • app/helpers.php:812,833,855,879 — legacy procedural senders (pinned to InfoSky).
  • config/app.php:284 (sms_provider), config/sms_gateway.php, config/whatsapp.php, config/mail.php — provider config.
  • Migrations: 2024_07_03_180133/185451, 2024_08_26_114226, 2025_03_31_113629, 2026_04_07_120000, 2026_04_20_100000/100001, 2026_07_13_132640/132656/132710.

The Communication Centre is best read not as one feature but as the ERP's shared voice: a handful of pluggable Kenyan SMS gateways, a direct Meta WhatsApp Cloud integration, and a light supplier email tool — all surfaced here for humans, and all quietly reused by every other book to keep customers, staff, and approvers informed.