OBS EXPERIENCES
1. Backend Architecture Design
The OBS MVP backend is a monolithic Node.js/Express API deployed in Docker, backed by a single PostgreSQL database. This approach minimises operational complexity for an early-stage product while laying the groundwork for future service extraction.
1.1 Technology Choices
| Component | Technology | Rationale |
|---|---|---|
| API Server | Node.js 20 LTS + Express 5 | Team familiarity; excellent async I/O for real-time features |
| Database | PostgreSQL 16 | Relational integrity for bookings; JSONB for flexible trek metadata |
| ORM | Prisma ORM | Type-safe queries; auto-generated migrations; clean schema file |
| Auth | JWT (access) + Refresh tokens | Stateless; works offline-first for mobile |
| File Storage | AWS S3 / Cloudflare R2 | Trek media, user documents, certificates |
| Resend (or AWS SES) | Transactional emails: booking confirmation, OTP, receipts | |
| SMS / OTP | MSG91 or Twilio India | OTP verification; WhatsApp booking confirmations |
| Payments | Razorpay | India-first; supports UPI, cards, EMI, net banking |
| Background Jobs | BullMQ + Redis | Scheduled reminders, email queues, certificate generation |
| Caching | Redis | Session store, rate limiting, frequently-read trek data |
| Containerisation | Docker + Docker Compose | Consistent dev/prod parity; easy VPS deployment |
1.2 Database Schema (Core Tables)
All tables include created_at, updated_at timestamps and soft-delete via deleted_at where applicable.
Users & Auth
| Table | Key Columns | Notes |
|---|---|---|
| users | id (uuid PK), email, phone, name, profile_photo_url, bio, referral_code, referred_by_id | Core identity table |
| auth_tokens | id, user_id (FK), refresh_token_hash, device_info, expires_at, revoked_at | Refresh token store; one row per device session |
| otps | id, phone/email, otp_hash, purpose, expires_at, verified_at | Purpose: login | signup | password_reset |
| emergency_contacts | id, user_id (FK), name, relationship, phone, email, is_primary | Min 2 required before trek activation |
Treks & Inventory
| Table | Key Columns | Notes |
|---|---|---|
| treks | id, slug, name, description, region, difficulty (enum), max_altitude_m, duration_days, min_group_size, max_group_size, status (draft|active|archived), meta (JSONB) | Master trek catalogue |
| trek_media | id, trek_id (FK), url, type (image|video), sort_order, is_hero | S3 keys for trek imagery/video |
| trek_itinerary_days | id, trek_id (FK), day_number, title, camp_name, altitude_m, distance_km, terrain_type, highlights, meal_plan | Day-by-day breakdown |
| trek_batches | id, trek_id (FK), start_date, end_date, price_inr, seats_total, seats_booked, seats_held, guide_id (FK), status (open|full|cancelled) | Each scheduled departure |
| guides | id, name, bio, photo_url, years_experience, phone, is_active | Guide roster; linked to batches |
| faqs | id, entity_type (trek|global), entity_id, question, answer, sort_order | Trek-specific and global FAQs |
Bookings & Payments
| Table | Key Columns | Notes |
|---|---|---|
| bookings | id, user_id (FK), batch_id (FK), status (pending|confirmed|cancelled|completed), participants_count, total_amount_inr, paid_amount_inr, coupon_id (FK), cancellation_reason, cancelled_at | Central booking record |
| booking_participants | id, booking_id (FK), name, age, id_proof_url, is_primary | Each trekker on a booking |
| payments | id, booking_id (FK), razorpay_order_id, razorpay_payment_id, amount_inr, status (created|captured|failed|refunded), payment_method, captured_at | Payment ledger; immutable append-only |
| coupons | id, code (unique), discount_type (pct|flat), discount_value, min_order_inr, max_uses, used_count, valid_from, valid_until, is_active | Promo/discount codes |
| installment_plans | id, booking_id (FK), installment_number, due_date, amount_inr, paid_at, payment_id (FK) | EMI / part-payment tracking |
Documents & Pre-Trek
| Table | Key Columns | Notes |
|---|---|---|
| user_documents | id, user_id (FK), booking_id (FK), doc_type (id_proof|fitness_form|waiver|medical), s3_key, original_filename, status (pending|approved|rejected), reviewed_by | Uploaded pre-trek docs |
| packing_checklist_items | id, trek_id (FK nullable), name, category, is_mandatory, sort_order | Default items; users can add custom items |
| user_checklist_progress | id, user_id (FK), booking_id (FK), item_id (FK), is_checked | Per-booking checklist state |
Loyalty & Referrals
| Table | Key Columns | Notes |
|---|---|---|
| loyalty_points | id, user_id (FK), points_delta (can be negative), reason, reference_id, reference_type, balance_after | Append-only ledger; never update rows |
| loyalty_tiers | id, name, min_points, max_points, discount_pct, perks (JSONB) | Newcomer → Trekker → Explorer → Summit Seeker → OBS Legend |
| referrals | id, referrer_id (FK), referee_id (FK), booking_id (FK nullable), status (registered|booking_confirmed), points_awarded_at | Referral tracking |
Support
| Table | Key Columns | Notes |
|---|---|---|
| support_tickets | id, user_id (FK), booking_id (FK nullable), subject, status (open|in_progress|resolved|closed), priority | Customer support tickets |
| support_messages | id, ticket_id (FK), sender_type (user|admin), message, attachments (JSONB) | Thread messages on a ticket |
| faqs | id, entity_type, entity_id, question, answer, sort_order | Static FAQ content (also covers global) |
1.3 API Route Structure
RESTful API with /api/v1 prefix. JWT Bearer token required on all protected routes (marked *).
Auth Routes (/api/v1/auth)
| Method | Path | Description |
|---|---|---|
| POST | /send-otp | Send OTP to phone/email |
| POST | /verify-otp | Verify OTP → return access + refresh tokens |
| POST | /login | Email + password login (fallback) |
| POST | /register | Email/social registration |
| POST | /refresh | Rotate refresh token |
| POST | /logout * | Revoke current device refresh token |
| POST | /logout-all * | Revoke all refresh tokens (user account) |
Trek Routes (/api/v1/treks)
| Method | Path | Description |
|---|---|---|
| GET | / | List all active treks (filters: region, difficulty, duration, price, month) |
| GET | /:slug | Trek detail page data (includes itinerary, media, batches, guides, FAQs) |
| GET | /:id/batches | Available batches with seat counts and prices |
| GET | /:id/reviews | Paginated verified reviews for a trek |
| GET | /:id/itinerary/pdf * | Generate + return offline itinerary PDF |
| GET | /:id/packing-list | Default packing list items for a trek |
Booking Routes (/api/v1/bookings)
| Method | Path | Description |
|---|---|---|
| POST | / * | Create booking (reserve seats, create Razorpay order) |
| GET | / * | List user's bookings (status filter) |
| GET | /:id * | Booking detail (itinerary, documents, payments, checklist) |
| POST | /:id/cancel * | Cancel booking (triggers refund calculation) |
| POST | /:id/participants * | Add/update participant details |
| POST | /:id/documents * | Upload pre-trek document (presigned S3 URL flow) |
| GET | /:id/documents * | List documents for a booking |
| PATCH | /:id/checklist * | Update packing checklist item state |
Payment Routes (/api/v1/payments)
| Method | Path | Description |
|---|---|---|
| POST | /webhook | Razorpay webhook handler (HMAC verified, public endpoint) |
| POST | /verify * | Client-side payment capture verification |
| GET | /booking/:bookingId * | Payment history for a booking |
User Routes (/api/v1/users)
| Method | Path | Description |
|---|---|---|
| GET | /me * | Current user profile + loyalty tier |
| PATCH | /me * | Update profile (name, bio, photo) |
| POST | /me/photo * | Upload profile photo (presigned S3 flow) |
| GET | /me/emergency-contacts * | List emergency contacts |
| POST | /me/emergency-contacts * | Add emergency contact |
| DELETE | /me/emergency-contacts/:id * | Remove emergency contact |
| GET | /me/loyalty * | Points balance, tier, history |
| GET | /me/referrals * | Referral stats and tracking |
| DELETE | /me * | GDPR data deletion request |
Misc Routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/faqs | Global FAQs (also trek-specific via ?trek_id=) |
| POST | /api/v1/support/tickets * | Create support ticket |
| GET | /api/v1/support/tickets * | User's support tickets |
| POST | /api/v1/support/tickets/:id/messages * | Reply to ticket |
| GET | /api/v1/coupons/validate * | Validate a coupon code against a batch |
1.4 Admin Routes (/api/v1/admin)
All admin routes require admin JWT (separate role claim). Admin panel is a separate Next.js app consuming these.
| Domain | Routes Summary |
|---|---|
| Treks CMS | CRUD treks, itinerary days, media upload, FAQ management |
| Batch Management | Create/edit/cancel batches; assign guides; adjust capacity |
| Bookings | View all bookings; manual confirmation; refund initiation; export CSV |
| Users | View users; adjust loyalty points; support escalations |
| Documents | Review uploaded participant docs; approve/reject with notes |
| Analytics | Revenue by period, booking conversion, trek popularity, coupon usage |
| Coupons | Create/deactivate coupon codes; usage reports |
| Guides | CRUD guide profiles; trek assignment history |
1.5 Key Backend Flows
Booking Flow
| Step | Action | Notes |
|---|---|---|
| 1 | Client calls POST /bookings with batch_id, participants_count, coupon_code | Server validates seat availability using database row-level lock |
| 2 | Server reserves seats (seats_held++) and creates Razorpay order | Hold expires in 15 mins if payment not completed |
| 3 | Client completes Razorpay checkout, receives payment_id | Happens client-side in Razorpay SDK |
| 4 | Client calls POST /payments/verify with razorpay signature | Server validates HMAC signature against Razorpay secret |
| 5 | Server confirms booking, converts held seats to booked | Sends confirmation email via Resend + SMS via MSG91 |
| 6 | BullMQ job enqueued: generate certificate, schedule reminders | Async - does not block the API response |
Referral Flow
| Step | Action |
|---|---|
| 1 | User A shares referral link containing their referral_code |
| 2 | User B registers with the referral code → referrals row created (status: registered) |
| 3 | User B completes their first booking → referrals status → booking_confirmed |
| 4 | BullMQ job awards ₹500 equivalent points to User A (configurable); sends notification |
1.6 Security Considerations
- Rate limiting via Redis on all auth endpoints (5 OTP attempts per 10 minutes per phone)
- Razorpay webhook signature verification (HMAC-SHA256) before any payment state change
- S3 document uploads use presigned URLs — backend never streams files through the API server
- All user-uploaded content scanned for malware via ClamAV or AWS Macie before approval
- Row-level security on booking queries — users can only access their own bookings
- Admin routes protected by separate role claim in JWT; admin panel on separate subdomain
- HTTPS enforced everywhere; HSTS headers on website and API
- Database credentials, JWT secrets, Razorpay keys stored in environment variables; never in code
2. Website Sitemap (Next.js)
The website is the primary discovery and booking channel. Built with Next.js App Router using SSG for trek detail pages (fast load, SEO-optimised) and RSC+Client components for interactive booking flows.
2.1 Public Pages (No Auth Required)
| Route | Page Name | Rendering | Key Components |
|---|---|---|---|
| / | Home / Landing | SSG + ISR (1hr) | Hero video, featured treks carousel, 'Why OBS' section, testimonials, CTA band |
| /treks | Trek Listing | SSR (filters) | Search bar, filter panel (region/difficulty/duration/price), trek cards grid, 'no results' state |
| /treks/[slug] | Trek Detail | SSG + ISR (1hr) | Hero media, quick facts bar, day-wise itinerary accordion, batch selector, guide profiles, reviews, packing list, weather widget, FAQ accordion, sticky Book Now CTA |
| /blog | Blog Index | SSG + ISR | Article cards, category filters, search |
| /blog/[slug] | Blog Post | SSG + ISR | Article content, related treks, author card, social share |
| /about | About OBS | SSG | Team, story, values, mission |
| /guides | Our Guides | SSG + ISR | Guide profiles grid with experience badges |
| /faq | FAQ | SSG + ISR | Accordion FAQ, search within FAQ, contact CTA |
| /contact | Contact Us | SSG | Contact form, WhatsApp link, office address, support hours |
| /terms | Terms & Conditions | SSG | Static legal content |
| /privacy | Privacy Policy | SSG | Static legal content |
| /cancellation | Cancellation Policy | SSG | Policy table, refund timeline |
| /404 | Not Found | Static | Trek suggestions, home link |
2.2 Auth Pages
| Route | Page Name | Notes |
|---|---|---|
| /login | Login / Sign Up | Unified auth page; tab between login / register; Google/Apple OAuth; phone OTP |
| /auth/callback | OAuth Callback | Server-side token exchange; redirect to /dashboard or intended page |
| /auth/verify-otp | OTP Verification | Phone OTP entry; resend timer; back link |
| /auth/forgot-password | Forgot Password | Email input → magic link or OTP reset flow |
2.3 Booking Flow (Auth Required)
| Route | Step | Notes |
|---|---|---|
| /book/[batchId] | Step 1 — Participants | Participant count selector, primary trekker details, emergency contact required |
| /book/[batchId]/details | Step 2 — Trek Details Review | Itinerary summary, what's included, cancellation policy acknowledgement |
| /book/[batchId]/payment | Step 3 — Payment | Order summary, coupon code input, Razorpay checkout widget, loyalty points redemption |
| /book/[batchId]/confirm | Step 4 — Confirmation | Booking ID, PDF download, next steps checklist, share card |
2.4 User Dashboard (Auth Required — /dashboard)
| Route | Page Name | Notes |
|---|---|---|
| /dashboard | Dashboard Home | Upcoming trek card, quick links, loyalty tier progress, recent activity |
| /dashboard/bookings | My Bookings | Tabs: Upcoming / Past / Cancelled; booking cards with status badges |
| /dashboard/bookings/[id] | Booking Detail | Trek info, participants, payment history, document status, packing checklist, group details |
| /dashboard/bookings/[id]/documents | Documents Upload | Upload ID proof, fitness form, waiver; document status tracker |
| /dashboard/profile | My Profile | Photo upload, bio, edit personal details, emergency contacts manager |
| /dashboard/loyalty | Loyalty & Rewards | Points balance, tier badge, history ledger, redeem options |
| /dashboard/referrals | Refer & Earn | Referral code card, share options, tracking table (invited/registered/booked), earnings summary |
| /dashboard/support | Support | Create ticket, ticket list with status, message thread view |
| /dashboard/settings | Settings | Notification preferences, language, privacy, account deletion |
2.5 Admin Panel (Separate Next.js App — admin.ohbhaisahab.com)
| Route | Section | Key Functionality |
|---|---|---|
| /admin/dashboard | Overview | Revenue today/week/month, bookings pending, seats available, open support tickets |
| /admin/treks | Trek CMS | List + create/edit treks; itinerary editor; media uploader; FAQ editor |
| /admin/batches | Batch Management | Calendar view + list view; create batch; assign guide; capacity management |
| /admin/bookings | Bookings | All bookings table with filters; booking detail; manual actions; CSV export |
| /admin/users | Users | User list; profile view; loyalty adjustment; flag/ban |
| /admin/documents | Document Review | Pending documents queue; approve/reject with notes |
| /admin/guides | Guides | Guide CRUD; assignment history |
| /admin/coupons | Coupons | Create/deactivate codes; usage analytics |
| /admin/support | Support Tickets | Ticket queue; assign to agent; respond; resolve |
| /admin/analytics | Analytics | Revenue charts, trek performance, conversion funnel, cohort retention |
3. Mobile App Sitemap (React Native Expo)
The Expo app uses Expo Router (file-based routing) with a 5-tab bottom navigation structure as defined in the PRD. Authentication state is managed via Zustand + Secure Store for JWT persistence.
3.1 Unauthenticated Stack
| Screen | Route / Component | Description |
|---|---|---|
| Splash / Cinematic Entry | app/index.tsx (redirects) | Himalayan video loop; logo fade-in; tagline animation; single Begin CTA |
| Welcome | app/(auth)/welcome.tsx | Two paths: 'New here' → sign-up | 'Trekked with OBS' → login |
| Login | app/(auth)/login.tsx | Phone OTP (primary), Google OAuth, Apple OAuth, Email+Password |
| OTP Verify | app/(auth)/verify-otp.tsx | 6-digit OTP input; 60s resend timer; auto-submit on fill |
| Sign Up | app/(auth)/register.tsx | Name, email, phone; referral code (optional); tshirt size (optional) |
| Forgot Password | app/(auth)/forgot-password.tsx | Email input → OTP reset flow |
| Guest Mode | app/(auth)/guest.tsx | Limited explore access; booking gates to auth prompt |
3.2 Main App — Bottom Tab Navigator
Tabs: Explore | My Treks | Profile (MVP scope — Tribe and Memories are post-MVP)
Tab 1: Explore
| Screen | Route | Description |
|---|---|---|
| Explore Home | app/(tabs)/explore/index.tsx | Featured trek cards, region quick-filters, upcoming batch highlights, search bar |
| Search / Filter | app/(tabs)/explore/search.tsx | Full-text search; filters (region, difficulty, duration, price, month); sort options |
| Trek Detail | app/(tabs)/explore/trek/[slug].tsx | Hero media carousel, quick facts, itinerary accordion, batch selector, guides, reviews, FAQs, Book Now CTA |
| Batch Selector | app/(tabs)/explore/trek/[slug]/batches.tsx | Calendar month view + list; seat badges; price per batch; Select CTA |
| Guide Profile | app/(tabs)/explore/guide/[id].tsx | Photo, bio, experience stats, past trek reviews |
| FAQ Detail | app/(tabs)/explore/faq.tsx | Global FAQ accordion; search within FAQ |
| Blog List | app/(tabs)/explore/blog/index.tsx | Article cards with category tags |
| Blog Post | app/(tabs)/explore/blog/[slug].tsx | Article content, related treks |
Tab 2: My Treks
| Screen | Route | Description |
|---|---|---|
| My Treks Home | app/(tabs)/my-treks/index.tsx | Upcoming trek hero card with countdown; past treks list; empty state with Explore CTA |
| Booking Detail | app/(tabs)/my-treks/booking/[id].tsx | Trek summary, participants, payment status, documents checklist, packing list, guide contact |
| Itinerary Viewer | app/(tabs)/my-treks/booking/[id]/itinerary.tsx | Day-by-day view with altitude, meals, terrain; elevation profile chart; offline-cached PDF download |
| Packing Checklist | app/(tabs)/my-treks/booking/[id]/checklist.tsx | Categorised checklist with checkboxes; custom item add; progress bar |
| Documents | app/(tabs)/my-treks/booking/[id]/documents.tsx | Upload ID proof, fitness form, waiver; status indicators (pending / approved / rejected) |
| Payment History | app/(tabs)/my-treks/booking/[id]/payments.tsx | Timeline of payments, receipts download, remaining balance |
| Past Trek Archive | app/(tabs)/my-treks/history.tsx | Timeline of completed treks; stats summary (total altitude, km, camps) |
Tab 3: Profile
| Screen | Route | Description |
|---|---|---|
| Profile Home | app/(tabs)/profile/index.tsx | Photo, name, bio, trek stats summary, loyalty tier badge, quick links |
| Edit Profile | app/(tabs)/profile/edit.tsx | Edit name, bio, photo upload, trek preferences |
| Loyalty & Rewards | app/(tabs)/profile/loyalty.tsx | Points balance, tier progress bar, points history ledger, redeem options |
| Referral Hub | app/(tabs)/profile/referrals.tsx | Unique code card with share sheet; tracking: invited / registered / booked; earnings |
| Emergency Contacts | app/(tabs)/profile/emergency-contacts.tsx | List contacts; add/edit/delete; minimum 2 required indicator |
| Support | app/(tabs)/profile/support/index.tsx | Ticket list with status tabs; Create new ticket button |
| Support Ticket | app/(tabs)/profile/support/[id].tsx | Message thread; attach screenshot; mark resolved |
| Settings | app/(tabs)/profile/settings.tsx | Notification preferences, language, privacy settings, offline data, delete account |
| Notification Prefs | app/(tabs)/profile/settings/notifications.tsx | Granular toggles: booking reminders, trek updates, promotional, community |
3.3 Booking Flow (Modal Stack — overlays any tab)
| Screen | Route | Description |
|---|---|---|
| Booking Start | app/book/[batchId]/index.tsx (modal) | Participant count stepper; primary trekker details; price breakdown |
| Participant Details | app/book/[batchId]/participants.tsx | Additional participant names, ages; emergency contact confirmation |
| Review & Coupon | app/book/[batchId]/review.tsx | Order summary, coupon code input, loyalty points toggle, policy acknowledgement |
| Payment | app/book/[batchId]/payment.tsx | Razorpay SDK checkout; loading state; retry on failure |
| Confirmation | app/book/[batchId]/confirm.tsx | Success animation, booking ID, share card, 'Go to My Treks' CTA |
3.4 Offline Strategy
- Itinerary PDFs are auto-cached to device storage 48 hours before trek start date (background fetch)
- Packing checklist state is persisted locally via SQLite (expo-sqlite); synced to server when online
- Trek detail page hero images and essential content cached via react-query persistent cache
- Offline indicator banner shown when network is unavailable; core app features remain usable
- Document upload queued locally when offline; auto-uploads when connectivity restored
4. Cloud & Infrastructure Cost Estimation
Estimates are in Indian Rupees (INR) per month at MVP scale (0–500 bookings/month, ~2,000 active users). Costs scale with usage — this is a conservative early-stage baseline. 1 USD ≈ ₹84.
4.1 Cloud Hosting — Backend API & Database
| Service | Provider | Spec / Plan | Cost/Month (INR) | Notes |
|---|---|---|---|---|
| App Server (API) | Hetzner VPS (CX21) or DigitalOcean | 2 vCPU, 4GB RAM, 40GB SSD | ₹1,680 – ₹2,100 | Single Docker host; ideal for MVP; scale to CX31 at growth |
| PostgreSQL DB | Same VPS (self-managed) or Supabase Free → Pro | Self: included above | Supabase Pro: 8GB storage | ₹0 – ₹3,360 | Self-managed on VPS saves cost; Supabase adds managed backups & PgBouncer |
| Redis Cache + BullMQ | Same VPS (Docker container) | ~256MB RAM allocation | ₹0 (included in VPS) | Upstash Redis ₹0 free tier for low throughput if preferred |
| Backup Storage | Hetzner Backup or S3 | Automated daily DB backups, 7-day retention | ₹420 – ₹840 | Non-negotiable; automate with pg_dump + rclone to S3 |
| Load Balancer / Proxy | Nginx on VPS (self-managed) | Reverse proxy + SSL termination | ₹0 | Use Caddy for auto-HTTPS simplicity |
| Managed Hosting Alt. | Railway.app (Starter Plan) | 1GB RAM, Postgres included, auto-deploy | ₹1,680 – ₹5,040 | Best DX for small team; auto-scaling; slightly higher cost |
| Backend Subtotal | ₹2,100 – ₹8,400/mo |
4.2 Website Hosting (Next.js)
| Service | Provider | Plan | Cost/Month (INR) | Notes |
|---|---|---|---|---|
| Next.js Website | Vercel (Hobby → Pro) | Hobby: Free | Pro: $20/mo | ₹0 – ₹1,680 | Hobby sufficient for early MVP; Pro needed at traffic scale or for teams |
| Admin Panel (Next.js) | Vercel Hobby or same VPS | Hobby: Free | ₹0 | Low traffic; free tier handles it easily |
| CDN (Static Assets) | Cloudflare Free | Unlimited bandwidth, DDoS protection | ₹0 | Point DNS to Cloudflare; instant free CDN globally |
| Domain (.com) | Namecheap / GoDaddy | ohbhaisahab.com annual | ₹168/mo (₹2,000/yr) | Already owned; renewal cost only |
| Website Subtotal | ₹168 – ₹1,848/mo |
4.3 File Storage (Trek Media + User Documents)
| Service | Provider | Plan | Cost/Month (INR) | Notes |
|---|---|---|---|---|
| Object Storage | Cloudflare R2 | 10GB free, $0.015/GB/mo after | ₹0 – ₹840 | Zero egress fees — critical for serving trek images to users |
| Image CDN / Transform | Cloudflare Images or imgix | CF Images: $5/mo for 100k images | ₹420 – ₹840 | Resize + optimise trek imagery on the fly; massive performance win |
| Document Storage | Cloudflare R2 (same bucket) | User ID proofs, fitness forms; separate private bucket | ₹0 – ₹420 | Private bucket; access via presigned URLs only |
| Storage Subtotal | ₹420 – ₹2,100/mo |
4.4 Communication Services
| Service | Provider | Plan / Volume | Cost/Month (INR) | Notes |
|---|---|---|---|---|
| Transactional Email | Resend | Free: 3,000 emails/mo | Pro: $20 for 50k | ₹0 – ₹1,680 | Booking confirmations, receipts, reminders; excellent deliverability |
| SMS & OTP | MSG91 (India) | ~₹0.18 per OTP SMS; 500 OTPs/mo | ₹500 – ₹1,500 | India-optimised; supports DLT registration required for commercial SMS |
| WhatsApp (Booking Updates) | MSG91 / Interakt | ~₹0.58/conversation (business-initiated) | ₹500 – ₹2,000 | Optional at MVP; high open rate; DLT + Meta approval required |
| Push Notifications | Expo Push Notifications (FCM/APNs) | Free (Expo managed) | ₹0 | Expo handles FCM/APNs routing; no separate vendor needed |
| Communications Subtotal | ₹1,000 – ₹5,180/mo |
4.5 Payment Processing
| Service | Provider | Fee Structure | Estimated Monthly Cost | Notes |
|---|---|---|---|---|
| Payment Gateway | Razorpay | 2% per transaction (domestic cards/UPI) | Variable — 2% of GMV | No monthly fee; purely transaction-based. On ₹5L GMV = ₹10,000 |
| Razorpay Account | Razorpay | Free account activation | ₹0 | KYC and bank account linking required; takes 2–3 business days |
| International Cards | Razorpay International | 3% per transaction | Variable | Only needed if targeting NRI trekkers |
4.6 Developer Accounts (One-Time Costs)
| Account | Platform | Cost | Notes |
|---|---|---|---|
| Google Play Developer | Google Play Console | $25 one-time (≈ ₹2,100) | Lifetime account; covers all Android apps |
| Apple Developer Program | App Store Connect | $99/year (≈ ₹8,316/year) | Annual renewal required; needed for iOS TestFlight + App Store |
| Expo Application Services (EAS) | Expo | Free tier: limited builds | Production plan: $99/mo | EAS Build for OTA updates and app store submissions; free tier sufficient at MVP |
4.7 Monitoring & DevOps
| Service | Provider | Plan | Cost/Month (INR) | Notes |
|---|---|---|---|---|
| Error Tracking | Sentry | Free: 5k errors/mo | Team: $26/mo | ₹0 – ₹2,184 | Free tier sufficient at MVP; essential for production debugging |
| Uptime Monitoring | Better Uptime / UptimeRobot | Free tier (5-min checks) | ₹0 | Alert on API or website downtime |
| Logging | Self-hosted (Docker logs + Loki) | On VPS (included) | ₹0 | Or Papertrail free tier for hosted log management |
| Analytics (Web) | Vercel Analytics or Plausible | Vercel: included in Pro | Plausible: $9/mo | ₹0 – ₹756 | Privacy-friendly; no cookie consent required with Plausible |
| CI/CD | GitHub Actions | Free for public repos; 2000 min/mo for private | ₹0 | Auto-deploy to VPS on push to main; Expo EAS build on PR |
| DevOps Subtotal | ₹0 – ₹2,940/mo |
4.8 Total Monthly Cost Summary
| Category | Low Estimate (INR/mo) | High Estimate (INR/mo) | Notes |
|---|---|---|---|
| Backend Hosting (VPS + DB) | ₹2,100 | ₹8,400 | Lower = Hetzner self-managed; Higher = Railway managed |
| Website Hosting (Vercel) | ₹168 | ₹1,848 | Lower = Hobby + domain; Higher = Pro plan |
| File Storage (R2 + CDN) | ₹420 | ₹2,100 | Scales with trek media volume |
| Communications (Email + SMS) | ₹1,000 | ₹5,180 | SMS cost grows with user registrations |
| Payment Gateway | 2% of GMV | 2% of GMV | Variable; not in fixed total |
| Monitoring & DevOps | ₹0 | ₹2,940 | Sentry paid + Plausible analytics |
| TOTAL (Fixed) | ≈ ₹3,700/mo | ≈ ₹20,468/mo | Excluding payment gateway % |
| Recommended Budget | ₹6,000 – ₹10,000/mo | Realistic mid-range for a lean MVP team at launch |
4.9 One-Time Setup Costs
| Item | Cost (INR) | Notes |
|---|---|---|
| Google Play Developer Account | ₹2,100 (one-time) | Permanent |
| Apple Developer Program (Year 1) | ₹8,316/year | Renew annually at ₹8,316 |
| DLT Registration (SMS) | ₹1,000 – ₹2,000 | Required by TRAI for commercial SMS in India; one-time with telecom |
| Domain (if not already owned) | ₹800 – ₹2,000/year | ohbhaisahab.com |
| SSL Certificate | ₹0 | Use Let's Encrypt via Caddy/Certbot — free automated |
| Initial VPS Setup & Hardening | One-time effort (engineer hours) | SSH keys, firewall, Docker, Nginx/Caddy, automated backups |
| TOTAL One-Time | ≈ ₹11,400 – ₹14,400 | Excluding engineer time |
4.10 Cost Scaling Guidance
- At 500 bookings/month (≈ ₹25L GMV): payment gateway fees ≈ ₹50,000; infrastructure stays in low range
- At 2,000 bookings/month: upgrade VPS to 4 vCPU / 8GB RAM (≈ +₹1,680/mo); consider managed Postgres (≈ +₹3,360/mo)
- At 10,000+ users: add Redis Cluster, CDN caching layer, consider Kubernetes or ECS; budget ₹50,000+/mo
- Mobile app OTA updates via Expo EAS Update are free on hobby tier; upgrade to production plan ($99/mo) at scale
- WhatsApp Business API becomes cost-effective over SMS above ₹100/customer LTV — factor in at Phase 2
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |
OBS EXPERIENCES
Addendum: Trek Batch Roles, Live Status & Loyalty
Extends MVP Architecture v1.0 | June 2026
1. Overview & Design Principles
This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.
Three principles guide the design:
- Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
- Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
- Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.
2. Database Schema Changes
2.1 Updated trek_batches table
Existing columns carry forward. New and changed columns are marked.
| Column | Type | Default | Notes |
|---|---|---|---|
| id | uuid PK | gen_random_uuid() | No change |
| trek_id | uuid FK → treks | — | No change |
| start_date | date | — | No change |
| end_date | date | — | No change |
| price_inr | integer | — | No change |
| seats_total | integer | — | No change |
| seats_booked | integer | 0 | No change |
| seats_held | integer | 0 | No change |
| status | trek_batch_status (enum) | 'scheduled' | NEW — see lifecycle §3 |
| status_updated_at | timestamptz | now() | NEW — when status last changed |
| status_updated_by | uuid FK → users | null | NEW — leader who changed it |
| started_at | timestamptz | null | NEW — set when status → in_progress |
| finished_at | timestamptz | null | NEW — set when status → completed |
| actual_end_date | date | null | NEW — may differ from planned end_date |
| incident_notes | text | null | NEW — free text, leader-only write |
| cancellation_reason | text | null | No change |
| meeting_point | text | null | NEW — e.g. 'Sankri base camp parking' |
| meeting_time | timestamptz | null | NEW — assembly time day 1 |
2.2 New table: batch_staff
Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE |
| user_id | uuid FK → users | Staff must have a user account |
| role | batch_staff_role (enum) | trek_leader | guide | coordinator — see §2.3 |
| assigned_by | uuid FK → users | Admin who made the assignment |
| assigned_at | timestamptz | Timestamp of assignment |
| is_active | boolean | true = currently on this batch; false = removed/replaced |
| notes | text | Internal notes (e.g. 'backup leader', 'local area expert') |
2.3 Enum: batch_staff_role
| Role value | Who holds it | Key permissions |
|---|---|---|
| trek_leader | 1 per batch (required) | Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin |
| guide | 1–N per batch (min 1 required) | Read-only access to participant list and docs · View day itinerary · Cannot change status |
| coordinator | 0–N per batch (optional) | Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member |
2.4 New table: batch_status_history
Append-only audit trail of every status transition. Never update or delete rows.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | CASCADE DELETE (audit trail is batch-scoped) |
| from_status | trek_batch_status | null if first transition (scheduled → ...) |
| to_status | trek_batch_status | The new status |
| changed_by | uuid FK → users | Must be the trek_leader for operational transitions |
| changed_at | timestamptz | Server time, not client time |
| note | text | Optional leader note at time of transition |
| location_lat | decimal(9,6) | Optional GPS at time of status change |
| location_lng | decimal(9,6) | Optional GPS at time of status change |
2.5 New table: loyalty_pending_credits
Staging table used by the post-completion loyalty job to ensure idempotency.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | — |
| batch_id | uuid FK → trek_batches | — |
| user_id | uuid FK → users | Trekker who completed the batch |
| points_to_credit | integer | Computed at job time from trek difficulty + loyalty rules |
| status | credit_status (enum) | pending | processing | credited | failed |
| job_run_id | uuid | BullMQ job ID — idempotency key |
| credited_at | timestamptz | Set when loyalty_points row is inserted |
| error_detail | text | Populated on failure for retry/manual review |
3. Trek Batch Status Lifecycle
3.1 Status enum values
| Status | Meaning | Who can set it | Trigger |
|---|---|---|---|
| scheduled | Batch is open for bookings; no trek activity yet | System (default on create) | Admin creates batch |
| assembling | Bookings closed; participants are gathering at meeting point | Trek Leader or Admin | Leader taps 'Start assembly' — typically morning of Day 1 |
| in_progress | Trek is actively underway | Trek Leader only | Leader taps 'Begin trek' after headcount at assembly |
| paused | Trek temporarily halted (weather window, medical situation, route issue) | Trek Leader only | Leader taps 'Pause trek'; must add a note explaining reason |
| completed | Trek finished; all participants safely returned | Trek Leader only | Leader taps 'Complete trek'; triggers loyalty job |
| cancelled | Batch will not run | Admin only (not leader) | Admin action; triggers refund workflow |
3.2 Allowed transitions
| From → | → To | Allowed? | Notes |
|---|---|---|---|
| scheduled | assembling | Yes | Leader or admin; closes new bookings |
| scheduled | cancelled | Yes | Admin only |
| assembling | in_progress | Yes | Leader only; sets started_at |
| assembling | cancelled | Yes | Admin only (e.g. sudden weather) |
| in_progress | paused | Yes | Leader only; note required |
| in_progress | completed | Yes | Leader only; sets finished_at, triggers loyalty job |
| in_progress | cancelled | No | Cannot cancel a running trek — use completed with incident note |
| paused | in_progress | Yes | Leader only; resumes trek |
| paused | completed | Yes | Leader only; if trek ends while paused (e.g. emergency extraction) |
| completed | any | No | Terminal state — no further transitions |
| cancelled | any | No | Terminal state — no further transitions |
3.3 Status transition API
PATCH /api/v1/batches/:id/status
Request body: { to_status, note?, location? }
Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.
Server-side logic on this endpoint:
- Validate transition is in the allowed matrix above; return 422 if not
- Check caller is active trek_leader on this batch (or admin)
- Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
- INSERT into batch_status_history
- If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
- If → in_progress or assembling: send push notification to all confirmed participants
- Return updated batch object
4. API Routes — New & Updated
4.1 Batch staff management (Admin)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/batches/:id/staff | List all staff assigned to a batch with roles |
| POST | /api/v1/admin/batches/:id/staff | Assign a user to a batch with a role. Body: { user_id, role, notes? } |
| PATCH | /api/v1/admin/batches/:id/staff/:staffId | Update role or notes for a staff member |
| DELETE | /api/v1/admin/batches/:id/staff/:staffId | Remove (set is_active=false) a staff member from a batch |
| GET | /api/v1/admin/users?role=trek_leader | List users eligible to be assigned as leaders |
4.2 Batch status management (Trek Leader / Admin)
| Method | Path | Description |
|---|---|---|
| PATCH | /api/v1/batches/:id/status | Transition batch status. Body: { to_status, note?, location_lat?, location_lng? } |
| GET | /api/v1/batches/:id/status-history | Full audit log of all status transitions for a batch |
| PATCH | /api/v1/batches/:id/incident | Trek leader posts/updates incident note on a running batch |
| GET | /api/v1/batches/live | Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed) |
| GET | /api/v1/batches/:id/live-status | Real-time status card for a single batch (polling or SSE) |
4.3 Leader-facing mobile routes
These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/leader/batches | Leader's assigned batches: upcoming, live, past |
| GET | /api/v1/leader/batches/:id | Full batch detail including participant list, docs status, staff roster |
| GET | /api/v1/leader/batches/:id/participants | All confirmed participants with doc/health status and emergency contacts |
| POST | /api/v1/leader/batches/:id/notify | Send push notification to all batch participants. Body: { title, message } |
4.4 Guide-facing mobile routes
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/guide/batches | Guide's assigned batches |
| GET | /api/v1/guide/batches/:id | Batch detail — itinerary, participant count, emergency contacts (no payment data) |
| GET | /api/v1/guide/batches/:id/participants | Participant names and emergency contacts only |
5. Frontend — Ongoing Treks Feature
5.1 Website: Ongoing Treks section
A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.
| Element | Data source | Behaviour |
|---|---|---|
| 'Live now' badge | batch.status = in_progress | Pulsing green dot; visible on trek card in explore listing |
| 'Assembling' badge | batch.status = assembling | Amber badge — 'Starting soon' |
| Ongoing Treks section (home) | GET /api/v1/batches/live | Horizontal scroll card strip; shows trek name, region, day N of M, leader name |
| Live status card | GET /api/v1/batches/:id/live-status (poll 60s) | Current status, day number, last status update time, incident note if any |
| /treks/ongoing page | Same endpoint | Full grid of all ongoing batches; filterable by region |
5.2 Mobile app: Trek Leader screens
A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.
| Screen | Route | Description |
|---|---|---|
| Leader home | app/(tabs)/leader/index.tsx | Cards for each assigned batch: upcoming / live / past |
| Batch control panel | app/(tabs)/leader/batch/[id].tsx | Status pill, big action button (context-aware), participant count, staff list |
| Status transition | app/(tabs)/leader/batch/[id]/status.tsx | Confirmation sheet with allowed next states, mandatory note input for pause/complete |
| Participant list | app/(tabs)/leader/batch/[id]/participants.tsx | Trekker names, doc status indicators, emergency contact tap-to-call |
| Notify participants | app/(tabs)/leader/batch/[id]/notify.tsx | Title + message compose; sends push to all confirmed participants |
| Status history | app/(tabs)/leader/batch/[id]/history.tsx | Timeline of all status changes with timestamps and notes |
| Incident log | app/(tabs)/leader/batch/[id]/incident.tsx | Free-text incident note; visible to admin, not to participants |
5.3 Mobile app: Guide screens
| Screen | Route | Description |
|---|---|---|
| Guide home | app/(tabs)/guide/index.tsx | Upcoming and active batch assignments |
| Batch view | app/(tabs)/guide/batch/[id].tsx | Read-only: itinerary, participant count, day-wise plan |
| Participant contacts | app/(tabs)/guide/batch/[id]/contacts.tsx | Names + emergency contacts (tap-to-call); no booking/payment data |
5.4 Participant-facing: ongoing trek card
In the My Treks tab, a booking with an active batch shows an enhanced card:
- Live status banner: 'Your trek is in progress — Day 3 of 7'
- Last update timestamp from status_updated_at
- Guide contact buttons (tap-to-call/WhatsApp for each guide)
- Trek leader name and emergency contact
- Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')
6. Loyalty Credit on Trek Completion
6.1 Credit rules
| Trek difficulty | Base points awarded | Notes |
|---|---|---|
| Easy | 100 pts | e.g. Kedarkantha meadows, beginner treks |
| Moderate | 200 pts | Most standard Himalayan treks |
| Difficult | 350 pts | Technical routes, high-altitude passes |
| Expedition | 500 pts | Summit attempts, multi-week treks |
Multipliers applied on top of base points:
| Multiplier condition | Multiplier | Example |
|---|---|---|
| First completed trek (new user) | 1.5× | Easy first trek: 100 × 1.5 = 150 pts |
| Repeat booking (2nd+ trek with OBS) | 1.1× | Moderate repeat: 200 × 1.1 = 220 pts |
| Trek completed in current season peak (configurable) | 1.2× | Peak season bonus |
| Full group (batch ≥ 90% full) | 1.1× | Rewarding popular batches |
| Multipliers stack multiplicatively | — | Repeat + peak: 200 × 1.1 × 1.2 = 264 pts |
6.2 BullMQ job: credit_loyalty_on_completion
Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.
| Step | Action | Error handling |
|---|---|---|
| 1 — Fetch participants | Query all bookings WHERE batch_id = X AND status = 'confirmed' | If no confirmed bookings: log and exit cleanly |
| 2 — Check idempotency | For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) | If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin. |
| 3 — Insert pending rows | INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING | ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee |
| 4 — Compute points | For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers | Round to nearest integer |
| 5 — Credit points | INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() | Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry. |
| 6 — Update tier | Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed | Run per-user after credit. Non-blocking — tier is cosmetic. |
| 7 — Notify | Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' | Notification failure does not roll back credits |
| 8 — Award certificate | Enqueue separate generate_certificate job per user | Separate job — idempotent; certificate URL stored on booking |
6.3 Points→tier recomputation
Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).
7. Admin Panel Additions
7.1 Batch staff assignment UI
- On any batch detail page in admin, a 'Staff' tab shows current assignments
- 'Assign staff' button opens a user search modal; select user → pick role → save
- Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
- 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note
7.2 Live batch monitor
New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):
- Batch name, trek, day N of M, leader name, participant count, last status update
- Quick-expand to see incident notes and full status history
- 'Contact leader' button (WhatsApp deep link or in-app message)
- Admin can force-complete or cancel any running batch from here (with mandatory reason)
7.3 Loyalty audit dashboard
- Table of all loyalty_pending_credits rows filterable by status
- 'Failed' rows shown prominently with error_detail and a manual retry button
- Batch completion summary: which batches have been credited, how many users, total points awarded
8. Migration Plan (Existing Data)
| Step | Action | Notes |
|---|---|---|
| 1 | Run migration: add new columns to trek_batches (status, status_updated_at etc) | All existing batches get status = 'scheduled' by default |
| 2 | Create new tables: batch_staff, batch_status_history, loyalty_pending_credits | No data migration needed — tables start empty |
| 3 | Create enum types: trek_batch_status, batch_staff_role, credit_status | Do this before altering tables that reference them |
| 4 | Migrate existing guide_id FK data | For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id) |
| 5 | Drop guide_id column from trek_batches | After verifying migration in step 4 is complete |
| 6 | Backfill status_history | Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness |