# 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 |

---

