diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5525dd1..b16904c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -97,14 +97,15 @@ model UserAddressDetails { } model UserDocuments { - id Int @id @default(autoincrement()) - userXid Int @map("user_xid") - user User @relation(fields: [userXid], references: [id], onDelete: Cascade) - fileName String @map("file_name") @db.VarChar(500) - 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()) + userXid Int @map("user_xid") + user User @relation(fields: [userXid], references: [id], onDelete: Cascade) + // documentTypeName String @map("document_type_name") @db.VarChar(50) + fileName String @map("file_name") @db.VarChar(500) + 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("user_documents") @@schema("usr") @@ -654,8 +655,8 @@ model HostHeader { cities Cities @relation(fields: [cityXid], references: [id], onDelete: Restrict) stateXid Int @map("state_xid") states States @relation(fields: [stateXid], references: [id], onDelete: Restrict) - countryXid Int @map("country_xid") - countries Countries @relation(fields: [countryXid], references: [id], onDelete: Restrict) + countryXid Int? @map("country_xid") + 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") @@ -669,14 +670,14 @@ model HostHeader { facebookUrl String? @map("facebook_url") @db.VarChar(80) linkedinUrl String? @map("linkedin_url") @db.VarChar(80) twitterUrl String? @map("twitter_url") @db.VarChar(80) - currencyXid Int @map("currency_xid") - currencies Currencies @relation(fields: [currencyXid], references: [id], onDelete: Restrict) - stepper Int @default(1) @map("stepper") + currencyXid Int? @map("currency_xid") + currencies Currencies? @relation(fields: [currencyXid], references: [id], onDelete: Restrict) + stepper Int? @default(1) @map("stepper") hostStatusInternal String @default("pending") @map("host_status_internal") @db.VarChar(20) hostStatusDisplay String @default("pending") @map("host_status_Display") @db.VarChar(20) adminStatusInternal String @default("pending") @map("admin_status_internal") @db.VarChar(20) adminStatusDisplay String @default("pending") @map("admin_status_display") @db.VarChar(20) - amStatus String @default("pending") @map("am_status") @db.VarChar(20) + amStatus String? @default("pending") @map("am_status") @db.VarChar(20) agreementAccepted Boolean @default(false) @map("agreement_accepted") accountManagerXid Int? @map("account_manager_xid") accountManager User? @relation("AccountManager", fields: [accountManagerXid], references: [id], onDelete: Restrict) diff --git a/serverless.yml b/serverless.yml index 253489e..a0b3d2c 100644 --- a/serverless.yml +++ b/serverless.yml @@ -91,7 +91,7 @@ functions: - 'common/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -107,7 +107,7 @@ functions: - 'common/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -123,7 +123,7 @@ functions: - 'common/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: path: /host/login @@ -138,7 +138,7 @@ functions: - 'common/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: path: /host/registration @@ -168,7 +168,7 @@ functions: - 'common/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -183,7 +183,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -198,7 +198,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -213,7 +213,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -228,7 +228,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -243,7 +243,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -258,7 +258,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -275,7 +275,7 @@ functions: - 'src/modules/host/services/**' - 'common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -289,7 +289,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -303,7 +303,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -317,7 +317,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -331,7 +331,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -347,7 +347,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - 'node_modules/@aws-sdk/**' - 'node_modules/@smithy/**' - 'node_modules/tslib/**' @@ -365,7 +365,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -379,7 +379,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -393,7 +393,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -407,7 +407,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -421,7 +421,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -435,14 +435,13 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - '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 package: @@ -450,7 +449,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -464,7 +463,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -478,7 +477,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -492,7 +491,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -506,7 +505,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -520,7 +519,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -534,7 +533,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -548,7 +547,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -562,7 +561,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -576,7 +575,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -590,7 +589,7 @@ functions: - 'src/modules/minglaradmin/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: @@ -605,7 +604,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - 'node_modules/@aws-sdk/**' - 'node_modules/@smithy/**' - 'node_modules/tslib/**' @@ -632,7 +631,7 @@ functions: - 'src/modules/host/services/**' - 'src/common/**' - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - 'node_modules/@aws-sdk/**' - 'node_modules/@smithy/**' - 'node_modules/tslib/**' @@ -650,3 +649,30 @@ functions: - httpApi: path: /host/submit-pqq-ans method: patch + + submitFinalPqqAnswer: + handler: src/modules/host/handlers/getPQQScore.handler + 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 diff --git a/src/common/middlewares/jwt/authForMinglarAdmin&Host.ts b/src/common/middlewares/jwt/authForMinglarAdmin&Host.ts new file mode 100644 index 0000000..5c38f1c --- /dev/null +++ b/src/common/middlewares/jwt/authForMinglarAdmin&Host.ts @@ -0,0 +1,118 @@ +import jwt from 'jsonwebtoken'; +import httpStatus from 'http-status'; +import { Request, Response, NextFunction } from 'express'; +import { PrismaClient } from '@prisma/client'; +import ApiError from '../../utils/helper/ApiError'; +import config from '../../../config/config'; +import { ROLE } from '@/common/utils/constants/common.constant'; + +const prisma = new PrismaClient(); + +interface DecodedToken { + id?: number; + sub?: string | number; + role?: string; + iat: number; + exp: number; +} + +interface UserPayload { + id: string; + role?: string; +} + +declare module 'express-serve-static-core' { + interface Request { + user?: UserPayload; + } +} + +/** + * Core authentication function - verifies JWT and validates Host user + * Can be used by both Express middleware and Lambda handlers + */ +export async function verifyMinglarAdminHostToken(token: string): Promise<{ id: number; role?: string }> { + if (!token) { + throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'); + } + + try { + const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken; + + const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null); + + if (!userId) { + throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'); + } + + // ✅ Fetch user from Prisma (Host user only) + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { role: true }, + }); + + if (!user) { + throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found'); + } + + // ✅ Check if user is active + if (user.isActive === false) { + throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.'); + } + + // ✅ Check Minglar Roles (role_xid = 1, 2, 3) + if (![ROLE.MINGLAR_ADMIN, ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER, ROLE.HOST].includes(user.roleXid)) { + throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.'); + } + + + return { id: user.id, role: user.role?.roleName }; + } catch (error) { + if (error instanceof jwt.TokenExpiredError) { + throw new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.'); + } + + if (error instanceof ApiError) { + throw error; + } + + throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.'); + } +} + +/** + * Verifies JWT and validates Host user (role_xid = 1) + */ +const verifyCallback = async ( + req: Request, + resolve: (value?: unknown) => void, + reject: (reason?: Error) => void +) => { + const token = req.header('x-auth-token') || req.cookies?.accessToken; + + try { + const userInfo = await verifyMinglarAdminHostToken(token); + + // Attach user to request + req.user = { id: userInfo.id.toString(), role: userInfo.role }; + + resolve(); + } catch (error) { + return reject(error as Error); + } +}; + +/** + * Express middleware — use as `auth()` in routes + */ +const authForHost = + () => + 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; diff --git a/src/common/utils/constants/host.constant.ts b/src/common/utils/constants/host.constant.ts index 32ef20c..71145e3 100644 --- a/src/common/utils/constants/host.constant.ts +++ b/src/common/utils/constants/host.constant.ts @@ -21,4 +21,8 @@ export const STEPPER = { BANK_DETAILS_UPDATED: 4, AGREEMENT_ACCEPTED: 5, REJECTED: 6 +} + +export const LAST_QUESTION_ID = { + Q_ID: 55 } \ No newline at end of file diff --git a/src/modules/host/handlers/getByIdPQQ.ts b/src/modules/host/handlers/getByIdPQQ.ts index 70b4dba..def1575 100644 --- a/src/modules/host/handlers/getByIdPQQ.ts +++ b/src/modules/host/handlers/getByIdPQQ.ts @@ -4,6 +4,7 @@ import { PrismaService } from '../../../common/database/prisma.service'; import ApiError from '../../../common/utils/helper/ApiError'; import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost'; import { HostService } from '../services/host.service'; +import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host'; const prismaService = new PrismaService(); const hostService = new HostService(prismaService); @@ -18,7 +19,7 @@ export const handler = safeHandler(async ( } // Verify token and get user info - const userInfo = await verifyHostToken(token); + const userInfo = await verifyMinglarAdminHostToken(token); const userId = Number(userInfo.id); let body: { question_xid: number, activity_xid: number }; diff --git a/src/modules/host/handlers/getPQQScore.ts b/src/modules/host/handlers/getPQQScore.ts new file mode 100644 index 0000000..24f9fc3 --- /dev/null +++ b/src/modules/host/handlers/getPQQScore.ts @@ -0,0 +1,310 @@ +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 { 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 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 { + 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 + } +} + +async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise { + 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}`; + } + + // Upload new file (replaces existing if same key) + 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 => { + 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?.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"); + + 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 + await new Promise((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; + } + + const chunks: Buffer[] = []; + let totalSize = 0; + const MAX_SIZE = 5 * 1024 * 1024; // 5 MB + + 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, + }); + } + }); + + 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}`)); + }); + + bb.end(bodyBuffer); + }); + + // 4) Extract required fields - only activityXid, pqqQuestionXid, pqqAnswerXid are required + 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 + ); + + let header; + if (existingHeader) { + console.log("🔄 Updating existing PQQ header"); + // Update existing header (comments can be null) + const header = await pqqService.updateHeader( + existingHeader.id, + comments + ); + } else { + console.log("🆕 Creating new PQQ header"); + // Create new header (comments can be null) + const header = await pqqService.createHeader( + activityXid, + pqqQuestionXid, + pqqAnswerXid, + comments + ); + } + // Calculate score after answer submission + const score = await pqqService.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`); + + // 7) Handle file UPSERT - only if files are provided + const uploadedFiles: any[] = []; + + 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, + 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}`); + } + } + } + + // 9) Prepare 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: { + headerId: header.id, + activityXid, + pqqQuestionXid, + pqqAnswerXid, + 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') + } + }) + }; + + } catch (error: any) { + console.error("❌ Error in submitPqqAnswer:", error); + throw error; + } +}); \ No newline at end of file diff --git a/src/modules/host/handlers/getbyidhandler.ts b/src/modules/host/handlers/getbyidhandler.ts index 6ab2163..95e2e77 100644 --- a/src/modules/host/handlers/getbyidhandler.ts +++ b/src/modules/host/handlers/getbyidhandler.ts @@ -1,9 +1,9 @@ +import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host'; import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; -import { safeHandler } from '../../../common/utils/handlers/safeHandler'; import { PrismaService } from '../../../common/database/prisma.service'; -import { HostService } from '../services/host.service'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; 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); @@ -18,7 +18,7 @@ export const handler = safeHandler(async ( throw new ApiError(400, 'This is a protected route. Please provide a valid token.'); } - const userInfo = await verifyHostToken(token); + const userInfo = await verifyMinglarAdminHostToken(token); const id = Number(userInfo.id) if (!id) { diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 04e6deb..c413bb7 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -48,9 +48,17 @@ export class HostService { include: { hostParent: true, HostBankDetails: true, - HostDocuments: true, + HostDocuments: { + include: { + documentType: true, + }, + }, HostSuggestion: true, HostTrack: true, + countries: true, + currencies: true, + states: true, + cities: true, } }); @@ -241,7 +249,8 @@ export class HostService { select: { pqqQuestionXid: true, pqqAnswerXid: true, - ActivityPQQSupportings: true + ActivityPQQSupportings: true, + ActivityPQQSuggestions: true } }) } @@ -293,14 +302,14 @@ export class HostService { const createdHost = await tx.hostHeader.create({ data: { - userXid: user_xid, + user: { connect: { id: user_xid } }, companyName: companyData.companyName, hostRefNumber: refNumber, address1: companyData.address1, address2: companyData.address2, - cityXid: companyData.cityXid, - stateXid: companyData.stateXid, - countryXid: companyData.countryXid, + cities: { connect: { id: companyData.cityXid } }, + states: { connect: { id: companyData.stateXid } }, + countries: { connect: { id: companyData.countryXid } }, pinCode: companyData.pinCode, logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, @@ -314,7 +323,7 @@ export class HostService { facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, - currencyXid: companyData.currencyXid, + // currencyXid: companyData.currencyXid, stepper: STEPPER.UNDER_REVIEW, hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, @@ -383,9 +392,9 @@ export class HostService { companyName: companyData.companyName, address1: companyData.address1, address2: companyData.address2, - cityXid: companyData.cityXid, - stateXid: companyData.stateXid, - countryXid: companyData.countryXid, + cities: { connect: { id: companyData.cityXid } }, + states: { connect: { id: companyData.stateXid } }, + countries: { connect: { id: companyData.countryXid } }, pinCode: companyData.pinCode, logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, @@ -399,7 +408,7 @@ export class HostService { facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, - currencyXid: companyData.currencyXid, + // currencyXid: companyData.currencyXid, stepper: STEPPER.UNDER_REVIEW // hostRefNumber: DO NOT UPDATE }, @@ -628,6 +637,87 @@ export class HostService { // }); // } + + async calculatePqqScoreForUser(activityXid: number) { + // 1. Get all headers for this activity (user's answers) + const answers = await this.prisma.activityPQQheader.findMany({ + where: { activityXid }, + include: { + pqqQuestions: { + include: { + pqqSubCategories: { + include: { + category: true + } + } + } + }, + pqqAnswers: true + } + }); + + if (!answers.length) { + return { + overallPercentage: 0, + categoryWise: {} + }; + } + + // Prepare accumulators + let totalUserPoints = 0; + let totalMaxPoints = 0; + + // For category-wise scoring + const categories: any = {}; // { [categoryId]: { userPoints, maxPoints, name } } + + for (const item of answers) { + const question = item.pqqQuestions; + const answer = item.pqqAnswers; + + const maxPoints = question.maxPoints; + const userPoints = answer.answerPoints; + + totalUserPoints += userPoints; + totalMaxPoints += maxPoints; + + // Category info + const category = question.pqqSubCategories.category; + const categoryId = category.id; + + if (!categories[categoryId]) { + categories[categoryId] = { + categoryId, + categoryName: category.categoryName, + userPoints: 0, + maxPoints: 0 + }; + } + + categories[categoryId].userPoints += userPoints; + categories[categoryId].maxPoints += maxPoints; + } + + // Overall percent + const overallPercentage = totalMaxPoints > 0 + ? (totalUserPoints / totalMaxPoints) * 100 + : 0; + + // Category percentages + const categoryWise: any = {}; + + for (const catId in categories) { + const c = categories[catId]; + categoryWise[c.categoryName] = + c.maxPoints > 0 ? (c.userPoints / c.maxPoints) * 100 : 0; + } + + return { + overallPercentage, + categoryWise + }; + } + + async createHeader( activityXid: number, pqqQuestionXid: number, diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index 6f285b8..255e6b0 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -485,82 +485,87 @@ export class MinglarService { }) } - async getAllHostApplications(userId: number, userRoleXid: number, search?: string, userStatus?: string) { - // Build where clause based on user role - const whereClause: any = { + async getAllHostApplications( + userId: number, + userRoleXid: number, + search?: string, + userStatus?: string + ) { + const filters: any = { isActive: true, user: { roleXid: { notIn: [ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER] - }, + } } }; - // Add search filter if search query is provided - if (search && search.trim() !== '') { - const searchTerm = search.trim(); + /** ----------------------------------- + * SEARCH FILTER (ID, EMAIL, NAME) + * ----------------------------------- */ + if (search?.trim()) { + const term = search.trim(); - // Check if search term is a number (for ID search) - const isNumeric = /^\d+$/.test(searchTerm); - - if (isNumeric) { - // Search by host ID - whereClause.id = parseInt(searchTerm); + if (/^\d+$/.test(term)) { + // Search by Host ID + filters.id = Number(term); } else { // Search by email or name - whereClause.user = { - ...whereClause.user, + filters.user = { + ...filters.user, OR: [ - { emailAddress: { contains: searchTerm, mode: 'insensitive' } }, - { firstName: { contains: searchTerm, mode: 'insensitive' } }, - { lastName: { contains: searchTerm, mode: 'insensitive' } } + { emailAddress: { contains: term, mode: "insensitive" } }, + { firstName: { contains: term, mode: "insensitive" } }, + { lastName: { contains: term, mode: "insensitive" } } ] }; } } - // Apply userStatus filter (case-insensitive) e.g. userStatus=new / NEW / New - if (userStatus && userStatus.trim().toLowerCase() === MINGLAR_STATUS_DISPLAY.NEW.toLowerCase()) { - whereClause.adminStatusDisplay = MINGLAR_STATUS_DISPLAY.NEW; + + /** ----------------------------------- + * USER STATUS FILTER (NEW) + * ----------------------------------- */ + if ( + userStatus && + userStatus.trim().toLowerCase() === MINGLAR_STATUS_DISPLAY.NEW.toLowerCase() + ) { + filters.adminStatusDisplay = MINGLAR_STATUS_DISPLAY.NEW; } + /** ----------------------------------- + * ROLE-BASED FILTER: + * CO_ADMIN & ACCOUNT_MANAGER only see assigned hosts + * ----------------------------------- */ if (userRoleXid === ROLE.CO_ADMIN || userRoleXid === ROLE.ACCOUNT_MANAGER) { - whereClause.accountManagerXid = userId; + filters.accountManagerXid = userId; } - const hostHeaders = await this.prisma.hostHeader.findMany({ - where: whereClause, + /** ----------------------------------- + * MAIN QUERY + * ----------------------------------- */ + const results = await this.prisma.hostHeader.findMany({ + where: filters, select: { id: true, + hostStatusInternal: true, hostStatusDisplay: true, + adminStatusDisplay: true, + adminStatusInternal: true, createdAt: true, companyName: true, assignedOn: true, - cities: { - select: { - id: true, - cityName: true, - } - }, - adminStatusDisplay: true, - countries: { - select: { - id: true, - countryName: true, - } - }, - states: { - select: { - id: true, - stateName: true, - } - }, + + cities: { select: { id: true, cityName: true } }, + states: { select: { id: true, stateName: true } }, + countries: { select: { id: true, countryName: true } }, + user: { select: { id: true, firstName: true, lastName: true, emailAddress: true, - mobileNumber: true, + mobileNumber: true } }, accountManager: { @@ -570,30 +575,34 @@ export class MinglarService { lastName: true, emailAddress: true, mobileNumber: true, - roleXid: true, + roleXid: true } } }, - orderBy: { - createdAt: 'desc' - } + orderBy: { createdAt: "desc" } }); - // Transform the data to return host, hostStatusDisplay, submittedOn, and accountManager details - return hostHeaders.map(host => ({ - hostId: host.id, - host: host.user, - hostStatusDisplay: host.hostStatusDisplay, - submittedOn: host.createdAt, - accountManager: host.accountManager || null, - companyName: host.companyName || null, - city: host.cities || null, - state: host.states || null, - country: host.countries || null, - assignedOn: host.assignedOn || null, + /** ----------------------------------- + * TRANSFORM RESPONSE + * ----------------------------------- */ + return results.map(h => ({ + hostId: h.id, + host: h.user, + hostStatusDisplay: h.hostStatusDisplay, + hostStatusInternal: h.hostStatusInternal, + adminStatusDisplay: h.adminStatusDisplay, + adminStatusInternal: h.adminStatusInternal, + submittedOn: h.createdAt, + accountManager: h.accountManager || null, + companyName: h.companyName || null, + city: h.cities || null, + state: h.states || null, + country: h.countries || null, + assignedOn: h.assignedOn || null })); } + async getAllCoadminAndAM() { // 1. Fetch all required users (Admin, Co-Admin, AM) const users = await this.prisma.user.findMany({