Appearance
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:
- 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.
- 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.
- 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, andreply_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
logscollection, discriminated byMODEL = 'fcm_token') withtoken,device_id,platform, andlast_used_atfields. - PushOrderEvent → SendOrderNotification: The
PushOrderEventevent is dispatched after order processing, triggering the queuedSendOrderNotificationlistener ($tries = 3,$backoff = 10seconds). - 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 onnotifications/{userId}/latest. - Transactional email (SendGrid): Account and order-receipt emails are sent as queued jobs using SendGrid dynamic templates (there are no Laravel
Mailableclasses). - 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) atapp/Http/Controllers/Api/BackOffice/NotificationController.php, delegating toNotificationOrchestrator→NotificationService. - Routes (
routes/api/backoffice/notifications.php, prefixed/back-office/notifications):GET /— paginated list for the current userPOST /— create a notification (admin / programmatic use); gated by thecreate-notificationspermission (routes/api/backoffice/notifications.php:11-12)GET /unread-countPOST /mark-all-readPOST /{id}/mark-read
- Storage: MongoDB
notificationscollection (app/RawModels/Notification.php). Each notification haslevel,level_id,title,description,link,read_by[], andcreated_at. Read state is per-user (read_byarray), sois_readis 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 viasrc/services/notificationService.ts, stores state insrc/store/modules/notification.ts, and subscribes to Firebase viauseFirebaseNotifications()(src/plugins/firebase.ts). The listener watches RTDB pathnotifications/{userId}/latestand 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/notificationsendpoint (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()dispatchesSendFirebaseNotificationsJob, which writesnotifications/{userId}/latest={timestamp, level, level_id}for each resolved recipient off the request thread (app/Jobs/SendFirebaseNotificationsJob.php:54-61; dispatch atapp/Services/NotificationService.php:71). Mark-as-read dispatchesSendFirebaseReadUpdateJobthe same way (NotificationService.php:155,180).
Two guards apply on create:
level_idmust belong to the current merchant.NotificationService::create()callsassertLevelTargetBelongsToCurrentMerchant()before any write; an unowned or unresolvable merchant / location / role / user id is rejected with a 403 (app/Services/NotificationService.php:58,81).linkis validated by theSafeLinkUrlrule — only a relative path, an in-page#fragment, or an absolutehttp(s)URL passes.javascript:,data:and protocol-relative//hostvalues 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) dispatchesnew PushOrderEvent($transaction->getLocationId())in the finalelsebranch 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).PushOrderEventis also dispatched fromKitchenDisplayService(forupdated_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 typeKitchen Display(DeviceTypes::KitchenDisplay) at the order's location, writes an RTDB timestamp for each viatriggerKDSUpdate(), then sends FCM to those devices. The event→listener mapping is via Laravel 11 auto-discovery (bootstrap/app.php), not anEventServiceProvider.- On failure the listener logs and re-queues (
Log::error+$this->release($this->backoff)); it does not re-throw, and afailed()hook logs the final failure. - FCM service:
app/Services/Common/FCMService.php(Kreait SDK). Key methods:registerToken(string $token, string $deviceId, string $platform = 'android')andsendToAll(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\NotFoundexception with message "Requested entity was not found", the offending token is deleted from thelogscollection.
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 isapp/Services/Common/EmailService.php. Template IDs live inconfig/services.php. There are noapp/MailMailable classes. - SMS is sent via Twilio through
app/Jobs/SendSMS.php(implements ShouldQueue), dispatched byapp/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 sendsSendLocalOrderReceiptMail; 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.
| Job class | Status | |
|---|---|---|
| Order receipt | SendLocalOrderReceiptMail | live (via ReceiptService) |
| Customer verification code | SendVerificationCodeMail | live |
| Device activation code | SendDeviceActivationCodeMail | live |
| Password reset | SendPasswordResetMail | live |
| Merchant invitation | SendMerchantInvitationMail | live |
| Back-office OTP | SendBackofficeOTPMail | live |
| Loyalty program alert | SendLoyaltyProgramAlertMail | live |
| Merchant credentials | SendMerchantCredentialsMail | live |
| Grace-period reminder | SendGracePeriodReminderMail | live |
| Order ready for pickup | SendLocalOrderReadyForPickupMail | scaffolded, no caller |
| Order out for delivery | SendLocalOrderOutForDeliveryMail | scaffolded, no caller |
| Order delivered | SendLocalOrderDeliveredMail | scaffolded, no caller |
| Order missed delivery | SendLocalOrderMissedDeliveryMail | scaffolded, no caller |
| Order ready for delivery | SendLocalOrderReadyDeliveryMail | scaffolded, no caller |
| Order pickup completed | SendLocalOrderPickupCompletedMail | scaffolded, no caller |
The "order lifecycle" emails (ready-for-pickup, out-for-delivery, delivered, missed-delivery, ready-for-delivery, pickup-completed) have
EmailServicemethods, 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
ReceiptServicewhen 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 inStoreReceiptSettingRequest, exposed viaReceiptSetting::getCcReceiptEmails(). - The receipt path actually delivers these as BCC, not CC:
EmailServicepasses the list toSendLocalOrderReceiptMail, which forwards it asbccReceiptEmailsand appliesaddBcc(...). 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
fromaddress.
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/notificationsback-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. PushOrderEventfires for orders processed through the native path only — Kassanet, ShopCaisse, Square, Lightspeed, and MplusKassa orders are excluded and use their own notification paths.- The
SendOrderNotificationlistener 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
logscollection when Firebase returns aNotFound("Requested entity was not found") error. - Order-receipt copies are configured as
cc_receipt_emailsunder 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/notificationsroute, 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/notificationsAPI 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
ProcessOrderJobprocesses a native-path order (not Kassanet/ShopCaisse/ Square/Lightspeed/MplusKassa), it dispatchesPushOrderEvent, and the queuedSendOrderNotificationlistener 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
NotFounderror, the token is deleted from thelogscollection. Tokens also storelast_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-timestampthat 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).
Related Features
Troubleshooting
- KDS not receiving notifications → Confirm the device type is
Kitchen Displayand 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 firePushOrderEvent). - 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.