fixed the submit company details api

This commit is contained in:
2025-11-28 15:27:28 +05:30
parent 15c1458f02
commit 80bd926e16
4 changed files with 162 additions and 195 deletions

View File

@@ -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,

View File

@@ -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<APIGatewayProxyResult> => {
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<string, any> = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 3) parse with Busboy
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) => {
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;
}
});
});

View File

@@ -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,

View File

@@ -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,