113 Commits

Author SHA1 Message Date
paritosh18
3937be710b Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into swagger 2025-12-22 13:35:24 +05:30
cab4408dc8 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-22 13:33:11 +05:30
be65a6c021 Added seed for energy level 2025-12-22 13:33:10 +05:30
paritosh18
f13e90ce39 upated createNewActivity handler 2025-12-22 13:30:55 +05:30
paritosh18
5a223f126f creat activity handler 2025-12-21 17:28:08 +05:30
paritosh18
2e4f318684 Add CreateActivityDto and update createFullActivity method for activity creation 2025-12-18 20:05:30 +05:30
paritosh18
53785bd5f2 Add handler for creating full activity with related records 2025-12-18 19:37:32 +05:30
c9b507f969 Enhance rejection email content with clickable link for application access 2025-12-18 16:55:57 +05:30
91871d1f44 Enhance search filter to include company name and user reference number in host retrieval 2025-12-17 17:29:17 +05:30
05e48063c9 Add isActive check when retrieving user in loginForHost method 2025-12-17 16:57:18 +05:30
a906dc5635 Add getAddActivityPrePopulate handler and implement prepopulate data retrieval
- Introduced a new handler for adding activity prepopulation.
- Enhanced the PrePopulateService to fetch all necessary prepopulate data for the new activity.
- Updated the updateBankDetails handler to correctly inject host ID.
- Improved user data retrieval in HostService to include additional fields.
- Added validation for host ID in showSuggestionToAM handler.
2025-12-17 16:17:55 +05:30
8ec8cf4854 fixed the path 2025-12-16 19:11:15 +05:30
fab7642302 Implement parent document deletion and improve host retrieval logic in onboarding process 2025-12-16 17:34:10 +05:30
4d3796c5f3 Refactor companyTypes seeding and enhance host retrieval to include user email address 2025-12-16 16:36:15 +05:30
2f3c531c56 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-16 16:09:28 +05:30
a2907929d4 updated serverless version 2025-12-16 16:08:59 +05:30
paritosh18
ef2b23ef83 Update serverless configuration to use exported stack output for Prisma Lambda Layer ARN, ensuring proper deployment functionality. 2025-12-16 16:07:03 +05:30
2767d29d79 Update parent company validation to allow optional fields and handle null values in company details submission. 2025-12-16 15:38:03 +05:30
46daec00ce fixed the keytooloong issue 2025-12-16 14:37:38 +05:30
43e494780d Refactor document name handling in host service to sanitize input and ensure valid connections for city, state, and country IDs during host creation and updates. 2025-12-16 13:16:22 +05:30
0da18b18f7 Add getSuggestionsForAM function and corresponding handler for retrieving suggestions based on host assignments. Update serverless configuration to include new API endpoint. 2025-12-16 12:07:42 +05:30
b5304b3c26 Refactor OTP email content for host registration and resend functionality to improve clarity and tone. 2025-12-15 16:53:12 +05:30
82340c2918 Implement email notifications for host application acceptance and rejection, including host details retrieval and email content customization. 2025-12-10 14:59:16 +05:30
paritosh18
31c312eb3c Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into swagger 2025-12-10 11:40:51 +05:30
b8f5f92c98 Add validation for city ID in company details submission to ensure existence in the database 2025-12-09 17:38:44 +05:30
3652d851f7 Enhance email notifications for host application approvals and rejections by including the host's first name in the greeting. 2025-12-09 15:02:15 +05:30
paritosh18
6166075967 Update service name in serverless configuration to 'minglar' and add 'zod' dependency in package-lock.json 2025-12-09 13:50:30 +05:30
paritosh18
b6cb5831c2 Refactor Prisma client usage and enhance service integration for improved connection management 2025-12-09 13:49:20 +05:30
paritosh18
a39cc1c3c8 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-09 12:09:38 +05:30
paritosh18
ae76618f7a Add environment configuration template and update .gitignore for new env files 2025-12-09 12:09:21 +05:30
e957fc5c50 Add token blacklist check in JWT middlewares to enhance session management and security 2025-12-09 12:07:03 +05:30
paritosh18
856db687c3 Update service name in serverless configuration to 'minglarDev' for development environment 2025-12-08 17:58:38 +05:30
a020a28993 Update host application handlers to retrieve user details using hostXid instead of userInfo.id for accurate email notifications. 2025-12-08 17:31:55 +05:30
paritosh18
f6e01ac9e3 Update CURL example and enhance Swagger documentation for admin profile update 2025-12-08 16:58:30 +05:30
1a520ae9e1 Remove email parameter from resendOtpHelper and normalize email addresses to lowercase in the resend OTP handler for consistent processing. 2025-12-08 15:25:21 +05:30
d5d6951e64 Normalize email addresses to lowercase in OTP verification handler for consistent processing. 2025-12-08 15:19:31 +05:30
bbd55562af Add HOST_LINK environment variable and update related configurations. Normalize email addresses to lowercase in login and registration handlers. Enhance email notifications for approved and rejected applications with HOST_LINK. 2025-12-08 15:08:05 +05:30
paritosh18
76970c914e Merge branch 'swagger' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into swagger 2025-12-08 12:27:39 +05:30
9abadba8f5 Refactor database access by introducing prisma.lambda.service for centralized PrismaClient usage across modules. Update imports in various handlers and services to utilize the new service, ensuring consistent database interactions and improved maintainability. 2025-12-08 11:23:58 +05:30
747566497c Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-06 16:33:57 +05:30
6c3e5ccd60 Add multiple states, cities, and banks to seed data in prisma/seed.ts 2025-12-06 16:33:35 +05:30
ca9ba601ad Add additional states and cities to seed data in Prisma 2025-12-06 16:27:54 +05:30
eab6565e12 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-06 16:11:53 +05:30
paritosh18
61737235a4 Update Swagger documentation: refine request/response schemas, enhance descriptions, and adjust parameter requirements 2025-12-06 13:13:56 +05:30
6a84876518 Remove profile completion percentage calculation from user update in MinglarService 2025-12-06 12:55:07 +05:30
d8fb4b242d Add roleXid field to user selection in MinglarService 2025-12-06 12:39:21 +05:30
paritosh18
844bbf9618 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into swagger 2025-12-06 12:37:19 +05:30
51b053310f Add validation to prevent duplicate host accounts in addPaymentDetails method 2025-12-06 12:06:04 +05:30
paritosh18
cdae23ec6c Add Swagger documentation and handlers for API endpoints
- Created swagger.yml to define Swagger UI and JSON endpoints.
- Implemented swagger.ts to serve Swagger UI HTML and JSON specifications.
- Updated swagger.json with detailed API documentation, including paths, components, and schemas for various requests and responses.
2025-12-05 20:35:46 +05:30
06010ef6e8 Refactor API paths for acceptPQByAM and rejectPQQByAM to remove activityId from URL and update request body parsing for activityId 2025-12-05 19:21:19 +05:30
7b9763c668 Enhance getAllHostActivityForMinglar method to exclude activities with DRAFT_PQ status 2025-12-05 18:51:50 +05:30
4c1a04d043 Update serverless configuration to comment out Prisma client dependencies and enhance MinglarService to exclude draft activities from host activity retrieval. 2025-12-05 18:50:14 +05:30
paritosh18
ecf45c3e7c Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-05 14:56:45 +05:30
paritosh18
20a931ab27 Add AWS Lambda Bundle Size Optimization Guide and update build scripts for Prisma layer 2025-12-05 14:56:43 +05:30
6a84fbc0c3 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-05 14:38:32 +05:30
9fc8336bd9 fixed the routes 2025-12-05 14:37:37 +05:30
b049146664 Refactor serverless configuration to rename service, add Prisma layer support, and include AM_INVITATION_LINK in environment variables. Update validation schema and email invitation content for improved user experience. 2025-12-05 13:45:49 +05:30
963f84681c Enhance HostService to retrieve and utilize existing logo paths for host and parent companies, ensuring consistency in logo display across company details. 2025-12-04 20:18:22 +05:30
ab9e02972e Refactor hostCompanyDetails validation schema to make address and location fields optional; update login handler to remove password comparison logic; enhance host service to include user status check and return specific user fields; clean up profile completion logic in MinglarService. 2025-12-04 20:01:09 +05:30
33b330a15b Merge branch 'pqq-optimized-logic' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into pqq-optimized-logic 2025-12-04 18:40:44 +05:30
d898dcd8ff Add S3 document deletion functionality in submitCompanyDetails handler; update host service to return activity ID with sorted categories; modify PQP details retrieval to use new admin host token for authentication. 2025-12-04 18:40:41 +05:30
paritosh18
ff18fcbf9f Merge branch 'pqq-optimized-logic' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into pqq-optimized-logic 2025-12-04 11:54:12 +05:30
paritosh18
a872ed89e4 Enhance MinglarService: Add id field to selection in PQQ data retrieval and ensure activityPqqHeaderId is set at the category level for better data integrity. 2025-12-04 11:53:37 +05:30
759eeb298c Update sorting logic for categories and subcategories to ensure proper order in responses. 2025-12-04 08:33:21 +05:30
1b72e65a71 Enhance PQQ data retrieval: Add comments and pqqAnswerXid fields to the selection in HostService and MinglarService. Update PQQAnswers structure to include all answer options for questions, improving data handling in responses. 2025-12-03 19:43:22 +05:30
4a7e5fbb1e Add PQQ functionality: Introduce new endpoints for creating activities and submitting answers, along with updates to the Minglar service for retrieving PQQ details. Update serverless configuration to include new function files. 2025-12-03 19:21:21 +05:30
ca5936d0db Update referencedBy field in HostService to default to null if undefined 2025-12-03 14:56:15 +05:30
c0d607a321 Add optional referencedBy field to hostCompanyDetails validation schema and update HostService to handle referencedBy data 2025-12-03 14:41:00 +05:30
1d684b7de6 Add referencedBy field to HostHeader model and update import paths for various handlers 2025-12-03 14:34:53 +05:30
16b16ac7ca Refactor import path for verifyMinglarAdminHostToken and update selection fields in getAllPQQQuesAndSubmittedAns method 2025-12-03 13:56:39 +05:30
0e0c63e31a path updated for the renamed file 2025-12-03 13:49:56 +05:30
822b425c1d Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-03 13:43:41 +05:30
42e2d7a579 taking the activity xid in get all submited ques ans 2025-12-03 13:43:30 +05:30
paritosh18
78f49b35dd Update API path for retrieving all PQQ questions and answers for AM 2025-12-03 13:37:01 +05:30
paritosh18
930ae708a1 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into sprint1 2025-12-03 13:20:38 +05:30
paritosh18
8fb3f16d18 Add endpoint to retrieve all PQQ questions and answers for AM 2025-12-03 13:20:21 +05:30
3d6226ddac added search 2025-12-03 13:18:28 +05:30
38c616a7af making entries in host track 2025-12-03 12:44:07 +05:30
c6ab3e57c0 fixed the condition of submit pq for review 2025-12-02 20:19:03 +05:30
paritosh18
776c03911e Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into sprint1 2025-12-02 20:11:19 +05:30
paritosh18
781058c443 Implement pagination for host activity retrieval in HostService and MinglarService 2025-12-02 20:10:10 +05:30
3b723e5d1f made accept pq by am api 2025-12-02 20:09:42 +05:30
paritosh18
56ebf44d37 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-02 19:58:52 +05:30
4f8274adb9 Add pinCode field to location selection in MinglarService 2025-12-02 19:00:30 +05:30
39f182b8e9 sending city state abd country in getbyidAM 2025-12-02 18:59:06 +05:30
d9f7cd9a0f fixed the optional chaining 2025-12-02 18:08:39 +05:30
4772c320ba Enhance HostService and MinglarService to include account manager details and process profile images. Updated getAllHostActivity to retrieve account manager information and generate presigned URLs for media and profile images. Refactored media processing logic for improved clarity and consistency. 2025-12-02 17:53:19 +05:30
paritosh18
f19f5e46c4 Remove commented-out validation for allowed titles in addPQQSuggestion handler 2025-12-02 17:33:30 +05:30
paritosh18
156aed2429 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-02 17:27:57 +05:30
paritosh18
fb77111e34 Refactor payment details handling: remove IFSC code validation, add currencyXid to DTO, and implement bank branch lookup for IFSC code retrieval. 2025-12-02 17:27:36 +05:30
6b673a173d Refactor host functions and constants for improved clarity and functionality. Renamed 'getAllActivityType' to 'prePopulateNewActivity' and updated its path. Added 'submitPQQForReview' handler for submitting PQQ for review. Enhanced error handling and response structure in 'submitPQQ_Answer' and 'submitPQQForReview' methods. Updated constants to standardize PQQ-related statuses. Improved S3 file handling logic in various handlers. 2025-12-02 17:24:01 +05:30
3c4b0db39f Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-02 13:42:27 +05:30
e72c260b18 Add ActivityTrack model to schema and update User and Activities models to include activityTracks relation. Modify seed data to reflect new interest names and activity types. Implement activity reference number generation in HostService for activity creation. 2025-12-02 13:42:14 +05:30
paritosh18
9a777eb3f9 Refactor addOrUpdateCompanyDetails method in HostService to streamline host status handling and improve document management logic for both new and existing companies. 2025-12-02 13:40:41 +05:30
paritosh18
c3f0a1d82a Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-02 13:19:16 +05:30
3f921febe0 Refactor host status determination logic in HostService to handle various submission cases, including updates and drafts. Update host suggestion review process to only mark suggestions as reviewed when not in draft state. 2025-12-01 20:25:23 +05:30
paritosh18
4bc5eb8d4d Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 19:56:43 +05:30
f3f0b2a81b Refactor HOST_SUGGESTION_TITLES to comments in minglar.constant.ts, update HostService to filter HostSuggestion by active and reviewed status, and modify addSuggestion handler to remove title validation against HOST_SUGGESTION_TITLES. Enhance MinglarService to include profile image handling and add city, country, and state selections in user queries. 2025-12-01 19:56:21 +05:30
paritosh18
6f0cdb4e0a Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 17:58:14 +05:30
ce9c8174d8 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-01 17:57:45 +05:30
c72e757bf3 Refactor Minglar admin functions: removed deprecated acceptHostApplicationMinglar handler, added editAgreementDetailsAndAccept handler, and updated related service methods for improved agreement processing. Cleaned up schema by removing isSubsidairy field and adjusted file handling logic in submitCompanyDetails. Enhanced suggestion handling by including isParent flag in addSuggestion. Updated host retrieval logic in getStepper. 2025-12-01 17:57:08 +05:30
paritosh18
6aaf49bf72 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 17:07:59 +05:30
paritosh18
1b31ca4a83 Implement pagination in getAllOnboardingHostApplications handler and update MinglarService to support pagination options. Enhance response structure to include total count of applications. 2025-12-01 17:05:08 +05:30
140f70615c Added adminStatusInternal field to the MinglarService for enhanced data handling. 2025-12-01 15:48:42 +05:30
32f4c7ce42 added filter in get host am api 2025-12-01 15:43:06 +05:30
paritosh18
067d1a1f1b Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 13:45:51 +05:30
paritosh18
470298a3fb Add search functionality to getAllOnboardingHostApplications and getAllOnboardingNewApplications handlers; update MinglarService to support search queries 2025-12-01 13:45:39 +05:30
1d7d0749b3 Refactor authentication: replaced instances of verifyHostToken with verifyMinglarAdminHostToken in prepopulate handlers for improved security and consistency. 2025-12-01 13:42:10 +05:30
paritosh18
d3fb1c85fb Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 13:32:29 +05:30
e723e680ab sending the internal admin status to frontend 2025-12-01 13:32:09 +05:30
paritosh18
0e50b8b187 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 13:29:46 +05:30
paritosh18
bbe725dd9e Update HTTP methods to PATCH for rejectHostApplication endpoints and implement pagination in getAllInvitationDetails and getAllInvitedCoadminAndAM handlers 2025-12-01 13:29:10 +05:30
e5861654e9 Enhance company types management: updated schema to include display order and relationships, modified validation to use company type XID, and seeded initial company types data. Updated services to reflect new structure and ensure proper data handling. 2025-12-01 13:26:06 +05:30
167 changed files with 8082 additions and 4274 deletions

44
.env.example Normal file
View File

@@ -0,0 +1,44 @@
# Environment Configuration Template
# Copy this file to .env.dev, .env.test, or .env.uat and fill in the values
# Database
DATABASE_URL=
DB_USERNAME=
DB_PASSWORD=
DB_DATABASE_NAME=
DB_HOSTNAME=
DB_PORT=5432
# Email Bypass (set to true for dev/test, false for production-like environments)
BY_PASS_EMAIL=
BYPASS_OTP=
# Brevo Email Configuration
BREVO_EMAIL_API_KEY=
BREVO_API_BASEURL=https://api.brevo.com
BREVO_FROM_EMAIL=
BREVO_SMTP_HOST=smtp-relay.brevo.com
BREVO_SMTP_PORT=587
BREVO_SMTP_USER=
BREVO_SMTP_PASS=
# JWT Configuration
REFRESH_TOKEN_SECRET=
JWT_SECRET=
JWT_ACCESS_EXPIRATION_MINUTES=30
JWT_REFRESH_EXPIRATION_DAYS=7
JWT_RESET_PASSWORD_EXPIRATION_MINUTES=15
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES=15
# Security
SALT_ROUNDS=
NODE_ENV=
# AWS S3
S3_BUCKET_NAME=
# Admin Configuration
MINGLAR_ADMIN_NAME=
MINGLAR_ADMIN_EMAIL=
AM_INVITATION_LINK=
HOST_LINK=

7
.gitignore vendored
View File

@@ -40,16 +40,15 @@ lerna-debug.log*
.env.test.local
.env.production.local
.env.local
.env.dev
.env.test
.env.uat
# temp
.tmp
.temp
undefined/
# tsx cache/temp directories
**/tsx-*/
**/temp/tsx-*
# Runtime data
pids
*.pid

View File

@@ -0,0 +1,490 @@
# AWS Lambda Bundle Size Optimization Guide
## Overview
This guide documents how to optimize AWS Lambda function bundle sizes when using:
- **Serverless Framework v4** (with built-in esbuild)
- **Prisma ORM** (with driver adapters)
- **NestJS** or any Node.js framework
## Problem
Lambda functions can become bloated (25+ MB) due to:
1. **Prisma binary engines** (~50MB uncompressed)
2. **AWS SDK v3** being bundled (~5-10MB)
3. **Dependencies copied to node_modules** instead of being bundled
## Solution Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Lambda Function │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Your Code (bundled by esbuild) ~50-500 KB │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Prisma Layer (shared) ~15 MB │ │
│ │ - @prisma/client │ │
│ │ - @prisma/adapter-pg │ │
│ │ - .prisma/client (generated) │ │
│ │ - pg driver │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ AWS Lambda Runtime │ │
│ │ - AWS SDK v3 (built-in for Node.js 18+) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## Step-by-Step Setup
### 1. Project Structure
```
your-project/
├── serverless.yml
├── package.json
├── prisma/
│ └── schema.prisma
├── layers/
│ └── prisma/
│ └── nodejs/
│ └── package.json
├── src/
│ └── ... your code
└── build-prisma-layer.ps1 (or .sh for Linux/Mac)
```
### 2. Prisma Schema Configuration
**`prisma/schema.prisma`**
```prisma
generator client {
provider = "prisma-client-js"
// For Prisma 7+ with driver adapters, no binary targets needed
// The WASM-based query engine is used automatically
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```
> **Note**: Prisma 7+ uses WASM-based query compiler instead of binary engines when using driver adapters, which is much smaller.
### 3. Layer Package.json
**`layers/prisma/nodejs/package.json`**
```json
{
"name": "prisma-layer",
"version": "1.0.0",
"description": "Lambda layer for Prisma with pg driver adapter",
"dependencies": {
"@prisma/client": "^7.0.0",
"@prisma/adapter-pg": "^7.0.0",
"pg": "^8.13.0"
}
}
```
### 4. Serverless Configuration
**`serverless.yml`**
```yaml
service: your-service-name
provider:
name: aws
runtime: nodejs22.x
region: your-region
memorySize: 512
# Apply Prisma layer to ALL functions
layers:
- !Ref PrismaLambdaLayer
environment:
DATABASE_URL: ${env:DATABASE_URL}
# ... other env vars
# esbuild configuration (Serverless v4 built-in)
build:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node20
platform: node
# Mark packages as external (not bundled into JS)
external:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma/client'
- '.prisma'
- 'pg'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
# Exclude from npm install in zip (CRITICAL!)
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
# Define the Prisma layer
layers:
prisma:
path: layers/prisma
name: ${self:service}-prisma-layer-${sls:stage}
description: Prisma client with pg driver adapter
compatibleRuntimes:
- nodejs22.x
retain: false
# Package configuration
package:
individually: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
- '!node_modules/.prisma/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!.git/**'
functions:
myFunction:
handler: src/handlers/myHandler.handler
events:
- httpApi:
path: /my-endpoint
method: get
plugins:
- serverless-offline
```
### 5. Build Script for Prisma Layer
**Windows (PowerShell) - `build-prisma-layer.ps1`**
```powershell
# Build Prisma Lambda Layer
$layerPath = "layers\prisma\nodejs"
Write-Host "Building Prisma Lambda Layer..." -ForegroundColor Cyan
# 1. Clean existing node_modules in layer
Write-Host "Cleaning layer node_modules..."
if (Test-Path "$layerPath\node_modules") {
Remove-Item -Recurse -Force "$layerPath\node_modules"
}
# 2. Install dependencies in layer
Write-Host "Installing layer dependencies..."
Push-Location $layerPath
npm install --omit=dev
Pop-Location
# 3. Generate Prisma client
Write-Host "Generating Prisma client..."
npx prisma generate
# 4. Copy .prisma/client to layer
Write-Host "Copying generated Prisma client to layer..."
$sourcePrisma = "node_modules\.prisma"
$destPrisma = "$layerPath\node_modules\.prisma"
if (Test-Path $sourcePrisma) {
if (Test-Path $destPrisma) {
Remove-Item -Recurse -Force $destPrisma
}
Copy-Item -Recurse $sourcePrisma $destPrisma
Write-Host "Copied .prisma/client successfully!" -ForegroundColor Green
} else {
Write-Host "ERROR: .prisma folder not found. Run 'npx prisma generate' first." -ForegroundColor Red
exit 1
}
# 5. Show layer size
$layerSize = (Get-ChildItem "$layerPath\node_modules" -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "`nTotal layer size: $([math]::Round($layerSize, 2)) MB" -ForegroundColor Yellow
Write-Host "Prisma layer built successfully!" -ForegroundColor Green
```
**Linux/Mac (Bash) - `build-prisma-layer.sh`**
```bash
#!/bin/bash
set -e
LAYER_PATH="layers/prisma/nodejs"
echo "Building Prisma Lambda Layer..."
# 1. Clean existing node_modules in layer
echo "Cleaning layer node_modules..."
rm -rf "$LAYER_PATH/node_modules"
# 2. Install dependencies in layer
echo "Installing layer dependencies..."
cd "$LAYER_PATH"
npm install --omit=dev
cd -
# 3. Generate Prisma client
echo "Generating Prisma client..."
npx prisma generate
# 4. Copy .prisma/client to layer
echo "Copying generated Prisma client to layer..."
if [ -d "node_modules/.prisma" ]; then
rm -rf "$LAYER_PATH/node_modules/.prisma"
cp -r "node_modules/.prisma" "$LAYER_PATH/node_modules/.prisma"
echo "Copied .prisma/client successfully!"
else
echo "ERROR: .prisma folder not found. Run 'npx prisma generate' first."
exit 1
fi
# 5. Show layer size
LAYER_SIZE=$(du -sm "$LAYER_PATH/node_modules" | cut -f1)
echo "Total layer size: ${LAYER_SIZE} MB"
echo "Prisma layer built successfully!"
```
### 6. Prisma Client Usage
**`src/common/database/prisma.client.ts`**
```typescript
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
// Connection pool for serverless
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 5, // Limit connections in Lambda
});
const adapter = new PrismaPg(pool);
// Single instance for Lambda warm starts
let prisma: PrismaClient;
export function getPrismaClient(): PrismaClient {
if (!prisma) {
prisma = new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
}
return prisma;
}
export { prisma };
```
---
## Deployment Workflow
```bash
# 1. Build the Prisma layer (run after schema changes)
./build-prisma-layer.ps1 # Windows
# or
./build-prisma-layer.sh # Linux/Mac
# 2. Deploy
npx serverless deploy --stage=dev
# 3. Deploy single function (faster, uses existing layer)
npx serverless deploy function -f myFunction --stage=dev
```
---
## Key Configuration Explained
### esbuild `external` vs `exclude`
| Property | Purpose |
|----------|---------|
| `external` | Tells esbuild NOT to bundle these into the JS file. They become `require()` calls. |
| `exclude` | Tells Serverless NOT to `npm install` these packages into the function zip. |
**Both are required!**
- `external` alone = esbuild doesn't bundle, but Serverless still installs to node_modules
- `exclude` alone = Serverless doesn't install, but esbuild bundles the code
### Layer Reference
```yaml
# This creates a CloudFormation reference to the layer defined in the same stack
layers:
- !Ref PrismaLambdaLayer
```
The `PrismaLambdaLayer` name comes from the layer key (`prisma`) converted to PascalCase + `LambdaLayer`.
### Why Exclude pg-* packages?
When `pg` is external, its dependencies still get installed:
- `pg-connection-string`
- `pg-pool`
- `pg-protocol`
- `pg-types`
- etc.
These must all be in the `exclude` list to prevent duplication.
---
## Expected Results
| Function Type | Before | After |
|---------------|--------|-------|
| Simple handlers | 25+ MB | **50-100 kB** |
| With validation (zod/yup) | 25+ MB | **300-500 kB** |
| With S3 uploads | 30+ MB | **1-2 MB** |
---
## Troubleshooting
### 1. "Cannot find module '@prisma/client'"
**Cause**: Layer doesn't have the generated `.prisma/client`
**Fix**: Run `build-prisma-layer.ps1` to regenerate the layer
### 2. Function size still large
**Debug**: Extract and inspect the zip:
```powershell
Expand-Archive ".serverless\build\your-function.zip" -DestinationPath "extracted"
Get-ChildItem "extracted\node_modules" -Directory
```
If you see `@prisma` or `pg` folders, the `exclude` config isn't working.
### 3. "Cannot resolve CloudFormation reference"
**Cause**: Using `${cf:...}` reference before first deploy
**Fix**: Use `!Ref PrismaLambdaLayer` instead (works on first deploy)
### 4. Cold starts still slow
Consider:
- **Provisioned Concurrency**: Pre-warm instances
- **Reduce memory**: Sometimes lower memory = same speed, lower cost
- **Connection pooling**: Use tools like PgBouncer for RDS
---
## Additional Optimizations
### 1. Remove duplicate validation libraries
Pick ONE of: `zod`, `yup`, or `class-validator`. Don't use all three.
### 2. Tree-shake NestJS
If not using full NestJS, import only what you need:
```typescript
// Instead of
import { Controller, Get } from '@nestjs/common';
// For Lambda handlers, you might not need NestJS at all
```
### 3. Use AWS SDK v3 selectively
```yaml
external:
- '@aws-sdk/*' # Exclude all
```
Then in code:
```typescript
// AWS SDK v3 is available in Lambda runtime (Node.js 18+)
import { S3Client } from '@aws-sdk/client-s3';
```
---
## Quick Reference
```yaml
# Minimal serverless.yml for Prisma + Lambda optimization
build:
esbuild:
bundle: true
minify: true
external:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma/client'
- '.prisma'
- 'pg'
- '@aws-sdk/*'
exclude:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
- '@aws-sdk/*'
layers:
prisma:
path: layers/prisma
name: ${self:service}-prisma-${sls:stage}
compatibleRuntimes:
- nodejs22.x
provider:
layers:
- !Ref PrismaLambdaLayer
```
---
## Version Compatibility
| Tool | Tested Version |
|------|----------------|
| Serverless Framework | v4.x |
| Prisma | v7.x |
| Node.js | 20.x, 22.x |
| AWS Lambda Runtime | nodejs20.x, nodejs22.x |
---
*Last updated: December 2025*

View File

@@ -1,46 +1,51 @@
# build-prisma-layer.ps1
# Script to rebuild the Prisma layer for Lambda deployment
# Usage: .\build-prisma-layer.ps1
# Build Prisma Lambda Layer
# Run this script before deploying to ensure the layer has the generated client
$ErrorActionPreference = "Stop"
$layerPath = "layers\prisma\nodejs"
$projectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$layerPath = Join-Path $projectRoot "layers\prisma\nodejs"
$nodeModulesPath = Join-Path $layerPath "node_modules"
Write-Host "Building Prisma Lambda Layer..." -ForegroundColor Cyan
Write-Host "🔄 Building Prisma layer for Lambda..." -ForegroundColor Cyan
# Step 1: Regenerate Prisma client
Write-Host "📦 Regenerating Prisma client..." -ForegroundColor Yellow
Push-Location $projectRoot
npx prisma generate
Pop-Location
# Step 2: Clean layer node_modules
Write-Host "🧹 Cleaning layer node_modules..." -ForegroundColor Yellow
if (Test-Path $nodeModulesPath) {
Remove-Item -Recurse -Force $nodeModulesPath
# 1. Clean existing node_modules in layer
Write-Host "Cleaning layer node_modules..."
if (Test-Path "$layerPath\node_modules") {
Remove-Item -Recurse -Force "$layerPath\node_modules"
}
# Step 3: Install layer dependencies
Write-Host "📥 Installing layer dependencies..." -ForegroundColor Yellow
# 2. Install dependencies in layer
Write-Host "Installing layer dependencies..."
Push-Location $layerPath
npm install --production
npm install --omit=dev
Pop-Location
# Step 4: Copy generated Prisma client to layer
Write-Host "📋 Copying generated Prisma client to layer..." -ForegroundColor Yellow
$sourcePrisma = Join-Path $projectRoot "node_modules\.prisma\client"
$destPrisma = Join-Path $nodeModulesPath ".prisma\client"
# 3. Generate Prisma client into the layer
Write-Host "Generating Prisma client into layer..."
# Set the output directory for Prisma client
$env:PRISMA_GENERATE_DATAPROXY = "false"
New-Item -ItemType Directory -Force -Path (Split-Path $destPrisma) | Out-Null
Copy-Item -Path $sourcePrisma -Destination $destPrisma -Recurse -Force
# Generate client - this creates .prisma/client
npx prisma generate
# Step 5: Calculate layer size
$layerSize = (Get-ChildItem -Path $layerPath -Recurse -File | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "✅ Layer built successfully!" -ForegroundColor Green
Write-Host "📊 Layer size: $([math]::Round($layerSize, 2)) MB" -ForegroundColor Cyan
# 4. Copy .prisma/client to layer
Write-Host "Copying generated Prisma client to layer..."
$sourcePrisma = "node_modules\.prisma"
$destPrisma = "$layerPath\node_modules\.prisma"
# List contents
Write-Host "`n📁 Layer contents:" -ForegroundColor Cyan
Get-ChildItem $nodeModulesPath -Directory | ForEach-Object { Write-Host " - $($_.Name)" }
if (Test-Path $sourcePrisma) {
if (Test-Path $destPrisma) {
Remove-Item -Recurse -Force $destPrisma
}
Copy-Item -Recurse $sourcePrisma $destPrisma
Write-Host "Copied .prisma/client successfully!" -ForegroundColor Green
} else {
Write-Host "ERROR: .prisma folder not found. Run 'npx prisma generate' first." -ForegroundColor Red
exit 1
}
# 5. Show layer size
Write-Host "`nLayer contents:"
Get-ChildItem "$layerPath\node_modules" -Directory | Select-Object Name
$layerSize = (Get-ChildItem "$layerPath\node_modules" -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "`nTotal layer size: $([math]::Round($layerSize, 2)) MB" -ForegroundColor Yellow
Write-Host "`nPrisma layer built successfully!" -ForegroundColor Green
Write-Host "Run 'serverless deploy' to deploy with the updated layer."

View File

@@ -1,40 +0,0 @@
#!/bin/bash
# build-prisma-layer.sh
# Script to rebuild the Prisma layer for Lambda deployment
# Usage: ./build-prisma-layer.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LAYER_PATH="$SCRIPT_DIR/layers/prisma/nodejs"
echo "🔄 Building Prisma layer for Lambda..."
# Step 1: Regenerate Prisma client
echo "📦 Regenerating Prisma client..."
cd "$SCRIPT_DIR"
npx prisma generate
# Step 2: Clean layer node_modules
echo "🧹 Cleaning layer node_modules..."
rm -rf "$LAYER_PATH/node_modules"
# Step 3: Install layer dependencies
echo "📥 Installing layer dependencies..."
cd "$LAYER_PATH"
npm install --production
# Step 4: Copy generated Prisma client to layer
echo "📋 Copying generated Prisma client to layer..."
mkdir -p "$LAYER_PATH/node_modules/.prisma"
cp -r "$SCRIPT_DIR/node_modules/.prisma/client" "$LAYER_PATH/node_modules/.prisma/client"
# Step 5: Calculate layer size
LAYER_SIZE=$(du -sh "$LAYER_PATH" | cut -f1)
echo "✅ Layer built successfully!"
echo "📊 Layer size: $LAYER_SIZE"
# List contents
echo ""
echo "📁 Layer contents:"
ls -d "$LAYER_PATH/node_modules"/*/ 2>/dev/null | xargs -n 1 basename | sed 's/^/ - /'

View File

@@ -10,7 +10,8 @@
"dependencies": {
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"pg": "^8.13.0"
"pg": "^8.13.0",
"zod": "^4.1.12"
}
},
"node_modules/@prisma/adapter-pg": {
@@ -223,6 +224,15 @@
"engines": {
"node": ">=0.4"
}
},
"node_modules/zod": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -1,10 +1,11 @@
{
"name": "prisma-layer",
"version": "1.0.0",
"description": "Lambda layer for Prisma 7 with pg driver adapter",
"description": "Lambda layer for Prisma 7 with pg driver adapter and zod",
"dependencies": {
"@prisma/client": "^7.0.1",
"@prisma/adapter-pg": "^7.0.1",
"pg": "^8.13.0"
"pg": "^8.13.0",
"zod": "^4.1.12"
}
}

38
package-lock.json generated
View File

@@ -46,7 +46,7 @@
"prisma": "^7.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"serverless": "4.17.0",
"serverless": "4.24.0",
"swagger-ui-express": "^5.0.0",
"tslib": "^2.8.1",
"uuid": "^13.0.0",
@@ -75,6 +75,7 @@
"serverless-offline": "^14.4.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"swagger-ui-express": "^5.0.1",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
@@ -6564,6 +6565,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -8329,6 +8331,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -8359,6 +8362,7 @@
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -8369,6 +8373,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -9379,6 +9384,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -9422,6 +9428,7 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -9447,6 +9454,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -9468,6 +9476,7 @@
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -9485,6 +9494,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -9495,6 +9505,7 @@
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
@@ -9511,6 +9522,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -9527,6 +9539,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -9773,6 +9786,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -10016,6 +10030,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -10864,6 +10879,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"dev": true,
"license": "MIT",
"peer": true
},
@@ -12437,6 +12453,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -12516,6 +12533,7 @@
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -12525,6 +12543,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -12709,6 +12728,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -12944,6 +12964,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -14224,6 +14245,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -14241,6 +14263,7 @@
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
@@ -14416,6 +14439,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -14454,6 +14478,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -14467,12 +14492,12 @@
}
},
"node_modules/serverless": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/serverless/-/serverless-4.17.0.tgz",
"integrity": "sha512-hoZmipwyN/h7y9HwkWGlJ0YT06RFq7WNOD7fFEiPfnSnnUMVTzeNHq2BRrUlpHhf5s9srCHDc2wx5I06acfq1Q==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/serverless/-/serverless-4.24.0.tgz",
"integrity": "sha512-bgxFQ6QyOGJC9IZjZIXo4m6bdWMl9I7HNZ4jrmwSpdePdsRd46igGRpSnhdYFOc71GNplhSOeoCibL94yCHfrg==",
"hasInstallScript": true,
"dependencies": {
"axios": "^1.8.3",
"axios": "^1.12.1",
"axios-proxy-builder": "^0.1.2",
"rimraf": "^5.0.5",
"xml2js": "0.6.2"
@@ -15012,6 +15037,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -15256,6 +15282,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"dev": true,
"license": "MIT",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
@@ -17031,6 +17058,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/write-file-atomic": {

View File

@@ -23,9 +23,7 @@
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"prisma:seed": "ts-node prisma/seed.ts",
"seeder": "tsx prisma/seed.ts",
"build:layer": "powershell -ExecutionPolicy Bypass -File ./build-prisma-layer.ps1",
"build:layer:unix": "chmod +x ./build-prisma-layer.sh && ./build-prisma-layer.sh"
"seeder": "tsx prisma/seed.ts"
},
"dependencies": {
"@aws-crypto/crc32c": "^5.2.0",
@@ -65,7 +63,7 @@
"prisma": "^7.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"serverless": "4.17.0",
"serverless": "4.24.0",
"swagger-ui-express": "^5.0.0",
"tslib": "^2.8.1",
"uuid": "^13.0.0",
@@ -94,6 +92,7 @@
"serverless-offline": "^14.4.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"swagger-ui-express": "^5.0.1",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",

View File

@@ -1,8 +1,8 @@
// prisma.ts
import { PrismaClient } from '@prisma/client';
// Re-export from the main singleton for consistency
import { prisma } from '../src/common/database/prisma.client';
// The DATABASE_URL environment variable will be automatically used
export const prisma = new PrismaClient();
export { prisma };
process.on('SIGINT', async () => {
await prisma.$disconnect();

View File

@@ -1,7 +1,7 @@
generator client {
provider = "prisma-client-js"
// No binaryTargets or previewFeatures needed - Prisma 7 uses JS-based driver adapters (no native engines)
// multiSchema and driverAdapters are now stable features
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Add Linux target
previewFeatures = ["multiSchema"]
}
datasource db {
@@ -66,6 +66,12 @@ model User {
friendOf Friends[] @relation("FriendUser")
userAddressDetails UserAddressDetails[]
userDocuments UserDocuments[]
activityTracks ActivityTrack[]
// 🔹 Activities created by this user
createdActivities Activities[] @relation("UserActivities")
// 🔹 Activities where this user is Account Manager
managedActivities Activities[] @relation("ActivityAccountManager")
@@map("users")
@@schema("usr")
@@ -393,24 +399,28 @@ model DocumentType {
}
model FoodCuisines {
id Int @id @default(autoincrement())
cuisineName String @unique @map("cuisine_name") @db.VarChar(30)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
id Int @id @default(autoincrement())
cuisineName String @unique @map("cuisine_name") @db.VarChar(30)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
activityCuisines ActivityCuisine[]
@@map("food_cuisines")
@@schema("mst")
}
model CompanyTypes {
id Int @id @default(autoincrement())
companyTypeName String @unique @map("company_type_name") @db.VarChar(30)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
id Int @id @default(autoincrement())
companyTypeName String @unique @map("company_type_name") @db.VarChar(100)
displayOrder Int @map("display_order")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
hostHeaders HostHeader[]
hostParents HostParent[]
@@map("company_types")
@@schema("mst")
@@ -430,13 +440,14 @@ model Amenities {
}
model FoodTypes {
id Int @id @default(autoincrement())
foodTypeName String @unique @map("food_type_name") @db.VarChar(30)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityFoodDetails ActivityFoodDetails[]
id Int @id @default(autoincrement())
foodTypeName String @unique @map("food_type_name") @db.VarChar(30)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityFoodCost ActivityFoodCost[]
activityFoodTypes ActivityFoodTypes[]
@@map("food_types")
@@schema("mst")
@@ -610,6 +621,7 @@ model EnergyLevels {
id Int @id @default(autoincrement())
energyLevelName String @map("energy_level_name") @db.VarChar(30)
energyIcon String @map("energy_icon") @db.VarChar(400)
energyColor String @map("energy_color") @db.VarChar(20)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -664,12 +676,13 @@ model HostHeader {
panNumber String? @map("pan_number") @db.VarChar(30)
gstNumber String? @map("gst_number") @db.VarChar(30)
formationDate DateTime? @map("formation_date")
companyType String? @map("company_type") @db.VarChar(30)
websiteUrl String? @map("website_url") @db.VarChar(50)
instagramUrl String? @map("instagram_url") @db.VarChar(80)
facebookUrl String? @map("facebook_url") @db.VarChar(80)
linkedinUrl String? @map("linkedin_url") @db.VarChar(80)
twitterUrl String? @map("twitter_url") @db.VarChar(80)
companyTypeXid Int? @map("company_type_xid")
companyTypes CompanyTypes? @relation(fields: [companyTypeXid], references: [id], onDelete: Restrict)
websiteUrl String? @map("website_url") @db.VarChar(250)
instagramUrl String? @map("instagram_url") @db.VarChar(250)
facebookUrl String? @map("facebook_url") @db.VarChar(250)
linkedinUrl String? @map("linkedin_url") @db.VarChar(250)
twitterUrl String? @map("twitter_url") @db.VarChar(250)
currencyXid Int? @map("currency_xid")
currencies Currencies? @relation(fields: [currencyXid], references: [id], onDelete: Restrict)
stepper Int? @default(1) @map("stepper")
@@ -691,6 +704,7 @@ model HostHeader {
amountPerBooking Int? @map("amount_per_booking")
payoutDurationNum Int? @map("payout_duration_num")
payoutDurationFrequency String? @map("payout_duration_frequency") @db.VarChar(20)
referencedBy String? @default("null") @map("referenced_by") @db.VarChar(100)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -780,17 +794,17 @@ model HostParent {
countries Countries? @relation(fields: [countryXid], references: [id], onDelete: Restrict)
pinCode String? @map("pin_code") @db.VarChar(30)
logoPath String? @map("logo_path") @db.VarChar(400)
isSubsidairy Boolean @default(false) @map("is_subsidairy")
registrationNumber String? @map("registration_number") @db.VarChar(30)
panNumber String? @map("pan_number") @db.VarChar(30)
gstNumber String? @map("gst_number") @db.VarChar(30)
formationDate DateTime? @map("formation_date")
companyType String? @map("company_type") @db.VarChar(30)
websiteUrl String? @map("website_url") @db.VarChar(80)
instagramUrl String? @map("instagram_url") @db.VarChar(80)
facebookUrl String? @map("facebook_url") @db.VarChar(80)
linkedinUrl String? @map("linkedin_url") @db.VarChar(80)
twitterUrl String? @map("twitter_url") @db.VarChar(80)
companyTypeXid Int? @map("company_type_xid")
companyTypes CompanyTypes? @relation(fields: [companyTypeXid], references: [id], onDelete: Restrict)
websiteUrl String? @map("website_url") @db.VarChar(250)
instagramUrl String? @map("instagram_url") @db.VarChar(250)
facebookUrl String? @map("facebook_url") @db.VarChar(250)
linkedinUrl String? @map("linkedin_url") @db.VarChar(250)
twitterUrl String? @map("twitter_url") @db.VarChar(250)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -838,38 +852,45 @@ model HostTrack {
// ACTIVITY MODELS
model Activities {
id Int @id @default(autoincrement())
hostXid Int @map("host_xid")
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
activityTypeXid Int @map("activity_type_xid")
activityType ActivityTypes @relation(fields: [activityTypeXid], references: [id], onDelete: Restrict)
frequenciesXid Int? @map("frequencies_xid")
frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict)
activityRefNumber String? @map("activity_ref_number") @db.VarChar(30)
activityTitle String? @map("activity_title") @db.VarChar(30)
activityDescription String? @map("activity_description") @db.VarChar(80)
checkInLat Float? @map("check_in_lat")
checkInLong Float? @map("check_in_long")
checkInAddress String? @map("check_in_address") @db.VarChar(150)
isCheckOutSame Boolean? @default(true) @map("is_check_out_same")
checkOutLat Float? @map("check_out_lat")
checkOutLong Float? @map("check_out_long")
checkOutAddress String? @map("check_out_address") @db.VarChar(150)
energyLevelXid Int? @map("energy_level_xid")
energyLevel EnergyLevels? @relation(fields: [energyLevelXid], references: [id], onDelete: Restrict)
activityDurationMins Int? @map("activity_duration_mins")
foodAvailable Boolean? @default(false) @map("food_available")
foodIsChargeable Boolean? @default(false) @map("food_is_chargeable")
alcoholAvailable Boolean? @default(false) @map("alcohol_available")
trainerAvailable Boolean? @default(false) @map("trainer_available")
trainerIsChargeable Boolean? @default(false) @map("trainer_is_chargeable")
pickUpDropAvailable Boolean? @default(false) @map("pick_up_drop_available")
pickUpDropIsChargeable Boolean? @default(false) @map("pick_up_drop_is_chargeable")
inActivityAvailable Boolean? @default(false) @map("in_activity_available")
inActivityIsChargeable Boolean? @default(false) @map("in_activity_is_chargeable")
equipmentAvailable Boolean? @default(false) @map("equipment_available")
equipmentIsChargeable Boolean? @default(false) @map("equipment_is_chargeable")
cancellationAvailable Boolean? @default(false) @map("cancellation_available")
id Int @id @default(autoincrement())
hostXid Int @map("host_xid")
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
activityTypeXid Int @map("activity_type_xid")
activityType ActivityTypes @relation(fields: [activityTypeXid], references: [id], onDelete: Restrict)
frequenciesXid Int? @map("frequencies_xid")
frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict)
activityRefNumber String? @map("activity_ref_number") @db.VarChar(30)
activityTitle String? @map("activity_title") @db.VarChar(30)
activityDescription String? @map("activity_description") @db.VarChar(255)
checkInLat Float? @map("check_in_lat")
checkInLong Float? @map("check_in_long")
checkInAddress String? @map("check_in_address") @db.VarChar(150)
isCheckOutSame Boolean? @default(true) @map("is_check_out_same")
checkOutLat Float? @map("check_out_lat")
checkOutLong Float? @map("check_out_long")
checkOutAddress String? @map("check_out_address") @db.VarChar(150)
energyLevelXid Int? @map("energy_level_xid")
energyLevel EnergyLevels? @relation(fields: [energyLevelXid], references: [id], onDelete: Restrict)
activityDurationMins Int? @map("activity_duration_mins")
foodAvailable Boolean? @default(false) @map("food_available")
foodIsChargeable Boolean? @default(false) @map("food_is_chargeable")
alcoholAvailable Boolean? @default(false) @map("alcohol_available")
trainerAvailable Boolean? @default(false) @map("trainer_available")
trainerIsChargeable Boolean? @default(false) @map("trainer_is_chargeable")
pickUpDropAvailable Boolean? @default(false) @map("pick_up_drop_available")
pickUpDropIsChargeable Boolean? @default(false) @map("pick_up_drop_is_chargeable")
inActivityAvailable Boolean? @default(false) @map("in_activity_available")
inActivityIsChargeable Boolean? @default(false) @map("in_activity_is_chargeable")
equipmentAvailable Boolean? @default(false) @map("equipment_available")
equipmentIsChargeable Boolean? @default(false) @map("equipment_is_chargeable")
cancellationAvailable Boolean? @default(false) @map("cancellation_available")
// 🔹 Creator / owner
userId Int?
user User? @relation("UserActivities", fields: [userId], references: [id])
// 🔹 Account Manager
accountManagerXid Int?
accountManager User? @relation("ActivityAccountManager", fields: [accountManagerXid], references: [id], onDelete: Restrict)
cancellationAllowedBeforeMins Int? @map("cancellation_allowed_before_mins")
currencyXid Int? @map("currency_xid")
currencies Currencies? @relation(fields: [currencyXid], references: [id], onDelete: Restrict)
@@ -892,18 +913,19 @@ model Activities {
ActivityEligibility ActivityEligibility[]
ActivitySuggestions ActivitySuggestions[]
ActivityAmDetails ActivityAmDetails[]
ActivityPrices ActivityPrices[]
ActivityVenueArtifacts ActivityVenueArtifacts[]
ActivityPQQheader ActivityPQQheader[]
ActivityAllowedEntry ActivityAllowedEntry[]
ActivityFoodDetails ActivityFoodDetails[]
ActivityFoodCost ActivityFoodCost[]
ActivityEquipments ActivityEquipments[]
ActivityNavigationModes ActivityNavigationModes[]
ActivityPickUpDetails ActivityPickUpDetails[]
ActivityAmenities ActivityAmenities[]
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
ScheduleHeader ScheduleHeader[]
ItineraryActivities ItineraryActivities[]
activityTracks ActivityTrack[]
activityFoodTypes ActivityFoodTypes[]
activityCuisines ActivityCuisine[]
activityPickUpTransports ActivityPickUpTransport[]
@@map("activities")
@@schema("act")
@@ -913,8 +935,7 @@ model ActivityOtherDetails {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
foodCuisines String? @map("food_cuisines") @db.VarChar(30)
exclusiveNotes String? @map("exclusive_notes") @db.VarChar(50)
exclusiveNotes String? @map("exclusive_notes") @db.VarChar(500)
dosNotes String? @map("dos_notes") @db.VarChar(200)
dontsNotes String? @map("donts_notes") @db.VarChar(200)
tipsNotes String? @map("tips_notes") @db.VarChar(100)
@@ -928,6 +949,25 @@ model ActivityOtherDetails {
@@schema("act")
}
model ActivityTrack {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
trackType String? @default("PQQ") @map("track_type")
updatedByRole String? @map("updated_by_role")
trackStatus String? @map("track_status")
updatedByXid Int? @map("updated_by_xid")
user User? @relation(fields: [updatedByXid], references: [id], onDelete: Cascade)
updatedOn DateTime? @map("updated_on")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("activity_track")
@@schema("act")
}
model ActivitiesMedia {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
@@ -961,6 +1001,8 @@ model ActivityVenues {
deletedAt DateTime? @map("deleted_at")
ScheduleHeader ScheduleHeader[]
ItineraryActivities ItineraryActivities[]
ActivityPrices ActivityPrices[] // <-- Added opposite relation
ActivityVenueArtifacts ActivityVenueArtifacts[] // <-- Added opposite relation
@@map("activity_venues")
@@schema("act")
@@ -1010,10 +1052,14 @@ model ActivityEligibility {
weightRestrictionName String? @map("weight_restriction_name") @db.VarChar(30)
weightEntered Int? @map("weight_entered")
weightIn String? @map("weight_in") @db.VarChar(30)
minWeight Int? @map("min_weight")
maxWeight Int? @map("max_weight")
isHeightRestriction Boolean @default(false) @map("is_height_restriction")
heightRestrictionName String? @map("height_restriction_name") @db.VarChar(30)
heightEntered Int? @map("height_entered")
heightIn String? @map("height_in") @db.VarChar(30)
minHeight Int? @map("min_height")
maxHeight Int? @map("max_height")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -1060,7 +1106,7 @@ model ActivityAmDetails {
model ActivityPrices {
id Int @id @default(autoincrement())
activityVenueXid Int @map("activity_venue_xid")
activityVenue Activities @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade)
activityVenue ActivityVenues @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade)
noOfSession Int @map("no_of_session")
isPackage Boolean @default(false) @map("is_package")
sessionValidity Int @map("session_validity")
@@ -1097,7 +1143,7 @@ model ActivityPriceTaxes {
model ActivityVenueArtifacts {
id Int @id @default(autoincrement())
activityVenueXid Int @map("activity_venue_xid")
activityVenue Activities @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade)
activityVenue ActivityVenues @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade)
mediaType String @map("media_type") @db.VarChar(30)
mediaFileName String @map("media_file_name") @db.VarChar(400)
isActive Boolean @default(true) @map("is_active")
@@ -1115,8 +1161,8 @@ model ActivityPQQheader {
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
pqqQuestionXid Int @map("pqq_question_xid")
pqqQuestions PQQQuestions @relation(fields: [pqqQuestionXid], references: [id], onDelete: Restrict)
pqqAnswerXid Int @map("pqq_answer_xid")
pqqAnswers PQQAnswers @relation(fields: [pqqAnswerXid], references: [id], onDelete: Restrict)
pqqAnswerXid Int? @map("pqq_answer_xid")
pqqAnswers PQQAnswers? @relation(fields: [pqqAnswerXid], references: [id], onDelete: Restrict)
comments String? @map("comments") @db.VarChar(200)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
@@ -1178,12 +1224,10 @@ model ActivityAllowedEntry {
@@schema("act")
}
model ActivityFoodDetails {
model ActivityFoodCost {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
foodTypeXid Int @map("food_type_xid")
foodType FoodTypes @relation(fields: [foodTypeXid], references: [id], onDelete: Restrict)
baseAmount Int @map("base_amount")
totalAmount Int @map("total_amount")
isActive Boolean @default(true) @map("is_active")
@@ -1191,23 +1235,55 @@ model ActivityFoodDetails {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityFoodTaxes ActivityFoodTaxes[]
foodTypes FoodTypes? @relation(fields: [foodTypesId], references: [id])
foodTypesId Int?
@@map("activity_food_details")
@@map("activity_food_cost")
@@schema("act")
}
model ActivityFoodTypes {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
foodTypeXid Int @map("food_type_xid")
foodType FoodTypes @relation(fields: [foodTypeXid], references: [id], onDelete: Restrict)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("activity_food_types")
@@schema("act")
}
model ActivityCuisine {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
foodCuisineXid Int @map("food_cuisine_xid")
foodCuisine FoodCuisines @relation(fields: [foodCuisineXid], references: [id], onDelete: Restrict)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("activity_cuisine")
@@schema("act")
}
model ActivityFoodTaxes {
id Int @id @default(autoincrement())
activityFoodDetailsXid Int @map("activity_food_details_xid")
activityFoodDetails ActivityFoodDetails @relation(fields: [activityFoodDetailsXid], references: [id], onDelete: Cascade)
taxXid Int @map("tax_xid")
taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
taxPer Float @map("tax_per")
taxAmount Int @map("tax_amount")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
id Int @id @default(autoincrement())
activityFoodCostXid Int @map("activity_food_cost_xid")
activityFoodCost ActivityFoodCost @relation(fields: [activityFoodCostXid], references: [id], onDelete: Cascade)
taxXid Int @map("tax_xid")
taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
taxPer Float @map("tax_per")
taxAmount Int @map("tax_amount")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("activity_food_taxes")
@@schema("act")
@@ -1225,6 +1301,7 @@ model ActivityEquipments {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
@@map("activity_equipments")
@@schema("act")
@@ -1233,7 +1310,7 @@ model ActivityEquipments {
model ActivityEquipmentTaxes {
id Int @id @default(autoincrement())
activityEquipmentXid Int @map("activity_equipment_xid")
activityEquipment Activities @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade)
activityEquipment ActivityEquipments @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade)
taxXid Int @map("tax_xid")
taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
taxPer Float @map("tax_per")
@@ -1284,54 +1361,57 @@ model ActivityNavigationModesTaxes {
}
model ActivityPickUpDetails {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
isPickUp Boolean @default(false) @map("is_pick_up")
locationLat Float? @map("location_lat")
locationLong Float? @map("location_long")
locationAddress String? @map("location_address") @db.VarChar(150)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityPickUpTransport ActivityPickUpTransport[]
@@map("activity_pick_up_details")
@@schema("act")
}
model ActivityPickUpTransport {
id Int @id @default(autoincrement())
activityPickUpDetailsXid Int @map("activity_pick_up_details_xid")
activityPickUpDetails ActivityPickUpDetails @relation(fields: [activityPickUpDetailsXid], references: [id], onDelete: Cascade)
transportModeXid Int @map("transport_mode_xid")
transportMode TransportModes @relation(fields: [transportModeXid], references: [id], onDelete: Restrict)
isTransportModeChargeable Boolean @default(false) @map("is_transport_mode_chargeable")
activityPickUpTransportXid Int @map("activity_pick_up_transport_xid")
activityPickUpTransport ActivityPickUpTransport @relation(fields: [activityPickUpTransportXid], references: [id], onDelete: Cascade)
isPickUp Boolean @default(false) @map("is_pick_up")
locationLat Float? @map("location_lat")
locationLong Float? @map("location_long")
locationAddress String? @map("location_address") @db.VarChar(150)
transportBasePrice Int @map("transport_base_price")
transportTotalPrice Int @map("transport_total_price")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityPickUpTransportTaxes ActivityPickUpTransportTaxes[]
activities Activities? @relation(fields: [activitiesId], references: [id])
activitiesId Int?
activityPickUpTransportTaxes ActivityPickUpTransportTaxes[]
@@map("activity_pick_up_details")
@@schema("act")
}
model ActivityPickUpTransport {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
transportModeXid Int @map("transport_mode_xid")
transportMode TransportModes @relation(fields: [transportModeXid], references: [id], onDelete: Restrict)
isTransportModeChargeable Boolean @default(false) @map("is_transport_mode_chargeable")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
pickupDetails ActivityPickUpDetails[]
@@map("activity_pick_up_transport")
@@schema("act")
}
model ActivityPickUpTransportTaxes {
id Int @id @default(autoincrement())
activityPickUpTransportXid Int @map("activity_pick_up_transport_xid")
activityPickUpTransport ActivityPickUpTransport @relation(fields: [activityPickUpTransportXid], references: [id], onDelete: Cascade)
taxXid Int @map("tax_xid")
taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
taxPer Float @map("tax_per")
taxAmount Int @map("tax_amount")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
id Int @id @default(autoincrement())
activityPickUpDetailsXid Int @map("activity_pick_up_details_xid")
activityPickUpDetails ActivityPickUpDetails @relation(fields: [activityPickUpDetailsXid], references: [id], onDelete: Cascade)
taxXid Int @map("tax_xid")
taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
taxPer Float @map("tax_per")
taxAmount Int @map("tax_amount")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("activity_pick_up_transport_taxes")
@@schema("act")

View File

@@ -1,6 +1,10 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import 'dotenv/config';
const prisma = new PrismaClient();
const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});
async function main() {
// ✅ Countries
@@ -29,11 +33,46 @@ async function main() {
update: {},
create: { countryXid: india.id, stateName: 'Maharashtra' },
});
const uttarpradesh = await prisma.states.upsert({
where: { stateName: 'Uttar-Pradesh' },
update: {},
create: { countryXid: india.id, stateName: 'Uttar-Pradesh' },
});
const Rajasthan = await prisma.states.upsert({
where: { stateName: 'Rajasthan' },
update: {},
create: { countryXid: india.id, stateName: 'Rajasthan' },
});
const Uttarakhand = await prisma.states.upsert({
where: { stateName: 'Uttarakhand' },
update: {},
create: { countryXid: india.id, stateName: 'Uttarakhand' },
});
const HimachalPradesh = await prisma.states.upsert({
where: { stateName: 'Himachal Pradesh' },
update: {},
create: { countryXid: india.id, stateName: 'Himachal Pradesh' },
});
const Gujrat = await prisma.states.upsert({
where: { stateName: 'Gujrat' },
update: {},
create: { countryXid: india.id, stateName: 'Gujrat' },
});
// ✅ Cities
await prisma.cities.createMany({
data: [
{ stateXid: maharashtra.id, cityName: 'Mumbai' },
{ stateXid: uttarpradesh.id, cityName: 'Azamgarh' },
{ stateXid: uttarpradesh.id, cityName: 'Lucknow' },
{ stateXid: uttarpradesh.id, cityName: 'Prayagraj' },
{ stateXid: Rajasthan.id, cityName: 'Jaipur' },
{ stateXid: Rajasthan.id, cityName: 'Jaisalmer' },
{ stateXid: Uttarakhand.id, cityName: 'Haridwar' },
{ stateXid: HimachalPradesh.id, cityName: 'Manali' },
{ stateXid: Gujrat.id, cityName: 'Surat' },
{ stateXid: Gujrat.id, cityName: 'Ahemdabad' },
{ stateXid: Gujrat.id, cityName: 'Rajkot' },
],
skipDuplicates: true,
});
@@ -52,6 +91,21 @@ async function main() {
update: {},
create: { countryXid: india.id, bankName: 'HDFC Bank' },
});
const indianBank = await prisma.banks.upsert({
where: { bankName: 'Indian Bank' },
update: {},
create: { countryXid: india.id, bankName: 'Indian Bank' },
});
const Kotak = await prisma.banks.upsert({
where: { bankName: 'Kotak Bank' },
update: {},
create: { countryXid: india.id, bankName: 'Kotak Bank' },
});
const BOI = await prisma.banks.upsert({
where: { bankName: 'Bank of India' },
update: {},
create: { countryXid: india.id, bankName: 'Bank of India' },
});
// ✅ Bank Branches
await prisma.bankBranches.createMany({
@@ -63,6 +117,27 @@ async function main() {
branchAddress: 'HDFC Fort Branch, Mumbai',
ifscCode: 'HDFC0001234',
},
{
bankXid: indianBank.id,
stateXid: maharashtra.id,
cityXid: (await prisma.cities.findFirst({ where: { cityName: 'Mumbai' } }))!.id,
branchAddress: 'Indian Bank Fort Branch, Mumbai',
ifscCode: 'IDIB0001234',
},
{
bankXid: Kotak.id,
stateXid: Uttarakhand.id,
cityXid: (await prisma.cities.findFirst({ where: { cityName: 'Haridwar' } }))!.id,
branchAddress: 'Kotak Fort Branch, Mumbai',
ifscCode: 'KTB0001234',
},
{
bankXid: BOI.id,
stateXid: uttarpradesh.id,
cityXid: (await prisma.cities.findFirst({ where: { cityName: 'Azamgarh' } }))!.id,
branchAddress: 'Bank of India Fort Branch, Mumbai',
ifscCode: 'BOI0001234',
},
],
skipDuplicates: true,
});
@@ -74,35 +149,51 @@ async function main() {
create: { interestName: 'Chill and Zen', displayOrder: 1 },
});
const sweatmode = await prisma.interests.upsert({
where: { interestName: 'Sweat Mode' },
where: { interestName: 'Sweat Mode On' },
update: {},
create: { interestName: 'Sweat Mode', displayOrder: 2 },
create: { interestName: 'Sweat Mode On', displayOrder: 2 },
});
const gameon = await prisma.interests.upsert({
where: { interestName: 'Game On' },
const trackracer = await prisma.interests.upsert({
where: { interestName: 'Track Racer' },
update: {},
create: { interestName: 'Game On', displayOrder: 3 },
create: { interestName: 'Track Racer', displayOrder: 3 },
});
const circuitracer = await prisma.interests.upsert({
where: { interestName: 'Circuit Racer' },
update: {},
create: { interestName: 'Circuit Racer', displayOrder: 4 },
});
const thermalGliding = await prisma.interests.upsert({
where: { interestName: 'Thermal Gliding' },
update: {},
create: { interestName: 'Thermal Gliding', displayOrder: 5 },
});
const partycentral = await prisma.interests.upsert({
where: { interestName: 'Party Central' },
update: {},
create: { interestName: 'Party Central', displayOrder: 4 },
create: { interestName: 'Party Central', displayOrder: 6 },
});
const artsy = await prisma.interests.upsert({
where: { interestName: 'Artsy' },
const aqua = await prisma.interests.upsert({
where: { interestName: 'Aqua' },
update: {},
create: { interestName: 'Artsy', displayOrder: 5 },
create: { interestName: 'Aqua', displayOrder: 7 },
});
const foodiediaries = await prisma.interests.upsert({
where: { interestName: 'Foodie Diaries' },
const foodie = await prisma.interests.upsert({
where: { interestName: 'Foodie' },
update: {},
create: { interestName: 'Foodie Diaries', displayOrder: 6 },
create: { interestName: 'Foodie', displayOrder: 8 },
});
await prisma.activityTypes.createMany({
data: [
{ interestXid: chillandzen.id, activityTypeName: 'Cricket' },
{ interestXid: chillandzen.id, activityTypeName: 'Football' },
{ interestXid: aqua.id, activityTypeName: 'Scuba-Diving' },
{ interestXid: sweatmode.id, activityTypeName: 'Cloudboarding' },
{ interestXid: partycentral.id, activityTypeName: 'Soaring Glider' },
{ interestXid: sweatmode.id, activityTypeName: 'Speedway Racer' },
{ interestXid: aqua.id, activityTypeName: 'Aerial Surfing' },
{ interestXid: foodie.id, activityTypeName: 'Wine Tasting' },
{ interestXid: trackracer.id, activityTypeName: 'Track Racer' },
{ interestXid: thermalGliding.id, activityTypeName: 'Thermal Gliding' },
],
skipDuplicates: true,
});
@@ -132,6 +223,58 @@ async function main() {
],
skipDuplicates: true, // prevents error if already seeded
});
// ✅ Energy Levels
await prisma.energyLevels.createMany({
data: [
{ energyLevelName: 'Low', energyIcon: '📶', energyColor: 'Red' },
{ energyLevelName: 'Medium', energyIcon: '📶', energyColor: 'Yellow' },
{ energyLevelName: 'High', energyIcon: '📶', energyColor: 'Green' },
],
skipDuplicates: true, // prevents error if already seeded
});
// ✅ Company types data
await prisma.companyTypes.upsert({
where: { companyTypeName: 'Proprietory' },
update: {},
create: { companyTypeName: 'Proprietory', displayOrder: 1 },
});
await prisma.companyTypes.upsert({
where: { companyTypeName: 'One Person Company' },
update: {},
create: { companyTypeName: 'One Person Company', displayOrder: 2 },
});
await prisma.companyTypes.upsert({
where: { companyTypeName: 'Limited Liability Partnership' },
update: {},
create: { companyTypeName: 'Limited Liability Partnership', displayOrder: 3 },
});
await prisma.companyTypes.upsert({
where: { companyTypeName: 'Partnership Firm' },
update: {},
create: { companyTypeName: 'Partnership Firm', displayOrder: 4 },
});
await prisma.companyTypes.upsert({
where: { companyTypeName: 'Private Limited' },
update: {},
create: { companyTypeName: 'Private Limited', displayOrder: 5 },
});
await prisma.companyTypes.upsert({
where: { companyTypeName: 'Non-Profit Organisation' },
update: {},
create: { companyTypeName: 'Non-Profit Organisation', displayOrder: 6 },
});
await prisma.companyTypes.upsert({
where: { companyTypeName: 'Public Limited' },
update: {},
create: { companyTypeName: 'Public Limited', displayOrder: 7 },
});
// ✅ Food Types
await prisma.foodTypes.createMany({

View File

@@ -1,15 +1,28 @@
service: minglar
useDotenv: true
params:
dev:
stage: dev
test:
stage: test
uat:
stage: uat
provider:
name: aws
runtime: nodejs22.x
region: ap-south-1
stage: ${opt:stage, 'dev'}
versionFunctions: false
memorySize: 512
# Apply Prisma layer to all functions
# Use the published layer version ARN (works for full deploy and `deploy function`)
# Reference the layer defined in this stack using CloudFormation Ref
layers:
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn}
# Use the exported stack output so deploy function works (expects a string ARN)
# For offline/local, fall back to an empty string so the CF lookup is optional.
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn, ''}
apiGateway:
binaryMediaTypes:
- '*/*'
@@ -42,6 +55,8 @@ provider:
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
HOST_LINK: ${env:HOST_LINK}
iam:
role:
@@ -59,6 +74,8 @@ provider:
custom:
serverless-offline:
reloadHandler: true
httpPort: 3000
noPrependStageInUrl: true
build:
esbuild:
@@ -67,14 +84,34 @@ build:
sourcemap: false
target: node20
platform: node
# Mark as external so they're not bundled into the JS
external:
# These are provided by the Prisma layer
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma/client'
- '.prisma'
- '@prisma/adapter-pg'
- 'pg'
- 'zod'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
# Exclude prevents npm install of these packages in the zip
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/adapter-pg'
- '@prisma/client'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'zod'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
# Define layers
layers:
@@ -90,6 +127,8 @@ package:
individually: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
- '!node_modules/.prisma/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
@@ -106,6 +145,8 @@ functions:
- ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/minglaradmin.yml)}
- ${file(./serverless/functions/prepopulate.yml)}
- ${file(./serverless/functions/pqq.yml)}
- ${file(./serverless/functions/swagger.yml)}
plugins:
- serverless-offline

View File

@@ -1,764 +0,0 @@
service: minglarDev
provider:
name: aws
runtime: nodejs22.x
region: ap-south-1
versionFunctions: false
memorySize: 512 # Default memory for all functions (can be overridden per function)
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
environment:
DATABASE_URL: ${env:DATABASE_URL}
DB_USERNAME: ${env:DB_USERNAME}
DB_PASSWORD: ${env:DB_PASSWORD}
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
DB_HOSTNAME: ${env:DB_HOSTNAME}
DB_PORT: ${env:DB_PORT}
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
BYPASS_OTP: ${env:BYPASS_OTP}
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
JWT_SECRET: ${env:JWT_SECRET}
SALT_ROUNDS: ${env:SALT_ROUNDS}
NODE_ENV: ${env:NODE_ENV}
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
iam:
role:
statements:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
custom:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node20
platform: node
concurrency: 5
external:
- '@prisma/client'
- '.prisma'
exclude:
- 'aws-sdk'
package:
individually: true
patterns:
- '!node_modules/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!*.config.js'
- '!.git/**'
- '!.github/**'
functions:
getHosts:
handler: src/modules/host/handlers/host.handler
memorySize: 384 # Lower memory for simple GET operations
package:
patterns:
- 'src/modules/host/handlers/host.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host
method: get
verifyOtp:
handler: src/modules/host/handlers/verifyOtp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/verifyOtp.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/verify-otp
method: post
loginForHost:
handler: src/modules/host/handlers/loginForHost.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/loginForHost.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/login
method: post
registrationOfHost:
handler: src/modules/host/handlers/registration.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/registration.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/registration
method: post
createPasswordForHost:
handler: src/modules/host/handlers/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/createPassword.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/create-password
method: post
addPaymentDetailsForHost:
handler: src/modules/host/handlers/addPaymentDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addPaymentDetails.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/add-payment-details
method: post
addActivity:
handler: src/modules/host/handlers/addActivity.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addActivity.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/add-activity
method: post
getHostById:
handler: src/modules/host/handlers/getbyidhandler.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getbyidhandler.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/getById
method: get
getPQQQuestionDetailsById:
handler: src/modules/host/handlers/getByIdPQQ.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getByIdPQQ.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/get-pqq-question-details
method: get
getLatestPQQQuestionDetails:
handler: src/modules/host/handlers/getLatestQuestionDetailsPQQ.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getLatestQuestionDetailsPQQ.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/get-latest-pqq-question-details
method: get
getActivityTypes:
handler: src/modules/host/handlers/getActivity.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getActivity.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/get-activity
method: get
acceptMinglarAgreement:
handler: src/modules/host/handlers/acceptAgreement.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/acceptAgreement.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/accept-agreement
method: patch
getStepperInfo:
handler: src/modules/host/handlers/getStepper.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getStepper.*'
- 'src/common/utils/handlers/safeHandler.*'
- 'src/common/database/**'
- 'src/modules/host/services/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /stepper
method: get
getSuggestion:
handler: src/modules/minglaradmin/handlers/getSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-suggestion
method: get
minglarRegistration:
handler: src/modules/minglaradmin/handlers/registration.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/registration
method: post
minglarLoginForAdmin:
handler: src/modules/minglaradmin/handlers/loginForMinglar.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/login
method: post
minglarCreatePassword:
handler: src/modules/minglaradmin/handlers/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/create-password
method: post
# Functions using AWS SDK - KEEP AS IS with higher memory
updateMinglarProfile:
handler: src/modules/minglaradmin/handlers/updateProfile.handler
memorySize: 512 # Higher memory for AWS SDK operations
timeout: 30
package:
patterns:
- 'src/modules/minglaradmin/handlers/updateProfile.*'
- 'src/modules/minglaradmin/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
- 'node_modules/@aws-sdk/**'
- 'node_modules/@smithy/**'
- 'node_modules/tslib/**'
- 'node_modules/fast-xml-parser/**'
events:
- httpApi:
path: /minglaradmin/update-profile
method: patch
prepopulateTeammate:
handler: src/modules/minglaradmin/handlers/prepopulateTeammate.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/prepopulate-Roles
method: get
inviteTeammate:
handler: src/modules/minglaradmin/handlers/inviteTeammate.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/invite-teammate
method: post
getAllHostApplication:
handler: src/modules/minglaradmin/handlers/getAllHostApplication.handler
memorySize: 512 # Higher memory for data-intensive operations
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-host-applications-am
method: get
getAllOnboardingHostApplications:
handler: src/modules/minglaradmin/handlers/getAllOnboardingHosts.handler
memorySize: 512 # Higher memory for data-intensive operations
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-host-applications-admin
method: get
getAllOnboardingHostApplications_New:
handler: src/modules/minglaradmin/handlers/getOnboardingNewApplications.handler
memorySize: 512 # Higher memory for data-intensive operations
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-host-applications-admin-new
method: get
getAllInvitationDetails:
handler: src/modules/minglaradmin/handlers/getAllInvitationDetails.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-invitation-details
method: get
addSuggestion:
handler: src/modules/minglaradmin/handlers/addSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/add-suggestion
method: post
getAllCoadminAndAMDetails:
handler: src/modules/minglaradmin/handlers/getAllCoadminAndAM.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-coadmin-and-am-details
method: get
getAllInvitedCoadminAndAMDetails:
handler: src/modules/minglaradmin/handlers/getAllInvitedCoadminAndAM.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-invited-coadmin-and-am
method: get
getAllBankAndCurrencyDetails:
handler: src/modules/prepopulate/handlers/getAllBankDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-all-bank-currency-details
method: get
getCityByState:
handler: src/modules/prepopulate/handlers/getCityByState.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/handlers/getCityByState.*'
- 'src/modules/prepopulate/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-city-by-state
method: get
getBranchByBankXid:
handler: src/modules/prepopulate/handlers/getBranchByBank.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/handlers/getBranchByBank.*'
- 'src/modules/prepopulate/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-branch-by-bank
method: get
getAllDocumentCountryStateCityDetails:
handler: src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-all-doc-country
method: get
getAllPqqQuesAns:
handler: src/modules/prepopulate/handlers/getAllPQQQuesWithAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-all-pqq-ques-ans
method: get
getFrequenciesOfActivity:
handler: src/modules/prepopulate/handlers/getAllFrequencies.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-all-Frequencies
method: get
assignAMToHost:
handler: src/modules/minglaradmin/handlers/assignAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/assign-am-to-host
method: patch
editAgreementDetails:
handler: src/modules/minglaradmin/handlers/editAgreementDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/edit-agreement-details
method: patch
acceptHostApplication:
handler: src/modules/minglaradmin/handlers/acceptHostApplication.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/accept-host-application
method: patch
acceptHostApplicationMinglar:
handler: src/modules/minglaradmin/handlers/acceptHostAppMinglar.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/accept-host-application-minglar
method: patch
rejectHostApplication:
handler: src/modules/minglaradmin/handlers/rejectHostApplication.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/reject-host-application
method: patch
rejectHostApplicationAM:
handler: src/modules/minglaradmin/handlers/rejectHostApplicationAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/reject-host-application-am
method: patch
# Functions using AWS SDK and S3 - KEEP AS IS with higher memory
addCompanyDetails:
handler: src/modules/host/handlers/addCompanyDetails.handler
memorySize: 512
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/addCompanyDetails.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
# Only include specific AWS SDK modules needed for S3
- 'node_modules/@aws-sdk/client-s3/**'
- 'node_modules/@aws-sdk/s3-request-presigner/**'
- 'node_modules/@aws-sdk/types/**'
- 'node_modules/@aws-sdk/middleware-logger/**'
- 'node_modules/@aws-sdk/util-utf8-node/**'
- 'node_modules/@aws-sdk/util-utf8-browser/**'
- 'node_modules/@smithy/**'
- 'node_modules/tslib/**'
# Remove these large/unnecessary packages:
- 'node_modules/fast-xml-parser/**' # Remove if not used
- 'node_modules/lambda-multipart-parser/**' # You're using busboy directly
- 'node_modules/busboy/**'
# Remove these AWS utility packages (included in main SDK):
- 'node_modules/@aws-crypto/**'
# - 'node_modules/uuid/**' # AWS SDK includes its own
# - 'node_modules/@aws/util-uri-escape/**'
# - 'node_modules/@aws/util-middleware/**'
- 'node_modules/@aws/smithy-client/**'
# - 'node_modules/@aws/lambda-invoke-store/**'
events:
- httpApi:
path: /host/add-company-details
method: patch
submitPqqAnswer:
handler: src/modules/host/handlers/submitPqqAns.handler
memorySize: 512
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/submitPqqAns.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
- 'node_modules/@aws-sdk/**'
- 'node_modules/@smithy/**'
- 'node_modules/tslib/**'
- 'node_modules/fast-xml-parser/**'
- 'node_modules/lambda-multipart-parser/**'
- 'node_modules/busboy/**'
- 'node_modules/@aws-crypto/**'
- 'node_modules/uuid/**'
- 'node_modules/@aws/util-uri-escape/**'
- 'node_modules/@aws/util-middleware/**'
- 'node_modules/@aws/smithy-client/**'
- 'node_modules/@aws/lambda-invoke-store/**'
events:
- httpApi:
path: /host/submit-pqq-ans
method: patch
submitFinalPqqAnswer:
handler: src/modules/host/handlers/getPQQScore.handler
memorySize: 512
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/getPQQScore.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
- 'node_modules/@aws-sdk/**'
- 'node_modules/@smithy/**'
- 'node_modules/tslib/**'
- 'node_modules/fast-xml-parser/**'
- 'node_modules/lambda-multipart-parser/**'
- 'node_modules/busboy/**'
- 'node_modules/@aws-crypto/**'
- 'node_modules/uuid/**'
- 'node_modules/@aws/util-uri-escape/**'
- 'node_modules/@aws/util-middleware/**'
- 'node_modules/@aws/smithy-client/**'
- 'node_modules/@aws/lambda-invoke-store/**'
events:
- httpApi:
path: /host/submit-final-pqq-ans
method: patch
addPQQSuggestion:
handler: src/modules/minglar/handlers/addPQQSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/addPQQSuggestion.*'
- 'src/modules/minglaradmin/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/**'
events:
- httpApi:
path: /minglar/add-Pqq-suggestion
method: post

View File

@@ -1,680 +1,370 @@
# Host Module Functions
# All authentication and host management endpoints
getHosts:
handler: src/modules/host/handlers/host.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/host.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host
method: get
verifyOTP:
handler: src/modules/host/handlers/Host_Admin/onboarding/verifyOTP.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/verifyOtp.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/verify-otp
method: post
login:
handler: src/modules/host/handlers/Host_Admin/onboarding/login.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/loginForHost.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/login
method: post
signUp:
handler: src/modules/host/handlers/Host_Admin/onboarding/signUp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/registration.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/registration
method: post
createPassword:
handler: src/modules/host/handlers/Host_Admin/onboarding/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/createPassword.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/create-password
method: post
updateBankDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addPaymentDetails.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/add-payment-details
method: post
saveActivity_ForPQQ:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/saveActivity_ForPQQ.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addActivity.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/add-activity
method: post
getHostById:
handler: src/modules/host/handlers/getbyidhandler.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getbyidhandler.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/getById
method: get
getPQQ_ByQuestionId:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_ByQuestionId.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getByIdPQQ.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-pqq-question-details
method: get
getPQQ_LastUpdatedQuestion:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_LastUpdatedQuestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getLatestQuestionDetailsPQQ.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-latest-pqq-question-details
method: get
getAllActivityType:
prePopulateNewActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllActivityType.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getActivityType.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-activity-type
path: /host/Activity_Hub/OnBoarding/prepopulate-new-activity
method: get
createNewActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.handler
memorySize: 1024
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/create-new-activity
method: patch
showSuggestion:
handler: src/modules/host/handlers/Host_Admin/onboarding/showSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Host_Admin/onboarding/showSuggestion.handler.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/get-suggestion
method: get
getAllHostActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllHostActivity.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/getAllHostActivity.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-all-host-activity
method: get
acceptAggrement:
handler: src/modules/host/handlers/Host_Admin/onboarding/acceptAggrement.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/acceptAgreement.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/accept-agreement
method: patch
getStepperInfo:
handler: src/modules/host/handlers/getStepper.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getStepper.*'
- 'src/common/utils/handlers/safeHandler.*'
- 'src/common/database/**'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /stepper
method: get
# Functions with S3/AWS SDK dependencies
submitCompanyDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
memorySize: 1024
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/addCompanyDetails.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/add-company-details
method: patch
submitPQQ_Answer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQ_Answer.handler
memorySize: 1024
package:
patterns:
- 'src/modules/host/handlers/submitPqqAns.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-pqq-answer
method: patch
updatePQQ_LastAnswer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQScore.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/submitPqqAns.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer
method: post
getAllPQQwithSubmittedAns:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
memorySize: 512
submitPQQForReview:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/**'
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-all-pqq-ques-submited-ans
method: get
updateSuggestionAsReviewed:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/updateSuggestionAsReviewed.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/update-suggestion-reviewed
path: /host/Activity_Hub/OnBoarding/submit-pqq-for-review
method: patch
getAllPQQwithSubmittedAns:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/prepopulate/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-all-pqq-ques-submited-ans
method: get
updateSuggestionAsReviewed:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/updateSuggestionAsReviewed.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/update-suggestion-reviewed
method: patch
resendOTPmail:
handler: src/modules/host/handlers/resendOtp.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/resendOtp/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /resend-otp
method: post

View File

@@ -1,8 +1,6 @@
# Minglar Admin Module Functions
# Admin dashboard and management endpoints
minglarRegistration:
handler: src/modules/minglaradmin/handlers/registration.handler
memorySize: 384
@@ -80,7 +78,6 @@ prepopulateRole:
path: /minglaradmin/prepopulate-Roles
method: get
getHostDetailsById:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/getByIdHostDetails.handler
memorySize: 384
@@ -267,8 +264,8 @@ assignAMToHost:
path: /minglaradmin/hosthub/onboarding/assign-am
method: patch
editAgreementDetails:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/editAgreementDetails.handler
editAgreementDetailsAndAccept:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/editAgreementDetailsAndAccept.handler
memorySize: 384
package:
patterns:
@@ -280,9 +277,24 @@ editAgreementDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/edit-agreement
path: /minglaradmin/hosthub/onboarding/edit-agreement-accept-host
method: patch
getAllPqqQuesAnsForAM:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-pqq-ques-ans-for-am
method: get
acceptHostApplication:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.handler
memorySize: 384
@@ -312,15 +324,15 @@ RejectPQQByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-pqq-by-am
path: /minglaradmin/hosthub/hosts/reject-pq-by-am
method: patch
acceptHostApplicationMinglar:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/acceptHostAppMinglar.handler
acceptPQByAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
@@ -328,7 +340,7 @@ acceptHostApplicationMinglar:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/accept-host-application-minglar
path: /minglaradmin/hosthub/hosts/accept-pq-by-am
method: patch
rejectHostApplication:
@@ -345,7 +357,7 @@ rejectHostApplication:
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/reject-host-application
method: post
method: patch
rejectHostApplicationAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.handler
@@ -361,7 +373,7 @@ rejectHostApplicationAM:
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-host-application-am
method: post
method: patch
addPQQSuggestion:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/addPQQSuggestion.handler
@@ -378,3 +390,36 @@ addPQQSuggestion:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-Pqq-suggestion
method: post
getAllPQPDetailsForAM:
handler: src/modules/minglaradmin/handlers/hosthub/pqp/getAllPQPDetailsForAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/pqp/getAllPQPDetailsForAM**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/pqp/pqp-details-for-am/{activityXid}
method: get
getSuggestionsForAM:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/show-suggestion-to-am/{hostXid}
method: get

View File

@@ -0,0 +1,29 @@
createActivityAndAllQuestionsEntry:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/createActivityAndAllQuestionsEntry.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/createActivityAndAllQuestionsEntry**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/create-activity
method: post
submitPQAnswer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQAnswer.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQAnswer**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-pq-answer
method: patch

View File

@@ -92,3 +92,18 @@ getFrequenciesOfActivity:
- httpApi:
path: /prepopulate/get-all-Frequencies
method: get
getAddActivityPrePopulate:
handler: src/modules/prepopulate/handlers/getAddActivityPrePopulate.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /prepopulate/get-add-activity-prepopulate
method: get

View File

@@ -0,0 +1,20 @@
# Swagger Documentation Functions
swaggerUi:
handler: src/handlers/swagger.swaggerUi
memorySize: 256
events:
- httpApi:
path: /api-docs
method: get
swaggerJson:
handler: src/handlers/swagger.swaggerJson
memorySize: 256
package:
patterns:
- 'swagger.json'
events:
- httpApi:
path: /swagger.json
method: get

View File

@@ -1,8 +1,9 @@
# Base packaging patterns shared across all functions
# Note: Prisma 7 uses driver adapters (no binary engines) - everything is in the layer
pattern1: 'src/common/**'
pattern2: 'common/**'
# Prisma packages are now provided by the layer, no need to include in function package
# REMOVED: Prisma is now provided by Lambda layer - DO NOT include in function packages
# pattern3: 'node_modules/@prisma/client/**'
# pattern4: 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
pattern3: '!node_modules/@prisma/**'
pattern4: '!node_modules/.prisma/**'
pattern5: '!node_modules/pg/**'
pattern5: '!node_modules/.prisma/client/libquery_engine*'

View File

@@ -1,11 +1,29 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
// Singleton pattern for Prisma client - prevents "Too many database connections" error
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
});
function createPrismaClient() {
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
return new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
});
}
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
// For serverless environments, always cache the client
if (process.env.IS_OFFLINE || process.env.AWS_LAMBDA_FUNCTION_NAME) {
globalForPrisma.prisma = prisma;
}

View File

@@ -0,0 +1,5 @@
// Re-export the singleton prisma client for Lambda handlers
// This ensures all Lambda functions use the same cached connection
import { prisma } from './prisma.client';
export const prismaClient = prisma;

View File

@@ -1,5 +1,5 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { PrismaService } from './prisma.lambda.service';
@Global()
@Module({

View File

@@ -1,15 +1,13 @@
import { Injectable, OnModuleInit, OnModuleDestroy, INestApplication } from '@nestjs/common';
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { prisma } from './prisma.client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
super({
adapter,
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
});
super();
// Use the singleton instance
Object.assign(this, prisma);
}
async onModuleInit() {
@@ -19,10 +17,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
async onModuleDestroy() {
await this.$disconnect();
}
async enableShutdownHooks(app: INestApplication) {
process.on('beforeExit', async () => {
await app.close();
});
}
}

View File

@@ -49,6 +49,17 @@ export async function verifyHostToken(token: string): Promise<{ id: number; role
include: { role: true },
});
const latestToken = await prisma.token.findFirst({
where: {
userXid: userId
},
orderBy: { id: 'desc' }
})
if (latestToken.isBlackListed == true) {
throw new ApiError(401, "This session is expired. Please login.")
}
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
@@ -104,12 +115,12 @@ const verifyCallback = async (
*/
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForHost;

View File

@@ -49,6 +49,17 @@ export async function verifyMinglarAdminToken(token: string): Promise<{ id: numb
include: { role: true },
});
const latestToken = await prisma.token.findFirst({
where: {
userXid: userId
},
orderBy: { id: 'desc' }
})
if (latestToken.isBlackListed == true) {
throw new ApiError(401, "This session is expired. Please login.")
}
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
@@ -105,12 +116,12 @@ const verifyCallback = async (
*/
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForHost;

View File

@@ -51,6 +51,17 @@ export async function verifyMinglarAdminHostToken(token: string): Promise<{ id:
include: { role: true },
});
const latestToken = await prisma.token.findFirst({
where: {
userXid: userId
},
orderBy: { id: 'desc' }
})
if (latestToken.isBlackListed == true) {
throw new ApiError(401, "This session is expired. Please login.")
}
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}

View File

@@ -49,6 +49,17 @@ export async function verifyOnlyMinglarAdminToken(token: string): Promise<{ id:
include: { role: true },
});
const latestToken = await prisma.token.findFirst({
where: {
userXid: userId
},
orderBy: { id: 'desc' }
})
if (latestToken.isBlackListed == true) {
throw new ApiError(401, "This session is expired. Please login.")
}
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
@@ -105,12 +116,12 @@ const verifyCallback = async (
*/
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForHost;

View File

@@ -50,6 +50,17 @@ export async function verifyUserToken(token: string): Promise<{ id: number; role
include: { role: true },
});
const latestToken = await prisma.token.findFirst({
where: {
userXid: userId
},
orderBy: { id: 'desc' }
})
if (latestToken.isBlackListed == true) {
throw new ApiError(401, "This session is expired. Please login.")
}
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
@@ -105,12 +116,12 @@ const verifyCallback = async (
*/
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForHost;

View File

@@ -1,72 +1,100 @@
export const HOST_STATUS_INTERNAL = {
HOST_SUBMITTED: "Host Submitted",
HOST_TO_UPDATE: "Host To Update",
REJECTED: "Rejected",
APPROVED: "Approved",
DRAFT: "Draft",
}
HOST_SUBMITTED: 'Host Submitted',
HOST_TO_UPDATE: 'Host To Update',
REJECTED: 'Rejected',
APPROVED: 'Approved',
DRAFT: 'Draft',
};
export const HOST_STATUS_DISPLAY = {
DRAFT: "Draft",
UNDER_REVIEW: "Under Review",
ENHANCING: "Enhancing",
REJECTED: "Rejected",
APPROVED: "Approved",
}
DRAFT: 'Draft',
UNDER_REVIEW: 'Under Review',
ENHANCING: 'Enhancing',
REJECTED: 'Rejected',
APPROVED: 'Approved',
};
export const STEPPER = {
NOT_SUBMITTED: 1,
UNDER_REVIEW: 2,
COMPANY_DETAILS_APPROVED: 3,
BANK_DETAILS_UPDATED: 4,
AGREEMENT_ACCEPTED: 5,
REJECTED: 6
}
export const LAST_QUESTION_ID = {
Q_ID: 55
}
NOT_SUBMITTED: 1,
UNDER_REVIEW: 2,
COMPANY_DETAILS_APPROVED: 3,
BANK_DETAILS_UPDATED: 4,
AGREEMENT_ACCEPTED: 5,
REJECTED: 6,
};
export const ACTIVITY_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed',
PQQ_TO_UPDATE: 'PQ To Update',
PQQ_SUBMITTED: 'PQ Submitted'
}
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
PQ_TO_UPDATE: 'PQ To Update',
PQ_SUBMITTED: 'PQ Submitted',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_SUBMITTED: 'Activity Submitted',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_REJECTED: 'Activity Rejected',
ACTIVITY_APPROVED: 'Activity Approved',
ACTIVITY_LISTED: 'Activity Listed',
ACTIVITY_UNLISTED: 'Activity Un Listed By Host',
};
export const ACTIVITY_DISPLAY_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed',
ENHANCING: 'Enchancing',
PQ_IN_REVIEW: 'PQ In Review'
}
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing',
PQ_IN_REVIEW: 'PQ In Review',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_IN_REVIEW: 'In Review',
ACTIVITY_TO_REVIEW: 'To Review',
ACTIVITY_NOT_LISTED: 'Not Listed',
ACTIVITY_LISTED: 'Listed',
ACTIVITY_UNLISTED: 'Un Listed',
};
export const ACTIVITY_AM_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed',
PQQ_REJECTED: 'PQ Rejected',
PQQ_TO_REVIEW: 'PQ To Review'
}
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
PQ_REJECTED: 'PQ Rejected',
PQ_TO_REVIEW: 'PQ To Review',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_REJECTED: 'Activity Rejected',
ACTIVITY_APPROVED: 'Activity Approved',
ACTIVITY_LISTED: 'Activity Listed',
};
export const ACTIVITY_AM_DISPLAY_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed',
ENHANCING: 'Enchancing',
NEW: 'New'
}
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing',
NEW: 'New',
PQ_APPROVED: 'PQ Approved',
REVISED: 'Revised',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_NEW: 'To Review',
ACTIVITY_ENHANCING: 'Enhancing',
ACTIVITY_NOT_LISTED: 'Not Listed',
ACTIVITY_LISTED: 'Listed',
};

View File

@@ -25,23 +25,38 @@ export const MINGLAR_INVITATION_STATUS = {
INVITED: 'Invited',
};
export const HOST_SUGGESTION_TITLES = {
COMPANY_DETAILS: 'Complete Details',
COMPANY_DOCUMENTATION: 'Company documentataion',
COMPANY_SOCIAL_PROOF: 'Social Proof',
ACTIVITY_INFORMATION: 'Activity Information',
ACTIVITY_LOCATION: 'Activity Location',
PICKUP_DROP_LOCATION: 'Pickup-Drop Location',
NUMBER_OF_PEOPLE: 'Number of People',
INCLUSION: 'Inclusion',
TAX_SETUP: 'Tax Setup',
ENERGY_LEVEL: 'Energy Level',
ELIGIBILITY_CRITERIA: 'Eligibility Criteria',
AMENITIES: 'Amenities',
EXLUSIVE_NOTES: 'Exclusive Notes',
CANCELLATION_POLICY: 'Cancellation Policy',
DOs_AND_DONTs: 'Dos and Donts',
TIPS_FOR_USERS: 'Tips for Users',
SUSTAINABILITY: 'Sustainability',
TERMS_AND_CONDITION_FOR_USER: 'Terms and Conditions for User'
};
export const ACTIVITY_TRACK_TYPE = {
PQ: 'PQ',
ACTIVITY: 'Activity'
}
export const ACTIVITY_TRACK_STATUS = {
REJECTED_BY_AM: 'Rejected By AM',
ACCEPTED_BY_AM: 'Accepted By AM',
ENHANCING: 'Enhancing',
PQ_SUBMITTED: 'PQ Submitted',
UNDER_REVIEW:'Under Review',
SUBMITTED:'Activity Submitted',
DRAFT:'Activity Draft'
}
// export const HOST_SUGGESTION_TITLES = {
// COMPANY_DETAILS: 'Complete Details',
// COMPANY_DOCUMENTATION: 'Company documentataion',
// COMPANY_SOCIAL_PROOF: 'Social Proof',
// ACTIVITY_INFORMATION: 'Activity Information',
// ACTIVITY_LOCATION: 'Activity Location',
// PICKUP_DROP_LOCATION: 'Pickup-Drop Location',
// NUMBER_OF_PEOPLE: 'Number of People',
// INCLUSION: 'Inclusion',
// TAX_SETUP: 'Tax Setup',
// ENERGY_LEVEL: 'Energy Level',
// ELIGIBILITY_CRITERIA: 'Eligibility Criteria',
// AMENITIES: 'Amenities',
// EXLUSIVE_NOTES: 'Exclusive Notes',
// CANCELLATION_POLICY: 'Cancellation Policy',
// DOs_AND_DONTs: 'Dos and Donts',
// TIPS_FOR_USERS: 'Tips for Users',
// SUSTAINABILITY: 'Sustainability',
// TERMS_AND_CONDITION_FOR_USER: 'Terms and Conditions for User'
// };

View File

@@ -12,7 +12,6 @@ export interface OtpResult {
export async function resendOtpHelper(
prisma: any,
userId: number,
email: string,
emailPurpose: "Register" | "Login" | "ForgotPassword",
otpLength: 4 | 6 = 4,
expiryMinutes: number = 5

View File

@@ -1,140 +0,0 @@
/**
* Host Activity Validation Schemas
* Production-ready Zod validations for host activity management
*/
import { z } from 'zod';
import { idSchema, optionalIdSchema, searchQuerySchema, paginationSchema } from '../validation.utils';
// ============================================
// CREATE ACTIVITY (FOR PQQ)
// ============================================
/**
* Create activity schema
*/
export const createActivitySchema = z.object({
activityTypeXid: idSchema.describe('Activity type ID'),
frequenciesXid: optionalIdSchema.describe('Frequency ID'),
});
export type CreateActivityInput = z.infer<typeof createActivitySchema>;
// ============================================
// GET ACTIVITY TYPE
// ============================================
/**
* Get all activity types query params
*/
export const getActivityTypeQuerySchema = z.object({
interestXid: z.coerce
.number()
.int('Interest ID must be an integer')
.positive('Interest ID must be positive')
.optional(),
});
export type GetActivityTypeQuery = z.infer<typeof getActivityTypeQuerySchema>;
// ============================================
// GET PQQ BY QUESTION ID
// ============================================
/**
* Get PQQ by question ID query params
*/
export const getPqqByQuestionIdQuerySchema = z.object({
question_xid: z.coerce
.number()
.int('Question ID must be an integer')
.positive('Question ID must be positive'),
activity_xid: z.coerce
.number()
.int('Activity ID must be an integer')
.positive('Activity ID must be positive'),
});
export type GetPqqByQuestionIdQuery = z.infer<typeof getPqqByQuestionIdQuerySchema>;
// ============================================
// SUBMIT PQQ ANSWER
// ============================================
/**
* Submit PQQ answer schema
*/
export const submitPqqAnswerSchema = z.object({
activityXid: idSchema.describe('Activity ID'),
questionXid: idSchema.describe('Question ID'),
answerXid: idSchema.describe('Answer ID'),
// For file uploads, these are handled separately
documentPath: z.string().max(500).optional(),
remarks: z.string().max(500, 'Remarks cannot exceed 500 characters').optional(),
});
export type SubmitPqqAnswerInput = z.infer<typeof submitPqqAnswerSchema>;
// ============================================
// UPDATE SUGGESTION AS REVIEWED
// ============================================
/**
* Update suggestion as reviewed schema
*/
export const updateSuggestionReviewedSchema = z.object({
activityPqqHeaderXid: idSchema.describe('Activity PQQ Header ID'),
activityPQQSuggestionId: idSchema.optional().describe('Activity PQQ Suggestion ID'),
});
export type UpdateSuggestionReviewedInput = z.infer<typeof updateSuggestionReviewedSchema>;
// ============================================
// GET ALL HOST ACTIVITY
// ============================================
/**
* Get all host activities query params
*/
export const getAllHostActivityQuerySchema = z.object({
hostXid: z.coerce
.number()
.int('Host ID must be an integer')
.positive('Host ID must be positive')
.optional(),
status: z
.enum(['pending', 'approved', 'rejected', 'draft'])
.optional(),
...paginationSchema.shape,
});
export type GetAllHostActivityQuery = z.infer<typeof getAllHostActivityQuerySchema>;
// ============================================
// GET LATEST QUESTION (activity_xid query param)
// ============================================
/**
* Get latest PQQ question query params
*/
export const getLatestPqqQuestionQuerySchema = z.object({
activity_xid: z.coerce
.number()
.int('Activity ID must be an integer')
.positive('Activity ID must be positive'),
});
export type GetLatestPqqQuestionQuery = z.infer<typeof getLatestPqqQuestionQuerySchema>;
// ============================================
// SEARCH QUERY (optional)
// ============================================
/**
* Optional search query params
*/
export const optionalSearchQuerySchema = z.object({
search: searchQuerySchema,
q: searchQuerySchema,
});
export type OptionalSearchQuery = z.infer<typeof optionalSearchQuerySchema>;

View File

@@ -1,27 +1,35 @@
/**
* Host Bank Details Validation Schema
* Production-ready Zod validation for payment/bank details
*/
import { z } from 'zod';
import { idSchema, ifscCodeSchema, accountNumberSchema } from '../validation.utils';
// validations/hostBankDetails.validation.ts
import { z } from "zod";
export const hostBankDetailsSchema = z.object({
accountNumber: accountNumberSchema,
accountNumber: z
.string()
.nonempty("Account number is required"),
accountHolderName: z
.string()
.min(2, 'Account holder name must be at least 2 characters')
.max(100, 'Account holder name cannot exceed 100 characters'),
.nonempty("Account holder name is required")
.min(2, { message: "Account holder name must be at least 2 characters" }),
ifscCode: ifscCodeSchema,
bankXid: z
.number()
.int({ message: "Bank ID must be an integer" })
.positive({ message: "Bank ID must be a positive number" }),
bankXid: idSchema.describe('Bank ID'),
hostXid: z
.number()
.int({ message: "Host ID must be an integer" })
.positive({ message: "Host ID must be a positive number" }),
hostXid: idSchema.describe('Host ID'),
bankBranchXid: z
.number()
.int({ message: "Bank branch ID must be an integer" })
.positive({ message: "Bank branch ID must be a positive number" }),
bankBranchXid: idSchema.describe('Bank branch ID'),
currencyXid: idSchema.describe('Currency ID'),
currencyXid: z
.number()
.int({ message: "Currency ID must be an integer" })
.positive({ message: "Currency ID must be a positive number" }),
});
export type HostBankDetailsInput = z.infer<typeof hostBankDetailsSchema>;
export type HostBankDetailsSchema = z.infer<typeof hostBankDetailsSchema>;

View File

@@ -1,139 +0,0 @@
/**
* Host Onboarding Validation Schemas
* Production-ready Zod validations for host registration and authentication
* Compatible with Zod v4
*/
import { z } from 'zod';
import {
emailSchema,
simplePasswordSchema,
otpSchema,
nameSchema,
optionalNameSchema,
mobileNumberSchema,
isdCodeSchema,
} from '../validation.utils';
// ============================================
// SIGNUP / REGISTRATION
// ============================================
/**
* Host registration/signup schema
*/
export const hostSignUpSchema = z.object({
email: emailSchema,
});
export type HostSignUpInput = z.infer<typeof hostSignUpSchema>;
// ============================================
// OTP VERIFICATION
// ============================================
/**
* OTP verification schema
*/
export const verifyOtpSchema = z.object({
otp: otpSchema,
});
export type VerifyOtpInput = z.infer<typeof verifyOtpSchema>;
/**
* OTP verification with email schema (for verifyOTP handler)
*/
export const verifyOtpWithEmailSchema = z.object({
email: emailSchema,
otp: otpSchema,
});
export type VerifyOtpWithEmailInput = z.infer<typeof verifyOtpWithEmailSchema>;
// ============================================
// LOGIN
// ============================================
/**
* Host login schema
*/
export const hostLoginSchema = z.object({
emailAddress: emailSchema,
userPassword: z
.string()
.min(1, 'Password is required'),
});
export type HostLoginInput = z.infer<typeof hostLoginSchema>;
// ============================================
// CREATE PASSWORD
// ============================================
/**
* Create password schema with confirmation matching
*/
export const createPasswordSchema = z
.object({
password: simplePasswordSchema,
confirmPassword: z.string().min(1, 'Confirm password is required'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Password and confirm password do not match',
path: ['confirmPassword'],
});
export type CreatePasswordInput = z.infer<typeof createPasswordSchema>;
// ============================================
// RESEND OTP
// ============================================
/**
* Resend OTP schema
*/
export const resendOtpSchema = z.object({
email: emailSchema,
purpose: z
.enum(['Register', 'Login', 'ForgotPassword'])
.optional()
.default('Register'),
});
export type ResendOtpInput = z.infer<typeof resendOtpSchema>;
// ============================================
// UPDATE PROFILE
// ============================================
/**
* Host profile update schema
*/
export const updateHostProfileSchema = z.object({
firstName: optionalNameSchema,
lastName: optionalNameSchema,
mobileNumber: mobileNumberSchema,
isdCode: isdCodeSchema,
dateOfBirth: z
.string()
.refine((val) => !val || !isNaN(Date.parse(val)), 'Invalid date format')
.optional(),
profileImage: z.string().max(500, 'Profile image path cannot exceed 500 characters').optional(),
});
export type UpdateHostProfileInput = z.infer<typeof updateHostProfileSchema>;
// ============================================
// ACCEPT AGREEMENT
// ============================================
/**
* Accept agreement schema (just confirmation)
*/
export const acceptAgreementSchema = z.object({
agreementAccepted: z.literal(true, {
message: 'Agreement must be accepted',
}),
});
export type AcceptAgreementInput = z.infer<typeof acceptAgreementSchema>;

View File

@@ -2,31 +2,29 @@ import { z } from "zod";
export const parentCompanySchema = z.object({
companyName: z.string()
.min(1, "Parent company name is required")
.max(100, "Parent company name cannot exceed 100 characters"),
.max(100, "Parent company name cannot exceed 100 characters")
.optional(),
address1: z.string()
.min(1, "Address1 is required")
.max(150, "Address1 cannot exceed 150 characters"),
.max(150, "Address1 cannot exceed 150 characters")
.optional(),
address2: z.string()
.max(150, "Address2 cannot exceed 150 characters")
.optional(),
cityXid: z.number().min(1, "City is required"),
stateXid: z.number().min(1, "State is required"),
countryXid: z.number().min(1, "Country is required"),
cityXid: z.number().optional(),
stateXid: z.number().optional(),
countryXid: z.number().optional(),
pinCode: z.string()
.min(4, "Pincode/Zipcode is required")
.max(30, "Pincode cannot exceed 30 characters"),
.max(30, "Pincode cannot exceed 30 characters")
.optional(),
logoPath: z.string()
.max(400, "Logo path cannot exceed 400 characters")
.optional(),
isSubsidairy: z.boolean().optional(),
registrationNumber: z.string()
.max(30, "Registration number cannot exceed 30 characters")
.optional(),
@@ -45,15 +43,15 @@ export const parentCompanySchema = z.object({
message: "Formation date must be a valid date",
}),
companyType: z.string()
.min(1, "Company type is required")
.max(30, "Company type cannot exceed 30 characters"),
companyTypeXid: z.number()
.optional(),
websiteUrl: z.string().nullable().optional(),
instagramUrl: z.string().nullable().optional(),
facebookUrl: z.string().nullable().optional(),
linkedinUrl: z.string().nullable().optional(),
twitterUrl: z.string().nullable().optional(),
websiteUrl: z.string().url().max(80, "Website URL cannot exceed 80 characters").optional(),
instagramUrl: z.string().url().max(80, "Instagram URL cannot exceed 80 characters").optional(),
facebookUrl: z.string().url().max(80, "Facebook URL cannot exceed 80 characters").optional(),
linkedinUrl: z.string().url().max(80, "LinkedIn URL cannot exceed 80 characters").optional(),
twitterUrl: z.string().url().max(80, "Twitter URL cannot exceed 80 characters").optional(),
});
@@ -106,15 +104,20 @@ export const hostCompanyDetailsSchema = z.object({
message: "Formation date must be a valid date",
}),
companyType: z.string()
.min(1, "Company type is required")
.max(30, "Company type cannot exceed 30 characters"),
companyTypeXid: z.number()
.int("Company type must be a valid integer")
.min(1, "Company type is required"),
referencedBy: z.string()
.optional(),
websiteUrl: z.string().nullable().optional(),
instagramUrl: z.string().nullable().optional(),
facebookUrl: z.string().nullable().optional(),
linkedinUrl: z.string().nullable().optional(),
twitterUrl: z.string().nullable().optional(),
websiteUrl: z.string().url().max(50, "Website URL cannot exceed 50 characters").optional(),
instagramUrl: z.string().url().max(80, "Instagram URL cannot exceed 80 characters").optional(),
facebookUrl: z.string().url().max(80, "Facebook URL cannot exceed 80 characters").optional(),
linkedinUrl: z.string().url().max(80, "LinkedIn URL cannot exceed 80 characters").optional(),
twitterUrl: z.string().url().max(80, "Twitter URL cannot exceed 80 characters").optional(),
parentCompany: parentCompanySchema.optional(),
});

View File

@@ -1,13 +0,0 @@
/**
* Host Module Validation Schemas Index
* Export all host-related validation schemas
*/
// Authentication & Onboarding
export * from './auth.validation';
export * from './login.validation';
export * from './addPaymentDetails.validation';
export * from './hostCompanyDetails.validation';
// Activity Management
export * from './activity.validation';

View File

@@ -1,16 +1,20 @@
/**
* Host Login Validation Schema
* Production-ready Zod validation for host login
*/
import { z } from 'zod';
import { emailSchema } from '../validation.utils';
// validations/hostBankDetails.validation.ts
import { z } from "zod";
export const loginForHostSchema = z.object({
emailAddress: emailSchema,
userPassword: z
.string()
.min(1, 'Password is required'),
emailAddress : z
.string()
.nonempty("Email is required"),
userPassword : z
.string()
.nonempty("Password is required")
.min(8, { message: "Password must be at least 8 characters" }),
});
export type LoginForHostInput = z.infer<typeof loginForHostSchema>;
export type loginForHostSchema = z.infer<typeof loginForHostSchema>;

View File

@@ -1,16 +0,0 @@
/**
* Validation Module Index
* Central export for all validation schemas and utilities
*/
// Validation Utilities
export * from './validation.utils';
// Host Module Validations
export * as hostValidation from './host';
// Minglar Admin Module Validations
export * as minglarValidation from './minglaradmin';
// Prepopulate Module Validations
export * as prepopulateValidation from './prepopulate';

View File

@@ -1,107 +0,0 @@
/**
* Minglar Admin Authentication Validation Schemas
* Production-ready Zod validations for admin authentication
* Compatible with Zod v4
*/
import { z } from 'zod';
import { emailSchema, simplePasswordSchema, otpSchema } from '../validation.utils';
// ============================================
// REGISTRATION
// ============================================
/**
* Minglar admin registration schema
*/
export const minglarRegistrationSchema = z.object({
email: emailSchema,
});
export type MinglarRegistrationInput = z.infer<typeof minglarRegistrationSchema>;
// ============================================
// LOGIN
// ============================================
/**
* Minglar admin login schema
*/
export const minglarLoginSchema = z.object({
emailAddress: emailSchema,
userPassword: z
.string()
.min(1, 'Password is required'),
});
export type MinglarLoginInput = z.infer<typeof minglarLoginSchema>;
// ============================================
// CREATE PASSWORD
// ============================================
/**
* Create password schema with confirmation
*/
export const minglarCreatePasswordSchema = z
.object({
password: simplePasswordSchema,
confirmPassword: z.string().min(1, 'Confirm password is required'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Password and confirm password do not match',
path: ['confirmPassword'],
});
export type MinglarCreatePasswordInput = z.infer<typeof minglarCreatePasswordSchema>;
// ============================================
// VERIFY OTP
// ============================================
/**
* OTP verification schema
*/
export const minglarVerifyOtpSchema = z.object({
otp: otpSchema,
});
export type MinglarVerifyOtpInput = z.infer<typeof minglarVerifyOtpSchema>;
// ============================================
// UPDATE PROFILE
// ============================================
/**
* Admin profile update schema
*/
export const minglarUpdateProfileSchema = z.object({
firstName: z
.string()
.min(1, 'First name is required')
.max(50, 'First name cannot exceed 50 characters')
.optional(),
lastName: z
.string()
.min(1, 'Last name is required')
.max(50, 'Last name cannot exceed 50 characters')
.optional(),
mobileNumber: z
.string()
.max(15, 'Mobile number cannot exceed 15 digits')
.regex(/^[0-9]*$/, 'Mobile number must contain only digits')
.optional(),
isdCode: z
.string()
.max(6, 'ISD code cannot exceed 6 characters')
.optional(),
dateOfBirth: z
.string()
.refine((val) => !val || !isNaN(Date.parse(val)), 'Invalid date format')
.optional(),
profileImage: z
.string()
.max(500, 'Profile image path cannot exceed 500 characters')
.optional(),
});
export type MinglarUpdateProfileInput = z.infer<typeof minglarUpdateProfileSchema>;

View File

@@ -1,252 +0,0 @@
/**
* Minglar Admin Host Hub Validation Schemas
* Production-ready Zod validations for host management by admins
*/
import { z } from 'zod';
import { idSchema, searchQuerySchema, paginationSchema } from '../validation.utils';
// ============================================
// GET ALL HOST APPLICATIONS
// ============================================
/**
* Query params for getting all host applications
*/
export const getAllHostApplicationsQuerySchema = z.object({
search: searchQuerySchema,
userStatus: z
.string()
.max(20, 'Status cannot exceed 20 characters')
.optional(),
roleFilter: z
.string()
.max(30, 'Role filter cannot exceed 30 characters')
.optional(),
...paginationSchema.shape,
});
export type GetAllHostApplicationsQuery = z.infer<typeof getAllHostApplicationsQuerySchema>;
// ============================================
// ASSIGN AM TO HOST
// ============================================
/**
* Assign account manager to host schema
*/
export const assignAmToHostSchema = z.object({
hostXid: idSchema.describe('Host ID'),
accountManagerXid: idSchema.describe('Account Manager ID'),
});
export type AssignAmToHostInput = z.infer<typeof assignAmToHostSchema>;
// ============================================
// UPDATE HOST STATUS
// ============================================
/**
* Update host status schema
*/
export const updateHostStatusSchema = z.object({
hostXid: idSchema.describe('Host ID'),
status: z
.enum(['approved', 'rejected', 'pending', 'resubmit'])
.describe('New host status'),
remarks: z
.string()
.max(500, 'Remarks cannot exceed 500 characters')
.optional(),
});
export type UpdateHostStatusInput = z.infer<typeof updateHostStatusSchema>;
// ============================================
// GET HOST BY ID
// ============================================
/**
* Get host by ID query params
*/
export const getHostByIdQuerySchema = z.object({
hostXid: z.coerce
.number()
.int('Host ID must be an integer')
.positive('Host ID must be positive'),
});
export type GetHostByIdQuery = z.infer<typeof getHostByIdQuerySchema>;
// ============================================
// ADD HOST SUGGESTION
// ============================================
/**
* Add suggestion to host schema
*/
export const addHostSuggestionSchema = z.object({
hostXid: idSchema.describe('Host ID'),
title: z
.string()
.min(1, 'Title is required')
.max(100, 'Title cannot exceed 100 characters'),
comments: z
.string()
.min(1, 'Comments are required')
.max(500, 'Comments cannot exceed 500 characters'),
isParent: z.boolean().optional().default(false),
});
export type AddHostSuggestionInput = z.infer<typeof addHostSuggestionSchema>;
// ============================================
// AGREEMENT SETTINGS
// ============================================
/**
* Update agreement settings schema
*/
export const updateAgreementSettingsSchema = z.object({
hostXid: idSchema.describe('Host ID'),
agreementStartDate: z
.string()
.refine((val) => !isNaN(Date.parse(val)), 'Invalid date format'),
durationNumber: z
.number()
.int('Duration must be an integer')
.positive('Duration must be positive'),
durationFrequency: z.enum(['days', 'months', 'years']),
isCommisionBase: z.boolean(),
commisionPer: z
.number()
.min(0, 'Commission percentage must be at least 0')
.max(100, 'Commission percentage cannot exceed 100')
.optional(),
amountPerBooking: z
.number()
.int('Amount must be an integer')
.positive('Amount must be positive')
.optional(),
payoutDurationNum: z
.number()
.int('Payout duration must be an integer')
.positive('Payout duration must be positive')
.optional(),
payoutDurationFrequency: z.enum(['days', 'months', 'years']).optional(),
});
export type UpdateAgreementSettingsInput = z.infer<typeof updateAgreementSettingsSchema>;
// ============================================
// ACCEPT/REJECT HOST APPLICATION (by hostXid)
// ============================================
/**
* Host application action schema (accept/reject by AM or Admin)
*/
export const hostApplicationActionSchema = z.object({
hostXid: idSchema.describe('Host ID'),
});
export type HostApplicationActionInput = z.infer<typeof hostApplicationActionSchema>;
// ============================================
// ADD PQQ SUGGESTION
// ============================================
/**
* Add PQQ suggestion schema
*/
export const addPqqSuggestionSchema = z.object({
title: z
.string()
.min(1, 'Title is required')
.max(100, 'Title cannot exceed 100 characters'),
comments: z
.string()
.min(1, 'Comments are required')
.max(500, 'Comments cannot exceed 500 characters'),
activity_pqq_header_xid: idSchema.describe('Activity PQQ Header ID'),
});
export type AddPqqSuggestionInput = z.infer<typeof addPqqSuggestionSchema>;
// ============================================
// GET HOST BY ID (path param)
// ============================================
/**
* Get host by ID from path params
*/
export const getHostByIdPathSchema = z.object({
host_xid: z.coerce
.number()
.int('Host ID must be an integer')
.positive('Host ID must be positive'),
});
export type GetHostByIdPathInput = z.infer<typeof getHostByIdPathSchema>;
/**
* Get host by ID from path params (alternative with 'id')
*/
export const getHostByIdAltPathSchema = z.object({
id: z.coerce
.number()
.int('Host ID must be an integer')
.positive('Host ID must be positive'),
});
export type GetHostByIdAltPathInput = z.infer<typeof getHostByIdAltPathSchema>;
// ============================================
// REJECT PQQ BY AM
// ============================================
/**
* Reject PQQ by AM schema
*/
export const rejectPqqByAmSchema = z.object({
activityId: idSchema.describe('Activity ID'),
});
export type RejectPqqByAmInput = z.infer<typeof rejectPqqByAmSchema>;
// ============================================
// EDIT AGREEMENT DETAILS
// ============================================
/**
* Edit agreement details schema
*/
export const editAgreementDetailsSchema = z.object({
host_xid: idSchema.describe('Host ID'),
agreementStartDate: z
.string()
.min(1, 'Agreement start date is required')
.refine((val) => !isNaN(Date.parse(val)), 'Invalid date format'),
duration: z
.number()
.int('Duration must be an integer')
.positive('Duration must be positive'),
isCommisionBase: z.boolean(),
commisionPer: z
.number()
.min(0, 'Commission percentage must be at least 0')
.max(100, 'Commission percentage cannot exceed 100')
.optional(),
amountPerBooking: z
.number()
.int('Amount must be an integer')
.positive('Amount must be positive')
.optional(),
durationFrequency: z.string().min(1, 'Duration frequency is required'),
payoutDurationNum: z
.number()
.int('Payout duration must be an integer')
.positive('Payout duration must be positive')
.optional(),
payoutDurationFrequency: z.string().optional(),
});
export type EditAgreementDetailsInput = z.infer<typeof editAgreementDetailsSchema>;

View File

@@ -1,13 +0,0 @@
/**
* Minglar Admin Module Validation Schemas Index
* Export all minglar admin-related validation schemas
*/
// Authentication
export * from './auth.validation';
// Teammate Management
export * from './teammate.validation';
// Host Hub Management
export * from './hosthub.validation';

View File

@@ -1,117 +0,0 @@
/**
* Minglar Admin Teammate Management Validation Schemas
* Production-ready Zod validations for teammate/invite management
*/
import { z } from 'zod';
import { emailSchema, idSchema, searchQuerySchema, paginationSchema } from '../validation.utils';
import { ROLE } from '../../constants/common.constant';
// ============================================
// INVITE TEAMMATE
// ============================================
/**
* Invite teammate schema
*/
export const inviteTeammateSchema = z.object({
emailAddress: emailSchema,
roleXid: z
.number()
.int('Role ID must be an integer')
.refine(
(val) => [ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER].includes(val),
'Invalid role. Only Co-Admin and Account Manager roles can be assigned'
),
isFixedSalary: z.boolean(),
perValue: z
.number()
.positive('Per value must be greater than 0')
.optional(),
}).refine(
(data) => {
// If Account Manager and not fixed salary, perValue is required
if (data.roleXid === ROLE.ACCOUNT_MANAGER && !data.isFixedSalary) {
return data.perValue !== undefined && data.perValue > 0;
}
return true;
},
{
message: 'Per value is required for commission-based Account Managers',
path: ['perValue'],
}
);
export type InviteTeammateInput = z.infer<typeof inviteTeammateSchema>;
// ============================================
// GET ALL COADMIN AND AM (with search)
// ============================================
/**
* Query params for getting co-admins and account managers
*/
export const getAllCoadminAndAMQuerySchema = z.object({
search: searchQuerySchema,
});
export type GetAllCoadminAndAMQuery = z.infer<typeof getAllCoadminAndAMQuerySchema>;
// ============================================
// GET ALL INVITED COADMIN AND AM (with search)
// ============================================
/**
* Query params for getting invited co-admins and account managers
*/
export const getAllInvitedCoadminAndAMQuerySchema = z.object({
search: searchQuerySchema,
});
export type GetAllInvitedCoadminAndAMQuery = z.infer<typeof getAllInvitedCoadminAndAMQuerySchema>;
// ============================================
// GET INVITATION DETAILS (with search)
// ============================================
/**
* Query params for getting invitation details
*/
export const getInvitationDetailsQuerySchema = z.object({
search: searchQuerySchema,
});
export type GetInvitationDetailsQuery = z.infer<typeof getInvitationDetailsQuerySchema>;
// ============================================
// UPDATE TEAMMATE STATUS
// ============================================
/**
* Update teammate status schema
*/
export const updateTeammateStatusSchema = z.object({
userId: idSchema.describe('User ID'),
status: z.enum(['active', 'inactive', 'suspended']),
});
export type UpdateTeammateStatusInput = z.infer<typeof updateTeammateStatusSchema>;
// ============================================
// ASSIGN REVENUE
// ============================================
/**
* Assign revenue to teammate schema
*/
export const assignRevenueSchema = z.object({
userId: idSchema.describe('User ID'),
isFixedSalary: z.boolean(),
perValue: z
.number()
.positive('Per value must be greater than 0'),
});
export type AssignRevenueInput = z.infer<typeof assignRevenueSchema>;

View File

@@ -1,6 +0,0 @@
/**
* Prepopulate Module Validation Schemas Index
* Export all prepopulate-related validation schemas
*/
export * from './prepopulate.validation';

View File

@@ -1,53 +0,0 @@
/**
* Prepopulate Module Validation Schemas
* Production-ready Zod validations for prepopulate endpoints
*/
import { z } from 'zod';
// ============================================
// GET BANK BRANCHES BY BANK ID
// ============================================
/**
* Get bank branches by bank ID query params
*/
export const getBankBranchesByBankIdQuerySchema = z.object({
bankXid: z.coerce
.number()
.int('Bank ID must be an integer')
.positive('Bank ID must be positive'),
});
export type GetBankBranchesByBankIdQuery = z.infer<typeof getBankBranchesByBankIdQuerySchema>;
// ============================================
// GET CITY BY STATE ID
// ============================================
/**
* Get city by state ID query params
*/
export const getCityByStateIdQuerySchema = z.object({
stateXid: z.coerce
.number()
.int('State ID must be an integer')
.positive('State ID must be positive'),
});
export type GetCityByStateIdQuery = z.infer<typeof getCityByStateIdQuerySchema>;
// ============================================
// GET AM DETAIL BY ID (path param)
// ============================================
/**
* Get AM detail by ID path params
*/
export const getAmDetailByIdPathSchema = z.object({
amXid: z.coerce
.number()
.int('Account Manager ID must be an integer')
.positive('Account Manager ID must be positive'),
});
export type GetAmDetailByIdPath = z.infer<typeof getAmDetailByIdPathSchema>;

View File

@@ -1,358 +0,0 @@
/**
* Production-Ready Validation Utilities
* Centralized validation helpers for Zod schemas
* Compatible with Zod v4
*/
import { z, ZodError, ZodSchema } from 'zod';
import ApiError from '../helper/ApiError';
/**
* Formats Zod validation errors into user-friendly messages
*/
export function formatZodErrors(error: ZodError): string {
return error.issues
.map((issue) => {
const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : '';
return `${path}${issue.message}`;
})
.join('; ');
}
/**
* Validates data against a Zod schema and throws ApiError on failure
* @param schema - Zod schema to validate against
* @param data - Data to validate
* @param errorCode - HTTP status code for validation errors (default: 400)
* @returns Validated and typed data
*/
export function validateSchema<T>(
schema: ZodSchema<T>,
data: unknown,
errorCode: number = 400
): T {
const result = schema.safeParse(data);
if (!result.success) {
const errorMessage = formatZodErrors(result.error);
throw new ApiError(errorCode, `Validation failed: ${errorMessage}`);
}
return result.data;
}
/**
* Safe validation that returns result object instead of throwing
*/
export function safeValidate<T>(
schema: ZodSchema<T>,
data: unknown
): { success: true; data: T } | { success: false; errors: string } {
const result = schema.safeParse(data);
if (!result.success) {
return {
success: false,
errors: formatZodErrors(result.error),
};
}
return {
success: true,
data: result.data,
};
}
/**
* Parses JSON body from API Gateway event safely
*/
export function parseBody<T>(
body: string | null,
schema: ZodSchema<T>
): T {
if (!body) {
throw new ApiError(400, 'Request body is required');
}
let parsedBody: unknown;
try {
parsedBody = JSON.parse(body);
} catch {
throw new ApiError(400, 'Invalid JSON in request body');
}
return validateSchema(schema, parsedBody);
}
/**
* Parses query string parameters with validation
*/
export function parseQueryParams<T>(
params: Record<string, string | undefined> | null,
schema: ZodSchema<T>
): T {
return validateSchema(schema, params || {});
}
// ============================================
// COMMON REUSABLE FIELD SCHEMAS (Zod v4 compatible)
// ============================================
/**
* Email validation with proper format
*/
export const emailSchema = z
.string()
.min(1, 'Email is required')
.email('Invalid email format')
.max(150, 'Email cannot exceed 150 characters')
.transform((val) => val.toLowerCase().trim());
/**
* Password validation with security requirements
*/
export const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.max(255, 'Password cannot exceed 255 characters')
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'Password must contain at least one uppercase letter, one lowercase letter, one number and one special character'
);
/**
* Simple password (for cases where strong password isn't required)
*/
export const simplePasswordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.max(255, 'Password cannot exceed 255 characters');
/**
* Mobile number validation (international format)
*/
export const mobileNumberSchema = z
.string()
.min(7, 'Mobile number must be at least 7 digits')
.max(15, 'Mobile number cannot exceed 15 digits')
.regex(/^[0-9]+$/, 'Mobile number must contain only digits')
.optional();
/**
* ISD Code validation
*/
export const isdCodeSchema = z
.string()
.max(6, 'ISD code cannot exceed 6 characters')
.regex(/^\+?[0-9]+$/, 'Invalid ISD code format')
.optional();
/**
* Name validation (first name, last name, etc.)
*/
export const nameSchema = z
.string()
.min(1, 'Name is required')
.max(50, 'Name cannot exceed 50 characters')
.regex(/^[a-zA-Z\s'-]+$/, 'Name can only contain letters, spaces, hyphens and apostrophes');
/**
* Optional name schema
*/
export const optionalNameSchema = nameSchema.optional();
/**
* Positive integer ID validation
*/
export const idSchema = z
.number()
.int('ID must be an integer')
.positive('ID must be a positive number');
/**
* Optional positive integer ID
*/
export const optionalIdSchema = z
.number()
.int('ID must be an integer')
.positive('ID must be a positive number')
.optional();
/**
* Coerce string to number for query params
*/
export const numericStringSchema = z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().int().positive());
/**
* Optional numeric string
*/
export const optionalNumericStringSchema = z
.string()
.optional()
.transform((val) => (val ? parseInt(val, 10) : undefined))
.pipe(z.number().int().positive().optional());
/**
* Address line validation
*/
export const addressLine1Schema = z
.string()
.min(1, 'Address is required')
.max(150, 'Address cannot exceed 150 characters');
export const addressLine2Schema = z
.string()
.max(150, 'Address cannot exceed 150 characters')
.optional();
/**
* Pin/Zip code validation
*/
export const pinCodeSchema = z
.string()
.min(4, 'Pin code must be at least 4 characters')
.max(30, 'Pin code cannot exceed 30 characters');
/**
* URL validation
*/
export const urlSchema = z
.string()
.url('Invalid URL format')
.max(500, 'URL cannot exceed 500 characters')
.optional()
.or(z.literal(''));
/**
* Social media URL (with empty string handling)
*/
export const socialUrlSchema = z
.string()
.max(80, 'URL cannot exceed 80 characters')
.refine(
(val) => !val || val === '' || z.string().url().safeParse(val).success,
'Invalid URL format'
)
.optional();
/**
* Date string validation
*/
export const dateStringSchema = z
.string()
.refine((val) => !isNaN(Date.parse(val)), 'Invalid date format')
.transform((val) => new Date(val));
/**
* Optional date string
*/
export const optionalDateStringSchema = z
.string()
.refine((val) => !val || !isNaN(Date.parse(val)), 'Invalid date format')
.optional();
/**
* Boolean schema with string coercion (for query params)
*/
export const booleanStringSchema = z
.string()
.transform((val) => val === 'true' || val === '1')
.pipe(z.boolean());
/**
* OTP validation (6 digits)
*/
export const otpSchema = z
.string()
.length(6, 'OTP must be exactly 6 digits')
.regex(/^[0-9]+$/, 'OTP must contain only digits');
/**
* Search query validation
*/
export const searchQuerySchema = z
.string()
.max(100, 'Search query cannot exceed 100 characters')
.transform((val) => val.trim())
.optional();
/**
* Pagination parameters
*/
export const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(10),
});
/**
* File path validation for S3
*/
export const filePathSchema = z
.string()
.max(500, 'File path cannot exceed 500 characters')
.optional();
/**
* Registration/Reference number validation
*/
export const registrationNumberSchema = z
.string()
.max(30, 'Registration number cannot exceed 30 characters')
.optional();
/**
* PAN number validation (India)
*/
export const panNumberSchema = z
.string()
.max(30, 'PAN number cannot exceed 30 characters')
.regex(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/, 'Invalid PAN number format')
.optional()
.or(z.literal(''));
/**
* GST number validation (India)
*/
export const gstNumberSchema = z
.string()
.max(30, 'GST number cannot exceed 30 characters')
.regex(
/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/,
'Invalid GST number format'
)
.optional()
.or(z.literal(''));
/**
* IFSC code validation (India)
*/
export const ifscCodeSchema = z
.string()
.min(1, 'IFSC code is required')
.regex(/^[A-Z]{4}0[A-Z0-9]{6}$/, 'Invalid IFSC code format');
/**
* Bank account number validation
*/
export const accountNumberSchema = z
.string()
.min(9, 'Account number must be at least 9 digits')
.max(18, 'Account number cannot exceed 18 digits')
.regex(/^[0-9]+$/, 'Account number must contain only digits');
/**
* Company name validation
*/
export const companyNameSchema = z
.string()
.min(1, 'Company name is required')
.max(100, 'Company name cannot exceed 100 characters');
/**
* Token validation
*/
export const tokenSchema = z
.string()
.min(1, 'Authentication token is required');

View File

@@ -81,6 +81,9 @@ const envVarsSchema = yup
DB_PORT: yup.number().default(3306).required('DB Port is required'),
//OTP Bypass
BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'),
// Email links
AM_INVITATION_LINK: yup.string().required('Link to send in AM invitation mail is required'),
HOST_LINK: yup.string().required('Link to host panel is required')
})
.noUnknown(true);
@@ -158,6 +161,8 @@ function getConfig() {
//Minglar admin
MinglarAdminEmail: envVars.MINGLAR_ADMIN_EMAIL,
MinglarAdminName: envVars.MINGLAR_ADMIN_NAME,
AM_INVITATION_LINK: envVars.AM_INVITATION_LINK,
HOST_LINK: envVars.HOST_LINK,
// oneSignal: {
// appID: envVars.ONESIGNAL_APPID,
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,

140
src/handlers/swagger.ts Normal file
View File

@@ -0,0 +1,140 @@
// src/handlers/swagger.ts
// Swagger UI handler for serverless-offline
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import * as fs from 'fs';
import * as path from 'path';
// Swagger UI HTML template
const getSwaggerHtml = (specUrl: string) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minglar API Documentation</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
<style>
html { box-sizing: border-box; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; }
body { margin: 0; background: #fafafa; }
.swagger-ui .topbar { display: none; }
.swagger-ui .info { margin: 20px 0; }
.swagger-ui .info .title { font-size: 36px; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "/swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
persistAuthorization: true,
displayRequestDuration: true,
filter: true,
showExtensions: true,
tryItOutEnabled: true
});
window.ui = ui;
};
</script>
</body>
</html>
`;
// Handler for Swagger UI HTML page
export const swaggerUi = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
const host = event.headers?.Host || event.headers?.host || 'localhost:3000';
// For serverless-offline, use simple direct URL without stage
const specUrl = `/swagger.json`;
return {
statusCode: 200,
headers: {
'Content-Type': 'text/html',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
},
body: getSwaggerHtml(specUrl),
};
};
// Handler for swagger.json
export const swaggerJson = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
// Read swagger.json from project root
const swaggerPath = path.join(process.cwd(), 'swagger.json');
const swaggerContent = fs.readFileSync(swaggerPath, 'utf8');
const swaggerDoc = JSON.parse(swaggerContent);
// Update server URL dynamically - ALWAYS use root URL without stage
const host = event.headers?.Host || event.headers?.host || 'localhost:3000';
const protocol = event.headers?.['X-Forwarded-Proto'] || 'http';
// For local development, use root URL. For AWS, use the full URL with stage only if deployed
if (process.env.AWS_LAMBDA_FUNCTION_NAME && process.env.AWS_EXECUTION_ENV) {
// Only add stage for actual AWS deployment, not serverless-offline
const stage = event.requestContext?.stage;
const stagePrefix = stage && stage !== '$default' ? `/${stage}` : '';
swaggerDoc.servers = [
{
url: `${protocol}://${host}${stagePrefix}`,
description: 'AWS API Gateway'
}
];
} else {
// Local development - no stage prefix
swaggerDoc.servers = [
{
url: `http://${host}`,
description: 'Local Development (serverless-offline)'
}
];
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
},
body: JSON.stringify(swaggerDoc, null, 2),
};
} catch (error) {
console.error('Error loading swagger.json:', error);
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
error: 'Failed to load swagger.json',
message: error instanceof Error ? error.message : 'Unknown error'
}),
};
}
};

View File

@@ -0,0 +1,162 @@
import { z } from 'zod';
/* ================= MEDIA ================= */
export const MediaDto = z.object({
mediaType: z.string().optional(), // "image/jpeg", "video/mp4", etc.
mediaFileName: z.string(), // S3 file URL
});
/* ================= PRICE =================
* ❌ No tax info here; root-level only
*/
export const PriceDto = z.object({
noOfSession: z.number().int().optional().default(1),
isPackage: z.boolean().optional().default(false),
sessionValidity: z.number().int().optional().default(0),
sessionValidityFrequency: z.string().optional().default('Days'),
basePrice: z.number().int().optional().default(0),
sellPrice: z.number().int(), // required
});
/* ================= VENUE ================= */
export const VenueDto = z.object({
venueName: z.string(),
venueCapacity: z.number().int().optional().default(0),
availableSeats: z.number().int().optional().default(0),
isMinPeopleReqMandatory: z.boolean().optional().default(false),
minPeopleRequired: z.number().int().nullable().optional(),
minReqfullfilledBeforeMins: z.number().int().nullable().optional(),
venueDescription: z.string().optional(),
// ✅ new: media per venue (for ActivityVenueArtifacts)
media: z.array(MediaDto).optional().default([]),
// price list per venue
prices: z.array(PriceDto).optional().default([]),
});
/* ================= PICKUP / DROP ================= */
export const PickupDetailDto = z.object({
isPickUp: z.boolean().optional().default(false),
locationLat: z.number().nullable().optional(),
locationLong: z.number().nullable().optional(),
locationAddress: z.string().nullable().optional(),
transportBasePrice: z.number().int().optional().default(0),
transportTotalPrice: z.number().int().optional().default(0),
});
export const PickupTransportDto = z.object({
transportModeXid: z.number().int(),
isTransportModeChargeable: z.boolean().optional().default(false),
pickupDetails: z.array(PickupDetailDto).optional().default([]),
});
/* ================= EQUIPMENT ================= */
export const EquipmentDto = z.object({
equipmentName: z.string(),
isEquipmentChargeable: z.boolean().optional().default(false),
equipmentBasePrice: z.number().int().optional().default(0),
equipmentTotalPrice: z.number().int().optional().default(0),
});
/* ================= ELIGIBILITY ================= */
export const EligibilityDto = z.object({
isAgeRestriction: z.boolean().optional().default(false),
ageRestrictionXid: z.number().int().nullable().optional(),
isWeightRestriction: z.boolean().optional().default(false),
weightRestrictionName: z.string().nullable().optional(),
weightEntered: z.number().int().nullable().optional(),
weightIn: z.string().nullable().optional(),
minWeight: z.number().int().nullable().optional(),
maxWeight: z.number().int().nullable().optional(),
isHeightRestriction: z.boolean().optional().default(false),
heightRestrictionName: z.string().nullable().optional(),
heightEntered: z.number().int().nullable().optional(),
heightIn: z.string().nullable().optional(),
minHeight: z.number().int().nullable().optional(),
maxHeight: z.number().int().nullable().optional(),
});
/* ================= OTHER DETAILS ================= */
export const OtherDetailsDto = z.object({
exclusiveNotes: z.string().optional(),
dosNotes: z.string().optional(),
dontsNotes: z.string().optional(),
tipsNotes: z.string().optional(),
termsAndCondition: z.string().optional(),
});
/* ================= CREATE ACTIVITY ================= */
export const CreateActivityDto = z.object({
/* 🔑 REQUIRED */
activityXid: z.number().int(),
/* OPTIONAL CORE */
activityTypeXid: z.number().int().optional(),
frequenciesXid: z.number().int().nullable().optional(),
activityTitle: z.string().optional(),
activityDescription: z.string().optional(),
/* LOCATION */
checkInLat: z.number().nullable().optional(),
checkInLong: z.number().nullable().optional(),
checkInAddress: z.string().nullable().optional(),
isCheckOutSame: z.boolean().optional().default(true),
checkOutLat: z.number().nullable().optional(),
checkOutLong: z.number().nullable().optional(),
checkOutAddress: z.string().nullable().optional(),
/* DURATION / ENERGY */
energyLevelXid: z.number().int().nullable().optional(),
activityDurationMins: z.number().int().nullable().optional(),
durationHours: z.number().int().optional(),
durationMins: z.number().int().optional(),
/* FLAGS */
foodAvailable: z.boolean().optional().default(false),
foodIsChargeable: z.boolean().optional().default(false),
alcoholAvailable: z.boolean().optional().default(false),
trainerAvailable: z.boolean().optional().default(false),
trainerIsChargeable: z.boolean().optional().default(false),
pickUpDropAvailable: z.boolean().optional().default(false),
pickUpDropIsChargeable: z.boolean().optional().default(false),
inActivityAvailable: z.boolean().optional().default(false),
inActivityIsChargeable: z.boolean().optional().default(false),
equipmentAvailable: z.boolean().optional().default(false),
equipmentIsChargeable: z.boolean().optional().default(false),
cancellationAvailable: z.boolean().optional().default(false),
/* MONEY / CURRENCY */
currencyXid: z.number().int().nullable().optional(),
sustainabilityScore: z.number().int().nullable().optional(),
safetyScore: z.number().int().nullable().optional(),
isInstantBooking: z.boolean().optional().default(false),
/* 🔥 ROOT-LEVEL TAX (SINGLE SOURCE OF TRUTH) */
taxXids: z.array(z.number().int()).optional().default([]),
/* 🔥 MEDIA ARRAYS */
media: z.array(MediaDto).optional().default([]), // Activity-level media
venues: z.array(VenueDto).optional().default([]), // Each venues media + prices
/* RELATION ARRAYS */
foodTypeIds: z.array(z.number().int()).optional().default([]),
cuisineIds: z.array(z.number().int()).optional().default([]),
pickupTransports: z.array(PickupTransportDto).optional().default([]),
navigationModes: z.array(z.number().int()).optional().default([]),
equipments: z.array(EquipmentDto).optional().default([]),
amenitiesIds: z.array(z.number().int()).optional().default([]),
/* EXTRA OBJECTS */
eligibility: EligibilityDto.optional(),
otherDetails: OtherDetailsDto.optional(),
});
export type CreateActivityInput = z.infer<typeof CreateActivityDto>;

View File

@@ -90,5 +90,6 @@ export class AddPaymentDetailsDTO {
this.accountHolderName = accountHolderName;
this.ifscCode = ifscCode;
this.hostXid = hostXid;
this.currencyXid = currencyXid;
}
}

View File

@@ -0,0 +1,311 @@
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import {
CreateActivityDto,
CreateActivityInput,
} from '../../../dto/createActivity.schema';
import { HostService } from '../../../services/host.service';
const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
/* ------------------------------- Utilities ------------------------------- */
function getExtensionFromMime(mimeType: string) {
const map: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/webp': 'webp',
'video/mp4': 'mp4',
'video/quicktime': 'mov',
'video/x-msvideo': 'avi',
'video/x-matroska': 'mkv',
};
return map[mimeType] || 'bin';
}
function normalizeJsonField(fields: any, key: string) {
if (!fields[key]) return undefined;
if (typeof fields[key] === 'object') return fields[key];
try {
return JSON.parse(fields[key]);
} catch {
throw new ApiError(400, `Invalid JSON in field: ${key}`);
}
}
/* -------------------------------- Handler -------------------------------- */
export const handler = safeHandler(
async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
/* 1⃣ AUTH */
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
401,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyHostToken(token);
/* 2⃣ CONTENT TYPE */
const contentType =
event.headers['content-type'] || event.headers['Content-Type'];
if (!contentType?.includes('multipart/form-data')) {
throw new ApiError(400, 'Content-Type must be multipart/form-data');
}
/* 3⃣ BODY BUFFER */
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body as string, 'base64')
: Buffer.from(event.body as string);
const fields: Record<string, any> = {};
const files: Array<{
buffer: Buffer;
mimeType: string;
fileName: string;
fieldName: string;
}> = [];
await new Promise<void>((resolve, reject) => {
const bb = Busboy({
headers: {
...event.headers,
'content-type': contentType,
},
});
bb.on('field', (name, value) => {
fields[name] = value;
});
bb.on('file', (fieldName, file, info) => {
const { filename, mimeType } = info;
const chunks: Buffer[] = [];
let size = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on('data', (chunk) => {
size += chunk.length;
if (size > MAX_SIZE) {
file.destroy(new Error('File exceeds 5MB limit'));
return;
}
chunks.push(chunk);
});
file.on('end', () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType: mimeType || 'application/octet-stream',
fileName: filename || 'unknown',
fieldName,
});
}
});
});
bb.on('finish', () => resolve());
bb.on('error', (err) => reject(new ApiError(400, err.message)));
bb.end(bodyBuffer);
});
/* 4⃣ FLAGS */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
/* 5⃣ ACTIVITY PAYLOAD */
const activityPayload: any = normalizeJsonField(fields, 'activity');
if (!activityPayload) {
throw new ApiError(400, 'activity payload is required');
}
/* 6⃣ NORMALIZE IDS */
if (activityPayload.activityXid) {
activityPayload.activityXid = Number(activityPayload.activityXid);
}
const numberKeys = [
'currencyXid',
'energyLevelXid',
'activityDurationMins',
'activityTypeXid',
'frequenciesXid',
'trainerTotalAmount',
'pickupDropTotalPrice',
'navigationModeTotalPrice',
'sustainabilityScore',
'safetyScore',
'checkInLat',
'checkInLong',
'checkOutLat',
'checkOutLong',
];
for (const key of numberKeys) {
if (activityPayload[key] !== undefined && activityPayload[key] !== null && activityPayload[key] !== '') {
activityPayload[key] = Number(activityPayload[key]);
}
}
/* 7⃣ NORMALIZE BOOLEANS */
const booleanKeys = [
'isInstantBooking',
'foodAvailable',
'foodIsChargeable',
'alcoholAvailable',
'trainerAvailable',
'trainerIsChargeable',
'pickUpDropAvailable',
'pickUpDropIsChargeable',
'inActivityAvailable',
'inActivityIsChargeable',
'equipmentAvailable',
'equipmentIsChargeable',
'cancellationAvailable',
'isCheckOutSame',
];
for (const key of booleanKeys) {
if (activityPayload[key] === 'true') activityPayload[key] = true;
if (activityPayload[key] === 'false') activityPayload[key] = false;
}
/* 8⃣ UPLOAD ACTIVITY-LEVEL MEDIA (images/videos) */
const uploadedActivityMedia: Array<{ mediaType?: string; mediaFileName: string }> = [];
for (const file of files.filter(
(f) => f.fieldName === 'activityImages' || f.fieldName === 'activityVideos',
)) {
const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Media/${Date.now()}_${file.fileName}`;
if (s3Key.length > 900) {
throw new ApiError(400, 'Generated S3 key too long');
}
await s3
.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: file.buffer,
ContentType: file.mimeType,
ACL: 'private',
})
.promise();
uploadedActivityMedia.push({
mediaType: file.mimeType,
mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
});
}
/* 🔥 MERGE ACTIVITY MEDIA */
const existingMedia = Array.isArray(activityPayload.media)
? activityPayload.media
: [];
activityPayload.media = [...existingMedia, ...uploadedActivityMedia];
/* 9⃣ PROCESS VENUE MEDIA UPLOADS */
// Group venue files by index: venueImages[0], venueImages[1], etc.
const venueFilesMap: Map<number, Array<{ buffer: Buffer; mimeType: string; fileName: string }>> = new Map();
for (const file of files) {
// Match patterns like: venueImages[0], venueVideos[1], etc.
const match = file.fieldName.match(/^venue(Images|Videos)\[(\d+)\]$/);
if (match) {
const venueIndex = parseInt(match[2], 10);
if (!venueFilesMap.has(venueIndex)) {
venueFilesMap.set(venueIndex, []);
}
venueFilesMap.get(venueIndex)!.push(file);
}
}
// Upload venue files and attach to corresponding venues
if (Array.isArray(activityPayload.venues)) {
for (let i = 0; i < activityPayload.venues.length; i++) {
const venue = activityPayload.venues[i];
const venueFiles = venueFilesMap.get(i) || [];
const uploadedVenueMedia: Array<{ mediaType?: string; mediaFileName: string }> = [];
for (const file of venueFiles) {
const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Venue_${i}/Media/${Date.now()}_${file.fileName}`;
if (s3Key.length > 900) {
throw new ApiError(400, 'Generated S3 key too long for venue media');
}
await s3
.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: file.buffer,
ContentType: file.mimeType,
ACL: 'private',
})
.promise();
uploadedVenueMedia.push({
mediaType: file.mimeType,
mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
});
}
// Merge with existing venue media
const existingVenueMedia = Array.isArray(venue.media) ? venue.media : [];
venue.media = [...existingVenueMedia, ...uploadedVenueMedia];
}
}
/* 🔟 VALIDATION */
let parsedDto: CreateActivityInput;
if (!isDraft) {
const parsed = CreateActivityDto.safeParse(activityPayload);
if (!parsed.success) {
throw new ApiError(
400,
parsed.error.issues.map((i) => i.message).join(', '),
);
}
parsedDto = parsed.data;
} else {
parsedDto = activityPayload as CreateActivityInput;
}
/* 1⃣1⃣ SAVE ACTIVITY */
const createdActivity = await hostService.createOrUpdateActivity(
userInfo.id,
parsedDto,
isDraft,
);
/* 1⃣2⃣ RESPONSE */
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: isDraft
? 'Activity saved as draft successfully'
: 'Activity created successfully',
data: createdActivity,
}),
};
},
);

View File

@@ -0,0 +1,61 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const hostService = new HostService(prismaClient);
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
401,
'This is a protected route. Please provide a valid token.',
);
}
// Verify token and get user info
const userInfo = await verifyHostToken(token);
let body: any = {};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (err) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityTypeXid, frequenciesXid } = body;
if (!activityTypeXid || !frequenciesXid) {
throw new ApiError(400, 'activityType and frequency ID is required');
}
// Get all host applications from service based on user role
const createdData = await hostService.createActivityAndAllQuestionsEntry(userInfo.id, activityTypeXid, frequenciesXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Activity created successfully',
data: createdData
}),
};
},
);

View File

@@ -1,19 +1,18 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { PrePopulateService } from '../../../../prepopulate/services/prepopulate.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { optionalSearchQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const prePopulateService = new PrePopulateService(prismaService);
const hostService = new HostService(prismaClient);
const prePopulateService = new PrePopulateService(prismaClient);
/**
* Get all activity types with interest handler
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -28,11 +27,11 @@ export const handler = safeHandler(async (
// Verify token and get user info
const userInfo = await verifyHostToken(token);
// Parse and validate query params using Zod
const { search, q } = parseQueryParams(event.queryStringParameters, optionalSearchQuerySchema);
const searchTerm = search || q || undefined;
const data = await hostService.getAllActivityTypesWithInterest(searchTerm);
// Read optional search query (supports ?search= or ?q=)
const search = event.queryStringParameters?.search || event.queryStringParameters?.q || undefined;
const data = await hostService.getAllActivityTypesWithInterest(search);
const frequencies = await prePopulateService.getAllFrequencies();
return {

View File

@@ -1,19 +1,19 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { optionalSearchQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
import { paginationService } from '../../../../../common/utils/pagination/pagination.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
/**
* Get all host activities handler
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -28,11 +28,18 @@ export const handler = safeHandler(async (
// Verify token and get user info
const userInfo = await verifyHostToken(token);
// Parse and validate query params using Zod
const { search, q } = parseQueryParams(event.queryStringParameters, optionalSearchQuerySchema);
const searchTerm = search || q || undefined;
// Get pagination params from event
const paginationParams = paginationService.getPaginationFromEvent(event);
const paginationOptions = paginationService.parsePaginationParams(paginationParams);
const data = await hostService.getAllHostActivity(searchTerm, Number(userInfo.id));
// Read optional search query (supports ?search= or ?q=)
const search = event.queryStringParameters?.search || event.queryStringParameters?.q || undefined;
const result = await hostService.getAllHostActivity(
search ? String(search) : undefined,
Number(userInfo.id),
paginationOptions
);
return {
@@ -44,8 +51,7 @@ export const handler = safeHandler(async (
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data,
...result,
}),
};
});

View File

@@ -1,14 +1,11 @@
import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { PrePopulateService } from '../../../../prepopulate/services/prepopulate.service';
import { HostService } from '../../../services/host.service';
import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host';
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -23,7 +20,15 @@ export const handler = safeHandler(async (
// Authenticate user using the shared authForHost function
await verifyMinglarAdminHostToken(token);
const result = await hostService.getAllPQQQuesAndSubmittedAns();
const activity_xid = event.queryStringParameters?.activity_xid
? Number(event.queryStringParameters.activity_xid)
: null;
if (!activity_xid) {
throw new ApiError(409, "Activity ID is required")
}
const result = await hostService.getAllPQQQuesAndSubmittedAns(Number(activity_xid));
return {
statusCode: 200,

View File

@@ -1,55 +1,42 @@
import config from '@/config/config';
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { LAST_QUESTION_ID } from '@/common/utils/constants/host.constant';
const prisma = new PrismaService();
const pqqService = new HostService(prisma);
const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
// Function to extract S3 key from URL
// Extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(bucketBaseUrl, '');
}
// Function to delete file from S3
// Delete file from S3
async function deleteFromS3(s3Key: string): Promise<void> {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: s3Key,
}).promise();
console.log(`File deleted from S3: ${s3Key}`);
} catch (error) {
console.error(`Error deleting file from S3: ${s3Key}`, error);
// Don't throw error here, continue with upload
console.log(`Deleted from S3: ${s3Key}`);
} catch (err) {
console.error(`Failed to delete from S3: ${s3Key}`, err);
}
}
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
let s3Key: string;
// Upload new file
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string): Promise<string> {
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
const s3Key = `${prefix}/${uniqueKey}`;
// If existing URL provided, use the same S3 key to replace the file
if (existingUrl) {
s3Key = getS3KeyFromUrl(existingUrl);
// Delete existing file first
await deleteFromS3(s3Key);
} else {
// Generate new unique key for new file
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
s3Key = `${prefix}/${uniqueKey}`;
}
// Upload new file (replaces existing if same key)
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
@@ -58,253 +45,160 @@ async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string
ACL: 'private'
}).promise();
console.log(`File uploaded to S3: ${s3Key}`);
console.log(`Uploaded to S3: ${s3Key}`);
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
// AUTH
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
if (!token) throw new ApiError(401, 'Missing token');
const user = await verifyHostToken(token);
// 2) Content-Type check
// Content-Type
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
if (!contentType?.startsWith("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
if (!event.isBase64Encoded)
throw new ApiError(400, "Body must be base64 encoded");
if (!event.isBase64Encoded) throw new ApiError(400, "Body must be base64 encoded");
const bodyBuffer = Buffer.from(event.body!, "base64");
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 3) Parse multipart data
// Parse multipart
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
// Skip if no filename (empty file field)
if (!filename) {
file.resume();
return;
}
if (!filename) return file.resume();
const chunks: Buffer[] = [];
let totalSize = 0;
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
let size = 0;
file.on('data', chunk => {
size += chunk.length;
if (size > 5 * 1024 * 1024)
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit`));
file.on('data', (chunk) => {
totalSize += chunk.length;
if (totalSize > MAX_SIZE) {
file.resume();
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit.`));
}
chunks.push(chunk);
});
file.on('end', () => {
// Only add file if we have data
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
fieldName: fieldname
});
}
});
file.on('error', (err) => {
reject(new ApiError(400, `File upload error: ${err.message}`));
});
});
bb.on('field', (fieldname, val) => {
// Handle empty or null values
if (val === '' || val === 'null' || val === 'undefined') {
fields[fieldname] = null;
} else {
try {
fields[fieldname] = JSON.parse(val);
} catch {
fields[fieldname] = val;
}
}
});
bb.on('close', () => {
console.log("✅ Busboy parsing completed");
console.log("📌 Fields:", fields);
console.log("📁 Files:", files.length);
resolve();
});
bb.on('error', (err) => {
console.error("❌ Busboy error:", err);
reject(new ApiError(400, `Multipart parsing error: ${err.message}`));
try { fields[fieldname] = JSON.parse(val); }
catch { fields[fieldname] = val; }
});
bb.on('close', resolve);
bb.on('error', err => reject(new ApiError(400, err.message)));
bb.end(bodyBuffer);
});
// 4) Extract required fields - only activityXid, pqqQuestionXid, pqqAnswerXid are required
// Required fields
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
// Comments and files are optional
const comments = fields.comments || null;
// Validate required fields
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Valid activityXid is required");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Valid pqqQuestionXid is required");
if (pqqQuestionXid !== LAST_QUESTION_ID.Q_ID) throw new ApiError(400, "Wrong question id.")
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
// console.log(`📝 Processing - Activity: ${activityXid}, Question: ${pqqQuestionXid}, Answer: ${pqqAnswerXid}`);
// console.log(`💬 Comments: ${comments ? 'Provided' : 'Not provided'}`);
// console.log(`📎 Files: ${files.length}`);
// 5) UPSERT: Check if header already exists for this combination
const existingHeader = await pqqService.findHeaderByCompositeKey(
activityXid,
pqqQuestionXid,
pqqAnswerXid
);
if (!activityXid || !pqqQuestionXid || !pqqAnswerXid)
throw new ApiError(400, "Missing required fields");
// UPSERT header
const existingHeader = await hostService.findHeaderByCompositeKey(activityXid, pqqQuestionXid);
let header;
if (existingHeader) {
console.log("🔄 Updating existing PQQ header");
// Update existing header (comments can be null)
header = await pqqService.updateHeader(
existingHeader.id,
comments
);
header = await hostService.updateHeader(existingHeader.id, pqqAnswerXid, comments);
} else {
console.log("🆕 Creating new PQQ header");
// Create new header (comments can be null)
header = await pqqService.createHeader(
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments
);
header = await hostService.createHeader(activityXid, pqqQuestionXid, pqqAnswerXid, comments);
}
// Calculate score after answer submission
const score = await pqqService.calculatePqqScoreForUser(activityXid);
// SCORE
const score = await hostService.calculatePqqScoreForUser(activityXid);
// 6) Get existing supporting files for this header
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
// Existing supporting files
const existingSupportingFiles = await hostService.getSupportingFilesByHeaderId(header.id);
// 7) Handle file UPSERT - only if files are provided
const uploadedFiles: any[] = [];
// Read deletedFiles from frontend
const deletedFiles = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
const deleteResults = [];
const addResults = [];
// DELETE explicitly requested files (Case 3)
if (deletedFiles.length > 0) {
for (const del of deletedFiles) {
const id = Number(del.id);
const record = existingSupportingFiles.find(f => f.id === id);
if (!record) continue;
// Delete from S3
if (record.mediaFileName) {
const key = getS3KeyFromUrl(record.mediaFileName);
await deleteFromS3(key);
}
// Delete from DB
await hostService.deleteSupportingFile(record.id);
deleteResults.push({ id: record.id, deleted: true });
}
}
// ADD new uploaded files (Case 1 + Case 3 new files)
if (files.length > 0) {
console.log("📤 Processing file uploads...");
for (let i = 0; i < files.length; i++) {
const file = files[i];
const existingFile = existingSupportingFiles[i] || null;
for (const file of files) {
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`,
existingFile ? existingFile.mediaFileName : undefined
`ActivityOnboarding/supportings/${activityXid}`
);
let supporting;
if (existingFile) {
// Update existing supporting file record
supporting = await pqqService.updateSupportingFile(
existingFile.id,
file.mimeType,
url
);
console.log(`🔄 Updated supporting file: ${existingFile.id}`);
} else {
// Create new supporting file record
supporting = await pqqService.addSupportingFile(
header.id,
file.mimeType,
url
);
console.log(`🆕 Created new supporting file: ${supporting.id}`);
}
uploadedFiles.push(supporting);
}
// 8) Delete any remaining existing files that weren't replaced
if (existingSupportingFiles.length > files.length) {
const filesToDelete = existingSupportingFiles.slice(files.length);
console.log(`🗑️ Deleting ${filesToDelete.length} unused supporting files`);
for (const fileToDelete of filesToDelete) {
await pqqService.deleteSupportingFile(fileToDelete.id);
// Also delete from S3
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
await deleteFromS3(s3Key);
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
}
}
} else {
console.log("📭 No files provided in request");
// If no files provided but existing files exist, delete them (cleanup)
if (existingSupportingFiles.length > 0) {
console.log(`🗑️ No new files provided, deleting ${existingSupportingFiles.length} existing files`);
for (const fileToDelete of existingSupportingFiles) {
await pqqService.deleteSupportingFile(fileToDelete.id);
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
await deleteFromS3(s3Key);
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
}
const newRec = await hostService.addSupportingFile(header.id, file.mimeType, url);
addResults.push(newRec);
}
}
// 9) Prepare response
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
const getAllUpdatedQuestionResponse = await hostService.getAllPQUpdatedResponse(activityXid)
// CASE 2 — NO deletion & NO new files => DO NOTHING to existing files
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
body: JSON.stringify({
success: true,
message: responseMessage,
message: existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully",
data: {
headerId: header.id,
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments: comments,
comments,
score,
files: {
uploaded: uploadedFiles,
total: uploadedFiles.length
},
operation: existingHeader ? 'updated' : 'created',
fileOperation: files.length > 0 ?
(existingSupportingFiles.length > 0 ? 'replaced' : 'added') :
(existingSupportingFiles.length > 0 ? 'removed' : 'unchanged')
getAllUpdatedQuestionResponse
}
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
} catch (err: any) {
console.error("❌ Error:", err);
throw err;
}
});

View File

@@ -1,14 +1,11 @@
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { getPqqByQuestionIdQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
@@ -23,11 +20,12 @@ export const handler = safeHandler(async (
const userInfo = await verifyMinglarAdminHostToken(token);
const userId = Number(userInfo.id);
// Parse and validate query params using Zod
const { question_xid, activity_xid } = parseQueryParams(
event.queryStringParameters,
getPqqByQuestionIdQuerySchema
);
const question_xid = Number(event.queryStringParameters?.question_xid);
const activity_xid = Number(event.queryStringParameters?.activity_xid);
if (!question_xid || !activity_xid) {
throw new ApiError(400, "Question and activity xid are required.")
}
// Fetch user with their HostHeader stepper info
const pqqQuestionDetails = await hostService.getPQQQuestionDetail(question_xid, activity_xid);

View File

@@ -1,14 +1,11 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { HostService } from '../../../services/host.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { getLatestPqqQuestionQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
@@ -23,20 +20,26 @@ export const handler = safeHandler(async (
const userInfo = await verifyHostToken(token);
const userId = Number(userInfo.id);
// Parse and validate query params using Zod
const { activity_xid } = parseQueryParams(
event.queryStringParameters,
getLatestPqqQuestionQuerySchema
);
const activity_xid = event.queryStringParameters?.activity_xid
? Number(event.queryStringParameters.activity_xid)
: null;
// Validate it
if (!activity_xid || isNaN(activity_xid)) {
throw new ApiError(400, "Activity id is required and must be a number.");
}
let result = null;
// Fetch user with their HostHeader stepper info
const pqqQuestionDetails = await hostService.getLatestQuestionDetailsPQQ(activity_xid);
const result = {
pqqQuestionXid: pqqQuestionDetails.pqqQuestionXid,
pqqAnswerXid: pqqQuestionDetails.pqqAnswerXid,
pqqSubCategoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategoryXid,
categoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategories.categoryXid
if (pqqQuestionDetails) {
result = {
pqqQuestionXid: pqqQuestionDetails.pqqQuestionXid,
pqqAnswerXid: pqqQuestionDetails.pqqAnswerXid || null,
pqqSubCategoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategoryXid || null,
categoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategories.categoryXid || null
}
}
return {

View File

@@ -1,14 +1,11 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { createActivitySchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -19,13 +16,23 @@ export const handler = safeHandler(async (
const userInfo = await verifyHostToken(token);
// Parse and validate request body using Zod
const { activityTypeXid, frequenciesXid } = parseBody(event.body, createActivitySchema);
let body: any = {};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (err) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityTypeXid, frequenciesXid } = body;
if (!activityTypeXid) {
throw new ApiError(400, 'activityTypeXid is required');
}
await hostService.createActivity(
userInfo.id,
activityTypeXid,
frequenciesXid,
Number(activityTypeXid),
frequenciesXid ? Number(frequenciesXid) : undefined,
);
return {

View File

@@ -0,0 +1,300 @@
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
// Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(bucketBaseUrl, '');
}
// Function to delete file from S3
async function deleteFromS3(s3Key: string): Promise<void> {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: s3Key,
}).promise();
console.log(`✅ File deleted from S3: ${s3Key}`);
} catch (error) {
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
// continue — we don't want S3 deletion failure to crash the whole request
}
}
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
// We intentionally do NOT reuse old key. If existingUrl is provided we delete old file and create a new random key.
if (existingUrl) {
try {
const oldKey = getS3KeyFromUrl(existingUrl);
await deleteFromS3(oldKey);
} catch (err) {
console.warn('Warning deleting existingUrl before upload', err);
}
}
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
const s3Key = `${prefix}/${uniqueKey}`;
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private'
}).promise();
console.log(`✅ File uploaded to S3: ${s3Key}`);
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
// 2) Content-Type check
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
if (!contentType?.includes("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
// 3) Body decoding
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body!, "base64")
: Buffer.from(event.body!, "binary");
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 4) Parse multipart data
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
if (!filename) {
file.resume();
return;
}
const chunks: Buffer[] = [];
let size = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on("data", (chunk) => {
size += chunk.length;
if (size > MAX_SIZE) {
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
return;
}
chunks.push(chunk);
});
file.on("end", () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
}
});
file.on("error", (err) =>
reject(new ApiError(400, `File upload error: ${err.message}`))
);
});
bb.on("field", (fieldname, val) => {
console.log(`FIELD RAW: ${fieldname} =`, val);
if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null;
else {
try {
const cleaned = val.trim();
// If it starts and ends with quotes, remove them
const withoutQuotes =
(cleaned.startsWith('"') && cleaned.endsWith('"'))
? cleaned.slice(1, -1)
: cleaned;
fields[fieldname] = JSON.parse(withoutQuotes);
} catch {
fields[fieldname] = val;
}
}
});
bb.on("close", () => resolve());
bb.on("error", (err) =>
reject(new ApiError(400, `Multipart parsing error: ${err.message}`))
);
// IMPORTANT FIX for HTTP API
bb.write(bodyBuffer);
bb.end();
});
// 5) Extract required fields
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = fields.comments || null;
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Valid activityXid is required");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Valid pqqQuestionXid is required");
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
// 6) UPSERT header
const existingHeader = await hostService.findHeaderByCompositeKey(
activityXid,
pqqQuestionXid,
);
let header;
if (existingHeader) {
console.log("🔄 Updating existing PQQ header");
header = await hostService.updateHeader(
existingHeader.id,
pqqAnswerXid,
comments
);
} else {
console.log("🆕 Creating new PQQ header");
header = await hostService.createHeader(
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments
);
}
// 7) Get existing supporting files
const existingSupportingFiles = await hostService.getSupportingFilesByHeaderId(header.id);
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
// 8) Parse incoming control fields
// fields.deletedFiles should be array like [{ id: number, url: string }, ...] or null
const deletedFiles: Array<{ id: number; url?: string }> = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
// fields.existingFiles can be an array of urls; we accept it but do not require it
const existingFilesFromFront: string[] = Array.isArray(fields.existingFiles) ? fields.existingFiles : [];
// Prepare response trackers
const deletedResults: Array<{ id: number; success: boolean; reason?: string }> = [];
const addedResults: Array<any> = [];
// 9) Handle explicit deletions (ONLY delete ids provided in deletedFiles)
if (deletedFiles.length > 0) {
console.log(`🗑️ Processing ${deletedFiles.length} explicit deletions`);
// Build a map of existing supporting files by id for quick lookup
const existingById = new Map<number, any>();
for (const f of existingSupportingFiles) {
existingById.set(f.id, f);
}
for (const del of deletedFiles) {
const id = Number(del.id);
if (!id || !existingById.has(id)) {
deletedResults.push({ id, success: false, reason: 'Not found or invalid id' });
continue;
}
const record = existingById.get(id);
try {
// delete from s3
if (record.mediaFileName) {
const s3Key = getS3KeyFromUrl(record.mediaFileName);
await deleteFromS3(s3Key);
}
// delete DB record
await hostService.deleteSupportingFile(record.id);
deletedResults.push({ id: record.id, success: true });
console.log(`🗑️ Deleted supporting file record ${record.id}`);
} catch (err: any) {
console.error(`❌ Failed to delete supporting file id ${id}`, err);
deletedResults.push({ id, success: false, reason: err.message || 'delete failed' });
}
}
} else {
console.log(' No explicit deletions requested (deletedFiles empty)');
}
// 10) Handle new uploaded files (these are ALWAYS added as new rows)
if (files.length > 0) {
console.log(`📤 Processing ${files.length} uploaded new file(s)`);
for (const file of files) {
try {
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`
);
// create DB record
const supporting = await hostService.addSupportingFile(
header.id,
file.mimeType,
url
);
addedResults.push(supporting);
console.log(`🆕 Created new supporting file record: ${supporting.id}`);
} catch (err: any) {
console.error('❌ Error uploading/creating supporting file', err);
// push failure result but continue processing other files
addedResults.push({ success: false, reason: err.message || 'upload/create failed' });
}
}
} else {
console.log('📭 No new files uploaded in request');
}
// NOTE: We DO NOT delete or modify existing supporting files that were not listed in deletedFiles.
// This satisfies your Case 2: "if no files are provided, do not touch existing supporting files".
const allPQPQuestionAnswerResponse = await hostService.getAllPQUpdatedResponse(activityXid)
// 11) Compose response
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: responseMessage,
data: {
responseOfUpdatedData: allPQPQuestionAnswerResponse,
operation: existingHeader ? 'updated' : 'created',
// summary label for UI convenience:
fileOperation: (deletedResults.length > 0 || addedResults.length > 0) ? 'modified' : 'unchanged'
}
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

@@ -0,0 +1,40 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const pqqService = new HostService(prismaClient);
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
const activity_xid = event.queryStringParameters?.activity_xid
? Number(event.queryStringParameters.activity_xid)
: null;
await pqqService.submitpqqforreview(Number(activity_xid), Number(user.id))
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: "Your PQQ has been submitted for review.",
data: null
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

@@ -1,16 +1,15 @@
import config from '@/config/config';
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const prisma = new PrismaService();
const pqqService = new HostService(prisma);
const pqqService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
@@ -30,34 +29,24 @@ async function deleteFromS3(s3Key: string): Promise<void> {
console.log(`✅ File deleted from S3: ${s3Key}`);
} catch (error) {
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
// Don't throw error here, continue with upload
// continue — we don't want S3 deletion failure to crash the whole request
}
}
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
let s3Key: string;
// If existing URL provided, use the same S3 key to replace the file
// if (existingUrl) {
// s3Key = getS3KeyFromUrl(existingUrl);
// // Delete existing file first
// await deleteFromS3(s3Key);
// } else {
// // Generate new unique key for new file
// const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
// s3Key = `${prefix}/${uniqueKey}`;
// }
// We intentionally do NOT reuse old key. If existingUrl is provided we delete old file and create a new random key.
if (existingUrl) {
// Delete old file, but DO NOT reuse its name
const oldKey = getS3KeyFromUrl(existingUrl);
await deleteFromS3(oldKey);
try {
const oldKey = getS3KeyFromUrl(existingUrl);
await deleteFromS3(oldKey);
} catch (err) {
console.warn('Warning deleting existingUrl before upload', err);
}
}
// Create new key always
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
s3Key = `${prefix}/${uniqueKey}`;
const s3Key = `${prefix}/${uniqueKey}`;
// Upload new file (replaces existing if same key)
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
@@ -82,7 +71,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
if (!contentType?.includes("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
// 3) Body decoding (FIXED same as addCompanyDetails)
// 3) Body decoding
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body!, "base64")
: Buffer.from(event.body!, "binary");
@@ -90,7 +79,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 4) Parse multipart data (FIXED using bb.write + bb.end exactly like working lambda)
// 4) Parse multipart data
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
@@ -152,41 +141,32 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
bb.end();
});
// 4) Extract required fields - only activityXid, pqqQuestionXid, pqqAnswerXid are required
// 5) Extract required fields
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
// Comments and files are optional
const comments = fields.comments || null;
// Validate required fields
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Valid activityXid is required");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Valid pqqQuestionXid is required");
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
// console.log(`📝 Processing - Activity: ${activityXid}, Question: ${pqqQuestionXid}, Answer: ${pqqAnswerXid}`);
// console.log(`💬 Comments: ${comments ? 'Provided' : 'Not provided'}`);
// console.log(`📎 Files: ${files.length}`);
// 5) UPSERT: Check if header already exists for this combination
// 6) UPSERT header
const existingHeader = await pqqService.findHeaderByCompositeKey(
activityXid,
pqqQuestionXid,
pqqAnswerXid
);
let header;
if (existingHeader) {
console.log("🔄 Updating existing PQQ header");
// Update existing header (comments can be null)
header = await pqqService.updateHeader(
existingHeader.id,
pqqAnswerXid,
comments
);
} else {
console.log("🆕 Creating new PQQ header");
// Create new header (comments can be null)
header = await pqqService.createHeader(
activityXid,
pqqQuestionXid,
@@ -195,79 +175,93 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
);
}
// 6) Get existing supporting files for this header
// 7) Get existing supporting files
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
// 7) Handle file UPSERT - only if files are provided
const uploadedFiles: any[] = [];
// 8) Parse incoming control fields
// fields.deletedFiles should be array like [{ id: number, url: string }, ...] or null
const deletedFiles: Array<{ id: number; url?: string }> = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
// fields.existingFiles can be an array of urls; we accept it but do not require it
const existingFilesFromFront: string[] = Array.isArray(fields.existingFiles) ? fields.existingFiles : [];
// Prepare response trackers
const deletedResults: Array<{ id: number; success: boolean; reason?: string }> = [];
const addedResults: Array<any> = [];
// 9) Handle explicit deletions (ONLY delete ids provided in deletedFiles)
if (deletedFiles.length > 0) {
console.log(`🗑️ Processing ${deletedFiles.length} explicit deletions`);
// Build a map of existing supporting files by id for quick lookup
const existingById = new Map<number, any>();
for (const f of existingSupportingFiles) {
existingById.set(f.id, f);
}
for (const del of deletedFiles) {
const id = Number(del.id);
if (!id || !existingById.has(id)) {
deletedResults.push({ id, success: false, reason: 'Not found or invalid id' });
continue;
}
const record = existingById.get(id);
try {
// delete from s3
if (record.mediaFileName) {
const s3Key = getS3KeyFromUrl(record.mediaFileName);
await deleteFromS3(s3Key);
}
// delete DB record
await pqqService.deleteSupportingFile(record.id);
deletedResults.push({ id: record.id, success: true });
console.log(`🗑️ Deleted supporting file record ${record.id}`);
} catch (err: any) {
console.error(`❌ Failed to delete supporting file id ${id}`, err);
deletedResults.push({ id, success: false, reason: err.message || 'delete failed' });
}
}
} else {
console.log(' No explicit deletions requested (deletedFiles empty)');
}
// 10) Handle new uploaded files (these are ALWAYS added as new rows)
if (files.length > 0) {
console.log("📤 Processing file uploads...");
for (let i = 0; i < files.length; i++) {
const file = files[i];
const existingFile = existingSupportingFiles[i] || null;
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`,
existingFile ? existingFile.mediaFileName : undefined
);
let supporting;
if (existingFile) {
// Update existing supporting file record
supporting = await pqqService.updateSupportingFile(
existingFile.id,
console.log(`📤 Processing ${files.length} uploaded new file(s)`);
for (const file of files) {
try {
const url = await uploadToS3(
file.buffer,
file.mimeType,
url
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`
);
console.log(`🔄 Updated supporting file: ${existingFile.id}`);
} else {
// Create new supporting file record
supporting = await pqqService.addSupportingFile(
// create DB record
const supporting = await pqqService.addSupportingFile(
header.id,
file.mimeType,
url
);
console.log(`🆕 Created new supporting file: ${supporting.id}`);
}
uploadedFiles.push(supporting);
}
// 8) Delete any remaining existing files that weren't replaced
if (existingSupportingFiles.length > files.length) {
const filesToDelete = existingSupportingFiles.slice(files.length);
console.log(`🗑️ Deleting ${filesToDelete.length} unused supporting files`);
for (const fileToDelete of filesToDelete) {
await pqqService.deleteSupportingFile(fileToDelete.id);
// Also delete from S3
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
await deleteFromS3(s3Key);
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
addedResults.push(supporting);
console.log(`🆕 Created new supporting file record: ${supporting.id}`);
} catch (err: any) {
console.error('❌ Error uploading/creating supporting file', err);
// push failure result but continue processing other files
addedResults.push({ success: false, reason: err.message || 'upload/create failed' });
}
}
} else {
console.log("📭 No files provided in request");
// If no files provided but existing files exist, delete them (cleanup)
if (existingSupportingFiles.length > 0) {
console.log(`🗑️ No new files provided, deleting ${existingSupportingFiles.length} existing files`);
for (const fileToDelete of existingSupportingFiles) {
await pqqService.deleteSupportingFile(fileToDelete.id);
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
await deleteFromS3(s3Key);
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
}
}
console.log('📭 No new files uploaded in request');
}
// 9) Prepare response
// NOTE: We DO NOT delete or modify existing supporting files that were not listed in deletedFiles.
// This satisfies your Case 2: "if no files are provided, do not touch existing supporting files".
// 11) Compose response
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
return {
@@ -284,15 +278,15 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments: comments,
comments,
files: {
uploaded: uploadedFiles,
total: uploadedFiles.length
added: addedResults,
deleted: deletedResults,
existingKeptCount: (existingSupportingFiles.length - deletedResults.filter(d => d.success).length)
},
operation: existingHeader ? 'updated' : 'created',
fileOperation: files.length > 0 ?
(existingSupportingFiles.length > 0 ? 'replaced' : 'added') :
(existingSupportingFiles.length > 0 ? 'removed' : 'unchanged')
// summary label for UI convenience:
fileOperation: (deletedResults.length > 0 || addedResults.length > 0) ? 'modified' : 'unchanged'
}
})
};

View File

@@ -1,14 +1,11 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { updateSuggestionReviewedSchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -19,8 +16,18 @@ export const handler = safeHandler(async (
const userInfo = await verifyHostToken(token);
// Parse and validate body using Zod
const { activityPqqHeaderXid, activityPQQSuggestionId } = parseBody(event.body, updateSuggestionReviewedSchema);
let body: any = {};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (err) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityPqqHeaderXid, activityPQQSuggestionId } = body;
if (!activityPqqHeaderXid) {
throw new ApiError(400, 'activityPqqHeaderXid is required');
}
await hostService.markPQQSuggestionReviewed(
userInfo.id,

View File

@@ -1,12 +1,11 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
/**
* Add suggestion handler for host applications

View File

@@ -1,14 +1,11 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { createPasswordSchema } from '../../../../../common/utils/validation/host/auth.validation';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -24,8 +21,30 @@ export const handler = safeHandler(async (
const userInfo = await verifyHostToken(token);
const user_xid = userInfo.id;
// Parse and validate request body using Zod
const { password } = parseBody(event.body, createPasswordSchema);
// Parse request body
let body: { password?: string; confirmPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { password, confirmPassword } = body;
if (!password || !confirmPassword) {
throw new ApiError(400, 'Password and confirm password are required');
}
// Validate password match
if (password !== confirmPassword) {
throw new ApiError(400, 'Password and confirm password do not match');
}
// Validate password length
if (password.length < 8) {
throw new ApiError(400, 'Password must be at least 8 characters long');
}
await hostService.createPassword(user_xid, password);

View File

@@ -1,26 +1,36 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { HostService } from '../../../services/host.service';
import { TokenService } from '../../../services/token.service';
import { GetHostLoginResponseDTO } from '../../../dto/host.dto';
import ApiError from '../../../../../common/utils/helper/ApiError';
import * as bcrypt from 'bcryptjs';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostLoginSchema } from '../../../../../common/utils/validation/host/auth.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const tokenService = new TokenService(prismaService);
const hostService = new HostService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse and validate request body using Zod
const { emailAddress, userPassword } = parseBody(event.body, hostLoginSchema);
// Parse request body
let body: { emailAddress?: string; userPassword?: string };
const loginForHost = await hostService.loginForHost(emailAddress, userPassword);
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, userPassword } = body;
if (!emailAddress || !userPassword) {
throw new ApiError(400, 'Email and password are required');
}
const emailToLowerCase = emailAddress.toLowerCase()
const loginForHost = await hostService.loginForHost(emailToLowerCase, userPassword);
if (!loginForHost) {
throw new ApiError(400, 'Failed to login');
@@ -30,15 +40,6 @@ export const handler = safeHandler(async (
throw new ApiError(401, 'Invalid credentials');
}
const matchPassword = await bcrypt.compare(
userPassword,
loginForHost.userPassword
);
if (!matchPassword) {
throw new ApiError(401, 'Invalid credentials');
}
const generateTokenForHost = await tokenService.generateAuthToken(
loginForHost.id
);

View File

@@ -1,12 +1,11 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { MinglarService } from '../../../../minglaradmin/services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
/**
* Get suggestions handler

View File

@@ -1,18 +1,15 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import * as bcrypt from 'bcryptjs';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { ROLE } from '../../../../../common/utils/constants/common.constant';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import * as bcrypt from 'bcryptjs';
import { OtpGeneratorSixDigit } from '../../../../../common/utils/helper/OtpGenerator';
import { encryptUserId } from '../../../../../common/utils/helper/CodeGenerator';
import { OtpGeneratorSixDigit } from '../../../../../common/utils/helper/OtpGenerator';
import { HostService } from '../../../services/host.service';
import { sendOtpEmailForHost } from '../../../services/sendOTPEmail.service';
import { ROLE } from '../../../../../common/utils/constants/common.constant';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostSignUpSchema } from '../../../../../common/utils/validation/host/auth.validation';
import { sendOtpEmailForHost } from '@/modules/host/services/sendOTPEmail.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export async function generateHostRefNumber(tx: any) {
const lastrecord = await tx.user.findFirst({
@@ -33,13 +30,27 @@ export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse and validate request body using Zod
const { email } = parseBody(event.body, hostSignUpSchema);
// Parse request body
let body: { email?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { email } = body;
if (!email) {
throw new ApiError(400, 'Email is required');
}
const emailToLowerCase = email.toLowerCase()
// Use a single transaction for user creation/lookup and OTP storage
const transactionResult = await prismaService.$transaction(async (tx) => {
const transactionResult = await prismaClient.$transaction(async (tx) => {
const user = await tx.user.findUnique({
where: { emailAddress: email },
where: { emailAddress: emailToLowerCase },
select: { emailAddress: true, id: true, userPassword: true },
});
@@ -57,7 +68,7 @@ export const handler = safeHandler(async (
} else {
// create new user record within the transaction
newUserLocal = await tx.user.create({
data: { emailAddress: email, roleXid: ROLE.HOST, userRefNumber: referenceNumber },
data: { emailAddress: emailToLowerCase, roleXid: ROLE.HOST, userRefNumber: referenceNumber },
});
}
@@ -92,7 +103,7 @@ export const handler = safeHandler(async (
}
// Send OTP email outside the DB transaction
// await sendOtpEmailForHost(transactionResult.newUser.emailAddress, transactionResult.otp);
await sendOtpEmailForHost(transactionResult.newUser.emailAddress, transactionResult.otp);
return {
statusCode: 200,

View File

@@ -1,10 +1,10 @@
// modules/host/handlers/addCompanyDetails.ts
import config from '@/config/config';
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import {
@@ -15,8 +15,17 @@ import {
import { HostService } from '../../../services/host.service';
import { sendEmailToAM, sendEmailToMinglarAdmin } from '../../../services/sendHostResubmitEmailToAM.service';
const prisma = new PrismaService();
const hostService = new HostService(prisma);
const hostService = new HostService(prismaClient);
function getExtensionFromMime(mimeType: string) {
const map: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'application/pdf': 'pdf',
'image/webp': 'webp',
};
return map[mimeType] || 'bin';
}
const s3 = new AWS.S3({
region: config.aws.region,
@@ -50,6 +59,25 @@ function cleanEmptyStrings(obj: any) {
return cleaned;
}
function getS3KeyFromUrl(url: string): string {
const base = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(base, "");
}
async function deleteFromS3(key: string) {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: key
}).promise();
console.log("✅ Deleted from S3:", key);
} catch (err) {
console.error("❌ Failed to delete from S3:", key, err);
}
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
@@ -112,6 +140,9 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
bb.end();
});
const deletedFiles = normalizeJsonField(fields, "deletedFiles") || [];
const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || [];
/** 4) Extract and clean isDraft flag */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
@@ -128,13 +159,22 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}
}
if (
companyDetailsRaw.parentCompany &&
Object.values(companyDetailsRaw.parentCompany).every(
(v) => v === undefined || v === null
)
) {
companyDetailsRaw.parentCompany = null;
}
/** 6) Profile update if provided */
if (fields.userProfile) {
const userProfileRaw = normalizeJsonField(fields, 'userProfile');
if (userProfileRaw) {
const { firstName, lastName, mobileNumber } = userProfileRaw;
await prisma.user.update({
await prismaClient.user.update({
where: { id: userInfo.id },
data: {
...(firstName && { firstName }),
@@ -178,13 +218,8 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const file = files.find((f) => f.fieldName === doc.fieldName);
// In DRAFT mode → allow missing documents
if (isDraft && !file) {
return { ...doc, file: null };
}
// In FINAL mode → file must exist
if (!file) {
throw new ApiError(400, `File not found for field: ${doc.fieldName}`);
return { ...doc, file: null };
}
return { ...doc, file };
@@ -213,9 +248,65 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
parsedParentCompany = parsedCompany.parentCompany || null;
}
/** 9.5) DELETE DOCUMENTS IF REQUESTED **/
if (Array.isArray(deletedFiles) && deletedFiles.length > 0) {
console.log(`🗑️ Deleting ${deletedFiles.length} documents...`);
for (const del of deletedFiles) {
const id = Number(del.id);
const url = del.url;
if (!id || !url) {
console.log("❌ Invalid delete entry:", del);
continue;
}
// Extract S3 key
const s3Key = getS3KeyFromUrl(url);
// Delete from S3
await deleteFromS3(s3Key);
// Delete from DB
await prismaClient.hostDocuments.delete({
where: { id }
});
console.log(`🗑️ Deleted host document ID ${id}`);
}
}
/** 9.6) DELETE PARENT DOCUMENTS **/
if (parsedCompany.isSubsidairy && Array.isArray(parentDeletedFiles) && parentDeletedFiles.length > 0) {
console.log(`🗑️ Deleting ${parentDeletedFiles.length} PARENT documents...`);
for (const del of parentDeletedFiles) {
const id = Number(del.id);
const url = del.url;
if (!id || !url) {
console.log("⚠️ Invalid parent delete entry:", del);
continue;
}
const s3Key = getS3KeyFromUrl(url);
// Delete S3
await deleteFromS3(s3Key);
// Delete DB
await prismaClient.hostParenetDocuments.delete({
where: { id }
});
console.log(`🗑️ Deleted PARENT document ID ${id}`);
}
}
/** 11) UPLOAD DOCUMENTS */
async function uploadToS3(buffer, mimeType, originalName, folderType, documentTypeXid?, fieldName?) {
const ext = originalName.split('.').pop() || 'jpg';
// const ext = originalName.split('.').pop() || 'jpg';
const ext = getExtensionFromMime(mimeType);
let s3Key = '';
@@ -249,7 +340,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
/** Upload host docs */
const uploadedHostDocs: Array<any> = [];
for (const doc of hostDocs) {
if (isDraft && !doc.file) continue;
if (!doc.file) continue;
const path = await uploadToS3(
doc.file.buffer,
@@ -270,7 +361,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
/** Upload parent docs */
const uploadedParentDocs: Array<any> = [];
for (const doc of parentDocs) {
if (!doc.file && isDraft) continue; // skip missing files in draft mode
if (!doc.file) continue; // skip missing files in draft mode
const path = await uploadToS3(
doc.file.buffer,
@@ -289,31 +380,67 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}
/** UPLOAD LOGO (if provided) */
const logoFile = files.find((f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile');
if (logoFile) {
const logoUrl = await uploadToS3(logoFile.buffer, logoFile.mimeType, logoFile.fileName, 'logo');
const logoFile = files.find(
(f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
);
if (logoFile && logoFile.buffer && logoFile.fileName) {
const logoUrl = await uploadToS3(
logoFile.buffer,
logoFile.mimeType,
logoFile.fileName,
'logo'
);
parsedCompany.logoPath = logoUrl;
}
/** UPLOAD PARENT COMPANY LOGO (if provided) */
const parentLogoFile = files.find((f) => f.fieldName === 'parentCompanyLogo');
if (parentLogoFile) {
const parentLogoFile = files.find(
(f) => f.fieldName === 'parentCompanyLogo'
);
if (parentLogoFile && parentLogoFile.buffer && parentLogoFile.mimeType) {
// 🔒 Only upload when an actual file is present
const parentLogoUrl = await uploadToS3(
parentLogoFile.buffer,
parentLogoFile.mimeType,
parentLogoFile.fileName,
parentLogoFile.fileName, // safe here because it's a real file
'parent_company_logo',
);
if (parsedParentCompany) {
parsedParentCompany.logoPath = parentLogoUrl;
} else {
// if no parent object exists yet (drafts or other flows), attach it safely
parsedParentCompany = parsedParentCompany || {};
parsedParentCompany.logoPath = parentLogoUrl;
parsedParentCompany = {
logoPath: parentLogoUrl,
};
}
}
if (parsedCompany.cityXid) {
const city = await prismaClient.cities.findUnique({
where: { id: Number(parsedCompany.cityXid) }
});
if (!city) {
throw new ApiError(400, `City with ID ${parsedCompany.cityXid} not found`);
}
}
if (!parsedCompany.isSubsidairy) {
const parentDocuments = await hostService.getParentDocumentsByHostId(userInfo.id);
if (parentDocuments.length > 0) {
for (const doc of parentDocuments) {
try {
const s3Key = getS3KeyFromUrl(doc.filePath);
await deleteFromS3(s3Key);
} catch (e) {
console.error("S3 delete failed:", doc.filePath, e);
}
}
}
await hostService.deleteExistingParentRecords(userInfo.id)
}
/** 12) SAVE / UPDATE HOST ENTRY */
const createdOrUpdated = await hostService.addOrUpdateCompanyDetails(
userInfo.id,

View File

@@ -1,13 +1,12 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { hostBankDetailsSchema } from '@/common/utils/validation/host/addPaymentDetails.validation';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { hostBankDetailsSchema } from '../../../../../common/utils/validation/host/addPaymentDetails.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -33,7 +32,7 @@ export const handler = safeHandler(async (
}
// Parse request body
let body: { bankXid?: number; bankBranchXid?: number; accountNumber?: string; confirmAccountNumber?: string; accountHolderName?: string; ifscCode?: string; currencyXid?: number };
let body: { bankXid?: number; bankBranchXid?: number; accountNumber?: string; confirmAccountNumber?: string; accountHolderName?: string; currencyXid?: number };
try {
body = event.body ? JSON.parse(event.body) : {};
@@ -44,7 +43,7 @@ export const handler = safeHandler(async (
// ✅ Validate payload using Zod
const validationResult = hostBankDetailsSchema.safeParse({
...(body as object),
hostXid: host.id, // inject hostId from token (not from user input)
hostXid: host.host.id, // inject hostId from token (not from user input)
});
if (!validationResult.success) {
@@ -54,7 +53,16 @@ export const handler = safeHandler(async (
const validatedData = validationResult.data;
await hostService.addPaymentDetails(validatedData);
// Fetch IFSC code from bank branch
const bankBranch = await hostService.getBankBranchById(validatedData.bankBranchXid);
if (!bankBranch) {
throw new ApiError(404, 'Bank branch not found');
}
await hostService.addPaymentDetails({
...validatedData,
ifscCode: bankBranch.ifscCode,
});
return {
statusCode: 200,

View File

@@ -1,24 +1,36 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { TokenService } from '../../../services/token.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { verifyOtpWithEmailSchema } from '../../../../../common/utils/validation/host/auth.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const tokenService = new TokenService(prismaService);
const hostService = new HostService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse and validate request body using Zod
const { email, otp } = parseBody(event.body, verifyOtpWithEmailSchema);
// Parse request body
let body: { email?: string; otp?: string };
await hostService.verifyHostOtp(email, otp);
const user = await hostService.getHostByEmail(email);
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { email, otp } = body;
if (!email || !otp) {
throw new ApiError(400, 'Email and OTP are required');
}
const emailToLowerCase = email.toLowerCase();
await hostService.verifyHostOtp(emailToLowerCase, otp);
const user = await hostService.getHostByEmail(emailToLowerCase);
const generateTokenForHost = await tokenService.generateAuthToken(
user.id
);

View File

@@ -1,12 +1,11 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
import { HostService } from '../services/host.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
@@ -26,11 +25,7 @@ export const handler = safeHandler(async (
}
// Fetch user with their HostHeader stepper info
const host = await hostService.getHostById(userId);
if (!host) {
throw new ApiError(404, 'Host record not found');
}
const host = await hostService.getHostIdByUserXid(userId);
return {
statusCode: 200,
@@ -42,7 +37,8 @@ export const handler = safeHandler(async (
success: true,
message: 'Stepper information retrieved successfully',
data: {
stepper: host.stepper,
stepper: host?.host?.stepper || null,
emailAddress: host.user?.emailAddress || null,
},
}),
};

View File

@@ -1,12 +1,11 @@
import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host';
import { verifyMinglarAdminHostToken } from '../../../common/middlewares/jwt/authForMinglarAdminHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../common/database/prisma.service';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
import { HostService } from '../services/host.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const hostService = new HostService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -14,7 +13,7 @@ export const handler = safeHandler(async (
): Promise<APIGatewayProxyResult> => {
// Get host ID from path parameters
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
if(!token) {
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}

View File

@@ -1,8 +1,8 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaClient } from '@prisma/client';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
const prisma = new PrismaClient();
const prisma = prismaClient;
export const handler = safeHandler(async (
event: APIGatewayProxyEvent
@@ -10,7 +10,6 @@ export const handler = safeHandler(async (
const result = await prisma.hostHeader.findMany({
select: {
id: true,
hostParent: true,
hostStatusDisplay: true,
accountManager: true,

View File

@@ -1,13 +1,11 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { PrismaService } from "../../../common/database/prisma.service";
import { prismaClient } from "../../../common/database/prisma.lambda.service";
import { safeHandler } from "../../../common/utils/handlers/safeHandler";
import ApiError from "../../../common/utils/helper/ApiError";
import { resendOtpHelper } from "../../../common/utils/helper/resendOtpHelper";
import { resendOtpEmail } from "../services/resendOTPEmail.service";
import { parseBody } from "../../../common/utils/validation/validation.utils";
import { resendOtpSchema } from "../../../common/utils/validation/host/auth.validation";
const prisma = new PrismaService();
const prisma = prismaClient;
// allowed purposes
const ALLOWED_PURPOSES = ["Register", "Login", "ForgotPassword"] as const;
@@ -15,16 +13,39 @@ type OtpPurpose = typeof ALLOWED_PURPOSES[number];
export const handler = safeHandler(
async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
// Parse and validate body using Zod
const { email, purpose: purposeFromBody } = parseBody(event.body, resendOtpSchema);
// parse body safely
let body: { email?: string; purpose?: string } = {};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch {
throw new ApiError(400, "Invalid JSON in request body");
}
// allow passing purpose via query string too (useful for GET requests)
const qsPurpose = event.queryStringParameters?.purpose;
const purpose = (purposeFromBody || qsPurpose || "Register") as OtpPurpose;
const purposeRaw = (body.purpose || qsPurpose || "").trim();
if (!purposeRaw) {
throw new ApiError(400, "purpose is required. Allowed values: Register, Login, ForgotPassword");
}
if (!ALLOWED_PURPOSES.includes(purposeRaw as OtpPurpose)) {
throw new ApiError(
400,
`Invalid purpose '${purposeRaw}'. Allowed values: ${ALLOWED_PURPOSES.join(", ")}`
);
}
const purpose = purposeRaw as OtpPurpose;
const email = (body.email || "").trim();
if (!email) throw new ApiError(400, "Email is required");
const emailToLowerCase = email.toLowerCase();
// find user (you can adapt the isActive / userStatus checks per your rules)
const user = await prisma.user.findUnique({
where: { emailAddress: email, isActive: true },
where: { emailAddress: emailToLowerCase, isActive: true },
select: { id: true, emailAddress: true, role: true },
});
@@ -37,7 +58,6 @@ export const handler = safeHandler(
const otpResult = await resendOtpHelper(
prisma,
user.id,
user.emailAddress,
purpose,
6, // 6-digit OTP
5 // expires in 5 minutes

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,8 @@ export async function resendOtpEmail(
const htmlContent = `
<p>Dear ${role},</p>
<p>Your new OTP is: <strong>${otp}</strong></p>
<p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
<p>Best regards,<br/>Minglar Team</p>
<p>This code will be valid for the next 5 minutes.</p>
<p>Warm regards,<br/>Minglar Team</p>
`;
try {

View File

@@ -13,9 +13,10 @@ export async function sendOtpEmailForHost(
const htmlContent = `
<p>Dear Host,</p>
<p>Your OTP for registration is: <strong>${otp}</strong></p>
<p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
<p>Best regards,<br/>Minglar Team</p>
<p>Youre almost all set! 🎉</p>
<p>Enter <strong>${otp}</strong> to wrap your registration.</p>
<p>This code will be valid for the next 5 minutes.</p>
<p>Warm regards,<br/>Minglar Team</p>
`;
try {

View File

@@ -1,10 +1,10 @@
import { PrismaService } from "../../../common/database/prisma.service";
import { PrismaClient } from '@prisma/client';
import jwt, { JwtPayload } from "jsonwebtoken";
import moment from "moment";
import config from "../../../config/config";
export class TokenService {
constructor(private readonly prisma: PrismaService = new PrismaService()) {}
constructor(private prisma: PrismaClient) { }
private generateToken(
user_xid: number,
@@ -53,6 +53,10 @@ export class TokenService {
config.jwt.secret
);
await this.prisma.token.deleteMany({
where: { userXid: user_xid }
})
await this.prisma.token.create({
data: {
token: refreshToken.token,
@@ -100,6 +104,10 @@ export class TokenService {
config.jwt.secret
);
await this.prisma.token.deleteMany({
where: { userXid: user_xid }
})
await this.prisma.token.create({
data: {
token: refreshToken.token,

View File

@@ -1,13 +1,11 @@
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { MinglarService } from '../services/minglar.service';
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { parseBody } from '../../../common/utils/validation/validation.utils';
import { minglarCreatePasswordSchema } from '../../../common/utils/validation/minglaradmin/auth.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -16,15 +14,37 @@ export const handler = safeHandler(async (
// Extract token from headers
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
if(!token) {
throw new (await import('../../../common/utils/helper/ApiError')).default(400, 'This is a protected route. Please provide a valid token.');
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Authenticate user using the shared authForHost function
const userInfo = await verifyMinglarAdminToken(token);
const user_xid = userInfo.id;
// Parse and validate request body using Zod
const { password } = parseBody(event.body, minglarCreatePasswordSchema);
// Parse request body
let body: { password?: string; confirmPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { password, confirmPassword } = body;
if (!password || !confirmPassword) {
throw new ApiError(400, 'Password and confirm password are required');
}
// Validate password match
if (password !== confirmPassword) {
throw new ApiError(400, 'Password and confirm password do not match');
}
// Validate password length
if (password.length < 8) {
throw new ApiError(400, 'Password must be at least 8 characters long');
}
await minglarService.createPassword(user_xid, password);
const userDetails = await minglarService.getBasicUserDetails(user_xid)

View File

@@ -3,19 +3,17 @@ import {
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { MinglarService } from '../services/minglar.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
import { parseQueryParams } from '../../../common/utils/validation/validation.utils';
import { getAmDetailByIdPathSchema } from '../../../common/utils/validation/prepopulate/prepopulate.validation';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
import { MinglarService } from '../services/minglar.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
/**
* Get AM details by ID handler
* Get all host applications handler
* Returns host details with status, submission date, and account manager info
*/
export const handler = safeHandler(
async (
@@ -34,12 +32,22 @@ export const handler = safeHandler(
await verifyMinglarAdminToken(token);
// Parse and validate path params using Zod
const { amXid } = parseQueryParams(event.pathParameters, getAmDetailByIdPathSchema);
const amXid = event.pathParameters?.amXid;
if (!amXid) {
throw new ApiError(
400,
'Account Manager XID is required in path parameters.',
);
}
const amId = Number(amXid);
if (Number.isNaN(amId)) {
throw new ApiError(400, 'Account Manager XID must be a valid number.');
}
// Get AM details by ID from service
const getAmDetailsByid = await minglarService.getAMdetailById(amXid);
// Get all host applications from service based on user role
const getAmDetailsByid = await minglarService.getAMdetailById( amId );
return {
statusCode: 200,

View File

@@ -1,18 +1,23 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { sendEmailToHostForApprovedApplication } from '../../../services/approvalMailtoHost.service';
import { MinglarService } from '../../../services/minglar.service';
import { sendEmailToHostForApprovedApplication } from '../../../services/approvalMailtoHost.service'
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostApplicationActionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
interface AddSuggestionBody {
hostXid: number;
title: string;
comments: string;
}
/**
* Accept host application handler
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -27,14 +32,22 @@ export const handler = safeHandler(async (
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Parse and validate request body using Zod
const { hostXid } = parseBody(event.body, hostApplicationActionSchema);
// Parse request body
let body: AddSuggestionBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { hostXid } = body;
// Add suggestion using service
await minglarService.acceptHostApplication(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(userInfo.id)
await sendEmailToHostForApprovedApplication(hostDetails.emailAddress)
const hostDetails = await minglarService.getUserDetails(hostXid)
await sendEmailToHostForApprovedApplication(hostDetails.emailAddress, hostDetails.firstName)
return {
statusCode: 200,

View File

@@ -0,0 +1,59 @@
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { MinglarService } from '../../../services/minglar.service';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { sendAMPQQAcceptanceMailtoHost } from '../../../../minglaradmin/services/approvalMailtoHost.service';
const minglarService = new MinglarService(prismaClient);
interface Body {
activityId: number;
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
const userInfo = await verifyMinglarAdminToken(token);
// Parse request body
let body: Body;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityId } = body;
if (!activityId) {
throw new ApiError(400, 'activityId is required');
}
await minglarService.acceptPQByAM(
Number(activityId),
Number(userInfo.id)
);
const hostXid = await minglarService.getHostXidByActivityId(activityId)
const hostDetails = await minglarService.getUserDetails(hostXid)
await sendAMPQQAcceptanceMailtoHost(hostDetails.emailAddress, hostDetails.firstName)
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Approved PQ successfully',
data: null,
}),
};
});

View File

@@ -1,15 +1,18 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { MinglarService } from '../../../services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { HOST_SUGGESTION_TITLES } from '../../../../../common/utils/constants/minglar.constant';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { addPqqSuggestionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
// import { HOST_SUGGESTION_TITLES } from '../../../../../common/utils/constants/minglar.constant';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
interface AddSuggestionBody {
title: string;
comments: string;
activity_pqq_header_xid:number
}
/**
* Add suggestion handler for host applications
@@ -30,7 +33,7 @@ export const handler = safeHandler(async (
const userInfo = await verifyMinglarAdminToken(token);
// Get user details
const user = await prismaService.user.findUnique({
const user = await prismaClient.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
@@ -39,14 +42,34 @@ export const handler = safeHandler(async (
throw new ApiError(404, 'User not found');
}
// Parse and validate request body using Zod
const { title, comments, activity_pqq_header_xid } = parseBody(event.body, addPqqSuggestionSchema);
// Parse request body
let body: AddSuggestionBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { title, comments , activity_pqq_header_xid} = body;
if (!title) {
throw new ApiError(400, 'Title is required');
}
if (!comments) {
throw new ApiError(400, 'Comments are required');
}
if(!activity_pqq_header_xid){
throw new ApiError(400 , "Activity Pqq HeaderXid Required");
}
// Validate title is one of the allowed types
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
if (!allowedTitles.includes(title)) {
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
}
// const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
// if (!allowedTitles.includes(title)) {
// throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
// }
// Add suggestion using service
await minglarService.addPqqSuggestion(title, comments, activity_pqq_header_xid,user.id);

View File

@@ -1,15 +1,18 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { MinglarService } from '../../../services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { HOST_SUGGESTION_TITLES } from '../../../../../common/utils/constants/minglar.constant';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { addHostSuggestionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
interface AddSuggestionBody {
hostXid: number;
title: string;
comments: string;
isParent: boolean;
}
/**
* Add suggestion handler for host applications
@@ -30,7 +33,7 @@ export const handler = safeHandler(async (
const userInfo = await verifyMinglarAdminToken(token);
// Get user details
const user = await prismaService.user.findUnique({
const user = await prismaClient.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
@@ -39,17 +42,38 @@ export const handler = safeHandler(async (
throw new ApiError(404, 'User not found');
}
// Parse and validate request body using Zod
const { hostXid, title, comments } = parseBody(event.body, addHostSuggestionSchema);
// Parse request body
let body: AddSuggestionBody;
// Validate title is one of the allowed types
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
if (!allowedTitles.includes(title)) {
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { hostXid, title, comments, isParent } = body;
// Validate required fields
if (!hostXid) {
throw new ApiError(400, 'Host ID is required');
}
if (!title) {
throw new ApiError(400, 'Title is required');
}
if (!comments) {
throw new ApiError(400, 'Comments are required');
}
// Validate title is one of the allowed types
// const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
// if (!allowedTitles.includes(title)) {
// throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
// }
// Add suggestion using service
await minglarService.addHostSuggestion(hostXid, title, comments, user.id);
await minglarService.addHostSuggestion(hostXid, title, comments, user.id, isParent);
return {
statusCode: 200,

View File

@@ -1,15 +1,12 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { MinglarService } from '../../../services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { paginationService } from '../../../../../common/utils/pagination/pagination.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { getAllHostApplicationsQuerySchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
import { MinglarService } from '../../../services/minglar.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
/**
* Get all host applications handler with pagination
@@ -28,7 +25,7 @@ export const handler = safeHandler(async (
const userInfo = await verifyMinglarAdminToken(token);
// Get user details including role
const user = await prismaService.user.findUnique({
const user = await prismaClient.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
@@ -37,15 +34,15 @@ export const handler = safeHandler(async (
throw new ApiError(404, 'User not found');
}
// Parse and validate query params using Zod
const { search, userStatus, roleFilter } = parseQueryParams(
event.queryStringParameters,
getAllHostApplicationsQuerySchema
);
// Get query parameters
const search = event.queryStringParameters?.search || '';
const userStatus = event.queryStringParameters?.userStatus || '';
const roleFilter = event.queryStringParameters?.roleFilter || '';
// Parse pagination parameters
const paginationParams = paginationService.getPaginationFromEvent(event);
const paginationOptions = paginationService.parsePaginationParams(paginationParams);
const applicationStatus = event.queryStringParameters?.applicationStatus || '';
// Get paginated host applications
const { data, totalCount } = await minglarService.getAllHostApplications(
@@ -54,7 +51,8 @@ export const handler = safeHandler(async (
search,
userStatus,
paginationOptions,
roleFilter
roleFilter,
applicationStatus
);
// Create paginated response

View File

@@ -1,15 +1,11 @@
import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { MinglarService } from '../../../services/minglar.service';
import { PrismaService } from '@/common/database/prisma.service';
import { safeHandler } from '@/common/utils/handlers/safeHandler';
import ApiError from '@/common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { parseQueryParams } from '@/common/utils/validation/validation.utils';
import { getHostByIdPathSchema } from '@/common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -23,8 +19,13 @@ export const handler = safeHandler(async (
await verifyMinglarAdminToken(token);
// Parse and validate path params using Zod
const { host_xid } = parseQueryParams(event.pathParameters, getHostByIdPathSchema);
const host_xid = event.pathParameters?.host_xid;
if (!host_xid) {
throw new ApiError(
400,
'Host ID is required in path parameters.',
);
}
const hostDetails = await minglarService.getHostDetailsById(Number(host_xid));

View File

@@ -1,18 +1,23 @@
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
import { sendAMRejectionMailtoHost } from '../../../services/rejectionMailtoHost.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostApplicationActionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
interface AddSuggestionBody {
hostXid: number;
title: string;
comments: string;
}
/**
* Reject host application handler for Account Managers
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -27,14 +32,22 @@ export const handler = safeHandler(async (
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Parse and validate request body using Zod
const { hostXid } = parseBody(event.body, hostApplicationActionSchema);
// Parse request body
let body: AddSuggestionBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { hostXid } = body;
// Add suggestion using service
await minglarService.rejectHostApplicationAM(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(userInfo.id)
await sendAMRejectionMailtoHost(hostDetails.emailAddress)
const hostDetails = await minglarService.getUserDetails(hostXid)
await sendAMRejectionMailtoHost(hostDetails.emailAddress, hostDetails.firstName)
return {
statusCode: 200,

View File

@@ -1,14 +1,16 @@
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { MinglarService } from '@/modules/minglaradmin/services/minglar.service';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { rejectPqqByAmSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
import { MinglarService } from '../../../services/minglar.service';
import { sendAMPQQRejectionMailtoHost } from '../../../../minglaradmin/services/rejectionMailtoHost.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
interface Body {
activityId: number;
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -19,10 +21,28 @@ export const handler = safeHandler(async (
const userInfo = await verifyMinglarAdminToken(token);
// Parse and validate body using Zod
const { activityId } = parseBody(event.body, rejectPqqByAmSchema);
// Parse request body
let body: Body;
await minglarService.rejectPQQbyAM(activityId);
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityId } = body;
if (!activityId) {
throw new ApiError(400, 'activityId is required');
}
await minglarService.rejectPQQbyAM(
Number(activityId),
Number(userInfo.id)
);
const hostXid = await minglarService.getHostXidByActivityId(activityId)
const hostDetails = await minglarService.getUserDetails(hostXid)
await sendAMPQQRejectionMailtoHost(hostDetails.emailAddress, hostDetails.firstName)
return {
statusCode: 201,
@@ -32,7 +52,7 @@ export const handler = safeHandler(async (
},
body: JSON.stringify({
success: true,
message: 'Rejected successfully',
message: 'Rejected PQ successfully',
data: null,
}),
};

View File

@@ -1,54 +0,0 @@
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { sendEmailToHostForMinglarApproval } from '../../../services/approvalMailtoHost.service';
import { MinglarService } from '../../../services/minglar.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostApplicationActionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Accept host application handler for Minglar Admin
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Parse and validate request body using Zod
const { hostXid } = parseBody(event.body, hostApplicationActionSchema);
// Add suggestion using service
await minglarService.acceptHostApplicationMinglarAdmin(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(userInfo.id)
if (!hostDetails?.emailAddress) {
throw new ApiError(404, 'Host details or email address not found');
}
await sendEmailToHostForMinglarApproval(hostDetails.emailAddress)
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Application accepted successfully',
data: null,
}),
};
});

View File

@@ -3,17 +3,18 @@ import {
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { MinglarService } from '../../../services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import { sendAMEmailForHostAssign } from '../../../services/AMEmail.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { assignAmToHostSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
interface assignAMToHostBody {
host_xid: number;
account_manager_xid: number;
}
/**
* Get all host applications handler
@@ -38,7 +39,7 @@ export const handler = safeHandler(
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Get user details including role
const user = await prismaService.user.findUnique({
const user = await prismaClient.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true },
});
@@ -47,11 +48,16 @@ export const handler = safeHandler(
throw new ApiError(404, 'User not found');
}
// Parse and validate request body using Zod
const { hostXid: host_xid, accountManagerXid: account_manager_xid } = parseBody(
event.body,
assignAmToHostSchema
);
// Parse request body
let body: assignAMToHostBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { host_xid, account_manager_xid } = body;
// Get all host applications from service based on user role
await minglarService.assignAMToHost(user.id, host_xid, account_manager_xid);

View File

@@ -1,14 +1,23 @@
import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin';
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { editAgreementDetailsSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
interface assignAMToHostBody {
host_xid: number,
agreementStartDate: string,
duration: number,
isCommisionBase: boolean,
commisionPer: number,
amountPerBooking: number,
durationFrequency: string,
payoutDurationNum: number,
payoutDurationFrequency: string
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -24,7 +33,7 @@ export const handler = safeHandler(async (
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Get user details including role
const user = await prismaService.user.findUnique({
const user = await prismaClient.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
@@ -33,7 +42,15 @@ export const handler = safeHandler(async (
throw new ApiError(404, 'User not found');
}
// Parse and validate request body using Zod
// Parse request body
let body: assignAMToHostBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const {
host_xid,
agreementStartDate,
@@ -44,10 +61,11 @@ export const handler = safeHandler(async (
durationFrequency,
payoutDurationNum,
payoutDurationFrequency
} = parseBody(event.body, editAgreementDetailsSchema);
} = body;
await minglarService.editAgreementDetails(
await minglarService.acceptHostApplicationMinglarAdmin(
host_xid,
userInfo.id,
agreementStartDate,
duration,
isCommisionBase,
@@ -55,8 +73,8 @@ export const handler = safeHandler(async (
amountPerBooking,
durationFrequency,
payoutDurationNum,
payoutDurationFrequency
);
payoutDurationFrequency);
// await sendEmailToHostForMinglarApproval(hostDetails.emailAddress)
return {
statusCode: 200,

View File

@@ -1,20 +1,19 @@
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { PrePopulateService } from '../../../../prepopulate/services/prepopulate.service';
import { MinglarService } from '../../../services/minglar.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { getHostByIdAltPathSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
import { optionalSearchQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
import { paginationService } from '../../../../../common/utils/pagination/pagination.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const prePopulateService = new PrePopulateService(prismaService);
const minglarService = new MinglarService(prismaClient);
const prePopulateService = new PrePopulateService(prismaClient);
/**
* Get all activities of a host handler
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -29,14 +28,20 @@ export const handler = safeHandler(async (
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Parse and validate path params using Zod
const { id: hostXid } = parseQueryParams(event.pathParameters, getHostByIdAltPathSchema);
const hostXid = Number(event.pathParameters?.id)
// Parse and validate query params using Zod
const { search, q } = parseQueryParams(event.queryStringParameters, optionalSearchQuerySchema);
const searchTerm = search || q || undefined;
// Get pagination params from event
const paginationParams = paginationService.getPaginationFromEvent(event);
const paginationOptions = paginationService.parsePaginationParams(paginationParams);
const data = await minglarService.getAllHostActivityForMinglar(searchTerm, hostXid);
// Read optional search query (supports ?search= or ?q=)
const search = event.queryStringParameters?.search || event.queryStringParameters?.q || undefined;
const result = await minglarService.getAllHostActivityForMinglar(
search ? String(search) : undefined,
hostXid,
paginationOptions
);
return {
@@ -48,7 +53,7 @@ export const handler = safeHandler(async (
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data,
...result,
}),
};
});

View File

@@ -1,53 +1,85 @@
import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { paginationService } from '../../../../../common/utils/pagination/pagination.service';
import { MinglarService } from '../../../services/minglar.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
/**
* Get all host applications handler
* Returns host details with status, submission date, and account manager info
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
401,
'This is a protected route. Please provide a valid token.',
);
}
// Verify token and get user info
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Verify token and get user info
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Get user details including role
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
// Get user details including role
const user = await prismaClient.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true },
});
if (!user) {
throw new ApiError(404, 'User not found');
}
if (!user) {
throw new ApiError(404, 'User not found');
}
// Get all host applications from service based on user role
const hostApplications = await minglarService.getAllOnboardingHostApplications();
// Extract optional search query
const queryParams = event.queryStringParameters || {};
const search =
(queryParams.search as string) ||
(queryParams.q as string) ||
undefined;
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host applications retrieved successfully',
data: hostApplications,
}),
};
});
// Pagination
const paginationParams = paginationService.getPaginationFromEvent(event);
const paginationOptions =
paginationService.parsePaginationParams(paginationParams);
// Get all host applications from service based on user role
const { data, totalCount } =
await minglarService.getAllOnboardingHostApplications(
paginationOptions,
search,
);
const paginatedResponse = paginationService.createPaginatedResponse(
data,
totalCount,
paginationOptions,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host applications retrieved successfully',
...paginatedResponse,
}),
};
},
);

View File

@@ -1,53 +1,85 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { paginationService } from '../../../../../common/utils/pagination/pagination.service';
import { MinglarService } from '../../../services/minglar.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
/**
* Get all host applications handler
* Get all NEW host applications handler
* Returns host details with status, submission date, and account manager info
*/
export const handler = safeHandler(async (
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
context?: Context,
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
throw new ApiError(
401,
'This is a protected route. Please provide a valid token.',
);
}
// Verify token and get user info
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Get user details including role
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
const user = await prismaClient.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true },
});
if (!user) {
throw new ApiError(404, 'User not found');
throw new ApiError(404, 'User not found');
}
// Extract optional search query
const queryParams = event.queryStringParameters || {};
const search =
(queryParams.search as string) ||
(queryParams.q as string) ||
undefined;
// Pagination
const paginationParams = paginationService.getPaginationFromEvent(event);
const paginationOptions =
paginationService.parsePaginationParams(paginationParams);
// Get all host applications from service based on user role
const hostApplications = await minglarService.getAllOnboardingHostApplications_New();
const { data, totalCount } =
await minglarService.getAllOnboardingHostApplications_New(
paginationOptions,
search,
);
const paginatedResponse = paginationService.createPaginatedResponse(
data,
totalCount,
paginationOptions,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host applications retrieved successfully',
data: hostApplications,
}),
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host applications retrieved successfully',
...paginatedResponse,
}),
};
});
},
);

View File

@@ -1,18 +1,23 @@
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
import { sendEmailToHostForRejectedApplication } from '../../../services/rejectionMailtoHost.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostApplicationActionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const minglarService = new MinglarService(prismaClient);
interface AddSuggestionBody {
hostXid: number;
title: string;
comments: string;
}
/**
* Reject host application handler for Minglar Admin
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
@@ -27,13 +32,21 @@ export const handler = safeHandler(async (
// Verify token and get user info
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Parse and validate request body using Zod
const { hostXid } = parseBody(event.body, hostApplicationActionSchema);
// Parse request body
let body: AddSuggestionBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { hostXid } = body;
// Add suggestion using service
await minglarService.rejectHostApplication(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(userInfo.id)
const hostDetails = await minglarService.getUserDetails(hostXid)
if (!hostDetails?.emailAddress) {
throw new ApiError(404, 'Host details or email address not found');
}

View File

@@ -0,0 +1,51 @@
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../../minglaradmin/services/minglar.service';
const minglarService = new MinglarService(prismaClient);
/**
* Get suggestions handler
* Retrieves suggestions based on user's role and host assignments
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
await verifyMinglarAdminToken(token);
const hostXid = Number(event.pathParameters?.hostXid)
if (!hostXid) {
throw new ApiError(
400,
'Host ID is required in path parameters.',
);
}
// Get suggestions using service
const suggestions = await minglarService.getSuggestionsForAM(hostXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestions retrieved successfully',
data: suggestions,
}),
};
});

View File

@@ -0,0 +1,49 @@
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { MinglarService } from '../../../services/minglar.service';
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
const minglarService = new MinglarService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Get host ID from path parameters
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
await verifyMinglarAdminHostToken(token);
const activityXid = event.pathParameters?.activityXid;
if (!activityXid) {
throw new ApiError(
400,
'Host ID is required in path parameters.',
);
}
const pqpDetails = await minglarService.getAllPQPDetailsForAM(Number(activityXid));
if (!pqpDetails) {
throw new ApiError(404, 'Record not found');
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'PQ details retrieved successfully',
data: pqpDetails,
}),
};
});

View File

@@ -1,25 +1,36 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../common/database/prisma.service';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
import { GetMinglarLoginResponseDTO } from '../dto/minglar.dto';
import { MinglarService } from '../services/minglar.service';
import { TokenService } from "../services/token.service";
import { parseBody } from '../../../common/utils/validation/validation.utils';
import { minglarLoginSchema } from '../../../common/utils/validation/minglaradmin/auth.validation';
const prismaService = new PrismaService();
const minglarSerivce = new MinglarService(prismaService);
const tokenService = new TokenService(prismaService);
const minglarSerivce = new MinglarService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse and validate request body using Zod
const { emailAddress, userPassword } = parseBody(event.body, minglarLoginSchema);
// Parse request body
let body: { emailAddress?: string; userPassword?: string };
const loginForMinglar = await minglarSerivce.loginForMinglar(emailAddress, userPassword);
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, userPassword } = body;
if (!emailAddress) {
throw new ApiError(400, 'Email is required');
}
const emailToLowerCase = emailAddress.toLowerCase()
const loginForMinglar = await minglarSerivce.loginForMinglar(emailToLowerCase, userPassword);
if (!loginForMinglar) {
throw new ApiError(400, 'Failed to login');

View File

@@ -3,13 +3,13 @@ import {
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { ROLE } from '../../../common/utils/constants/common.constant';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
import { ROLE } from '../../../common/utils/constants/common.constant';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
const prismaService = new PrismaService();
const prismaService = prismaClient;
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,

Some files were not shown because too many files have changed in this diff Show More