feat: implement connection request management including schema, API endpoints, and business logic for users and agents.

This commit is contained in:
pradeepkumar
2026-02-02 09:59:25 +05:30
parent 497a1a4ba6
commit 93070474f1
10 changed files with 601 additions and 1 deletions

View File

@@ -14,6 +14,7 @@ import { AgentTypesModule } from './agent-types';
import { AgentsModule } from './agents';
import { UploadModule } from './upload';
import { ProfileFieldsModule } from './profile-fields';
import { ConnectionRequestsModule } from './connection-requests';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@@ -51,6 +52,7 @@ import { AppService } from './app.service';
AgentsModule,
UploadModule,
ProfileFieldsModule,
ConnectionRequestsModule,
],
controllers: [AppController],
providers: [

View File

@@ -0,0 +1,191 @@
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { ConnectionRequestsService } from './connection-requests.service';
import { CreateConnectionRequestDto, RespondConnectionRequestDto } from './dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { UserRole, ConnectionStatus } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
@ApiTags('Connection Requests')
@Controller('connection-requests')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth()
export class ConnectionRequestsController {
constructor(
private readonly connectionRequestsService: ConnectionRequestsService,
private readonly prisma: PrismaService,
) {}
@Post()
@Roles(UserRole.USER)
@ApiOperation({ summary: 'Create a connection request to an agent' })
@ApiResponse({ status: 201, description: 'Connection request created successfully' })
@ApiResponse({ status: 400, description: 'Agent is not available or invalid request' })
@ApiResponse({ status: 409, description: 'Connection request already exists' })
async createRequest(
@CurrentUser() user: { id: string },
@Body() dto: CreateConnectionRequestDto,
) {
// Return raw data - TransformInterceptor will wrap it
return this.connectionRequestsService.createRequest(user.id, dto);
}
@Get('my-requests')
@Roles(UserRole.USER)
@ApiOperation({ summary: 'Get all connection requests sent by the current user' })
@ApiQuery({ name: 'status', enum: ConnectionStatus, required: false })
@ApiResponse({ status: 200, description: 'Connection requests retrieved successfully' })
async getMyRequests(
@CurrentUser() user: { id: string },
@Query('status') status?: ConnectionStatus,
) {
// Return raw data - TransformInterceptor will wrap it
return this.connectionRequestsService.getMyRequests(user.id, status);
}
@Get('received')
@Roles(UserRole.AGENT)
@ApiOperation({ summary: 'Get all connection requests received by the current agent' })
@ApiQuery({ name: 'status', enum: ConnectionStatus, required: false })
@ApiResponse({ status: 200, description: 'Connection requests retrieved successfully' })
async getReceivedRequests(
@CurrentUser() user: { id: string },
@Query('status') status?: ConnectionStatus,
) {
// Get agent profile for the current user
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { userId: user.id },
select: { id: true },
});
if (!agentProfile) {
// Return empty array - TransformInterceptor will wrap it
return [];
}
// Return raw data - TransformInterceptor will wrap it
return this.connectionRequestsService.getReceivedRequests(
agentProfile.id,
status,
);
}
@Get('received/counts')
@Roles(UserRole.AGENT)
@ApiOperation({ summary: 'Get connection request counts for the current agent' })
@ApiResponse({ status: 200, description: 'Request counts retrieved successfully' })
async getRequestCounts(@CurrentUser() user: { id: string }) {
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { userId: user.id },
select: { id: true },
});
if (!agentProfile) {
// Return default counts - TransformInterceptor will wrap it
return { pending: 0, accepted: 0, rejected: 0, total: 0 };
}
// Return raw data - TransformInterceptor will wrap it
return this.connectionRequestsService.getRequestCounts(agentProfile.id);
}
@Get('status/:agentProfileId')
@Roles(UserRole.USER, UserRole.AGENT)
@ApiOperation({ summary: 'Get connection status between current user and an agent' })
@ApiResponse({ status: 200, description: 'Connection status retrieved successfully' })
async getConnectionStatus(
@CurrentUser() user: { id: string },
@Param('agentProfileId') agentProfileId: string,
) {
// Return raw data - TransformInterceptor will wrap it
return this.connectionRequestsService.getConnectionStatus(
user.id,
agentProfileId,
);
}
@Get(':id')
@ApiOperation({ summary: 'Get a specific connection request by ID' })
@ApiResponse({ status: 200, description: 'Connection request retrieved successfully' })
@ApiResponse({ status: 404, description: 'Connection request not found' })
async getRequestById(
@CurrentUser() user: { id: string; role: UserRole },
@Param('id') id: string,
) {
let agentProfileId: string | undefined;
if (user.role === UserRole.AGENT) {
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { userId: user.id },
select: { id: true },
});
agentProfileId = agentProfile?.id;
}
// Return raw data - TransformInterceptor will wrap it
return this.connectionRequestsService.getRequestById(
id,
user.id,
agentProfileId,
);
}
@Patch(':id/respond')
@Roles(UserRole.AGENT)
@ApiOperation({ summary: 'Respond to a connection request (accept/reject)' })
@ApiResponse({ status: 200, description: 'Response recorded successfully' })
@ApiResponse({ status: 400, description: 'Request already responded to' })
@ApiResponse({ status: 403, description: 'Not authorized to respond to this request' })
@ApiResponse({ status: 404, description: 'Connection request not found' })
async respondToRequest(
@CurrentUser() user: { id: string },
@Param('id') id: string,
@Body() dto: RespondConnectionRequestDto,
) {
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { userId: user.id },
select: { id: true },
});
if (!agentProfile) {
// Throw an error instead of returning non-standard response
throw new Error('Agent profile not found');
}
// Return raw data - TransformInterceptor will wrap it
return this.connectionRequestsService.respondToRequest(
id,
agentProfile.id,
dto,
);
}
@Delete(':id')
@Roles(UserRole.USER)
@ApiOperation({ summary: 'Cancel a pending connection request' })
@ApiResponse({ status: 200, description: 'Connection request cancelled successfully' })
@ApiResponse({ status: 400, description: 'Only pending requests can be cancelled' })
@ApiResponse({ status: 403, description: 'Not authorized to cancel this request' })
@ApiResponse({ status: 404, description: 'Connection request not found' })
async cancelRequest(
@CurrentUser() user: { id: string },
@Param('id') id: string,
) {
await this.connectionRequestsService.cancelRequest(id, user.id);
// Return null/undefined - TransformInterceptor will wrap it as { success: true, data: null }
return null;
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ConnectionRequestsController } from './connection-requests.controller';
import { ConnectionRequestsService } from './connection-requests.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [ConnectionRequestsController],
providers: [ConnectionRequestsService],
exports: [ConnectionRequestsService],
})
export class ConnectionRequestsModule {}

View File

@@ -0,0 +1,318 @@
import {
Injectable,
NotFoundException,
ConflictException,
ForbiddenException,
BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { ConnectionStatus } from '@prisma/client';
import { CreateConnectionRequestDto, RespondConnectionRequestDto } from './dto';
@Injectable()
export class ConnectionRequestsService {
constructor(private prisma: PrismaService) {}
/**
* Create a new connection request from user to agent
*/
async createRequest(userId: string, dto: CreateConnectionRequestDto) {
// Check if agent profile exists and is available
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { id: dto.agentProfileId },
select: {
id: true,
isAvailable: true,
isPublic: true,
firstName: true,
lastName: true,
userId: true,
},
});
if (!agentProfile) {
throw new NotFoundException('Agent profile not found');
}
if (!agentProfile.isPublic) {
throw new BadRequestException('This agent profile is not public');
}
if (!agentProfile.isAvailable) {
throw new BadRequestException('This agent is currently not available for connections');
}
// Prevent users from connecting to their own agent profile
if (agentProfile.userId === userId) {
throw new BadRequestException('You cannot send a connection request to yourself');
}
// Check for existing request
const existingRequest = await this.prisma.connectionRequest.findUnique({
where: {
userId_agentProfileId: {
userId,
agentProfileId: dto.agentProfileId,
},
},
});
if (existingRequest) {
if (existingRequest.status === ConnectionStatus.PENDING) {
throw new ConflictException('You already have a pending request to this agent');
}
if (existingRequest.status === ConnectionStatus.ACCEPTED) {
throw new ConflictException('You are already connected with this agent');
}
if (existingRequest.status === ConnectionStatus.REJECTED) {
// Allow re-requesting after rejection by updating the existing request
return this.prisma.connectionRequest.update({
where: { id: existingRequest.id },
data: {
status: ConnectionStatus.PENDING,
message: dto.message,
respondedAt: null,
updatedAt: new Date(),
},
include: {
agentProfile: {
select: {
id: true,
firstName: true,
lastName: true,
avatar: true,
},
},
},
});
}
}
// Create new connection request
return this.prisma.connectionRequest.create({
data: {
userId,
agentProfileId: dto.agentProfileId,
message: dto.message,
status: ConnectionStatus.PENDING,
},
include: {
agentProfile: {
select: {
id: true,
firstName: true,
lastName: true,
avatar: true,
},
},
},
});
}
/**
* Get all connection requests sent by a user
*/
async getMyRequests(userId: string, status?: ConnectionStatus) {
const where: { userId: string; status?: ConnectionStatus } = { userId };
if (status) {
where.status = status;
}
return this.prisma.connectionRequest.findMany({
where,
include: {
agentProfile: {
select: {
id: true,
firstName: true,
lastName: true,
avatar: true,
headline: true,
city: true,
state: true,
isVerified: true,
agentType: {
select: {
name: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' },
});
}
/**
* Get all connection requests received by an agent
*/
async getReceivedRequests(agentProfileId: string, status?: ConnectionStatus) {
const where: { agentProfileId: string; status?: ConnectionStatus } = { agentProfileId };
if (status) {
where.status = status;
}
return this.prisma.connectionRequest.findMany({
where,
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
},
orderBy: { createdAt: 'desc' },
});
}
/**
* Respond to a connection request (accept/reject)
*/
async respondToRequest(
requestId: string,
agentProfileId: string,
dto: RespondConnectionRequestDto,
) {
const request = await this.prisma.connectionRequest.findUnique({
where: { id: requestId },
});
if (!request) {
throw new NotFoundException('Connection request not found');
}
if (request.agentProfileId !== agentProfileId) {
throw new ForbiddenException('You are not authorized to respond to this request');
}
if (request.status !== ConnectionStatus.PENDING) {
throw new BadRequestException('This request has already been responded to');
}
return this.prisma.connectionRequest.update({
where: { id: requestId },
data: {
status: dto.status as ConnectionStatus,
respondedAt: new Date(),
},
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
},
});
}
/**
* Get a single connection request by ID
*/
async getRequestById(requestId: string, userId: string, agentProfileId?: string) {
const request = await this.prisma.connectionRequest.findUnique({
where: { id: requestId },
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentProfile: {
select: {
id: true,
firstName: true,
lastName: true,
avatar: true,
headline: true,
},
},
},
});
if (!request) {
throw new NotFoundException('Connection request not found');
}
// Check authorization - either the user who sent it or the agent who received it
const isUserRequest = request.userId === userId;
const isAgentRequest = agentProfileId && request.agentProfileId === agentProfileId;
if (!isUserRequest && !isAgentRequest) {
throw new ForbiddenException('You are not authorized to view this request');
}
return request;
}
/**
* Cancel a pending connection request (user only)
*/
async cancelRequest(requestId: string, userId: string) {
const request = await this.prisma.connectionRequest.findUnique({
where: { id: requestId },
});
if (!request) {
throw new NotFoundException('Connection request not found');
}
if (request.userId !== userId) {
throw new ForbiddenException('You are not authorized to cancel this request');
}
if (request.status !== ConnectionStatus.PENDING) {
throw new BadRequestException('Only pending requests can be cancelled');
}
return this.prisma.connectionRequest.delete({
where: { id: requestId },
});
}
/**
* Get connection status between a user and an agent
*/
async getConnectionStatus(userId: string, agentProfileId: string) {
const request = await this.prisma.connectionRequest.findUnique({
where: {
userId_agentProfileId: {
userId,
agentProfileId,
},
},
select: {
id: true,
status: true,
createdAt: true,
respondedAt: true,
},
});
return request || null;
}
/**
* Get counts for agent's connection requests
*/
async getRequestCounts(agentProfileId: string) {
const [pending, accepted, rejected] = await Promise.all([
this.prisma.connectionRequest.count({
where: { agentProfileId, status: ConnectionStatus.PENDING },
}),
this.prisma.connectionRequest.count({
where: { agentProfileId, status: ConnectionStatus.ACCEPTED },
}),
this.prisma.connectionRequest.count({
where: { agentProfileId, status: ConnectionStatus.REJECTED },
}),
]);
return { pending, accepted, rejected, total: pending + accepted + rejected };
}
}

View File

@@ -0,0 +1,20 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsUUID, MaxLength } from 'class-validator';
export class CreateConnectionRequestDto {
@ApiProperty({
example: 'uuid-agent-profile-id',
description: 'The ID of the agent profile to connect with',
})
@IsUUID('4')
agentProfileId: string;
@ApiPropertyOptional({
example: 'Hi, I am interested in your services for buying a home.',
description: 'Optional message to send with the connection request',
})
@IsOptional()
@IsString()
@MaxLength(1000)
message?: string;
}

View File

@@ -0,0 +1,2 @@
export * from './create-connection-request.dto';
export * from './respond-connection-request.dto';

View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn } from 'class-validator';
export class RespondConnectionRequestDto {
@ApiProperty({
enum: ['ACCEPTED', 'REJECTED'],
example: 'ACCEPTED',
description: 'The response status (ACCEPTED or REJECTED)',
})
@IsIn(['ACCEPTED', 'REJECTED'], {
message: 'Status must be ACCEPTED or REJECTED',
})
status: 'ACCEPTED' | 'REJECTED';
}

View File

@@ -0,0 +1,3 @@
export * from './connection-requests.module';
export * from './connection-requests.service';
export * from './connection-requests.controller';

View File

@@ -228,7 +228,8 @@ export class ProfileFieldsService {
await this.prisma.$transaction(operations);
return { success: true, updated: updates.length };
// Return raw data - TransformInterceptor will wrap it
return { updated: updates.length };
}
/**