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
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.
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) |
These patterns are used across all modules I built. Understanding them is essential.
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
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:
save(), find(), findOne(), update(), delete() to automatically inject tenantIdcreateQueryBuilder() to call andWhere('alias.tenantId = :tenantId') on SELECT queriestenantId columnNever call super.find() or raw TypeORM find() directly in a TenantAwareRepository subclass — you will get cross-tenant data leaks.
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 |
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
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.
All endpoints return AppResponse:
interface AppResponse {
message: string; // e.g. "SUC_LOGIN", "SUC_BOOKING_CREATED"
data: any;
}
Path: src/modules/booking-auth/
Purpose: Customer-facing authentication — signup, login, password reset (OTP-based), and Google OAuth.
| 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 |
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 |
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:
accessToken from frontendhttps://www.googleapis.com/oauth2/v3/userinfo to get profileconst 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.
isUsed = true via createQueryBuilder().update(). This is done directly on the entity manager (bypasses TenantAwareRepository) — do not change this pattern.socialAccountId, then by email. If found by email but the socialAccountId doesn't match, it updates the existing user with the new social data.isPasswordChanged flag: Set to true on first changePassword call. Useful for forcing password change on first login flows.BookingAuthModule exports: BookingAuthService, BookingAuthRepository, BookingJwtStrategy, PassportModule. These are consumed by BookingUserModule and BookingsModule.
Path: src/modules/booking-users/
Purpose: Customer profile management (self-service) and admin customer management.
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 |
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.
/profile): use @UseGuards(AuthGuard('booking-jwt')) — customer can only access their own record/, /:id): use @UseGuards(AuthGuard('jwt'), PermissionGuard) — admin can access any customercleanNullValues()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.
Path: src/modules/bookings/
Purpose: Full booking lifecycle — create, status management, cancellation, PDF generation (itinerary, invoice, confirmation voucher), and email delivery.
| 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) |
DRAFT → PROCESSING → CONFIRMED → COMPLETED
↓ ↓
CANCELLED CANCELLED
DRAFT — Booking created, payment not yet madePROCESSING — Razorpay payment captured; webhook triggers this transitionCONFIRMED — Admin confirms the bookingCOMPLETED — Trip completedCANCELLED — Cancelled by admin (with optional Razorpay refund) or customer requested cancellationRoutes 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 |
POST /bookings creates a booking in DRAFT status. Key points:
buildBookingItinerarySnapshot() to snapshot the itinerary pricing at booking time — so price changes to the itinerary after booking don't affect the bookingpricingSelection field determines which pricing scenario was selected (hotel_transfer, hotel_only, etc.)POST /payments/create-order to initiate Razorpay paymentPDFs 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.
BookingsModule has two forwardRef() circular dependencies:
BookingsModule ↔ RazorpayModule — BookingsService calls RazorpayService.processRefund() for admin cancellations; RazorpayService calls BookingsService.updateBookingStatus() on webhook eventsBookingsModule ↔ ItineraryModule — BookingsService calls ItineraryService for price detailsNever remove the forwardRef() wrappers without resolving the circular dependency first.
// 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.
Path: src/modules/hotel/
Purpose: Hotel inventory management, itinerary/day mapping, media uploads, and amenities.
The Hotel module operates in two contexts:
Permission.HOTEL_INVENTORY) — Create/read/update/delete hotels in the global inventory. These are the master hotel records.Permission.TRIP) — Map inventory hotels to itineraries, map hotels to specific days, set primary hotel per city/itinerary.| 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 |
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 |
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.
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.
HotelModule exports HotelRepository and HotelService — both consumed by ItineraryModule.
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.
| 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 |
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:
Traveler-based pricing — The updateTravelerPrice() method in ItineraryRepository is called whenever a markup changes, to keep cached prices in sync.
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 |
ItineraryModule has the most circular dependencies:
ItineraryModule ↔ CollectionModule (via forwardRef())ItineraryModule ↔ ActivityModule (via forwardRef())Additionally, ItineraryModule imports (non-circular): DaysModule, FlightModule.
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.
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.
ItineraryModule exports ItineraryRepository and ItineraryService — consumed by MarkupModule and BookingsModule.
Path: src/modules/markup/
Purpose: Pricing markup management — apply flat or percentage markups globally across all trips or to specific itineraries.
| 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 |
| Entity | Purpose |
|---|---|
Markup |
Markup record (amount, type, global flag) |
MarkupItineraryMapping |
Links a non-global markup to specific itineraries |
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 |
createMarkup throws ConflictException if a global markup already exists for this tenant.deleteMarkup checks isGlobal and throws if true.updateMarkup validates that removing itineraries won't leave the markup unmapped.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.normalizeMarkup() transforms the raw DB response into a cleaner appliesTo shape:
{
"appliesTo": {
"type": "ALL_TRIPS", // or "SPECIFIC_TRIPS"
"tripIds": [],
"trips": []
}
}
Path: src/modules/razorpay/
Purpose: Full payment lifecycle — order creation, payment verification, webhook handling, refunds, and transaction audit logging.
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.
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).
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
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.
// Always use Decimal.js for financial amounts
const amountInPaise = new Decimal(amount).mul(100).toNumber();
Never use raw floating-point arithmetic for prices — 0.1 + 0.2 !== 0.3 in JavaScript.
POST /payments/refund (admin only):
bookingId, amount, optional reasonBookingPaymentpayments.refund(paymentId, { amount })TransactionLogSendMailerUtilityEvery payment operation (success and failure) is written to TransactionLog. Fields include:
bookingId, paymentId, orderIdoperation — CREATE_ORDER, CAPTURE, REFUND, WEBHOOK_EVENT, etc.status — SUCCESS / FAILUREerrorMessage — raw Razorpay error if applicablemetadata — full Razorpay response JSONRazorpayModule ↔ BookingsModule — both import each other via forwardRef(). RazorpayService needs BookingsService.updateBookingStatus() for webhook processing; BookingsService needs RazorpayService.processRefund() for admin cancellation.
Path: src/modules/rate-limit-test/
Shared files: src/shared/decorators/rate-limit.decorator.ts, src/shared/guards/custom-rate-limit.guard.ts
Rate limiting is currently DISABLED. The
CustomRateLimitGuardhasisRateLimitDisabled = trueat line 12 ofcustom-rate-limit.guard.ts. To re-enable, change this flag tofalse.
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.
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 |
IP extraction priority order:
CF-Connecting-IP (Cloudflare)X-Real-IPX-Forwarded-For (first IP in chain)req.ip (direct connection)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).
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>
/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 |
Path: src/modules/transfer/
Purpose: Vehicle/transfer inventory management, itinerary mapping, and primary transfer designation. Structurally mirrors the Hotel module.
| Entity | Purpose |
|---|---|
Transfers |
Master transfer/vehicle record |
TransferMedia |
S3 keys for vehicle images |
TransferItineraryMapping |
Links a transfer to an itinerary |
TransferTagMapping |
Tag associations |
Defined as an enum in the frontend/DTO layer:
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 |
Same two-mode split as Hotel:
TRANSFER_INVENTORY — master record managementTRIP — mapping to itinerariesTransferModule exports TransferRepository and TransferService.
Path: src/modules/collection/
Purpose: Curated trip collections for frontend display — Header collections (navigation/hero) and Landing collections (landing page showcases).
| 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.
| Entity | Purpose |
|---|---|
Collection |
Base collection record |
HeaderCollection |
Header-specific collection data |
LandingCollection |
Landing-specific collection data |
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) |
CollectionModule ↔ ItineraryModule — CollectionService is exported and consumed directly by ItineraryModule for itinerary-collection management operations.
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.
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
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/