diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d6b13af..fbb44ef 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -161,6 +161,9 @@ model User { reportsSubmitted UserReport[] @relation("ReportsSubmitted") reportsReceived UserReport[] @relation("ReportsReceived") + // Verification actions (admin) + verificationActions VerificationHistory[] @relation("VerificationActions") + @@index([email]) @@index([role]) @@index([status]) @@ -275,6 +278,9 @@ model AgentProfile { @@index([agentTypeId]) @@index([slug]) @@index([city, state]) + // Verification History + verificationHistory VerificationHistory[] + @@index([isVerified]) @@index([verificationStatus]) @@index([isPublic]) @@ -761,3 +767,22 @@ model ContactMessage { @@index([createdAt]) @@map("contact_messages") } + +// ============================================ +// VERIFICATION HISTORY +// ============================================ +model VerificationHistory { + id String @id @default(uuid()) + agentProfileId String + status VerificationStatus + note String? + adminId String? + createdAt DateTime @default(now()) + + agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) + admin User? @relation("VerificationActions", fields: [adminId], references: [id]) + + @@index([agentProfileId]) + @@index([createdAt]) + @@map("verification_history") +} diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index 599d9e9..f82005a 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -473,4 +473,55 @@ export class NotificationsService implements OnModuleInit { }, ); } + + @OnEvent('notification.verification') + async handleVerificationNotification(event: { + recipientUserId: string; + status: string; + note?: string; + agentName: string; + }) { + const isApproved = event.status === 'APPROVED'; + const actionUrl = '/agent/dashboard'; + + const title = isApproved + ? 'Profile Verified!' + : 'Profile Verification Update'; + + const description = isApproved + ? 'Congratulations! Your profile has been verified. You now have the verified badge on your profile.' + : `Your profile verification was not approved.${event.note ? ` Reason: ${event.note}` : ''} Please review and resubmit.`; + + // Create in-app notification + push (always for verification) + const user = await this.prisma.user.findUnique({ + where: { id: event.recipientUserId }, + select: { email: true, fcmTokens: true }, + }); + + if (!user) return; + + // Always create in-app notification + await this.prisma.notification.create({ + data: { + userId: event.recipientUserId, + type: 'system', + title, + description, + actionUrl, + data: { type: 'verification', status: event.status, link: actionUrl }, + }, + }); + + // Always send push notification + await this.sendPushNotification(event.recipientUserId, user, { + title, + body: description, + data: { type: 'verification', status: event.status, link: actionUrl }, + }); + + // Always send email — forced, ignores preferences + if (user.email) { + await this.sendEmailNotification(user.email, title, description, actionUrl); + } + } } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index fbc657d..2c0f896 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -330,6 +330,16 @@ export class UsersController { return this.usersService.updateVerification(id, data, admin.id); } + @Get(':id/verification-history') + @Roles(UserRole.ADMIN) + @ApiOperation({ summary: 'Admin: Get verification history for an agent' }) + async getVerificationHistory(@Param('id') id: string) { + const user = await this.usersService.findOne(id); + const agentProfileId = (user as any)?.agentProfile?.id; + if (!agentProfileId) throw new NotFoundException('Agent profile not found'); + return this.usersService.getVerificationHistory(agentProfileId); + } + // ========================================== // ADMIN MANAGEMENT (SUPER_ADMIN only) // ========================================== diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 1a66aa6..b065d42 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,6 +1,7 @@ import { Injectable, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; import { UserStatus, UserRole, Prisma, VerificationStatus } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import * as argon2 from 'argon2'; type UserWithProfiles = Prisma.UserGetPayload<{ @@ -9,7 +10,10 @@ type UserWithProfiles = Prisma.UserGetPayload<{ @Injectable() export class UsersService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly eventEmitter: EventEmitter2, + ) {} // ============================================= // USER PROFILE METHODS (for regular users) @@ -390,6 +394,24 @@ export class UsersService { }, }); + // Save verification history + await this.prisma.verificationHistory.create({ + data: { + agentProfileId: user.agentProfile.id, + status: data.status, + note: data.note || null, + adminId, + }, + }); + + // Emit notification to the agent + this.eventEmitter.emit('notification.verification', { + recipientUserId: userId, + status: data.status, + note: data.note, + agentName: `${updatedProfile.firstName || ''} ${updatedProfile.lastName || ''}`.trim(), + }); + return { id: user.id, agentProfileId: updatedProfile.id, @@ -402,6 +424,17 @@ export class UsersService { }; } + // Get verification history for an agent + async getVerificationHistory(agentProfileId: string) { + return this.prisma.verificationHistory.findMany({ + where: { agentProfileId }, + include: { + admin: { select: { id: true, email: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + } + // ============================================= // NOTIFICATION PREFERENCES // =============================================