From 77cef98091eea8ebfb202ed96f1a42497cffc41a Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Wed, 4 Mar 2026 17:04:14 +0530 Subject: [PATCH] Implement updateHostProfile endpoint and related service logic; remove navigation modes seeding; add logging for user activities --- prisma/seed.ts | 10 - serverless/functions/host.yml | 16 ++ .../Host_Admin/onboarding/acceptAggrement.ts | 12 +- .../host/handlers/updateHostProfile.ts | 244 ++++++++++++++++++ src/modules/host/services/host.service.ts | 230 ++++++++++++++--- .../getActivityFromConnectionsInterest.ts | 2 + src/modules/user/services/user.service.ts | 4 + 7 files changed, 462 insertions(+), 56 deletions(-) create mode 100644 src/modules/host/handlers/updateHostProfile.ts diff --git a/prisma/seed.ts b/prisma/seed.ts index 864f2b0..be881b5 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -693,16 +693,6 @@ async function main() { skipDuplicates: true, }); - // ✅ Navigation Modes - await prisma.navigationModes.createMany({ - data: [ - { navigationModeName: 'Elephant Ride', navigationModeIcon: '🚗' }, - { navigationModeName: 'Horse Ride', navigationModeIcon: '🏍️' }, - { navigationModeName: 'Camel Ride', navigationModeIcon: '🚶' }, - ], - skipDuplicates: true, - }); - // ✅ Transport Modes await prisma.transportModes.createMany({ data: [ diff --git a/serverless/functions/host.yml b/serverless/functions/host.yml index 40e67bc..9b19ed0 100644 --- a/serverless/functions/host.yml +++ b/serverless/functions/host.yml @@ -292,6 +292,22 @@ getStepperInfo: path: /stepper method: get +updateHostProfile: + handler: src/modules/host/handlers/updateHostProfile.handler + memorySize: 384 + package: + patterns: + - 'src/modules/host/handlers/updateHostProfile.*' + - 'src/modules/host/services/**' + - ${file(./serverless/patterns/base.yml):pattern1} + - ${file(./serverless/patterns/base.yml):pattern2} + - ${file(./serverless/patterns/base.yml):pattern3} + - ${file(./serverless/patterns/base.yml):pattern4} + events: + - httpApi: + path: /host/profile + method: patch + # Functions with S3/AWS SDK dependencies submitCompanyDetails: handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler diff --git a/src/modules/host/handlers/Host_Admin/onboarding/acceptAggrement.ts b/src/modules/host/handlers/Host_Admin/onboarding/acceptAggrement.ts index 500eec1..1cbb556 100644 --- a/src/modules/host/handlers/Host_Admin/onboarding/acceptAggrement.ts +++ b/src/modules/host/handlers/Host_Admin/onboarding/acceptAggrement.ts @@ -1,6 +1,6 @@ -import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; +import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import ApiError from '../../../../../common/utils/helper/ApiError'; import { HostService } from '../../../services/host.service'; @@ -25,9 +25,8 @@ export const handler = safeHandler(async ( // Verify token and get user info const userInfo = await verifyHostToken(token); - - // Add suggestion using service - await hostService.acceptMinglarAgreement(userInfo.id); + // Accept agreement and get dynamic fields and PDF URL + const result = await hostService.acceptMinglarAgreement(userInfo.id); return { statusCode: 200, @@ -38,7 +37,10 @@ export const handler = safeHandler(async ( body: JSON.stringify({ success: true, message: 'Application accepted successfully', - data: null, + data: { + filePath: result.filePath, + dynamicFields: result.dynamicFields, + }, }), }; }); diff --git a/src/modules/host/handlers/updateHostProfile.ts b/src/modules/host/handlers/updateHostProfile.ts new file mode 100644 index 0000000..65461b5 --- /dev/null +++ b/src/modules/host/handlers/updateHostProfile.ts @@ -0,0 +1,244 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import dayjs from 'dayjs'; +import { z } from 'zod'; +import { prismaClient } from '../../../common/database/prisma.lambda.service'; +import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { ROLE } from '../../../common/utils/constants/common.constant'; + +const updateHostProfileSchema = z + .strictObject({ + // Personal + fullName: z.string().min(1).optional(), + firstName: z.string().min(1).optional(), + lastName: z.string().min(1).optional(), + isdCode: z.string().min(1).max(6).optional(), + mobileNumber: z.string().min(5).max(15).optional(), + dateOfBirth: z.string().min(1).optional(), + + // Address + address1: z.string().min(1).optional(), + address2: z.string().min(1).optional(), + countryXid: z.number().int().positive().optional(), + stateXid: z.number().int().positive().optional(), + cityXid: z.number().int().positive().optional(), + pinCode: z.string().min(1).optional(), + + // explicitly forbidden + emailAddress: z.any().optional(), + }) + .strip(); + +function parseDob(dateOfBirth: string): Date { + const parsed = dayjs(dateOfBirth, ['YYYY-MM-DD', 'MM/DD/YYYY', 'DD/MM/YYYY'], true); + if (!parsed.isValid()) { + throw new ApiError(400, 'Invalid dateOfBirth. Use YYYY-MM-DD (recommended) or MM/DD/YYYY.'); + } + return parsed.toDate(); +} + +function splitFullName(fullName: string): { firstName: string; lastName: string | null } { + const parts = fullName.trim().split(/\s+/).filter(Boolean); + const firstName = parts[0] || ''; + const lastName = parts.length > 1 ? parts.slice(1).join(' ') : null; + return { firstName, lastName }; +} + +function getAuthToken(event: APIGatewayProxyEvent): string { + 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.'); + } + return token; +} + +function parseJsonBody(event: APIGatewayProxyEvent): any { + try { + return event.body ? JSON.parse(event.body) : {}; + } catch { + throw new ApiError(400, 'Invalid JSON in request body'); + } +} + +function validateBody(body: any) { + const parsed = updateHostProfileSchema.safeParse(body); + if (!parsed.success) { + throw new ApiError(400, parsed.error.issues.map((i) => i.message).join(', ')); + } + if (parsed.data.emailAddress !== undefined) { + throw new ApiError(400, 'Email address cannot be updated.'); + } + return parsed.data; +} + +function normalizeNameFields(data: any): { firstName?: string; lastName?: string | null } { + if (data.fullName && !data.firstName && !data.lastName) { + const split = splitFullName(data.fullName); + return { firstName: split.firstName, lastName: split.lastName }; + } + return { firstName: data.firstName, lastName: data.lastName }; +} + +function buildAddressInput(data: any) { + return { + address1: data.address1, + address2: data.address2, + countryXid: data.countryXid, + stateXid: data.stateXid, + cityXid: data.cityXid, + pinCode: data.pinCode, + }; +} + +function hasAnyDefined(obj: Record) { + return Object.values(obj).some((v) => v !== undefined); +} + +async function ensureHostUser(tx: any, userId: number) { + const user = await tx.user.findUnique({ + where: { id: userId, isActive: true }, + select: { id: true, roleXid: true }, + }); + + if (!user) throw new ApiError(404, 'User not found'); + if (user.roleXid !== ROLE.HOST) throw new ApiError(403, 'Access denied.'); +} + +async function updateUserIfNeeded(tx: any, userId: number, input: { firstName?: string; lastName?: string | null; isdCode?: string; mobileNumber?: string; dateOfBirth?: string }) { + const userUpdateData: any = {}; + if (input.firstName !== undefined) userUpdateData.firstName = input.firstName || null; + if (input.lastName !== undefined) userUpdateData.lastName = input.lastName; + if (input.isdCode !== undefined) userUpdateData.isdCode = input.isdCode || null; + if (input.mobileNumber !== undefined) userUpdateData.mobileNumber = input.mobileNumber || null; + if (input.dateOfBirth !== undefined) { + userUpdateData.dateOfBirth = input.dateOfBirth ? parseDob(input.dateOfBirth) : null; + } + + if (!hasAnyDefined(userUpdateData)) return; + + await tx.user.update({ + where: { id: userId }, + data: { + ...userUpdateData, + isProfileUpdated: true, + }, + }); +} + +async function upsertAddressIfNeeded(tx: any, userId: number, addressData: Record) { + if (!hasAnyDefined(addressData)) return; + + const existingAddress = await tx.userAddressDetails.findFirst({ + where: { userXid: userId, isActive: true }, + select: { id: true }, + }); + + const addressUpdateData: any = {}; + if (addressData.address1 !== undefined) addressUpdateData.address1 = addressData.address1; + if (addressData.address2 !== undefined) addressUpdateData.address2 = addressData.address2; + if (addressData.countryXid !== undefined) addressUpdateData.countryXid = addressData.countryXid; + if (addressData.stateXid !== undefined) addressUpdateData.stateXid = addressData.stateXid; + 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, + }); + return; + } + + const required = ['address1', 'countryXid', 'stateXid', 'cityXid', 'pinCode'] as const; + const missing = required.filter((k) => addressData[k] === undefined); + if (missing.length) { + throw new ApiError(400, `Missing required address fields: ${missing.join(', ')}`); + } + + await tx.userAddressDetails.create({ + data: { + userXid: userId, + ...addressUpdateData, + }, + }); +} + +async function getProfileSnapshot(tx: any, userId: number) { + const updated = await tx.user.findUnique({ + where: { id: userId }, + select: { + id: true, + firstName: true, + lastName: true, + emailAddress: true, + isdCode: true, + mobileNumber: true, + dateOfBirth: true, + profileImage: true, + isProfileUpdated: true, + userAddressDetails: { + where: { isActive: true }, + take: 1, + select: { + id: true, + address1: true, + address2: true, + countryXid: true, + stateXid: true, + cityXid: true, + pinCode: true, + }, + }, + }, + }); + + return { + user: updated, + address: updated?.userAddressDetails?.[0] ?? null, + }; +} + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context, +): Promise => { + const token = getAuthToken(event); + const userInfo = await verifyHostToken(token); + const userId = Number(userInfo.id); + if (!userId || Number.isNaN(userId)) { + throw new ApiError(400, 'Invalid user id'); + } + + const body = parseJsonBody(event); + const data = validateBody(body); + const name = normalizeNameFields(data); + const address = buildAddressInput(data); + + const result = await prismaClient.$transaction(async (tx) => { + await ensureHostUser(tx, userId); + await updateUserIfNeeded(tx, userId, { + firstName: name.firstName, + lastName: name.lastName, + isdCode: data.isdCode, + mobileNumber: data.mobileNumber, + dateOfBirth: data.dateOfBirth, + }); + await upsertAddressIfNeeded(tx, userId, address); + return getProfileSnapshot(tx, userId); + }); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Profile updated successfully', + data: result, + }), + }; +}); + diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 72de2bc..095bd3e 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -391,6 +391,22 @@ const s3 = new AWS.S3({ region: config.aws.region, }); +type UpdateHostProfileInput = { + firstName?: string; + lastName?: string | null; + isdCode?: string; + mobileNumber?: string; + dateOfBirth?: Date; + address?: { + address1?: string; + address2?: string; + countryXid?: number; + stateXid?: number; + cityXid?: number; + pinCode?: string; + }; +}; + @Injectable() export class HostService { constructor(private prisma: PrismaClient) { } @@ -577,6 +593,114 @@ export class HostService { return this.prisma.user.delete({ where: { id } }); } + /** + * Update the logged-in Host's personal profile details. + * Email is intentionally NOT editable here. + */ + async updateHostProfileDetails(userId: number, input: UpdateHostProfileInput) { + return this.prisma.$transaction(async (tx) => { + const user = await tx.user.findUnique({ + where: { id: userId, isActive: true }, + select: { id: true, roleXid: true }, + }); + + if (!user) throw new ApiError(404, 'User not found'); + if (user.roleXid !== ROLE.HOST) throw new ApiError(403, 'Access denied.'); + + // 1) Update `User` (whitelist only) + const userUpdateData: any = {}; + if (input.firstName !== undefined) userUpdateData.firstName = input.firstName || null; + if (input.lastName !== undefined) userUpdateData.lastName = input.lastName; + if (input.isdCode !== undefined) userUpdateData.isdCode = input.isdCode || null; + if (input.mobileNumber !== undefined) userUpdateData.mobileNumber = input.mobileNumber || null; + if (input.dateOfBirth !== undefined) userUpdateData.dateOfBirth = input.dateOfBirth; + + if (Object.keys(userUpdateData).length > 0) { + await tx.user.update({ + where: { id: userId }, + data: { + ...userUpdateData, + isProfileUpdated: true, + }, + }); + } + + // 2) Update/Create `UserAddressDetails` (if any address field sent) + const addressData = input.address || {}; + const hasAnyAddressField = Object.values(addressData).some((v) => v !== undefined); + + if (hasAnyAddressField) { + const existingAddress = await tx.userAddressDetails.findFirst({ + where: { userXid: userId, isActive: true }, + select: { id: true }, + }); + + const addressUpdateData: any = {}; + if (addressData.address1 !== undefined) addressUpdateData.address1 = addressData.address1; + if (addressData.address2 !== undefined) addressUpdateData.address2 = addressData.address2; + if (addressData.countryXid !== undefined) addressUpdateData.countryXid = addressData.countryXid; + if (addressData.stateXid !== undefined) addressUpdateData.stateXid = addressData.stateXid; + 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 { + const required = ['address1', 'countryXid', 'stateXid', 'cityXid', 'pinCode'] as const; + const missing = required.filter((k) => addressData[k] === undefined); + + if (missing.length) { + throw new ApiError(400, `Missing required address fields: ${missing.join(', ')}`); + } + + await tx.userAddressDetails.create({ + data: { + userXid: userId, + ...addressUpdateData, + }, + }); + } + } + + // 3) Return updated profile snapshot (including read-only email) + const updated = await tx.user.findUnique({ + where: { id: userId }, + select: { + id: true, + firstName: true, + lastName: true, + emailAddress: true, + isdCode: true, + mobileNumber: true, + dateOfBirth: true, + profileImage: true, + isProfileUpdated: true, + userAddressDetails: { + where: { isActive: true }, + take: 1, + select: { + id: true, + address1: true, + address2: true, + countryXid: true, + stateXid: true, + cityXid: true, + pinCode: true, + }, + }, + }, + }); + + return { + user: updated, + address: updated?.userAddressDetails?.[0] ?? null, + }; + }); + } + async getHostByEmail(email: string): Promise { return this.prisma.user.findUnique({ where: { emailAddress: email } }); } @@ -919,55 +1043,79 @@ export class HostService { acceptDate, }; - const pdfBuffer = await renderAgreementPdf(agreementVars); + let pdfUrl: string | null = null; - const existingCount = await this.prisma.hostAgreement.count({ - where: { hostXid: host.id, isActive: true }, - }); + try { + const pdfBuffer = await renderAgreementPdf(agreementVars); - const nextVersionNumber = `AG${existingCount + 1}`; - const baseKey = `Documents/Host/${host.id}/agreements/${nextVersionNumber}`; - - const pdfKey = `${baseKey}.pdf`; - - await s3 - .upload({ - Bucket: config.aws.bucketName, - Key: pdfKey, - Body: pdfBuffer, - ContentType: 'application/pdf', - ACL: 'private', - }) - .promise(); - - const pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`; - - await this.prisma.$transaction(async (tx) => { - // Optional: mark previous agreements inactive - await tx.hostAgreement.updateMany({ + const existingCount = await this.prisma.hostAgreement.count({ where: { hostXid: host.id, isActive: true }, - data: { isActive: false }, }); - await tx.hostAgreement.create({ - data: { - hostXid: host.id, - filePath: pdfUrl, - versionNumber: nextVersionNumber, - isActive: true, - }, + const nextVersionNumber = `AG${existingCount + 1}`; + const baseKey = `Documents/Host/${host.id}/agreements/${nextVersionNumber}`; + + const pdfKey = `${baseKey}.pdf`; + + await s3 + .upload({ + Bucket: config.aws.bucketName, + Key: pdfKey, + Body: pdfBuffer, + ContentType: 'application/pdf', + ACL: 'private', + }) + .promise(); + + pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`; + } catch (error) { + console.error('Error generating or uploading PDF:', error); + // Continue without PDF - will return dynamic fields instead + } + + try { + const existingCount = await this.prisma.hostAgreement.count({ + where: { hostXid: host.id, isActive: true }, }); - await tx.hostHeader.update({ - where: { id: host.id }, - data: { - stepper: STEPPER.AGREEMENT_ACCEPTED, - isApproved: true, - agreementAccepted: true, - agreementStartDate: host.agreementStartDate || new Date(), - }, + const nextVersionNumber = `AG${existingCount + 1}`; + + await this.prisma.$transaction(async (tx) => { + // Optional: mark previous agreements inactive + await tx.hostAgreement.updateMany({ + where: { hostXid: host.id, isActive: true }, + data: { isActive: false }, + }); + + await tx.hostAgreement.create({ + data: { + hostXid: host.id, + filePath: pdfUrl, + versionNumber: nextVersionNumber, + isActive: true, + }, + }); + + await tx.hostHeader.update({ + where: { id: host.id }, + data: { + stepper: STEPPER.AGREEMENT_ACCEPTED, + isApproved: true, + agreementAccepted: true, + agreementStartDate: host.agreementStartDate || new Date(), + }, + }); }); - }); + } catch (error) { + console.error('Error creating host agreement record:', error); + // Continue without creating agreement record - will return dynamic fields instead + } + + // Return dynamic fields and PDF URL + return { + filePath: pdfUrl, + dynamicFields: agreementVars, + }; } /** diff --git a/src/modules/user/handlers/connections/getActivityFromConnectionsInterest.ts b/src/modules/user/handlers/connections/getActivityFromConnectionsInterest.ts index c444c1d..b6ae923 100644 --- a/src/modules/user/handlers/connections/getActivityFromConnectionsInterest.ts +++ b/src/modules/user/handlers/connections/getActivityFromConnectionsInterest.ts @@ -45,6 +45,8 @@ export const handler = safeHandler(async ( throw new ApiError(400, 'Invalid schoolCompanyXids'); } + console.log('schoolCompanyXids', schoolCompanyXids); + const result = await userService.getAllActivitiesFromConnectionsUserInterests( userId, schoolCompanyXids, diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index 290ff11..672f6d8 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -2661,6 +2661,8 @@ export class UserService { }, }); + console.log('networkUsers', networkUsers); + if (!networkUsers.length) { return { interests: [], @@ -2695,6 +2697,8 @@ export class UserService { const distinctInterests = networkUserInterests.map(i => i.interestXid); + console.log('distinctInterests', distinctInterests); + if (!distinctInterests.length) { return { interests: [],