From 0d858f54119b065338a0b3cd7913eb93dd8a0e0e Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Sat, 22 Nov 2025 11:59:48 +0530 Subject: [PATCH] made getall document country city and replacing the files on s3 when updating the company details sending mail to am or admin --- package-lock.json | 24 +- package.json | 4 + serverless.yml | 26 ++ .../host/hostCompanyDetails.validation.ts | 2 +- src/config/config.ts | 6 + .../host/handlers/addCompanyDetails.ts | 257 +++++++++++++----- src/modules/host/services/host.service.ts | 44 +++ .../sendHostResubmitEmailToAM.service.ts | 78 ++++++ .../handlers/getAllDocTypeWithCountryState.ts | 39 +++ .../services/prepopulate.service.ts | 20 ++ 10 files changed, 432 insertions(+), 68 deletions(-) create mode 100644 src/modules/host/services/sendHostResubmitEmailToAM.service.ts create mode 100644 src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.ts diff --git a/package-lock.json b/package-lock.json index 6b371ae..bdd691d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@aws-crypto/crc32c": "^5.2.0", + "@aws-crypto/sha256-browser": "^5.2.0", "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-s3": "^3.928.0", "@aws-sdk/s3-request-presigner": "^3.310.0", + "@aws/lambda-invoke-store": "^0.2.1", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.0", @@ -44,6 +47,7 @@ "serverless": "4.17.0", "swagger-ui-express": "^5.0.0", "tslib": "^2.8.1", + "uuid": "^13.0.0", "yup": "^1.7.1", "zod": "^4.1.12" }, @@ -5928,6 +5932,14 @@ "node": ">= 10.0.0" } }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/axios": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", @@ -13364,11 +13376,15 @@ } }, "node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/package.json b/package.json index 521d923..5879671 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,12 @@ "seeder": "tsx prisma/seed.ts" }, "dependencies": { + "@aws-crypto/crc32c": "^5.2.0", + "@aws-crypto/sha256-browser": "^5.2.0", "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-s3": "^3.928.0", "@aws-sdk/s3-request-presigner": "^3.310.0", + "@aws/lambda-invoke-store": "^0.2.1", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.0", @@ -61,6 +64,7 @@ "serverless": "4.17.0", "swagger-ui-express": "^5.0.0", "tslib": "^2.8.1", + "uuid": "^13.0.0", "yup": "^1.7.1", "zod": "^4.1.12" }, diff --git a/serverless.yml b/serverless.yml index 95f9e7c..dfb53f6 100644 --- a/serverless.yml +++ b/serverless.yml @@ -40,6 +40,8 @@ provider: SALT_ROUNDS: ${env:SALT_ROUNDS} NODE_ENV: ${env:NODE_ENV} S3_BUCKET_NAME: ${env:S3_BUCKET_NAME} + MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME} + MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL} iam: role: @@ -408,6 +410,22 @@ functions: - httpApi: path: /prepopulate/get-all-bank-currency-details method: get + + + getAllDocumentCountryStateCityDetails: + handler: src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.handler + package: + patterns: + - 'src/modules/minglaradmin/**' + - 'common/**' + - 'src/common/**' + - 'node_modules/@prisma/client/**' + - 'node_modules/.prisma/**' + + events: + - httpApi: + path: /prepopulate/get-all-doc-country + method: get getAllPqqQuesAns: handler: src/modules/prepopulate/handlers/getAllPQQQuesWithAns.handler @@ -498,6 +516,14 @@ functions: - 'node_modules/@smithy/**' - 'node_modules/tslib/**' - 'node_modules/fast-xml-parser/**' + - 'node_modules/lambda-multipart-parser/**' + - 'node_modules/busboy/**' + - 'node_modules/@aws-crypto/**' + - 'node_modules/uuid/**' + - 'node_modules/@aws/util-uri-escape/**' + - 'node_modules/@aws/util-middleware/**' + - 'node_modules/@aws/smithy-client/**' + - 'node_modules/@aws/lambda-invoke-store/**' events: - httpApi: diff --git a/src/common/utils/validation/host/hostCompanyDetails.validation.ts b/src/common/utils/validation/host/hostCompanyDetails.validation.ts index 418835f..5bcebfc 100644 --- a/src/common/utils/validation/host/hostCompanyDetails.validation.ts +++ b/src/common/utils/validation/host/hostCompanyDetails.validation.ts @@ -55,7 +55,7 @@ export const hostCompanyDetailsSchema = z.object({ pinCode: z.string().min(4, "Pincode/Zipcode is required"), logoPath: z.string().optional(), isSubsidairy: z.boolean(), - registrationNumber: z.string().min(1, "Registration number is required"), + registrationNumber: z.string().optional(), panNumber: z.string().min(1, "PAN number is required"), gstNumber: z.string().optional(), formationDate: z.string().refine((val) => !isNaN(Date.parse(val)), { diff --git a/src/config/config.ts b/src/config/config.ts index 36abc1a..975aa21 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -59,6 +59,9 @@ const envVarsSchema = yup .nullable() .required('the from field in the emails sent by the app api key'), BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'), + // Minglar Admin + MINGLAR_ADMIN_EMAIL: yup.string().required('Minglar admin email address is required.'), + MINGLAR_ADMIN_NAME: yup.string().required('Minglar admin name is required.'), // //one signal // ONESIGNAL_APPID: yup.string().required('One signal app id is required'), // ONESIGNAL_REST_APIKEY: yup @@ -152,6 +155,9 @@ function getConfig() { api_key: envVars?.BREVO_EMAIL_API_KEY, BrevobaseURL: envVars?.BREVO_API_BASEURL, }, + //Minglar admin + MinglarAdminEmail: envVars.MINGLAR_ADMIN_EMAIL, + MinglarAdminName: envVars.MINGLAR_ADMIN_NAME, // oneSignal: { // appID: envVars.ONESIGNAL_APPID, // restApiKey: envVars.ONESIGNAL_REST_APIKEY, diff --git a/src/modules/host/handlers/addCompanyDetails.ts b/src/modules/host/handlers/addCompanyDetails.ts index 311c911..9901f42 100644 --- a/src/modules/host/handlers/addCompanyDetails.ts +++ b/src/modules/host/handlers/addCompanyDetails.ts @@ -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 = {}; const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = []; - // 3) parse with Busboy + // 3) parse with Busboy - FIXED VERSION 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; @@ -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; } -}); +}); \ 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 4b4d69a..3fb1693 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -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({ diff --git a/src/modules/host/services/sendHostResubmitEmailToAM.service.ts b/src/modules/host/services/sendHostResubmitEmailToAM.service.ts new file mode 100644 index 0000000..8565bb8 --- /dev/null +++ b/src/modules/host/services/sendHostResubmitEmailToAM.service.ts @@ -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 = ` +

Dear ${amName},

+

Host ${hostCompanyName} with reference number: ${hostRefNumber} has re-submited the application with implimented suggestions.

+

Please review their appliaction and take the necessary action.

+

Best regards,
Minglar Team

+ `; + + 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 = ` +

Dear ${minglarAdminName},

+

Host ${hostCompanyName} with reference number: ${hostRefNumber} has submited their application.

+

Please review their appliaction and take the necessary action.

+

Best regards,
Minglar Team

+ `; + + 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."); + } +} diff --git a/src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.ts b/src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.ts new file mode 100644 index 0000000..4cb64a4 --- /dev/null +++ b/src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.ts @@ -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 => { + // 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, + }), + }; +}); + diff --git a/src/modules/prepopulate/services/prepopulate.service.ts b/src/modules/prepopulate/services/prepopulate.service.ts index 4903490..4ec4431 100644 --- a/src/modules/prepopulate/services/prepopulate.service.ts +++ b/src/modules/prepopulate/services/prepopulate.service.ts @@ -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 }; + }