fix: resolve all ESLint errors and TypeScript compilation errors
- Auto-fixed 712 import ordering errors via `pnpm lint --fix` - Manually fixed 13 remaining ESLint errors: - Prefixed unused vars with _ (mockAdminUser, params) - Removed unused imports (UnauthorizedException, vi, screen) - Moved imports above vi.mock() calls to fix import group ordering - Removed eslint-disable for non-existent rules - Fixed empty object pattern in Playwright fixture - Fixed ~40 TypeScript TS4111 index signature errors in test files: - Used bracket notation for Record<string, unknown> property access - Added missing PropertyMedia fields (id, order, caption) to test data - Fixed pre-existing test failures in rate-limit guard specs: - Added NODE_ENV override to bypass test-mode skip in guard Both `pnpm lint` and `pnpm typecheck` now exit 0 cleanly. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import type { AgentPublicProfileData, IAgentRepository } from '../../domain/repositories/agent.repository';
|
||||
import { GetAgentPublicProfileHandler } from '../queries/get-agent-public-profile/get-agent-public-profile.handler';
|
||||
import { GetAgentPublicProfileQuery } from '../queries/get-agent-public-profile/get-agent-public-profile.query';
|
||||
|
||||
describe('GetAgentPublicProfileHandler', () => {
|
||||
let handler: GetAgentPublicProfileHandler;
|
||||
let mockAgentRepo: { [K in keyof IAgentRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
const mockProfile: AgentPublicProfileData = {
|
||||
id: 'agent-1',
|
||||
fullName: 'Nguyễn Văn A',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
phone: '0901234567',
|
||||
email: 'agent@example.com',
|
||||
agency: 'Công ty BĐS ABC',
|
||||
licenseNumber: 'BDS-001',
|
||||
bio: 'Chuyên viên bất động sản khu vực Quận 7',
|
||||
qualityScore: 85,
|
||||
totalDeals: 50,
|
||||
isVerified: true,
|
||||
serviceAreas: ['Quận 7', 'Quận 2', 'Thủ Đức'],
|
||||
memberSince: '2024-01-15',
|
||||
activeListings: [
|
||||
{
|
||||
id: 'listing-1',
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5000000000',
|
||||
status: 'ACTIVE',
|
||||
property: {
|
||||
id: 'prop-1',
|
||||
title: 'Căn hộ cao cấp Quận 7',
|
||||
propertyType: 'APARTMENT',
|
||||
address: '123 Nguyễn Hữu Thọ',
|
||||
district: 'Quận 7',
|
||||
city: 'TP.HCM',
|
||||
areaM2: 75,
|
||||
bedrooms: 2,
|
||||
bathrooms: 2,
|
||||
imageUrl: 'https://example.com/image.jpg',
|
||||
},
|
||||
},
|
||||
],
|
||||
avgReviewRating: 4.5,
|
||||
totalReviews: 20,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockAgentRepo = {
|
||||
findByUserId: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
save: vi.fn(),
|
||||
getDashboard: vi.fn(),
|
||||
getPublicProfile: vi.fn(),
|
||||
getQualityScoreInputs: vi.fn(),
|
||||
};
|
||||
|
||||
handler = new GetAgentPublicProfileHandler(mockAgentRepo as any);
|
||||
});
|
||||
|
||||
it('returns full public profile for an agent', async () => {
|
||||
mockAgentRepo.getPublicProfile.mockResolvedValue(mockProfile);
|
||||
|
||||
const query = new GetAgentPublicProfileQuery('agent-1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result).toEqual(mockProfile);
|
||||
expect(result!.id).toBe('agent-1');
|
||||
expect(result!.fullName).toBe('Nguyễn Văn A');
|
||||
expect(result!.qualityScore).toBe(85);
|
||||
expect(result!.activeListings).toHaveLength(1);
|
||||
expect(result!.serviceAreas).toContain('Quận 7');
|
||||
expect(mockAgentRepo.getPublicProfile).toHaveBeenCalledWith('agent-1');
|
||||
});
|
||||
|
||||
it('returns null when agent does not exist', async () => {
|
||||
mockAgentRepo.getPublicProfile.mockResolvedValue(null);
|
||||
|
||||
const query = new GetAgentPublicProfileQuery('nonexistent-agent');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockAgentRepo.getPublicProfile).toHaveBeenCalledWith('nonexistent-agent');
|
||||
});
|
||||
|
||||
it('returns profile with empty active listings', async () => {
|
||||
const profileNoListings: AgentPublicProfileData = {
|
||||
...mockProfile,
|
||||
activeListings: [],
|
||||
totalDeals: 0,
|
||||
};
|
||||
mockAgentRepo.getPublicProfile.mockResolvedValue(profileNoListings);
|
||||
|
||||
const query = new GetAgentPublicProfileQuery('agent-1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result!.activeListings).toHaveLength(0);
|
||||
expect(result!.totalDeals).toBe(0);
|
||||
});
|
||||
|
||||
it('returns profile with null optional fields', async () => {
|
||||
const profileMinimal: AgentPublicProfileData = {
|
||||
...mockProfile,
|
||||
avatarUrl: null,
|
||||
email: null,
|
||||
agency: null,
|
||||
licenseNumber: null,
|
||||
bio: null,
|
||||
};
|
||||
mockAgentRepo.getPublicProfile.mockResolvedValue(profileMinimal);
|
||||
|
||||
const query = new GetAgentPublicProfileQuery('agent-1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result!.avatarUrl).toBeNull();
|
||||
expect(result!.email).toBeNull();
|
||||
expect(result!.agency).toBeNull();
|
||||
expect(result!.licenseNumber).toBeNull();
|
||||
expect(result!.bio).toBeNull();
|
||||
expect(result!.fullName).toBe('Nguyễn Văn A');
|
||||
});
|
||||
|
||||
it('returns profile for unverified agent with zero reviews', async () => {
|
||||
const unverifiedProfile: AgentPublicProfileData = {
|
||||
...mockProfile,
|
||||
isVerified: false,
|
||||
avgReviewRating: 0,
|
||||
totalReviews: 0,
|
||||
qualityScore: 0,
|
||||
};
|
||||
mockAgentRepo.getPublicProfile.mockResolvedValue(unverifiedProfile);
|
||||
|
||||
const query = new GetAgentPublicProfileQuery('agent-new');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result!.isVerified).toBe(false);
|
||||
expect(result!.avgReviewRating).toBe(0);
|
||||
expect(result!.totalReviews).toBe(0);
|
||||
expect(result!.qualityScore).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { RecalculateQualityScoreCommand } from '../../application/commands/recalculate-quality-score/recalculate-quality-score.command';
|
||||
import { GetAgentDashboardQuery } from '../../application/queries/get-agent-dashboard/get-agent-dashboard.query';
|
||||
import { GetAgentPublicProfileQuery } from '../../application/queries/get-agent-public-profile/get-agent-public-profile.query';
|
||||
import { AgentsController } from '../controllers/agents.controller';
|
||||
|
||||
describe('AgentsController', () => {
|
||||
let controller: AgentsController;
|
||||
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
|
||||
let mockQueryBus: { execute: ReturnType<typeof vi.fn> };
|
||||
|
||||
const mockAgentUser = { sub: 'user-1', phone: '0901234567', role: 'AGENT' };
|
||||
const _mockAdminUser = { sub: 'admin-1', phone: '0901234568', role: 'ADMIN' };
|
||||
|
||||
beforeEach(() => {
|
||||
mockCommandBus = { execute: vi.fn() };
|
||||
mockQueryBus = { execute: vi.fn() };
|
||||
controller = new AgentsController(mockCommandBus as any, mockQueryBus as any);
|
||||
});
|
||||
|
||||
describe('GET /agents/me/dashboard — getDashboard', () => {
|
||||
it('dispatches GetAgentDashboardQuery with current user', async () => {
|
||||
const mockDashboard = {
|
||||
agentId: 'agent-1',
|
||||
qualityScore: 85,
|
||||
totalDeals: 12,
|
||||
responseTimeAvg: 600,
|
||||
isVerified: true,
|
||||
totalLeads: 30,
|
||||
leadsByStatus: { NEW: 5, CONTACTED: 10 },
|
||||
conversionRate: 0.167,
|
||||
totalInquiries: 45,
|
||||
unreadInquiries: 3,
|
||||
totalListings: 15,
|
||||
activeListings: 10,
|
||||
avgReviewRating: 4.5,
|
||||
totalReviews: 20,
|
||||
};
|
||||
mockQueryBus.execute.mockResolvedValue(mockDashboard);
|
||||
|
||||
const result = await controller.getDashboard(mockAgentUser as any);
|
||||
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledWith(
|
||||
expect.any(GetAgentDashboardQuery),
|
||||
);
|
||||
const query = mockQueryBus.execute.mock.calls[0]![0] as GetAgentDashboardQuery;
|
||||
expect(query.userId).toBe('user-1');
|
||||
expect(result).toEqual(mockDashboard);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /agents/:agentId/profile — getPublicProfile', () => {
|
||||
it('dispatches GetAgentPublicProfileQuery and returns profile', async () => {
|
||||
const mockProfile = {
|
||||
id: 'agent-1',
|
||||
fullName: 'Nguyễn Văn A',
|
||||
avatarUrl: null,
|
||||
phone: '0901234567',
|
||||
email: null,
|
||||
agency: null,
|
||||
licenseNumber: null,
|
||||
bio: null,
|
||||
qualityScore: 85,
|
||||
totalDeals: 12,
|
||||
isVerified: true,
|
||||
serviceAreas: ['Quận 7'],
|
||||
memberSince: '2024-01-15',
|
||||
activeListings: [],
|
||||
avgReviewRating: 4.5,
|
||||
totalReviews: 20,
|
||||
};
|
||||
mockQueryBus.execute.mockResolvedValue(mockProfile);
|
||||
|
||||
const result = await controller.getPublicProfile('agent-1');
|
||||
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledWith(
|
||||
expect.any(GetAgentPublicProfileQuery),
|
||||
);
|
||||
const query = mockQueryBus.execute.mock.calls[0]![0] as GetAgentPublicProfileQuery;
|
||||
expect(query.agentId).toBe('agent-1');
|
||||
expect(result).toEqual(mockProfile);
|
||||
});
|
||||
|
||||
it('throws NotFoundException when profile is null', async () => {
|
||||
mockQueryBus.execute.mockResolvedValue(null);
|
||||
|
||||
await expect(controller.getPublicProfile('nonexistent')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
await expect(controller.getPublicProfile('nonexistent')).rejects.toThrow(
|
||||
'Không tìm thấy môi giới',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /agents/:agentId/recalculate-score — recalculateScore', () => {
|
||||
it('dispatches RecalculateQualityScoreCommand and returns success message', async () => {
|
||||
mockCommandBus.execute.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.recalculateScore('agent-1');
|
||||
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledWith(
|
||||
expect.any(RecalculateQualityScoreCommand),
|
||||
);
|
||||
const cmd = mockCommandBus.execute.mock.calls[0]![0] as RecalculateQualityScoreCommand;
|
||||
expect(cmd.agentId).toBe('agent-1');
|
||||
expect(result).toEqual({ message: 'Quality score recalculated' });
|
||||
});
|
||||
});
|
||||
});
|
||||
5
apps/api/src/modules/reviews/application/index.ts
Normal file
5
apps/api/src/modules/reviews/application/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { CreateReviewCommand } from './commands/create-review/create-review.command';
|
||||
export { DeleteReviewCommand } from './commands/delete-review/delete-review.command';
|
||||
export { GetReviewsByTargetQuery } from './queries/get-reviews-by-target/get-reviews-by-target.query';
|
||||
export { GetReviewsByUserQuery } from './queries/get-reviews-by-user/get-reviews-by-user.query';
|
||||
export { GetAverageRatingQuery } from './queries/get-average-rating/get-average-rating.query';
|
||||
1
apps/api/src/modules/reviews/domain/entities/index.ts
Normal file
1
apps/api/src/modules/reviews/domain/entities/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ReviewEntity, type ReviewProps } from './review.entity';
|
||||
2
apps/api/src/modules/reviews/domain/events/index.ts
Normal file
2
apps/api/src/modules/reviews/domain/events/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ReviewCreatedEvent } from './review-created.event';
|
||||
export { ReviewDeletedEvent } from './review-deleted.event';
|
||||
4
apps/api/src/modules/reviews/domain/index.ts
Normal file
4
apps/api/src/modules/reviews/domain/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './entities';
|
||||
export * from './value-objects';
|
||||
export * from './events';
|
||||
export * from './repositories';
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
REVIEW_REPOSITORY,
|
||||
type IReviewRepository,
|
||||
type PaginatedResult,
|
||||
} from './review.repository';
|
||||
export { type ReviewItemData, type ReviewStatsData } from './review-read.dto';
|
||||
@@ -0,0 +1 @@
|
||||
export { Rating } from './rating.vo';
|
||||
Reference in New Issue
Block a user