Skip to content

SMS Integrations (Yuboto, Routee, Plivo, Bulker, Apifon, OmniMessaging)

Flow ID: IN-14 | Module(s): ecommercen/helpers/, src/Yuboto/, src/Bulker/, src/Apifon/, src/Omni/ | Complexity: High | Last Updated: 2026-06-28

Business Overview

Ecommercen sends SMS (and optionally Viber) notifications to customers for order status updates, primarily when an order is ready for pickup ("from store" status). The platform supports six SMS providers through a unified dispatcher function, allowing each client to select their preferred provider via configuration.

The SMS system handles two distinct use cases:

  1. Order-triggered SMS (sendSms()) -- Sends a predefined order notification message with special Friday/Saturday delivery handling. Records the message in customer communication history.
  2. Generic SMS dispatch (sendSmsMsg()) -- Sends arbitrary SMS/Viber messages through the configured provider. Used by jobs, gift card notifications, and other subsystems.

All providers share a common phone validation and normalization layer optimized for Greek mobile numbers.

API Reference

Unified Dispatcher Functions

FunctionParametersDescription
sendSms($orderSerial, $phone, $provider, $extra)string, string, string, arrayOrder notification SMS with history logging
sendSmsMsg($msg, $viberMsg, $phone, $provider, $extra)string, string, string, string, arrayGeneric SMS/Viber dispatch
isValidPhoneForSms($phone)stringValidates Greek mobile number format
convertToValidPhoneForSms($phone)stringNormalizes to 30xx format
getFirstValidPhoneForSms($phones)arrayReturns first valid phone from array
getFirstValidPhoneForSmsFormatted($phones)arrayReturns first valid phone, normalized
getValidPhoneForPayway($payway, ...)string, stringsReturns valid phone for specific payment methods

Provider-Specific APIs

ProviderClassChannelsFeatures
YubotoLegacy helper functionsSMSCallback URL support
YubotoOmniAdvisable\Yuboto\YubotoOmniSMS + ViberPriority-based channel selection
OmniMessagingAdvisable\Omni\OmniMessagingSMS + ViberTransaction ID tracking, validity periods
RouteeLegacy helper functionsSMS, SMS+ViberDLR callback URL
PlivoLegacy helper functionsSMS onlyBasic send
BulkerAdvisable\Bulker\BulkerSmsSMS onlyStatus polling, delivery reports
ApifonAdvisable\Apifon\ApifonSMS + ViberOAuth2, TTL-based Viber-to-SMS fallback, callback URL support

Code Flow

Order Notification SMS

Order status changes to "from_store"
  -> AdvSendEmailAndSMSBasedOnFromStoreStatus job (or controller)
     -> sendSms($orderSerial, $phone, $provider, $extra)
        -> Determine message based on day of week:
           - Friday: Saturday delivery message (t('sms.order.message.saturday.*'))
           - Other days: Standard message (t('sms.order.message.*'))
        -> Build Viber message variant (t('viber.order.from_store.message*'))
        -> Log to customer_message_history_model (type: ORDER_ON_STORE, channel: SMS)
        -> Route to provider via switch/case

Provider Dispatch Flow

sendSmsMsg($msg, $viberMsg, $phone, $provider, $extra)
  switch ($provider):
    'plivo'         -> load plivo_helper   -> sendSmsPlivo($msg, $phone)
    'routee'        -> load routee_helper  -> sendSmsRoutee($msg, $phone, $callbackUrl)
    'routeeViber'   -> load routee_helper  -> sendViberSmsRoutee($msg, $phone, $callbackUrl)
    'yuboto'        -> load yuboto_helper  -> sendSmsYuboto($msg, $phone, $callbackUrl)
    'yuboto_omni'   -> load yuboto_omni config -> YubotoOmni::sendMessage($msg, $viberMsg, $phone, $callbackUrl)
    'omni_messaging'-> load omni config    -> OmniMessaging::sendMessage($msg, $viberMsg, $phone, $orderId)
    'bulker'        -> load bulker config  -> BulkerSms::sendMessage($msg, $phone, $orderId)
    'apifon'        -> di()->get(Apifon::class) -> Apifon::sendMessage($phone, $msg, $viberMsg, $callbackUrl)

YubotoOmni Channel Priority

YubotoOmni::sendMessage($msg, $viberMsg, $phone, $callbackUrl)
  -> Build JSON payload:
     - phonenumbers: $phone
     - dlr: true if callbackUrl provided
  -> If Viber message provided:
     - viber: { sender, text, priority: 0 }  (primary)
  -> If SMS message provided:
     - sms: { sender, text, priority: 1 if Viber exists, else 0 }  (fallback)
  -> POST to {apiBaseUrl}/omni/v1/Send
  -> Auth: Basic (base64-encoded API key)
  -> Check response: ErrorCode == 0, Message[0].status not Error/Failed

OmniMessaging Dual Channel

OmniMessaging::sendMessage($msg, $viberMsg, $phone, $orderId)
  -> Build JSON payload:
     - transaction_id: $orderId
     - channels: [
         { viber: { message: { text }, validity_period: 10 } },
         { sms: { from: sender_name, text, encoding: 0, validity_period: 120 } }
       ]
     - destinations: [{ phone_number }]
  -> POST to {apiBaseUrl}/sendings
  -> Auth: Basic (base64-encoded userID:authKey)

Apifon SMS with Viber Fallback

Apifon::sendMessage($to, $msg, $viberMsg[, $callbackUrl = ''])
  -> authenticate() if not already authenticated (OAuth2 client_credentials)
  -> If enabledIm (Viber enabled):
     - POST to /services/api/v1/im/send
     - Include im_channels with TTL for Viber-to-SMS fallback
  -> Else:
     - POST to /services/api/v1/sms/send
  -> Body: message (text, sender_id), subscribers (number)
  -> If $callbackUrl non-empty: injected as `callback_url` in both SMS and IM/Viber payloads

$callbackUrl is optional (defaults to ''). The upstream dispatcher in sms_helper.php extracts $extra['callback_url'] (defaulting to '' when absent) and passes it as the 4th argument, consistent with how yuboto, routee, and routeeViber are handled. No upstream caller sets callback_url in $extra for the apifon case yet, so the default path is unchanged; a client can supply a DLR endpoint via its own shopmodule_helper override (#400).

Bulker Status Polling

AdvGetBulkerSmsStatus job (cron)         # legacy (ecommercen/job/libraries/)
GetBulkerSmsStatus job (cron)            # modern port (src/Domains/Order/Jobs/, #155)
  -> Query orders with sms_status = 4 (buffered)
  -> For each order:
     -> BulkerSms::getSmsStatus($orderId)
        -> GET {apiBaseUrl}/dlr.php?auth_key={key}&id={orderId}
        -> Map response (switch on content_response[2]):
             "2"     -> return 3 (undelivered)
             "4"     -> return 4 (buffered)
             default -> return 2 (delivered)  # includes gateway response "1"
           Any API error / empty content / non-OK HTTP -> return 4 (buffered, fail-safe)
     -> Update shop_order.sms_status

Modern port (#155). Advisable\Domains\Order\Jobs\GetBulkerSmsStatus is the PSR-4 port of the legacy AdvGetBulkerSmsStatus. It reads buffered orders via Repository::findOrderIdsBySmsStatus(int $smsStatus): int[] — a projected SELECT id FROM shop_order WHERE sms_status = ? that returns only order IDs rather than hydrating full Order entities (as match() would), mirroring the legacy job's narrow SELECT id, status, sms_status query (src/Domains/Order/Order/Repository/Repository.php:51-77). Writes go through the Order WriteRepository. The gateway call is the same BulkerSms::getSmsStatus() the legacy Bulker library wrapped, so the status-code mapping is unchanged. One behavioural refinement: an order that comes back still-buffered (4) is skipped rather than re-written with the identical value.

Registered in src/Domains/Order/container.php and in the commandOptions array in application/config/jobs.php:206 — the commandOptions entry is required wiring alongside container registration; without it the dispatcher cannot resolve the FQCN. The jobs.php schedule entry ships commented out (operator opt-in), so the legacy job stays the active path until an operator flips it.

Note on unconfigured deployments: createBulkerClient() (GetBulkerSmsStatus.php:93-124) returns null only when bulker config is absent or lacks apiBaseUrl/auth_key. The shipped placeholder config (apiBaseUrl='http://api.bulker.gr/http', auth_key='XXXXXXXXXXXXXXXXXX') is non-empty, so enabling the schedule on a deployment that has not set real credentials will still build a client and call the gateway — gateway errors map to status 4 (buffered) and are skipped; no order data is corrupted. Configure application/config/bulker.php before enabling the schedule.

Architecture

ecommercen/helpers/
  sms_helper.php               # Unified dispatcher (sendSms, sendSmsMsg, phone validation)
  yuboto_helper.php            # Yuboto legacy SMS helper
  routee_helper.php            # Routee SMS/Viber helper
  plivo_helper.php             # Plivo SMS helper

src/Yuboto/
  YubotoOmni.php               # YubotoOmni SMS+Viber client (priority-based channels)

src/Omni/
  OmniMessaging.php            # OmniMessaging SMS+Viber client (dual channel)

src/Bulker/
  BulkerSms.php                # Bulker SMS client with delivery status polling

src/Apifon/
  Apifon.php                   # Apifon OAuth2 SMS+Viber client

ecommercen/job/libraries/
  AdvSendEmailAndSMSBasedOnFromStoreStatus.php   # Job: SMS on from_store status
  AdvSendEmailAndSMSBasedOnSentStatus.php        # Job: SMS on sent status
  AdvGetBulkerSmsStatus.php                      # Job: Bulker delivery status polling (legacy)

src/Domains/Order/Jobs/
  GetBulkerSmsStatus.php                         # Job: Bulker delivery status polling (modern port, #155)

ecommercen/gift_cards/jobs/
  GiftCardSendSms.php          # Gift card SMS notification

application/libraries/
  Yuboto.php                   # Client-overridable Yuboto library
  Routee.php                   # Client-overridable Routee library
  Bulker.php                   # Client-overridable Bulker library

application/config/
  yuboto.php                   # Yuboto legacy config
  yuboto_omni.php              # YubotoOmni config
  routee.php                   # Routee config
  plivo.php                    # Plivo config
  bulker.php                   # Bulker config

Provider Authentication

ProviderAuth MethodDetails
Yuboto (legacy)Config-basedAPI key in helper config
YubotoOmniBasic AuthBase64-encoded API key
OmniMessagingBasic AuthBase64-encoded userID:authKey
RouteeConfig-basedVia routee_helper config
PlivoConfig-basedVia plivo_helper config
BulkerQuery parameterauth_key in URL
ApifonOAuth2 Bearerclient_credentials grant, Bearer token

Data Model

Tables

TableColumnDescription
shop_ordersms_statusSMS delivery status: 1=sent, 2=delivered, 3=undelivered, 4=buffered
customer_message_historyVariousCommunication log: customer_id, type (ORDER_ON_STORE), channel (SMS), subject, body

SMS Status Values

ValueMeaningContext
1SentInitial state after successful send
2DeliveredConfirmed delivery (Bulker polling)
3UndeliveredFailed delivery (Bulker polling)
4BufferedIn transit, pending confirmation

Configuration

Provider Config Files

ProviderConfig FileKey Settings
Yubotoapplication/config/yuboto.phpAPI URL, sender name, API key
YubotoOmniapplication/config/yuboto_omni.phpapiBaseUrl, apiKey, smsSender, viberSender, timeout
Routeeapplication/config/routee.phpAPI credentials, sender
Plivoapplication/config/plivo.phpAuth ID, auth token, sender
Bulkerapplication/config/bulker.phpapiBaseUrl, auth_key, sender_name

Apifon (Registry)

GroupKeyDescription
APIFONENABLEDEnable/disable Apifon
APIFONSENDER_IDSMS sender name
APIFONTTLViber TTL before SMS fallback (seconds)
APIFONENABLE_IMEnable Viber channel
APIFONAPI_TOKENOAuth2 client_id
APIFONAPI_KEYOAuth2 client_secret

Cron Jobs

JobScheduleDescription
AdvSendEmailAndSMSBasedOnFromStoreStatusEvent-driven / cronSends SMS when order reaches from_store status
AdvSendEmailAndSMSBasedOnSentStatusEvent-driven / cronSends SMS when order is shipped
AdvGetBulkerSmsStatusPeriodicPolls Bulker API for delivery status of buffered messages (legacy; active)
Advisable\Domains\Order\Jobs\GetBulkerSmsStatusPeriodicModern port of the above (#155); ships commented-out in jobs.php (operator opt-in)

Client Extension Points

Provider Selection

The SMS provider is selected per client via the $provider parameter passed to sendSms() / sendSmsMsg(). The calling controller or job determines the provider based on client configuration.

Phone Validation

The isValidPhoneForSms() and convertToValidPhoneForSms() functions are Greece-specific (3069/003069/69 prefix patterns). Clients outside Greece must override these functions in application/helpers/sms_helper.php. The source code contains TODO comments acknowledging this limitation.

Message Templates

SMS and Viber message content is managed through the language system:

  • sms.order.message.prefix / sms.order.message.suffix -- Standard SMS
  • sms.order.message.saturday.prefix / sms.order.message.saturday.suffix -- Saturday delivery SMS
  • viber.order.from_store.message / viber.order.from_store.message.saturday -- Viber messages

Override these in application/language/{lang}/ for custom message wording.

Apifon

Client-override seam (4.112.0, #387). Advisable\Apifon\Apifon is bound in src/Apifon/container.php and resolved everywhere via di()->get(Apifon::class). Six members are now protected (API_BASE_URL, LIST_ENDPOINT, $config, $logger, ensureAuthenticated(), getList()), so a client can subclass instead of editing upstream src/. Re-bind from custom/Apifon/container.php by registering Custom\Apifon\Apifon and aliasing it to Advisable\Apifon\Apifon::class; all call sites (sms_helper.php, Adv_order.php) pick up the subclass transparently.

Custom Providers

Add a new provider by:

  1. Adding a new case in sendSmsMsg() switch statement (client repo: override sms_helper.php)
  2. Creating the provider class in application/libraries/ or custom/
  3. Adding config file in application/config/

Business Rules

  1. Friday/Saturday handling: If the SMS is triggered on a Friday (dayOfWeek == 5), a different message template is used to inform the customer about Saturday delivery.
  2. Phone normalization: Greek mobile numbers are accepted in three formats and normalized to the international 30xx format:
    • 3069xxxxxxxx (12 digits) -- already normalized
    • 003069xxxxxxxx (14 digits) -- strip leading 00
    • 69xxxxxxxx (10 digits) -- prepend 30
  3. Communication history: Every order SMS is logged in customer_message_history for audit and customer service reference.
  4. Viber priority: YubotoOmni sends Viber first (priority 0), with SMS as fallback (priority 1). Apifon uses a TTL-based approach: if Viber delivery is not confirmed within the TTL window, SMS is sent automatically.
  5. Payment-specific SMS: getValidPhoneForPayway() only returns a valid phone for paid_at_store and paybybank payment methods, ensuring SMS is only sent for relevant order types.
  6. Bulker status polling: Orders sent via Bulker have their delivery status polled by a cron job. The sms_status field tracks the delivery lifecycle (sent -> delivered/undelivered/buffered).
  7. Fire-and-forget: Most providers (except Bulker) operate on a fire-and-forget model -- the SMS is sent and no delivery confirmation is tracked beyond the initial success/failure response.
  8. Gift card SMS: Gift card activations can trigger SMS notifications via GiftCardSendSms, using the same unified dispatcher.

Known Issues & Security Gaps

  1. Phone validation is Greece-specific. isValidPhoneForSms() and convertToValidPhoneForSms() recognise only 3069/003069/69 prefix patterns. Clients outside Greece must override these functions. See Client Extension Points — Phone Validation for override guidance.

  2. Bulker unconfigured-deployment behaviour. createBulkerClient() returns null only when bulker config is absent or its apiBaseUrl/auth_key keys are empty. The shipped placeholder config (apiBaseUrl='http://api.bulker.gr/http', auth_key='XXXXXXXXXXXXXXXXXX') is non-empty, so enabling the cron schedule on a deployment that has not set real credentials will still build a client and fire live gateway calls for every buffered order. Gateway errors map to status 4 (buffered) and are skipped — no order data is corrupted — but unexpected outbound traffic will occur. Configure application/config/bulker.php with real credentials before enabling the schedule. Source: src/Domains/Order/Jobs/GetBulkerSmsStatus.php:93-124.

Tests

Unit

  • tests/Unit/Domains/Order/Jobs/GetBulkerSmsStatusTest.php — 9 tests covering the read→poll→write matrix: unconfigured gateway (null client early-return), buffered-only selection criterion, delivered/undelivered status writes, still-buffered skip, mid-batch continuation (break vs continue), non-positive id guard, and execute() full-pipeline.

Integration

  • tests/Integration/Domains/Order/Jobs/GetBulkerSmsStatusContainerTest.php — 3 tests: container registration, DI resolution, and createBulkerClient() real-config path (against the real CI bulker config).
  • tests/Integration/Domains/Order/Order/RepositoryTest.php — includes a DB-backed test for findOrderIdsBySmsStatus() verifying the id-only projected SQL and int return type.

Legacy

AdvGetBulkerSmsStatus (legacy job) has no dedicated test coverage.