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 77bb31b..e12b86b 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) { @@ -394,7 +398,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, @@ -403,10 +407,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) { @@ -431,92 +467,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, @@ -548,191 +693,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, @@ -740,62 +722,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, @@ -803,53 +776,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({ @@ -954,7 +990,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: { @@ -1020,7 +1056,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); @@ -1035,7 +1071,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({ @@ -1043,13 +1079,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), } }) @@ -1080,24 +1112,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(), }, }); @@ -1124,6 +1155,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: {