Skip to content

Notifications, help, and settings (Vendor)

The notifications inbox uses GET /auth/vendor/notifications (JWT only; no notification module on the API). Store-originated user_notifications rows include status: 0 = read, 1 = unread; POST /auth/vendor/notifications/read persists read state. Broadcast rows have status: null; the client uses sessionStorage “seen” IDs for unread styling on those only. Email/push preference toggles use GET/PATCH /auth/vendor/settings/notifications (JWT only; no settings module—same pattern as admin). Help is static. Profile uses /dashboard/profile?tab= (profile | notifications | password) for account info, prefs, and change password; PATCH /auth/vendor/password returns 400 with auth-004 when the current password is wrong (not 401, so the client does not force logout). Settings uses path segments under /dashboard/settings/* (e.g. /dashboard/settings/general, …/devices) with a shared DashboardTabbedSectionLayout; legacy ?tab= on /dashboard/settings redirects to the matching segment.


GoalsNon-goals
Clarify real API vs static helpMobile push guarantees
Explain seen-state behavior (sessionStorage)Cross-device read sync (not implied)

  • Notifications — merged server list with client seen IDs (Notifications section). The list API is available to any authenticated vendor; UI may still gate /dashboard/notifications by module notification.
  • Notification prefs (PATCH /auth/vendor/settings/notifications) — Saved per logged-in user: vendors.indochina_notification_prefs for owners and vendor_employees.indochina_notification_prefs for staff (JSON slice of the same push* / email* keys as before). Reads fall back to stores.dashboard_prefs for the JWT’s active store when the user column is still empty (legacy until they save once). Writers that gate in-app inserts use the owner column with the same store fallback where applicable; per-actor POS new_order uses owner vs employee columns as documented below.
  • Help — static content.
  • Settings — URL-driven sections (NavLink tabs + Outlet); DashboardTabbedSectionLayout (@indochina/shared) so navigation is a horizontal bar below lg and a left rail on desktop.

  1. User opens Notifications → marks items → seen state in sessionStorage for session.
  2. Help for FAQs; Settings for prefs.

  • Notifications may contain order PII—lock screens on shared devices.

IDScenarioExpected
NH1Fetch notificationsAPI items render
NH2Mark seenStyling updates; survives navigation until storage clear
NH3Settings tabs?tab= updates and persists view state
NH4Delete selected (user rows)Rows removed after success; broadcast rows have no delete checkbox

  • /dashboard/notifications
  • /dashboard/help
  • /dashboard/profile — user profile (separate from settings tabs)
  • /dashboard/settings — tabs via ?tab= query (general, notifications, devices, team; navigation updates the URL)

Access (UI): /dashboard/notifications may require module notification in navigation; TopBar can still fetch the list when the user is authenticated.

API: GET /auth/vendor/notificationsfetchVendorNotifications in vendor-notifications.api.ts (Laravel VendorController@get_notifications parity per file comment).

Bulk delete (store / user rows only): POST /auth/vendor/notifications/delete with body { "ids": string[] } — deletes up to 100 IDs scoped to the authenticated vendor’s user_notifications rows. Broadcast items from the merged list are not deletable via this endpoint. The inbox UI paginates the filtered list client-side and calls deleteVendorNotifications after multi-select.

Mark read (user_notifications only): POST /auth/vendor/notifications/read with { "ids": string[] } — sets status to 0 for up to 100 rows. The vendor web client calls markVendorNotificationsRead when marking store rows read; broadcast rows still rely on sessionStorage seen IDs (vendor-notification-seen-ids).

The page merges server status for user rows with sessionStorage “seen” IDs for broadcast unread styling. Category/icon helpers live in lib/vendor-notification-display.ts. Preference toggles do not hide existing inbox rows: when e.g. New orders is turned off, the API stops creating new user_notifications of that type, but rows already stored still appear in GET /auth/vendor/notifications until they age out of the server window or the user deletes them. The list scopes user_notifications by vendor_id, time window, and recipient (recipient_audience / recipient_user_id vs JWT sub) so targeted rows do not leak across staff accounts.

In-app notification types (user_notifications)

Section titled “In-app notification types (user_notifications)”

Rows are created by Nest writers (e.g. VendorStoreInAppNotificationsService, VendorOrderInAppNotificationsService, PosService for new_order, AdminOpsNotificationsService for new_store) when the effective toggles allow it (owner vendors column with store dashboard_prefs fallback when the column is unset). Each in-app row stores data as JSON with at least title, description, type, and source (vendor or pos).

Recipient columns (user_notifications): recipient_audience (string, e.g. vendor_account, vendor_employee) and optional recipient_user_id make the intended audience explicit. For vendor JWTs, recipient_user_id matches sub on the token (vendors.id for owners, vendor_employees.id for staff). vendor_account with a null user id means “everyone on that vendor JWT”; a non-null recipient_user_id limits the row to that login only. The merged GET /auth/vendor/notifications list applies this filter so targeted rows do not leak across staff accounts. Legacy rows with a null audience behave as before (visible to any vendor login for that vendor_id).

new_order (POS): Owner prefs (vendors.notificationPrefs with store dashboard_prefs fallback) drive the vendor_account row via pushOrders / pushOrdersOwner (resolvePushOrdersForOwner). When a vendor employee places the order, the vendor_employee row is gated separately by that employee’s prefs + store fallback (pushOrdersEmployee, resolvePushOrdersForEmployee). A vendor owner actor only gets the vendor_account row when owner prefs allow. An employee actor can get zero, one, or two rows depending on ownerWants and employeeWants independently.

data.typePreference key (must be true to emit)When it fires (summary)
low_stockpushLowStockAggregated item stock crosses into low stock (was not low, then is).
receivable_duepushReceivableDuePOS flow records an outstanding receivable with a due reminder (e.g. after placing an order with BNPL terms).
new_discountpushNewDiscountVendor creates a coupon for the store.
discount_duplicatedpushNewDiscountVendor duplicates an existing coupon (same toggle as new discount).
new_customerpushNewCustomerVendor creates a store customer record.
return_requestpushReturnRequestA return/refund request is successfully submitted for an order (e.g. refundRequest path).

Email for the same domains uses parallel keys where implemented (emailReturnRequest, emailReceivableDue, emailNewDiscount, emailNewCustomer, etc.); in-app rows above only honor the push* flags.

Vendor UI filters: the notifications page groups items into existing tabs — roughly low_stock / new_discount / discount_duplicatedSystem, receivable_duePayments, return_requestOrders, new_customerCustomers.

/dashboard/help is a static marketing-style help hub (search box and topic cards). There is no CMS or API fetch in the page component.

Service: apps/vendor-web/src/services/vendor-settings.api.ts.

Tab / areaAPI (representative)
Context bootstrapGET /vendor/settings/context
GeneralGET/PATCH /vendor/settings/general
Profile (any authenticated vendor; no settings module)GET/PATCH /auth/vendor/profile
Notification prefsGET/PATCH /auth/vendor/settings/notifications (JWT only; same pattern as admin)
BillingGET /vendor/settings/billing (read-focused in UI)
Devices / terminalsGET/PATCH /vendor/settings/devices, GET .../devices/:deviceId (payment terminal detail), GET .../devices/discover, POST .../devices/test-connect, POST .../devices/payment-terminal/close-batch, terminal API key regenerate

The legacy Team tab inside /dashboard/settings was retired — staff accounts and invitations now live under Employees, Managers, and Roles in the dashboard sidebar. The backend endpoints GET /vendor/settings/team and POST /vendor/settings/team/invites still exist for compatibility but are no longer surfaced by the vendor web UI.

Access: /dashboard/settings maps to module settings.

Notification prefs (vendor JWT): GET/PATCH /auth/vendor/settings/notifications merges vendors.notificationPrefs or vendor_employees.notificationPrefs with store dashboard_prefs when the user column is empty (same keys as StoreNotificationPrefs). In-app insert paths for New store, Order completed, Low stock, Return requests, Receivable due, New discount, New customer all gate on owner prefs (resolveVendorOwnerNotificationPrefsForStore), like the owner-targeted new_order row. Vendor employees do not use New store added or New customer in-app (no vendor_employee rows for those types; settings API returns those flags as off for employees and the UI hides the toggles).

Language and theme, if present, follow i18next and any global theme toggles elsewhere in the app; the settings tabs above reflect what is wired to /vendor/settings/* in code.