Notifications, Short-Links & Remote Config — Technical Reference¶
Part of the Aashray Technical Reference. Pairs with Business Logic — Notifications, Short-links & Config, which describes these mechanisms in plain terms with no code detail. Related technical detail currently lives inline in: Accounts, Identity & Auth, Booking Lifecycle & Engine, Admin Panel Map (not yet split out into its own business file).
This file is 100% derived from reading the backend, admin, and app source on main — no operational/staff-knowledge content lives here (that's in the business-logic companion). It is the cross-repo, backend-authoritative spec for every way Aashray reaches a user outside the request/response cycle, plus two infrastructure services the app depends on. It covers: push notifications (Expo push, token capture at login, notification→deeplink navigation), transactional email (SES SMTP + Handlebars templates — which email sends when), WhatsApp (Meta Cloud API templates for booking creation and every status transition, wifi, and the daily kitchen meal count), short-links + the /go/ redirect service (how the app's Profile tutorial links resolve), and remote app config / version gating (the updates endpoint the app polls to force upgrades). Backend is the source of truth throughout; the member app and admin panel are described as clients of these mechanisms.
Where the three transports overlap: a single booking action typically fans out to email + WhatsApp + push through different code paths. sendUnifiedEmail (email) internally also calls sendUnifiedWhatsApp; push is sent separately by the calling controller via notifyCardno / sendDualUserNotifications.
Verification status: audited & anchored per
docs/DOCS-METHODOLOGY.mdon 2026-07-07 againstaashray-backendmain. Every rule carries a[src: …]anchor. This domain was found fully accurate — all discrepancies (Expo-not-FCM, uncleared stale tokens,'/home'vs'/'default mismatch, hardcoded WhatsApp recipients, local-file meal-count baseline, no admin route forupdates) confirmed real against code; no false claims.
1. Push notifications (Expo)¶
What it is. Server-initiated push delivered through Expo's push service (expo-server-sdk), not raw FCM. Tokens are Expo push tokens of the form ExponentPushToken[…]. Firebase Cloud Messaging is only the underlying Android transport that Expo brokers; the backend never talks to FCM directly.
Member-app behavior.
- A push token is registered when the app foregrounds and is passed into login (see Accounts & Identity). On Android the app creates a default channel (max importance).
- Tapping a notification reads its data payload: a screen field (with optional params) drives in-app navigation via expo-router — a notification can land the user on any route. On error it falls back to Home.
- Because notifications (and deep links) can enter the app on a deep screen, those screens must stand alone (clear back/Home affordance).
Backend rules & logic.
- services/notification.service.js is a singleton NotificationService wrapping new Expo(). Config: defaultSound:'default', defaultTitle:'Notification', defaultBody:'This is a notification', receiptCheckDelay:15000, maxRetries:3, ratePerSecond:600, chunkSize:100. [src: services/notification.service.js:13-22,277]
- sendPushNotifications(arg1, arg2?, arg3?) accepts two signatures: (tokens[], notification, options?) or (tokenData[], options?). It maps to per-token message objects, validates every token with Expo.isExpoPushToken (invalid/missing tokens are logged and skipped), throws if no valid tokens remain.
- Each message: { to, sound, title, body, data:{ screen: screen||'/', ...data }, priority:'default', badge, channelId, categoryId, mutableContent, ttl }. Default screen when unset is '/'. [src: services/notification.service.js:39-61,174,186]
- Delivery: messages are grouped into per-second buckets of ratePerSecond (600), each bucket chunked (~100) via chunkPushNotifications, sent with retry + linear backoff (1000ms × attempt, up to maxRetries), and a 1s pause between buckets.
- After sending, _processReceipts fetches receipts and logs errors; a DeviceNotRegistered receipt is logged as a warning but the stale token is NOT removed from the card record.
- helpers/notification.helper.js wrappers:
- sendSingleNotification(token, options) — one recipient.
- sendDualUserNotifications({primary, bookedBy, screen='/home', data}) — sends to a primary (direct token or cardno lookup in CardDb) and optionally the "booked-by" user (cardno lookup). Never throws; returns {success, sentCount, totalRequested}. Skips silently when no token found.
- notifyCardno(cardno, {title, body, screen, data, sound}) — looks up token by cardno and sends one; returns {success, reason?} (missing_cardno/no_token/invalid_payload/error).
- getOtherBookingUser(booking, cancellerCardno) — pure helper returning the other party (cardno vs bookedBy) for dual notifications on cancellation.
Token capture / lifecycle.
- Capture: POST /api/v1/client/verifyAndLogin body carries { mobno, password, token }; on a correct password the backend runs CardDb.update({ token }, { where:{ mobno } }). The Expo token lives in card_db.token (TEXT, nullable).
- Clear: GET /api/v1/client/logout?cardno=… sets token = null.
- There is no dedicated token-refresh endpoint — the token is only rewritten on the next login. [src: controllers/client/auth.controller.js:78-87,131-134; models/card.model.js:83-86]
Every route.
| Method | Path | Auth realm | Purpose | Key params/body |
|---|---|---|---|---|
| POST | /api/v1/client/verifyAndLogin |
Public (credential) | Login; also captures the Expo push token | body { mobno, password, token } |
| GET | /api/v1/client/logout |
Member | Logout; clears stored token | query cardno |
| POST | /api/v1/profile/notification |
Member | Generic/manual push sender (test or ad-hoc) | body { tokenData: [{ token, title, body, screen, data }] } |
(No standalone "send push" admin route exists — pushes are triggered inline by booking/admin controllers, below.)
Admin operations. Push is fired as a side-effect of admin actions, not from a dedicated screen:
- Admin room / travel / adhyayan / flat management confirm/cancel/checkout → sendDualUserNotifications(...).
- adhyayanManagement soft-delete shibir → collects all affected cardno + bookedBy tokens and sendPushNotifications(tokens, { title:'Adhyayan Cancelled by Admin', body:'…cancelled by admin…', screen:'/bookings' }).
- Member-side cancellations (room/travel/utsav booking controllers) notify the other party via notifyCardno(other, { title, body, screen:'/bookings' }).
Data & relationships. Single column card_db.token. No separate device/token table, so one token per card (last-logged-in device wins). Push recipients are always resolved from cardno → token.
Edge cases & branches.
- Invalid or missing Expo tokens are dropped silently (logged) — a user who never logged in on this build gets nothing.
- DeviceNotRegistered errors are observed but not self-healed (stale token persists).
- sendDualUserNotifications defaults screen to '/home', while NotificationService defaults to '/' — inconsistent default target.
- Rate limiting is best-effort in-process; a crash mid-send does not resume.
Discrepancies.
- The client stack docs say "Firebase push notifications," but the backend uses Expo push (ExponentPushToken) — Firebase is only the Android transport. Treat Expo as the contract. [src: services/notification.service.js:13,174]
- Stale-token cleanup is a known gap (DeviceNotRegistered not cleared). This is the code-level cause of the business-logic companion's "no way to update a stale phone notification address." [src: services/notification.service.js:235-237]
- The two mismatched default deep-link targets ('/home' vs '/') mean a notification sent through a path that omits screen can land the user on a different fallback screen depending on which helper sent it. [src: helpers/notification.helper.js:49 ('/home'); services/notification.service.js:186 ('/')]
2. Transactional email¶
What it is. Handlebars-templated email sent through Amazon SES over SMTP via nodemailer. Fire-and-forget: sendMail uses a callback (no await/throw), so email failures never block a booking.
Member-app behavior. No in-app email UI; email is a passive confirmation channel. The member sees bookings/toasts in-app; the email arrives separately to card.email (or the booker's email).
Backend rules & logic.
- utils/sendMail.js: transporter from SES_SMTP_HOST/PORT/USERNAME/PASSWORD; from = "Vitraag Vigyaan Aashray" <SES_SMTP_EMAIL>; header X-SES-CONFIGURATION-SET: aashrayconfigset; templates in /emails (.hbs, no layout, partials from same dir). Logs email_sent / email_send_failed.
- controllers/helper.js orchestrators:
- sendUnifiedEmail(cardno, bookingIds{type:[ids]}, bookedBy, bookingStatus=CONFIRMED, template='unifiedBookingEmail', sendWhatsApp=true) — loads details for each booked type (utsav/adhyayan/travel/room/flat/food) from the DB, builds a context with showAdhyanDetail/showRoomDetail/… flags, subject = getSubject(status), welcomeMessage = getWelcomeMessage(status, country). Sends to user.email || bookedBy.email. If travel booked, cardno != null, and NODE_ENV==='prod', also sends a copy to RAJ_PRAVAS_EMAIL. When sendWhatsApp is true it then calls sendUnifiedWhatsApp(...) (email + WhatsApp share this entry point). [src: controllers/helper.js:353-359,625,652-656,671-682; getSubject :327-335]
- sendUnifiedEmailForBookedBy(userBookingIdMap, bookedBy, status, sendWhatsApp=true) — flattens a per-card map, detects self vs on-behalf, delegates to sendUnifiedEmail.
- getSubject: pending → 'Bookings created', cancelled → 'Bookings cancelled', else 'Bookings confirmed'.
- getWelcomeMessage: pending → NRI variant ("NRIs can pay at the Research Center upon arrival") vs domestic ("Payment is due within 24 hours"); cancelled/confirmed have their own copy.
- helpers/mailer.helper.js:
- sendCancellationEmail(cardno, bookingIds, bookedBy) → sendUnifiedEmail(..., STATUS_CANCELLED, 'unifiedCancellationEmail', sendWhatsApp=false) — WhatsApp deliberately suppressed here (cancellation WA is handled by the status-change functions in §3).
- sendOpenBookingEmail(bookingType, openBookings) — when a waitlisted booking "opens up," dispatches per-type: utsav→sendUtsavBookingUpdateEmail, adhyayan→sendAdhyayanBookingUpdateNotification, travel→sendTravelBookingStatusUpdateMail.
When each email sends.
| Event | Trigger site | Template | Status |
|---|---|---|---|
| Booking created (self/guest/mumukshu) | roomBooking / guestBooking / mumukshuBooking controllers → sendUnifiedEmailForBookedBy |
unifiedBookingEmail |
pending |
| Booking confirmed after payment | payment.controller → sendUnifiedEmail(...CONFIRMED, 'unifiedBookingEmail') |
unifiedBookingEmail |
confirmed |
| Admin manual room confirm | roomManagement.controller → sendUnifiedEmail(...CONFIRMED) |
unifiedBookingEmail |
confirmed |
| Cancellation | sendCancellationEmail |
unifiedCancellationEmail |
cancelled |
| Waitlist opened | sendOpenBookingEmail |
rajAdhyayanUpdate / utsavStatusUpdate / rajPravasStatusUpdate |
open/available |
| Forgot password | auth.controller (line ~188) |
forgotPasswordEmail |
— (also WhatsApp temp password) |
| Maintenance request | maintenanceRequest.controller |
maintainanceRequest |
— |
| Travel booking (Raj Pravas) copy | helper.js when travel + prod |
same template, RAJ_PRAVAS_EMAIL cc |
any |
Every route. Email has no HTTP routes of its own — it is always a side-effect of the booking/auth/maintenance routes documented in their own domains.
Admin operations. Admin confirm/cancel actions reuse sendUnifiedEmail / sendCancellationEmail; travel management sends a sendMail(...) directly. There is no template-management UI — templates are checked-in .hbs files.
Data & relationships. Recipient = card_db.email; content assembled from the same booking tables (RoomBooking, FlatBooking, TravelDb, ShibirBookingDb, UtsavBooking, FoodDb) joined to ShibirDb/UtsavDb/UtsavPackagesDb. /emails templates present: unifiedBookingEmail, unifiedCancellationEmail, rajAdhyayanUpdate, rajAdhyayanCancellation, rajPravasStatusUpdate, rajPravasCancellation, _rajPravasConfirmation, rajSharanCancellation, utsavBooking, utsavStatusUpdate, forgotPasswordEmail, maintainanceRequest, plus _footer / styles partials.
Edge cases & branches.
- If neither user.email nor bookedBy.email exists, no email is sent (silent).
- Fire-and-forget: an SES failure is logged but never surfaced to the user or the API response.
- The Raj Pravas cc only fires in prod.
Discrepancies. Cancellation email suppresses WhatsApp (WA cancellation is a separate transition-based path), whereas booking/open emails do fan out to WhatsApp — the "email ↔ WhatsApp" coupling is inconsistent by design and easy to misread.
3. WhatsApp messaging¶
What it is. Template ("HSM") messages via the Meta WhatsApp Cloud API. utils/sendWhatsAppMessage(phone, templateName, components, langCode?) POSTs to WHATSAPP_API_URL with Authorization: Bearer WHATSAPP_BEARER_TOKEN, language default WHATSAPP_DEFAULT_LANG (en_US). helpers/whatsapp.helper.js (~2600 lines) owns all template selection.
Member-app behavior. Passive channel — the member receives WhatsApp confirmations/updates; there is no in-app WhatsApp UI. Numbers are normalized by utils/phoneFormatter.formatWhatsAppPhone(mobno, country) (India → 91 prefix; country-code map for US/UAE/UK/etc.; UAE leading-1 correction; fallback 91).
Backend rules & logic.
- Template naming convention: bn_/bk_ prefix + type token (adh=adhyayan, sha=room/sharan, flt=flat, pvs=travel/pravas, usv=utsav, psd=food/prasad) + audience (s_b self, gu_b guest booker, gu_f guest attendee, mu_b mumukshu booker) + status (w/wg waiting, ppg/pp payment-pending, cf/cnf/cnfm confirmed) + optional _nri. bn_ = booking notification (creation), bk_ = status-change transition.
- sendWithTemplateFallback(phone, template, components) — tries the template in default lang, retries in en, then falls back to a "confirmed" variant (with parameter-count fixups for known templates), finally to booking_adhyayan_self_confirmed. Handles Meta's 404 "template does not exist." Non-fatal.
- buildBodyComponents pads/normalizes params (Meta rejects empty strings → single space).
- Creation fan-out: sendUnifiedWhatsApp(cardno|user, adhyayan[], travel[], flat[], utsav[], room[], bookedForCardno?, food[]) runs per-type senders concurrently (Promise.allSettled): sendAdhyayanWhatsApp, sendRoomWhatsApp, sendTravelWhatsApp, sendUtsavWhatsApp, sendFlatWhatsApp, sendFoodWhatsApp.
- Status-change (transition) senders: sendAdhyayanStatusChangeWhatsApp, sendRoomStatusChangeWhatsApp, sendFlatStatusChangeWhatsApp, sendTravelStatusChangeWhatsApp, sendUtsavStatusChangeWhatsApp all take (booking, previousStatus, options), map prev→new status to a bk_* template, and dispatch separately to the attendee (self) and the booker (guest). They resolve credits refunded, payment id, and pick _a (admin) vs _c (cron/callback) variants from updatedBy/options.isCron.
- Cost/volume skips: adhyayan "Param Gyaan Sabha" is skipped; PR (permanent resident) self food bookings are skipped (still sent when a PR books food for a guest). Adhyayan status-change WhatsApp only fires when the shibir location === RESEARCH_CENTRE.
Per-domain triggers (status-change messages).
| Domain | Function | Triggered from |
|---|---|---|
| Room | sendRoomStatusChangeWhatsApp |
admin room mgmt (confirm/cancel/checkin/checkout), payment callback, client room cancel |
| Flat | sendFlatStatusChangeWhatsApp |
admin room/flat mgmt, payment callback, client cancel |
| Adhyayan | sendAdhyayanStatusChangeWhatsApp |
adhyayanBooking.helper, admin adhyayan mgmt (RC only) |
| Utsav | sendUtsavStatusChangeWhatsApp |
admin utsav mgmt, payment callback, client utsav cancel |
| Travel | sendTravelStatusChangeWhatsApp |
admin travel mgmt, payment callback, client travel cancel |
Other WhatsApp messages.
- Wifi: sendWifiRequestWhatsApp(cardno, username, status, code?, deviceType?) — status→template map deleted→per_wf_code_req_del, rejected→…rej, reset→…res, pending→…pend, approved→per_wf_code_req_cnf_ad_m; device type deduced from username suffix (ph/pc/tb). sendWifiLowAlertWhatsApp(activeCount) → template temp_wifi_code_alert to a hardcoded admin number 919819988657. [src: helpers/whatsapp.helper.js:1257-1259]
- Late checkout fee waived: sendLateCheckoutFeeWaivedWhatsApp(transaction) → bk_sha_s_f_lcf_waived.
- Password update: password_update_app template (from profile/auth password update).
- Tomorrow's kitchen meal count: sendTomorrowMealsCount(recipients[]) — sums FoodDb (individual) + BulkFoodBooking breakfast/lunch/dinner for tomorrow (Asia/Kolkata), writes a local baseline file last_meals_count.json, and sends template daily_kitchen_meal_count to each recipient card. checkAndSendMealsCountUpdate() — recomputes and compares to the baseline file; if changed, notifies Maharaj (hardcoded cardno 0002823407); if no baseline exists (e.g. after restart) it saves the baseline and skips notifying to avoid a false alert. [src: helpers/whatsapp.helper.js:2418 (COUNT_FILE), 2494,2552-2559 (Maharaj :2559)]
Every route. WhatsApp has no HTTP routes — all sends are side-effects of booking/admin/wifi/cron code paths.
Admin operations. Admin booking-management actions and cron jobs are the primary WhatsApp initiators; the daily meal-count uses a scheduled job. No admin WhatsApp console exists in the reviewed code.
Data & relationships. Recipient phone from card_db.mobno + card_db.country. res_status gates PR food skips. Payment ids read from Transactions (razorpay_order_id). Meal counts read FoodDb + BulkFoodBooking.
Edge cases & branches.
- No mobile number → skipped with a warning.
- Template-missing (404) → fallback chain; if all fail, the error is logged (non-fatal).
- NRI (country !== 'india') selects _nri payment-pending templates.
- Day-visit room bookings (nights===0 / roomtype==='NA') use dedicated *_sdv templates.
- The meal-count baseline is a single JSON file on local disk — on multi-instance or ephemeral (Render) deploys the baseline can reset, silently skipping one change notification. This is the code-level cause of the meal-count fragility referenced in the business-logic companion.
Discrepancies.
- Two hardcoded recipients: wifi-low-alert phone 919819988657 and meal-count cardno 0002823407. These are the "fixed recipient lists" the business-logic companion refers to — changing them today requires an engineer to edit and redeploy code, not a staff-facing setting. [src: helpers/whatsapp.helper.js:1259,2559]
- Meal-count durability depends on a local file, not the DB — fragile across restarts/instances.
- Cancellation WhatsApp comes from the status-change functions, not from the cancellation email path (which sets sendWhatsApp=false) — the two channels diverge on cancellation.
4. Short-links & the /go/ redirect¶
What it is. A first-party URL shortener. Admins mint slug → target_url links (grouped by department type); the public /go/:slug endpoint 302-redirects and counts clicks. The member app's Profile/Support tutorial rows open these short URLs.
Member-app behavior. On Profile → Support, each help topic expands to tutorial rows with Apple/iOS and Android buttons that open platform-specific how-to links formatted as https://aashray.vitraagvigyaan.org/go/{slug} (a light haptic fires on open). The app doesn't create links; it only opens them.
Backend rules & logic.
- Model short_links: slug (unique, string), target_url (TEXT), type (ENUM accounts|room|card|office|food|adhyayan|travel|utsav|avt|wifi, default wifi), active (bool, default true), click_count (int, default 0), createdBy (string), timestamps. [src: models/short_link.model.js:13-56]
- Create validates: slug matches ^[A-Za-z0-9_-]+$, target_url is a valid http/https URL, type ∈ VALID_TYPES; uniqueness enforced in a locked transaction; createdBy = req.user.username.
- Authorization is two-layer: route-level authorizeRoles(...) broad gate (any admin role) plus per-type check in the controller via TYPE_ROLE_MAP (each type → [ROLE_SUPER_ADMIN, ROLE_<TYPE>_ADMIN]). Update also re-checks the role for both the current and the new type when type changes. [src: controllers/admin/shortLink.controller.js:31-42,64-78,173-186; routes/admin/shortLink.routes.js:35,42-57]
- Redirect: redirectShortLink finds { slug, active:true }, increment('click_count'), then res.redirect(target_url) (302). Missing/inactive → 404. [src: controllers/admin/shortLink.controller.js:135-148; routes/admin/redirect.routes.js:10; app.js:197-198]
Every route.
| Method | Path | Auth realm | Purpose | Key params/body |
|---|---|---|---|---|
| POST | /api/v1/short-links/ |
Admin (role-gated) | Create a short link | body { slug, target_url, type } |
| GET | /api/v1/short-links/:type |
Admin (type role) | List all links of a department type | param type |
| PUT | /api/v1/short-links/:id |
Admin (role-gated) | Update target_url / type / active |
param id; body { target_url?, type?, active? } |
| DELETE | /api/v1/short-links/:id |
Admin (role-gated) | Delete a link | param id |
| GET | /go/:slug |
Public | Resolve + count click + 302 redirect | param slug |
Mounts (app.js): /api/v1/short-links (admin, router.use(auth)) and / for the redirect router (GET /go/:slug), placed before the catch-all 404.
Admin operations. admin/common/shortLinks.js + .html UI: a client-side roleTypeMap computes the admin's allowed types from sessionStorage roles, fetches links per allowed type, and renders a table with Copy / Enable-Disable / Delete actions plus summary cards (Total / Active / Disabled / Total Clicks). It builds the display URL as https://aashray.vitraagvigyaan.org/go/{link.slug}.
Data & relationships. Standalone short_links table; type maps to admin roles via TYPE_ROLE_MAP (same role constants used across the Admin Panel Map). No FK to bookings/cards; createdBy is a free-text username.
Edge cases & branches.
- Duplicate slug → 400 (SequelizeUniqueConstraintError also mapped to 400).
- Disabled link (active:false) → 404 from /go/ (no distinct "disabled" message).
- click_count increments on every hit including bots/link-previews; no dedupe or rate limit.
- Changing a link's type requires the admin to hold a role for both old and new types.
Discrepancies.
- Route-level and controller-level role checks are intentionally duplicated (commented as such) — redundant but not a bug.
- The redirect is unauthenticated and uncounted-for-abuse (any public hit inflates click_count).
5. Remote app config / updates (version gating)¶
What it is. A minimal remote-config surface: a single updates table per-OS that the app polls to decide whether an upgrade is available or mandatory (force-update).
Member-app behavior. The app is expected to call the updates endpoint with its OS and compare its running version to latestVersion; when mandatory is true it should block usage until updated, showing releaseNotes. (Note: the app-client spec does not document this call — see Discrepancies.)
Backend rules & logic.
- Model updates: os (ENUM android|ios), version (string), mandatory (bool, default false), releaseNotes (TEXT), timestamps. [src: models/updates.model.js:13-29]
- checkForUpdates(req) validates os ∈ {android, ios} (400 otherwise; it lowercases os first, so mixed-case is accepted), fetches the latest row for that OS by createdAt DESC (404 if none), and returns { latestVersion, mandatory, releaseNotes }. [src: controllers/client/updates.controller.js:9-32; routes/client/updates.routes.js:6; app.js:164]
- No per-user flags, no feature toggles, no A/B config — version gating only.
Every route.
| Method | Path | Auth realm | Purpose | Key params/body |
|---|---|---|---|---|
| GET | /api/v1/updates/ |
Public (no auth) | Return latest version + mandatory flag + release notes for an OS | query os=android\|ios |
Admin operations. No admin route to create/edit updates rows exists in the reviewed code — rows are presumably inserted directly in the DB (or via an unshown tool). There is no UI counterpart in admin/.
Data & relationships. Standalone updates table; unrelated to cards/bookings. It is the only true "remote config" store. Note: the app-client spec's other "config" flags (showDevelopmentDashboard, isFlatOwner, credit balances) live on the user/card record, not here.
Edge cases & branches.
- Missing/invalid os → 400; no version row → 404 (the app must treat 404 gracefully — likely "no update").
- "Latest" is purely by insertion time (createdAt), not by semantic version compare — an out-of-order insert could regress the reported version.
Discrepancies.
- The app-client spec does not document the updates/force-update flow at all — the endpoint exists backend-side but its client consumer is unspecified here.
- No auth on a version endpoint is fine, but there is also no admin management path for it — operationally opaque. [src: only routes/client/updates.routes.js references the model; no admin updates route/controller]
- Version freshness relies on createdAt ordering rather than a version comparison.
How this connects to other domains (technical)¶
- Accounts, Identity & Auth: login is where the Expo push token is captured (
verifyAndLoginbodytoken) and logout clears it; forgot-password fans out to both email (forgotPasswordEmail) and WhatsApp (temp password). The recipient identity for all three transports is thecard_dbrow (token,email,mobno,country,res_status). - Booking Lifecycle & Engine and Payments, Credits & Reconciliation: booking creation and payment confirmation are the main fan-out points —
sendUnifiedEmail(email +sendUnifiedWhatsApp) plus controller-level push. Status transitions (confirm/cancel/checkin/checkout, waitlist-open) drive thebk_*WhatsApp templates andsendDualUserNotifications. - Adhyayan & Utsav: adhyayan status-change WhatsApp is Research-Centre-gated; utsav updates have dedicated templates and emails; feedback deep links (
/adhyayan/feedback/:id,/utsav/feedback/:id) are reached via the notificationscreenpayload. - Admin Panel Map: short-link
typeauthorization reuses the same admin role constants; admin booking-management actions are the primary initiators of push/WhatsApp/email side-effects; the shortLinks admin UI lives underadmin/common/. - Services, WiFi, Maintenance, Support & Gate: wifi request lifecycle and low-code alerts, and maintenance requests, each have their own WhatsApp/email templates but share the same
sendWhatsAppMessage/sendMailplumbing described here. - Deep-linking: the
/go/redirect and the push-notificationscreenpayload are the two mechanisms that can land a user on a deep route; the deep-link route table lives in the app-client data-model appendix (API Reference).