Emenem Tours — Backend KT Document

Author: Saswath Singh
Date: June 2026
Scope: Backend modules created/refactored in M-M_Trip_Backend
Stack: NestJS 10 · TypeORM · PostgreSQL · Redis · AWS S3 · Razorpay


Table of Contents

  1. Project Architecture Overview
  2. Cross-Cutting Concerns
  3. Module: Booking Auth
  4. Module: Booking Users
  5. Module: Bookings
  6. Module: Hotel
  7. Module: Itinerary
  8. Module: Markup
  9. Module: Razorpay
  10. Module: Rate Limiting
  11. Module: Transfer
  12. Module: Collection
  13. Circular Dependency Map
  14. Frontend Cross-Reference

1. Project Architecture Overview

src/
├── main.ts                  # Bootstrap — global prefix v1, CORS, Swagger, guards, filters
├── app.module.ts            # Root module — registers all feature modules
├── modules/                 # All feature modules (41 total)
│   ├── booking-auth/
│   ├── booking-users/
│   ├── bookings/
│   ├── hotel/
│   ├── itinerary/
│   ├── markup/
│   ├── razorpay/
│   ├── rate-limit-test/
│   ├── transfer/
│   └── collection/
└── shared/
    ├── entity/              # 110+ TypeORM entities
    ├── enums/
    ├── guards/              # PermissionGuard, CustomRateLimitGuard, etc.
    ├── decorators/          # @Permissions(), @GetUser(), @GetBookingUser(), rate limit decorators
    ├── tenant/              # TenantAwareRepository, tenant-context.provider
    └── utility/             # S3Service, SendMailerUtility, throwException, etc.

Key bootstrap settings (src/main.ts)

Setting Value
Global prefix v1
CORS allowed origins travelterminus.com and *.travelterminus.com
Swagger URL /api (protected by SwaggerAuthMiddleware)
Global pipe ValidationPipe with whitelist: true, forbidNonWhitelisted: true
Global interceptor ReqResInterceptor
Global exception filter AllHttpExceptionFilter
Trust proxy Level 1 (for correct IP extraction behind nginx)
Global guard CustomRateLimitGuard (bound as APP_GUARD)

2. Cross-Cutting Concerns

These patterns are used across all modules I built. Understanding them is essential.

2.1 Dual JWT Authentication

The system has two completely separate auth flows:

Auth Flow Strategy Name Guard Usage Token Source Used By
Admin / Agent jwt @UseGuards(AuthGuard('jwt')) Authorization: Bearer header Admin routes
Customer booking-jwt @UseGuards(AuthGuard('booking-jwt')) booking_auth signed cookie (falls back to Authorization: Bearer) Customer-facing booking routes

Never mix these up. Admin routes use jwt, customer routes use booking-jwt.

Extracting the current user in a controller:

// Admin user
@GetUser() user: User

// Customer
@GetBookingUser() bookingUser: BookingUser

2.2 Multi-Tenancy

Every request is scoped to a tenant. The flow is:

HTTP Request
  → TenantMiddleware (resolves tenantId from subdomain)
  → getTenantId() stored in AsyncLocalStorage
  → TenantAwareRepository automatically injects tenantId into all queries

TenantAwareRepository<T> (src/shared/tenant/tenant-aware.repository.ts) is the base class for all repositories I created. It:

Never call super.find() or raw TypeORM find() directly in a TenantAwareRepository subclass — you will get cross-tenant data leaks.

2.3 Permission System

Admin routes are protected by two guards stacked together:

@UseGuards(AuthGuard('jwt'), PermissionGuard)
@Permissions(Permission.BOOKING)

The Permission enum is defined in src/shared/enums/permission.enum.ts. Relevant values:

Permission Used In
Permission.TRIP Itinerary, Hotel (trip management), Transfer (mapping)
Permission.HOTEL_INVENTORY Hotel (inventory management)
Permission.TRANSFER_INVENTORY Transfer (inventory management)
Permission.BOOKING Bookings, BookingUsers, Razorpay (admin)
Permission.MARKUP Markup
Permission.COLLECTION Collection

2.4 Standard Module Structure

Every module follows this exact pattern:

module-name/
├── module-name.module.ts       # NestJS module definition
├── module-name.controller.ts   # Route handlers (thin layer)
├── module-name.service.ts      # Business logic
├── module-name.repository.ts   # DB access (extends TenantAwareRepository<Entity>)
└── dto/                        # Request/response shape validation

2.5 Error Handling

All service methods use throwException(error) (from src/shared/utility/throw-exception.ts) inside a catch block. This utility re-throws NestJS HttpException instances as-is and wraps generic errors in InternalServerErrorException. Do not throw raw errors.

2.6 Response Shape

All endpoints return AppResponse:

interface AppResponse {
  message: string;   // e.g. "SUC_LOGIN", "SUC_BOOKING_CREATED"
  data: any;
}

3. Module: Booking Auth

Path: src/modules/booking-auth/
Purpose: Customer-facing authentication — signup, login, password reset (OTP-based), and Google OAuth.

3.1 Entities Used

Entity File Purpose
BookingUser booking-user.entity.ts Core customer user record
BookingOtp booking-otp.entity.ts OTP storage with expiry and isUsed flag
BookingResetPasswordToken booking-reset-password-token.entity.ts Password reset tokens (one-time use)
BookingToken booking-user-token.entity.ts Stores issued access + refresh tokens

3.2 API Endpoints

All routes are under POST /v1/booking-auth/

Route Auth Rate Limit Description
user/signup Public @SignupRateLimit Register new customer
user-login Public @StrictRateLimit Email/password login
forgot-password Public @OtpRateLimit Send OTP to email
otp-verify Public @OtpRateLimit Verify OTP → returns reset token
resend-otp Public @OtpRateLimit Regenerate and resend OTP
reset-password Public @PasswordResetRateLimit Set new password using reset token
change-password booking-jwt @AuthenticatedRateLimit Change password (authenticated)
social-login Public @StrictRateLimit Google OAuth access-token login

3.3 Authentication Flow

Password Login:

POST /booking-auth/user-login
  → validatePassword() (bcrypt compare)
  → generateJWTToken()
      → creates accessToken (short-lived) + refreshToken (long-lived)
      → sets signed HttpOnly cookie: booking_auth = { accessToken, refreshToken }
  → storeLoginToken() — saves both tokens to BookingToken table
  → returns { accessToken, user }

JWT Token Structure (accessToken payload):

{
  "sid": "<uuid>",
  "email": "user@email.com",
  "booking": {
    "id": "<userId>",
    "username": "First Last",
    "firstName": "First",
    "email": "user@email.com",
    "date": "<timestamp>",
    "roleId": 1,
    "isActive": true
  }
}

booking-jwt Strategy (strategy/booking-jwt.strategy.ts):
Extracts token from req.signedCookies.booking_auth.accessToken first, falls back to Authorization: Bearer header.

Password Reset Flow:

POST /forgot-password    → invalidates old OTPs → creates new BookingOtp → sends email
POST /otp-verify         → validates OTP (expiry + isUsed check) → marks OTP used
                         → invalidates old reset tokens → creates BookingResetPasswordToken
                         → returns { userId, token }
POST /reset-password     → validates reset token → hashes new password with new salt
                         → saves user → deactivates reset token

Google OAuth:

3.4 Password Hashing

const salt = await bcrypt.genSalt();
user.password = await bcrypt.hash(password, salt);
user.salt = salt;

Both password and salt are stored. The validatePassword() method on BookingUser entity does the compare.

3.5 Notable Gotchas

3.6 Exports

BookingAuthModule exports: BookingAuthService, BookingAuthRepository, BookingJwtStrategy, PassportModule. These are consumed by BookingUserModule and BookingsModule.


4. Module: Booking Users

Path: src/modules/booking-users/
Purpose: Customer profile management (self-service) and admin customer management.

4.1 API Endpoints

Routes under /v1/booking-users/

Route Auth Permission Description
PUT :id/profile booking-jwt Customer updates own profile + profile pic
GET :id/profile booking-jwt Customer fetches own profile
GET / jwt Permission.BOOKING Admin lists all customers (paginated)
GET :id jwt Permission.BOOKING Admin gets customer detail
PUT :id jwt Permission.BOOKING Admin updates customer details

4.2 Profile Picture Upload

Profile pictures are uploaded to S3 via S3Service:

// On profile update with new image:
await s3Service.uploadFile(file, S3BucketTypeEnum.PROFILE_PICTURE);

// If old profile pic exists, delete from S3:
await s3Service.deleteFile(oldProfilePicKey);

The stored value is the S3 key/path, not a full URL. The full URL is constructed using configService.get("app.doc_url") + profilePic at response time.

4.3 Dual Auth Split

4.4 cleanNullValues()

All profile responses go through cleanNullValues() (from src/shared/utility/common-functions.methods.ts) which strips null/undefined fields from the response object. This keeps response payloads lean.


5. Module: Bookings

Path: src/modules/bookings/
Purpose: Full booking lifecycle — create, status management, cancellation, PDF generation (itinerary, invoice, confirmation voucher), and email delivery.

5.1 Entities Used

Entity Purpose
Booking Core booking record
BookingPayment Payment records linked to booking
BookingTraveler Traveler details per booking
ConfirmationVoucher Hotel/service voucher data
TransactionLog Audit trail (from Razorpay module)

5.2 Booking Status Flow

DRAFT → PROCESSING → CONFIRMED → COMPLETED
          ↓              ↓
       CANCELLED      CANCELLED

5.3 API Endpoints

Routes under /v1/bookings/

Route Auth Description
POST / booking-jwt Customer creates booking (DRAFT status)
GET / booking-jwt Customer lists own bookings
GET /admin jwt + Permission.BOOKING Admin lists all bookings with filters
PUT :id/status jwt + Permission.BOOKING Admin updates booking status
GET :bookingId/itinerary-pdf booking-jwt Customer downloads itinerary PDF
GET :bookingId/itinerary-pdf/admin jwt Admin downloads itinerary PDF
POST :id/cancel-request booking-jwt Customer submits cancellation request
POST :bookingId/cancel jwt + Permission.BOOKING Admin cancels booking + optional Razorpay refund
PUT :bookingId/remarks jwt + Permission.BOOKING Admin saves internal remarks
GET :bookingId/confirmation-voucher jwt Admin fetches voucher data
PUT :bookingId/confirmation-voucher jwt Admin saves voucher data
POST :bookingId/confirmation-voucher/media jwt Upload media file to voucher
DELETE :bookingId/confirmation-voucher/:mediaId/media jwt Delete voucher media
POST :bookingId/confirmation-voucher/generate-pdf jwt Generate confirmation PDF
POST :bookingId/confirmation-voucher/send-email jwt Email confirmation voucher to customer
GET :bookingId/invoice booking-jwt Customer downloads invoice PDF
GET :bookingId/invoice/admin jwt Admin downloads invoice PDF

5.4 Booking Creation

POST /bookings creates a booking in DRAFT status. Key points:

5.5 PDF Generation

PDFs are generated using html-pdf-node with Handlebars templates:

PDF Type Template Location Triggered By
Itinerary PDF src/shared/templates/ Customer (/itinerary-pdf) or Admin
Invoice src/shared/templates/ Customer (/invoice) or Admin
Confirmation Voucher src/shared/templates/ Admin (/confirmation-voucher/generate-pdf)

The generated PDF is streamed as a response with Content-Type: application/pdf.

5.6 Circular Dependencies

BookingsModule has two forwardRef() circular dependencies:

Never remove the forwardRef() wrappers without resolving the circular dependency first.

5.7 Confirmation Voucher Media Upload

// MIME types allowed
const VOUCHER_MEDIA_ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
// Max file size
const VOUCHER_MEDIA_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB

Files are uploaded to S3 under S3BucketTypeEnum.VOUCHER_MEDIA. The mediaId returned is used for deletion.


6. Module: Hotel

Path: src/modules/hotel/
Purpose: Hotel inventory management, itinerary/day mapping, media uploads, and amenities.

6.1 Two Modes of Operation

The Hotel module operates in two contexts:

  1. Inventory management (Permission.HOTEL_INVENTORY) — Create/read/update/delete hotels in the global inventory. These are the master hotel records.
  2. Trip management (Permission.TRIP) — Map inventory hotels to itineraries, map hotels to specific days, set primary hotel per city/itinerary.

6.2 Key Entities

Entity Purpose
Hotels Master hotel record
HotelMedia S3 keys for hotel images
HotelItineraryMapping Links a hotel to an itinerary
DayResourceMapping Links a hotel (or other resource) to a specific day
HotelAmenityMapping Hotel-to-amenity associations
Amenities Master amenities list

6.3 API Endpoints

Routes under /v1/hotel/

Route Auth Permission Description
GET / jwt HOTEL_INVENTORY List hotels paginated
POST /check-inventory jwt TRIP Check hotels by category/location
GET /search-inventory jwt TRIP Search hotels by name
POST /inventory jwt HOTEL_INVENTORY Create hotel in inventory (with media)
GET /amenities jwt TRIP List amenities
GET /facilities/amenities jwt TRIP Paginated amenities list
GET :hotel_id jwt TRIP Get hotel by ID
GET day/:day_id jwt TRIP Get hotel mapped to a day
GET itinerary/:itinerary_id Public Hotels for an itinerary (public)
GET /city/hotel-details jwt TRIP Hotels by city for an itinerary
POST / jwt TRIP Create hotel with media
PUT :id jwt TRIP Update hotel
DELETE :id jwt TRIP Delete hotel
POST /map-itinerary jwt TRIP Map/unmap hotel to itinerary
POST /map-day jwt TRIP Map hotel to a specific day
PATCH /primary jwt TRIP Set primary hotel for city/itinerary

6.4 Media Upload

Uses FileFieldsInterceptor for multi-field file uploads. Files are renamed via reviseFileName() utility before S3 upload. The stored value is the S3 key; full URL is constructed at response time.

6.5 Primary Hotel Logic

Each itinerary + city combination can have one primary hotel — the one shown first/prominently. PATCH /primary sets the isPrimary flag on the HotelItineraryMapping and unsets it on all other mappings for the same itinerary+city.

6.6 Exports

HotelModule exports HotelRepository and HotelService — both consumed by ItineraryModule.


7. Module: Itinerary

Path: src/modules/itinerary/
Purpose: The core trip-planning module. Manages itinerary CRUD, pricing engine, PDF/voucher generation, AI-based trip creation, lead assignment, and duplication/import.

This is the largest and most complex module in the codebase.

7.1 Key Entities

Entity Purpose
Itinerary Core trip record
ItineraryLocation Locations associated with an itinerary
ItineraryMedia Banner images (S3 keys)
ItineraryDiscount Discount mappings
DayResourceMapping Day-level hotel/transfer/activity assignments

7.2 Pricing Engine

The pricing engine is the most critical piece. It supports 4 pricing scenarios:

Scenario Key Includes
Hotel + Transfer hotel_transfer Hotel cost + Transfer cost + Markup
Hotel Only hotel_only Hotel cost only + Markup
Transfer Only transfer_only Transfer cost only + Markup
Activities Only activities_only Activity costs + Markup

Pricing table (PUT :id/pricing-table) stores base pricing per scenario. Actual customer price is computed dynamically by getPriceDetails() which:

  1. Fetches the pricing table
  2. Applies the active Markup (global or itinerary-specific) — see Markup module
  3. Returns per-traveler breakdown

Traveler-based pricing — The updateTravelerPrice() method in ItineraryRepository is called whenever a markup changes, to keep cached prices in sync.

7.3 API Endpoints (Key Routes)

Routes under /v1/itinerary/

Route Auth Description
POST / jwt Create itinerary with banner image
PUT :id jwt Edit itinerary
PUT /info/:itn_id jwt Set itinerary info + media (multipart)
PATCH /published-status/:itn_id jwt Toggle publish/unpublish
DELETE /:itn_id jwt Delete itinerary
GET / Public Customer-facing itinerary listing
GET /agent jwt Agent itinerary listing
GET /by-region Public Itineraries by state/country/city
GET :id Public Public itinerary detail
POST /duplicate/:itn_Id jwt Duplicate an itinerary
POST /import-itinerary jwt Import itinerary from external source
POST /import-itinerary-full jwt Full import in one call
POST /ai/itn-conversion jwt AI-powered itinerary creation
GET :id/price-details jwt Admin price with traveler options
GET :id/customer-price-details booking-jwt Customer price details
PUT :id/pricing-table jwt Upsert pricing table (all 4 scenarios)
PATCH :itineraryId/discount jwt Set itinerary discount
GET :id/hotel-mappings jwt Hotel mappings for itinerary
GET /day/:day_id Public Full day details (hotels/flights/activities)
POST /send jwt Send itinerary via WhatsApp + email
POST /generate-pdf Public Public PDF generation
POST /lead/:itn_Id Optional JWT Assign itinerary to a lead
GET /lead/:id Optional JWT Itinerary details for lead
PUT /lead/unlink/:id jwt Unlink trip from lead
GET /lead/status/:leadId Optional JWT Lead status by UUID
PUT :id/travelers jwt Save pax/traveler details

7.4 Circular Dependencies

ItineraryModule has the most circular dependencies:

Additionally, ItineraryModule imports (non-circular): DaysModule, FlightModule.

7.5 Optional JWT Guard

Lead-assignment endpoints use @UseGuards(OptionalJwtAuthGuard) — this allows the request to proceed whether or not a valid JWT is present. The guard sets req.user = null for unauthenticated requests instead of throwing 401.

7.6 Publish Validation

Before an itinerary can be published (PATCH /published-status/:itn_id), it goes through PublishValidationUtil (utils/publish-validation.util.ts) which checks required fields, at least one day, pricing table populated, etc. Do not remove these checks — they enforce data integrity before the trip goes live to customers.

7.7 Exports

ItineraryModule exports ItineraryRepository and ItineraryService — consumed by MarkupModule and BookingsModule.


8. Module: Markup

Path: src/modules/markup/
Purpose: Pricing markup management — apply flat or percentage markups globally across all trips or to specific itineraries.

8.1 Markup Types

Field Values Behaviour
isGlobal true / false Global = applies to all trips; Specific = mapped to named itineraries
type 1 (flat) / 2 (percentage) Flat = fixed amount added; Percentage = % of base price
category MARKUP / GST Differentiates markup from GST entries
isActive true / false Only active markups are applied to pricing

8.2 Entities

Entity Purpose
Markup Markup record (amount, type, global flag)
MarkupItineraryMapping Links a non-global markup to specific itineraries

8.3 API Endpoints

All routes under /v1/markup/ require jwt + Permission.MARKUP.

Route Description
POST / Create markup (global or per-itinerary)
PUT :id Update markup — add/remove itinerary mappings
DELETE :id Soft delete markup
GET / List all markups (paginated, filterable)
GET :id Get markup with itinerary details
GET /unmapped-itineraries Paginated itineraries not yet linked to any markup

8.4 Business Rules

  1. Only one global markup per tenantcreateMarkup throws ConflictException if a global markup already exists for this tenant.
  2. Global markup cannot be deleteddeleteMarkup checks isGlobal and throws if true.
  3. Non-global markups must retain at least one itineraryupdateMarkup validates that removing itineraries won't leave the markup unmapped.
  4. Price refresh on change — After any create/update, refreshItineraryPricesForMarkup() is called:
    // Gets all itinerary IDs affected by this markup
    const itineraryIds = await markupRepository.getItineraryIdsAffectedByMarkup(markupId);
    // Recalculates traveler price for each
    for (const id of itineraryIds) {
      await itineraryRepository.updateTravelerPrice(id);
    }
    This keeps the travelerPrice cached on the Itinerary entity in sync after any markup change.

8.5 Response Normalization

normalizeMarkup() transforms the raw DB response into a cleaner appliesTo shape:

{
  "appliesTo": {
    "type": "ALL_TRIPS",       // or "SPECIFIC_TRIPS"
    "tripIds": [],
    "trips": []
  }
}

9. Module: Razorpay

Path: src/modules/razorpay/
Purpose: Full payment lifecycle — order creation, payment verification, webhook handling, refunds, and transaction audit logging.

9.1 Multi-Tenant Credentials

Razorpay credentials are stored per tenant in the PaymentGatewayConfig entity (not in .env). The getRazorpayInstance() method fetches credentials for the current tenant and instantiates a new Razorpay SDK object per request.

Mode selection — The RAZORPAY_MODE env var (or fallback PAYMENT_MODE) controls test vs. live:

RAZORPAY_MODE=test   → uses isTestMode=true config
RAZORPAY_MODE=live   → uses isTestMode=false config

If neither is set, the most recently updated active config for the tenant is used.

9.2 API Endpoints

Routes under /v1/payments/

Route Auth Description
POST /create-order booking-jwt Create Razorpay order for a booking
POST /verify booking-jwt Verify payment signature + confirm booking
GET :bookingId/status booking-jwt Get payment status for a booking
POST /refund jwt Admin initiates refund
GET :bookingId/refunds jwt Get refund details
POST /webhook None (public) Razorpay webhook receiver
GET /transaction-logs jwt Paginated transaction audit log
GET /payment-history jwt Payment attempts history

The /webhook route is excluded from TenantMiddleware (listed in TENANT_EXCLUDED_PATHS in app.module.ts).

9.3 Payment Flow

1. Customer creates booking → DRAFT status
2. POST /payments/create-order
   → fetchTenant Razorpay config
   → create Razorpay order (amount in paise via Decimal.js)
   → save BookingPayment record (status: PENDING)
   → return { orderId, amount, currency, keyId }

3. Frontend opens Razorpay checkout using keyId + orderId

4. POST /payments/verify
   → verify HMAC-SHA256 signature: HMAC(orderId|paymentId, keySecret)
   → if valid: capture payment via Razorpay API
   → update BookingPayment status → CAPTURED
   → log TransactionLog entry
   → call BookingsService.updateBookingStatus() → PROCESSING

5. Razorpay webhook (async confirmation)
   → payment.captured: idempotent sync (already PROCESSING)
   → payment.failed: update to FAILED, log
   → refund.processed / refund.failed: update refund status

9.4 Signature Verification

const expectedSignature = crypto
  .createHmac("sha256", keySecret)
  .update(`${orderId}|${paymentId}`)
  .digest("hex");

if (expectedSignature !== receivedSignature) {
  throw new BadRequestException("ERR_INVALID_PAYMENT_SIGNATURE");
}

This must match exactly — any tamper with orderId or paymentId will fail.

9.5 Paise Conversion

// Always use Decimal.js for financial amounts
const amountInPaise = new Decimal(amount).mul(100).toNumber();

Never use raw floating-point arithmetic for prices0.1 + 0.2 !== 0.3 in JavaScript.

9.6 Refunds

POST /payments/refund (admin only):

9.7 Transaction Log

Every payment operation (success and failure) is written to TransactionLog. Fields include:

9.8 Circular Dependency

RazorpayModuleBookingsModule — both import each other via forwardRef(). RazorpayService needs BookingsService.updateBookingStatus() for webhook processing; BookingsService needs RazorpayService.processRefund() for admin cancellation.


10. Module: Rate Limiting

Path: src/modules/rate-limit-test/
Shared files: src/shared/decorators/rate-limit.decorator.ts, src/shared/guards/custom-rate-limit.guard.ts

10.1 Current Status

Rate limiting is currently DISABLED. The CustomRateLimitGuard has isRateLimitDisabled = true at line 12 of custom-rate-limit.guard.ts. To re-enable, change this flag to false.

10.2 Architecture

CustomRateLimitGuard is bound globally as APP_GUARD:

// app.module.ts
{ provide: APP_GUARD, useClass: CustomRateLimitGuard }

This means it runs on every single request by default. Endpoints opt into specific tiers via decorators, or use @SkipThrottle() to bypass entirely.

10.3 Rate Limit Tiers

All 12 tiers are defined in src/shared/decorators/rate-limit.decorator.ts:

Decorator Limit Window Typical Use
@PublicRateLimit() 30 req 1 min Public listing endpoints
@AuthenticatedRateLimit() 100 req 1 min Standard authenticated endpoints
@StrictRateLimit() 8 req 15 min Login, social login
@PasswordResetRateLimit() 5 req 15 min Password reset
@OtpRateLimit() 5 req 15 min OTP send/verify/resend
@SignupRateLimit() 3 req 30 min User registration
@ContactRateLimit() 5 req 1 hr Contact form
@UploadRateLimit() 20 req 1 min File uploads
@BulkRateLimit() 5 req 1 hr Bulk operations
@SearchRateLimit() 100 req 1 min Search endpoints
@AdminRateLimit() 200 req 1 min Admin dashboard
@WebhookRateLimit() 100 req 1 min Webhook receivers
@SsoRateLimit() 10 req 1 hr SSO/OAuth flows
(default, no decorator) 40 req 1 min Undecorated endpoints

10.4 Client Identification

IP extraction priority order:

  1. CF-Connecting-IP (Cloudflare)
  2. X-Real-IP
  3. X-Forwarded-For (first IP in chain)
  4. req.ip (direct connection)
  5. Browser fingerprint hash (fallback for no public IP)

10.5 Storage

Currently uses an in-memory Map<string, { count, resetTime }>. This is per-process — in a multi-instance/load-balanced deployment, each instance has independent counters. To get accurate multi-instance rate limiting, replace with the Redis-based ThrottlerStorageRedisService (the @nest-lab/throttler-storage-redis package is already installed).

10.6 Response Headers

When rate limiting is active, these headers are added to responses:

X-RateLimit-Limit-{name}: <limit>
X-RateLimit-Remaining-{name}: <remaining>
X-RateLimit-Reset-{name}: <resetTimestamp>

10.7 Test Endpoints

/v1/rate-limit-test/ provides diagnostic endpoints (excluded from TenantMiddleware):

Route Purpose
GET /no-limit @SkipThrottle() — confirms bypass works
GET /public-limit Tests @PublicRateLimit()
GET /authenticated-limit Tests @AuthenticatedRateLimit()
GET /strict-limit Tests @StrictRateLimit()
GET /contact-limit Tests @ContactRateLimit()
GET /reset-counters Clears all in-memory counters
GET /status Shows current counter state
POST /bulk-test Simulates multiple endpoint calls

11. Module: Transfer

Path: src/modules/transfer/
Purpose: Vehicle/transfer inventory management, itinerary mapping, and primary transfer designation. Structurally mirrors the Hotel module.

11.1 Key Entities

Entity Purpose
Transfers Master transfer/vehicle record
TransferMedia S3 keys for vehicle images
TransferItineraryMapping Links a transfer to an itinerary
TransferTagMapping Tag associations

11.2 Vehicle Types

Defined as an enum in the frontend/DTO layer:

11.3 API Endpoints

Routes under /v1/transfer/

Route Auth Permission Description
GET / jwt TRANSFER_INVENTORY List transfers paginated
POST /check-by-name jwt TRIP Bulk check transfers by name
GET /search jwt TRIP Search transfers by name
POST /inventory jwt TRANSFER_INVENTORY Create transfer in inventory (with media)
GET :transfer_id jwt TRANSFER_INVENTORY Get transfer by ID
POST / jwt TRANSFER_INVENTORY Create transfer with media
PUT :id jwt TRANSFER_INVENTORY Update transfer
DELETE :id jwt TRANSFER_INVENTORY Delete transfer
PATCH :id/status jwt TRANSFER_INVENTORY Toggle active/inactive
POST /map-itinerary jwt TRIP Map/unmap transfer to itinerary
GET /itinerary-mappings/:itinerary_id jwt TRIP Get mappings for itinerary
GET /itinerary/:itinerary_id/transfers jwt TRIP Full transfer details for itinerary
PATCH /primary jwt TRIP Set primary transfer for itinerary

11.4 Inventory vs. Trip Permissions

Same two-mode split as Hotel:

11.5 Exports

TransferModule exports TransferRepository and TransferService.


12. Module: Collection

Path: src/modules/collection/
Purpose: Curated trip collections for frontend display — Header collections (navigation/hero) and Landing collections (landing page showcases).

12.1 Collection Types

Type Purpose Route Prefix
Header Top navigation / hero section collections /collection/header/
Landing Landing page showcase collections /collection/landing/

Each collection type has its own create/update DTOs and slightly different data shapes, but share the same underlying Collection entity pattern.

12.2 Entities

Entity Purpose
Collection Base collection record
HeaderCollection Header-specific collection data
LandingCollection Landing-specific collection data

12.3 API Endpoints

Routes under /v1/collection/

Header:

Route Auth Description
POST /header jwt + Permission.COLLECTION Create header collection
GET /header Public (@PublicRateLimit) Get header collection details
GET /header/itinerary-list/:headerCollectionId jwt + Permission.COLLECTION Paginated itineraries in header collection
PUT /header/:id jwt + Permission.COLLECTION Update header collection
PATCH /header/:id/status jwt + Permission.COLLECTION Toggle active status
DELETE /header/:id jwt + Permission.COLLECTION Delete header collection

Landing:

Route Auth Description
POST /landing jwt + Permission.COLLECTION Create landing collection
GET /landing Public (@PublicRateLimit) Get landing collection details
GET /landing/itinerary-list/:landingCollectionId jwt + Permission.COLLECTION Paginated itineraries in landing
PUT /landing/:id jwt + Permission.COLLECTION Update landing collection

Common:

Route Auth Description
PATCH :id/status jwt + Permission.COLLECTION Toggle active status (generic)
DELETE :id jwt + Permission.COLLECTION Delete collection (generic)

12.4 Circular Dependency

CollectionModuleItineraryModuleCollectionService is exported and consumed directly by ItineraryModule for itinerary-collection management operations.

12.5 Public Endpoints

GET /header and GET /landing are public (no auth) and use @PublicRateLimit() (30 req/min). These are called by the frontend landing page and header on every page load — do not add auth guards to these.


13. Circular Dependency Map

BookingsModule ←──forwardRef──→ RazorpayModule
BookingsModule ←──forwardRef──→ ItineraryModule
ItineraryModule ←─forwardRef──→ CollectionModule
ItineraryModule ←─forwardRef──→ ActivityModule

Rule: Never import these module pairs directly (without forwardRef()). NestJS will throw a circular dependency error at startup. If you add a new cross-dependency between these modules, always use:

// In module.ts imports array:
forwardRef(() => SomeModule)

// In service constructor:
@Inject(forwardRef(() => SomeService))
private readonly someService: SomeService

14. Frontend Cross-Reference

The frontend (M-M_Trip_Frontend/src/services/trip/) has service files that correspond to each backend module:

Backend Module Frontend Service File
booking-auth customerAuth/customerAuth.service.ts
booking-users user.service.ts
bookings bookings.service.ts
hotel hotel.service.ts
itinerary tripDetails.service.ts, tripList.service.ts
markup markup.service.ts
razorpay bookings.service.ts (payment section)
transfer transfer.service.ts
collection tripCollection.service.ts

Two Axios instances on the frontend:

Instance Used For Auth Header
http Admin/agent calls Authorization: Bearer <adminJwt> + tenantid header
clientHttp Customer calls Sends booking_auth cookie + tenantid header; on 401 → clears cookies + redirects to /trip/login

The frontend base path is /trip (configured in next.config.js as basePath: '/trip').


Document generated from live codebase inspection — M-M_Trip_Backend at /var/www/html/M&M Travels/M-M_Trip_Backend/