feat: Implement testimonials module with submission, link generation, and viewing functionalities.

This commit is contained in:
pradeepkumar
2026-03-05 05:36:36 +05:30
parent 0defcb24fb
commit 87671ce8b6
8 changed files with 263 additions and 0 deletions

View File

@@ -0,0 +1 @@
export * from './submit-testimonial.dto';

View File

@@ -0,0 +1,28 @@
import { IsString, IsInt, Min, Max, MaxLength, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SubmitTestimonialDto {
@ApiProperty({ description: 'Rating from 1 to 5', minimum: 1, maximum: 5 })
@IsInt()
@Min(1)
@Max(5)
rating: number;
@ApiProperty({ description: 'Testimonial text' })
@IsString()
@IsNotEmpty()
@MaxLength(2000)
text: string;
@ApiProperty({ description: 'Author name' })
@IsString()
@IsNotEmpty()
@MaxLength(100)
authorName: string;
@ApiProperty({ description: 'Author role, e.g. Home Buyer, Investor' })
@IsString()
@IsNotEmpty()
@MaxLength(100)
authorRole: string;
}

View File

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

View File

@@ -0,0 +1,84 @@
import {
Controller,
Get,
Post,
Body,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { TestimonialsService } from './testimonials.service';
import { SubmitTestimonialDto } 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 { Public } from '../auth/decorators/public.decorator';
import { UserRole } from '@prisma/client';
@ApiTags('Testimonials')
@Controller('testimonials')
export class TestimonialsController {
constructor(private readonly testimonialsService: TestimonialsService) {}
@Post('generate-link')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT)
@ApiBearerAuth()
@ApiOperation({ summary: 'Generate a shareable testimonial link' })
@ApiResponse({ status: 201, description: 'Link generated successfully' })
async generateLink(@CurrentUser() user: { id: string }) {
return this.testimonialsService.generateLink(user.id);
}
@Get('my')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT)
@ApiBearerAuth()
@ApiOperation({ summary: "Get agent's own testimonials" })
@ApiQuery({ name: 'sort', enum: ['latest', 'oldest'], required: false })
@ApiResponse({ status: 200, description: 'Testimonials retrieved successfully' })
async getMyTestimonials(
@CurrentUser() user: { id: string },
@Query('sort') sort?: 'latest' | 'oldest',
) {
return this.testimonialsService.getMyTestimonials(user.id, sort);
}
@Get('agent/:agentProfileId')
@Public()
@ApiOperation({ summary: 'Get published testimonials for an agent (public)' })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiResponse({ status: 200, description: 'Testimonials retrieved successfully' })
async getAgentTestimonials(
@Param('agentProfileId') agentProfileId: string,
@Query('limit') limit?: string,
) {
return this.testimonialsService.getAgentTestimonials(
agentProfileId,
limit ? parseInt(limit, 10) : undefined,
);
}
@Get('submit/:token')
@Public()
@ApiOperation({ summary: 'Get agent info for testimonial submission form' })
@ApiResponse({ status: 200, description: 'Agent info retrieved' })
@ApiResponse({ status: 404, description: 'Invalid token' })
async getAgentByToken(@Param('token') token: string) {
return this.testimonialsService.getAgentByToken(token);
}
@Post('submit/:token')
@Public()
@ApiOperation({ summary: 'Submit a testimonial for an agent' })
@ApiResponse({ status: 201, description: 'Testimonial submitted successfully' })
@ApiResponse({ status: 404, description: 'Invalid token' })
async submitTestimonial(
@Param('token') token: string,
@Body() dto: SubmitTestimonialDto,
) {
return this.testimonialsService.submitTestimonial(token, dto);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma';
import { TestimonialsController } from './testimonials.controller';
import { TestimonialsService } from './testimonials.service';
@Module({
imports: [PrismaModule],
controllers: [TestimonialsController],
providers: [TestimonialsService],
exports: [TestimonialsService],
})
export class TestimonialsModule {}

View File

@@ -0,0 +1,108 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { SubmitTestimonialDto } from './dto';
import { randomBytes } from 'crypto';
@Injectable()
export class TestimonialsService {
constructor(private readonly prisma: PrismaService) {}
async generateLink(userId: string) {
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { userId },
select: { id: true, testimonialToken: true },
});
if (!agentProfile) {
throw new NotFoundException('Agent profile not found');
}
// Generate a URL-safe token
const token = randomBytes(8).toString('base64url');
await this.prisma.agentProfile.update({
where: { id: agentProfile.id },
data: { testimonialToken: token },
});
return { token };
}
async getMyTestimonials(userId: string, sort: 'latest' | 'oldest' = 'latest') {
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { userId },
select: { id: true },
});
if (!agentProfile) {
throw new NotFoundException('Agent profile not found');
}
return this.prisma.testimonial.findMany({
where: { agentProfileId: agentProfile.id },
orderBy: { createdAt: sort === 'latest' ? 'desc' : 'asc' },
});
}
async getAgentTestimonials(agentProfileId: string, limit?: number) {
return this.prisma.testimonial.findMany({
where: { agentProfileId, isPublished: true },
orderBy: { createdAt: 'desc' },
...(limit ? { take: limit } : {}),
});
}
async getAgentByToken(token: string) {
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { testimonialToken: token },
select: {
id: true,
firstName: true,
lastName: true,
avatar: true,
agentType: { select: { name: true } },
},
});
if (!agentProfile) {
throw new NotFoundException('Invalid testimonial link');
}
return agentProfile;
}
async submitTestimonial(token: string, dto: SubmitTestimonialDto) {
const agentProfile = await this.prisma.agentProfile.findUnique({
where: { testimonialToken: token },
select: { id: true, totalReviews: true, averageRating: true },
});
if (!agentProfile) {
throw new NotFoundException('Invalid testimonial link');
}
// Create testimonial and update stats in a transaction
const [testimonial] = await this.prisma.$transaction([
this.prisma.testimonial.create({
data: {
agentProfileId: agentProfile.id,
rating: dto.rating,
text: dto.text,
authorName: dto.authorName,
authorRole: dto.authorRole,
},
}),
this.prisma.agentProfile.update({
where: { id: agentProfile.id },
data: {
totalReviews: agentProfile.totalReviews + 1,
averageRating:
(agentProfile.averageRating * agentProfile.totalReviews + dto.rating) /
(agentProfile.totalReviews + 1),
},
}),
]);
return testimonial;
}
}