Files
goodgo-platform/docs/audits/ADMIN_MODULE_TEST_ANALYSIS.md
Ho Ngoc Hai b8512ebff4 docs: consolidate audit and analysis reports into docs/audits/
Move 36 root-level audit/analysis documents and 7 web app audit documents
into docs/audits/ directory to declutter the project root. Remove stale
EXPLORATION_SUMMARY.txt.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 01:37:50 +07:00

758 lines
24 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Admin Module - Untested Handlers/Source Files Analysis
## Executive Summary
This document provides a comprehensive analysis of untested source files in the admin module (`apps/api/src/modules/admin/`), with detailed code listings and testing patterns for reference.
**Analysis Date:** 2026-04-11
---
## 1. UNTESTED HANDLER FILES
### 1.1 reject-listing.handler.ts (Commands)
**Path:** `apps/api/src/modules/admin/application/commands/reject-listing/reject-listing.handler.ts`
**Status:** ❌ NO TEST FILE FOUND
#### Source Code:
```typescript
import { Inject } from '@nestjs/common';
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings';
import { NotFoundException, ValidationException } from '@modules/shared';
import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event';
import { RejectListingCommand } from './reject-listing.command';
export interface RejectListingResult {
listingId: string;
status: string;
message: string;
}
@CommandHandler(RejectListingCommand)
export class RejectListingHandler implements ICommandHandler<RejectListingCommand> {
constructor(
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
private readonly eventBus: EventBus,
) {}
async execute(command: RejectListingCommand): Promise<RejectListingResult> {
const listing = await this.listingRepo.findById(command.listingId);
if (!listing) {
throw new NotFoundException('Listing không tồn tại');
}
if (listing.status !== 'PENDING_REVIEW') {
throw new ValidationException(
`Listing đang ở trạng thái ${listing.status}, chỉ có thể từ chối listing đang chờ duyệt`,
{ currentStatus: listing.status },
);
}
listing.reject(command.reason);
await this.listingRepo.update(listing);
this.eventBus.publish(
new ListingRejectedEvent(listing.id, command.adminId, command.reason),
);
return {
listingId: listing.id,
status: 'REJECTED',
message: 'Listing đã bị từ chối',
};
}
}
```
#### Associated Command Class:
```typescript
export class RejectListingCommand {
constructor(
public readonly listingId: string,
public readonly adminId: string,
public readonly reason: string,
) {}
}
```
#### Key Testing Points:
- ✅ Successfully rejects a PENDING_REVIEW listing
- ✅ Throws NotFoundException when listing doesn't exist
- ✅ Throws ValidationException when listing is not in PENDING_REVIEW status
- ✅ Calls listingRepo.update() exactly once
- ✅ Publishes ListingRejectedEvent with correct data
- ✅ Returns correct RejectListingResult with status 'REJECTED'
---
### 1.2 get-revenue-stats.handler.ts (Queries)
**Path:** `apps/api/src/modules/admin/application/queries/get-revenue-stats/get-revenue-stats.handler.ts`
**Status:** ❌ NO TEST FILE FOUND
#### Source Code:
```typescript
import { Inject } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type RevenueStatsItem } from '../../../domain/repositories/admin-query.repository';
import { GetRevenueStatsQuery } from './get-revenue-stats.query';
@QueryHandler(GetRevenueStatsQuery)
export class GetRevenueStatsHandler implements IQueryHandler<GetRevenueStatsQuery> {
constructor(
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
) {}
async execute(query: GetRevenueStatsQuery): Promise<RevenueStatsItem[]> {
return this.adminQueryRepo.getRevenueStats(query.startDate, query.endDate, query.groupBy);
}
}
```
#### Associated Query Class:
```typescript
export class GetRevenueStatsQuery {
constructor(
public readonly startDate: Date,
public readonly endDate: Date,
public readonly groupBy: 'day' | 'month' = 'month',
) {}
}
```
#### RevenueStatsItem Interface:
```typescript
export interface RevenueStatsItem {
period: string;
totalRevenue: bigint;
subscriptionRevenue: bigint;
listingFeeRevenue: bigint;
featuredListingRevenue: bigint;
transactionCount: number;
}
```
#### Key Testing Points:
- ✅ Returns RevenueStatsItem[] from repository
- ✅ Passes correct startDate, endDate, and groupBy parameters
- ✅ Calls adminQueryRepo.getRevenueStats() exactly once
- ✅ Supports both 'day' and 'month' groupBy values
- ✅ Default groupBy is 'month'
- ✅ Handles empty results (empty array)
---
### 1.3 user-deactivated.listener.ts (Listeners)
**Path:** `apps/api/src/modules/admin/application/listeners/user-deactivated.listener.ts`
**Status:** ❌ NO TEST FILE FOUND
#### Source Code:
```typescript
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { type UserDeactivatedEvent } from '@modules/auth';
import { type LoggerService, type PrismaService } from '@modules/shared';
@Injectable()
export class UserDeactivatedListener {
constructor(
private readonly prisma: PrismaService,
private readonly logger: LoggerService,
) {}
@OnEvent('user.deactivated', { async: true })
async handle(event: UserDeactivatedEvent): Promise<void> {
this.logger.log(`Handling user.deactivated for user ${event.aggregateId}`, 'UserDeactivatedListener');
const deactivated = await this.prisma.listing.updateMany({
where: {
sellerId: event.aggregateId,
status: { in: ['ACTIVE', 'PENDING_REVIEW'] },
},
data: { status: 'EXPIRED' },
});
this.logger.log(
`Expired ${deactivated.count} listings for deactivated user ${event.aggregateId}`,
'UserDeactivatedListener',
);
}
}
```
#### Key Testing Points:
- ✅ Handles 'user.deactivated' event (async: true)
- ✅ Updates listings with status ACTIVE or PENDING_REVIEW to EXPIRED
- ✅ Only updates listings for the deactivated seller
- ✅ Logs initial event handling
- ✅ Logs number of expired listings after operation
- ✅ Returns void (Promise<void>)
- ✅ Handles case with 0 listings updated
- ✅ Handles case with multiple listings updated
---
## 2. EXISTING TEST FILES STRUCTURE & PATTERNS
### 2.1 Test File for Reference: approve-listing.handler.spec.ts
**Path:** `apps/api/src/modules/admin/application/__tests__/approve-listing.handler.spec.ts`
#### Complete Source Code:
```typescript
import { ListingEntity } from '@modules/listings/domain/entities/listing.entity';
import { type IListingRepository } from '@modules/listings/domain/repositories/listing.repository';
import { Price } from '@modules/listings/domain/value-objects/price.vo';
import { ApproveListingCommand } from '../commands/approve-listing/approve-listing.command';
import { ApproveListingHandler } from '../commands/approve-listing/approve-listing.handler';
function createPendingListing(id = 'listing-1'): ListingEntity {
const price = Price.create(1_000_000_000n).unwrap();
const listing = ListingEntity.createNew(
id, 'prop-1', 'seller-1', 'SALE', price, 100, 'agent-1',
);
listing.submitForReview();
listing.clearDomainEvents();
return listing;
}
describe('ApproveListingHandler', () => {
let handler: ApproveListingHandler;
let mockListingRepo: { [K in keyof IListingRepository]: ReturnType<typeof vi.fn> };
let mockEventBus: { publish: ReturnType<typeof vi.fn> };
beforeEach(() => {
mockListingRepo = {
findById: vi.fn(),
findByIdWithProperty: vi.fn(),
save: vi.fn(),
update: vi.fn(),
search: vi.fn(),
findByStatus: vi.fn(),
findBySellerId: vi.fn(),
};
mockEventBus = { publish: vi.fn() };
handler = new ApproveListingHandler(
mockListingRepo as any,
mockEventBus as any,
);
});
it('approves a pending listing successfully', async () => {
const listing = createPendingListing();
mockListingRepo.findById.mockResolvedValue(listing);
mockListingRepo.update.mockResolvedValue(undefined);
const command = new ApproveListingCommand('listing-1', 'admin-1', 'Looks good');
const result = await handler.execute(command);
expect(result.status).toBe('ACTIVE');
expect(result.listingId).toBe('listing-1');
expect(mockListingRepo.update).toHaveBeenCalledTimes(1);
expect(mockEventBus.publish).toHaveBeenCalled();
});
it('throws NotFoundException when listing does not exist', async () => {
mockListingRepo.findById.mockResolvedValue(null);
const command = new ApproveListingCommand('nonexistent', 'admin-1');
await expect(handler.execute(command)).rejects.toThrow('Listing không tồn tại');
});
it('throws ValidationException when listing is not pending review', async () => {
const price = Price.create(500_000_000n).unwrap();
const listing = ListingEntity.createNew(
'listing-1', 'prop-1', 'seller-1', 'SALE', price, 80,
);
listing.clearDomainEvents();
mockListingRepo.findById.mockResolvedValue(listing);
const command = new ApproveListingCommand('listing-1', 'admin-1');
await expect(handler.execute(command)).rejects.toThrow(/trạng thái/);
});
});
```
#### Key Patterns Used:
1. **Factory Function:** `createPendingListing()` creates test entities
2. **Mock Setup:** Typed mocks using `ReturnType<typeof vi.fn>`
3. **Initialization:** beforeEach() sets up fresh mocks for each test
4. **Entity Testing:** Uses actual domain entities with state transitions
5. **Domain Events:** Clears domain events after setup with `.clearDomainEvents()`
6. **Test Structure:**
- Happy path (success case)
- Error cases (NotFoundException, ValidationException)
- Side effect verification (repository calls, events published)
---
### 2.2 Listener Test Pattern: user-banned.listener.spec.ts
**Path:** `apps/api/src/modules/admin/application/__tests__/user-banned.listener.spec.ts`
#### Complete Source Code:
```typescript
import { UserBannedListener } from '../listeners/user-banned.listener';
describe('UserBannedListener', () => {
let listener: UserBannedListener;
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
let mockPrisma: {
listing: { updateMany: ReturnType<typeof vi.fn> };
user: { findUnique: ReturnType<typeof vi.fn> };
};
let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn> };
beforeEach(() => {
mockCommandBus = { execute: vi.fn().mockResolvedValue(undefined) };
mockPrisma = {
listing: { updateMany: vi.fn().mockResolvedValue({ count: 3 }) },
user: { findUnique: vi.fn() },
};
mockLogger = { log: vi.fn(), warn: vi.fn() };
listener = new UserBannedListener(
mockCommandBus as any,
mockPrisma as any,
mockLogger as any,
);
});
it('deactivates all user listings when banned', async () => {
mockPrisma.user.findUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
await listener.handle({
aggregateId: 'user-1',
adminId: 'admin-1',
reason: 'Vi phạm chính sách',
eventName: 'user.banned',
occurredAt: new Date(),
});
expect(mockPrisma.listing.updateMany).toHaveBeenCalledWith({
where: {
sellerId: 'user-1',
status: { in: ['ACTIVE', 'PENDING_REVIEW', 'DRAFT'] },
},
data: { status: 'EXPIRED' },
});
});
it('notifies banned user via email', async () => {
mockPrisma.user.findUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });
await listener.handle({
aggregateId: 'user-1',
adminId: 'admin-1',
reason: 'Spam',
eventName: 'user.banned',
occurredAt: new Date(),
});
expect(mockCommandBus.execute).toHaveBeenCalledWith(
expect.objectContaining({
userId: 'user-1',
channel: 'EMAIL',
templateKey: 'user.banned',
templateData: { reason: 'Spam' },
}),
);
});
it('skips email notification when user has no email', async () => {
mockPrisma.user.findUnique.mockResolvedValue({ id: 'user-1', email: null });
await listener.handle({
aggregateId: 'user-1',
adminId: 'admin-1',
reason: 'Vi phạm',
eventName: 'user.banned',
occurredAt: new Date(),
});
expect(mockPrisma.listing.updateMany).toHaveBeenCalled();
expect(mockCommandBus.execute).not.toHaveBeenCalled();
});
});
```
#### Listener Test Patterns:
1. **Event Object Construction:** Manually create event with all properties
2. **Multiple Dependencies:** Mock CommandBus, Prisma, Logger
3. **Side Effects Testing:** Verify database calls and command execution
4. **Conditional Logic:** Test both happy path and edge cases (no email)
5. **Chained Operations:** Test interactions between multiple services
---
### 2.3 Query Handler Pattern: get-dashboard-stats.handler.spec.ts
**Path:** `apps/api/src/modules/admin/application/__tests__/get-dashboard-stats.handler.spec.ts`
#### Complete Source Code:
```typescript
import { type IAdminQueryRepository } from '../../domain/repositories/admin-query.repository';
import { GetDashboardStatsHandler } from '../queries/get-dashboard-stats/get-dashboard-stats.handler';
import { GetDashboardStatsQuery } from '../queries/get-dashboard-stats/get-dashboard-stats.query';
describe('GetDashboardStatsHandler', () => {
let handler: GetDashboardStatsHandler;
let mockAdminQueryRepo: { [K in keyof IAdminQueryRepository]: ReturnType<typeof vi.fn> };
beforeEach(() => {
mockAdminQueryRepo = {
getModerationQueue: vi.fn(),
getDashboardStats: vi.fn(),
getRevenueStats: vi.fn(),
getUsers: vi.fn(),
};
handler = new GetDashboardStatsHandler(mockAdminQueryRepo as any);
});
it('returns dashboard stats', async () => {
const stats = {
totalUsers: 150,
totalListings: 500,
activeListings: 200,
pendingModerationCount: 15,
totalAgents: 30,
verifiedAgents: 20,
totalTransactions: 50,
newUsersLast30Days: 25,
newListingsLast30Days: 40,
};
mockAdminQueryRepo.getDashboardStats.mockResolvedValue(stats);
const result = await handler.execute(new GetDashboardStatsQuery());
expect(result).toEqual(stats);
expect(mockAdminQueryRepo.getDashboardStats).toHaveBeenCalledTimes(1);
});
});
```
#### Query Handler Test Patterns:
1. **Simple Delegation:** Query handlers typically just delegate to repository
2. **Minimal Setup:** Mock only the repository
3. **Result Verification:** Check the returned data matches expectations
4. **Call Count:** Verify the repository method was called exactly once
---
## 3. HANDLER CODE FOR REFERENCE
### 3.1 Similar Handler: ban-user.handler.spec.ts
**Path:** `apps/api/src/modules/admin/application/__tests__/ban-user.handler.spec.ts`
#### Complete Source Code (for comparison to reject-listing):
```typescript
import { UserEntity } from '@modules/auth/domain/entities/user.entity';
import { type IUserRepository } from '@modules/auth/domain/repositories/user.repository';
import { HashedPassword } from '@modules/auth/domain/value-objects/hashed-password.vo';
import { Phone } from '@modules/auth/domain/value-objects/phone.vo';
import { BanUserCommand } from '../commands/ban-user/ban-user.command';
import { BanUserHandler } from '../commands/ban-user/ban-user.handler';
async function createUser(role = 'BUYER' as any, isActive = true): Promise<UserEntity> {
const phone = Phone.create('0901234567').unwrap();
const hash = await HashedPassword.fromPlain('Password123');
const passwordHash = hash.isOk ? hash.unwrap() : null;
const user = new UserEntity('user-1', {
email: null,
phone,
passwordHash,
fullName: 'Test User',
avatarUrl: null,
role,
kycStatus: 'NONE',
kycData: null,
isActive,
});
return user;
}
describe('BanUserHandler', () => {
let handler: BanUserHandler;
let mockUserRepo: { [K in keyof IUserRepository]: ReturnType<typeof vi.fn> };
let mockEventBus: { publish: ReturnType<typeof vi.fn> };
beforeEach(() => {
mockUserRepo = {
findById: vi.fn(),
findByPhone: vi.fn(),
findByEmail: vi.fn(),
save: vi.fn(),
update: vi.fn(),
};
mockEventBus = { publish: vi.fn() };
handler = new BanUserHandler(
mockUserRepo as any,
mockEventBus as any,
);
});
it('bans an active user successfully', async () => {
const user = await createUser('BUYER', true);
mockUserRepo.findById.mockResolvedValue(user);
mockUserRepo.update.mockResolvedValue(undefined);
const command = new BanUserCommand('user-1', 'admin-1', 'Spam activity');
const result = await handler.execute(command);
expect(result.isActive).toBe(false);
expect(result.message).toContain('bị ban');
expect(mockUserRepo.update).toHaveBeenCalledTimes(1);
expect(mockEventBus.publish).toHaveBeenCalled();
});
it('unbans a banned user successfully', async () => {
const user = await createUser('BUYER', false);
mockUserRepo.findById.mockResolvedValue(user);
mockUserRepo.update.mockResolvedValue(undefined);
const command = new BanUserCommand('user-1', 'admin-1', 'Resolved', true);
const result = await handler.execute(command);
expect(result.isActive).toBe(true);
expect(result.message).toContain('gỡ ban');
});
it('throws NotFoundException when user does not exist', async () => {
mockUserRepo.findById.mockResolvedValue(null);
const command = new BanUserCommand('nonexistent', 'admin-1', 'test');
await expect(handler.execute(command)).rejects.toThrow('Người dùng không tồn tại');
});
it('prevents banning an admin user', async () => {
const admin = await createUser('ADMIN', true);
mockUserRepo.findById.mockResolvedValue(admin);
const command = new BanUserCommand('user-1', 'admin-1', 'test');
await expect(handler.execute(command)).rejects.toThrow(/admin/i);
});
it('throws when trying to ban already banned user', async () => {
const user = await createUser('BUYER', false);
mockUserRepo.findById.mockResolvedValue(user);
const command = new BanUserCommand('user-1', 'admin-1', 'test');
await expect(handler.execute(command)).rejects.toThrow('Người dùng đã bị ban');
});
});
```
---
## 4. INFRASTRUCTURE & REPOSITORY FILES (No tests required but provided for context)
### 4.1 prisma-admin-query.repository.ts
**Path:** `apps/api/src/modules/admin/infrastructure/repositories/prisma-admin-query.repository.ts`
**Status:** Infrastructure layer - Integration tests may be appropriate instead
**Note:** This is an implementation detail that delegates to more granular query functions. Consider integration tests instead of unit tests.
---
## 5. PRESENTATION LAYER FILES (No tests typically required)
### 5.1 admin.controller.ts
**Path:** `apps/api/src/modules/admin/presentation/controllers/admin.controller.ts`
**Status:** Controllers typically use E2E tests, not unit tests
**Note:** Controllers are tested via E2E/integration tests, not unit tests in this architecture.
---
## 6. TEST WRITING RECOMMENDATIONS
### For reject-listing.handler.spec.ts
Follow the pattern from `approve-listing.handler.spec.ts`:
1. Create helper function to build listing entities
2. Mock `IListingRepository` and `EventBus`
3. Test 3 scenarios:
- Successfully rejects pending listing → publishes event
- Throws when listing doesn't exist
- Throws when listing not in PENDING_REVIEW status
### For get-revenue-stats.handler.spec.ts
Follow the pattern from `get-dashboard-stats.handler.spec.ts`:
1. Mock `IAdminQueryRepository`
2. Test single scenario:
- Returns results from repository.getRevenueStats() call
3. Verify correct parameters passed:
- startDate, endDate, groupBy
### For user-deactivated.listener.spec.ts
Follow the pattern from `user-banned.listener.spec.ts`:
1. Mock `PrismaService` and `LoggerService`
2. Test scenarios:
- Successfully expires listings for deactivated user
- Updates both ACTIVE and PENDING_REVIEW listings
- Logs correct messages
- Handles case with 0 listings updated
- Handles case with multiple listings updated
---
## 7. COMPLETE FILE STRUCTURE
```
admin/
├── application/
│ ├── __tests__/
│ │ ├── adjust-subscription.handler.spec.ts ✅
│ │ ├── admin-audit.listener.spec.ts ✅
│ │ ├── approve-kyc.handler.spec.ts ✅
│ │ ├── approve-listing.handler.spec.ts ✅ (REFERENCE)
│ │ ├── ban-user.handler.spec.ts ✅
│ │ ├── bulk-moderate-listings.handler.spec.ts ✅
│ │ ├── get-audit-logs.handler.spec.ts ✅
│ │ ├── get-dashboard-stats.handler.spec.ts ✅ (REFERENCE)
│ │ ├── get-kyc-queue.handler.spec.ts ✅
│ │ ├── get-moderation-queue.handler.spec.ts ✅
│ │ ├── get-user-detail.handler.spec.ts ✅
│ │ ├── get-users.handler.spec.ts ✅
│ │ ├── reject-kyc.handler.spec.ts ✅
│ │ ├── update-user-status.handler.spec.ts ✅
│ │ ├── user-banned.listener.spec.ts ✅ (REFERENCE)
│ │ └── [MISSING] reject-listing.handler.spec.ts ❌
│ │ └── [MISSING] get-revenue-stats.handler.spec.ts ❌
│ │ └── [MISSING] user-deactivated.listener.spec.ts ❌
│ │
│ ├── commands/
│ │ ├── adjust-subscription/
│ │ ├── approve-kyc/
│ │ ├── approve-listing/
│ │ ├── ban-user/
│ │ ├── bulk-moderate-listings/
│ │ ├── reject-kyc/
│ │ ├── reject-listing/ ❌ NO TEST
│ │ ├── update-user-status/
│ │ └── index.ts
│ │
│ ├── listeners/
│ │ ├── admin-audit.listener.ts ✅
│ │ ├── user-banned.listener.ts ✅
│ │ ├── user-deactivated.listener.ts ❌ NO TEST
│ │ └── (no index.ts)
│ │
│ ├── queries/
│ │ ├── get-audit-logs/
│ │ ├── get-dashboard-stats/
│ │ ├── get-kyc-queue/
│ │ ├── get-moderation-queue/
│ │ ├── get-revenue-stats/ ❌ NO TEST
│ │ ├── get-user-detail/
│ │ ├── get-users/
│ │ └── index.ts
│ │
│ ├── index.ts
│ └── application.module.ts
├── domain/
│ ├── __tests__/
│ │ └── admin-events.spec.ts ✅
│ │
│ ├── events/
│ │ ├── kyc-approved.event.ts
│ │ ├── kyc-rejected.event.ts
│ │ ├── listing-approved.event.ts
│ │ ├── listing-rejected.event.ts
│ │ ├── subscription-adjusted.event.ts
│ │ ├── user-banned.event.ts
│ │ ├── user-unbanned.event.ts
│ │ └── index.ts
│ │
│ ├── repositories/
│ │ ├── admin-query.repository.ts
│ │ ├── audit-log.repository.ts
│ │ └── index.ts
│ │
│ ├── domain.module.ts
│ └── index.ts
├── infrastructure/
│ ├── repositories/
│ │ ├── admin-stats.queries.ts
│ │ ├── admin-user.queries.ts
│ │ ├── prisma-admin-query.repository.ts ( Integration test candidate)
│ │ ├── prisma-audit-log.repository.ts ( Integration test candidate)
│ │ └── index.ts
│ │
│ ├── infrastructure.module.ts
│ └── index.ts
├── presentation/
│ ├── controllers/
│ │ ├── admin.controller.ts ( E2E test - not unit test)
│ │ ├── admin-moderation.controller.ts ( E2E test - not unit test)
│ │ └── index.ts
│ │
│ ├── dto/
│ │ ├── adjust-subscription.dto.ts
│ │ ├── approve-kyc.dto.ts
│ │ ├── approve-listing.dto.ts
│ │ ├── ban-user.dto.ts
│ │ ├── bulk-moderate.dto.ts
│ │ ├── get-audit-logs-query.dto.ts
│ │ ├── get-users-query.dto.ts
│ │ ├── reject-kyc.dto.ts
│ │ ├── reject-listing.dto.ts
│ │ ├── revenue-stats.dto.ts
│ │ ├── update-user-status.dto.ts
│ │ └── index.ts
│ │
│ ├── presentation.module.ts
│ └── index.ts
├── admin.module.ts
└── index.ts
```
Legend:
- ✅ Has test file
- ❌ Missing test file
- Not a unit test candidate (integration/E2E)
---
## Summary: Critical Files Requiring Tests
| File | Type | Priority | Pattern to Follow |
|------|------|----------|-------------------|
| reject-listing.handler.ts | Command Handler | HIGH | approve-listing.handler.spec.ts |
| get-revenue-stats.handler.ts | Query Handler | HIGH | get-dashboard-stats.handler.spec.ts |
| user-deactivated.listener.ts | Event Listener | HIGH | user-banned.listener.spec.ts |
---