From 80bd926e16c433b26570c6d6e0fbbbb2549551a8 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Fri, 28 Nov 2025 15:27:28 +0530 Subject: [PATCH] fixed the submit company details api --- .../handlers/Host_Admin/onboarding/signUp.ts | 2 +- .../onboarding/submitCompanyDetails.ts | 341 ++++++++---------- src/modules/host/services/host.service.ts | 12 +- .../minglaradmin/handlers/registration.ts | 2 +- 4 files changed, 162 insertions(+), 195 deletions(-) diff --git a/src/modules/host/handlers/Host_Admin/onboarding/signUp.ts b/src/modules/host/handlers/Host_Admin/onboarding/signUp.ts index 7bd2d08..178a893 100644 --- a/src/modules/host/handlers/Host_Admin/onboarding/signUp.ts +++ b/src/modules/host/handlers/Host_Admin/onboarding/signUp.ts @@ -85,7 +85,7 @@ export const handler = safeHandler(async ( } // Send OTP email outside the DB transaction - await sendOtpEmailForHost(transactionResult.newUser.emailAddress, transactionResult.otp); + // await sendOtpEmailForHost(transactionResult.newUser.emailAddress, transactionResult.otp); return { statusCode: 200, diff --git a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts index a64f791..7f22123 100644 --- a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts +++ b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts @@ -11,11 +11,9 @@ import { hostCompanyDetailsSchema, hostDocumentsSchema, parentCompanySchema, - // REQUIRED_DOC_TYPES } from '../../../../../common/utils/validation/host/hostCompanyDetails.validation'; import { HostService } from '../../../services/host.service'; import { sendEmailToAM, sendEmailToMinglarAdmin } from '../../../services/sendHostResubmitEmailToAM.service'; -import { HOST_STATUS_INTERNAL } from '@/common/utils/constants/host.constant'; const prisma = new PrismaService(); const hostService = new HostService(prisma); @@ -26,7 +24,6 @@ const s3 = new AWS.S3({ function normalizeJsonField(fields: any, key: string) { if (!fields[key]) return undefined; - const val = fields[key]; if (typeof val === "object") return val; @@ -38,47 +35,51 @@ function normalizeJsonField(fields: any, key: string) { throw new ApiError(400, `Invalid JSON in field: ${key}`); } } - throw new ApiError(400, `Invalid input: ${key} must be object or JSON string.`); } +function cleanEmptyStrings(obj: any) { + if (!obj || typeof obj !== "object") return obj; + + const cleaned: any = {}; + for (const key of Object.keys(obj)) { + if (obj[key] === "") cleaned[key] = undefined; + else if (typeof obj[key] === "object") cleaned[key] = cleanEmptyStrings(obj[key]); + else cleaned[key] = obj[key]; + } + return cleaned; +} + export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise => { try { - // 1) Auth + + /** 1) AUTH */ const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']; if (!token) throw new ApiError(400, 'This is a protected route. Please provide a valid token.'); const userInfo = await verifyHostToken(token); - // 2) multipart check + /** 2) CHECK CONTENT TYPE */ const contentType = event.headers['content-type'] || event.headers['Content-Type']; if (!contentType?.includes('multipart/form-data')) { throw new ApiError(400, 'Content-Type must be multipart/form-data.'); } - // Handle both base64 and non-base64 encoded bodies - let bodyBuffer: Buffer; - if (event.isBase64Encoded) { - bodyBuffer = Buffer.from(event.body as string, 'base64'); - } else { - bodyBuffer = Buffer.from(event.body as string, 'binary'); - } + /** 3) HANDLE BODY */ + const bodyBuffer = event.isBase64Encoded + ? Buffer.from(event.body as string, 'base64') + : Buffer.from(event.body as string, 'binary'); const fields: Record = {}; const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = []; - // 3) parse with Busboy await new Promise((resolve, reject) => { - const bb = Busboy({ - headers: { - 'content-type': contentType - } - }); + const bb = Busboy({ headers: { 'content-type': contentType } }); bb.on('file', (fieldname, file, info) => { const { filename, mimeType } = info; const chunks: Buffer[] = []; let totalSize = 0; - const MAX_SIZE = 5 * 1024 * 1024; // 5 MB + const MAX_SIZE = 5 * 1024 * 1024; file.on('data', (chunk) => { totalSize += chunk.length; @@ -100,143 +101,72 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } }); - file.on('error', (error) => { - reject(new ApiError(400, `File upload error: ${error.message}`)); - }); + file.on('error', (error) => reject(new ApiError(400, `File upload error: ${error.message}`))); }); - bb.on('field', (fieldname, val) => { - fields[fieldname] = val; - }); - - bb.on('close', () => { - resolve(); - }); - - bb.on('error', (error) => { - reject(new ApiError(400, `Multipart parsing error: ${error.message}`)); - }); + bb.on('field', (fieldname, val) => fields[fieldname] = val); + bb.on('close', () => resolve()); + bb.on('error', (error) => reject(new ApiError(400, `Multipart parsing error: ${error.message}`))); bb.write(bodyBuffer); bb.end(); }); - // Extract isDraft flag from fields (default to false if not provided) - const isDraft = fields.isDraft === 'true' || fields.isDraft === true; + /** 4) Extract and clean isDraft flag */ + const isDraft = fields.isDraft === "true" || fields.isDraft === true; + /** 5) PROCESS companyDetails ONCE ONLY (IMPORTANT FIX) */ + let companyDetailsRaw = normalizeJsonField(fields, "companyDetails"); + if (!companyDetailsRaw) throw new ApiError(400, "companyDetails is required."); + + if (isDraft) { + companyDetailsRaw = cleanEmptyStrings(companyDetailsRaw); + + // IMPORTANT: also clean parent company nested fields + if (companyDetailsRaw.parentCompany) { + companyDetailsRaw.parentCompany = cleanEmptyStrings(companyDetailsRaw.parentCompany); + } + } + + + + /** 6) Profile update if provided */ if (fields.userProfile) { const userProfileRaw = normalizeJsonField(fields, "userProfile"); if (userProfileRaw) { const { firstName, lastName, mobileNumber } = userProfileRaw; - // Update user profile if provided - if (firstName || lastName || mobileNumber) { - await prisma.user.update({ - where: { id: userInfo.id }, - data: { - ...(firstName && { firstName }), - ...(lastName && { lastName }), - ...(mobileNumber && { mobileNumber }), - }, - }); - } + await prisma.user.update({ + where: { id: userInfo.id }, + data: { + ...(firstName && { firstName }), + ...(lastName && { lastName }), + ...(mobileNumber && { mobileNumber }), + }, + }); } } - // 4) Validate required root fields - if (!fields.companyDetails) throw new ApiError(400, 'Missing companyDetails field.'); - if (!fields.documents) throw new ApiError(400, 'Missing documents field.'); - - // 5) Parse companyDetails - const companyDetailsRaw = normalizeJsonField(fields, "companyDetails"); - if (!companyDetailsRaw) throw new ApiError(400, "companyDetails is required."); - // --- FIXED HOST ID INITIALIZATION --- - let hostId: number; - - // Check if host already exists - let existingHost = await prisma.hostHeader.findFirst({ - where: { userXid: userInfo.id }, - }); - - // Define uploadToS3 function (same as before) - async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, folderType: 'logo' | 'documents' | 'parent_company', documentTypeXid?: number, fieldName?: string) { - let s3Key: string; - - const sanitizeFileName = (name: string) => { - return name - .toLowerCase() - .replace(/[^a-z0-9.]/g, '_') - .replace(/_+/g, '_') - .replace(/^_+|_+$/g, ''); - }; - - const fileExtension = originalName.split('.').pop() || 'pdf'; - - if (folderType === 'logo') { - const sanitizedFileName = sanitizeFileName(originalName); - s3Key = `Documents/Host/${userInfo.id}/logo/${sanitizedFileName}`; - } else if (folderType === 'documents' && documentTypeXid && fieldName) { - const fileName = `${documentTypeXid}_${fieldName}.${fileExtension}`; - const sanitizedFileName = sanitizeFileName(fileName); - s3Key = `Documents/Host/${userInfo.id}/documents/${sanitizedFileName}`; - } else if (folderType === 'parent_company' && documentTypeXid && fieldName) { - const fileName = `${documentTypeXid}_${fieldName}.${fileExtension}`; - const sanitizedFileName = sanitizeFileName(fileName); - s3Key = `Documents/Host/${userInfo.id}/parent_company/${sanitizedFileName}`; - } else { - throw new ApiError(400, 'Invalid folder type or missing documentTypeXid/fieldName'); - } - - await s3 - .upload({ - Bucket: config.aws.bucketName, - Key: s3Key, - Body: buffer, - ContentType: mimeType, - ACL: 'private', - }) - .promise(); - - console.log(`File uploaded successfully: ${s3Key}`); - return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`; - } - - // 5.5) Handle company logo upload - const logoFile = files.find((f) => f.fieldName === 'companyLogo'); - if (logoFile) { - const logoPath = await uploadToS3( - logoFile.buffer, - logoFile.mimeType, - logoFile.fileName, - 'logo' - ); - companyDetailsRaw.logoPath = logoPath; - console.log('Company logo uploaded:', logoPath); - } - - // 6) Zod validation for companyDetails + /** 7) VALIDATION - SKIPPED IF DRAFT */ let parsedCompany: any = companyDetailsRaw; if (!isDraft) { - const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetailsRaw); - if (!companyValidation.success) { - const message = companyValidation.error.issues.map((i) => i.message).join(', '); + const validate = hostCompanyDetailsSchema.safeParse(companyDetailsRaw); + if (!validate.success) { + const message = validate.error.issues.map(i => i.message).join(', '); throw new ApiError(400, `Validation failed: ${message}`); } - parsedCompany = companyValidation.data; + parsedCompany = validate.data; } - - // 7) Parse documents metadata + /** 8) DOCUMENT METADATA */ const documentsMetadataRaw = normalizeJsonField(fields, "documents"); if (!Array.isArray(documentsMetadataRaw)) throw new ApiError(400, "documents must be an array."); - if (!documentsMetadataRaw.length) throw new ApiError(400, 'Documents must be a non-empty array.'); - // Validate documents metadata shape if (!isDraft) { const docsParse = hostDocumentsSchema.safeParse(documentsMetadataRaw); if (!docsParse.success) { - const message = docsParse.error.issues.map((i) => i.message).join(', '); + const message = docsParse.error.issues.map(i => i.message).join(', '); throw new ApiError(400, `Documents validation failed: ${message}`); } } @@ -246,42 +176,79 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< owner: d.owner || 'host', })); - // 8) Map uploaded files to metadata const documentMetadata = documentsMetadata.map((doc: any) => { - const file = files.find((f) => f.fieldName === doc.fieldName); - if (!file) throw new ApiError(400, `File not found for field: ${doc.fieldName}`); + const file = files.find(f => f.fieldName === doc.fieldName); + + // In DRAFT mode → allow missing documents + if (isDraft && !file) { + return { ...doc, file: null }; + } + + // In FINAL mode → file must exist + if (!file) { + throw new ApiError(400, `File not found for field: ${doc.fieldName}`); + } + return { ...doc, file }; }); - // 9) Split host vs parent docs - const hostDocs = documentMetadata.filter((d) => d.owner === 'host'); - const parentDocs = documentMetadata.filter((d) => d.owner === 'parent'); - // 11) If isSubsidairy === true and parentCompany provided -> validate parent company & docs + /** 9) SPLIT host & parent docs */ + const hostDocs = documentMetadata.filter(d => d.owner === 'host'); + const parentDocs = documentMetadata.filter(d => d.owner === 'parent'); + + /** 10) VALIDATE PARENT COMPANY (ONLY IN FINAL SUBMISSION) */ let parsedParentCompany: any = null; + if (!isDraft && parsedCompany.isSubsidairy) { if (!parsedCompany.parentCompany) { - throw new ApiError(400, 'isSubsidairy is true but parentCompany object is missing inside companyDetails.'); + throw new ApiError(400, 'isSubsidairy is true but parentCompany object is missing.'); } - const parentValidation = parentCompanySchema.safeParse(parsedCompany.parentCompany); - if (!parentValidation.success) { - const message = parentValidation.error.issues.map((i) => i.message).join(', '); + const parentCheck = parentCompanySchema.safeParse(parsedCompany.parentCompany); + if (!parentCheck.success) { + const message = parentCheck.error.issues.map(i => i.message).join(', '); throw new ApiError(400, `Parent company validation failed: ${message}`); } - parsedParentCompany = parsedCompany.parentCompany; + + parsedParentCompany = parentCheck.data; } else { parsedParentCompany = parsedCompany.parentCompany || null; } + /** 11) UPLOAD DOCUMENTS */ + async function uploadToS3(buffer, mimeType, originalName, folderType, documentTypeXid?, fieldName?) { + const sanitizeFileName = (name: string) => + name.toLowerCase().replace(/[^a-z0-9.]/g, '_').replace(/_+/g, '_'); - // 12) Upload files to S3 (same for both draft and final submission) - const uploadedHostDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = []; - const uploadedParentDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = []; + const ext = originalName.split('.').pop() || 'pdf'; - // Upload host documents + let s3Key = ""; + if (folderType === 'logo') { + s3Key = `Documents/Host/${userInfo.id}/logo/${sanitizeFileName(originalName)}`; + } else if (folderType === 'documents') { + s3Key = `Documents/Host/${userInfo.id}/documents/${sanitizeFileName(`${documentTypeXid}_${fieldName}.${ext}`)}`; + } else if (folderType === 'parent_company') { + s3Key = `Documents/Host/${userInfo.id}/parent_company/${sanitizeFileName(`${documentTypeXid}_${fieldName}.${ext}`)}`; + } + + await s3.upload({ + Bucket: config.aws.bucketName, + Key: s3Key, + Body: buffer, + ContentType: mimeType, + ACL: 'private' + }).promise(); + + return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`; + } + + /** Upload host docs */ + const uploadedHostDocs = []; for (const doc of hostDocs) { - const filePath = await uploadToS3( + if (isDraft && !doc.file) continue; + + const path = await uploadToS3( doc.file.buffer, doc.file.mimeType, doc.file.fileName, @@ -289,72 +256,71 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< doc.documentTypeXid, doc.fieldName ); + uploadedHostDocs.push({ documentTypeXid: doc.documentTypeXid, documentName: doc.fieldName, - filePath, + filePath: path, }); } - // Upload parent company documents - if (parentDocs.length > 0) { - for (const doc of parentDocs) { - const filePath = await uploadToS3( - doc.file.buffer, - doc.file.mimeType, - doc.file.fileName, - 'parent_company', - doc.documentTypeXid, - doc.fieldName - ); - uploadedParentDocs.push({ - documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, - filePath, - }); - } + + /** Upload parent docs */ + const uploadedParentDocs = []; + for (const doc of parentDocs) { + if (!doc.file && isDraft) continue; // skip missing files in draft mode + + const path = await uploadToS3( + doc.file.buffer, + doc.file.mimeType, + doc.file.fileName, + 'parent_company', + doc.documentTypeXid, + doc.fieldName + ); + + uploadedParentDocs.push({ + documentTypeXid: doc.documentTypeXid, + documentName: doc.documentName, + filePath: path, + }); } - // 13) Persist using hostService - PASS isDraft flag + + /** 12) SAVE / UPDATE HOST ENTRY */ const createdOrUpdated = await hostService.addOrUpdateCompanyDetails( userInfo.id, parsedCompany, uploadedHostDocs, parsedParentCompany, uploadedParentDocs, - isDraft // Pass the isDraft flag + isDraft ); if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.'); - // Update hostId if it was a new creation - if (!existingHost) { - hostId = createdOrUpdated.id; - console.log(`Host created with ID: ${hostId}`); - } - - // 14) Send emails only for FINAL submission (not draft) + /** 13) SEND EMAIL ONLY IN FINAL SUBMISSION */ if (!isDraft) { - const getSuggestionDetails = await hostService.getSuggestionDetails(userInfo.id) + const details = await hostService.getSuggestionDetails(userInfo.id); - if (getSuggestionDetails.hostDetails.accountManagerXid !== null) { + if (details.hostDetails.accountManagerXid) { await sendEmailToAM( - getSuggestionDetails.hostDetails.accountManager.emailAddress, - getSuggestionDetails.hostDetails.accountManager.firstName, - getSuggestionDetails.hostDetails.companyName, - getSuggestionDetails.hostDetails.hostRefNumber + details.hostDetails.accountManager.emailAddress, + details.hostDetails.accountManager.firstName, + details.hostDetails.companyName, + details.hostDetails.hostRefNumber ); } else { await sendEmailToMinglarAdmin( config.MinglarAdminEmail, config.MinglarAdminName, - getSuggestionDetails.hostDetails.companyName, - getSuggestionDetails.hostDetails.hostRefNumber - ) + details.hostDetails.companyName, + details.hostDetails.hostRefNumber + ); } } - // 15) Success response + /** RESPONSE */ return { statusCode: 200, headers: { @@ -363,18 +329,17 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< }, body: JSON.stringify({ success: true, - message: isDraft - ? 'Company details saved as draft successfully.' - : 'Company (and parent if provided) details and documents uploaded successfully.', + message: isDraft ? "Company details saved as draft successfully." : "Company details uploaded successfully.", data: { id: createdOrUpdated.id, - hostRefNumber: (createdOrUpdated as any).hostRefNumber, + hostRefNumber: createdOrUpdated.hostRefNumber, isDraft } }), }; + } catch (error: any) { - console.error('❌ Error in addCompanyDetails:', error); + console.error("❌ Error in addCompanyDetails:", 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 0d67dbf..45d5469 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -388,7 +388,8 @@ export class HostService { registrationNumber: companyData.registrationNumber, panNumber: companyData.panNumber, gstNumber: companyData.gstNumber || null, - formationDate: new Date(companyData.formationDate), + // formationDate: new Date(companyData.formationDate), + formationDate: companyData.formationDate ? new Date(companyData.formationDate) : null, companyType: companyData.companyType, websiteUrl: companyData.websiteUrl || null, instagramUrl: companyData.instagramUrl || null, @@ -431,7 +432,8 @@ export class HostService { registrationNumber: parentCompanyData.registrationNumber, panNumber: parentCompanyData.panNumber, gstNumber: parentCompanyData.gstNumber || null, - formationDate: new Date(parentCompanyData.formationDate), + // formationDate: new Date(parentCompanyData.formationDate), + formationDate: parentCompanyData.formationDate ? new Date(parentCompanyData.formationDate) : null, companyType: parentCompanyData.companyType, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, @@ -471,7 +473,7 @@ export class HostService { registrationNumber: companyData.registrationNumber, panNumber: companyData.panNumber, gstNumber: companyData.gstNumber || null, - formationDate: new Date(companyData.formationDate), + formationDate: companyData.formationDate ? new Date(companyData.formationDate) : null, companyType: companyData.companyType, websiteUrl: companyData.websiteUrl || null, instagramUrl: companyData.instagramUrl || null, @@ -520,7 +522,7 @@ export class HostService { registrationNumber: parentCompanyData.registrationNumber, panNumber: parentCompanyData.panNumber, gstNumber: parentCompanyData.gstNumber || null, - formationDate: new Date(parentCompanyData.formationDate), + formationDate: parentCompanyData.formationDate ? new Date(parentCompanyData.formationDate) : null, companyType: parentCompanyData.companyType, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, @@ -555,7 +557,7 @@ export class HostService { registrationNumber: parentCompanyData.registrationNumber, panNumber: parentCompanyData.panNumber, gstNumber: parentCompanyData.gstNumber || null, - formationDate: new Date(parentCompanyData.formationDate), + formationDate: parentCompanyData.formationDate ? new Date(parentCompanyData.formationDate) : null, companyType: parentCompanyData.companyType, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, diff --git a/src/modules/minglaradmin/handlers/registration.ts b/src/modules/minglaradmin/handlers/registration.ts index 4b73e1a..618cb42 100644 --- a/src/modules/minglaradmin/handlers/registration.ts +++ b/src/modules/minglaradmin/handlers/registration.ts @@ -68,7 +68,7 @@ export const handler = safeHandler(async ( throw new ApiError(500, 'Failed to send OTP'); } - await sendOtpEmailForMinglarAdmin(newUser?.emailAddress, otpResult.otp); + // await sendOtpEmailForMinglarAdmin(newUser?.emailAddress, otpResult.otp); return { statusCode: 200,