feat: implement connection request management including schema, API endpoints, and business logic for users and agents.
This commit is contained in:
@@ -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: [
|
||||
|
||||
191
src/connection-requests/connection-requests.controller.ts
Normal file
191
src/connection-requests/connection-requests.controller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
12
src/connection-requests/connection-requests.module.ts
Normal file
12
src/connection-requests/connection-requests.module.ts
Normal 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 {}
|
||||
318
src/connection-requests/connection-requests.service.ts
Normal file
318
src/connection-requests/connection-requests.service.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
20
src/connection-requests/dto/create-connection-request.dto.ts
Normal file
20
src/connection-requests/dto/create-connection-request.dto.ts
Normal 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;
|
||||
}
|
||||
2
src/connection-requests/dto/index.ts
Normal file
2
src/connection-requests/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './create-connection-request.dto';
|
||||
export * from './respond-connection-request.dto';
|
||||
@@ -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';
|
||||
}
|
||||
3
src/connection-requests/index.ts
Normal file
3
src/connection-requests/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './connection-requests.module';
|
||||
export * from './connection-requests.service';
|
||||
export * from './connection-requests.controller';
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user