Notifications
Every notification is triggered by a domain event written to the outbox in the same database transaction as the change it describes. Order, payment and delivery code never call the notification service directly, so a slow mail server can never roll back a paid order, and a crash can never lose the email for an order that did commit.
Channels and providers
| Channel | Provider |
|---|---|
| SMTP | |
| Push | Firebase Cloud Messaging (device tokens registered by the apps) |
| SMS | Twilio — also carries sign-in codes and delivery codes |
| In-app | No provider: the stored notification is the delivery, shown in the account's notification list. |
A channel with no configured provider leaves its notifications queued rather than dropped. A development "console" provider exists and is refused at start-up in production.
Rules that shape delivery
- Quiet hours. Routine notices are held until the recipient's morning — delayed, never dropped. Urgent ones (a failed payment, a driver's next job, a chargeback) go out regardless.
- Digests. Busy streams are batched: a seller with forty orders gets one "40 new orders", not forty pushes.
- Language. Every template exists in English and Arabic. The locale comes from the device, then the user's profile, then English.
- Preferences. Buyers can switch notifications on or off per event and channel.
- Templates. Admins can override the wording of a template from the admin console (Message templates).
What is sent today
The catalogue (generated into docs/notifications.md) holds 33 rules for 32 events across six audiences. Sign-in and password-reset codes are sent by email as well.
Buyer
| Event | Channels | Quiet hours | Digest | Message |
|---|---|---|---|---|
order.placed | EMAIL · IN_APP | sent anyway | individual | Order confirmed |
order.paid | EMAIL · IN_APP | sent anyway | individual | Payment received |
order.payment_failed | PUSH · EMAIL · IN_APP | sent anyway | individual | Payment problem |
order.shipped | PUSH · EMAIL · IN_APP | held till morning | batched | Order is on its way |
delivery.out_for_delivery | PUSH · IN_APP | sent anyway | individual | Arriving soon, with ETA |
delivery.otp_issued | SMS · IN_APP | sent anyway | individual | Your delivery code |
delivery.completed | PUSH · IN_APP | held till morning | batched | Delivered |
delivery.failed | PUSH · SMS · IN_APP | sent anyway | individual | We could not deliver |
order.cancelled | EMAIL · IN_APP | held till morning | individual | Order was cancelled |
refund.settled | EMAIL · IN_APP | held till morning | individual | Refund sent |
ticket.replied | PUSH · EMAIL · IN_APP | held till morning | individual | Support replied |
Seller
| Event | Channels | Quiet hours | Digest | Message |
|---|---|---|---|---|
order.group_placed | PUSH · EMAIL · IN_APP | sent anyway | batched | New order |
order.accept_deadline_missed | PUSH · EMAIL · IN_APP | sent anyway | individual | Order past its acceptance deadline |
payout.paid | EMAIL · IN_APP | held till morning | individual | Payout sent |
review.published | IN_APP | held till morning | batched | New review |
catalog.product_rejected | EMAIL · IN_APP | held till morning | batched | Product was not approved |
Driver
| Event | Channels | Quiet hours | Digest | Message |
|---|---|---|---|---|
delivery.task_assigned | PUSH | sent anyway | individual | New job |
delivery.task_reassigned | PUSH · IN_APP | sent anyway | individual | Job reassigned |
driver.earning_recorded | IN_APP | held till morning | batched | You earned an amount |
Admin, finance, support
| Event | Channels | Quiet hours | Digest | Message |
|---|---|---|---|---|
order.accept_deadline_missed | EMAIL · IN_APP | sent anyway | batched | Unanswered order |
payment.chargeback_opened | EMAIL · IN_APP | sent anyway | individual | Chargeback opened |
seller.application_submitted | IN_APP | held till morning | batched | Sellers awaiting approval |
review.flagged | IN_APP | held till morning | batched | Reviews need moderation |
cod.settlement_discrepancy | EMAIL · IN_APP | sent anyway | batched | COD settlement short |
ticket.sla_breached | EMAIL · IN_APP | sent anyway | individual | Support SLA breached |
These events have templates but nothing in the code raises them yet, so they are never sent: inventory.low_stock, message.received, ticket.opened, order.accept_deadline_near, ledger.reconciliation_failed, delivery.cash_limit_near.
Live tracking
A realtime channel (Socket.IO, namespace /realtime) lets a buyer subscribe to their own order and receive driver position updates, and lets dispatchers subscribe to the fleet. GET /tracking/orders/{orderNumber} returns the last known position and an ETA. The driver app does not send location pings yet, so in practice positions are not flowing.