Add new handlers for accepting and rejecting host applications, including email notifications. Updated serverless configuration with new function timeouts and added missing handlers. Refactored profile update logic to improve file upload handling and added user detail retrieval methods in the Minglar service.

This commit is contained in:
2025-11-22 19:22:34 +05:30
parent e7c94a1b19
commit 3b1aac921f
12 changed files with 716 additions and 274 deletions

View File

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

View File

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

View File

@@ -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
}
})
}

View File

@@ -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 = `
<p>Dear ${amName},</p>
@@ -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 = `
<p>Dear ${minglarAdminName},</p>

View File

@@ -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<APIGatewayProxyResult> => {
// 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,
}),
};
});

View File

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

View File

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

View File

@@ -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<APIGatewayProxyResult> => {
// 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,
}),
};
});

View File

@@ -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<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.');
}
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,
}),
};
});
});

View File

@@ -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 = `
<p>Dear Host,</p>
<p>Congratulations, Your application to minglar admin has been approved.</p>
<p>You can start onboarding your activities through the host panel.</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 minglar admin via email.");
}
}
export async function sendEmailToHostForMinglarApproval(
emailAddress: string,
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Approval for your application";
const htmlContent = `
<p>Dear Host,</p>
<p>Congratulations, Your application to minglar admin has been approved by minglar admin.</p>
<p>Minglar admin will assign account manager to your application.</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 minglar admin via email.");
}
}

View File

@@ -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<boolean> {
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,
}
})
}
}

View File

@@ -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 = `
<p>Dear Host,</p>
<p>Sorry to say that, But your application to minglar admin has been rejected.</p>
<p>If you have any questions please contact to minglar admin.</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 minglar admin via email.");
}
}
export async function sendAMRejectionMailtoHost(
emailAddress: string,
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Improvement of your application";
const htmlContent = `
<p>Dear Host,</p>
<p>Your account manager has made some suggestions on your application.<br/>
Please improve it and re-submit the application to onboard on minglar.</p>
<p>If you have any questions please contact to minglar admin.</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 minglar admin via email.");
}
}