made getall document country city and replacing the files on s3 when updating the company details sending mail to am or admin

This commit is contained in:
2025-11-22 11:59:48 +05:30
parent 976c9fc686
commit 0d858f5411
10 changed files with 432 additions and 68 deletions

View File

@@ -15,6 +15,7 @@ import {
hostDocumentsSchema
} from '../../../common/utils/validation/host/hostCompanyDetails.validation';
import { HostService } from '../../host/services/host.service';
import { sendEmailToAM, sendEmailToMinglarAdmin } from '../services/sendHostResubmitEmailToAM.service';
const prisma = new PrismaService();
const hostService = new HostService(prisma);
@@ -50,16 +51,28 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
// 2) multipart check
const contentType = event.headers['content-type'] || event.headers['Content-Type'];
if (!contentType?.startsWith('multipart/form-data')) throw new ApiError(400, 'Content-Type must be multipart/form-data.');
if (!event.isBase64Encoded) throw new ApiError(400, 'Event body must be base64 encoded for multipart uploads.');
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');
}
const bodyBuffer = Buffer.from(event.body as string, 'base64');
const fields: Record<string, any> = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 3) parse with Busboy
// 3) parse with Busboy - FIXED VERSION
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;
@@ -70,36 +83,64 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
file.on('data', (chunk) => {
totalSize += chunk.length;
if (totalSize > MAX_SIZE) {
file.resume();
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit.`));
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
return;
}
chunks.push(chunk);
});
file.on('end', () => {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType: mimeType || 'application/octet-stream',
fileName: filename || 'unknown',
fieldName: fieldname,
});
}
});
file.on('error', (error) => {
reject(new ApiError(400, `File upload error: ${error.message}`));
});
});
bb.on('field', (fieldname, val) => {
// Keep raw string for JSON parse later; try parse for convenience
try {
fields[fieldname] = JSON.parse(val);
} catch {
fields[fieldname] = val;
}
// Store as string initially, parse later in normalizeJsonField
fields[fieldname] = val;
});
bb.on('close', resolve);
bb.on('error', reject);
bb.end(bodyBuffer);
bb.on('close', () => {
resolve();
});
bb.on('error', (error) => {
reject(new ApiError(400, `Multipart parsing error: ${error.message}`));
});
bb.write(bodyBuffer);
bb.end();
});
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 }),
},
});
}
}
}
// 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.');
@@ -108,6 +149,83 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const companyDetailsRaw = normalizeJsonField(fields, "companyDetails");
if (!companyDetailsRaw) throw new ApiError(400, "companyDetails is required.");
// Get existing host to determine host ID for folder structure
const existingHost = await prisma.hostHeader.findFirst({
where: { userXid: userInfo.id },
});
let hostId: number;
if (existingHost) {
hostId = existingHost.id;
} else {
// For new hosts, we'll use user ID temporarily and update after host creation
hostId = userInfo.id;
}
// Define uploadToS3 function with proper folder structure using fieldName for filenames
// Define uploadToS3 function with proper folder structure using fieldName for filenames
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, folderType: 'logo' | 'documents' | 'parent_company', documentTypeXid?: number, fieldName?: string) {
let s3Key: string;
// Sanitize file name: remove special characters and spaces
const sanitizeFileName = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9.]/g, '_') // Replace special characters with underscore
.replace(/_+/g, '_') // Replace multiple underscores with single
.replace(/^_+|_+$/g, ''); // Remove leading/trailing underscores
};
// Get file extension from original file name
const fileExtension = originalName.split('.').pop() || 'pdf';
// Determine folder structure based on type
if (folderType === 'logo') {
// Logo: Documents/Host/logo/{HostID}/{sanitized_filename}
const sanitizedFileName = sanitizeFileName(originalName);
s3Key = `Documents/Host/logo/${hostId}/${sanitizedFileName}`;
} else if (folderType === 'documents' && documentTypeXid && fieldName) {
// Host Documents: Documents/Host/documents/{HostID}/{documentTypeXid}_{fieldName}.{extension}
const fileName = `${documentTypeXid}_${fieldName}.${fileExtension}`;
const sanitizedFileName = sanitizeFileName(fileName);
s3Key = `Documents/Host/documents/${hostId}/${sanitizedFileName}`;
} else if (folderType === 'parent_company' && documentTypeXid && fieldName) {
// Parent Documents: Documents/Host/parent_company/{HostID}/{documentTypeXid}_{fieldName}.{extension}
const fileName = `${documentTypeXid}_${fieldName}.${fileExtension}`;
const sanitizedFileName = sanitizeFileName(fileName);
s3Key = `Documents/Host/parent_company/${hostId}/${sanitizedFileName}`;
} else {
throw new ApiError(400, 'Invalid folder type or missing documentTypeXid/fieldName');
}
// Upload new file (S3 will automatically replace if same key exists)
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 (includes optional parentCompany)
const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetailsRaw);
if (!companyValidation.success) {
@@ -129,21 +247,21 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}
const documentsMetadata = documentsMetadataRaw.map((d: any) => ({
...d,
owner: d.owner === 'parent' ? 'parent' : 'host', // default host
owner: d.owner || 'host', // default to host
}));
// 9) Map uploaded files to metadata (one entry per file - Q2 = A)
// 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}`);
return { ...doc, file };
});
// 10) Split host vs parent docs
// 9) Split host vs parent docs
const hostDocs = documentMetadata.filter((d) => d.owner === 'host');
const parentDocs = documentMetadata.filter((d) => d.owner === 'parent');
// 11) Ensure required docs for host exist (IDs 1,2,3,4)
// 10) Ensure required docs for host exist (IDs 1,2,3,4)
const hostUploadedTypes = hostDocs.map((d) => d.documentTypeXid);
const requiredHostTypes = Object.values(REQUIRED_DOC_TYPES);
const missingHostDocs = requiredHostTypes.filter((typeId) => !hostUploadedTypes.includes(typeId));
@@ -151,7 +269,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
throw new ApiError(400, `Missing mandatory documents for host: ${missingHostDocs.join(', ')}`);
}
// 12) If isSubsidairy === true and parentCompany provided -> validate parent company & docs
// 11) If isSubsidairy === true and parentCompany provided -> validate parent company & docs
let parsedParentCompany: any = null;
if (parsedCompany.isSubsidairy) {
if (!parsedCompany.parentCompany) {
@@ -163,16 +281,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const message = parentValidation.error.issues.map((i) => i.message).join(', ');
throw new ApiError(400, `Parent company validation failed: ${message}`);
}
let parentCompanyRaw = parsedCompany.parentCompany;
if (typeof parentCompanyRaw === "string") {
try {
parentCompanyRaw = JSON.parse(parentCompanyRaw);
} catch {
throw new ApiError(400, "Invalid JSON in parentCompany.");
}
}
parsedParentCompany = parentCompanyRaw;
parsedParentCompany = parsedCompany.parentCompany;
const parentUploadedTypes = parentDocs.map((d) => d.documentTypeXid);
const requiredParentTypes = Object.values(REQUIRED_DOC_TYPES);
@@ -182,49 +291,47 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}
}
// 13) Upload files to S3 (host docs under Documents/Host/, parent docs under Documents/Host/parent_company/)
// 12) Upload files to S3 with proper folder structure using fieldName for filenames
const uploadedHostDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = [];
const uploadedParentDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = [];
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string) {
const uniqueKey = `${userInfo.id}_${crypto.randomUUID()}_${originalName}`;
const s3Key = `${prefix}/${uniqueKey}`;
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 files
// Upload host documents with proper folder structure using fieldName
for (const doc of hostDocs) {
const filePath = await uploadToS3(doc.file.buffer, doc.file.mimeType, doc.file.fileName, 'Documents/Host');
const filePath = await uploadToS3(
doc.file.buffer,
doc.file.mimeType,
doc.file.fileName, // Use original file name for extension
'documents',
doc.documentTypeXid,
doc.fieldName // Use fieldName for the filename
);
uploadedHostDocs.push({
documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName,
documentName: doc.documentName, // Keep documentName for database
filePath,
});
}
// upload parent files (if any)
// Upload parent company documents with proper folder structure using fieldName
if (parentDocs.length > 0) {
for (const doc of parentDocs) {
const filePath = await uploadToS3(doc.file.buffer, doc.file.mimeType, doc.file.fileName, 'Documents/Host/parent_company');
const filePath = await uploadToS3(
doc.file.buffer,
doc.file.mimeType,
doc.file.fileName, // Use original file name for extension
'parent_company',
doc.documentTypeXid,
doc.fieldName // Use fieldName for the filename
);
uploadedParentDocs.push({
documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName,
documentName: doc.documentName, // Keep documentName for database
filePath,
});
}
}
// 14) Persist using hostService
// 13) Persist using hostService
const createdOrUpdated = await hostService.addOrUpdateCompanyDetails(
userInfo.id,
parsedCompany,
@@ -235,7 +342,31 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');
// 15) Success
// Update hostId if it was a new creation
if (!existingHost) {
hostId = createdOrUpdated.id;
console.log(`Host created with ID: ${hostId}`);
}
const getSuggestionDetails = await hostService.getSuggestionDetails(userInfo.id)
if (getSuggestionDetails && getSuggestionDetails.hostDetails.accountManagerXid !== null) {
await sendEmailToAM(
getSuggestionDetails.hostDetails.accountManager.emailAddress,
getSuggestionDetails.hostDetails.accountManager.firstName,
getSuggestionDetails.hostDetails.companyName,
getSuggestionDetails.hostDetails.hostRefNumber
);
} else {
await sendEmailToMinglarAdmin(
config.MinglarAdminEmail,
config.MinglarAdminName,
getSuggestionDetails.hostDetails.companyName,
getSuggestionDetails.hostDetails.hostRefNumber
)
}
// 14) Success
return {
statusCode: 200,
headers: {
@@ -252,4 +383,4 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
console.error('❌ Error in addCompanyDetails:', error);
throw error;
}
});
});

View File

@@ -480,6 +480,50 @@ export class HostService {
});
}
async getSuggestionDetails(user_xid: number) {
const hostDetails = await this.prisma.hostHeader.findFirst({
where: { userXid: user_xid, isActive: true },
include: {
user: {
select: {
id: true,
emailAddress: true,
firstName: true,
}
},
accountManager: {
select: {
id: true,
emailAddress: true,
firstName: true,
}
}
}
});
if (!hostDetails) {
return { hostSuggestionDetails: [], hostDetails: null };
}
const hostSuggestionDetails = await this.prisma.hostSuggestion.findMany({
where: { hostXid: hostDetails.id, isActive: true, isreviewed: false }
});
if (hostSuggestionDetails) {
await this.prisma.hostSuggestion.updateMany({
where: { hostXid: hostDetails.id, isActive: true, isreviewed: false },
data: {
isreviewed: true,
reviewedByXid: hostDetails.id,
reviewOn: new Date(),
}
})
}
return { hostSuggestionDetails, hostDetails };
}
async generateHostRefNumber(tx: any) {
const lastHost = await tx.hostHeader.findFirst({

View File

@@ -0,0 +1,78 @@
import { brevoService } from "@/common/email/brevoApi";
import ApiError from "@/common/utils/helper/ApiError";
export async function sendEmailToAM(
emailAddress: string,
amName: string,
hostCompanyName: string,
hostRefNumber: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Host Application Re-Submited";
const htmlContent = `
<p>Dear ${amName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has re-submited the application with implimented suggestions.</p>
<p>Please review their appliaction and take the necessary action.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}
export async function sendEmailToMinglarAdmin(
emailAddress: string,
minglarAdminName: string,
hostCompanyName: string,
hostRefNumber: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "New Host Application Recieved";
const htmlContent = `
<p>Dear ${minglarAdminName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has submited their application.</p>
<p>Please review their appliaction and take the necessary action.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}

View File

@@ -0,0 +1,39 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } 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 { PrePopulateService } from '../services/prepopulate.service';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
const prismaService = new PrismaService();
const prePopulateService = new PrePopulateService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Extract token from headers
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.');
}
// Authenticate user using the shared authForHost function
await verifyHostToken(token);
const result = await prePopulateService.getAllDocumentTypeWithCountryStateCity();
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data: result,
}),
};
});

View File

@@ -59,6 +59,26 @@ export class PrePopulateService {
});
}
async getAllDocumentTypeWithCountryStateCity() {
const [documentDetails, countryDetails, stateDetails, cityDetails] =
await this.prisma.$transaction([
this.prisma.documentType.findMany({
where: { isActive: true, isVisible: true },
orderBy: { displayOrder: 'asc' }
}),
this.prisma.countries.findMany({
where: { isActive: true },
}),
this.prisma.states.findMany({
where: { isActive: true }
}),
this.prisma.cities.findMany({
where: { isActive: true }
})
]);
return { documentDetails, countryDetails, stateDetails, cityDetails };
}