Appearance
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:
- Order-triggered SMS (
sendSms()) -- Sends a predefined order notification message with special Friday/Saturday delivery handling. Records the message in customer communication history. - 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
| Function | Parameters | Description |
|---|---|---|
sendSms($orderSerial, $phone, $provider, $extra) | string, string, string, array | Order notification SMS with history logging |
sendSmsMsg($msg, $viberMsg, $phone, $provider, $extra) | string, string, string, string, array | Generic SMS/Viber dispatch |
isValidPhoneForSms($phone) | string | Validates Greek mobile number format |
convertToValidPhoneForSms($phone) | string | Normalizes to 30xx format |
getFirstValidPhoneForSms($phones) | array | Returns first valid phone from array |
getFirstValidPhoneForSmsFormatted($phones) | array | Returns first valid phone, normalized |
getValidPhoneForPayway($payway, ...) | string, strings | Returns valid phone for specific payment methods |
Provider-Specific APIs
| Provider | Class | Channels | Features |
|---|---|---|---|
| Yuboto | Legacy helper functions | SMS | Callback URL support |
| YubotoOmni | Advisable\Yuboto\YubotoOmni | SMS + Viber | Priority-based channel selection |
| OmniMessaging | Advisable\Omni\OmniMessaging | SMS + Viber | Transaction ID tracking, validity periods |
| Routee | Legacy helper functions | SMS, SMS+Viber | DLR callback URL |
| Plivo | Legacy helper functions | SMS only | Basic send |
| Bulker | Advisable\Bulker\BulkerSms | SMS only | Status polling, delivery reports |
| Apifon | Advisable\Apifon\Apifon | SMS + Viber | OAuth2, 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/caseProvider 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/FailedOmniMessaging 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_statusModern port (#155).
Advisable\Domains\Order\Jobs\GetBulkerSmsStatusis the PSR-4 port of the legacyAdvGetBulkerSmsStatus. It reads buffered orders viaRepository::findOrderIdsBySmsStatus(int $smsStatus): int[]— a projectedSELECT id FROM shop_order WHERE sms_status = ?that returns only order IDs rather than hydrating full Order entities (asmatch()would), mirroring the legacy job's narrowSELECT id, status, sms_statusquery (src/Domains/Order/Order/Repository/Repository.php:51-77). Writes go through the OrderWriteRepository. The gateway call is the sameBulkerSms::getSmsStatus()the legacyBulkerlibrary 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.phpand in thecommandOptionsarray inapplication/config/jobs.php:206— thecommandOptionsentry is required wiring alongside container registration; without it the dispatcher cannot resolve the FQCN. Thejobs.phpschedule 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) returnsnullonly whenbulkerconfig is absent or lacksapiBaseUrl/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. Configureapplication/config/bulker.phpbefore 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 configProvider Authentication
| Provider | Auth Method | Details |
|---|---|---|
| Yuboto (legacy) | Config-based | API key in helper config |
| YubotoOmni | Basic Auth | Base64-encoded API key |
| OmniMessaging | Basic Auth | Base64-encoded userID:authKey |
| Routee | Config-based | Via routee_helper config |
| Plivo | Config-based | Via plivo_helper config |
| Bulker | Query parameter | auth_key in URL |
| Apifon | OAuth2 Bearer | client_credentials grant, Bearer token |
Data Model
Tables
| Table | Column | Description |
|---|---|---|
shop_order | sms_status | SMS delivery status: 1=sent, 2=delivered, 3=undelivered, 4=buffered |
customer_message_history | Various | Communication log: customer_id, type (ORDER_ON_STORE), channel (SMS), subject, body |
SMS Status Values
| Value | Meaning | Context |
|---|---|---|
| 1 | Sent | Initial state after successful send |
| 2 | Delivered | Confirmed delivery (Bulker polling) |
| 3 | Undelivered | Failed delivery (Bulker polling) |
| 4 | Buffered | In transit, pending confirmation |
Configuration
Provider Config Files
| Provider | Config File | Key Settings |
|---|---|---|
| Yuboto | application/config/yuboto.php | API URL, sender name, API key |
| YubotoOmni | application/config/yuboto_omni.php | apiBaseUrl, apiKey, smsSender, viberSender, timeout |
| Routee | application/config/routee.php | API credentials, sender |
| Plivo | application/config/plivo.php | Auth ID, auth token, sender |
| Bulker | application/config/bulker.php | apiBaseUrl, auth_key, sender_name |
Apifon (Registry)
| Group | Key | Description |
|---|---|---|
APIFON | ENABLED | Enable/disable Apifon |
APIFON | SENDER_ID | SMS sender name |
APIFON | TTL | Viber TTL before SMS fallback (seconds) |
APIFON | ENABLE_IM | Enable Viber channel |
APIFON | API_TOKEN | OAuth2 client_id |
APIFON | API_KEY | OAuth2 client_secret |
Cron Jobs
| Job | Schedule | Description |
|---|---|---|
AdvSendEmailAndSMSBasedOnFromStoreStatus | Event-driven / cron | Sends SMS when order reaches from_store status |
AdvSendEmailAndSMSBasedOnSentStatus | Event-driven / cron | Sends SMS when order is shipped |
AdvGetBulkerSmsStatus | Periodic | Polls Bulker API for delivery status of buffered messages (legacy; active) |
Advisable\Domains\Order\Jobs\GetBulkerSmsStatus | Periodic | Modern 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 SMSsms.order.message.saturday.prefix/sms.order.message.saturday.suffix-- Saturday delivery SMSviber.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:
- Adding a new
caseinsendSmsMsg()switch statement (client repo: overridesms_helper.php) - Creating the provider class in
application/libraries/orcustom/ - Adding config file in
application/config/
Business Rules
- 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. - Phone normalization: Greek mobile numbers are accepted in three formats and normalized to the international
30xxformat:3069xxxxxxxx(12 digits) -- already normalized003069xxxxxxxx(14 digits) -- strip leading0069xxxxxxxx(10 digits) -- prepend30
- Communication history: Every order SMS is logged in
customer_message_historyfor audit and customer service reference. - 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.
- Payment-specific SMS:
getValidPhoneForPayway()only returns a valid phone forpaid_at_storeandpaybybankpayment methods, ensuring SMS is only sent for relevant order types. - Bulker status polling: Orders sent via Bulker have their delivery status polled by a cron job. The
sms_statusfield tracks the delivery lifecycle (sent -> delivered/undelivered/buffered). - 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.
- Gift card SMS: Gift card activations can trigger SMS notifications via
GiftCardSendSms, using the same unified dispatcher.
Known Issues & Security Gaps
Phone validation is Greece-specific.
isValidPhoneForSms()andconvertToValidPhoneForSms()recognise only3069/003069/69prefix patterns. Clients outside Greece must override these functions. See Client Extension Points — Phone Validation for override guidance.Bulker unconfigured-deployment behaviour.
createBulkerClient()returnsnullonly whenbulkerconfig is absent or itsapiBaseUrl/auth_keykeys 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. Configureapplication/config/bulker.phpwith 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 (breakvscontinue), non-positive id guard, andexecute()full-pipeline.
Integration
tests/Integration/Domains/Order/Jobs/GetBulkerSmsStatusContainerTest.php— 3 tests: container registration, DI resolution, andcreateBulkerClient()real-config path (against the real CIbulkerconfig).tests/Integration/Domains/Order/Order/RepositoryTest.php— includes a DB-backed test forfindOrderIdsBySmsStatus()verifying the id-only projected SQL and int return type.
Legacy
AdvGetBulkerSmsStatus (legacy job) has no dedicated test coverage.
Related Flows
- IN-13 Newsletter Integrations -- Apifon subscriber management
- CF-07 Order Confirmation -- Order status changes that trigger SMS
- SY-01 Cron Framework -- Cron scheduling for SMS jobs
- AD-13 Settings -- Admin configuration for SMS providers