2025-11-19 16:55:54 +05:30
|
|
|
|
import config from '@/config/config';
|
|
|
|
|
|
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
|
|
|
|
|
|
import AWS from 'aws-sdk';
|
|
|
|
|
|
import Busboy from 'busboy';
|
|
|
|
|
|
import crypto from 'crypto';
|
2025-11-26 17:31:08 +05:30
|
|
|
|
import { PrismaService } from '../../../../../common/database/prisma.service';
|
|
|
|
|
|
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
|
|
|
|
|
|
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
|
|
|
|
|
|
import ApiError from '../../../../../common/utils/helper/ApiError';
|
|
|
|
|
|
import { HostService } from '../../../services/host.service';
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
|
|
|
|
|
const prisma = new PrismaService();
|
|
|
|
|
|
const pqqService = new HostService(prisma);
|
|
|
|
|
|
|
|
|
|
|
|
const s3 = new AWS.S3({ region: config.aws.region });
|
|
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
// Function to extract S3 key from URL
|
|
|
|
|
|
function getS3KeyFromUrl(url: string): string {
|
|
|
|
|
|
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
|
|
|
|
|
|
return url.replace(bucketBaseUrl, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Function to delete file from S3
|
|
|
|
|
|
async function deleteFromS3(s3Key: string): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await s3.deleteObject({
|
|
|
|
|
|
Bucket: config.aws.bucketName,
|
|
|
|
|
|
Key: s3Key,
|
2025-11-19 16:55:54 +05:30
|
|
|
|
}).promise();
|
2025-11-22 20:05:43 +05:30
|
|
|
|
console.log(`✅ File deleted from S3: ${s3Key}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
|
|
|
|
|
|
// Don't throw error here, continue with upload
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
|
|
|
|
|
|
let s3Key: string;
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
// If existing URL provided, use the same S3 key to replace the file
|
|
|
|
|
|
if (existingUrl) {
|
|
|
|
|
|
s3Key = getS3KeyFromUrl(existingUrl);
|
|
|
|
|
|
// Delete existing file first
|
|
|
|
|
|
await deleteFromS3(s3Key);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Generate new unique key for new file
|
|
|
|
|
|
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
|
|
|
|
|
|
s3Key = `${prefix}/${uniqueKey}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Upload new file (replaces existing if same key)
|
|
|
|
|
|
await s3.upload({
|
|
|
|
|
|
Bucket: config.aws.bucketName,
|
|
|
|
|
|
Key: s3Key,
|
|
|
|
|
|
Body: buffer,
|
|
|
|
|
|
ContentType: mimeType,
|
|
|
|
|
|
ACL: 'private'
|
|
|
|
|
|
}).promise();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ File uploaded to S3: ${s3Key}`);
|
|
|
|
|
|
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
|
2025-11-19 16:55:54 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
2025-11-22 20:05:43 +05:30
|
|
|
|
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);
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
// 2) Content-Type check
|
|
|
|
|
|
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
|
2025-11-29 10:29:58 +05:30
|
|
|
|
if (!contentType?.includes("multipart/form-data"))
|
2025-11-22 20:05:43 +05:30
|
|
|
|
throw new ApiError(400, "Content-Type must be multipart/form-data");
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-29 10:29:58 +05:30
|
|
|
|
// 3) Body decoding (FIXED – same as addCompanyDetails)
|
|
|
|
|
|
const bodyBuffer = event.isBase64Encoded
|
|
|
|
|
|
? Buffer.from(event.body!, "base64")
|
|
|
|
|
|
: Buffer.from(event.body!, "binary");
|
2025-11-22 20:05:43 +05:30
|
|
|
|
|
|
|
|
|
|
const fields: any = {};
|
|
|
|
|
|
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
|
|
|
|
|
|
|
2025-11-29 10:29:58 +05:30
|
|
|
|
// 4) Parse multipart data (FIXED – using bb.write + bb.end exactly like working lambda)
|
2025-11-22 20:05:43 +05:30
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
|
|
const bb = Busboy({ headers: { 'content-type': contentType } });
|
|
|
|
|
|
|
|
|
|
|
|
bb.on('file', (fieldname, file, info) => {
|
|
|
|
|
|
const { filename, mimeType } = info;
|
2025-11-29 10:29:58 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
if (!filename) {
|
|
|
|
|
|
file.resume();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const chunks: Buffer[] = [];
|
2025-11-29 10:29:58 +05:30
|
|
|
|
let size = 0;
|
|
|
|
|
|
const MAX_SIZE = 5 * 1024 * 1024;
|
|
|
|
|
|
|
|
|
|
|
|
file.on("data", (chunk) => {
|
|
|
|
|
|
size += chunk.length;
|
|
|
|
|
|
if (size > MAX_SIZE) {
|
|
|
|
|
|
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
|
|
|
|
|
|
return;
|
2025-11-22 20:05:43 +05:30
|
|
|
|
}
|
|
|
|
|
|
chunks.push(chunk);
|
|
|
|
|
|
});
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-29 10:29:58 +05:30
|
|
|
|
file.on("end", () => {
|
2025-11-22 20:05:43 +05:30
|
|
|
|
if (chunks.length > 0) {
|
|
|
|
|
|
files.push({
|
|
|
|
|
|
buffer: Buffer.concat(chunks),
|
|
|
|
|
|
mimeType,
|
|
|
|
|
|
fileName: filename,
|
|
|
|
|
|
fieldName: fieldname,
|
2025-11-19 16:55:54 +05:30
|
|
|
|
});
|
2025-11-22 20:05:43 +05:30
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-29 10:29:58 +05:30
|
|
|
|
file.on("error", (err) =>
|
|
|
|
|
|
reject(new ApiError(400, `File upload error: ${err.message}`))
|
|
|
|
|
|
);
|
2025-11-22 20:05:43 +05:30
|
|
|
|
});
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-29 10:29:58 +05:30
|
|
|
|
bb.on("field", (fieldname, val) => {
|
|
|
|
|
|
if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null;
|
|
|
|
|
|
else {
|
2025-11-22 20:05:43 +05:30
|
|
|
|
try {
|
|
|
|
|
|
fields[fieldname] = JSON.parse(val);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
fields[fieldname] = val;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-29 10:29:58 +05:30
|
|
|
|
bb.on("close", () => resolve());
|
|
|
|
|
|
bb.on("error", (err) =>
|
|
|
|
|
|
reject(new ApiError(400, `Multipart parsing error: ${err.message}`))
|
|
|
|
|
|
);
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-29 10:29:58 +05:30
|
|
|
|
// IMPORTANT FIX for HTTP API
|
|
|
|
|
|
bb.write(bodyBuffer);
|
|
|
|
|
|
bb.end();
|
2025-11-22 20:05:43 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 4) Extract required fields - only activityXid, pqqQuestionXid, pqqAnswerXid are required
|
|
|
|
|
|
const activityXid = Number(fields.activityXid);
|
|
|
|
|
|
const pqqQuestionXid = Number(fields.pqqQuestionXid);
|
|
|
|
|
|
const pqqAnswerXid = Number(fields.pqqAnswerXid);
|
2025-11-29 10:29:58 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
// 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");
|
|
|
|
|
|
|
2025-11-24 19:19:02 +05:30
|
|
|
|
// console.log(`📝 Processing - Activity: ${activityXid}, Question: ${pqqQuestionXid}, Answer: ${pqqAnswerXid}`);
|
|
|
|
|
|
// console.log(`💬 Comments: ${comments ? 'Provided' : 'Not provided'}`);
|
|
|
|
|
|
// console.log(`📎 Files: ${files.length}`);
|
2025-11-22 20:05:43 +05:30
|
|
|
|
|
|
|
|
|
|
// 5) UPSERT: Check if header already exists for this combination
|
|
|
|
|
|
const existingHeader = await pqqService.findHeaderByCompositeKey(
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
pqqQuestionXid,
|
|
|
|
|
|
pqqAnswerXid
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let header;
|
|
|
|
|
|
if (existingHeader) {
|
|
|
|
|
|
console.log("🔄 Updating existing PQQ header");
|
|
|
|
|
|
// Update existing header (comments can be null)
|
|
|
|
|
|
header = await pqqService.updateHeader(
|
|
|
|
|
|
existingHeader.id,
|
|
|
|
|
|
comments
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("🆕 Creating new PQQ header");
|
|
|
|
|
|
// Create new header (comments can be null)
|
|
|
|
|
|
header = await pqqService.createHeader(
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
pqqQuestionXid,
|
|
|
|
|
|
pqqAnswerXid,
|
|
|
|
|
|
comments
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6) Get existing supporting files for this header
|
|
|
|
|
|
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
|
|
|
|
|
|
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
// 7) Handle file UPSERT - only if files are provided
|
|
|
|
|
|
const uploadedFiles: any[] = [];
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
if (files.length > 0) {
|
|
|
|
|
|
console.log("📤 Processing file uploads...");
|
2025-11-29 10:29:58 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
|
|
|
|
const file = files[i];
|
|
|
|
|
|
const existingFile = existingSupportingFiles[i] || null;
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
const url = await uploadToS3(
|
|
|
|
|
|
file.buffer,
|
|
|
|
|
|
file.mimeType,
|
|
|
|
|
|
file.fileName,
|
|
|
|
|
|
`ActivityOnboarding/supportings/${activityXid}`,
|
|
|
|
|
|
existingFile ? existingFile.mediaFileName : undefined
|
|
|
|
|
|
);
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
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}`);
|
2025-11-19 16:55:54 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
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`);
|
2025-11-29 10:29:58 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
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");
|
2025-11-29 10:29:58 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
// 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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-19 16:55:54 +05:30
|
|
|
|
}
|
2025-11-22 20:05:43 +05:30
|
|
|
|
|
|
|
|
|
|
// 9) Prepare response
|
|
|
|
|
|
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
|
2025-11-29 10:29:58 +05:30
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
return {
|
|
|
|
|
|
statusCode: 200,
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
"Access-Control-Allow-Origin": "*"
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: responseMessage,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
headerId: header.id,
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
pqqQuestionXid,
|
|
|
|
|
|
pqqAnswerXid,
|
|
|
|
|
|
comments: comments,
|
|
|
|
|
|
files: {
|
|
|
|
|
|
uploaded: uploadedFiles,
|
|
|
|
|
|
total: uploadedFiles.length
|
|
|
|
|
|
},
|
|
|
|
|
|
operation: existingHeader ? 'updated' : 'created',
|
2025-11-29 10:29:58 +05:30
|
|
|
|
fileOperation: files.length > 0 ?
|
|
|
|
|
|
(existingSupportingFiles.length > 0 ? 'replaced' : 'added') :
|
2025-11-22 20:05:43 +05:30
|
|
|
|
(existingSupportingFiles.length > 0 ? 'removed' : 'unchanged')
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error("❌ Error in submitPqqAnswer:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|