diff --git a/issue-891-test-files-fix.patch b/issue-891-test-files-fix.patch new file mode 100644 index 00000000..945e3d6e --- /dev/null +++ b/issue-891-test-files-fix.patch @@ -0,0 +1,838 @@ +diff --git a/src/admin/admin-audit.interceptor.spec.ts b/src/admin/admin-audit.interceptor.spec.ts +index a412ada..ccf2d3f 100644 +--- a/src/admin/admin-audit.interceptor.spec.ts ++++ b/src/admin/admin-audit.interceptor.spec.ts +@@ -1,10 +1,17 @@ + import { AdminAuditInterceptor } from './admin-audit.interceptor'; + import { ExecutionContext, CallHandler } from '@nestjs/common'; + import { of } from 'rxjs'; ++import { PrismaService } from '../database/prisma.service'; ++ ++interface MockPrisma { ++ activityLog: { ++ create: jest.Mock; ++ }; ++} + + describe('AdminAuditInterceptor', () => { + let interceptor: AdminAuditInterceptor; +- let mockPrisma: any; ++ let mockPrisma: MockPrisma; + + beforeEach(() => { + mockPrisma = { +@@ -12,10 +19,10 @@ describe('AdminAuditInterceptor', () => { + create: jest.fn().mockResolvedValue({}), + }, + }; +- interceptor = new AdminAuditInterceptor(mockPrisma); ++ interceptor = new AdminAuditInterceptor(mockPrisma as unknown as PrismaService); + }); + +- function makeContext(overrides: Partial = {}): ExecutionContext { ++ function makeContext(overrides: Record = {}): ExecutionContext { + const request = { + authUser: { sub: 'admin-1' }, + headers: { 'user-agent': 'test-agent' }, +diff --git a/src/auth/login-rate-limit.service.spec.ts b/src/auth/login-rate-limit.service.spec.ts +index 7965bc1..a708f97 100644 +--- a/src/auth/login-rate-limit.service.spec.ts ++++ b/src/auth/login-rate-limit.service.spec.ts +@@ -1,8 +1,18 @@ + import { LoginRateLimitService } from './login-rate-limit.service'; ++import { PrismaService } from '../database/prisma.service'; ++ ++interface MockPrisma { ++ loginAttempt: { ++ findFirst: jest.Mock; ++ count: jest.Mock; ++ create: jest.Mock; ++ updateMany: jest.Mock; ++ }; ++} + + describe('LoginRateLimitService', () => { + let service: LoginRateLimitService; +- let mockPrisma: any; ++ let mockPrisma: MockPrisma; + + const email = 'test@example.com'; + const ip = '1.2.3.4'; +@@ -16,7 +26,7 @@ describe('LoginRateLimitService', () => { + updateMany: jest.fn().mockResolvedValue({ count: 1 }), + }, + }; +- service = new LoginRateLimitService(mockPrisma); ++ service = new LoginRateLimitService(mockPrisma as unknown as PrismaService); + }); + + describe('isAccountLocked', () => { +diff --git a/src/auth/rate-limit.guard.spec.ts b/src/auth/rate-limit.guard.spec.ts +index 1eb827d..e51b812 100644 +--- a/src/auth/rate-limit.guard.spec.ts ++++ b/src/auth/rate-limit.guard.spec.ts +@@ -3,7 +3,7 @@ import { RateLimitService } from './rate-limit.service'; + import { ExecutionContext, HttpException } from '@nestjs/common'; + import { Reflector } from '@nestjs/core'; + +-function makeContext(overrides: Partial = {}): ExecutionContext { ++function makeContext(overrides: Record = {}): ExecutionContext { + const request = { + method: 'POST', + url: '/auth/login', +@@ -40,7 +40,7 @@ describe('RateLimitGuard - auth/signup endpoints', () => { + checkUserRateLimit: jest.fn().mockResolvedValue(notExceeded), + checkEndpointRateLimit: jest.fn().mockResolvedValue({ ...notExceeded, limit: 0 }), + getHeaders: jest.fn().mockReturnValue({}), +- } as any; ++ } as unknown as jest.Mocked; + + guard = new RateLimitGuard(reflector, rateLimitService); + }); +@@ -68,10 +68,11 @@ describe('RateLimitGuard - auth/signup endpoints', () => { + try { + await guard.canActivate(ctx); + fail('should have thrown'); +- } catch (e: any) { ++ } catch (e: unknown) { + expect(e).toBeInstanceOf(HttpException); +- expect(e.getStatus()).toBe(429); +- const body = e.getResponse(); ++ const httpException = e as HttpException; ++ expect(httpException.getStatus()).toBe(429); ++ const body = httpException.getResponse(); + expect(body).toMatchObject({ retryAfter: 60 }); + } + }); +diff --git a/src/auth/rbac.spec.ts b/src/auth/rbac.spec.ts +index 37451be..accc20d 100644 +--- a/src/auth/rbac.spec.ts ++++ b/src/auth/rbac.spec.ts +@@ -3,13 +3,14 @@ + * Tests that USER, AGENT, and ADMIN roles are correctly enforced. + */ + import { Test, TestingModule } from '@nestjs/testing'; +-import { ForbiddenException } from '@nestjs/common'; ++import { ForbiddenException, ExecutionContext } from '@nestjs/common'; + import { Reflector } from '@nestjs/core'; + import { RolesGuard } from '../auth/guards/roles.guard'; + import { AuthUserPayload } from '../auth/types/auth-user.type'; + import { UserRole } from '../types/prisma.types'; ++import { CreatePropertyDto } from '../properties/dto/property.dto'; + +-function makeContext(role: UserRole, requiredRoles: UserRole[] | null) { ++function makeContext(role: UserRole, requiredRoles: UserRole[] | null): ExecutionContext { + const user: AuthUserPayload = { sub: 'u1', email: 'u@test.com', role, type: 'access' }; + const request = { authUser: user }; + return { +@@ -17,7 +18,7 @@ function makeContext(role: UserRole, requiredRoles: UserRole[] | null) { + getHandler: () => ({}), + getClass: () => ({}), + _requiredRoles: requiredRoles, +- }; ++ } as unknown as ExecutionContext; + } + + describe('RolesGuard - RBAC enforcement', () => { +@@ -41,19 +42,19 @@ describe('RolesGuard - RBAC enforcement', () => { + it('allows ADMIN to access admin routes', () => { + setupRoles([UserRole.ADMIN]); + const ctx = makeContext(UserRole.ADMIN, [UserRole.ADMIN]); +- expect(guard.canActivate(ctx as any)).toBe(true); ++ expect(guard.canActivate(ctx)).toBe(true); + }); + + it('rejects USER from admin routes', () => { + setupRoles([UserRole.ADMIN]); + const ctx = makeContext(UserRole.USER, [UserRole.ADMIN]); +- expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); ++ expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + + it('rejects AGENT from admin routes', () => { + setupRoles([UserRole.ADMIN]); + const ctx = makeContext(UserRole.AGENT, [UserRole.ADMIN]); +- expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); ++ expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + }); + +@@ -61,19 +62,19 @@ describe('RolesGuard - RBAC enforcement', () => { + it('allows AGENT to access agent+admin routes', () => { + setupRoles([UserRole.AGENT, UserRole.ADMIN]); + const ctx = makeContext(UserRole.AGENT, [UserRole.AGENT, UserRole.ADMIN]); +- expect(guard.canActivate(ctx as any)).toBe(true); ++ expect(guard.canActivate(ctx)).toBe(true); + }); + + it('allows ADMIN to access agent+admin routes', () => { + setupRoles([UserRole.AGENT, UserRole.ADMIN]); + const ctx = makeContext(UserRole.ADMIN, [UserRole.AGENT, UserRole.ADMIN]); +- expect(guard.canActivate(ctx as any)).toBe(true); ++ expect(guard.canActivate(ctx)).toBe(true); + }); + + it('rejects USER from agent+admin routes', () => { + setupRoles([UserRole.AGENT, UserRole.ADMIN]); + const ctx = makeContext(UserRole.USER, [UserRole.AGENT, UserRole.ADMIN]); +- expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); ++ expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + }); + +@@ -81,7 +82,7 @@ describe('RolesGuard - RBAC enforcement', () => { + it('allows any authenticated user when no roles are required', () => { + setupRoles(null); + const ctx = makeContext(UserRole.USER, null); +- expect(guard.canActivate(ctx as any)).toBe(true); ++ expect(guard.canActivate(ctx)).toBe(true); + }); + }); + +@@ -93,8 +94,8 @@ describe('RolesGuard - RBAC enforcement', () => { + switchToHttp: () => ({ getRequest: () => request }), + getHandler: () => ({}), + getClass: () => ({}), +- }; +- expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); ++ } as unknown as ExecutionContext; ++ expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + + it('throws ForbiddenException when no user in request', () => { +@@ -103,8 +104,8 @@ describe('RolesGuard - RBAC enforcement', () => { + switchToHttp: () => ({ getRequest: () => ({}) }), + getHandler: () => ({}), + getClass: () => ({}), +- }; +- expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); ++ } as unknown as ExecutionContext; ++ expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + }); + }); +@@ -163,7 +164,7 @@ describe('PropertiesController - RBAC', () => { + zipCode: '10001', + price: 100000, + propertyType: 'House', +- } as any; ++ } as CreatePropertyDto; + const result = await controller.create(dto, regularUser); + expect(result).toEqual({ id: 'p1' }); + expect(service.create).toHaveBeenCalledWith(dto, regularUser.sub); +@@ -178,7 +179,7 @@ describe('PropertiesController - RBAC', () => { + zipCode: '10001', + price: 200000, + propertyType: 'Condo', +- } as any; ++ } as CreatePropertyDto; + await controller.create(dto, adminUser); + expect(service.create).toHaveBeenCalledWith(dto, adminUser.sub); + }); +@@ -201,19 +202,19 @@ describe('AdminController - RBAC (guard integration)', () => { + it('ADMIN can access admin dashboard', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.ADMIN]); + const ctx = makeContext(UserRole.ADMIN, [UserRole.ADMIN]); +- expect(guard.canActivate(ctx as any)).toBe(true); ++ expect(guard.canActivate(ctx)).toBe(true); + }); + + it('USER cannot access admin dashboard', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.ADMIN]); + const ctx = makeContext(UserRole.USER, [UserRole.ADMIN]); +- expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); ++ expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + + it('AGENT cannot access admin dashboard', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.ADMIN]); + const ctx = makeContext(UserRole.AGENT, [UserRole.ADMIN]); +- expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); ++ expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + }); + +@@ -233,18 +234,18 @@ describe('TransactionsController - RBAC (guard integration)', () => { + it('AGENT can create transactions', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.AGENT, UserRole.ADMIN]); + const ctx = makeContext(UserRole.AGENT, [UserRole.AGENT, UserRole.ADMIN]); +- expect(guard.canActivate(ctx as any)).toBe(true); ++ expect(guard.canActivate(ctx)).toBe(true); + }); + + it('ADMIN can create transactions', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.AGENT, UserRole.ADMIN]); + const ctx = makeContext(UserRole.ADMIN, [UserRole.AGENT, UserRole.ADMIN]); +- expect(guard.canActivate(ctx as any)).toBe(true); ++ expect(guard.canActivate(ctx)).toBe(true); + }); + + it('USER cannot create transactions', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.AGENT, UserRole.ADMIN]); + const ctx = makeContext(UserRole.USER, [UserRole.AGENT, UserRole.ADMIN]); +- expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); ++ expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + }); +diff --git a/src/blockchain/blockchain.service.spec.ts b/src/blockchain/blockchain.service.spec.ts +index 751ec9c..20940a8 100644 +--- a/src/blockchain/blockchain.service.spec.ts ++++ b/src/blockchain/blockchain.service.spec.ts +@@ -15,8 +15,8 @@ describe('BlockchainService', () => { + let prismaService: PrismaService; + + const mockConfigService = { +- get: jest.fn((key: string, defaultValue?: any) => { +- const config: Record = { ++ get: jest.fn((key: string, defaultValue?: unknown) => { ++ const config: Record = { + BLOCKCHAIN_ENABLED: 'true', + BLOCKCHAIN_NETWORK: BlockchainNetwork.SEPOLIA, + BLOCKCHAIN_RPC_URL: 'https://sepolia.infura.io/v3/test', +@@ -248,8 +248,8 @@ describe('BlockchainService', () => { + it('should handle disabled blockchain by recording locally', async () => { + // Override config to disable blockchain + const disabledConfig = { +- get: jest.fn((key: string, defaultValue?: any) => { +- const config: Record = { ++ get: jest.fn((key: string, defaultValue?: unknown) => { ++ const config: Record = { + BLOCKCHAIN_ENABLED: 'false', + BLOCKCHAIN_NETWORK: BlockchainNetwork.SEPOLIA, + }; +diff --git a/src/commissions/commissions.service.spec.ts b/src/commissions/commissions.service.spec.ts +index 76c89bb..0c0014a 100644 +--- a/src/commissions/commissions.service.spec.ts ++++ b/src/commissions/commissions.service.spec.ts +@@ -6,7 +6,6 @@ import { Decimal } from '@prisma/client/runtime/library'; + + describe('CommissionsService', () => { + let service: CommissionsService; +- let prisma: PrismaService; + + const mockPrismaService = { + transaction: { +@@ -28,7 +27,6 @@ describe('CommissionsService', () => { + }).compile(); + + service = module.get(CommissionsService); +- prisma = module.get(PrismaService); + }); + + afterEach(() => { +@@ -53,8 +51,8 @@ describe('CommissionsService', () => { + + await service.createCommissionsForTransaction('tx-1'); + +- expect((prisma as any).commission.create).toHaveBeenCalledTimes(2); +- expect((prisma as any).commission.create).toHaveBeenNthCalledWith( ++ expect(mockPrismaService.commission.create).toHaveBeenCalledTimes(2); ++ expect(mockPrismaService.commission.create).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + data: { +@@ -67,7 +65,7 @@ describe('CommissionsService', () => { + }, + }), + ); +- expect((prisma as any).commission.create).toHaveBeenNthCalledWith( ++ expect(mockPrismaService.commission.create).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + data: { +@@ -89,7 +87,7 @@ describe('CommissionsService', () => { + + await service.updateCommissionsStatus('tx-1', 'COMPLETED'); + +- expect((prisma as any).commission.updateMany).toHaveBeenCalledWith({ ++ expect(mockPrismaService.commission.updateMany).toHaveBeenCalledWith({ + where: { transactionId: 'tx-1' }, + data: { status: 'COMPLETED' }, + }); +@@ -106,7 +104,7 @@ describe('CommissionsService', () => { + { sub: 'agent-1', email: 'agent@test.com', role: 'AGENT', type: 'access' }, + ); + +- expect((prisma as any).commission.findMany).toHaveBeenCalledWith( ++ expect(mockPrismaService.commission.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ agentId: 'agent-1' }), + }), +diff --git a/src/documents/documents-download.controller.spec.ts b/src/documents/documents-download.controller.spec.ts +index 729c60e..d5da61e 100644 +--- a/src/documents/documents-download.controller.spec.ts ++++ b/src/documents/documents-download.controller.spec.ts +@@ -7,6 +7,7 @@ import { RolesGuard } from '../auth/guards/roles.guard'; + import { AuthUserPayload } from '../auth/types/auth-user.type'; + import { UserRole } from '../types/prisma.types'; + import { Response } from 'express'; ++import { RequestSignedUploadDto } from './dto/document-access.dto'; + + describe('DocumentsDownloadController', () => { + let controller: DocumentsDownloadController; +@@ -109,7 +110,7 @@ describe('DocumentsDownloadController', () => { + fileName: 'test.pdf', + mimeType: 'application/pdf', + fileSizeBytes: 1024 +- } as any; ++ } as RequestSignedUploadDto; + + const mockSignedUrlResponse = { + url: 'http://signed.url/upload', +diff --git a/src/documents/documents.controller.spec.ts b/src/documents/documents.controller.spec.ts +index 65d7323..7270ba3 100644 +--- a/src/documents/documents.controller.spec.ts ++++ b/src/documents/documents.controller.spec.ts +@@ -6,6 +6,7 @@ import { UserRole } from '../types/prisma.types'; + import { Response } from 'express'; + import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; + import { RolesGuard } from '../auth/guards/roles.guard'; ++import { CreateDocumentDto, UpdateDocumentDto, SignDocumentDto } from './dto/document.dto'; + + describe('DocumentsController', () => { + let controller: DocumentsController; +@@ -51,7 +52,7 @@ describe('DocumentsController', () => { + + it('create', async () => { + mockDocumentsService.create.mockResolvedValue('created'); +- expect(await controller.create({} as any, mockUser)).toBe('created'); ++ expect(await controller.create({} as CreateDocumentDto, mockUser)).toBe('created'); + }); + + it('findAll', async () => { +@@ -67,7 +68,7 @@ describe('DocumentsController', () => { + it('update', async () => { + mockDocumentsService.findAuthorizedById.mockResolvedValue('doc'); + mockDocumentsService.update.mockResolvedValue('updated'); +- expect(await controller.update('id', {} as any, mockUser)).toBe('updated'); ++ expect(await controller.update('id', {} as UpdateDocumentDto, mockUser)).toBe('updated'); + }); + + it('remove', async () => { +@@ -89,7 +90,7 @@ describe('DocumentsController', () => { + it('sign', async () => { + mockDocumentsService.findAuthorizedById.mockResolvedValue('doc'); + mockDocumentsService.signDocument.mockResolvedValue('signed'); +- expect(await controller.sign('id', {} as any, mockUser)).toBe('signed'); ++ expect(await controller.sign('id', {} as SignDocumentDto, mockUser)).toBe('signed'); + }); + + it('bulkDownload', async () => { +diff --git a/src/documents/documents.service.spec.ts b/src/documents/documents.service.spec.ts +index dd59e6f..7de6201 100644 +--- a/src/documents/documents.service.spec.ts ++++ b/src/documents/documents.service.spec.ts +@@ -1,8 +1,10 @@ + import { Test, TestingModule } from '@nestjs/testing'; + import { ForbiddenException, NotFoundException } from '@nestjs/common'; + import { PassThrough } from 'stream'; ++import { Response } from 'express'; + import { DocumentsService } from './documents.service'; + import { PrismaService } from '../database/prisma.service'; ++import { CreateDocumentDto, UpdateDocumentDto, SignDocumentDto } from './dto/document.dto'; + + describe('DocumentsService', () => { + let service: DocumentsService; +@@ -125,16 +127,22 @@ describe('DocumentsService', () => { + + describe('bulkDownload', () => { + /** Create a mock writable stream that acts like an Express Response */ +- function mockStreamRes() { +- const stream = new PassThrough(); +- (stream as any).setHeader = jest.fn(); +- (stream as any).setHeaders = jest.fn(); ++ function mockStreamRes(): Response { ++ const stream = new PassThrough() as PassThrough & { ++ setHeader: jest.Mock; ++ setHeaders: jest.Mock; ++ }; ++ stream.setHeader = jest.fn(); ++ stream.setHeaders = jest.fn(); + // Suppress the 'data' events to avoid jest hanging +- jest.spyOn(stream, 'pipe').mockImplementation(function (this: any, dest: any) { ++ jest.spyOn(stream, 'pipe').mockImplementation(function ( ++ this: PassThrough, ++ dest: NodeJS.WritableStream & { end: (chunk: string) => void }, ++ ) { + dest.end('fake-zip-data'); +- return dest; ++ return dest as unknown as ReturnType; + }); +- return stream as any; ++ return stream as unknown as Response; + } + + it('should throw NotFoundException when no documents found', async () => { +@@ -219,7 +227,7 @@ describe('DocumentsService', () => { + it('create', async () => { + mockPrismaService.document.create.mockResolvedValue({ id: 'doc-1' }); + const result = await service.create( +- { documentType: 'TITLE_DEED', propertyId: 'prop-1' } as any, ++ { documentType: 'TITLE_DEED', propertyId: 'prop-1' } as CreateDocumentDto, + 'user-1', + ); + expect(result.id).toBe('doc-1'); +@@ -249,7 +257,7 @@ describe('DocumentsService', () => { + it('update', async () => { + mockPrismaService.document.findUnique.mockResolvedValue({ id: 'doc-1' }); + mockPrismaService.document.update.mockResolvedValue({ id: 'doc-1', status: 'VERIFIED' }); +- expect(await service.update('doc-1', { status: 'VERIFIED' } as any)).toHaveProperty('status', 'VERIFIED'); ++ expect(await service.update('doc-1', { status: 'VERIFIED' } as UpdateDocumentDto)).toHaveProperty('status', 'VERIFIED'); + }); + + it('remove', async () => { +@@ -261,8 +269,8 @@ describe('DocumentsService', () => { + + describe('Extended Coverage Operations - Branches', () => { + it('should execute methods with alternate parameters to trigger secondary branches', async () => { +- const safeExec = async (promise: any) => { +- try { await promise; } catch (e) {} ++ const safeExec = async (promise: Promise) => { ++ try { await promise; } catch (e) { /* expected in some branches */ } + }; + + // Execute standard paths +@@ -273,17 +281,17 @@ describe('DocumentsService', () => { + await safeExec(service.deleteExpired()); + await safeExec(service.flagExpiryNotified('doc-1')); + // Fixed: Removed the 3rd argument +- await safeExec(service.signDocument('doc-1', { signature: 'test' } as any)); ++ await safeExec(service.signDocument('doc-1', { signature: 'test' } as unknown as SignDocumentDto)); + // Fixed: Removed the 2nd argument + await safeExec(service.verifySignature('doc-1')); + + // Execute alternate paths (missing users, admin roles, null parameters) +- await safeExec(service.getVersions('doc-1', null as any, 'ADMIN')); +- await safeExec(service.getVersion('doc-1', 'v1', null as any, 'ADMIN')); +- await safeExec(service.findAuthorizedById('doc-1', null as any, null as any)); +- await safeExec(service.findAll(null as any, { category: 'legal' } as any, 'ADMIN')); ++ await safeExec(service.getVersions('doc-1', null as unknown as string, 'ADMIN')); ++ await safeExec(service.getVersion('doc-1', 'v1', null as unknown as string, 'ADMIN')); ++ await safeExec(service.findAuthorizedById('doc-1', null as unknown as string, null as unknown as string)); ++ await safeExec(service.findAll(null as unknown as string, { category: 'legal' }, 'ADMIN')); + // Fixed: Removed the 3rd argument +- await safeExec(service.signDocument('doc-1', {} as any)); ++ await safeExec(service.signDocument('doc-1', {} as unknown as SignDocumentDto)); + }); + }); + +diff --git a/src/documents/signed-url/signed-url-providers.spec.ts b/src/documents/signed-url/signed-url-providers.spec.ts +index 86f1152..f478f03 100644 +--- a/src/documents/signed-url/signed-url-providers.spec.ts ++++ b/src/documents/signed-url/signed-url-providers.spec.ts +@@ -2,9 +2,10 @@ import { AzureSignedUrlProvider } from './azure-signed-url-provider'; + import { GcsSignedUrlProvider } from './gcs-signed-url-provider'; + import { NotConfiguredSignedUrlProvider } from './not-configured.signed-url-provider'; + import { S3SignedUrlProvider } from './s3-signed-url-provider'; ++import { SignedUrlRequest } from './signed-url-provider.interface'; + + describe('Signed URL Providers', () => { +- const mockPayload = { operation: 'download', objectKey: 'test.pdf' } as any; ++ const mockPayload = { operation: 'download', objectKey: 'test.pdf' } as SignedUrlRequest; + + it('AzureSignedUrlProvider should execute getSignedUrl', async () => { + const provider = new AzureSignedUrlProvider(); +diff --git a/src/documents/signed-url/signed-url.service.spec.ts b/src/documents/signed-url/signed-url.service.spec.ts +index 54aaeb5..7d3fc08 100644 +--- a/src/documents/signed-url/signed-url.service.spec.ts ++++ b/src/documents/signed-url/signed-url.service.spec.ts +@@ -1,5 +1,6 @@ + import { Test, TestingModule } from '@nestjs/testing'; + import { SignedUrlService } from './signed-url.service'; ++import { SignedUrlRequest, SignedUrlResponse } from './signed-url-provider.interface'; + + describe('SignedUrlService', () => { + let service: SignedUrlService; +@@ -31,8 +32,8 @@ describe('SignedUrlService', () => { + + describe('getSignedUrl', () => { + it('should delegate to the injected provider', async () => { +- const payload = { operation: 'download' as any, objectKey: 'test.pdf' }; +- mockProvider.getSignedUrl.mockResolvedValue({ url: 'http://signed' } as any); ++ const payload = { operation: 'download', objectKey: 'test.pdf' } as SignedUrlRequest; ++ mockProvider.getSignedUrl.mockResolvedValue({ url: 'http://signed' } as SignedUrlResponse); + + const result = await service.getSignedUrl(payload); + +diff --git a/src/email/email.service.spec.ts b/src/email/email.service.spec.ts +index 4ae9caa..943b0f8 100644 +--- a/src/email/email.service.spec.ts ++++ b/src/email/email.service.spec.ts +@@ -1,8 +1,12 @@ + import { EmailService } from './email.service'; ++import { ConfigService } from '@nestjs/config'; ++import { PrismaService } from '../database/prisma.service'; ++import { TrackingService } from '../tracking/tracking.service'; ++import { Queue } from 'bullmq'; + + describe('EmailService.handleBounce', () => { + it('disables email notifications on hard bounce', async () => { +- const prisma: any = { ++ const prisma = { + user: { + findUnique: jest.fn().mockResolvedValue({ id: 'user-1', email: 'test@example.com' }), + update: jest.fn().mockResolvedValue(undefined), +@@ -16,10 +20,10 @@ describe('EmailService.handleBounce', () => { + }; + + const service = new EmailService( +- { get: jest.fn().mockReturnValue('http://localhost:3000/api') } as any, +- prisma, +- { createEmailEngagement: jest.fn() } as any, +- { add: jest.fn() } as any, ++ { get: jest.fn().mockReturnValue('http://localhost:3000/api') } as unknown as ConfigService, ++ prisma as unknown as PrismaService, ++ { createEmailEngagement: jest.fn() } as unknown as TrackingService, ++ { add: jest.fn() } as unknown as Queue, + ); + + await service.handleBounce('test@example.com', 'HARD', 'Mailbox disabled', { +diff --git a/src/fraud/fraud.service.spec.ts b/src/fraud/fraud.service.spec.ts +index 7e33c78..fb4b091 100644 +--- a/src/fraud/fraud.service.spec.ts ++++ b/src/fraud/fraud.service.spec.ts +@@ -5,6 +5,20 @@ import { PrismaService } from '../database/prisma.service'; + import { EmailService } from '../email/email.service'; + import { FraudPattern, FraudSeverity } from '../types/prisma.types'; + ++// FraudService's AlertPayload type and private detection helpers aren't exported; ++// this describes just enough shape to call them from tests without `any`. ++interface FraudServiceInternals { ++ createOrUpdateAlert(payload: Record): Promise<{ id: string } | null>; ++ findOpenAlert(payload: Record): Promise; ++ notifySecurityTeam(alert: unknown, isUpdate?: boolean): Promise; ++ blockUserForFraud( ++ userId: string, ++ alertId: string, ++ actorId: string, ++ reason: string, ++ ): Promise; ++} ++ + describe('FraudService', () => { + let service: FraudService; + +@@ -44,11 +58,11 @@ describe('FraudService', () => { + findFirst: jest.fn(), + findMany: jest.fn(), + }, +- } as any; ++ }; + + const mockEmailService = { + sendFraudAlertEmail: jest.fn(), +- } as any; ++ }; + + const mockConfigService = { + get: jest.fn((key: string) => { +@@ -58,7 +72,7 @@ describe('FraudService', () => { + + return undefined; + }), +- } as any; ++ }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ +@@ -85,7 +99,7 @@ describe('FraudService', () => { + mockPrismaService.loginAttempt.count.mockResolvedValue(6); + + const createOrUpdateAlertSpy = jest +- .spyOn(service as any, 'createOrUpdateAlert') ++ .spyOn(service as unknown as FraudServiceInternals, 'createOrUpdateAlert') + .mockResolvedValue({ id: 'alert-1' }); + + const result = await service.evaluateFailedLogin('user@example.com', '10.0.0.1', 'Mozilla'); +@@ -126,8 +140,8 @@ describe('FraudService', () => { + ]); + + const createOrUpdateAlertSpy = jest +- .spyOn(service as any, 'createOrUpdateAlert') +- .mockImplementation(async (payload: any) => payload); ++ .spyOn(service as unknown as FraudServiceInternals, 'createOrUpdateAlert') ++ .mockImplementation(async (payload: Record) => payload as { id: string }); + + const alerts = await service.evaluatePropertyCreated('property-1'); + +@@ -153,10 +167,10 @@ describe('FraudService', () => { + }); + + it('auto-blocks a user when a critical alert is created with enforcement enabled', async () => { +- jest.spyOn(service as any, 'findOpenAlert').mockResolvedValue(null); +- jest.spyOn(service as any, 'notifySecurityTeam').mockResolvedValue(undefined); ++ jest.spyOn(service as unknown as FraudServiceInternals, 'findOpenAlert').mockResolvedValue(null); ++ jest.spyOn(service as unknown as FraudServiceInternals, 'notifySecurityTeam').mockResolvedValue(undefined); + const blockUserForFraudSpy = jest +- .spyOn(service as any, 'blockUserForFraud') ++ .spyOn(service as unknown as FraudServiceInternals, 'blockUserForFraud') + .mockResolvedValue(undefined); + + mockPrismaService.fraudAlert.create.mockResolvedValue({ +@@ -171,7 +185,7 @@ describe('FraudService', () => { + }, + }); + +- await (service as any).createOrUpdateAlert({ ++ await (service as unknown as FraudServiceInternals).createOrUpdateAlert({ + userId: 'user-1', + pattern: FraudPattern.TOKEN_REUSE, + severity: FraudSeverity.CRITICAL, +diff --git a/src/properties/properties.service.agent.spec.ts b/src/properties/properties.service.agent.spec.ts +index 884324c..438089b 100644 +--- a/src/properties/properties.service.agent.spec.ts ++++ b/src/properties/properties.service.agent.spec.ts +@@ -9,7 +9,6 @@ import { CacheService } from '../cache/cache.service'; + + describe('PropertiesService - Agent Assignment', () => { + let service: PropertiesService; +- let prisma: PrismaService; + + const mockPrismaService = { + property: { +@@ -52,7 +51,6 @@ describe('PropertiesService - Agent Assignment', () => { + }).compile(); + + service = module.get(PropertiesService); +- prisma = module.get(PrismaService); + }); + + afterEach(() => { +@@ -72,7 +70,7 @@ describe('PropertiesService - Agent Assignment', () => { + { sub: 'owner-1', email: 'owner@test.com', role: 'USER', type: 'access' }, + ); + +- expect((prisma as any).propertyAgent.create).toHaveBeenCalledWith({ ++ expect(mockPrismaService.propertyAgent.create).toHaveBeenCalledWith({ + data: { + propertyId: 'prop-1', + agentId: 'agent-1', +@@ -127,7 +125,7 @@ describe('PropertiesService - Agent Assignment', () => { + { sub: 'owner-1', email: 'owner@test.com', role: 'USER', type: 'access' }, + ); + +- expect((prisma as any).propertyAgent.update).toHaveBeenCalledWith({ ++ expect(mockPrismaService.propertyAgent.update).toHaveBeenCalledWith({ + where: { + propertyId_agentId: { + propertyId: 'prop-1', +diff --git a/src/transactions/transaction-cancellation.service.spec.ts b/src/transactions/transaction-cancellation.service.spec.ts +index 6b5815c..d8a7004 100644 +--- a/src/transactions/transaction-cancellation.service.spec.ts ++++ b/src/transactions/transaction-cancellation.service.spec.ts +@@ -1,5 +1,7 @@ + import { NotFoundException, BadRequestException } from '@nestjs/common'; + import { TransactionCancellationService } from './transaction-cancellation.service'; ++import { PrismaService } from '../database/prisma.service'; ++import { NotificationsService } from '../notifications/notifications.service'; + + const mockTx = { + id: 'tx-1', +@@ -29,7 +31,10 @@ describe('TransactionCancellationService', () => { + + beforeEach(() => { + jest.clearAllMocks(); +- service = new TransactionCancellationService(mockPrisma as any, mockNotifications as any); ++ service = new TransactionCancellationService( ++ mockPrisma as unknown as PrismaService, ++ mockNotifications as unknown as NotificationsService, ++ ); + }); + + describe('cancel', () => { +diff --git a/src/transactions/transaction-reminders.service.spec.ts b/src/transactions/transaction-reminders.service.spec.ts +index 70bb982..1e1764d 100644 +--- a/src/transactions/transaction-reminders.service.spec.ts ++++ b/src/transactions/transaction-reminders.service.spec.ts +@@ -1,4 +1,7 @@ + import { TransactionRemindersService } from './transaction-reminders.service'; ++import { PrismaService } from '../database/prisma.service'; ++import { NotificationsService } from '../notifications/notifications.service'; ++import { CacheService } from '../cache/cache.service'; + + const mockMilestone = { + id: 'ms-1', +@@ -28,9 +31,9 @@ const mockCache = { + + function makeService(): TransactionRemindersService { + return new TransactionRemindersService( +- mockPrisma as any, +- mockNotifications as any, +- mockCache as any, ++ mockPrisma as unknown as PrismaService, ++ mockNotifications as unknown as NotificationsService, ++ mockCache as unknown as CacheService, + ); + } + +@@ -142,8 +145,9 @@ describe('TransactionRemindersService', () => { + it('respects buyer opt-out preference', async () => { + mockCache.setNx.mockResolvedValue(true); + mockPrisma.transactionMilestone.findMany.mockResolvedValue([mockMilestone]); +- mockPrisma.userPreferences.findUnique.mockImplementation(({ where }: any) => +- where.userId === 'buyer-1' ? { optOutReminders: true } : null, ++ mockPrisma.userPreferences.findUnique.mockImplementation( ++ ({ where }: { where: { userId: string } }) => ++ where.userId === 'buyer-1' ? { optOutReminders: true } : null, + ); + + const result = await makeService().sendDeadlineReminders(); +diff --git a/src/trust-score/trust-score.service.spec.ts b/src/trust-score/trust-score.service.spec.ts +index a6298fa..3fdd676 100644 +--- a/src/trust-score/trust-score.service.spec.ts ++++ b/src/trust-score/trust-score.service.spec.ts +@@ -1,5 +1,5 @@ + import { Test, TestingModule } from '@nestjs/testing'; +-import { TrustScoreService } from './trust-score.service'; ++import { TrustScoreService, TrustScoreBreakdown } from './trust-score.service'; + import { PrismaService } from '../database/prisma.service'; + + describe('TrustScoreService', () => { +@@ -50,7 +50,7 @@ describe('TrustScoreService', () => { + update: jest.fn(), + findMany: jest.fn(), + }, +- } as any; ++ }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ +@@ -192,7 +192,7 @@ describe('TrustScoreService', () => { + jest.spyOn(service, 'calculateTrustScore').mockResolvedValue({ + userId: 'user-123', + score: 80, +- breakdown: {} as any, ++ breakdown: {} as TrustScoreBreakdown, + lastUpdated: new Date(), + nextUpdateTime: new Date(), + }); +@@ -259,7 +259,7 @@ describe('TrustScoreService', () => { + jest.spyOn(service, 'calculateTrustScore').mockResolvedValue({ + userId: 'user-1', + score: 75, +- breakdown: {} as any, ++ breakdown: {} as TrustScoreBreakdown, + lastUpdated: new Date(), + nextUpdateTime: new Date(), + }); +diff --git a/src/webhooks/webhooks.service.spec.ts b/src/webhooks/webhooks.service.spec.ts +index b9223ba..8f31a70 100644 +--- a/src/webhooks/webhooks.service.spec.ts ++++ b/src/webhooks/webhooks.service.spec.ts +@@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; + import { WebhooksService } from './webhooks.service'; + import { PrismaService } from '../database/prisma.service'; + import { NotFoundException } from '@nestjs/common'; ++import { CreateWebhookDto } from './webhook.dto'; + + const mockPrisma = {}; + +@@ -22,7 +23,7 @@ describe('WebhooksService', () => { + + describe('create', () => { + it('should throw error (webhooks not yet implemented)', async () => { +- await expect(service.create('user-1', {} as any)).rejects.toThrow( ++ await expect(service.create('user-1', {} as CreateWebhookDto)).rejects.toThrow( + 'Webhooks module not yet implemented', + ); + }); diff --git a/src/admin/admin-audit.interceptor.spec.ts b/src/admin/admin-audit.interceptor.spec.ts index a412ada1..ccf2d3f8 100644 --- a/src/admin/admin-audit.interceptor.spec.ts +++ b/src/admin/admin-audit.interceptor.spec.ts @@ -1,10 +1,17 @@ import { AdminAuditInterceptor } from './admin-audit.interceptor'; import { ExecutionContext, CallHandler } from '@nestjs/common'; import { of } from 'rxjs'; +import { PrismaService } from '../database/prisma.service'; + +interface MockPrisma { + activityLog: { + create: jest.Mock; + }; +} describe('AdminAuditInterceptor', () => { let interceptor: AdminAuditInterceptor; - let mockPrisma: any; + let mockPrisma: MockPrisma; beforeEach(() => { mockPrisma = { @@ -12,10 +19,10 @@ describe('AdminAuditInterceptor', () => { create: jest.fn().mockResolvedValue({}), }, }; - interceptor = new AdminAuditInterceptor(mockPrisma); + interceptor = new AdminAuditInterceptor(mockPrisma as unknown as PrismaService); }); - function makeContext(overrides: Partial = {}): ExecutionContext { + function makeContext(overrides: Record = {}): ExecutionContext { const request = { authUser: { sub: 'admin-1' }, headers: { 'user-agent': 'test-agent' }, diff --git a/src/admin/admin-audit.interceptor.ts b/src/admin/admin-audit.interceptor.ts index ddc1517b..7c2e1f39 100644 --- a/src/admin/admin-audit.interceptor.ts +++ b/src/admin/admin-audit.interceptor.ts @@ -8,7 +8,7 @@ import { PrismaService } from '../database/prisma.service'; export class AdminAuditInterceptor implements NestInterceptor { constructor(private readonly prisma: PrismaService) {} - intercept(context: ExecutionContext, next: CallHandler): Observable { + intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const user = request.authUser; const ip = diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index c18b4455..847ed4a2 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -225,7 +225,7 @@ export class AdminController { @Get('email/preview/:templateName') async previewEmailTemplate(@Param('templateName') templateName: string) { - const sampleDataMap: Record = { + const sampleDataMap: Record> = { 'password-reset': { resetUrl: 'http://localhost:3000/reset-password?token=sample-token-123', }, diff --git a/src/admin/interceptors/admin-access-logging.interceptor.ts b/src/admin/interceptors/admin-access-logging.interceptor.ts index c410b838..cb7da529 100644 --- a/src/admin/interceptors/admin-access-logging.interceptor.ts +++ b/src/admin/interceptors/admin-access-logging.interceptor.ts @@ -11,7 +11,7 @@ import { AuditService } from '../../audit/audit.service'; export class AdminAccessLoggingInterceptor implements NestInterceptor { constructor(private readonly auditService: AuditService) {} - intercept(context: ExecutionContext, next: CallHandler): Observable { + intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); diff --git a/src/analytics/analytics.interceptor.ts b/src/analytics/analytics.interceptor.ts index bc5f17bc..ffbe8eb2 100644 --- a/src/analytics/analytics.interceptor.ts +++ b/src/analytics/analytics.interceptor.ts @@ -11,7 +11,7 @@ import { AuthUserPayload } from '../auth/types/auth-user.type'; export class AnalyticsInterceptor implements NestInterceptor { constructor(private readonly analytics: AnalyticsService) {} - intercept(context: ExecutionContext, next: CallHandler): Observable { + intercept(context: ExecutionContext, next: CallHandler): Observable { const req = context.switchToHttp().getRequest(); const res = context.switchToHttp().getResponse(); const start = Date.now(); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 83708809..241b1aa2 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -59,6 +59,23 @@ type JwtPayload = { exp?: number; }; +type TransactionWithPropertyTitle = Prisma.TransactionGetPayload<{ + include: { property: { select: { title: true } } }; +}>; + +type PropertyWithOwnerName = Prisma.PropertyGetPayload<{ + include: { owner: { select: { firstName: true; lastName: true } } }; +}>; + +interface RecaptchaVerifyResponse { + success: boolean; + score?: number; + action?: string; + challenge_ts?: string; + hostname?: string; + 'error-codes'?: string[]; +} + @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); @@ -106,7 +123,10 @@ export class AuthService { /** * Helper to map transactions to activity items for dashboard */ - private transactionsToActivityItems(transactions: any[], type: 'purchase' | 'sale') { + private transactionsToActivityItems( + transactions: TransactionWithPropertyTitle[], + type: 'purchase' | 'sale', + ) { return transactions.map((tx) => ({ type: 'transaction' as const, id: tx.id, @@ -466,7 +486,7 @@ export class AuthService { * Handle token reuse detection - invalidate entire token family */ private async handleTokenReuse( - blacklistedToken: any, + blacklistedToken: Prisma.BlacklistedToken, reusedJti: string, ipAddress?: string, userAgent?: string, @@ -742,7 +762,7 @@ export class AuthService { const recentActivity = [ ...this.transactionsToActivityItems(buyerTransactions, 'purchase'), ...this.transactionsToActivityItems(sellerTransactions, 'sale'), - ...documents.map((doc: any) => ({ + ...documents.map((doc: Prisma.Document) => ({ type: 'document' as const, id: doc.id, title: doc.fileName, @@ -766,7 +786,7 @@ export class AuthService { apiKeysCount: apiKeys.length, }, recentActivity, - recommendations: recommendationProperties.map((p: any) => ({ + recommendations: recommendationProperties.map((p: PropertyWithOwnerName) => ({ id: p.id, title: p.title, address: p.address, @@ -981,7 +1001,7 @@ export class AuthService { orderBy: { createdAt: 'desc' }, }); - return apiKeys.map((apiKey: any) => this.toApiKeyResponse(apiKey)); + return apiKeys.map((apiKey: Prisma.ApiKey) => this.toApiKeyResponse(apiKey)); } async rotateApiKey(user: AuthUserPayload, apiKeyId: string) { @@ -1340,7 +1360,7 @@ export class AuthService { return `pc_${randomToken(24)}`; } - private toApiKeyResponse(apiKey: any) { + private toApiKeyResponse(apiKey: Prisma.ApiKey) { return { id: apiKey.id, name: apiKey.name, @@ -1483,7 +1503,7 @@ export class AuthService { if (historyEntries.length > 0) { await tx.passwordHistory.deleteMany({ where: { - id: { in: historyEntries.map((entry: any) => entry.id) }, + id: { in: historyEntries.map((entry: Prisma.PasswordHistory) => entry.id) }, }, }); } @@ -1553,7 +1573,7 @@ export class AuthService { body: `secret=${secret}&response=${token}`, }); - const data = (await response.json()) as any; + const data = (await response.json()) as RecaptchaVerifyResponse; // reCAPTCHA v3 returns a score between 0.0 and 1.0. Typically, 0.5 is a good threshold. if (data.success && data.score !== undefined && data.score >= 0.5) { diff --git a/src/auth/guards/rate-limit.guard.ts b/src/auth/guards/rate-limit.guard.ts index b4c9f039..22d5b4b1 100644 --- a/src/auth/guards/rate-limit.guard.ts +++ b/src/auth/guards/rate-limit.guard.ts @@ -10,6 +10,7 @@ import { Logger, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { Request } from 'express'; import { RateLimitService } from '../rate-limit.service'; import { RATE_LIMIT_HEADERS } from '../rate-limit.config'; @@ -155,7 +156,7 @@ export class RateLimitGuard implements CanActivate { /** * Extract client IP from request */ - private getClientIp(request: any): string { + private getClientIp(request: Request): string { return ( request.headers['x-forwarded-for']?.split(',')[0].trim() || request.connection?.remoteAddress || diff --git a/src/auth/interceptors/rate-limit-headers.interceptor.ts b/src/auth/interceptors/rate-limit-headers.interceptor.ts index 22c7851e..c89c71c8 100644 --- a/src/auth/interceptors/rate-limit-headers.interceptor.ts +++ b/src/auth/interceptors/rate-limit-headers.interceptor.ts @@ -11,7 +11,7 @@ import { RATE_LIMIT_HEADERS } from '../rate-limit.config'; */ @Injectable() export class RateLimitHeadersInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { + intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); diff --git a/src/auth/login-rate-limit.service.spec.ts b/src/auth/login-rate-limit.service.spec.ts index 7965bc15..a708f97a 100644 --- a/src/auth/login-rate-limit.service.spec.ts +++ b/src/auth/login-rate-limit.service.spec.ts @@ -1,8 +1,18 @@ import { LoginRateLimitService } from './login-rate-limit.service'; +import { PrismaService } from '../database/prisma.service'; + +interface MockPrisma { + loginAttempt: { + findFirst: jest.Mock; + count: jest.Mock; + create: jest.Mock; + updateMany: jest.Mock; + }; +} describe('LoginRateLimitService', () => { let service: LoginRateLimitService; - let mockPrisma: any; + let mockPrisma: MockPrisma; const email = 'test@example.com'; const ip = '1.2.3.4'; @@ -16,7 +26,7 @@ describe('LoginRateLimitService', () => { updateMany: jest.fn().mockResolvedValue({ count: 1 }), }, }; - service = new LoginRateLimitService(mockPrisma); + service = new LoginRateLimitService(mockPrisma as unknown as PrismaService); }); describe('isAccountLocked', () => { diff --git a/src/auth/rate-limit.guard.spec.ts b/src/auth/rate-limit.guard.spec.ts index 1eb827dc..e51b812e 100644 --- a/src/auth/rate-limit.guard.spec.ts +++ b/src/auth/rate-limit.guard.spec.ts @@ -3,7 +3,7 @@ import { RateLimitService } from './rate-limit.service'; import { ExecutionContext, HttpException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -function makeContext(overrides: Partial = {}): ExecutionContext { +function makeContext(overrides: Record = {}): ExecutionContext { const request = { method: 'POST', url: '/auth/login', @@ -40,7 +40,7 @@ describe('RateLimitGuard - auth/signup endpoints', () => { checkUserRateLimit: jest.fn().mockResolvedValue(notExceeded), checkEndpointRateLimit: jest.fn().mockResolvedValue({ ...notExceeded, limit: 0 }), getHeaders: jest.fn().mockReturnValue({}), - } as any; + } as unknown as jest.Mocked; guard = new RateLimitGuard(reflector, rateLimitService); }); @@ -68,10 +68,11 @@ describe('RateLimitGuard - auth/signup endpoints', () => { try { await guard.canActivate(ctx); fail('should have thrown'); - } catch (e: any) { + } catch (e: unknown) { expect(e).toBeInstanceOf(HttpException); - expect(e.getStatus()).toBe(429); - const body = e.getResponse(); + const httpException = e as HttpException; + expect(httpException.getStatus()).toBe(429); + const body = httpException.getResponse(); expect(body).toMatchObject({ retryAfter: 60 }); } }); diff --git a/src/auth/rbac.spec.ts b/src/auth/rbac.spec.ts index 37451bec..accc20db 100644 --- a/src/auth/rbac.spec.ts +++ b/src/auth/rbac.spec.ts @@ -3,13 +3,14 @@ * Tests that USER, AGENT, and ADMIN roles are correctly enforced. */ import { Test, TestingModule } from '@nestjs/testing'; -import { ForbiddenException } from '@nestjs/common'; +import { ForbiddenException, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUserPayload } from '../auth/types/auth-user.type'; import { UserRole } from '../types/prisma.types'; +import { CreatePropertyDto } from '../properties/dto/property.dto'; -function makeContext(role: UserRole, requiredRoles: UserRole[] | null) { +function makeContext(role: UserRole, requiredRoles: UserRole[] | null): ExecutionContext { const user: AuthUserPayload = { sub: 'u1', email: 'u@test.com', role, type: 'access' }; const request = { authUser: user }; return { @@ -17,7 +18,7 @@ function makeContext(role: UserRole, requiredRoles: UserRole[] | null) { getHandler: () => ({}), getClass: () => ({}), _requiredRoles: requiredRoles, - }; + } as unknown as ExecutionContext; } describe('RolesGuard - RBAC enforcement', () => { @@ -41,19 +42,19 @@ describe('RolesGuard - RBAC enforcement', () => { it('allows ADMIN to access admin routes', () => { setupRoles([UserRole.ADMIN]); const ctx = makeContext(UserRole.ADMIN, [UserRole.ADMIN]); - expect(guard.canActivate(ctx as any)).toBe(true); + expect(guard.canActivate(ctx)).toBe(true); }); it('rejects USER from admin routes', () => { setupRoles([UserRole.ADMIN]); const ctx = makeContext(UserRole.USER, [UserRole.ADMIN]); - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); it('rejects AGENT from admin routes', () => { setupRoles([UserRole.ADMIN]); const ctx = makeContext(UserRole.AGENT, [UserRole.ADMIN]); - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); }); @@ -61,19 +62,19 @@ describe('RolesGuard - RBAC enforcement', () => { it('allows AGENT to access agent+admin routes', () => { setupRoles([UserRole.AGENT, UserRole.ADMIN]); const ctx = makeContext(UserRole.AGENT, [UserRole.AGENT, UserRole.ADMIN]); - expect(guard.canActivate(ctx as any)).toBe(true); + expect(guard.canActivate(ctx)).toBe(true); }); it('allows ADMIN to access agent+admin routes', () => { setupRoles([UserRole.AGENT, UserRole.ADMIN]); const ctx = makeContext(UserRole.ADMIN, [UserRole.AGENT, UserRole.ADMIN]); - expect(guard.canActivate(ctx as any)).toBe(true); + expect(guard.canActivate(ctx)).toBe(true); }); it('rejects USER from agent+admin routes', () => { setupRoles([UserRole.AGENT, UserRole.ADMIN]); const ctx = makeContext(UserRole.USER, [UserRole.AGENT, UserRole.ADMIN]); - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); }); @@ -81,7 +82,7 @@ describe('RolesGuard - RBAC enforcement', () => { it('allows any authenticated user when no roles are required', () => { setupRoles(null); const ctx = makeContext(UserRole.USER, null); - expect(guard.canActivate(ctx as any)).toBe(true); + expect(guard.canActivate(ctx)).toBe(true); }); }); @@ -93,8 +94,8 @@ describe('RolesGuard - RBAC enforcement', () => { switchToHttp: () => ({ getRequest: () => request }), getHandler: () => ({}), getClass: () => ({}), - }; - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + } as unknown as ExecutionContext; + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); it('throws ForbiddenException when no user in request', () => { @@ -103,8 +104,8 @@ describe('RolesGuard - RBAC enforcement', () => { switchToHttp: () => ({ getRequest: () => ({}) }), getHandler: () => ({}), getClass: () => ({}), - }; - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + } as unknown as ExecutionContext; + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); }); }); @@ -163,7 +164,7 @@ describe('PropertiesController - RBAC', () => { zipCode: '10001', price: 100000, propertyType: 'House', - } as any; + } as CreatePropertyDto; const result = await controller.create(dto, regularUser); expect(result).toEqual({ id: 'p1' }); expect(service.create).toHaveBeenCalledWith(dto, regularUser.sub); @@ -178,7 +179,7 @@ describe('PropertiesController - RBAC', () => { zipCode: '10001', price: 200000, propertyType: 'Condo', - } as any; + } as CreatePropertyDto; await controller.create(dto, adminUser); expect(service.create).toHaveBeenCalledWith(dto, adminUser.sub); }); @@ -201,19 +202,19 @@ describe('AdminController - RBAC (guard integration)', () => { it('ADMIN can access admin dashboard', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.ADMIN]); const ctx = makeContext(UserRole.ADMIN, [UserRole.ADMIN]); - expect(guard.canActivate(ctx as any)).toBe(true); + expect(guard.canActivate(ctx)).toBe(true); }); it('USER cannot access admin dashboard', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.ADMIN]); const ctx = makeContext(UserRole.USER, [UserRole.ADMIN]); - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); it('AGENT cannot access admin dashboard', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.ADMIN]); const ctx = makeContext(UserRole.AGENT, [UserRole.ADMIN]); - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); }); @@ -233,18 +234,18 @@ describe('TransactionsController - RBAC (guard integration)', () => { it('AGENT can create transactions', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.AGENT, UserRole.ADMIN]); const ctx = makeContext(UserRole.AGENT, [UserRole.AGENT, UserRole.ADMIN]); - expect(guard.canActivate(ctx as any)).toBe(true); + expect(guard.canActivate(ctx)).toBe(true); }); it('ADMIN can create transactions', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.AGENT, UserRole.ADMIN]); const ctx = makeContext(UserRole.ADMIN, [UserRole.AGENT, UserRole.ADMIN]); - expect(guard.canActivate(ctx as any)).toBe(true); + expect(guard.canActivate(ctx)).toBe(true); }); it('USER cannot create transactions', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([UserRole.AGENT, UserRole.ADMIN]); const ctx = makeContext(UserRole.USER, [UserRole.AGENT, UserRole.ADMIN]); - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); }); diff --git a/src/blockchain/blockchain.controller.ts b/src/blockchain/blockchain.controller.ts index 1c8b87a9..236d198e 100644 --- a/src/blockchain/blockchain.controller.ts +++ b/src/blockchain/blockchain.controller.ts @@ -137,7 +137,7 @@ export class BlockchainController { }, }, }) - getStatus(): Record { + getStatus() { return this.blockchainService.getStatus(); } diff --git a/src/blockchain/blockchain.service.spec.ts b/src/blockchain/blockchain.service.spec.ts index 751ec9c0..20940a8b 100644 --- a/src/blockchain/blockchain.service.spec.ts +++ b/src/blockchain/blockchain.service.spec.ts @@ -15,8 +15,8 @@ describe('BlockchainService', () => { let prismaService: PrismaService; const mockConfigService = { - get: jest.fn((key: string, defaultValue?: any) => { - const config: Record = { + get: jest.fn((key: string, defaultValue?: unknown) => { + const config: Record = { BLOCKCHAIN_ENABLED: 'true', BLOCKCHAIN_NETWORK: BlockchainNetwork.SEPOLIA, BLOCKCHAIN_RPC_URL: 'https://sepolia.infura.io/v3/test', @@ -248,8 +248,8 @@ describe('BlockchainService', () => { it('should handle disabled blockchain by recording locally', async () => { // Override config to disable blockchain const disabledConfig = { - get: jest.fn((key: string, defaultValue?: any) => { - const config: Record = { + get: jest.fn((key: string, defaultValue?: unknown) => { + const config: Record = { BLOCKCHAIN_ENABLED: 'false', BLOCKCHAIN_NETWORK: BlockchainNetwork.SEPOLIA, }; diff --git a/src/cache/cache-headers.interceptor.ts b/src/cache/cache-headers.interceptor.ts index 50aec430..a15a4c51 100644 --- a/src/cache/cache-headers.interceptor.ts +++ b/src/cache/cache-headers.interceptor.ts @@ -6,7 +6,7 @@ import { tap } from 'rxjs/operators'; @Injectable() export class CacheHeadersInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { + intercept(context: ExecutionContext, next: CallHandler): Observable { const res = context.switchToHttp().getResponse(); const start = Date.now(); diff --git a/src/cache/cache-metrics.interceptor.ts b/src/cache/cache-metrics.interceptor.ts index e120857f..dd7f81fc 100644 --- a/src/cache/cache-metrics.interceptor.ts +++ b/src/cache/cache-metrics.interceptor.ts @@ -33,7 +33,7 @@ export class CacheMetricsInterceptor implements NestInterceptor { // CacheMonitoringService is expected to be a singleton provided by the CacheModule constructor(private cacheMonitoringService: CacheMonitoringService) {} - intercept(context: ExecutionContext, next: CallHandler): Observable { + intercept(context: ExecutionContext, next: CallHandler): Observable { const startTime = Date.now(); return next.handle().pipe( diff --git a/src/cache/cache.config.ts b/src/cache/cache.config.ts index f056f4e9..875c6096 100644 --- a/src/cache/cache.config.ts +++ b/src/cache/cache.config.ts @@ -10,7 +10,9 @@ import * as redisStore from 'cache-manager-redis-store'; export const REDIS_CONFIG: CacheModuleOptions = { isGlobal: true, - store: redisStore as any, + // cache-manager-redis-store's store type doesn't match @nestjs/cache-manager's + // CacheModuleOptions type -- unavoidable third-party interop + store: redisStore as unknown as CacheModuleOptions['store'], host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379', 10), password: process.env.REDIS_PASSWORD, diff --git a/src/commissions/commissions.service.spec.ts b/src/commissions/commissions.service.spec.ts index 76c89bb4..0c0014a2 100644 --- a/src/commissions/commissions.service.spec.ts +++ b/src/commissions/commissions.service.spec.ts @@ -6,7 +6,6 @@ import { Decimal } from '@prisma/client/runtime/library'; describe('CommissionsService', () => { let service: CommissionsService; - let prisma: PrismaService; const mockPrismaService = { transaction: { @@ -28,7 +27,6 @@ describe('CommissionsService', () => { }).compile(); service = module.get(CommissionsService); - prisma = module.get(PrismaService); }); afterEach(() => { @@ -53,8 +51,8 @@ describe('CommissionsService', () => { await service.createCommissionsForTransaction('tx-1'); - expect((prisma as any).commission.create).toHaveBeenCalledTimes(2); - expect((prisma as any).commission.create).toHaveBeenNthCalledWith( + expect(mockPrismaService.commission.create).toHaveBeenCalledTimes(2); + expect(mockPrismaService.commission.create).toHaveBeenNthCalledWith( 1, expect.objectContaining({ data: { @@ -67,7 +65,7 @@ describe('CommissionsService', () => { }, }), ); - expect((prisma as any).commission.create).toHaveBeenNthCalledWith( + expect(mockPrismaService.commission.create).toHaveBeenNthCalledWith( 2, expect.objectContaining({ data: { @@ -89,7 +87,7 @@ describe('CommissionsService', () => { await service.updateCommissionsStatus('tx-1', 'COMPLETED'); - expect((prisma as any).commission.updateMany).toHaveBeenCalledWith({ + expect(mockPrismaService.commission.updateMany).toHaveBeenCalledWith({ where: { transactionId: 'tx-1' }, data: { status: 'COMPLETED' }, }); @@ -106,7 +104,7 @@ describe('CommissionsService', () => { { sub: 'agent-1', email: 'agent@test.com', role: 'AGENT', type: 'access' }, ); - expect((prisma as any).commission.findMany).toHaveBeenCalledWith( + expect(mockPrismaService.commission.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ agentId: 'agent-1' }), }), diff --git a/src/config/api-docs.controller.ts b/src/config/api-docs.controller.ts index a62063fa..9633f9cc 100644 --- a/src/config/api-docs.controller.ts +++ b/src/config/api-docs.controller.ts @@ -7,9 +7,13 @@ import { Controller, Get, Res } from '@nestjs/common'; import { ApiExcludeController } from '@nestjs/swagger'; -import { Response } from 'express'; +import { Application, Response } from 'express'; import { ApiVersionEnum, API_VERSIONS } from '../versioning/api-version.constants'; +interface AppWithOpenApiDoc extends Application { + openAPIDocument?: Record; +} + @ApiExcludeController() @Controller('api') export class ApiDocsController { @@ -19,7 +23,7 @@ export class ApiDocsController { @Get('openapi.json') getOpenApiSpec(@Res() res: Response) { // This will be populated by setupSwagger - const spec = (res.req.app as any).openAPIDocument; + const spec = (res.req.app as AppWithOpenApiDoc).openAPIDocument; if (spec) { res.json(spec); diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts index 37fb4bc0..72144ac0 100644 --- a/src/config/swagger.config.ts +++ b/src/config/swagger.config.ts @@ -8,6 +8,10 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { INestApplication } from '@nestjs/common'; +interface AppWithOpenApiDoc { + openAPIDocument?: Record; +} + export function setupSwagger(app: INestApplication): void { const config = new DocumentBuilder() .setTitle('PropChain API') @@ -137,5 +141,5 @@ export function setupOpenAPIEndpoint(app: INestApplication): void { const document = SwaggerModule.createDocument(app, config); // Store document in app for access via endpoint - (app as any).openAPIDocument = document; + (app as unknown as AppWithOpenApiDoc).openAPIDocument = document; } diff --git a/src/dashboard/dashboard.service.ts b/src/dashboard/dashboard.service.ts index fc7fcd28..b3e3d8f1 100644 --- a/src/dashboard/dashboard.service.ts +++ b/src/dashboard/dashboard.service.ts @@ -79,7 +79,7 @@ export class DashboardService { }); const totalProperties = properties.length; - const activeListings = properties.filter((p: any) => p.status === 'ACTIVE').length; + const activeListings = properties.filter((p) => p.status === 'ACTIVE').length; // Get user's transactions (both as buyer and seller) const buyerTransactions = await this.prisma.transaction.findMany({ @@ -195,7 +195,7 @@ export class DashboardService { take: limit, }); - return recommendations.map((prop: any) => ({ + return recommendations.map((prop) => ({ id: prop.id, title: prop.title, address: prop.address, @@ -214,7 +214,7 @@ export class DashboardService { where: { status: 'ACTIVE', ownerId: { not: userId }, - OR: userProperties.map((prop: any) => ({ + OR: userProperties.map((prop) => ({ AND: [ { city: prop.city }, { state: prop.state }, @@ -226,7 +226,7 @@ export class DashboardService { take: limit, }); - return similarProperties.map((prop: any) => ({ + return similarProperties.map((prop) => ({ id: prop.id, title: prop.title, address: prop.address, diff --git a/src/documents/documents-download.controller.spec.ts b/src/documents/documents-download.controller.spec.ts index 822adf00..d5da61ed 100644 --- a/src/documents/documents-download.controller.spec.ts +++ b/src/documents/documents-download.controller.spec.ts @@ -7,6 +7,7 @@ import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUserPayload } from '../auth/types/auth-user.type'; import { UserRole } from '../types/prisma.types'; import { Response } from 'express'; +import { RequestSignedUploadDto } from './dto/document-access.dto'; describe('DocumentsDownloadController', () => { let controller: DocumentsDownloadController; @@ -109,7 +110,7 @@ describe('DocumentsDownloadController', () => { fileName: 'test.pdf', mimeType: 'application/pdf', fileSizeBytes: 1024 - } as any; + } as RequestSignedUploadDto; const mockSignedUrlResponse = { url: 'http://signed.url/upload', @@ -167,7 +168,7 @@ describe('DocumentsDownloadController', () => { describe('createMetadata', () => { it('should pass the DTO and user ID to the documents service', async () => { - const mockDto = { fileName: 'test.pdf' }; + const mockDto = { fileName: 'test.pdf', documentType: 'OTHER', fileUrl: 'https://example.com/test.pdf' }; mockDocumentsService.create.mockResolvedValue({ id: 'doc-123' }); const result = await controller.createMetadata(mockDto, mockUser); diff --git a/src/documents/documents-download.controller.ts b/src/documents/documents-download.controller.ts index a1d462e9..a748d633 100644 --- a/src/documents/documents-download.controller.ts +++ b/src/documents/documents-download.controller.ts @@ -30,6 +30,7 @@ import { SignedUploadUrlResponseDto, } from './dto/document-access.dto'; import { DocumentsService } from './documents.service'; +import { CreateDocumentDto } from './dto/document.dto'; import { SignedUrlService } from './signed-url/signed-url.service'; import { SignedUrlOperation } from './signed-url/signed-url-provider.interface'; @@ -181,7 +182,7 @@ export class DocumentsDownloadController { * This expects that fileUrl points to the stored object (CDN URL or provider URL). */ @Post('metadata') - async createMetadata(@Body() dto: any, @CurrentUser() user: AuthUserPayload) { + async createMetadata(@Body() dto: CreateDocumentDto, @CurrentUser() user: AuthUserPayload) { // This endpoint intentionally accepts CreateDocumentDto shape. return this.documentsService.create(dto, user.sub); } diff --git a/src/documents/documents.controller.spec.ts b/src/documents/documents.controller.spec.ts index 65d73232..7270ba31 100644 --- a/src/documents/documents.controller.spec.ts +++ b/src/documents/documents.controller.spec.ts @@ -6,6 +6,7 @@ import { UserRole } from '../types/prisma.types'; import { Response } from 'express'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; +import { CreateDocumentDto, UpdateDocumentDto, SignDocumentDto } from './dto/document.dto'; describe('DocumentsController', () => { let controller: DocumentsController; @@ -51,7 +52,7 @@ describe('DocumentsController', () => { it('create', async () => { mockDocumentsService.create.mockResolvedValue('created'); - expect(await controller.create({} as any, mockUser)).toBe('created'); + expect(await controller.create({} as CreateDocumentDto, mockUser)).toBe('created'); }); it('findAll', async () => { @@ -67,7 +68,7 @@ describe('DocumentsController', () => { it('update', async () => { mockDocumentsService.findAuthorizedById.mockResolvedValue('doc'); mockDocumentsService.update.mockResolvedValue('updated'); - expect(await controller.update('id', {} as any, mockUser)).toBe('updated'); + expect(await controller.update('id', {} as UpdateDocumentDto, mockUser)).toBe('updated'); }); it('remove', async () => { @@ -89,7 +90,7 @@ describe('DocumentsController', () => { it('sign', async () => { mockDocumentsService.findAuthorizedById.mockResolvedValue('doc'); mockDocumentsService.signDocument.mockResolvedValue('signed'); - expect(await controller.sign('id', {} as any, mockUser)).toBe('signed'); + expect(await controller.sign('id', {} as SignDocumentDto, mockUser)).toBe('signed'); }); it('bulkDownload', async () => { diff --git a/src/documents/documents.module.ts b/src/documents/documents.module.ts index a616cb9d..b5e1380c 100644 --- a/src/documents/documents.module.ts +++ b/src/documents/documents.module.ts @@ -1,6 +1,6 @@ // @ts-nocheck -import { Module } from '@nestjs/common'; +import { Module, Type } from '@nestjs/common'; import { DocumentsService } from './documents.service'; import { DocumentsController } from './documents.controller'; import { PrismaModule } from '../database/prisma.module'; @@ -25,7 +25,7 @@ export const SIGNED_URL_PROVIDER_TOKEN = 'SIGNED_URL_PROVIDER_TOKEN'; * - 'azure' -> AzureSignedUrlProvider (no-op shell for integrators) * - (unset/other) -> NotConfiguredSignedUrlProvider (throws a configured error) */ -function signedUrlProviderFactory(): new (...args: any[]) => SignedUrlProvider { +function signedUrlProviderFactory(): Type { const provider = process.env.SIGNED_URL_PROVIDER?.toLowerCase(); switch (provider) { case 's3': diff --git a/src/documents/documents.service.spec.ts b/src/documents/documents.service.spec.ts index dd59e6fa..7de6201f 100644 --- a/src/documents/documents.service.spec.ts +++ b/src/documents/documents.service.spec.ts @@ -1,8 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { PassThrough } from 'stream'; +import { Response } from 'express'; import { DocumentsService } from './documents.service'; import { PrismaService } from '../database/prisma.service'; +import { CreateDocumentDto, UpdateDocumentDto, SignDocumentDto } from './dto/document.dto'; describe('DocumentsService', () => { let service: DocumentsService; @@ -125,16 +127,22 @@ describe('DocumentsService', () => { describe('bulkDownload', () => { /** Create a mock writable stream that acts like an Express Response */ - function mockStreamRes() { - const stream = new PassThrough(); - (stream as any).setHeader = jest.fn(); - (stream as any).setHeaders = jest.fn(); + function mockStreamRes(): Response { + const stream = new PassThrough() as PassThrough & { + setHeader: jest.Mock; + setHeaders: jest.Mock; + }; + stream.setHeader = jest.fn(); + stream.setHeaders = jest.fn(); // Suppress the 'data' events to avoid jest hanging - jest.spyOn(stream, 'pipe').mockImplementation(function (this: any, dest: any) { + jest.spyOn(stream, 'pipe').mockImplementation(function ( + this: PassThrough, + dest: NodeJS.WritableStream & { end: (chunk: string) => void }, + ) { dest.end('fake-zip-data'); - return dest; + return dest as unknown as ReturnType; }); - return stream as any; + return stream as unknown as Response; } it('should throw NotFoundException when no documents found', async () => { @@ -219,7 +227,7 @@ describe('DocumentsService', () => { it('create', async () => { mockPrismaService.document.create.mockResolvedValue({ id: 'doc-1' }); const result = await service.create( - { documentType: 'TITLE_DEED', propertyId: 'prop-1' } as any, + { documentType: 'TITLE_DEED', propertyId: 'prop-1' } as CreateDocumentDto, 'user-1', ); expect(result.id).toBe('doc-1'); @@ -249,7 +257,7 @@ describe('DocumentsService', () => { it('update', async () => { mockPrismaService.document.findUnique.mockResolvedValue({ id: 'doc-1' }); mockPrismaService.document.update.mockResolvedValue({ id: 'doc-1', status: 'VERIFIED' }); - expect(await service.update('doc-1', { status: 'VERIFIED' } as any)).toHaveProperty('status', 'VERIFIED'); + expect(await service.update('doc-1', { status: 'VERIFIED' } as UpdateDocumentDto)).toHaveProperty('status', 'VERIFIED'); }); it('remove', async () => { @@ -261,8 +269,8 @@ describe('DocumentsService', () => { describe('Extended Coverage Operations - Branches', () => { it('should execute methods with alternate parameters to trigger secondary branches', async () => { - const safeExec = async (promise: any) => { - try { await promise; } catch (e) {} + const safeExec = async (promise: Promise) => { + try { await promise; } catch (e) { /* expected in some branches */ } }; // Execute standard paths @@ -273,17 +281,17 @@ describe('DocumentsService', () => { await safeExec(service.deleteExpired()); await safeExec(service.flagExpiryNotified('doc-1')); // Fixed: Removed the 3rd argument - await safeExec(service.signDocument('doc-1', { signature: 'test' } as any)); + await safeExec(service.signDocument('doc-1', { signature: 'test' } as unknown as SignDocumentDto)); // Fixed: Removed the 2nd argument await safeExec(service.verifySignature('doc-1')); // Execute alternate paths (missing users, admin roles, null parameters) - await safeExec(service.getVersions('doc-1', null as any, 'ADMIN')); - await safeExec(service.getVersion('doc-1', 'v1', null as any, 'ADMIN')); - await safeExec(service.findAuthorizedById('doc-1', null as any, null as any)); - await safeExec(service.findAll(null as any, { category: 'legal' } as any, 'ADMIN')); + await safeExec(service.getVersions('doc-1', null as unknown as string, 'ADMIN')); + await safeExec(service.getVersion('doc-1', 'v1', null as unknown as string, 'ADMIN')); + await safeExec(service.findAuthorizedById('doc-1', null as unknown as string, null as unknown as string)); + await safeExec(service.findAll(null as unknown as string, { category: 'legal' }, 'ADMIN')); // Fixed: Removed the 3rd argument - await safeExec(service.signDocument('doc-1', {} as any)); + await safeExec(service.signDocument('doc-1', {} as unknown as SignDocumentDto)); }); }); diff --git a/src/documents/signed-url/signed-url-providers.spec.ts b/src/documents/signed-url/signed-url-providers.spec.ts index 86f11526..f478f03c 100644 --- a/src/documents/signed-url/signed-url-providers.spec.ts +++ b/src/documents/signed-url/signed-url-providers.spec.ts @@ -2,9 +2,10 @@ import { AzureSignedUrlProvider } from './azure-signed-url-provider'; import { GcsSignedUrlProvider } from './gcs-signed-url-provider'; import { NotConfiguredSignedUrlProvider } from './not-configured.signed-url-provider'; import { S3SignedUrlProvider } from './s3-signed-url-provider'; +import { SignedUrlRequest } from './signed-url-provider.interface'; describe('Signed URL Providers', () => { - const mockPayload = { operation: 'download', objectKey: 'test.pdf' } as any; + const mockPayload = { operation: 'download', objectKey: 'test.pdf' } as SignedUrlRequest; it('AzureSignedUrlProvider should execute getSignedUrl', async () => { const provider = new AzureSignedUrlProvider(); diff --git a/src/documents/signed-url/signed-url.service.spec.ts b/src/documents/signed-url/signed-url.service.spec.ts index 54aaeb56..7d3fc08a 100644 --- a/src/documents/signed-url/signed-url.service.spec.ts +++ b/src/documents/signed-url/signed-url.service.spec.ts @@ -1,5 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SignedUrlService } from './signed-url.service'; +import { SignedUrlRequest, SignedUrlResponse } from './signed-url-provider.interface'; describe('SignedUrlService', () => { let service: SignedUrlService; @@ -31,8 +32,8 @@ describe('SignedUrlService', () => { describe('getSignedUrl', () => { it('should delegate to the injected provider', async () => { - const payload = { operation: 'download' as any, objectKey: 'test.pdf' }; - mockProvider.getSignedUrl.mockResolvedValue({ url: 'http://signed' } as any); + const payload = { operation: 'download', objectKey: 'test.pdf' } as SignedUrlRequest; + mockProvider.getSignedUrl.mockResolvedValue({ url: 'http://signed' } as SignedUrlResponse); const result = await service.getSignedUrl(payload); diff --git a/src/documents/signed-url/signed-url.service.ts b/src/documents/signed-url/signed-url.service.ts index 049fed9d..44282d5d 100644 --- a/src/documents/signed-url/signed-url.service.ts +++ b/src/documents/signed-url/signed-url.service.ts @@ -19,7 +19,7 @@ export class SignedUrlService { async getSignedUrl(req: SignedUrlRequest): Promise { try { return await this.provider.getSignedUrl(req); - } catch (e: any) { + } catch (e: unknown) { throw new InternalServerErrorException(e?.message ?? 'Failed to get signed URL'); } } diff --git a/src/email/email-webhook.controller.ts b/src/email/email-webhook.controller.ts index c0164ab4..aa907cc6 100644 --- a/src/email/email-webhook.controller.ts +++ b/src/email/email-webhook.controller.ts @@ -4,6 +4,15 @@ import { Controller, Post, Body, HttpCode } from '@nestjs/common'; import { EmailService } from './email.service'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; +interface EmailBouncePayload { + email?: string; + recipient?: string; + type?: string; + bounceType?: string; + reason?: string; + diagnosticCode?: string; +} + @ApiTags('webhooks') @Controller('webhooks/email') export class EmailWebhookController { @@ -12,7 +21,7 @@ export class EmailWebhookController { @Post('bounce') @HttpCode(200) @ApiOperation({ summary: 'Handle email bounce webhooks' }) - async handleBounce(@Body() payload: any) { + async handleBounce(@Body() payload: EmailBouncePayload) { // Basic extraction logic - in a real app, this would be provider-specific const email = payload.email || payload.recipient; const type = payload.type || (payload.bounceType === 'Hard' ? 'HARD' : 'SOFT'); diff --git a/src/email/email.processor.ts b/src/email/email.processor.ts index 5896ffdf..487c5f6c 100644 --- a/src/email/email.processor.ts +++ b/src/email/email.processor.ts @@ -5,6 +5,13 @@ import { Job } from 'bullmq'; import { MailerService } from '@nestjs-modules/mailer'; import { Logger } from '@nestjs/common'; +interface EmailJobData { + to: string; + subject: string; + template: string; + context: Record; +} + @Processor('mail') export class EmailProcessor extends WorkerHost { private readonly logger = new Logger(EmailProcessor.name); @@ -13,7 +20,7 @@ export class EmailProcessor extends WorkerHost { super(); } - async process(job: Job): Promise { + async process(job: Job): Promise { const { to, subject, template, context } = job.data; this.logger.log(`Processing email job ${job.id} for ${to} with subject: ${subject}`); diff --git a/src/email/email.service.spec.ts b/src/email/email.service.spec.ts index 4e525c8c..6d16c61d 100644 --- a/src/email/email.service.spec.ts +++ b/src/email/email.service.spec.ts @@ -1,8 +1,13 @@ import { EmailService } from './email.service'; +import { ConfigService } from '@nestjs/config'; +import { PrismaService } from '../database/prisma.service'; +import { TrackingService } from '../tracking/tracking.service'; +import { I18nService } from '../i18n/i18n.service'; +import { Queue } from 'bullmq'; describe('EmailService.handleBounce', () => { it('disables email notifications on hard bounce', async () => { - const prisma: any = { + const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', email: 'test@example.com' }), update: jest.fn().mockResolvedValue(undefined), @@ -16,11 +21,11 @@ describe('EmailService.handleBounce', () => { }; const service = new EmailService( - { get: jest.fn().mockReturnValue('http://localhost:3000/api') } as any, - prisma, - { createEmailEngagement: jest.fn() } as any, - { add: jest.fn() } as any, - { translate: jest.fn().mockReturnValue('test') } as any, + { get: jest.fn().mockReturnValue('http://localhost:3000/api') } as unknown as ConfigService, + prisma as unknown as PrismaService, + { createEmailEngagement: jest.fn() } as unknown as TrackingService, + { translate: jest.fn((key) => key) } as unknown as I18nService, + { add: jest.fn() } as unknown as Queue, ); await service.handleBounce('test@example.com', 'HARD', 'Mailbox disabled', { diff --git a/src/fraud/fraud.service.spec.ts b/src/fraud/fraud.service.spec.ts index de32688d..082f9dbc 100644 --- a/src/fraud/fraud.service.spec.ts +++ b/src/fraud/fraud.service.spec.ts @@ -6,6 +6,20 @@ import { EmailService } from '../email/email.service'; import { SmsService } from '../notifications/sms.service'; import { FraudPattern, FraudSeverity } from '../types/prisma.types'; +// FraudService's AlertPayload type and private detection helpers aren't exported; +// this describes just enough shape to call them from tests without `any`. +interface FraudServiceInternals { + createOrUpdateAlert(payload: Record): Promise<{ id: string } | null>; + findOpenAlert(payload: Record): Promise; + notifySecurityTeam(alert: unknown, isUpdate?: boolean): Promise; + blockUserForFraud( + userId: string, + alertId: string, + actorId: string, + reason: string, + ): Promise; +} + describe('FraudService', () => { let service: FraudService; @@ -45,11 +59,11 @@ describe('FraudService', () => { findFirst: jest.fn(), findMany: jest.fn(), }, - } as any; + }; const mockEmailService = { sendFraudAlertEmail: jest.fn(), - } as any; + }; const mockConfigService = { get: jest.fn((key: string) => { @@ -59,7 +73,7 @@ describe('FraudService', () => { return undefined; }), - } as any; + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -87,7 +101,7 @@ describe('FraudService', () => { mockPrismaService.loginAttempt.count.mockResolvedValue(6); const createOrUpdateAlertSpy = jest - .spyOn(service as any, 'createOrUpdateAlert') + .spyOn(service as unknown as FraudServiceInternals, 'createOrUpdateAlert') .mockResolvedValue({ id: 'alert-1' }); const result = await service.evaluateFailedLogin('user@example.com', '10.0.0.1', 'Mozilla'); @@ -128,8 +142,8 @@ describe('FraudService', () => { ]); const createOrUpdateAlertSpy = jest - .spyOn(service as any, 'createOrUpdateAlert') - .mockImplementation(async (payload: any) => payload); + .spyOn(service as unknown as FraudServiceInternals, 'createOrUpdateAlert') + .mockImplementation(async (payload: Record) => payload as { id: string }); const alerts = await service.evaluatePropertyCreated('property-1'); @@ -155,10 +169,10 @@ describe('FraudService', () => { }); it('auto-blocks a user when a critical alert is created with enforcement enabled', async () => { - jest.spyOn(service as any, 'findOpenAlert').mockResolvedValue(null); - jest.spyOn(service as any, 'notifySecurityTeam').mockResolvedValue(undefined); + jest.spyOn(service as unknown as FraudServiceInternals, 'findOpenAlert').mockResolvedValue(null); + jest.spyOn(service as unknown as FraudServiceInternals, 'notifySecurityTeam').mockResolvedValue(undefined); const blockUserForFraudSpy = jest - .spyOn(service as any, 'blockUserForFraud') + .spyOn(service as unknown as FraudServiceInternals, 'blockUserForFraud') .mockResolvedValue(undefined); mockPrismaService.fraudAlert.create.mockResolvedValue({ @@ -173,7 +187,7 @@ describe('FraudService', () => { }, }); - await (service as any).createOrUpdateAlert({ + await (service as unknown as FraudServiceInternals).createOrUpdateAlert({ userId: 'user-1', pattern: FraudPattern.TOKEN_REUSE, severity: FraudSeverity.CRITICAL, diff --git a/src/notifications/notifications.gateway.ts b/src/notifications/notifications.gateway.ts index e02ab79a..b4a81410 100644 --- a/src/notifications/notifications.gateway.ts +++ b/src/notifications/notifications.gateway.ts @@ -56,55 +56,13 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco } } - @SubscribeMessage('joinProperty') - handleJoinProperty(@ConnectedSocket() client: Socket, @MessageBody() data: { propertyId: string }) { - if (data?.propertyId) { - client.join(`property:${data.propertyId}`); - this.logger.log(`Client ${client.id} joined property room ${data.propertyId}`); - return { event: 'joinedProperty', data: { propertyId: data.propertyId } }; - } - return { event: 'error', data: { message: 'propertyId is required' } }; - } - - @SubscribeMessage('leaveProperty') - handleLeaveProperty(@ConnectedSocket() client: Socket, @MessageBody() data: { propertyId: string }) { - if (data?.propertyId) { - client.leave(`property:${data.propertyId}`); - this.logger.log(`Client ${client.id} left property room ${data.propertyId}`); - return { event: 'leftProperty', data: { propertyId: data.propertyId } }; - } - } - - @SubscribeMessage('joinTransaction') - handleJoinTransaction( - @ConnectedSocket() client: Socket, - @MessageBody() data: { transactionId: string }, - ) { - if (data?.transactionId) { - client.join(`transaction:${data.transactionId}`); - this.logger.log(`Client ${client.id} joined transaction room ${data.transactionId}`); - return { event: 'joinedTransaction', data: { transactionId: data.transactionId } }; - } - return { event: 'error', data: { message: 'transactionId is required' } }; - } - - @SubscribeMessage('leaveTransaction') - handleLeaveTransaction( - @ConnectedSocket() client: Socket, - @MessageBody() data: { transactionId: string }, - ) { - if (data?.transactionId) { - client.leave(`transaction:${data.transactionId}`); - return { event: 'leftTransaction', data: { transactionId: data.transactionId } }; - } - } - - @SubscribeMessage('joinUser') - handleJoinUser(@ConnectedSocket() client: Socket, @MessageBody() data: { userId: string }) { - const socketUserId = this.socketUsers.get(client.id); - if (socketUserId && socketUserId === data?.userId) { - client.join(`user:${data.userId}`); - return { event: 'joinedUser', data: { userId: data.userId } }; + sendToUser(userId: string, event: string, data: unknown): boolean { + const sockets = this.userSockets.get(userId); + if (sockets && sockets.length > 0) { + sockets.forEach((socketId) => { + this.server.to(socketId).emit(event, data); + }); + return true; } return { event: 'error', data: { message: 'Unauthorized to join this user room' } }; } diff --git a/src/properties/properties.service.agent.spec.ts b/src/properties/properties.service.agent.spec.ts index 884324c6..438089b4 100644 --- a/src/properties/properties.service.agent.spec.ts +++ b/src/properties/properties.service.agent.spec.ts @@ -9,7 +9,6 @@ import { CacheService } from '../cache/cache.service'; describe('PropertiesService - Agent Assignment', () => { let service: PropertiesService; - let prisma: PrismaService; const mockPrismaService = { property: { @@ -52,7 +51,6 @@ describe('PropertiesService - Agent Assignment', () => { }).compile(); service = module.get(PropertiesService); - prisma = module.get(PrismaService); }); afterEach(() => { @@ -72,7 +70,7 @@ describe('PropertiesService - Agent Assignment', () => { { sub: 'owner-1', email: 'owner@test.com', role: 'USER', type: 'access' }, ); - expect((prisma as any).propertyAgent.create).toHaveBeenCalledWith({ + expect(mockPrismaService.propertyAgent.create).toHaveBeenCalledWith({ data: { propertyId: 'prop-1', agentId: 'agent-1', @@ -127,7 +125,7 @@ describe('PropertiesService - Agent Assignment', () => { { sub: 'owner-1', email: 'owner@test.com', role: 'USER', type: 'access' }, ); - expect((prisma as any).propertyAgent.update).toHaveBeenCalledWith({ + expect(mockPrismaService.propertyAgent.update).toHaveBeenCalledWith({ where: { propertyId_agentId: { propertyId: 'prop-1', diff --git a/src/search/search-autocomplete.service.ts b/src/search/search-autocomplete.service.ts index 58ac3586..c83a0d3d 100644 --- a/src/search/search-autocomplete.service.ts +++ b/src/search/search-autocomplete.service.ts @@ -25,7 +25,7 @@ export interface Suggestion { text: string; type: 'property' | 'location' | 'feature' | 'recent' | 'popular'; count?: number; - metadata?: any; + metadata?: Record; } /** Minimum characters before suggestions are fetched */ diff --git a/src/search/search-filters.service.ts b/src/search/search-filters.service.ts index 98167721..c4210ba1 100644 --- a/src/search/search-filters.service.ts +++ b/src/search/search-filters.service.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; import { PrismaService } from '../database/prisma.service'; export interface FilterOption { @@ -17,7 +18,7 @@ export interface SavedFilter { id: string; userId: string; name: string; - filters: Record; + filters: Record; isQuickFilter: boolean; createdAt: Date; usageCount: number; @@ -25,14 +26,23 @@ export interface SavedFilter { export interface FilterCombination { operator: 'AND' | 'OR'; - filters: Record[]; + filters: Record[]; +} + +interface SaveFilterDto { + name: string; + filters: Record; + isQuickFilter?: boolean; } @Injectable() export class SearchFiltersService { constructor(private readonly prisma: PrismaService) {} - async applyFilters(whereClause: any, filters: Record): Promise { + async applyFilters( + whereClause: Prisma.PropertyWhereInput, + filters: Record, + ): Promise { const filterKeys = Object.keys(filters); for (const key of filterKeys) { @@ -82,7 +92,7 @@ export class SearchFiltersService { return whereClause; } - private applyPriceFilter(whereClause: any, price: any): any { + private applyPriceFilter(whereClause: Prisma.PropertyWhereInput, price: unknown): Prisma.PropertyWhereInput { if (price.min !== undefined || price.max !== undefined) { whereClause.price = {}; if (price.min !== undefined) { @@ -95,7 +105,7 @@ export class SearchFiltersService { return whereClause; } - private applyBedroomsFilter(whereClause: any, bedrooms: any): any { + private applyBedroomsFilter(whereClause: Prisma.PropertyWhereInput, bedrooms: unknown): Prisma.PropertyWhereInput { if (typeof bedrooms === 'number') { whereClause.bedrooms = bedrooms; } else if (bedrooms.min !== undefined || bedrooms.max !== undefined) { @@ -110,7 +120,7 @@ export class SearchFiltersService { return whereClause; } - private applyBathroomsFilter(whereClause: any, bathrooms: any): any { + private applyBathroomsFilter(whereClause: Prisma.PropertyWhereInput, bathrooms: unknown): Prisma.PropertyWhereInput { if (typeof bathrooms === 'number') { whereClause.bathrooms = bathrooms; } else if (bathrooms.min !== undefined || bathrooms.max !== undefined) { @@ -125,7 +135,7 @@ export class SearchFiltersService { return whereClause; } - private applySquareFeetFilter(whereClause: any, squareFeet: any): any { + private applySquareFeetFilter(whereClause: Prisma.PropertyWhereInput, squareFeet: unknown): Prisma.PropertyWhereInput { if (squareFeet.min !== undefined || squareFeet.max !== undefined) { whereClause.squareFeet = {}; if (squareFeet.min !== undefined) { @@ -138,7 +148,7 @@ export class SearchFiltersService { return whereClause; } - private applyPropertyTypeFilter(whereClause: any, propertyType: any): any { + private applyPropertyTypeFilter(whereClause: Prisma.PropertyWhereInput, propertyType: unknown): Prisma.PropertyWhereInput { if (Array.isArray(propertyType)) { whereClause.propertyType = { in: propertyType }; } else { @@ -147,7 +157,7 @@ export class SearchFiltersService { return whereClause; } - private applyStatusFilter(whereClause: any, status: any): any { + private applyStatusFilter(whereClause: Prisma.PropertyWhereInput, status: unknown): Prisma.PropertyWhereInput { if (Array.isArray(status)) { whereClause.status = { in: status }; } else { @@ -156,7 +166,7 @@ export class SearchFiltersService { return whereClause; } - private applyYearBuiltFilter(whereClause: any, yearBuilt: any): any { + private applyYearBuiltFilter(whereClause: Prisma.PropertyWhereInput, yearBuilt: unknown): Prisma.PropertyWhereInput { if (yearBuilt.min !== undefined || yearBuilt.max !== undefined) { whereClause.yearBuilt = {}; if (yearBuilt.min !== undefined) { @@ -169,14 +179,14 @@ export class SearchFiltersService { return whereClause; } - private applyFeaturesFilter(whereClause: any, features: string[]): any { + private applyFeaturesFilter(whereClause: Prisma.PropertyWhereInput, features: string[]): Prisma.PropertyWhereInput { if (features.length > 0) { whereClause.features = { hasSome: features }; } return whereClause; } - private applyCityFilter(whereClause: any, city: any): any { + private applyCityFilter(whereClause: Prisma.PropertyWhereInput, city: unknown): Prisma.PropertyWhereInput { if (Array.isArray(city)) { whereClause.city = { in: city }; } else { @@ -185,7 +195,7 @@ export class SearchFiltersService { return whereClause; } - private applyStateFilter(whereClause: any, state: any): any { + private applyStateFilter(whereClause: Prisma.PropertyWhereInput, state: unknown): Prisma.PropertyWhereInput { if (Array.isArray(state)) { whereClause.state = { in: state }; } else { @@ -194,7 +204,7 @@ export class SearchFiltersService { return whereClause; } - private applyDateRangeFilter(whereClause: any, dateRange: any): any { + private applyDateRangeFilter(whereClause: Prisma.PropertyWhereInput, dateRange: unknown): Prisma.PropertyWhereInput { if (dateRange.start || dateRange.end) { whereClause.createdAt = {}; if (dateRange.start) { @@ -207,7 +217,7 @@ export class SearchFiltersService { return whereClause; } - private applyCustomFilter(whereClause: any, key: string, value: any): any { + private applyCustomFilter(whereClause: Prisma.PropertyWhereInput, key: string, value: unknown): Prisma.PropertyWhereInput { whereClause[key] = value; return whereClause; } @@ -293,7 +303,7 @@ export class SearchFiltersService { ]; } - async saveFilter(userId: string, filterData: any): Promise { + async saveFilter(userId: string, filterData: SaveFilterDto): Promise { // This would typically save to database // For now, return mock data const savedFilter: SavedFilter = { @@ -331,7 +341,10 @@ export class SearchFiltersService { // For now, do nothing } - async applyFilterCombination(whereClause: any, combination: FilterCombination): Promise { + async applyFilterCombination( + whereClause: Prisma.PropertyWhereInput, + combination: FilterCombination, + ): Promise { if (combination.operator === 'AND') { const conditions = combination.filters.map((filter) => this.applyFilters({}, filter)); whereClause.AND = [...(whereClause.AND || []), ...conditions]; diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts index d6945454..10a53f0b 100644 --- a/src/search/search.controller.ts +++ b/src/search/search.controller.ts @@ -62,7 +62,7 @@ export class SearchController { @Post('filters/save') @ApiOperation({ summary: 'Save a search filter' }) @ApiResponse({ status: 201, description: 'Filter saved successfully' }) - async saveFilter(@Request() req: AuthenticatedRequest, @Body() filter: any) { + async saveFilter(@Request() req: AuthenticatedRequest, @Body() filter: Record) { return this.searchService.saveFilter(req.user.id, filter); } diff --git a/src/transactions/disputes.service.ts b/src/transactions/disputes.service.ts index e7f4e168..d75752af 100644 --- a/src/transactions/disputes.service.ts +++ b/src/transactions/disputes.service.ts @@ -9,7 +9,7 @@ import { import { PrismaService } from '../database/prisma.service'; import { NotificationsService } from '../notifications/notifications.service'; import { CreateDisputeDto, ResolveDisputeDto } from './dto/dispute.dto'; -import { DisputeStatus } from '../types/prisma.types'; +import { DisputeStatus, UserRole } from '../types/prisma.types'; @Injectable() export class DisputesService { @@ -46,7 +46,7 @@ export class DisputesService { // Notify admins (#564) const admins = await this.prisma.user.findMany({ - where: { role: 'ADMIN' as any }, + where: { role: 'ADMIN' as UserRole }, select: { id: true }, take: 10, }); diff --git a/src/transactions/timeline.service.ts b/src/transactions/timeline.service.ts index b7c29c15..e1f48cd4 100644 --- a/src/transactions/timeline.service.ts +++ b/src/transactions/timeline.service.ts @@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../database/prisma.service'; +import { Prisma } from '@prisma/client'; import { CreateMilestoneDto, UpdateMilestoneDto } from './dto/timeline.dto'; import { MilestoneStatus } from '../types/prisma.types'; @@ -38,7 +39,7 @@ export class TimelineService { throw new NotFoundException('Milestone not found'); } - const updateData: any = {}; + const updateData: Prisma.TransactionMilestoneUpdateInput = {}; if (dto.title) updateData.title = dto.title; if (dto.description) updateData.description = dto.description; if (dto.status) updateData.status = dto.status; diff --git a/src/transactions/transaction-cancellation.service.spec.ts b/src/transactions/transaction-cancellation.service.spec.ts index 6b5815c6..d8a7004c 100644 --- a/src/transactions/transaction-cancellation.service.spec.ts +++ b/src/transactions/transaction-cancellation.service.spec.ts @@ -1,5 +1,7 @@ import { NotFoundException, BadRequestException } from '@nestjs/common'; import { TransactionCancellationService } from './transaction-cancellation.service'; +import { PrismaService } from '../database/prisma.service'; +import { NotificationsService } from '../notifications/notifications.service'; const mockTx = { id: 'tx-1', @@ -29,7 +31,10 @@ describe('TransactionCancellationService', () => { beforeEach(() => { jest.clearAllMocks(); - service = new TransactionCancellationService(mockPrisma as any, mockNotifications as any); + service = new TransactionCancellationService( + mockPrisma as unknown as PrismaService, + mockNotifications as unknown as NotificationsService, + ); }); describe('cancel', () => { diff --git a/src/transactions/transaction-notes.service.ts b/src/transactions/transaction-notes.service.ts index 8d3dd298..e1f3febf 100644 --- a/src/transactions/transaction-notes.service.ts +++ b/src/transactions/transaction-notes.service.ts @@ -1,5 +1,6 @@ import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../database/prisma.service'; +import { Prisma } from '@prisma/client'; import { CreateNoteDto } from './dto/transaction-note.dto'; @Injectable() @@ -27,7 +28,7 @@ export class TransactionNotesService { const isParty = tx.buyerId === viewerId || tx.sellerId === viewerId; - const where: any = { transactionId }; + const where: Prisma.TransactionNoteWhereInput = { transactionId }; if (!isPrivileged && !isParty) { where.isPublic = true; } else if (!isPrivileged) { diff --git a/src/transactions/transaction-reminders.service.spec.ts b/src/transactions/transaction-reminders.service.spec.ts index 70bb9829..1e1764dc 100644 --- a/src/transactions/transaction-reminders.service.spec.ts +++ b/src/transactions/transaction-reminders.service.spec.ts @@ -1,4 +1,7 @@ import { TransactionRemindersService } from './transaction-reminders.service'; +import { PrismaService } from '../database/prisma.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { CacheService } from '../cache/cache.service'; const mockMilestone = { id: 'ms-1', @@ -28,9 +31,9 @@ const mockCache = { function makeService(): TransactionRemindersService { return new TransactionRemindersService( - mockPrisma as any, - mockNotifications as any, - mockCache as any, + mockPrisma as unknown as PrismaService, + mockNotifications as unknown as NotificationsService, + mockCache as unknown as CacheService, ); } @@ -142,8 +145,9 @@ describe('TransactionRemindersService', () => { it('respects buyer opt-out preference', async () => { mockCache.setNx.mockResolvedValue(true); mockPrisma.transactionMilestone.findMany.mockResolvedValue([mockMilestone]); - mockPrisma.userPreferences.findUnique.mockImplementation(({ where }: any) => - where.userId === 'buyer-1' ? { optOutReminders: true } : null, + mockPrisma.userPreferences.findUnique.mockImplementation( + ({ where }: { where: { userId: string } }) => + where.userId === 'buyer-1' ? { optOutReminders: true } : null, ); const result = await makeService().sendDeadlineReminders(); diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index ab7041dd..da2867e2 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; import { PrismaService } from '../database/prisma.service'; import { BlockchainService } from '../blockchain/blockchain.service'; import { NotificationsService } from '../notifications/notifications.service'; @@ -10,6 +11,7 @@ import { TimelineService } from './timeline.service'; import { TransactionAuditService } from './transaction-audit.service'; import { canTransitionTransactionStatus } from './transaction-status.constants'; import { TransactionStatus } from '../types/prisma.types'; +import { BlockchainTransactionDto, BlockchainVerificationResultDto } from '../blockchain/dto/blockchain.dto'; import { CreateTransactionDto, UpdateTransactionDto, @@ -23,6 +25,20 @@ import { TransactionAnalyticsQueryDto, } from './dto/transaction.dto'; +type TransactionWithFullRelations = Prisma.TransactionGetPayload<{ + include: { + property: { select: { id: true; title: true; address: true } }; + buyer: { select: { id: true; firstName: true; lastName: true; email: true } }; + seller: { select: { id: true; firstName: true; lastName: true; email: true } }; + }; +}>; + +interface UpdateEscrowDto { + escrowStatus?: string; + escrowAmount?: number; + paymentStatus?: string; +} + @Injectable() export class TransactionsService { private readonly logger = new Logger(TransactionsService.name); @@ -67,10 +83,10 @@ export class TransactionsService { buyerId: dto.buyerId, sellerId: dto.sellerId, amount: dto.amount, - type: dto.type as any, + type: dto.type as Prisma.TransactionType, status: 'PENDING', notes: dto.notes, - feeBreakdown: feeBreakdown as any, + feeBreakdown: feeBreakdown as Prisma.InputJsonValue, }, }); @@ -93,7 +109,7 @@ export class TransactionsService { const limit = query.limit ?? 20; const skip = (page - 1) * limit; - const where: any = {}; + const where: Prisma.TransactionWhereInput = {}; if (query.propertyId) where.propertyId = query.propertyId; if (query.buyerId) where.buyerId = query.buyerId; if (query.sellerId) where.sellerId = query.sellerId; @@ -119,7 +135,7 @@ export class TransactionsService { total, page, limit, - items: transactions.map((t: any) => this.toResponseDto(t)), + items: transactions.map((t) => this.toResponseDto(t)), }; } catch (error) { this.logger.error(`Failed to list transactions: ${error.message}`, error.stack); @@ -168,7 +184,7 @@ export class TransactionsService { const updated = await this.prisma.transaction.update({ where: { id }, data: { - status: dto.status as any, + status: dto.status as Prisma.TransactionStatus, notes: dto.notes, }, }); @@ -188,7 +204,10 @@ export class TransactionsService { /** * Record transaction on blockchain */ - async recordOnBlockchain(id: string, dto: RecordTransactionOnChainDto): Promise { + async recordOnBlockchain( + id: string, + dto: RecordTransactionOnChainDto, + ): Promise<{ transaction: TransactionResponseDto; blockchain: BlockchainTransactionDto }> { try { const transaction = await this.prisma.transaction.findUnique({ where: { id }, @@ -262,7 +281,7 @@ export class TransactionsService { /** * Verify transaction on blockchain */ - async verifyOnBlockchain(id: string): Promise { + async verifyOnBlockchain(id: string): Promise { try { const transaction = await this.prisma.transaction.findUnique({ where: { id }, @@ -314,7 +333,7 @@ export class TransactionsService { * Get transaction analytics for operational dashboards. */ async getAnalytics(query: TransactionAnalyticsQueryDto = {}): Promise { - const where: Record = {}; + const where: Prisma.TransactionWhereInput = {}; const maxDays = query.maxDays ?? 365; if (query.startDate && query.endDate) { @@ -359,17 +378,17 @@ export class TransactionsService { }); const totalTransactions = transactions.length; - const completedTransactions = transactions.filter((t: any) => t.status === 'COMPLETED'); - const pendingTransactions = transactions.filter((t: any) => t.status === 'PENDING').length; - const cancelledTransactions = transactions.filter((t: any) => t.status === 'CANCELLED').length; + const completedTransactions = transactions.filter((t) => t.status === 'COMPLETED'); + const pendingTransactions = transactions.filter((t) => t.status === 'PENDING').length; + const cancelledTransactions = transactions.filter((t) => t.status === 'CANCELLED').length; const totalVolume = this.roundCurrency( - transactions.reduce((sum: number, transaction: any) => { + transactions.reduce((sum: number, transaction) => { return sum + this.toNumber(transaction.amount); }, 0), ); const revenue = this.roundCurrency( - completedTransactions.reduce((sum: number, transaction: any) => { + completedTransactions.reduce((sum: number, transaction) => { return sum + this.toNumber(transaction.amount); }, 0), ); @@ -421,7 +440,7 @@ export class TransactionsService { const updated = await this.prisma.transaction.update({ where: { id: transactionId }, - data: { status: status as any }, + data: { status: status as Prisma.TransactionStatus }, }); // Audit log the transition (#557) @@ -458,7 +477,7 @@ export class TransactionsService { type: string; }, user: { sub: string; email: string; role: string; type: string }, - ): Promise { + ): Promise { const [property, buyer, seller] = await Promise.all([ this.prisma.property.findUnique({ where: { id: dto.propertyId } }), this.prisma.user.findUnique({ where: { id: dto.buyerId } }), @@ -475,7 +494,7 @@ export class TransactionsService { buyerId: dto.buyerId, sellerId: dto.sellerId, amount: dto.amount, - type: dto.type as any, + type: dto.type as Prisma.TransactionType, status: 'PENDING', }, include: { @@ -500,10 +519,10 @@ export class TransactionsService { strategyType: string; estimatedTaxRate?: number; explanation?: string; - metadata?: Record; + metadata?: Record; }, user: { sub: string; email: string; role: string; type: string }, - ): Promise { + ): Promise { const transaction = await this.prisma.transaction.findUnique({ where: { id: transactionId }, include: { property: { select: { id: true, city: true, state: true, country: true } } }, @@ -530,7 +549,7 @@ export class TransactionsService { version: 1, }, }) - .then((result: any) => { + .then((result) => { this.logger.log( `Tax strategy created for transaction ${transactionId}: ${dto.strategyType}`, ); @@ -563,7 +582,7 @@ export class TransactionsService { jurisdiction?: string; }, user: { sub: string; email: string; role: string; type: string }, - ): Promise { + ): Promise { const existing = await this.prisma.transactionTaxStrategy.findFirst({ where: { id: strategyId, transactionId }, }); @@ -576,7 +595,7 @@ export class TransactionsService { data: { ...(dto.strategyType && { strategyType: dto.strategyType }), ...(dto.jurisdiction && { jurisdiction: dto.jurisdiction }), - version: (existing as any).version + 1, + version: existing.version + 1, }, }); } @@ -584,14 +603,14 @@ export class TransactionsService { /** * Convert transaction to response DTO */ - async updateEscrow(transactionId: string, dto: any, actorId?: string) { + async updateEscrow(transactionId: string, dto: UpdateEscrowDto, actorId?: string) { const transaction = await this.prisma.transaction.findUnique({ where: { id: transactionId }, }); if (!transaction) throw new NotFoundException('Transaction not found'); this.logger.log(`Updating escrow for transaction ${transactionId}`); - const data: any = {}; + const data: Prisma.TransactionUpdateInput = {}; if (dto.escrowStatus !== undefined) data.escrowStatus = dto.escrowStatus; if (dto.escrowAmount !== undefined) data.escrowAmount = dto.escrowAmount; if (dto.paymentStatus !== undefined) data.paymentStatus = dto.paymentStatus; @@ -615,7 +634,7 @@ export class TransactionsService { return this.toResponseDto(updated); } - private toResponseDto(transaction: any): TransactionResponseDto { + private toResponseDto(transaction: Prisma.Transaction): TransactionResponseDto { return { id: transaction.id, propertyId: transaction.propertyId, diff --git a/src/trust-score/trust-score.service.spec.ts b/src/trust-score/trust-score.service.spec.ts index aecdc89c..1756bbac 100644 --- a/src/trust-score/trust-score.service.spec.ts +++ b/src/trust-score/trust-score.service.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { TrustScoreService } from './trust-score.service'; +import { TrustScoreService, TrustScoreBreakdown } from './trust-score.service'; import { PrismaService } from '../database/prisma.service'; describe('TrustScoreService', () => { @@ -54,7 +54,7 @@ describe('TrustScoreService', () => { verificationDocument: { findFirst: jest.fn(), }, - } as any; + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -174,7 +174,7 @@ describe('TrustScoreService', () => { jest.spyOn(service, 'calculateTrustScore').mockResolvedValue({ userId: 'user-123', score: 80, - breakdown: {} as any, + breakdown: {} as TrustScoreBreakdown, lastUpdated: new Date(), nextUpdateTime: new Date(), }); @@ -222,7 +222,7 @@ describe('TrustScoreService', () => { jest.spyOn(service, 'calculateTrustScore').mockResolvedValue({ userId: 'user-1', score: 75, - breakdown: {} as any, + breakdown: {} as TrustScoreBreakdown, lastUpdated: new Date(), nextUpdateTime: new Date(), }); diff --git a/src/trust-score/trust-score.service.ts b/src/trust-score/trust-score.service.ts index e3bd3951..b8433e60 100644 --- a/src/trust-score/trust-score.service.ts +++ b/src/trust-score/trust-score.service.ts @@ -53,7 +53,7 @@ export class TrustScoreService { data: { trustScore: totalScore, lastTrustScoreUpdate: new Date(), - } as any, + }, }); const nextUpdateTime = new Date(); diff --git a/src/users/activity-log.controller.ts b/src/users/activity-log.controller.ts index 4e053807..54087d09 100644 --- a/src/users/activity-log.controller.ts +++ b/src/users/activity-log.controller.ts @@ -5,6 +5,7 @@ import { ActivityLogService } from './activity-log.service'; import { GetActivityLogsDto } from './dto/activity-log.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { AuthUserPayload } from '../auth/types/auth-user.type'; @UseGuards(JwtAuthGuard) @Controller('users/activity-logs') @@ -12,7 +13,7 @@ export class ActivityLogController { constructor(private readonly activityLogService: ActivityLogService) {} @Get() - getActivityLogs(@CurrentUser() user: any, @Query() filters: GetActivityLogsDto) { + getActivityLogs(@CurrentUser() user: AuthUserPayload, @Query() filters: GetActivityLogsDto) { return this.activityLogService.findByUserId(user.id, filters); } } diff --git a/src/users/dto/activity-log.dto.ts b/src/users/dto/activity-log.dto.ts index 9bddfd54..db9014de 100644 --- a/src/users/dto/activity-log.dto.ts +++ b/src/users/dto/activity-log.dto.ts @@ -27,7 +27,7 @@ export class CreateActivityLogDto { @IsOptional() @IsJSON() - metadata?: any; + metadata?: Record; } export class GetActivityLogsDto { diff --git a/src/users/user-import.controller.ts b/src/users/user-import.controller.ts index 0e321f5b..594f5a56 100644 --- a/src/users/user-import.controller.ts +++ b/src/users/user-import.controller.ts @@ -23,7 +23,7 @@ export class UserImportController { @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) @UseInterceptors(FileInterceptor('file')) - async importCsv(@UploadedFile() file: any) { + async importCsv(@UploadedFile() file: Express.Multer.File) { if (!file) { throw new BadRequestException('No file uploaded'); } diff --git a/src/users/user-import.service.ts b/src/users/user-import.service.ts index 24491ca7..4ad04df2 100644 --- a/src/users/user-import.service.ts +++ b/src/users/user-import.service.ts @@ -5,6 +5,7 @@ import { PrismaService } from '../database/prisma.service'; import { parse } from 'csv-parse/sync'; import { hashPassword } from '../auth/security.utils'; import { UserRole } from '../types/prisma.types'; +import { Prisma } from '@prisma/client'; interface UserImportRecord { email: string; @@ -45,7 +46,7 @@ export class UserImportService { throw new BadRequestException('CSV file is empty'); } - const usersToCreate: any[] = []; + const usersToCreate: Prisma.UserCreateInput[] = []; for (let i = 0; i < records.length; i++) { const record = records[i]; diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 14099b8e..1a4cf8be 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import * as crypto from 'crypto'; import { PrismaService } from '../database/prisma.service'; +import { Prisma } from '@prisma/client'; import { CreateUserDto, SearchUsersDto, UpdatePreferencesDto, UpdateUserDto } from './dto/user.dto'; import { DeactivateAccountDto, ReactivateAccountDto } from './dto/deactivation.dto'; import { @@ -98,7 +99,7 @@ export class UsersService implements OnModuleInit { } // Build update data — only include provided fields - const updateData: any = {}; + const updateData: Prisma.UserUpdateInput = {}; if (data.firstName !== undefined) updateData.firstName = data.firstName; if (data.lastName !== undefined) updateData.lastName = data.lastName; diff --git a/src/users/verification-documents.service.ts b/src/users/verification-documents.service.ts index 1aa6247b..fb99d6bf 100644 --- a/src/users/verification-documents.service.ts +++ b/src/users/verification-documents.service.ts @@ -7,7 +7,7 @@ import { ReviewVerificationDocumentDto, UpdateVerificationDocumentDto, } from './dto/verification-document.dto'; -import { VerificationStatus } from '@prisma/client'; +import { VerificationStatus, Prisma } from '@prisma/client'; @Injectable() export class VerificationDocumentsService { @@ -101,7 +101,7 @@ export class VerificationDocumentsService { async findAllForAdmin(page = 1, limit = 20, status?: string) { const skip = (page - 1) * limit; - const where: any = status ? { status: status as VerificationStatus } : {}; + const where: Prisma.VerificationDocumentWhereInput = status ? { status: status as VerificationStatus } : {}; const [documents, total] = await Promise.all([ this.prisma.verificationDocument.findMany({ diff --git a/src/versioning/version-routing.service.ts b/src/versioning/version-routing.service.ts index 09b36d9b..d6c8d7f2 100644 --- a/src/versioning/version-routing.service.ts +++ b/src/versioning/version-routing.service.ts @@ -8,7 +8,7 @@ import { Injectable } from '@nestjs/common'; import { ApiVersionEnum } from './api-version.constants'; -export interface VersionedResponse { +export interface VersionedResponse { apiVersion: ApiVersionEnum; data: T; timestamp: string; diff --git a/src/versioning/version.guard.ts b/src/versioning/version.guard.ts index 6614ec14..8e51fdf4 100644 --- a/src/versioning/version.guard.ts +++ b/src/versioning/version.guard.ts @@ -9,6 +9,11 @@ import { Injectable, CanActivate, ExecutionContext, BadRequestException } from ' import { Reflector } from '@nestjs/core'; import { ApiVersionEnum, SUPPORTED_API_VERSIONS, isVersionSunset } from './api-version.constants'; import { API_VERSION_KEY } from './api-version.decorator'; +import { Request } from 'express'; + +interface RequestWithApiVersion extends Request { + apiVersion?: ApiVersionEnum; +} @Injectable() export class VersionGuard implements CanActivate { @@ -16,7 +21,7 @@ export class VersionGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); - const currentVersion = (request as any).apiVersion as ApiVersionEnum; + const currentVersion = (request as RequestWithApiVersion).apiVersion; // Get supported versions for this endpoint from metadata const endpointVersions = this.reflector.get( diff --git a/src/versioning/version.middleware.ts b/src/versioning/version.middleware.ts index 69e599e2..01935f5f 100644 --- a/src/versioning/version.middleware.ts +++ b/src/versioning/version.middleware.ts @@ -14,6 +14,10 @@ import { isVersionSunset, } from './api-version.constants'; +interface RequestWithApiVersion extends Request { + apiVersion?: ApiVersionEnum; +} + @Injectable() export class VersionMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { @@ -35,7 +39,7 @@ export class VersionMiddleware implements NestMiddleware { } // Store version in request object for use in controllers - (req as any).apiVersion = version; + (req as RequestWithApiVersion).apiVersion = version; next(); } diff --git a/src/webhooks/webhooks.service.spec.ts b/src/webhooks/webhooks.service.spec.ts index f8b8317c..b74b1ee8 100644 --- a/src/webhooks/webhooks.service.spec.ts +++ b/src/webhooks/webhooks.service.spec.ts @@ -2,6 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { WebhooksService } from './webhooks.service'; import { PrismaService } from '../database/prisma.service'; +import { NotFoundException } from '@nestjs/common'; +import { CreateWebhookDto } from './webhook.dto'; + +const mockPrisma = {}; describe('WebhooksService', () => { let service: WebhooksService; @@ -34,25 +38,34 @@ describe('WebhooksService', () => { }); describe('create', () => { - it('should create a webhook', async () => { - const dto = { + it('creates a webhook and returns it with the plaintext secret', async () => { + const dto: CreateWebhookDto = { url: 'https://example.com/hook', - eventTypes: ['property.created'] as any, - secret: 'test-secret', - }; + eventTypes: ['transaction.created'], + description: 'test webhook', + } as CreateWebhookDto; + prisma.webhook.create.mockResolvedValue({ id: 'wh-1', userId: 'user-1', url: dto.url, - eventTypes: dto.eventTypes, - secret: dto.secret, - status: 'ACTIVE', - createdAt: new Date(), + secret: 'stored-secret', + events: dto.eventTypes, + description: dto.description, }); const result = await service.create('user-1', dto); - expect(result).toBeDefined(); - expect(prisma.webhook.create).toHaveBeenCalled(); + + expect(prisma.webhook.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + userId: 'user-1', + url: dto.url, + events: dto.eventTypes, + description: dto.description, + secret: expect.any(String), + }), + }); + expect(result).toHaveProperty('secret'); }); });