From 6b673a173d400bbe2bde31342c9d06066531e37c Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 2 Dec 2025 17:24:01 +0530 Subject: [PATCH] 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. --- serverless/functions/host.yml | 21 +- src/common/utils/constants/host.constant.ts | 20 +- .../Activity_Hub/OnBoarding/getPQQScore.ts | 262 ++---- .../OnBoarding/getPQQ_LastUpdatedQuestion.ts | 13 +- .../OnBoarding/submitPQQForReview.ts | 41 + .../OnBoarding/submitPQQ_Answer.ts | 187 ++--- src/modules/host/services/host.service.ts | 793 ++++++++++-------- .../services/prepopulate.service.ts | 5 +- 8 files changed, 675 insertions(+), 667 deletions(-) create mode 100644 src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.ts diff --git a/serverless/functions/host.yml b/serverless/functions/host.yml index fc3d776..97615bf 100644 --- a/serverless/functions/host.yml +++ b/serverless/functions/host.yml @@ -161,7 +161,7 @@ getPQQ_LastUpdatedQuestion: 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: @@ -174,7 +174,7 @@ getAllActivityType: - ${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 showSuggestion: @@ -330,6 +330,23 @@ updatePQQ_LastAnswer: path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer method: post + +submitPQQForReview: + handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.handler + memorySize: 384 + package: + patterns: + - '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/submit-pqq-for-review + method: patch + getAllPQQwithSubmittedAns: handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler memorySize: 512 diff --git a/src/common/utils/constants/host.constant.ts b/src/common/utils/constants/host.constant.ts index 3a24c92..63115d7 100644 --- a/src/common/utils/constants/host.constant.ts +++ b/src/common/utils/constants/host.constant.ts @@ -23,19 +23,15 @@ export const STEPPER = { REJECTED: 6 } -export const LAST_QUESTION_ID = { - Q_ID: 55 -} - 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' + PQ_FAILED: 'PQ Failed', + PQ_TO_UPDATE: 'PQ To Update', + PQ_SUBMITTED: 'PQ Submitted' } export const ACTIVITY_DISPLAY_STATUS = { @@ -44,7 +40,7 @@ export const ACTIVITY_DISPLAY_STATUS = { REJECTED: 'Rejected', DRAFT: 'Draft', UNDER_REVIEW: 'Under-Review', - PQQ_FAILED: 'PQQ Failed', + PQ_FAILED: 'PQ Failed', ENHANCING: 'Enchancing', PQ_IN_REVIEW: 'PQ In Review' } @@ -55,9 +51,9 @@ export const ACTIVITY_AM_INTERNAL_STATUS = { REJECTED: 'Rejected', DRAFT: 'Draft', UNDER_REVIEW: 'Under-Review', - PQQ_FAILED: 'PQQ Failed', - PQQ_REJECTED: 'PQ Rejected', - PQQ_TO_REVIEW: 'PQ To Review' + PQ_FAILED: 'PQ Failed', + PQ_REJECTED: 'PQ Rejected', + PQ_TO_REVIEW: 'PQ To Review' } export const ACTIVITY_AM_DISPLAY_STATUS = { @@ -66,7 +62,7 @@ export const ACTIVITY_AM_DISPLAY_STATUS = { REJECTED: 'Rejected', DRAFT: 'Draft', UNDER_REVIEW: 'Under-Review', - PQQ_FAILED: 'PQQ Failed', + PQ_FAILED: 'PQ Failed', ENHANCING: 'Enchancing', NEW: 'New' } \ No newline at end of file diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQScore.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQScore.ts index 1483dc8..0214c15 100644 --- a/src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQScore.ts +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQScore.ts @@ -8,48 +8,36 @@ import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHo 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 +// 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 { 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 { - let s3Key: string; +// Upload new file +async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string): Promise { + 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 +46,161 @@ 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 => { 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((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 pqqService.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 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, - pqqAnswerXid, - comments - ); + header = await pqqService.createHeader(activityXid, pqqQuestionXid, pqqAnswerXid, comments); } - // Calculate score after answer submission + + // SCORE const score = await pqqService.calculatePqqScoreForUser(activityXid); - - // 6) Get existing supporting files for this header + // 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[] = []; + // 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 pqqService.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 pqqService.addSupportingFile(header.id, file.mimeType, url); + addResults.push(newRec); } } - // 9) Prepare response - const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully"; + // 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') + added: addResults, + deleted: deleteResults + } } }) }; - } catch (error: any) { - console.error("❌ Error in submitPqqAnswer:", error); - throw error; + } catch (err: any) { + console.error("❌ Error:", err); + throw err; } -}); \ No newline at end of file +}); diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_LastUpdatedQuestion.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_LastUpdatedQuestion.ts index 672785f..e34df79 100644 --- a/src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_LastUpdatedQuestion.ts +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_LastUpdatedQuestion.ts @@ -29,15 +29,18 @@ export const handler = safeHandler(async ( 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 { diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.ts new file mode 100644 index 0000000..eecd14d --- /dev/null +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.ts @@ -0,0 +1,41 @@ +import { verifyHostToken } from '@/common/middlewares/jwt/authForHost'; +import { APIGatewayProxyEvent, APIGatewayProxyResult } 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 { HostService } from '../../../services/host.service'; + +const prisma = new PrismaService(); +const pqqService = new HostService(prisma); + +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); + + const activity_xid = event.queryStringParameters?.activity_xid + ? Number(event.queryStringParameters.activity_xid) + : null; + + await pqqService.submitpqqforreview(Number(activity_xid)) + + 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; + } +}); diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQ_Answer.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQ_Answer.ts index 2f0e927..9e15174 100644 --- a/src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQ_Answer.ts +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQ_Answer.ts @@ -30,34 +30,24 @@ async function deleteFromS3(s3Key: string): 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 + // 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 { - 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 +72,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 +80,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((resolve, reject) => { const bb = Busboy({ headers: { 'content-type': contentType } }); @@ -152,41 +142,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 +176,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 = []; + + // 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(); + 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 +279,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' } }) }; @@ -301,4 +296,4 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< console.error("❌ Error in submitPqqAnswer:", error); throw error; } -}); \ No newline at end of file +}); diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 430afaf..f3cfa84 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -53,6 +53,12 @@ export async function generateActivityRefNumber(tx: any) { return `ACT-${String(nextId).padStart(6, '0')}`;; } +function round2(value: number) { + return Math.round(value * 100) / 100; +} + +const bucket = config.aws.bucketName; + @Injectable() export class HostService { constructor(private prisma: PrismaService) { } @@ -140,8 +146,6 @@ export class HostService { return { stepper: STEPPER.NOT_SUBMITTED } as any; } - const bucket = config.aws.bucketName; - if (host.HostDocuments?.length) { for (const doc of host.HostDocuments) { @@ -383,7 +387,7 @@ export class HostService { } async getPQQQuestionDetail(question_xid: number, activity_xid: number) { - return await this.prisma.activityPQQheader.findFirst({ + const detailsOfQuestion = await this.prisma.activityPQQheader.findFirst({ where: { activityXid: activity_xid, pqqQuestionXid: question_xid, @@ -392,10 +396,42 @@ export class HostService { select: { pqqQuestionXid: true, pqqAnswerXid: true, - ActivityPQQSupportings: true, - ActivityPQQSuggestions: true, + ActivityPQQSupportings: { + select: { + id: true, + activityPqqHeaderXid: true, + mediaFileName: true, + mediaType: true + } + }, + ActivityPQQSuggestions: { + where: { isActive: true, isReviewed: false }, + select: { + id: true, + title: true, + comments: true, + } + }, }, }); + + if (detailsOfQuestion.ActivityPQQSupportings?.length) { + + for (const doc of detailsOfQuestion.ActivityPQQSupportings) { + if (doc.mediaFileName) { + const filePath = doc.mediaFileName; + + // If full URL is saved, extract only key + const key = filePath.startsWith('http') + ? filePath.split('.com/')[1] + : filePath; + + (doc as any).presignedUrl = await getPresignedUrl(bucket, key); + } + } + } + + return detailsOfQuestion; } async getLatestQuestionDetailsPQQ(activity_xid: number) { @@ -420,92 +456,201 @@ export class HostService { } async addOrUpdateCompanyDetails( - user_xid: number, - companyData: HostCompanyDetailsInput, - documents: HostDocumentInput[], - parentCompanyData?: any | null, - parentDocuments?: HostDocumentInput[], - isDraft: boolean = false, -) { - return await this.prisma.$transaction(async (tx) => { - // Check if host already has a company - const existingHostCompany = await tx.hostHeader.findFirst({ - where: { userXid: user_xid }, - include: { hostParent: true }, - }); + user_xid: number, + companyData: HostCompanyDetailsInput, + documents: HostDocumentInput[], + parentCompanyData?: any | null, + parentDocuments?: HostDocumentInput[], + isDraft: boolean = false, + ) { + return await this.prisma.$transaction(async (tx) => { + // Check if host already has a company + const existingHostCompany = await tx.hostHeader.findFirst({ + where: { userXid: user_xid }, + include: { hostParent: true }, + }); - let hostStatusInternal; - let hostStatusDisplay; - let minglarStatusInternal; - let minglarStatusDisplay; + let hostStatusInternal; + let hostStatusDisplay; + let minglarStatusInternal; + let minglarStatusDisplay; - if (existingHostCompany) { - hostStatusInternal = existingHostCompany.hostStatusInternal; - hostStatusDisplay = existingHostCompany.hostStatusDisplay; - minglarStatusInternal = existingHostCompany.adminStatusInternal; - minglarStatusDisplay = existingHostCompany.adminStatusDisplay; - } - - // CASE 1: Host was asked to update AND is submitting final - if ( - existingHostCompany && - existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE && - !isDraft - ) { - hostStatusInternal = HOST_STATUS_INTERNAL.HOST_SUBMITTED; - hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW; - - minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW; - minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW; - } - // CASE 2: Host was asked to update BUT saving draft - else if ( - existingHostCompany && - existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE && - isDraft - ) { - // keep original - hostStatusInternal = existingHostCompany.hostStatusInternal; - hostStatusDisplay = existingHostCompany.hostStatusDisplay; - minglarStatusInternal = existingHostCompany.adminStatusInternal; - minglarStatusDisplay = existingHostCompany.adminStatusDisplay; - } - // CASE 3: Normal create or update - else { - hostStatusInternal = isDraft - ? HOST_STATUS_INTERNAL.DRAFT - : HOST_STATUS_INTERNAL.HOST_SUBMITTED; - - hostStatusDisplay = isDraft - ? HOST_STATUS_DISPLAY.DRAFT - : HOST_STATUS_DISPLAY.UNDER_REVIEW; - - minglarStatusInternal = isDraft - ? MINGLAR_STATUS_INTERNAL.DRAFT - : MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW; - - minglarStatusDisplay = isDraft - ? MINGLAR_STATUS_DISPLAY.DRAFT - : MINGLAR_STATUS_DISPLAY.NEW; - } - - const stepper = isDraft ? STEPPER.NOT_SUBMITTED : STEPPER.UNDER_REVIEW; - - // ------------------------------------------------------- - // CREATE FLOW - // ------------------------------------------------------- - if (!existingHostCompany) { - if (!isDraft) { - const existingByPan = await tx.hostHeader.findFirst({ - where: { panNumber: companyData.panNumber }, - }); - if (existingByPan) - throw new ApiError(400, 'Company already exists with this pan/bin number'); + if (existingHostCompany) { + hostStatusInternal = existingHostCompany.hostStatusInternal; + hostStatusDisplay = existingHostCompany.hostStatusDisplay; + minglarStatusInternal = existingHostCompany.adminStatusInternal; + minglarStatusDisplay = existingHostCompany.adminStatusDisplay; } - const createdHost = await tx.hostHeader.create({ + // CASE 1: Host was asked to update AND is submitting final + if ( + existingHostCompany && + existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE && + !isDraft + ) { + hostStatusInternal = HOST_STATUS_INTERNAL.HOST_SUBMITTED; + hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW; + + minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW; + minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW; + } + // CASE 2: Host was asked to update BUT saving draft + else if ( + existingHostCompany && + existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE && + isDraft + ) { + // keep original + hostStatusInternal = existingHostCompany.hostStatusInternal; + hostStatusDisplay = existingHostCompany.hostStatusDisplay; + minglarStatusInternal = existingHostCompany.adminStatusInternal; + minglarStatusDisplay = existingHostCompany.adminStatusDisplay; + } + // CASE 3: Normal create or update + else { + hostStatusInternal = isDraft + ? HOST_STATUS_INTERNAL.DRAFT + : HOST_STATUS_INTERNAL.HOST_SUBMITTED; + + hostStatusDisplay = isDraft + ? HOST_STATUS_DISPLAY.DRAFT + : HOST_STATUS_DISPLAY.UNDER_REVIEW; + + minglarStatusInternal = isDraft + ? MINGLAR_STATUS_INTERNAL.DRAFT + : MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW; + + minglarStatusDisplay = isDraft + ? MINGLAR_STATUS_DISPLAY.DRAFT + : MINGLAR_STATUS_DISPLAY.NEW; + } + + const stepper = isDraft ? STEPPER.NOT_SUBMITTED : STEPPER.UNDER_REVIEW; + + // ------------------------------------------------------- + // CREATE FLOW + // ------------------------------------------------------- + if (!existingHostCompany) { + if (!isDraft) { + const existingByPan = await tx.hostHeader.findFirst({ + where: { panNumber: companyData.panNumber }, + }); + if (existingByPan) + throw new ApiError(400, 'Company already exists with this pan/bin number'); + } + + const createdHost = await tx.hostHeader.create({ + data: { + user: { connect: { id: user_xid } }, + companyName: companyData.companyName, + address1: companyData.address1, + address2: companyData.address2, + cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined, + states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined, + countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined, + pinCode: companyData.pinCode, + logoPath: companyData.logoPath || null, + isSubsidairy: companyData.isSubsidairy, + registrationNumber: companyData.registrationNumber, + panNumber: companyData.panNumber, + gstNumber: companyData.gstNumber || null, + formationDate: companyData.formationDate + ? new Date(companyData.formationDate as any) + : null, + companyTypes: companyData.companyTypeXid + ? { connect: { id: companyData.companyTypeXid } } + : undefined, + websiteUrl: companyData.websiteUrl || null, + instagramUrl: companyData.instagramUrl || null, + facebookUrl: companyData.facebookUrl || null, + linkedinUrl: companyData.linkedinUrl || null, + twitterUrl: companyData.twitterUrl || null, + stepper, + hostStatusInternal, + hostStatusDisplay, + adminStatusInternal: minglarStatusInternal, + adminStatusDisplay: minglarStatusDisplay, + }, + }); + + // host documents + if (documents?.length) { + const docsData = documents.map((doc) => ({ + hostXid: createdHost.id, + documentTypeXid: doc.documentTypeXid, + documentName: doc.documentName, + filePath: doc.filePath, + })); + await tx.hostDocuments.createMany({ data: docsData }); + } + + // parent create + if (companyData.isSubsidairy && parentCompanyData) { + const createdParent = await tx.hostParent.create({ + data: { + host: { connect: { id: createdHost.id } }, + companyName: parentCompanyData.companyName, + address1: parentCompanyData.address1 || null, + address2: parentCompanyData.address2 || null, + cities: parentCompanyData.cityXid + ? { connect: { id: parentCompanyData.cityXid } } + : undefined, + states: parentCompanyData.stateXid + ? { connect: { id: parentCompanyData.stateXid } } + : undefined, + countries: parentCompanyData.countryXid + ? { connect: { id: parentCompanyData.countryXid } } + : undefined, + pinCode: parentCompanyData.pinCode || null, + logoPath: parentCompanyData.logoPath || null, + registrationNumber: parentCompanyData.registrationNumber || null, + panNumber: parentCompanyData.panNumber || null, + gstNumber: parentCompanyData.gstNumber || null, + formationDate: parentCompanyData.formationDate + ? new Date(parentCompanyData.formationDate as any) + : null, + companyTypes: parentCompanyData.companyTypeXid + ? { connect: { id: parentCompanyData.companyTypeXid } } + : undefined, + websiteUrl: parentCompanyData.websiteUrl || null, + instagramUrl: parentCompanyData.instagramUrl || null, + facebookUrl: parentCompanyData.facebookUrl || null, + linkedinUrl: parentCompanyData.linkedinUrl || null, + twitterUrl: parentCompanyData.twitterUrl || null, + }, + }); + + // parent docs + if (parentDocuments?.length) { + const parentDocsData = parentDocuments.map((doc) => ({ + hostParentXid: createdParent.id, + documentTypeXid: doc.documentTypeXid, + documentName: doc.documentName, + filePath: doc.filePath, + })); + await tx.hostParenetDocuments.createMany({ data: parentDocsData }); + } + } + + // ⭐ FIX — TRACK USING createdHost (no null risk) + await tx.hostTrack.create({ + data: { + hostXid: createdHost.id, + updatedByRole: ROLE_NAME.HOST, + updatedByXid: user_xid, + trackStatus: createdHost.hostStatusInternal, + }, + }); + + return createdHost; + } + + // ------------------------------------------------------- + // UPDATE FLOW + // ------------------------------------------------------- + const updatedHost = await tx.hostHeader.update({ + where: { id: existingHostCompany.id }, data: { - user: { connect: { id: user_xid } }, companyName: companyData.companyName, address1: companyData.address1, address2: companyData.address2, @@ -537,191 +682,28 @@ export class HostService { }, }); - // host documents + // documents UPSERT if (documents?.length) { - const docsData = documents.map((doc) => ({ - hostXid: createdHost.id, - documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, - filePath: doc.filePath, - })); - await tx.hostDocuments.createMany({ data: docsData }); - } - - // parent create - if (companyData.isSubsidairy && parentCompanyData) { - const createdParent = await tx.hostParent.create({ - data: { - host: { connect: { id: createdHost.id } }, - companyName: parentCompanyData.companyName, - address1: parentCompanyData.address1 || null, - address2: parentCompanyData.address2 || null, - cities: parentCompanyData.cityXid - ? { connect: { id: parentCompanyData.cityXid } } - : undefined, - states: parentCompanyData.stateXid - ? { connect: { id: parentCompanyData.stateXid } } - : undefined, - countries: parentCompanyData.countryXid - ? { connect: { id: parentCompanyData.countryXid } } - : undefined, - pinCode: parentCompanyData.pinCode || null, - logoPath: parentCompanyData.logoPath || null, - registrationNumber: parentCompanyData.registrationNumber || null, - panNumber: parentCompanyData.panNumber || null, - gstNumber: parentCompanyData.gstNumber || null, - formationDate: parentCompanyData.formationDate - ? new Date(parentCompanyData.formationDate as any) - : null, - companyTypes: parentCompanyData.companyTypeXid - ? { connect: { id: parentCompanyData.companyTypeXid } } - : undefined, - websiteUrl: parentCompanyData.websiteUrl || null, - instagramUrl: parentCompanyData.instagramUrl || null, - facebookUrl: parentCompanyData.facebookUrl || null, - linkedinUrl: parentCompanyData.linkedinUrl || null, - twitterUrl: parentCompanyData.twitterUrl || null, - }, - }); - - // parent docs - if (parentDocuments?.length) { - const parentDocsData = parentDocuments.map((doc) => ({ - hostParentXid: createdParent.id, - documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, - filePath: doc.filePath, - })); - await tx.hostParenetDocuments.createMany({ data: parentDocsData }); - } - } - - // ⭐ FIX — TRACK USING createdHost (no null risk) - await tx.hostTrack.create({ - data: { - hostXid: createdHost.id, - updatedByRole: ROLE_NAME.HOST, - updatedByXid: user_xid, - trackStatus: createdHost.hostStatusInternal, - }, - }); - - return createdHost; - } - - // ------------------------------------------------------- - // UPDATE FLOW - // ------------------------------------------------------- - const updatedHost = await tx.hostHeader.update({ - where: { id: existingHostCompany.id }, - data: { - companyName: companyData.companyName, - address1: companyData.address1, - address2: companyData.address2, - cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined, - states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined, - countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined, - pinCode: companyData.pinCode, - logoPath: companyData.logoPath || null, - isSubsidairy: companyData.isSubsidairy, - registrationNumber: companyData.registrationNumber, - panNumber: companyData.panNumber, - gstNumber: companyData.gstNumber || null, - formationDate: companyData.formationDate - ? new Date(companyData.formationDate as any) - : null, - companyTypes: companyData.companyTypeXid - ? { connect: { id: companyData.companyTypeXid } } - : undefined, - websiteUrl: companyData.websiteUrl || null, - instagramUrl: companyData.instagramUrl || null, - facebookUrl: companyData.facebookUrl || null, - linkedinUrl: companyData.linkedinUrl || null, - twitterUrl: companyData.twitterUrl || null, - stepper, - hostStatusInternal, - hostStatusDisplay, - adminStatusInternal: minglarStatusInternal, - adminStatusDisplay: minglarStatusDisplay, - }, - }); - - // documents UPSERT - if (documents?.length) { - for (const doc of documents) { - const existingDoc = await tx.hostDocuments.findFirst({ - where: { - hostXid: updatedHost.id, - documentTypeXid: doc.documentTypeXid, - }, - }); - - if (existingDoc) { - await tx.hostDocuments.update({ - where: { id: existingDoc.id }, - data: { - filePath: doc.filePath, - documentName: doc.documentName || existingDoc.documentName, - }, - }); - } else { - await tx.hostDocuments.create({ - data: { + for (const doc of documents) { + const existingDoc = await tx.hostDocuments.findFirst({ + where: { hostXid: updatedHost.id, documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, - filePath: doc.filePath, }, }); - } - } - } - // parent logic untouched - if (companyData.isSubsidairy) { - const parentRecords = existingHostCompany.hostParent; - const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords; - - if (!parentRecord) { - const createdParent = await tx.hostParent.create({ - data: { - host: { connect: { id: updatedHost.id } }, - companyName: parentCompanyData.companyName, - address1: parentCompanyData.address1 || null, - address2: parentCompanyData.address2 || null, - cities: parentCompanyData.cityXid - ? { connect: { id: parentCompanyData.cityXid } } - : undefined, - states: parentCompanyData.stateXid - ? { connect: { id: parentCompanyData.stateXid } } - : undefined, - countries: parentCompanyData.countryXid - ? { connect: { id: parentCompanyData.countryXid } } - : undefined, - pinCode: parentCompanyData.pinCode || null, - logoPath: parentCompanyData.logoPath || null, - registrationNumber: parentCompanyData.registrationNumber || null, - panNumber: parentCompanyData.panNumber || null, - gstNumber: parentCompanyData.gstNumber || null, - formationDate: parentCompanyData.formationDate - ? new Date(parentCompanyData.formationDate as any) - : null, - companyTypes: parentCompanyData.companyTypeXid - ? { connect: { id: parentCompanyData.companyTypeXid } } - : undefined, - websiteUrl: parentCompanyData.websiteUrl || null, - instagramUrl: parentCompanyData.instagramUrl || null, - facebookUrl: parentCompanyData.facebookUrl || null, - linkedinUrl: parentCompanyData.linkedinUrl || null, - twitterUrl: parentCompanyData.twitterUrl || null, - }, - }); - - if (parentDocuments?.length) { - for (const doc of parentDocuments) { - await tx.hostParenetDocuments.create({ + if (existingDoc) { + await tx.hostDocuments.update({ + where: { id: existingDoc.id }, data: { - hostParentXid: createdParent.id, + filePath: doc.filePath, + documentName: doc.documentName || existingDoc.documentName, + }, + }); + } else { + await tx.hostDocuments.create({ + data: { + hostXid: updatedHost.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, @@ -729,62 +711,53 @@ export class HostService { }); } } - } else { - await tx.hostParent.update({ - where: { id: parentRecord.id }, - data: { - companyName: parentCompanyData.companyName, - address1: parentCompanyData.address1 || null, - address2: parentCompanyData.address2 || null, - cities: parentCompanyData.cityXid - ? { connect: { id: parentCompanyData.cityXid } } - : undefined, - states: parentCompanyData.stateXid - ? { connect: { id: parentCompanyData.stateXid } } - : undefined, - countries: parentCompanyData.countryXid - ? { connect: { id: parentCompanyData.countryXid } } - : undefined, - pinCode: parentCompanyData.pinCode || null, - logoPath: parentCompanyData.logoPath || null, - registrationNumber: parentCompanyData.registrationNumber || null, - panNumber: parentCompanyData.panNumber || null, - gstNumber: parentCompanyData.gstNumber || null, - formationDate: parentCompanyData.formationDate - ? new Date(parentCompanyData.formationDate as any) - : null, - companyTypes: parentCompanyData.companyTypeXid - ? { connect: { id: parentCompanyData.companyTypeXid } } - : undefined, - websiteUrl: parentCompanyData.websiteUrl || null, - instagramUrl: parentCompanyData.instagramUrl || null, - facebookUrl: parentCompanyData.facebookUrl || null, - linkedinUrl: parentCompanyData.linkedinUrl || null, - twitterUrl: parentCompanyData.twitterUrl || null, - }, - }); + } - if (parentDocuments?.length) { - for (const doc of parentDocuments) { - const existingParentDoc = await tx.hostParenetDocuments.findFirst({ - where: { - hostParentXid: parentRecord.id, - documentTypeXid: doc.documentTypeXid, - }, - }); + // parent logic untouched + if (companyData.isSubsidairy) { + const parentRecords = existingHostCompany.hostParent; + const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords; - if (existingParentDoc) { - await tx.hostParenetDocuments.update({ - where: { id: existingParentDoc.id }, - data: { - filePath: doc.filePath, - documentName: doc.documentName || existingParentDoc.documentName, - }, - }); - } else { + if (!parentRecord) { + const createdParent = await tx.hostParent.create({ + data: { + host: { connect: { id: updatedHost.id } }, + companyName: parentCompanyData.companyName, + address1: parentCompanyData.address1 || null, + address2: parentCompanyData.address2 || null, + cities: parentCompanyData.cityXid + ? { connect: { id: parentCompanyData.cityXid } } + : undefined, + states: parentCompanyData.stateXid + ? { connect: { id: parentCompanyData.stateXid } } + : undefined, + countries: parentCompanyData.countryXid + ? { connect: { id: parentCompanyData.countryXid } } + : undefined, + pinCode: parentCompanyData.pinCode || null, + logoPath: parentCompanyData.logoPath || null, + registrationNumber: parentCompanyData.registrationNumber || null, + panNumber: parentCompanyData.panNumber || null, + gstNumber: parentCompanyData.gstNumber || null, + formationDate: parentCompanyData.formationDate + ? new Date(parentCompanyData.formationDate as any) + : null, + companyTypes: parentCompanyData.companyTypeXid + ? { connect: { id: parentCompanyData.companyTypeXid } } + : undefined, + websiteUrl: parentCompanyData.websiteUrl || null, + instagramUrl: parentCompanyData.instagramUrl || null, + facebookUrl: parentCompanyData.facebookUrl || null, + linkedinUrl: parentCompanyData.linkedinUrl || null, + twitterUrl: parentCompanyData.twitterUrl || null, + }, + }); + + if (parentDocuments?.length) { + for (const doc of parentDocuments) { await tx.hostParenetDocuments.create({ data: { - hostParentXid: parentRecord.id, + hostParentXid: createdParent.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, @@ -792,53 +765,116 @@ export class HostService { }); } } + } else { + await tx.hostParent.update({ + where: { id: parentRecord.id }, + data: { + companyName: parentCompanyData.companyName, + address1: parentCompanyData.address1 || null, + address2: parentCompanyData.address2 || null, + cities: parentCompanyData.cityXid + ? { connect: { id: parentCompanyData.cityXid } } + : undefined, + states: parentCompanyData.stateXid + ? { connect: { id: parentCompanyData.stateXid } } + : undefined, + countries: parentCompanyData.countryXid + ? { connect: { id: parentCompanyData.countryXid } } + : undefined, + pinCode: parentCompanyData.pinCode || null, + logoPath: parentCompanyData.logoPath || null, + registrationNumber: parentCompanyData.registrationNumber || null, + panNumber: parentCompanyData.panNumber || null, + gstNumber: parentCompanyData.gstNumber || null, + formationDate: parentCompanyData.formationDate + ? new Date(parentCompanyData.formationDate as any) + : null, + companyTypes: parentCompanyData.companyTypeXid + ? { connect: { id: parentCompanyData.companyTypeXid } } + : undefined, + websiteUrl: parentCompanyData.websiteUrl || null, + instagramUrl: parentCompanyData.instagramUrl || null, + facebookUrl: parentCompanyData.facebookUrl || null, + linkedinUrl: parentCompanyData.linkedinUrl || null, + twitterUrl: parentCompanyData.twitterUrl || null, + }, + }); + + if (parentDocuments?.length) { + for (const doc of parentDocuments) { + const existingParentDoc = await tx.hostParenetDocuments.findFirst({ + where: { + hostParentXid: parentRecord.id, + documentTypeXid: doc.documentTypeXid, + }, + }); + + if (existingParentDoc) { + await tx.hostParenetDocuments.update({ + where: { id: existingParentDoc.id }, + data: { + filePath: doc.filePath, + documentName: doc.documentName || existingParentDoc.documentName, + }, + }); + } else { + await tx.hostParenetDocuments.create({ + data: { + hostParentXid: parentRecord.id, + documentTypeXid: doc.documentTypeXid, + documentName: doc.documentName, + filePath: doc.filePath, + }, + }); + } + } + } + } + } else { + const previousParent = existingHostCompany.hostParent; + let prevParentId = null; + + if (Array.isArray(previousParent) && previousParent.length) { + prevParentId = previousParent[0].id; + } else if (previousParent && typeof previousParent === 'object' && 'id' in previousParent) { + prevParentId = previousParent.id; + } + + if (prevParentId) { + await tx.hostParenetDocuments.deleteMany({ + where: { hostParentXid: prevParentId }, + }); + await tx.hostParent.delete({ where: { id: prevParentId } }); } } - } else { - const previousParent = existingHostCompany.hostParent; - let prevParentId = null; - if (Array.isArray(previousParent) && previousParent.length) { - prevParentId = previousParent[0].id; - } else if (previousParent && typeof previousParent === 'object' && 'id' in previousParent) { - prevParentId = previousParent.id; - } - - if (prevParentId) { - await tx.hostParenetDocuments.deleteMany({ - where: { hostParentXid: prevParentId }, - }); - await tx.hostParent.delete({ where: { id: prevParentId } }); - } - } - - // ⭐ FIX — USE updatedHost instead of re-querying hostHeader - await tx.hostTrack.create({ - data: { - hostXid: updatedHost.id, - updatedByRole: ROLE_NAME.HOST, - updatedByXid: user_xid, - trackStatus: updatedHost.hostStatusInternal, - }, - }); - - // suggestion update unchanged - if (!isDraft) { - await tx.hostSuggestion.updateMany({ - where: { hostXid: updatedHost.id, isActive: true, isreviewed: false }, + // ⭐ FIX — USE updatedHost instead of re-querying hostHeader + await tx.hostTrack.create({ data: { - isreviewed: true, - reviewedByXid: user_xid, - reviewOn: new Date(), + hostXid: updatedHost.id, + updatedByRole: ROLE_NAME.HOST, + updatedByXid: user_xid, + trackStatus: updatedHost.hostStatusInternal, }, }); - } - return updatedHost; - }); -} + // suggestion update unchanged + if (!isDraft) { + await tx.hostSuggestion.updateMany({ + where: { hostXid: updatedHost.id, isActive: true, isreviewed: false }, + data: { + isreviewed: true, + reviewedByXid: user_xid, + reviewOn: new Date(), + }, + }); + } + + return updatedHost; + }); + } + - async getSuggestionDetails(user_xid: number) { const hostDetails = await this.prisma.hostHeader.findFirst({ @@ -943,7 +979,7 @@ export class HostService { return await this.prisma.$transaction(async (tx) => { // 1. Get all headers for this activity (user's answers) const answers = await this.prisma.activityPQQheader.findMany({ - where: { activityXid }, + where: { activityXid, isActive: true }, include: { pqqQuestions: { include: { @@ -1009,7 +1045,7 @@ export class HostService { // Overall percent const overallPercentage = - totalMaxPoints > 0 ? (totalUserPoints / totalMaxPoints) * 100 : 0; + totalMaxPoints > 0 ? round2((totalUserPoints / totalMaxPoints) * 100) : 0; // ---------- 🔥 ONLY FIRST 2 CATEGORIES ---------- const categoryArray = Object.values(categories); @@ -1024,7 +1060,7 @@ export class HostService { for (const c of topTwo) { categoryWise[c.categoryName] = - c.maxPoints > 0 ? (c.userPoints / c.maxPoints) * 100 : 0; + c.maxPoints > 0 ? round2((c.userPoints / c.maxPoints) * 100) : 0; } await this.prisma.activities.update({ @@ -1032,13 +1068,9 @@ export class HostService { id: activityXid }, data: { - totalScore: overallPercentage, - sustainabilityScore: categoryWise.Sustainability, - safetyScore: categoryWise.Safety, - activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQQ_SUBMITTED, - activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.PQ_IN_REVIEW, - amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQQ_TO_REVIEW, - amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.NEW + totalScore: round2(overallPercentage), + sustainabilityScore: round2(categoryWise.Sustainability), + safetyScore: round2(categoryWise.Safety), } }) @@ -1069,24 +1101,23 @@ export class HostService { async findHeaderByCompositeKey( activityXid: number, pqqQuestionXid: number, - pqqAnswerXid: number, ) { return await this.prisma.activityPQQheader.findFirst({ where: { activityXid, pqqQuestionXid, - pqqAnswerXid, }, }); } - async updateHeader(headerId: number, comments?: string | null) { + async updateHeader(headerId: number, pqqAnswerXid: number, comments?: string | null) { return await this.prisma.activityPQQheader.update({ where: { id: headerId, }, data: { comments: comments || null, // Handle null comments + pqqAnswerXid: pqqAnswerXid, updatedAt: new Date(), }, }); @@ -1113,6 +1144,32 @@ export class HostService { }); } + async submitpqqforreview(activity_xid: number) { + const activity = await this.prisma.activities.findFirst({ + where: { id: activity_xid, isActive: true }, + select: { + id: true, + activityTitle: true, + activityRefNumber: true, + } + }) + + if (!activity) { + throw new ApiError(404, "Activity not found") + } + + await this.prisma.activities.update({ + where: { id: activity_xid }, + data: { + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQ_SUBMITTED, + activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.PQ_IN_REVIEW, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQ_TO_REVIEW, + amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.NEW + } + }) + + } + async updateSupportingFile( supportingFileId: number, mimeType: string, diff --git a/src/modules/prepopulate/services/prepopulate.service.ts b/src/modules/prepopulate/services/prepopulate.service.ts index 257df81..f8dbccd 100644 --- a/src/modules/prepopulate/services/prepopulate.service.ts +++ b/src/modules/prepopulate/services/prepopulate.service.ts @@ -67,7 +67,10 @@ export class PrePopulateService { async getAllPQQQuesAndAns() { return await this.prisma.pQQCategories.findMany({ where: { isActive: true }, - include: { + select: { + id: true, + categoryName: true, + displayOrder: true, pqqsubCategories: { where: { isActive: true }, select: {