chore: update project documentation, audit reports, and initialize IDE configuration files
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled

This commit is contained in:
Ho Ngoc Hai
2026-04-19 03:12:54 +07:00
parent 3be106074d
commit 11f2bf26e6
101 changed files with 21312 additions and 20672 deletions

View File

@@ -1,34 +1,34 @@
# Detailed Handler Comparison & Code Patterns
# So Sánh Handler Chi Tiết & Các Mẫu Code
## File Structure Comparison
## So Sánh Cấu Trúc Tệp
### Tested Handler Pattern: approve-listing
### Mẫu Handler Đã Được Kiểm Thử: approve-listing
```
approve-listing/
├── approve-listing.command.ts (simple class)
├── approve-listing.handler.ts (the handler to test)
└── (no query.ts - it's a Command, not a Query)
├── approve-listing.command.ts (lớp đơn giản)
├── approve-listing.handler.ts (handler cần kiểm thử)
└── (không có query.ts - đây là Command, không phải Query)
Test file:
Tệp kiểm thử:
└── approve-listing.handler.spec.ts
```
### Untested Handler: reject-listing
### Handler Chưa Được Kiểm Thử: reject-listing
```
reject-listing/
├── reject-listing.command.ts (simple class)
├── reject-listing.handler.ts (NEEDS TEST)
└── (no query.ts - it's a Command, not a Query)
├── reject-listing.command.ts (lớp đơn giản)
├── reject-listing.handler.ts (CẦN KIỂM THỬ)
└── (không có query.ts - đây là Command, không phải Query)
Test file:
└── ❌ MISSING: reject-listing.handler.spec.ts
Tệp kiểm thử:
└── ❌ THIẾU: reject-listing.handler.spec.ts
```
---
## Side-by-Side Handler Comparison
## So Sánh Handler Đặt Cạnh Nhau
### APPROVE Listing Handler:
### Handler APPROVE Listing:
```typescript
@CommandHandler(ApproveListingCommand)
export class ApproveListingHandler implements ICommandHandler<ApproveListingCommand> {
@@ -73,7 +73,7 @@ export class ApproveListingHandler implements ICommandHandler<ApproveListingComm
}
```
### REJECT Listing Handler (virtually identical pattern):
### Handler REJECT Listing (mẫu gần như giống hệt):
```typescript
@CommandHandler(RejectListingCommand)
export class RejectListingHandler implements ICommandHandler<RejectListingCommand> {
@@ -89,7 +89,7 @@ export class RejectListingHandler implements ICommandHandler<RejectListingComman
throw new NotFoundException('Listing không tồn tại');
}
// 2. Check status (same as approve!)
// 2. Check status (giống như approve!)
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`,
@@ -97,18 +97,18 @@ export class RejectListingHandler implements ICommandHandler<RejectListingComman
);
}
// 3. Apply domain logic (different method: reject instead of approve)
// 3. Apply domain logic (phương thức khác: reject thay vì approve)
listing.reject(command.reason);
// 4. Persist
await this.listingRepo.update(listing);
// 5. Publish event (different event type)
// 5. Publish event (loại event khác)
this.eventBus.publish(
new ListingRejectedEvent(listing.id, command.adminId, command.reason),
);
// 6. Return result (different status)
// 6. Return result (trạng thái khác)
return {
listingId: listing.id,
status: 'REJECTED',
@@ -118,19 +118,19 @@ export class RejectListingHandler implements ICommandHandler<RejectListingComman
}
```
### Differences:
| Aspect | Approve | Reject |
### Sự Khác Biệt:
| Khía cạnh | Approve | Reject |
|--------|---------|--------|
| Domain Method | `listing.approve()` | `listing.reject()` |
| Phương thức domain | `listing.approve()` | `listing.reject()` |
| Event | `ListingApprovedEvent` | `ListingRejectedEvent` |
| Result Status | `'ACTIVE'` | `'REJECTED'` |
| Result Message | `'Listing đã được phê duyệt'` | `'Listing đã bị từ chối'` |
| Trạng thái kết quả | `'ACTIVE'` | `'REJECTED'` |
| Thông báo kết quả | `'Listing đã được phê duyệt'` | `'Listing đã bị từ chối'` |
---
## Test Code Walkthrough
## Hướng Dẫn Code Kiểm Thử
### ApproveListingHandler Test:
### Kiểm Thử ApproveListingHandler:
```typescript
describe('ApproveListingHandler', () => {
@@ -138,7 +138,7 @@ describe('ApproveListingHandler', () => {
let mockListingRepo: { [K in keyof IListingRepository]: ReturnType<typeof vi.fn> };
let mockEventBus: { publish: ReturnType<typeof vi.fn> };
// SETUP: Create fresh mocks for each test
// THIẾT LẬP: Tạo mock mới cho mỗi test
beforeEach(() => {
mockListingRepo = {
findById: vi.fn(),
@@ -152,46 +152,46 @@ describe('ApproveListingHandler', () => {
mockEventBus = { publish: vi.fn() };
// Instantiate handler with mocks
// Khởi tạo handler với các mock
handler = new ApproveListingHandler(
mockListingRepo as any,
mockEventBus as any,
);
});
// TEST 1: Happy Path - Successfully approve
// TEST 1: Luồng bình thường - Phê duyệt thành công
it('approves a pending listing successfully', async () => {
// Arrange: Create a listing entity in PENDING_REVIEW state
// Arrange: Tạo một thực thể listing ở trạng thái PENDING_REVIEW
const listing = createPendingListing();
mockListingRepo.findById.mockResolvedValue(listing);
mockListingRepo.update.mockResolvedValue(undefined);
// Act: Execute the command
// Act: Thực thi lệnh
const command = new ApproveListingCommand('listing-1', 'admin-1', 'Looks good');
const result = await handler.execute(command);
// Assert: Verify result
// Assert: Kiểm tra kết quả
expect(result.status).toBe('ACTIVE');
expect(result.listingId).toBe('listing-1');
// Assert: Verify side effects
// Assert: Kiểm tra các hiệu ứng phụ
expect(mockListingRepo.update).toHaveBeenCalledTimes(1);
expect(mockEventBus.publish).toHaveBeenCalled();
});
// TEST 2: Error - Listing not found
// TEST 2: Lỗi - Không tìm thấy listing
it('throws NotFoundException when listing does not exist', async () => {
// Arrange: Mock returns null (not found)
// Arrange: Mock trả về null (không tìm thấy)
mockListingRepo.findById.mockResolvedValue(null);
// Act & Assert: Expect exception
// Act & Assert: Kỳ vọng ngoại lệ
const command = new ApproveListingCommand('nonexistent', 'admin-1');
await expect(handler.execute(command)).rejects.toThrow('Listing không tồn tại');
});
// TEST 3: Error - Wrong status
// TEST 3: Lỗi - Sai trạng thái
it('throws ValidationException when listing is not pending review', async () => {
// Arrange: Create listing NOT in PENDING_REVIEW status
// Arrange: Tạo listing KHÔNG ở trạng thái PENDING_REVIEW
const price = Price.create(500_000_000n).unwrap();
const listing = ListingEntity.createNew(
'listing-1', 'prop-1', 'seller-1', 'SALE', price, 80,
@@ -199,23 +199,23 @@ describe('ApproveListingHandler', () => {
listing.clearDomainEvents();
mockListingRepo.findById.mockResolvedValue(listing);
// Act & Assert: Expect exception
// Act & Assert: Kỳ vọng ngoại lệ
const command = new ApproveListingCommand('listing-1', 'admin-1');
await expect(handler.execute(command)).rejects.toThrow(/trạng thái/);
});
});
```
### How to adapt for RejectListingHandler:
### Cách điều chỉnh cho RejectListingHandler:
1. **Import changes:**
1. **Thay đổi import:**
```typescript
import { RejectListingCommand } from '../commands/reject-listing/reject-listing.command';
import { RejectListingHandler } from '../commands/reject-listing/reject-listing.handler';
// Keep everything else the same
// Giữ nguyên tất cả phần còn lại
```
2. **Test 1 (Happy path) changes:**
2. **Thay đổi Test 1 (Luồng bình thường):**
```typescript
it('rejects a pending listing successfully', async () => {
const listing = createPendingListing();
@@ -225,20 +225,20 @@ describe('ApproveListingHandler', () => {
const command = new RejectListingCommand('listing-1', 'admin-1', 'Too many issues');
const result = await handler.execute(command);
expect(result.status).toBe('REJECTED'); // Changed from 'ACTIVE'
expect(result.message).toContain('từ chối'); // Changed assertion
expect(result.status).toBe('REJECTED'); // Đổi từ 'ACTIVE'
expect(result.message).toContain('từ chối'); // Thay đổi câu kiểm tra
expect(mockListingRepo.update).toHaveBeenCalledTimes(1);
expect(mockEventBus.publish).toHaveBeenCalled();
});
```
3. **Tests 2 & 3 remain almost identical** (only import names change)
3. **Test 2 & 3 gần như giữ nguyên** (chỉ thay đổi tên import)
---
## Query Handler Comparison
## So Sánh Query Handler
### Tested Query Handler: get-dashboard-stats
### Query Handler Đã Được Kiểm Thử: get-dashboard-stats
```typescript
@QueryHandler(GetDashboardStatsQuery)
export class GetDashboardStatsHandler implements IQueryHandler<GetDashboardStatsQuery> {
@@ -252,7 +252,7 @@ export class GetDashboardStatsHandler implements IQueryHandler<GetDashboardStats
}
```
### Untested Query Handler: get-revenue-stats
### Query Handler Chưa Được Kiểm Thử: get-revenue-stats
```typescript
@QueryHandler(GetRevenueStatsQuery)
export class GetRevenueStatsHandler implements IQueryHandler<GetRevenueStatsQuery> {
@@ -261,13 +261,13 @@ export class GetRevenueStatsHandler implements IQueryHandler<GetRevenueStatsQuer
) {}
async execute(query: GetRevenueStatsQuery): Promise<RevenueStatsItem[]> {
// KEY DIFFERENCE: Passes query params to repo method
// SỰ KHÁC BIỆT CHÍNH: Truyền tham số query vào phương thức repo
return this.adminQueryRepo.getRevenueStats(query.startDate, query.endDate, query.groupBy);
}
}
```
### Query Handler Test Pattern:
### Mẫu Kiểm Thử Query Handler:
```typescript
describe('GetRevenueStatsHandler', () => {
let handler: GetRevenueStatsHandler;
@@ -277,7 +277,7 @@ describe('GetRevenueStatsHandler', () => {
mockAdminQueryRepo = {
getModerationQueue: vi.fn(),
getDashboardStats: vi.fn(),
getRevenueStats: vi.fn(), // This one will be tested
getRevenueStats: vi.fn(), // Cái này sẽ được kiểm thử
getUsers: vi.fn(),
};
@@ -285,7 +285,7 @@ describe('GetRevenueStatsHandler', () => {
});
it('returns revenue stats for date range', async () => {
// Arrange: Mock data
// Arrange: Dữ liệu mock
const mockStats: RevenueStatsItem[] = [
{
period: '2024-04',
@@ -304,10 +304,10 @@ describe('GetRevenueStatsHandler', () => {
const query = new GetRevenueStatsQuery(startDate, endDate, 'month');
const result = await handler.execute(query);
// Assert: Verify result
// Assert: Kiểm tra kết quả
expect(result).toEqual(mockStats);
// Assert: Verify params passed correctly
// Assert: Kiểm tra các tham số được truyền đúng
expect(mockAdminQueryRepo.getRevenueStats).toHaveBeenCalledWith(
startDate,
endDate,
@@ -322,14 +322,14 @@ describe('GetRevenueStatsHandler', () => {
const query = new GetRevenueStatsQuery(
new Date('2024-04-01'),
new Date('2024-04-30'),
'day' // Changed parameter
'day' // Tham số đã thay đổi
);
await handler.execute(query);
expect(mockAdminQueryRepo.getRevenueStats).toHaveBeenCalledWith(
expect.any(Date),
expect.any(Date),
'day' // Verify 'day' was passed
'day' // Kiểm tra 'day' đã được truyền vào
);
});
@@ -350,9 +350,9 @@ describe('GetRevenueStatsHandler', () => {
---
## Listener Comparison
## So Sánh Listener
### UserBannedListener (Tested):
### UserBannedListener (Đã Được Kiểm Thử):
```typescript
@Injectable()
export class UserBannedListener {
@@ -366,7 +366,7 @@ export class UserBannedListener {
async handle(event: UserBannedEvent): Promise<void> {
this.logger.log(`Handling user.banned for user ${event.aggregateId}`, 'UserBannedListener');
// Deactivate listings
// Vô hiệu hóa các listing
const deactivated = await this.prisma.listing.updateMany({
where: {
sellerId: event.aggregateId,
@@ -380,7 +380,7 @@ export class UserBannedListener {
'UserBannedListener',
);
// Send email notification
// Gửi thông báo email
const user = await this.prisma.user.findUnique({
where: { id: event.aggregateId },
select: { id: true, email: true },
@@ -401,7 +401,7 @@ export class UserBannedListener {
}
```
### UserDeactivatedListener (Untested):
### UserDeactivatedListener (Chưa Được Kiểm Thử):
```typescript
@Injectable()
export class UserDeactivatedListener {
@@ -414,10 +414,10 @@ export class UserDeactivatedListener {
async handle(event: UserDeactivatedEvent): Promise<void> {
this.logger.log(`Handling user.deactivated for user ${event.aggregateId}`, 'UserDeactivatedListener');
// Similar to UserBannedListener but:
// 1. NO CommandBus (simpler)
// 2. NO email notification
// 3. Different status list: ['ACTIVE', 'PENDING_REVIEW'] (no DRAFT)
// Tương t UserBannedListener nhưng:
// 1. KHÔNG có CommandBus (đơn giản hơn)
// 2. KHÔNG có thông báo email
// 3. Danh sách trạng thái khác: ['ACTIVE', 'PENDING_REVIEW'] (không có DRAFT)
const deactivated = await this.prisma.listing.updateMany({
where: {
sellerId: event.aggregateId,
@@ -434,16 +434,16 @@ export class UserDeactivatedListener {
}
```
### Key Differences:
| Aspect | UserBanned | UserDeactivated |
### Sự Khác Biệt Chính:
| Khía cạnh | UserBanned | UserDeactivated |
|--------|-----------|-----------------|
| Event name | `'user.banned'` | `'user.deactivated'` |
| Has CommandBus | ✅ Yes (for email) | ❌ No |
| Status list | `['ACTIVE', 'PENDING_REVIEW', 'DRAFT']` | `['ACTIVE', 'PENDING_REVIEW']` |
| Sends notification | ✅ Yes (email) | ❌ No |
| Complexity | Higher | Lower |
| Tên event | `'user.banned'` | `'user.deactivated'` |
| CommandBus | ✅ Có (để gửi email) | ❌ Không |
| Danh sách trạng thái | `['ACTIVE', 'PENDING_REVIEW', 'DRAFT']` | `['ACTIVE', 'PENDING_REVIEW']` |
| Gửi thông báo | ✅ (email) | ❌ Không |
| Độ phức tạp | Cao hơn | Thấp hơn |
### Listener Test Pattern (simplified for UserDeactivated):
### Mẫu Kiểm Thử Listener (đơn giản hóa cho UserDeactivated):
```typescript
describe('UserDeactivatedListener', () => {
let listener: UserDeactivatedListener;
@@ -514,4 +514,3 @@ describe('UserDeactivatedListener', () => {
});
});
```