made update profile for minglar admin

This commit is contained in:
2025-11-14 17:22:07 +05:30
parent b2298e0fb1
commit 8e91fee8ee
5 changed files with 409 additions and 214 deletions

View File

@@ -15,12 +15,14 @@ model User {
firstName String? @map("first_name")
lastName String? @map("last_name")
roleXid Int? @map("role_xid")
dateOfBirth DateTime? @map("date_of_birth")
role Roles? @relation(fields: [roleXid], references: [id], onDelete: Restrict)
emailAddress String @unique @map("email_address")
isdCode String? @map("isd_code")
mobileNumber String? @map("mobile_number")
userPassword String? @map("user_password")
userPasscode String? @map("user_passcode")
profileImage String? @map("profile_image")
isEmailVerfied Boolean? @default(false) @map("is_email_verified")
isMobileVerfied Boolean? @default(false) @map("is_mobile_verified")
isActive Boolean? @default(true) @map("is_active")
@@ -55,11 +57,53 @@ model User {
connectDetails ConnectDetails[]
friends Friends[]
friendOf Friends[] @relation("FriendUser")
userAddressDetails UserAddressDetails[]
userDocuments UserDocuments[]
@@map("users")
@@schema("usr")
}
model UserAddressDetails {
id Int @id @default(autoincrement())
userXid Int @map("user_xid")
user User @relation(fields: [userXid], references: [id], onDelete: Cascade)
address1 String @map("address_1")
address2 String? @map("address_2")
countryXid Int @map("country_xid")
country Countries @relation(fields: [countryXid], references: [id], onDelete: Restrict)
stateXid Int @map("state_xid")
states States @relation(fields: [stateXid], references: [id], onDelete: Restrict)
cityXid Int @map("city_xid")
cities Cities @relation(fields: [cityXid], references: [id], onDelete: Restrict)
pinCode String @map("pin_code")
locationName String? @map("location_name")
locationAddress String? @map("location_address")
locationLat Float? @map("location_lat")
locationLong Float? @map("location_long")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("user_address_details")
@@schema("usr")
}
model UserDocuments {
id Int @id @default(autoincrement())
userXid Int @map("user_xid")
user User @relation(fields: [userXid], references: [id], onDelete: Cascade)
fileName String @map("file_name")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("user_documents")
@@schema("usr")
}
model UserOtp {
id Int @id @default(autoincrement())
userXid Int @map("user_xid")
@@ -160,20 +204,21 @@ model UserInterests {
}
model Countries {
id Int @id @default(autoincrement())
countryName String @unique @map("country_name")
countryCode String @unique @map("country_code")
countryFlag String @map("country_flag")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
Currencies Currencies[]
States States[]
Taxes Taxes[]
Banks Banks[]
HostHeader HostHeader[]
hostParent HostParent[]
id Int @id @default(autoincrement())
countryName String @unique @map("country_name")
countryCode String @unique @map("country_code")
countryFlag String @map("country_flag")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
Currencies Currencies[]
States States[]
Taxes Taxes[]
Banks Banks[]
HostHeader HostHeader[]
hostParent HostParent[]
userAddressDetails UserAddressDetails[]
@@map("countries")
@@schema("mst")
@@ -197,35 +242,37 @@ model Currencies {
}
model States {
id Int @id @default(autoincrement())
countryXid Int @map("country_xid")
country Countries @relation(fields: [countryXid], references: [id], onDelete: Cascade)
stateName String @unique @map("state_name")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
Cities Cities[]
BankBranches BankBranches[]
HostHeader HostHeader[]
hostParent HostParent[]
id Int @id @default(autoincrement())
countryXid Int @map("country_xid")
country Countries @relation(fields: [countryXid], references: [id], onDelete: Cascade)
stateName String @unique @map("state_name")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
Cities Cities[]
BankBranches BankBranches[]
HostHeader HostHeader[]
hostParent HostParent[]
userAddressDetails UserAddressDetails[]
@@map("states")
@@schema("mst")
}
model Cities {
id Int @id @default(autoincrement())
stateXid Int @map("state_xid")
states States @relation(fields: [stateXid], references: [id], onDelete: Cascade)
cityName String @unique @map("city_name")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
BankBranches BankBranches[]
HostHeader HostHeader[]
hostParent HostParent[]
id Int @id @default(autoincrement())
stateXid Int @map("state_xid")
states States @relation(fields: [stateXid], references: [id], onDelete: Cascade)
cityName String @unique @map("city_name")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
BankBranches BankBranches[]
HostHeader HostHeader[]
hostParent HostParent[]
userAddressDetails UserAddressDetails[]
@@map("cities")
@@schema("mst")

View File

@@ -258,6 +258,23 @@ functions:
path: /minglaradmin/create-password
method: post
updateMinglarProfile:
handler: src/modules/minglaradmin/handlers/updateProfile.handler
package:
patterns:
- "src/modules/host/handlers/addCompanyDetails.*"
- "src/modules/host/services/**"
- "common/**"
- "src/common/**"
- "node_modules/@prisma/client/**"
- "node_modules/.prisma/**"
- "node_modules/@aws-sdk/**"
- "node_modules/@smithy/**"
events:
- httpApi:
path: /minglaradmin/update-profile
method: patch
addCompanyDetails:
handler: src/modules/host/handlers/addCompanyDetails.handler

View File

@@ -1,177 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { HostService } from '../../host/services/host.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
import {
hostCompanyDetailsSchema,
REQUIRED_DOC_TYPES,
} from '../../../common/utils/validation/host/hostCompanyDetails.validation';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
import config from '@/config/config';
const prisma = new PrismaService();
const hostService = new HostService(prisma);
const s3 = new AWS.S3({
region: config.aws.region,
});
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// ✅ 1. Verify Token
// 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
const userInfo = await verifyHostToken(token);
// ✅ 2. Ensure content-type is multipart/form-data
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.');
const bodyBuffer = Buffer.from(event.body as string, 'base64');
const fields: Record<string, any> = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// ✅ 3. Parse multipart data using Busboy
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
const chunks: Buffer[] = [];
let totalSize = 0;
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
file.on('data', (chunk) => {
totalSize += chunk.length;
if (totalSize > MAX_SIZE) {
file.resume();
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit.`));
}
chunks.push(chunk);
});
file.on('end', () => {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
});
});
bb.on('field', (fieldname, val) => {
try {
fields[fieldname] = JSON.parse(val);
} catch {
fields[fieldname] = val;
}
});
bb.on('close', resolve);
bb.on('error', reject);
bb.end(bodyBuffer);
});
// ✅ 4. Validate fields
if (!fields.companyDetails) throw new ApiError(400, 'Missing companyDetails field.');
if (!fields.documents) throw new ApiError(400, 'Missing documents field.');
if (files.length === 0) throw new ApiError(400, 'At least one document file is required.');
// ✅ Parse & validate JSON inputs
let companyDetails;
try {
companyDetails = typeof fields.companyDetails === 'string' ? JSON.parse(fields.companyDetails) : fields.companyDetails;
} catch {
throw new ApiError(400, 'Invalid JSON in companyDetails.');
}
const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetails);
if (!companyValidation.success) {
const message = companyValidation.error.issues.map((e) => e.message).join(', ');
throw new ApiError(400, `Validation failed: ${message}`);
}
const parsedCompany = companyValidation.data;
let documentsMetadata;
try {
documentsMetadata = typeof fields.documents === 'string' ? JSON.parse(fields.documents) : fields.documents;
} catch {
throw new ApiError(400, 'Invalid JSON in documents.');
}
if (!Array.isArray(documentsMetadata) || documentsMetadata.length === 0)
throw new ApiError(400, 'Documents must be a non-empty array.');
// ✅ 5. Map uploaded files to document 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 };
});
// ✅ 6. Ensure all required document types exist
const uploadedDocTypes = documentMetadata.map((d) => d.documentTypeXid);
const missingDocs = Object.entries(REQUIRED_DOC_TYPES)
.filter(([_, typeId]) => !uploadedDocTypes.includes(typeId))
.map(([name]) => name);
if (missingDocs.length > 0)
throw new ApiError(400, `Missing mandatory documents: ${missingDocs.join(', ')}`);
// ✅ 7. Upload to S3
const uploadedDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = [];
for (const doc of documentMetadata) {
const uniqueKey = `${userInfo.id}_${crypto.randomUUID()}_${doc.file.fileName}`;
const s3Key = `Documents/Host/${uniqueKey}`;
await s3
.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: doc.file.buffer,
ContentType: doc.file.mimeType,
ACL: 'private',
})
.promise();
uploadedDocs.push({
documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName,
filePath: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
});
}
// ✅ 8. Save company details + documents in DB via MinglarService
const createdHost = await hostService.addCompanyDetails(parsedCompany, uploadedDocs);
if (!createdHost) throw new ApiError(400, 'Failed to add company details.');
// ✅ 9. Success response
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Company details and documents uploaded successfully.',
data: createdHost,
}),
};
} catch (error: any) {
console.error('❌ Error in addCompanyDetails:', error);
throw error;
}
});

View File

@@ -0,0 +1,151 @@
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';
import config from '@/config/config';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const s3 = new AWS.S3({
region: config.aws.region,
});
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.');
}
// 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');
}
// 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}`;
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: profileImageFile.data,
ContentType: profileImageFile.contentType,
ACL: 'private',
}).promise();
profileImagePath = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
// 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

@@ -131,4 +131,161 @@ export class MinglarService {
return existingUser;
}
async updateProfile(
userId: number,
userData: {
firstName?: string;
lastName?: string;
mobileNumber?: string;
dateOfBirth?: string;
profileImage?: string;
},
addressData: {
address1?: string;
address2?: string;
stateXid?: number;
countryXid?: number;
cityXid?: number;
pinCode?: string;
},
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;
if (Object.keys(userUpdateData).length > 0) {
await tx.user.update({
where: { id: userId },
data: userUpdateData,
});
}
// 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');
}
await tx.userAddressDetails.create({
data: {
userXid: userId,
...addressUpdateData,
},
});
}
}
// 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' },
});
// 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
});
} else {
// Create new document
await tx.userDocuments.create({
data: {
userXid: userId,
fileName: doc.filePath, // Store S3 URL in fileName
},
});
}
}
}
// 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 },
},
},
});
if (!updatedUser) {
throw new ApiError(404, 'User not found');
}
// 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
}
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),
};
});
}
}