Skip to content

Notifications

Overview

There is no merchant-facing "Notifications" settings page in Upvendo. The /settings/notifications route does not exist, and there are no notification preference toggles (order-confirmation email, "notify when ready", status updates, new-order alerts, low-stock alerts, CC/reply-to fields, etc.) anywhere in the back office. Notification behaviour is driven by backend code, not by user-configurable settings.

What does exist falls into three distinct, code-grounded areas:

  1. An in-app notification center — a bell-icon dropdown in the back-office navbar, backed by a notifications API and a Firebase real-time listener.
  2. Push-to-KDS notifications — Firebase Cloud Messaging (FCM) push plus a Firebase Realtime Database (RTDB) signalling layer that wakes Kitchen Display devices when an order arrives.
  3. Transactional emails / SMS — order receipts and account-related messages sent via SendGrid (email) and Twilio (SMS).

Key Purpose: Deliver order alerts to Kitchen Display devices, surface in-app notifications to back-office users, and send transactional receipts and account messages to customers and merchants.

Note on accuracy: Earlier versions of this doc described a configurable notifications settings page with toggles such as order_confirmation_email, order_ready_notification, status_updates, new_order_alert, low_stock_alert, cc_email, and reply_to_email. None of those field IDs exist in either the backend or the back office. They have been removed.

Key Concepts

  • In-app notification center: A navbar bell dropdown listing notifications for the current user, with unread badge, mark-as-read, and click-through to a link. Backed by the back-office notifications API and a Firebase RTDB listener.
  • Firebase Cloud Messaging (FCM): Push service used to wake Kitchen Display devices, implemented with the Kreait Firebase SDK and device-specific tokens.
  • FCM tokens: Device registration tokens stored in MongoDB (the shared logs collection, discriminated by MODEL = 'fcm_token') with token, device_id, platform, and last_used_at fields.
  • PushOrderEvent → SendOrderNotification: The PushOrderEvent event is dispatched after order processing, triggering the queued SendOrderNotification listener ($tries = 3, $backoff = 10 seconds).
  • Firebase Realtime Database (RTDB) signalling: For KDS, the backend writes a timestamp to devices/{deviceId}/latest-order-timestamp; for the back-office notification center, the front end listens on notifications/{userId}/latest.
  • Transactional email (SendGrid): Account and order-receipt emails are sent as queued jobs using SendGrid dynamic templates (there are no Laravel Mailable classes).
  • Transactional SMS (Twilio): Receipts (when the contact is a phone number) and OTP codes are sent via Twilio.

How It Actually Works

1. In-app notification center (back office)

A bell-icon dropdown in the navbar shows notifications for the logged-in user.

  • Backend: NotificationController (in-app notifications, not push) at app/Http/Controllers/Api/BackOffice/NotificationController.php, delegating to NotificationOrchestratorNotificationService.
  • Routes (routes/api/backoffice/notifications.php, prefixed /back-office/notifications):
    • GET / — paginated list for the current user
    • POST / — create a notification (admin / programmatic use); gated by the create-notifications permission (routes/api/backoffice/notifications.php:11-12)
    • GET /unread-count
    • POST /mark-all-read
    • POST /{id}/mark-read
  • Storage: MongoDB notifications collection (app/RawModels/Notification.php). Each notification has level, level_id, title, description, link, read_by[], and created_at. Read state is per-user (read_by array), so is_read is computed per requesting user.
  • Levels (app/Enums/NotificationLevel.php): merchant, location, role, user.
  • Front end: src/components/navbar/NavBarNotifications.vue (used by the vertical-nav layout, which is the active default) fetches via src/services/notificationService.ts, stores state in src/store/modules/notification.ts, and subscribes to Firebase via useFirebaseNotifications() (src/plugins/firebase.ts). The listener watches RTDB path notifications/{userId}/latest and re-fetches from the API when the timestamp changes.

Important — no automatic producers found. The notification center is plumbing-complete (storage, API, UI, Firebase listener), but no backend event/listener was found that automatically creates in-app notifications. In practice notifications appear to be created only via the POST /back-office/notifications endpoint (the front-end service comments label this "admin use"). Marking-as-unread is explicitly a no-op (the backend does not support it).

Creation does drive the real-time path: NotificationService::create() dispatches SendFirebaseNotificationsJob, which writes notifications/{userId}/latest = {timestamp, level, level_id} for each resolved recipient off the request thread (app/Jobs/SendFirebaseNotificationsJob.php:54-61; dispatch at app/Services/NotificationService.php:71). Mark-as-read dispatches SendFirebaseReadUpdateJob the same way (NotificationService.php:155,180).

Two guards apply on create:

  • level_id must belong to the current merchant. NotificationService::create() calls assertLevelTargetBelongsToCurrentMerchant() before any write; an unowned or unresolvable merchant / location / role / user id is rejected with a 403 (app/Services/NotificationService.php:58,81).
  • link is validated by the SafeLinkUrl rule — only a relative path, an in-page # fragment, or an absolute http(s) URL passes. javascript:, data: and protocol-relative //host values are rejected (app/Http/Requests/BackOffice/Notification/StoreNotificationRequest.php:27, app/Rules/SafeLinkUrl.php).

2. Push notifications to Kitchen Display (KDS) devices

  • ProcessOrderJob (app/Jobs/ProcessOrderJob.php) dispatches new PushOrderEvent($transaction->getLocationId()) in the final else branch of its provider chain — i.e. only for orders that are not Kassanet, not ShopCaisse, not Square, not Lightspeed, and not MplusKassa (those POS integrations use their own paths). PushOrderEvent is also dispatched from KitchenDisplayService (for updated_order) and the Lightspeed K-Series webhook job.
  • SendOrderNotification (app/Listeners/SendOrderNotification.php, implements ShouldQueue, $tries = 3, $backoff = 10) runs on the queue. It fetches devices of type Kitchen Display (DeviceTypes::KitchenDisplay) at the order's location, writes an RTDB timestamp for each via triggerKDSUpdate(), then sends FCM to those devices. The event→listener mapping is via Laravel 11 auto-discovery (bootstrap/app.php), not an EventServiceProvider.
  • On failure the listener logs and re-queues (Log::error + $this->release($this->backoff)); it does not re-throw, and a failed() hook logs the final failure.
  • FCM service: app/Services/Common/FCMService.php (Kreait SDK). Key methods: registerToken(string $token, string $deviceId, string $platform = 'android') and sendToAll(array $data, string $type = 'data', array $fcmTokenFilter = []). Tokens are registered from device endpoints (AppUpdateController::registerToken, KitchenDisplayController::updateFCMToken).
  • Stale-token cleanup: When a multicast send returns a Kreait Messaging\NotFound exception with message "Requested entity was not found", the offending token is deleted from the logs collection.

3. Transactional emails and SMS

  • Email is sent via SendGrid dynamic templates through queued jobs that extend the abstract app/Jobs/SendGridMail.php (implements ShouldQueue). The dispatcher is app/Services/Common/EmailService.php. Template IDs live in config/services.php. There are no app/Mail Mailable classes.
  • SMS is sent via Twilio through app/Jobs/SendSMS.php (implements ShouldQueue), dispatched by app/Services/Common/SMSService.php. No Vonage/Nexmo integration exists.
  • Receipts are the only customer order email/SMS that actually fires in production, via ReceiptService::send() on order completion. If the contact is an email it sends SendLocalOrderReceiptMail; if it is a phone it sends a Twilio receipt SMS.

Email Notification Types

The following email jobs exist (all under app/Jobs/, all queued). Only the ones marked live are actually triggered by current code paths.

EmailJob classStatus
Order receiptSendLocalOrderReceiptMaillive (via ReceiptService)
Customer verification codeSendVerificationCodeMaillive
Device activation codeSendDeviceActivationCodeMaillive
Password resetSendPasswordResetMaillive
Merchant invitationSendMerchantInvitationMaillive
Back-office OTPSendBackofficeOTPMaillive
Loyalty program alertSendLoyaltyProgramAlertMaillive
Merchant credentialsSendMerchantCredentialsMaillive
Grace-period reminderSendGracePeriodReminderMaillive
Order ready for pickupSendLocalOrderReadyForPickupMailscaffolded, no caller
Order out for deliverySendLocalOrderOutForDeliveryMailscaffolded, no caller
Order deliveredSendLocalOrderDeliveredMailscaffolded, no caller
Order missed deliverySendLocalOrderMissedDeliveryMailscaffolded, no caller
Order ready for deliverySendLocalOrderReadyDeliveryMailscaffolded, no caller
Order pickup completedSendLocalOrderPickupCompletedMailscaffolded, no caller

The "order lifecycle" emails (ready-for-pickup, out-for-delivery, delivered, missed-delivery, ready-for-delivery, pickup-completed) have EmailService methods, job classes, and template IDs, but no code invokes them — they are dormant. There is no per-status notification pipeline that emails customers as an order moves through received → preparing → ready → delivered.

SMS Notification Types

Sent via Twilio (SMSService / SendSMS):

  • Receipt SMS — sent by ReceiptService when the customer contact is a phone.
  • Customer OTP — live.
  • Back-office OTP — live.
  • Device activation code — the SMS path appears commented out; email activation is the active path.

CC / receipt copies

Order receipt emails can copy additional addresses, configured under Receipts settings (route /settings/receipts), not under a notifications page.

  • Setting key: cc_receipt_emails (an array of email addresses), validated in StoreReceiptSettingRequest, exposed via ReceiptSetting::getCcReceiptEmails().
  • The receipt path actually delivers these as BCC, not CC: EmailService passes the list to SendLocalOrderReceiptMail, which forwards it as bccReceiptEmails and applies addBcc(...). The back-office UI labels this field "BCC Receipt Emails" accordingly.

There is no reply-to email configuration anywhere in the backend or back office. Emails are sent only with a from address.

Location

  • In-app notification center (back-office API): app/Http/Controllers/Api/BackOffice/NotificationController.php, routes /back-office/notifications (routes/api/backoffice/notifications.php).
  • Notification center UI: src/components/navbar/NavBarNotifications.vue (navbar bell, vertical-nav layout).
  • Push / KDS: app/Events/PushOrderEvent.php, app/Listeners/SendOrderNotification.php, app/Services/Common/FCMService.php.
  • Email: app/Services/Common/EmailService.php, app/Jobs/SendGridMail.php.
  • SMS: app/Services/Common/SMSService.php, app/Jobs/SendSMS.php.
  • There is no /settings/notifications back-office route and no notifications settings page or nav entry.

Business Rules

  • There is no notifications settings page and no notification preference toggles; notification behaviour is determined by backend code, not user settings.
  • FCM push and RTDB KDS signalling target only devices of type Kitchen Display (DeviceTypes::KitchenDisplay) at the order's location.
  • PushOrderEvent fires for orders processed through the native path only — Kassanet, ShopCaisse, Square, Lightspeed, and MplusKassa orders are excluded and use their own notification paths.
  • The SendOrderNotification listener retries up to 3 times with a 10-second backoff; failures are logged and re-queued, never thrown.
  • Invalid/unregistered FCM tokens are auto-deleted from the logs collection when Firebase returns a NotFound ("Requested entity was not found") error.
  • Order-receipt copies are configured as cc_receipt_emails under Receipts settings, and are delivered as BCC.
  • The in-app notification center has no automatic producers in the backend; its read/unread state is per-user, and "mark as unread" is unsupported.

FAQs

  • "Is there a notification settings page in the back office?" No. There is no /settings/notifications route, no settings page, no nav entry, and no notification preference toggles. Notification behaviour is hardcoded in backend events, listeners, and services.
  • "What is the bell icon in the back-office navbar?" It is an in-app notification center backed by the /back-office/notifications API and a Firebase real-time listener. It lists notifications for the current user with unread counts, mark-as-read, and click-through links. Note: no backend code was found that automatically creates these notifications — they are created via the create endpoint (admin/programmatic use).
  • "What triggers a push notification to a Kitchen Display?" When ProcessOrderJob processes a native-path order (not Kassanet/ShopCaisse/ Square/Lightspeed/MplusKassa), it dispatches PushOrderEvent, and the queued SendOrderNotification listener sends FCM and writes an RTDB timestamp to the location's Kitchen Display devices.
  • "What email notifications does the system send?" Order receipts (live), plus account/operational emails: customer verification codes, device activation codes, password resets, merchant invitations, merchant credentials, back-office OTP, loyalty alerts, and grace-period reminders. Order-lifecycle emails (ready/out-for-delivery/delivered/etc.) exist as code but are not currently wired to fire.
  • "Does the system send SMS?" Yes, via Twilio: receipts (when the contact is a phone) and OTP codes.
  • "Are there low-stock or status-update notifications?" No low-stock notification feature exists. There is no per-status customer notification pipeline; only the receipt is sent to customers.
  • "How does the system handle stale FCM tokens?" When a send fails with a Firebase NotFound error, the token is deleted from the logs collection. Tokens also store last_used_at.
  • "What is the difference between RTDB and FCM for KDS?" FCM sends a push to wake the KDS app; RTDB provides a signalling timestamp at devices/{deviceId}/latest-order-timestamp that the KDS app listens to for immediate updates.

Relations

Depends On

  • Locations: KDS push targets devices at the order's location.
  • Receipts: Receipt email/SMS content and the BCC (cc_receipt_emails) list.
  • Devices: KDS device type and FCM token registration.

Affects

  • Transactions: Order processing triggers KDS push and the receipt.
  • Customers: Receipt delivery (email/SMS).

Troubleshooting

  • KDS not receiving notifications → Confirm the device type is Kitchen Display and its FCM token is registered and not stale. Confirm the order went through the native path (Kassanet, ShopCaisse, Square, Lightspeed, and MplusKassa orders do not fire PushOrderEvent).
  • Customer not receiving a receipt → Check the email/SMS job logs. Verify the contact (email or phone) is valid and that SendGrid (email) / Twilio (SMS) is configured in the environment.
  • Back-office bell shows nothing → Notifications are created only via the create endpoint and no automatic backend producer was found, so an empty list is expected unless notifications have been created programmatically. Confirm the Firebase config (VITE_FIREBASE_DATABASE_URL) is set so the real-time listener can attach.

Device-side / external delivery behaviour (Firebase push receipt on the device, SendGrid and Twilio delivery success) is not verified here — it depends on external services and is outside the scope of this repository review.