Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1

This commit is contained in:
paritosh18
2025-12-02 17:27:57 +05:30
8 changed files with 675 additions and 667 deletions

View File

@@ -161,7 +161,7 @@ getPQQ_LastUpdatedQuestion:
path: /host/Activity_Hub/OnBoarding/get-latest-pqq-question-details path: /host/Activity_Hub/OnBoarding/get-latest-pqq-question-details
method: get method: get
getAllActivityType: prePopulateNewActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllActivityType.handler handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllActivityType.handler
memorySize: 384 memorySize: 384
package: package:
@@ -174,7 +174,7 @@ getAllActivityType:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /host/Activity_Hub/OnBoarding/get-activity-type path: /host/Activity_Hub/OnBoarding/prepopulate-new-activity
method: get method: get
showSuggestion: showSuggestion:
@@ -330,6 +330,23 @@ updatePQQ_LastAnswer:
path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer
method: post 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: getAllPQQwithSubmittedAns:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
memorySize: 512 memorySize: 512

View File

@@ -23,19 +23,15 @@ export const STEPPER = {
REJECTED: 6 REJECTED: 6
} }
export const LAST_QUESTION_ID = {
Q_ID: 55
}
export const ACTIVITY_INTERNAL_STATUS = { export const ACTIVITY_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ', DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved', APPROVED: 'Approved',
REJECTED: 'Rejected', REJECTED: 'Rejected',
DRAFT: 'Draft', DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review', UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed', PQ_FAILED: 'PQ Failed',
PQQ_TO_UPDATE: 'PQ To Update', PQ_TO_UPDATE: 'PQ To Update',
PQQ_SUBMITTED: 'PQ Submitted' PQ_SUBMITTED: 'PQ Submitted'
} }
export const ACTIVITY_DISPLAY_STATUS = { export const ACTIVITY_DISPLAY_STATUS = {
@@ -44,7 +40,7 @@ export const ACTIVITY_DISPLAY_STATUS = {
REJECTED: 'Rejected', REJECTED: 'Rejected',
DRAFT: 'Draft', DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review', UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed', PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing', ENHANCING: 'Enchancing',
PQ_IN_REVIEW: 'PQ In Review' PQ_IN_REVIEW: 'PQ In Review'
} }
@@ -55,9 +51,9 @@ export const ACTIVITY_AM_INTERNAL_STATUS = {
REJECTED: 'Rejected', REJECTED: 'Rejected',
DRAFT: 'Draft', DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review', UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed', PQ_FAILED: 'PQ Failed',
PQQ_REJECTED: 'PQ Rejected', PQ_REJECTED: 'PQ Rejected',
PQQ_TO_REVIEW: 'PQ To Review' PQ_TO_REVIEW: 'PQ To Review'
} }
export const ACTIVITY_AM_DISPLAY_STATUS = { export const ACTIVITY_AM_DISPLAY_STATUS = {
@@ -66,7 +62,7 @@ export const ACTIVITY_AM_DISPLAY_STATUS = {
REJECTED: 'Rejected', REJECTED: 'Rejected',
DRAFT: 'Draft', DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review', UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed', PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing', ENHANCING: 'Enchancing',
NEW: 'New' NEW: 'New'
} }

View File

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

View File

@@ -29,15 +29,18 @@ export const handler = safeHandler(async (
if (!activity_xid || isNaN(activity_xid)) { if (!activity_xid || isNaN(activity_xid)) {
throw new ApiError(400, "Activity id is required and must be a number."); throw new ApiError(400, "Activity id is required and must be a number.");
} }
let result = null;
// Fetch user with their HostHeader stepper info // Fetch user with their HostHeader stepper info
const pqqQuestionDetails = await hostService.getLatestQuestionDetailsPQQ(activity_xid); const pqqQuestionDetails = await hostService.getLatestQuestionDetailsPQQ(activity_xid);
const result = { if (pqqQuestionDetails) {
pqqQuestionXid: pqqQuestionDetails.pqqQuestionXid, result = {
pqqAnswerXid: pqqQuestionDetails.pqqAnswerXid, pqqQuestionXid: pqqQuestionDetails.pqqQuestionXid,
pqqSubCategoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategoryXid, pqqAnswerXid: pqqQuestionDetails.pqqAnswerXid || null,
categoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategories.categoryXid pqqSubCategoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategoryXid || null,
categoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategories.categoryXid || null
}
} }
return { return {

View File

@@ -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<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
const activity_xid = event.queryStringParameters?.activity_xid
? Number(event.queryStringParameters.activity_xid)
: null;
await pqqService.submitpqqforreview(Number(activity_xid))
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: "Your PQQ has been submitted for review.",
data: null
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

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

View File

@@ -53,6 +53,12 @@ export async function generateActivityRefNumber(tx: any) {
return `ACT-${String(nextId).padStart(6, '0')}`;; return `ACT-${String(nextId).padStart(6, '0')}`;;
} }
function round2(value: number) {
return Math.round(value * 100) / 100;
}
const bucket = config.aws.bucketName;
@Injectable() @Injectable()
export class HostService { export class HostService {
constructor(private prisma: PrismaService) { } constructor(private prisma: PrismaService) { }
@@ -140,8 +146,6 @@ export class HostService {
return { stepper: STEPPER.NOT_SUBMITTED } as any; return { stepper: STEPPER.NOT_SUBMITTED } as any;
} }
const bucket = config.aws.bucketName;
if (host.HostDocuments?.length) { if (host.HostDocuments?.length) {
for (const doc of host.HostDocuments) { for (const doc of host.HostDocuments) {
@@ -394,7 +398,7 @@ export class HostService {
} }
async getPQQQuestionDetail(question_xid: number, activity_xid: number) { async getPQQQuestionDetail(question_xid: number, activity_xid: number) {
return await this.prisma.activityPQQheader.findFirst({ const detailsOfQuestion = await this.prisma.activityPQQheader.findFirst({
where: { where: {
activityXid: activity_xid, activityXid: activity_xid,
pqqQuestionXid: question_xid, pqqQuestionXid: question_xid,
@@ -403,10 +407,42 @@ export class HostService {
select: { select: {
pqqQuestionXid: true, pqqQuestionXid: true,
pqqAnswerXid: true, pqqAnswerXid: true,
ActivityPQQSupportings: true, ActivityPQQSupportings: {
ActivityPQQSuggestions: true, 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) { async getLatestQuestionDetailsPQQ(activity_xid: number) {
@@ -431,92 +467,201 @@ export class HostService {
} }
async addOrUpdateCompanyDetails( async addOrUpdateCompanyDetails(
user_xid: number, user_xid: number,
companyData: HostCompanyDetailsInput, companyData: HostCompanyDetailsInput,
documents: HostDocumentInput[], documents: HostDocumentInput[],
parentCompanyData?: any | null, parentCompanyData?: any | null,
parentDocuments?: HostDocumentInput[], parentDocuments?: HostDocumentInput[],
isDraft: boolean = false, isDraft: boolean = false,
) { ) {
return await this.prisma.$transaction(async (tx) => { return await this.prisma.$transaction(async (tx) => {
// Check if host already has a company // Check if host already has a company
const existingHostCompany = await tx.hostHeader.findFirst({ const existingHostCompany = await tx.hostHeader.findFirst({
where: { userXid: user_xid }, where: { userXid: user_xid },
include: { hostParent: true }, include: { hostParent: true },
}); });
let hostStatusInternal; let hostStatusInternal;
let hostStatusDisplay; let hostStatusDisplay;
let minglarStatusInternal; let minglarStatusInternal;
let minglarStatusDisplay; let minglarStatusDisplay;
if (existingHostCompany) { if (existingHostCompany) {
hostStatusInternal = existingHostCompany.hostStatusInternal; hostStatusInternal = existingHostCompany.hostStatusInternal;
hostStatusDisplay = existingHostCompany.hostStatusDisplay; hostStatusDisplay = existingHostCompany.hostStatusDisplay;
minglarStatusInternal = existingHostCompany.adminStatusInternal; minglarStatusInternal = existingHostCompany.adminStatusInternal;
minglarStatusDisplay = existingHostCompany.adminStatusDisplay; 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');
} }
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: { data: {
user: { connect: { id: user_xid } },
companyName: companyData.companyName, companyName: companyData.companyName,
address1: companyData.address1, address1: companyData.address1,
address2: companyData.address2, address2: companyData.address2,
@@ -548,191 +693,28 @@ export class HostService {
}, },
}); });
// host documents // documents UPSERT
if (documents?.length) { if (documents?.length) {
const docsData = documents.map((doc) => ({ for (const doc of documents) {
hostXid: createdHost.id, const existingDoc = await tx.hostDocuments.findFirst({
documentTypeXid: doc.documentTypeXid, where: {
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: {
hostXid: updatedHost.id, hostXid: updatedHost.id,
documentTypeXid: doc.documentTypeXid, documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName,
filePath: doc.filePath,
}, },
}); });
}
}
}
// parent logic untouched if (existingDoc) {
if (companyData.isSubsidairy) { await tx.hostDocuments.update({
const parentRecords = existingHostCompany.hostParent; where: { id: existingDoc.id },
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({
data: { data: {
hostParentXid: createdParent.id, filePath: doc.filePath,
documentName: doc.documentName || existingDoc.documentName,
},
});
} else {
await tx.hostDocuments.create({
data: {
hostXid: updatedHost.id,
documentTypeXid: doc.documentTypeXid, documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName, documentName: doc.documentName,
filePath: doc.filePath, 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) { // parent logic untouched
for (const doc of parentDocuments) { if (companyData.isSubsidairy) {
const existingParentDoc = await tx.hostParenetDocuments.findFirst({ const parentRecords = existingHostCompany.hostParent;
where: { const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords;
hostParentXid: parentRecord.id,
documentTypeXid: doc.documentTypeXid,
},
});
if (existingParentDoc) { if (!parentRecord) {
await tx.hostParenetDocuments.update({ const createdParent = await tx.hostParent.create({
where: { id: existingParentDoc.id }, data: {
data: { host: { connect: { id: updatedHost.id } },
filePath: doc.filePath, companyName: parentCompanyData.companyName,
documentName: doc.documentName || existingParentDoc.documentName, address1: parentCompanyData.address1 || null,
}, address2: parentCompanyData.address2 || null,
}); cities: parentCompanyData.cityXid
} else { ? { 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({ await tx.hostParenetDocuments.create({
data: { data: {
hostParentXid: parentRecord.id, hostParentXid: createdParent.id,
documentTypeXid: doc.documentTypeXid, documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName, documentName: doc.documentName,
filePath: doc.filePath, 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) { // ⭐ FIX — USE updatedHost instead of re-querying hostHeader
prevParentId = previousParent[0].id; await tx.hostTrack.create({
} 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 },
data: { data: {
isreviewed: true, hostXid: updatedHost.id,
reviewedByXid: user_xid, updatedByRole: ROLE_NAME.HOST,
reviewOn: new Date(), 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) { async getSuggestionDetails(user_xid: number) {
const hostDetails = await this.prisma.hostHeader.findFirst({ const hostDetails = await this.prisma.hostHeader.findFirst({
@@ -954,7 +990,7 @@ export class HostService {
return await this.prisma.$transaction(async (tx) => { return await this.prisma.$transaction(async (tx) => {
// 1. Get all headers for this activity (user's answers) // 1. Get all headers for this activity (user's answers)
const answers = await this.prisma.activityPQQheader.findMany({ const answers = await this.prisma.activityPQQheader.findMany({
where: { activityXid }, where: { activityXid, isActive: true },
include: { include: {
pqqQuestions: { pqqQuestions: {
include: { include: {
@@ -1020,7 +1056,7 @@ export class HostService {
// Overall percent // Overall percent
const overallPercentage = const overallPercentage =
totalMaxPoints > 0 ? (totalUserPoints / totalMaxPoints) * 100 : 0; totalMaxPoints > 0 ? round2((totalUserPoints / totalMaxPoints) * 100) : 0;
// ---------- 🔥 ONLY FIRST 2 CATEGORIES ---------- // ---------- 🔥 ONLY FIRST 2 CATEGORIES ----------
const categoryArray = Object.values(categories); const categoryArray = Object.values(categories);
@@ -1035,7 +1071,7 @@ export class HostService {
for (const c of topTwo) { for (const c of topTwo) {
categoryWise[c.categoryName] = 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({ await this.prisma.activities.update({
@@ -1043,13 +1079,9 @@ export class HostService {
id: activityXid id: activityXid
}, },
data: { data: {
totalScore: overallPercentage, totalScore: round2(overallPercentage),
sustainabilityScore: categoryWise.Sustainability, sustainabilityScore: round2(categoryWise.Sustainability),
safetyScore: categoryWise.Safety, safetyScore: round2(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
} }
}) })
@@ -1080,24 +1112,23 @@ export class HostService {
async findHeaderByCompositeKey( async findHeaderByCompositeKey(
activityXid: number, activityXid: number,
pqqQuestionXid: number, pqqQuestionXid: number,
pqqAnswerXid: number,
) { ) {
return await this.prisma.activityPQQheader.findFirst({ return await this.prisma.activityPQQheader.findFirst({
where: { where: {
activityXid, activityXid,
pqqQuestionXid, pqqQuestionXid,
pqqAnswerXid,
}, },
}); });
} }
async updateHeader(headerId: number, comments?: string | null) { async updateHeader(headerId: number, pqqAnswerXid: number, comments?: string | null) {
return await this.prisma.activityPQQheader.update({ return await this.prisma.activityPQQheader.update({
where: { where: {
id: headerId, id: headerId,
}, },
data: { data: {
comments: comments || null, // Handle null comments comments: comments || null, // Handle null comments
pqqAnswerXid: pqqAnswerXid,
updatedAt: new Date(), 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( async updateSupportingFile(
supportingFileId: number, supportingFileId: number,
mimeType: string, mimeType: string,

View File

@@ -67,7 +67,10 @@ export class PrePopulateService {
async getAllPQQQuesAndAns() { async getAllPQQQuesAndAns() {
return await this.prisma.pQQCategories.findMany({ return await this.prisma.pQQCategories.findMany({
where: { isActive: true }, where: { isActive: true },
include: { select: {
id: true,
categoryName: true,
displayOrder: true,
pqqsubCategories: { pqqsubCategories: {
where: { isActive: true }, where: { isActive: true },
select: { select: {