From dcb2259c7d07c2c41abb30721225c6753977019e Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Mon, 13 Apr 2026 15:13:43 +0530 Subject: [PATCH] implement chat functionality for users and hosts with message sending and retrieval --- prisma/schema.prisma | 21 +++ serverless/functions/host.yml | 30 ++++ serverless/functions/user.yml | 30 ++++ src/common/services/chat.service.ts | 150 ++++++++++++++++++ src/modules/host/handlers/chat/getMessages.ts | 77 +++++++++ src/modules/host/handlers/chat/sendMessage.ts | 78 +++++++++ src/modules/user/handlers/chat/getMessages.ts | 77 +++++++++ src/modules/user/handlers/chat/sendMessage.ts | 78 +++++++++ 8 files changed, 541 insertions(+) create mode 100644 src/common/services/chat.service.ts create mode 100644 src/modules/host/handlers/chat/getMessages.ts create mode 100644 src/modules/host/handlers/chat/sendMessage.ts create mode 100644 src/modules/user/handlers/chat/getMessages.ts create mode 100644 src/modules/user/handlers/chat/sendMessage.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7c65903..a2e4799 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -79,6 +79,8 @@ model User { // 🔹 Activities where this user is Account Manager managedActivities Activities[] @relation("ActivityAccountManager") activitySortings ActivitySorting[] + sentActivityMessages ActivityMessages[] @relation("ActivityMessageSender") + receivedActivityMessages ActivityMessages[] @relation("ActivityMessageReceiver") @@map("users") @@schema("usr") @@ -1036,11 +1038,30 @@ model Activities { activityCuisines ActivityCuisine[] activityPickUpTransports ActivityPickUpTransport[] userBucketInterests UserBucketInterested[] + activityMessages ActivityMessages[] @@map("activities") @@schema("act") } +model ActivityMessages { + id Int @id @default(autoincrement()) + activityXid Int @map("activity_xid") + activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) + senderXid Int @map("sender_xid") + sender User @relation("ActivityMessageSender", fields: [senderXid], references: [id], onDelete: Restrict) + receivedXid Int @map("received_xid") + received User @relation("ActivityMessageReceiver", fields: [receivedXid], references: [id], onDelete: Restrict) + message String @map("message") @db.VarChar(2000) + status String @default("unread") @map("status") @db.VarChar(30) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([activityXid, senderXid, receivedXid]) + @@map("activity_messages") + @@schema("act") +} + model ActivityOtherDetails { id Int @id @default(autoincrement()) activityXid Int @map("activity_xid") diff --git a/serverless/functions/host.yml b/serverless/functions/host.yml index ba81f1d..2a029b4 100644 --- a/serverless/functions/host.yml +++ b/serverless/functions/host.yml @@ -585,3 +585,33 @@ submitPQAnswer: - httpApi: path: /Activity_Hub/OnBoarding/submit-pq-answer method: patch + +sendHostChatMessage: + handler: src/modules/host/handlers/chat/sendMessage.handler + memorySize: 384 + package: + patterns: + - 'src/modules/host/**' + - ${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/chat/send-message + method: post + +getHostChatMessages: + handler: src/modules/host/handlers/chat/getMessages.handler + memorySize: 384 + package: + patterns: + - 'src/modules/host/**' + - ${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/chat/messages + method: get diff --git a/serverless/functions/user.yml b/serverless/functions/user.yml index 6c15442..33bb60f 100644 --- a/serverless/functions/user.yml +++ b/serverless/functions/user.yml @@ -557,3 +557,33 @@ getMatchingBucketInterestedActivities: - httpApi: path: /itinerary/get-matching-bucket-interested-activities method: post + +sendUserChatMessage: + handler: src/modules/user/handlers/chat/sendMessage.handler + memorySize: 384 + package: + patterns: + - 'src/modules/user/**' + - ${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: /chat/send-message + method: post + +getUserChatMessages: + handler: src/modules/user/handlers/chat/getMessages.handler + memorySize: 384 + package: + patterns: + - 'src/modules/user/**' + - ${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: /chat/messages + method: get diff --git a/src/common/services/chat.service.ts b/src/common/services/chat.service.ts new file mode 100644 index 0000000..4540ab5 --- /dev/null +++ b/src/common/services/chat.service.ts @@ -0,0 +1,150 @@ +import { PrismaClient } from '@prisma/client'; +import ApiError from '../utils/helper/ApiError'; + +interface SendMessageInput { + activityXid: number; + senderXid: number; + receiverXid: number; + message: string; + status?: string; +} + +interface GetMessagesInput { + activityXid: number; + userXid: number; + otherUserXid: number; + limit?: number; +} + +export class ChatService { + constructor(private prisma: PrismaClient) {} + + private async getHostUserIdForActivity(activityXid: number): Promise { + const activity = await this.prisma.activities.findUnique({ + where: { id: activityXid }, + select: { host: { select: { userXid: true } } }, + }); + + if (!activity) { + throw new ApiError(404, 'Activity not found'); + } + + const hostUserXid = activity.host?.userXid; + + if (!hostUserXid) { + throw new ApiError(400, 'Host user not found for activity'); + } + + return hostUserXid; + } + + async sendMessage(input: SendMessageInput) { + if (!input.activityXid || isNaN(input.activityXid)) { + throw new ApiError(400, 'Valid activityXid is required'); + } + + if (!input.senderXid || isNaN(input.senderXid)) { + throw new ApiError(400, 'Valid senderXid is required'); + } + + if (!input.receiverXid || isNaN(input.receiverXid)) { + throw new ApiError(400, 'Valid receiverXid is required'); + } + + if (input.senderXid === input.receiverXid) { + throw new ApiError(400, 'Sender and receiver cannot be the same'); + } + + const message = input.message?.trim(); + if (!message) { + throw new ApiError(400, 'Message is required'); + } + + const hostUserXid = await this.getHostUserIdForActivity(input.activityXid); + + if (input.senderXid !== hostUserXid && input.receiverXid !== hostUserXid) { + throw new ApiError( + 400, + 'Sender or receiver must be the host for this activity' + ); + } + + const receiverExists = await this.prisma.user.findUnique({ + where: { id: input.receiverXid }, + select: { id: true }, + }); + + if (!receiverExists) { + throw new ApiError(404, 'Receiver not found'); + } + + return this.prisma.activityMessages.create({ + data: { + activityXid: input.activityXid, + senderXid: input.senderXid, + receivedXid: input.receiverXid, + message, + status: input.status?.trim() || 'unread', + }, + }); + } + + async getMessages(input: GetMessagesInput) { + if (!input.activityXid || isNaN(input.activityXid)) { + throw new ApiError(400, 'Valid activityXid is required'); + } + + if (!input.userXid || isNaN(input.userXid)) { + throw new ApiError(400, 'Valid userXid is required'); + } + + if (!input.otherUserXid || isNaN(input.otherUserXid)) { + throw new ApiError(400, 'Valid otherUserXid is required'); + } + + if (input.userXid === input.otherUserXid) { + throw new ApiError(400, 'Invalid otherUserXid'); + } + + const hostUserXid = await this.getHostUserIdForActivity(input.activityXid); + + if (input.userXid !== hostUserXid && input.otherUserXid !== hostUserXid) { + throw new ApiError( + 400, + 'Conversation must include the host for this activity' + ); + } + + const limit = Math.min(Math.max(input.limit || 50, 1), 200); + + const messages = await this.prisma.activityMessages.findMany({ + where: { + activityXid: input.activityXid, + OR: [ + { + senderXid: input.userXid, + receivedXid: input.otherUserXid, + }, + { + senderXid: input.otherUserXid, + receivedXid: input.userXid, + }, + ], + }, + orderBy: { createdAt: 'asc' }, + take: limit, + }); + + await this.prisma.activityMessages.updateMany({ + where: { + activityXid: input.activityXid, + senderXid: input.otherUserXid, + receivedXid: input.userXid, + status: 'unread', + }, + data: { status: 'read' }, + }); + + return messages; + } +} diff --git a/src/modules/host/handlers/chat/getMessages.ts b/src/modules/host/handlers/chat/getMessages.ts new file mode 100644 index 0000000..1f5bd31 --- /dev/null +++ b/src/modules/host/handlers/chat/getMessages.ts @@ -0,0 +1,77 @@ +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from 'aws-lambda'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost'; +import { ChatService } from '../../../../common/services/chat.service'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../common/utils/helper/ApiError'; + +const chatService = new ChatService(prismaClient); + +export const handler = safeHandler( + async ( + event: APIGatewayProxyEvent, + context?: Context + ): Promise => { + 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.' + ); + } + + const userInfo = await verifyHostToken(token); + const hostUserId = Number(userInfo.id); + + if (!hostUserId || isNaN(hostUserId)) { + throw new ApiError(400, 'Invalid host user ID'); + } + + const activityXidParam = + event.queryStringParameters?.activityXid || + event.queryStringParameters?.activity_xid; + const otherUserXidParam = + event.queryStringParameters?.otherUserXid || + event.queryStringParameters?.other_user_xid || + event.queryStringParameters?.userXid || + event.queryStringParameters?.user_xid; + const limitParam = event.queryStringParameters?.limit; + + const activityXid = Number(activityXidParam); + const otherUserXid = Number(otherUserXidParam); + const limit = limitParam ? Number(limitParam) : undefined; + + if (!activityXid || isNaN(activityXid)) { + throw new ApiError(400, 'Valid activityXid is required'); + } + + if (!otherUserXid || isNaN(otherUserXid)) { + throw new ApiError(400, 'Valid otherUserXid is required'); + } + + const messages = await chatService.getMessages({ + activityXid, + userXid: hostUserId, + otherUserXid, + limit, + }); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Messages retrieved successfully', + data: messages, + }), + }; + } +); diff --git a/src/modules/host/handlers/chat/sendMessage.ts b/src/modules/host/handlers/chat/sendMessage.ts new file mode 100644 index 0000000..a5d2adf --- /dev/null +++ b/src/modules/host/handlers/chat/sendMessage.ts @@ -0,0 +1,78 @@ +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from 'aws-lambda'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost'; +import { ChatService } from '../../../../common/services/chat.service'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../common/utils/helper/ApiError'; + +const chatService = new ChatService(prismaClient); + +export const handler = safeHandler( + async ( + event: APIGatewayProxyEvent, + context?: Context + ): Promise => { + 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.' + ); + } + + const userInfo = await verifyHostToken(token); + const hostUserId = Number(userInfo.id); + + if (!hostUserId || isNaN(hostUserId)) { + throw new ApiError(400, 'Invalid host user ID'); + } + + let body: any; + try { + body = JSON.parse(event.body || '{}'); + } catch { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + const activityXid = Number(body.activityXid ?? body.activity_xid); + const receiverXid = Number( + body.receiverXid ?? body.receivedXid ?? body.received_xid + ); + const message = body.message; + const status = body.status; + + if (!activityXid || isNaN(activityXid)) { + throw new ApiError(400, 'Valid activityXid is required'); + } + + if (!receiverXid || isNaN(receiverXid)) { + throw new ApiError(400, 'Valid receiverXid is required'); + } + + const result = await chatService.sendMessage({ + activityXid, + senderXid: hostUserId, + receiverXid, + message, + status, + }); + + return { + statusCode: 201, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Message sent successfully', + data: result, + }), + }; + } +); diff --git a/src/modules/user/handlers/chat/getMessages.ts b/src/modules/user/handlers/chat/getMessages.ts new file mode 100644 index 0000000..10eb48b --- /dev/null +++ b/src/modules/user/handlers/chat/getMessages.ts @@ -0,0 +1,77 @@ +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from 'aws-lambda'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser'; +import { ChatService } from '../../../../common/services/chat.service'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../common/utils/helper/ApiError'; + +const chatService = new ChatService(prismaClient); + +export const handler = safeHandler( + async ( + event: APIGatewayProxyEvent, + context?: Context + ): Promise => { + 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.' + ); + } + + const userInfo = await verifyUserToken(token); + const userId = Number(userInfo.id); + + if (!userId || isNaN(userId)) { + throw new ApiError(400, 'Invalid user ID'); + } + + const activityXidParam = + event.queryStringParameters?.activityXid || + event.queryStringParameters?.activity_xid; + const otherUserXidParam = + event.queryStringParameters?.otherUserXid || + event.queryStringParameters?.other_user_xid || + event.queryStringParameters?.userXid || + event.queryStringParameters?.user_xid; + const limitParam = event.queryStringParameters?.limit; + + const activityXid = Number(activityXidParam); + const otherUserXid = Number(otherUserXidParam); + const limit = limitParam ? Number(limitParam) : undefined; + + if (!activityXid || isNaN(activityXid)) { + throw new ApiError(400, 'Valid activityXid is required'); + } + + if (!otherUserXid || isNaN(otherUserXid)) { + throw new ApiError(400, 'Valid otherUserXid is required'); + } + + const messages = await chatService.getMessages({ + activityXid, + userXid: userId, + otherUserXid, + limit, + }); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Messages retrieved successfully', + data: messages, + }), + }; + } +); diff --git a/src/modules/user/handlers/chat/sendMessage.ts b/src/modules/user/handlers/chat/sendMessage.ts new file mode 100644 index 0000000..fa5122a --- /dev/null +++ b/src/modules/user/handlers/chat/sendMessage.ts @@ -0,0 +1,78 @@ +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from 'aws-lambda'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser'; +import { ChatService } from '../../../../common/services/chat.service'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../common/utils/helper/ApiError'; + +const chatService = new ChatService(prismaClient); + +export const handler = safeHandler( + async ( + event: APIGatewayProxyEvent, + context?: Context + ): Promise => { + 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.' + ); + } + + const userInfo = await verifyUserToken(token); + const userId = Number(userInfo.id); + + if (!userId || isNaN(userId)) { + throw new ApiError(400, 'Invalid user ID'); + } + + let body: any; + try { + body = JSON.parse(event.body || '{}'); + } catch { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + const activityXid = Number(body.activityXid ?? body.activity_xid); + const receiverXid = Number( + body.receiverXid ?? body.receivedXid ?? body.received_xid + ); + const message = body.message; + const status = body.status; + + if (!activityXid || isNaN(activityXid)) { + throw new ApiError(400, 'Valid activityXid is required'); + } + + if (!receiverXid || isNaN(receiverXid)) { + throw new ApiError(400, 'Valid receiverXid is required'); + } + + const result = await chatService.sendMessage({ + activityXid, + senderXid: userId, + receiverXid, + message, + status, + }); + + return { + statusCode: 201, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Message sent successfully', + data: result, + }), + }; + } +);