diff --git a/serverless.yml b/serverless.yml index dfb53f6..b37fc6d 100644 --- a/serverless.yml +++ b/serverless.yml @@ -287,6 +287,7 @@ functions: updateMinglarProfile: handler: src/modules/minglaradmin/handlers/updateProfile.handler + timeout: 30 package: patterns: - 'src/modules/host/handlers/updateProfile.*' @@ -299,7 +300,6 @@ functions: - 'node_modules/@smithy/**' - 'node_modules/tslib/**' - 'node_modules/fast-xml-parser/**' - events: - httpApi: @@ -410,8 +410,7 @@ functions: - httpApi: path: /prepopulate/get-all-bank-currency-details method: get - - + getAllDocumentCountryStateCityDetails: handler: src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.handler package: @@ -487,6 +486,21 @@ functions: path: /minglaradmin/accept-host-application method: patch + acceptHostApplicationMinglar: + handler: src/modules/minglaradmin/handlers/acceptHostAppMinglar.handler + package: + patterns: + - 'src/modules/minglaradmin/**' + - 'common/**' + - 'src/common/**' + - 'node_modules/@prisma/client/**' + - 'node_modules/.prisma/**' + + events: + - httpApi: + path: /minglaradmin/accept-host-application-minglar + method: patch + rejectHostApplication: handler: src/modules/minglaradmin/handlers/rejectHostApplication.handler package: @@ -502,6 +516,21 @@ functions: path: /minglaradmin/reject-host-application method: patch + rejectHostApplicationAM: + handler: src/modules/minglaradmin/handlers/rejectHostApplicationAM.handler + package: + patterns: + - 'src/modules/minglaradmin/**' + - 'common/**' + - 'src/common/**' + - 'node_modules/@prisma/client/**' + - 'node_modules/.prisma/**' + + events: + - httpApi: + path: /minglaradmin/reject-host-application-am + method: patch + addCompanyDetails: handler: src/modules/host/handlers/addCompanyDetails.handler package: diff --git a/src/modules/host/handlers/addCompanyDetails.ts b/src/modules/host/handlers/addCompanyDetails.ts index b25ea1e..1999f67 100644 --- a/src/modules/host/handlers/addCompanyDetails.ts +++ b/src/modules/host/handlers/addCompanyDetails.ts @@ -3,16 +3,15 @@ import config from '@/config/config'; import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import AWS from 'aws-sdk'; import Busboy from 'busboy'; -import crypto from 'crypto'; 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 { hostCompanyDetailsSchema, + hostDocumentsSchema, parentCompanySchema, - REQUIRED_DOC_TYPES, - hostDocumentsSchema + REQUIRED_DOC_TYPES } from '../../../common/utils/validation/host/hostCompanyDetails.validation'; import { HostService } from '../../host/services/host.service'; import { sendEmailToAM, sendEmailToMinglarAdmin } from '../services/sendHostResubmitEmailToAM.service'; @@ -350,7 +349,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< const getSuggestionDetails = await hostService.getSuggestionDetails(userInfo.id) - if (getSuggestionDetails && getSuggestionDetails.hostDetails.accountManagerXid !== null) { + if (getSuggestionDetails.hostDetails.accountManagerXid !== null) { await sendEmailToAM( getSuggestionDetails.hostDetails.accountManager.emailAddress, getSuggestionDetails.hostDetails.accountManager.firstName, diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 3fb1693..3456ccd 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -228,7 +228,9 @@ export class HostService { await this.prisma.hostHeader.update({ where: { id: hostDetails.id }, data: { - stepper: STEPPER.AGREEMENT_ACCEPTED + stepper: STEPPER.AGREEMENT_ACCEPTED, + agreementAccepted: true, + isApproved: true } }) } diff --git a/src/modules/host/services/sendHostResubmitEmailToAM.service.ts b/src/modules/host/services/sendHostResubmitEmailToAM.service.ts index 8565bb8..87c1bd9 100644 --- a/src/modules/host/services/sendHostResubmitEmailToAM.service.ts +++ b/src/modules/host/services/sendHostResubmitEmailToAM.service.ts @@ -11,7 +11,7 @@ export async function sendEmailToAM( // messageId: string }> { - const subject = "Host Application Re-Submited"; + const subject = `Host Application Re-Submited : ${hostCompanyName}`; const htmlContent = `

Dear ${amName},

@@ -49,7 +49,7 @@ export async function sendEmailToMinglarAdmin( // messageId: string }> { - const subject = "New Host Application Recieved"; + const subject = `New Host Application Recieved : ${hostCompanyName}`; const htmlContent = `

Dear ${minglarAdminName},

diff --git a/src/modules/minglaradmin/handlers/acceptHostAppMinglar.ts b/src/modules/minglaradmin/handlers/acceptHostAppMinglar.ts new file mode 100644 index 0000000..00b6fd4 --- /dev/null +++ b/src/modules/minglaradmin/handlers/acceptHostAppMinglar.ts @@ -0,0 +1,65 @@ +import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin'; +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 { sendEmailToHostForMinglarApproval } from '../services/approvalMailtoHost.service'; +import { MinglarService } from '../services/minglar.service'; + +const prismaService = new PrismaService(); +const minglarService = new MinglarService(prismaService); + +interface AddSuggestionBody { + hostXid: number; + title: string; + comments: string; +} + +/** + * Add suggestion handler for host applications + * Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions + * Types: Setup Profile, Review Account, Add Payment Details, Agreement + */ +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Verify authentication token + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) { + throw new ApiError(401, 'This is a protected route. Please provide a valid token.'); + } + + // Verify token and get user info + const userInfo = await verifyOnlyMinglarAdminToken(token); + + // Parse request body + let body: AddSuggestionBody; + + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (error) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + const { hostXid } = body; + + + // Add suggestion using service + await minglarService.acceptHostApplicationMinglarAdmin(hostXid, userInfo.id); + const hostDetails = await minglarService.getUserDetails(userInfo.id) + await sendEmailToHostForMinglarApproval(hostDetails.emailAddress) + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Application accepted successfully', + data: null, + }), + }; +}); diff --git a/src/modules/minglaradmin/handlers/acceptHostApplication.ts b/src/modules/minglaradmin/handlers/acceptHostApplication.ts index a903f16..a71cc7d 100644 --- a/src/modules/minglaradmin/handlers/acceptHostApplication.ts +++ b/src/modules/minglaradmin/handlers/acceptHostApplication.ts @@ -4,6 +4,7 @@ import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authFor import { safeHandler } from '../../../common/utils/handlers/safeHandler'; import ApiError from '../../../common/utils/helper/ApiError'; import { MinglarService } from '../services/minglar.service'; +import { sendEmailToHostForApprovedApplication } from '../services/approvalMailtoHost.service' const prismaService = new PrismaService(); const minglarService = new MinglarService(prismaService); @@ -46,6 +47,8 @@ export const handler = safeHandler(async ( // Add suggestion using service await minglarService.acceptHostApplication(hostXid, userInfo.id); + const hostDetails = await minglarService.getUserDetails(userInfo.id) + await sendEmailToHostForApprovedApplication(hostDetails.emailAddress) return { statusCode: 200, diff --git a/src/modules/minglaradmin/handlers/rejectHostApplication.ts b/src/modules/minglaradmin/handlers/rejectHostApplication.ts index 075a62e..780a0b6 100644 --- a/src/modules/minglaradmin/handlers/rejectHostApplication.ts +++ b/src/modules/minglaradmin/handlers/rejectHostApplication.ts @@ -4,6 +4,7 @@ import { PrismaService } from '../../../common/database/prisma.service'; import { safeHandler } from '../../../common/utils/handlers/safeHandler'; import ApiError from '../../../common/utils/helper/ApiError'; import { MinglarService } from '../services/minglar.service'; +import { sendEmailToHostForRejectedApplication } from '../services/rejectionMailtoHost.service'; const prismaService = new PrismaService(); const minglarService = new MinglarService(prismaService); @@ -46,6 +47,8 @@ export const handler = safeHandler(async ( // Add suggestion using service await minglarService.rejectHostApplication(hostXid, userInfo.id); + const hostDetails = await minglarService.getUserDetails(userInfo.id) + await sendEmailToHostForRejectedApplication(hostDetails.emailAddress) return { statusCode: 200, diff --git a/src/modules/minglaradmin/handlers/rejectHostApplicationAM.ts b/src/modules/minglaradmin/handlers/rejectHostApplicationAM.ts new file mode 100644 index 0000000..9f02400 --- /dev/null +++ b/src/modules/minglaradmin/handlers/rejectHostApplicationAM.ts @@ -0,0 +1,65 @@ +import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin'; +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 { MinglarService } from '../services/minglar.service'; +import { sendAMRejectionMailtoHost } from '../services/rejectionMailtoHost.service'; + +const prismaService = new PrismaService(); +const minglarService = new MinglarService(prismaService); + +interface AddSuggestionBody { + hostXid: number; + title: string; + comments: string; +} + +/** + * Add suggestion handler for host applications + * Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions + * Types: Setup Profile, Review Account, Add Payment Details, Agreement + */ +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Verify authentication token + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) { + throw new ApiError(401, 'This is a protected route. Please provide a valid token.'); + } + + // Verify token and get user info + const userInfo = await verifyMinglarAdminToken(token); + + // Parse request body + let body: AddSuggestionBody; + + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (error) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + const { hostXid } = body; + + + // Add suggestion using service + await minglarService.rejectHostApplicationAM(hostXid, userInfo.id); + const hostDetails = await minglarService.getUserDetails(userInfo.id) + await sendAMRejectionMailtoHost(hostDetails.emailAddress) + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Application rejected successfully', + data: null, + }), + }; +}); diff --git a/src/modules/minglaradmin/handlers/updateProfile.ts b/src/modules/minglaradmin/handlers/updateProfile.ts index 4708dca..d9d33eb 100644 --- a/src/modules/minglaradmin/handlers/updateProfile.ts +++ b/src/modules/minglaradmin/handlers/updateProfile.ts @@ -1,13 +1,13 @@ -import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; -import { safeHandler } from '../../../common/utils/handlers/safeHandler'; -import { PrismaService } from '../../../common/database/prisma.service'; -import { MinglarService } from '../services/minglar.service'; -import ApiError from '../../../common/utils/helper/ApiError'; -import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin'; -import { parseMultipartFormData, parseJsonField } from '../../../common/utils/helper/parseMultipartFormData'; -import AWS from 'aws-sdk'; -import crypto from 'crypto'; +// modules/minglar/handlers/updateProfile.ts import config from '@/config/config'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import AWS from 'aws-sdk'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { parseJsonField, parseMultipartFormData } from '../../../common/utils/helper/parseMultipartFormData'; +import { MinglarService } from '../services/minglar.service'; const prismaService = new PrismaService(); const minglarService = new MinglarService(prismaService); @@ -16,136 +16,184 @@ const s3 = new AWS.S3({ region: config.aws.region, }); +// Define uploadToS3 function with proper folder structure and file replacement +async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, folderType: 'profile' | 'documents', userId: number, documentType?: 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() || 'jpg'; + + // Determine folder structure based on type + if (folderType === 'profile') { + // Profile Images: MinglarAdmin/ProfileImages/{UserID}/profile_image.{extension} + const fileName = `profile_image.${fileExtension}`; + const sanitizedFileName = sanitizeFileName(fileName); + s3Key = `MinglarAdmin/ProfileImages/${userId}/${sanitizedFileName}`; + } else if (folderType === 'documents' && documentType) { + // Documents: MinglarAdmin/Documents/{UserID}/{documentType}.{extension} + const fileName = `${documentType}.${fileExtension}`; + const sanitizedFileName = sanitizeFileName(fileName); + s3Key = `MinglarAdmin/Documents/${userId}/${sanitizedFileName}`; + } else { + throw new ApiError(400, 'Invalid folder type or missing documentType'); + } + + // 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}`; +} + 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.'); - } + try { + // 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.'); + } - // Verify token and get user info - const userInfo = await verifyMinglarAdminToken(token); - const userId = Number(userInfo.id); + // Verify token and get user info + const userInfo = await verifyMinglarAdminToken(token); + const userId = Number(userInfo.id); - if (!userId || isNaN(userId)) { - throw new ApiError(400, 'Invalid user ID'); - } + if (!userId || isNaN(userId)) { + throw new ApiError(400, 'Invalid user ID'); + } - // Parse multipart form data - const contentType = event.headers['Content-Type'] || event.headers['content-type']; - const isBase64Encoded = event.isBase64Encoded || false; - - const { fields, files } = parseMultipartFormData( - event.body, - contentType, - isBase64Encoded - ); - - // Parse JSON fields - const userData = parseJsonField(fields, 'userData') || {}; - const addressData = parseJsonField(fields, 'addressData') || {}; - - // Extract user fields - const { firstName, lastName, mobileNumber, dateOfBirth, profileImage } = userData; - - // Extract address fields - const { address1, address2, stateXid, countryXid, cityXid, pinCode } = addressData; - - // Handle file uploads (profileImage, aadharCard, panCard) - const uploadedFiles: Array<{ fileName: string; filePath: string; documentType?: string }> = []; - let profileImagePath: string | undefined = profileImage; - - // Upload profile image if provided as file - const profileImageFile = files.find(f => f.fieldName === 'profileImage'); - if (profileImageFile) { - const uniqueKey = `${userId}_${crypto.randomUUID()}_${profileImageFile.fileName}`; - const s3Key = `MinglarAdmin/ProfileImages/${uniqueKey}`; + // Parse multipart form data + const contentType = event.headers['Content-Type'] || event.headers['content-type']; + const isBase64Encoded = event.isBase64Encoded || false; - await s3.upload({ - Bucket: config.aws.bucketName, - Key: s3Key, - Body: profileImageFile.data, - ContentType: profileImageFile.contentType, - ACL: 'private', - }).promise(); + const { fields, files } = parseMultipartFormData( + event.body, + contentType, + isBase64Encoded + ); - profileImagePath = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`; + // Parse JSON fields + const userData = parseJsonField(fields, 'userData') || {}; + const addressData = parseJsonField(fields, 'addressData') || {}; + + // Extract user fields + const { firstName, lastName, mobileNumber, dateOfBirth, profileImage } = userData; + + // Extract address fields + const { address1, address2, stateXid, countryXid, cityXid, pinCode } = addressData; + + // Handle file uploads with proper folder structure and replacement + const uploadedFiles: Array<{ fileName: string; filePath: string; documentType?: string }> = []; + let profileImagePath: string | undefined = profileImage; + + // Upload profile image if provided as file + const profileImageFile = files.find(f => f.fieldName === 'profileImage'); + if (profileImageFile) { + profileImagePath = await uploadToS3( + profileImageFile.data, + profileImageFile.contentType, + profileImageFile.fileName, + 'profile', + userId + ); + console.log('Profile image uploaded:', profileImagePath); + } + + // Upload documents (aadharCard, panCard) with proper naming and replacement + const aadharFile = files.find(f => f.fieldName === 'aadharCard'); + const panFile = files.find(f => f.fieldName === 'panCard'); + + if (aadharFile) { + const filePath = await uploadToS3( + aadharFile.data, + aadharFile.contentType, + aadharFile.fileName, + 'documents', + userId, + 'aadhar' + ); + uploadedFiles.push({ + fileName: aadharFile.fileName, + filePath, + documentType: 'aadhar' + }); + console.log('Aadhar document uploaded:', filePath); + } + + if (panFile) { + const filePath = await uploadToS3( + panFile.data, + panFile.contentType, + panFile.fileName, + 'documents', + userId, + 'pan' + ); + uploadedFiles.push({ + fileName: panFile.fileName, + filePath, + documentType: 'pan' + }); + console.log('PAN document uploaded:', filePath); + } + + // Update profile using service + const result = await minglarService.updateProfile( + userId, + { + firstName, + lastName, + mobileNumber, + dateOfBirth, + profileImage: profileImagePath, + }, + { + address1, + address2, + stateXid, + countryXid, + cityXid, + pinCode, + }, + uploadedFiles.filter(f => f.documentType).map(f => ({ + fileName: f.fileName, + filePath: f.filePath, + })) + ); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Profile updated successfully', + data: result, + }), + }; + } catch (error: any) { + console.error('❌ Error in updateProfile:', error); + throw error; } - - // Upload documents (aadharCard, panCard) - const aadharFile = files.find(f => f.fieldName === 'aadharCard'); - const panFile = files.find(f => f.fieldName === 'panCard'); - - if (aadharFile) { - const uniqueKey = `${userId}_${crypto.randomUUID()}_${aadharFile.fileName}`; - const s3Key = `MinglarAdmin/Documents/${uniqueKey}`; - - await s3.upload({ - Bucket: config.aws.bucketName, - Key: s3Key, - Body: aadharFile.data, - ContentType: aadharFile.contentType, - ACL: 'private', - }).promise(); - - const filePath = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`; - uploadedFiles.push({ fileName: aadharFile.fileName, filePath, documentType: 'aadhar' }); - } - - if (panFile) { - const uniqueKey = `${userId}_${crypto.randomUUID()}_${panFile.fileName}`; - const s3Key = `MinglarAdmin/${userId}/documents/pan_${uniqueKey}`; - - await s3.upload({ - Bucket: config.aws.bucketName, - Key: s3Key, - Body: panFile.data, - ContentType: panFile.contentType, - ACL: 'private', - }).promise(); - - const filePath = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`; - uploadedFiles.push({ fileName: panFile.fileName, filePath, documentType: 'pan' }); - } - - // Update profile using service - const result = await minglarService.updateProfile( - userId, - { - firstName, - lastName, - mobileNumber, - dateOfBirth, - profileImage: profileImagePath, - }, - { - address1, - address2, - stateXid, - countryXid, - cityXid, - pinCode, - }, - uploadedFiles.filter(f => f.documentType).map(f => ({ - fileName: f.fileName, - filePath: f.filePath, - })) - ); - - return { - statusCode: 200, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - body: JSON.stringify({ - success: true, - message: 'Profile updated successfully', - data: result, - }), - }; -}); - +}); \ No newline at end of file diff --git a/src/modules/minglaradmin/services/approvalMailtoHost.service.ts b/src/modules/minglaradmin/services/approvalMailtoHost.service.ts new file mode 100644 index 0000000..f39fbf4 --- /dev/null +++ b/src/modules/minglaradmin/services/approvalMailtoHost.service.ts @@ -0,0 +1,72 @@ +import { brevoService } from "@/common/email/brevoApi"; +import ApiError from "@/common/utils/helper/ApiError"; + +export async function sendEmailToHostForApprovedApplication( + emailAddress: string, +): Promise<{ + sent: boolean; + // messageId: string +}> { + + const subject = "Approval for your application"; + + const htmlContent = ` +

Dear Host,

+

Congratulations, Your application to minglar admin has been approved.

+

You can start onboarding your activities through the host panel.

+

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 minglar admin via email."); + } +} + +export async function sendEmailToHostForMinglarApproval( + emailAddress: string, +): Promise<{ + sent: boolean; + // messageId: string +}> { + + const subject = "Approval for your application"; + + const htmlContent = ` +

Dear Host,

+

Congratulations, Your application to minglar admin has been approved by minglar admin.

+

Minglar admin will assign account manager to your application.

+

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 minglar admin via email."); + } +} diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index 71a0d99..f5fcdfd 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -1,5 +1,5 @@ import { ROLE, USER_STATUS } from '@/common/utils/constants/common.constant'; -import { HOST_STATUS_DISPLAY, HOST_STATUS_INTERNAL } from '@/common/utils/constants/host.constant'; +import { HOST_STATUS_DISPLAY, HOST_STATUS_INTERNAL, STEPPER } from '@/common/utils/constants/host.constant'; import { MINGLAR_INVITATION_STATUS, MINGLAR_STATUS_DISPLAY, MINGLAR_STATUS_INTERNAL } from '@/common/utils/constants/minglar.constant'; import { Injectable } from '@nestjs/common'; import { User } from '@prisma/client'; @@ -65,7 +65,7 @@ export class MinglarService { } async getAllHosts() { - return this.prisma.user.findMany({ where: { roleXid: 3 } }); + return this.prisma.user.findMany({ where: { roleXid: ROLE.HOST } }); } async updateHost(id: number, data: UpdateMinglarDto) { @@ -83,6 +83,12 @@ export class MinglarService { return this.prisma.user.findUnique({ where: { emailAddress: email } }); } + async getUserDetails(id: number) { + return await this.prisma.user.findUnique({ + where: { id: id } + }) + } + async verifyHostOtp(email: string, otp: string): Promise { const user = await this.prisma.user.findUnique({ where: { emailAddress: email }, @@ -269,154 +275,186 @@ export class MinglarService { }, documents: Array<{ fileName: string; filePath: string }> ) { - return await this.prisma.$transaction(async (tx) => { - // 1. Update User table - const userUpdateData: any = {}; - if (userData.firstName !== undefined) userUpdateData.firstName = userData.firstName; - if (userData.lastName !== undefined) userUpdateData.lastName = userData.lastName; - if (userData.mobileNumber !== undefined) userUpdateData.mobileNumber = userData.mobileNumber; - if (userData.dateOfBirth !== undefined) userUpdateData.dateOfBirth = new Date(userData.dateOfBirth); - if (userData.profileImage !== undefined) userUpdateData.profileImage = userData.profileImage; + try { + return await this.prisma.$transaction(async (tx) => { + console.log('Starting transaction for user:', userId); - if (Object.keys(userUpdateData).length > 0) { - await tx.user.update({ - where: { id: userId }, - data: userUpdateData, - }); - } + // 1. Update User table (optimized) + const userUpdateData: any = {}; + const userFields = ['firstName', 'lastName', 'mobileNumber', 'dateOfBirth', 'profileImage']; - // 2. Update or create UserAddressDetails - if (Object.keys(addressData).length > 0) { - const existingAddress = await tx.userAddressDetails.findFirst({ - where: { userXid: userId, isActive: true }, - }); - - const addressUpdateData: any = {}; - if (addressData.address1 !== undefined) addressUpdateData.address1 = addressData.address1; - if (addressData.address2 !== undefined) addressUpdateData.address2 = addressData.address2; - if (addressData.stateXid !== undefined) addressUpdateData.stateXid = addressData.stateXid; - if (addressData.countryXid !== undefined) addressUpdateData.countryXid = addressData.countryXid; - if (addressData.cityXid !== undefined) addressUpdateData.cityXid = addressData.cityXid; - if (addressData.pinCode !== undefined) addressUpdateData.pinCode = addressData.pinCode; - - if (existingAddress) { - await tx.userAddressDetails.update({ - where: { id: existingAddress.id }, - data: addressUpdateData, - }); - } else { - if (!addressData.address1 || !addressData.stateXid || !addressData.countryXid || !addressData.cityXid || !addressData.pinCode) { - throw new ApiError(400, 'All address fields are required for new address'); + userFields.forEach(field => { + if (userData[field as keyof typeof userData] !== undefined) { + if (field === 'dateOfBirth' && userData.dateOfBirth) { + userUpdateData[field] = new Date(userData.dateOfBirth); + } else { + userUpdateData[field] = userData[field as keyof typeof userData]; + } } - await tx.userAddressDetails.create({ - data: { - userXid: userId, - ...addressUpdateData, - }, + }); + + if (Object.keys(userUpdateData).length > 0) { + console.log('Updating user data:', userUpdateData); + await tx.user.update({ + where: { id: userId }, + data: userUpdateData, }); } - } - // 3. Update or create UserDocuments (store S3 URL in fileName field) - if (documents && documents.length > 0) { - const existingDocs = await tx.userDocuments.findMany({ - where: { userXid: userId, isActive: true }, - orderBy: { createdAt: 'asc' }, - }); + // 2. Update or create UserAddressDetails + if (Object.keys(addressData).length > 0) { + console.log('Processing address data:', addressData); - // Update existing documents or create new ones - for (let i = 0; i < documents.length; i++) { - const doc = documents[i]; - if (existingDocs[i]) { - // Update existing document - await tx.userDocuments.update({ - where: { id: existingDocs[i].id }, - data: { fileName: doc.filePath }, // Store S3 URL in fileName + const existingAddress = await tx.userAddressDetails.findFirst({ + where: { userXid: userId, isActive: true }, + select: { id: true } // Only select needed field + }); + + const addressUpdateData: any = {}; + const addressFields = ['address1', 'address2', 'stateXid', 'countryXid', 'cityXid', 'pinCode']; + + addressFields.forEach(field => { + if (addressData[field as keyof typeof addressData] !== undefined) { + addressUpdateData[field] = addressData[field as keyof typeof addressData]; + } + }); + + if (existingAddress) { + await tx.userAddressDetails.update({ + where: { id: existingAddress.id }, + data: addressUpdateData, }); } else { - // Create new document - await tx.userDocuments.create({ + // Validate required fields + const requiredFields = ['address1', 'stateXid', 'countryXid', 'cityXid', 'pinCode']; + const missingFields = requiredFields.filter(field => !addressData[field as keyof typeof addressData]); + + if (missingFields.length > 0) { + throw new ApiError(400, `Missing required address fields: ${missingFields.join(', ')}`); + } + + await tx.userAddressDetails.create({ data: { userXid: userId, - fileName: doc.filePath, // Store S3 URL in fileName + ...addressUpdateData, }, }); } } - } - // 4. Fetch updated user data to calculate percentage - const updatedUser = await tx.user.findUnique({ - where: { id: userId }, - include: { - userAddressDetails: { - where: { isActive: true }, - take: 1, - }, - userDocuments: { - where: { isActive: true }, - }, - }, - }); + // 3. Handle documents more efficiently + if (documents && documents.length > 0) { + console.log('Processing documents:', documents.length); - if (!updatedUser) { - throw new ApiError(404, 'User not found'); - } + // Use deleteMany and createMany for better performance + await tx.userDocuments.deleteMany({ + where: { userXid: userId, isActive: true }, + }); - // 5. Calculate profile completion percentage - let percentage = 0; - - // Profile Image: 15% - if (updatedUser.profileImage) { - percentage += 15; - } - - // Name and Phone Number: 15% - if (updatedUser.firstName && updatedUser.lastName && updatedUser.mobileNumber) { - percentage += 15; - } - - // Location Info: 25% - if (updatedUser.userAddressDetails && updatedUser.userAddressDetails.length > 0) { - const address = updatedUser.userAddressDetails[0]; - if (address.address1 && address.stateXid && address.countryXid && address.cityXid && address.pinCode) { - percentage += 25; - } - } - - // Documents (Aadhar and PAN): 45% - if (updatedUser.userDocuments && updatedUser.userDocuments.length >= 2) { - percentage += 45; - } else if (updatedUser.userDocuments && updatedUser.userDocuments.length === 1) { - percentage += 22.5; // Half if only one document - } - - const profilePercentage = Math.min(percentage, 100) - if (profilePercentage > 80) { - await this.prisma.user.update({ - where: { - id: userId - }, - data: { - isProfileUpdated: true + if (documents.length > 0) { + await tx.userDocuments.createMany({ + data: documents.map(doc => ({ + userXid: userId, + fileName: doc.filePath, + isActive: true, + })), + }); } - }) - } + } - return { - user: { - id: updatedUser.id, - firstName: updatedUser.firstName, - lastName: updatedUser.lastName, - mobileNumber: updatedUser.mobileNumber, - dateOfBirth: updatedUser.dateOfBirth, - profileImage: updatedUser.profileImage, - }, - address: updatedUser.userAddressDetails[0] || null, - documents: updatedUser.userDocuments, - profileCompletionPercentage: Math.min(percentage, 100), - }; - }); + // 4. Fetch updated user data efficiently + const updatedUser = await tx.user.findUnique({ + where: { id: userId }, + select: { + id: true, + firstName: true, + lastName: true, + mobileNumber: true, + dateOfBirth: true, + profileImage: true, + userAddressDetails: { + where: { isActive: true }, + take: 1, + select: { + id: true, + address1: true, + address2: true, + stateXid: true, + countryXid: true, + cityXid: true, + pinCode: true, + } + }, + userDocuments: { + where: { isActive: true }, + select: { + id: true, + fileName: true, + } + }, + }, + }); + + if (!updatedUser) { + throw new ApiError(404, 'User not found after update'); + } + + // 5. Calculate profile completion percentage + let percentage = 0; + + // Profile Image: 15% + if (updatedUser.profileImage) percentage += 15; + + // Name and Phone Number: 15% + if (updatedUser.firstName && updatedUser.lastName && updatedUser.mobileNumber) { + percentage += 15; + } + + // Location Info: 25% + if (updatedUser.userAddressDetails.length > 0) { + const address = updatedUser.userAddressDetails[0]; + if (address.address1 && address.stateXid && address.countryXid && address.cityXid && address.pinCode) { + percentage += 25; + } + } + + // Documents: 45% + if (updatedUser.userDocuments.length >= 2) { + percentage += 45; + } else if (updatedUser.userDocuments.length === 1) { + percentage += 22.5; + } + + const profilePercentage = Math.min(percentage, 100); + + // Update profile completion status + if (profilePercentage > 80) { + await tx.user.update({ + where: { id: userId }, + data: { isProfileUpdated: true } + }); + } + + console.log('Transaction completed successfully'); + + return { + user: { + id: updatedUser.id, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + mobileNumber: updatedUser.mobileNumber, + dateOfBirth: updatedUser.dateOfBirth, + profileImage: updatedUser.profileImage, + }, + address: updatedUser.userAddressDetails[0] || null, + documents: updatedUser.userDocuments, + profileCompletionPercentage: profilePercentage, + }; + }); + } catch (error) { + console.error('Error in updateProfile transaction:', error); + throw error; + } } async getAllInvitationDetails() { @@ -704,11 +742,31 @@ export class MinglarService { adminStatusDisplay: MINGLAR_STATUS_DISPLAY.TO_REVIEW }, data: { - isApproved: true, hostStatusInternal: HOST_STATUS_INTERNAL.APPROVED, hostStatusDisplay: HOST_STATUS_DISPLAY.APPROVED, adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_APPROVED, - adminStatusDisplay: MINGLAR_STATUS_DISPLAY.APPROVED + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.APPROVED, + stepper: STEPPER.COMPANY_DETAILS_APPROVED + } + }) + } + + + async acceptHostApplicationMinglarAdmin(host_xid: number, user_xid: number) { + return await this.prisma.hostHeader.update({ + where: { + id: host_xid, + hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, + hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, + adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW, + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW + }, + data: { + isApproved: true, + hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, + hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, + adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED, + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED, } }) } @@ -747,5 +805,30 @@ export class MinglarService { } + async rejectHostApplicationAM(host_xid: number, user_xid: number) { + const hostDetails = await this.prisma.hostHeader.findFirst({ + where: { id: host_xid }, + select: { id: true, userXid: true } + }) + if (!hostDetails) { + throw new Error("Host not found"); + } + await this.prisma.hostHeader.update({ + where: { + id: host_xid, + hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, + hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW + }, + data: { + hostStatusInternal: HOST_STATUS_INTERNAL.REJECTED, + hostStatusDisplay: HOST_STATUS_DISPLAY.REJECTED, + adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED, + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.REJECTED, + + } + }) + } + + } diff --git a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts new file mode 100644 index 0000000..48b6e2d --- /dev/null +++ b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts @@ -0,0 +1,73 @@ +import { brevoService } from "@/common/email/brevoApi"; +import ApiError from "@/common/utils/helper/ApiError"; + +export async function sendEmailToHostForRejectedApplication( + emailAddress: string, +): Promise<{ + sent: boolean; + // messageId: string +}> { + + const subject = "Rejection for your application"; + + const htmlContent = ` +

Dear Host,

+

Sorry to say that, But your application to minglar admin has been rejected.

+

If you have any questions please contact to minglar admin.

+

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 minglar admin via email."); + } +} + +export async function sendAMRejectionMailtoHost( + emailAddress: string, +): Promise<{ + sent: boolean; + // messageId: string +}> { + + const subject = "Improvement of your application"; + + const htmlContent = ` +

Dear Host,

+

Your account manager has made some suggestions on your application.
+ Please improve it and re-submit the application to onboard on minglar.

+

If you have any questions please contact to minglar admin.

+

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 minglar admin via email."); + } +}