feat(load-tests): add K6 coverage for search, admin, and MCP endpoints
Add three new K6 load test scripts to cover previously untested API surfaces: - search-advanced.js: Combined geo + text + filter queries, paginated deep search, and sort variations against /search and /search/geo (300 peak VUs) - admin.js: Moderation queue CRUD, bulk moderation, dashboard stats, audit logs, and user management endpoints (50 peak VUs) - mcp.js: MCP server discovery, SSE connection, property-search tool calls, valuation/batch-valuation, and feature extraction (120 peak VUs) Also updates README with new suite documentation, per-suite custom thresholds, and adds the new suites to the CI workflow_dispatch selector. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
375
CODEBASE_QUICK_REFERENCE.md
Normal file
375
CODEBASE_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# GoodGo Platform - Quick Reference Guide
|
||||
|
||||
## 🗂️ File Structure Quick Links
|
||||
|
||||
### Pages (where to place new features)
|
||||
- **Inquiry pages**: `apps/web/app/[locale]/(dashboard)/inquiries/`
|
||||
- **Lead pages**: `apps/web/app/[locale]/(dashboard)/leads/`
|
||||
- **Example pages**: `apps/web/app/[locale]/(dashboard)/listings/` (reference)
|
||||
|
||||
### API Layer
|
||||
- **Inquiry API**: Create `apps/web/lib/inquiries-api.ts`
|
||||
- **Lead API**: Create `apps/web/lib/leads-api.ts`
|
||||
- **Base client**: `apps/web/lib/api-client.ts` ← reuse this
|
||||
|
||||
### Components
|
||||
- **UI base components**: `apps/web/components/ui/` (Button, Card, Badge, Table, Select, Input)
|
||||
- **Domain components**: `apps/web/components/inquiries/`, `apps/web/components/leads/`
|
||||
- **Example domain component**: `apps/web/components/listings/listing-status-badge.tsx`
|
||||
|
||||
### Hooks
|
||||
- **Create hooks**: `apps/web/lib/hooks/use-inquiries.ts`, `apps/web/lib/hooks/use-leads.ts`
|
||||
- **Example hook**: `apps/web/lib/hooks/use-listings.ts`
|
||||
|
||||
### Stores (if needed)
|
||||
- **Location**: `apps/web/lib/` (e.g., `inquiry-store.ts`, `lead-store.ts`)
|
||||
- **Example**: `apps/web/lib/comparison-store.ts`
|
||||
|
||||
### Backend API
|
||||
- **Inquiries controller**: `apps/api/src/modules/inquiries/presentation/controllers/inquiries.controller.ts`
|
||||
- **Leads controller**: `apps/api/src/modules/leads/presentation/controllers/leads.controller.ts`
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Backend API Endpoints
|
||||
|
||||
### Inquiries
|
||||
```
|
||||
POST /api/v1/inquiries Create inquiry
|
||||
GET /api/v1/inquiries/listing/{id} List by listing (paginated)
|
||||
GET /api/v1/inquiries/agent/me List my inquiries (AGENT role)
|
||||
PATCH /api/v1/inquiries/{id}/read Mark as read (AGENT role)
|
||||
```
|
||||
|
||||
### Leads
|
||||
```
|
||||
POST /api/v1/leads Create lead (AGENT role)
|
||||
GET /api/v1/leads List leads (AGENT role, paginated)
|
||||
GET /api/v1/leads/stats Get stats (AGENT role)
|
||||
PATCH /api/v1/leads/{id}/status Update status (AGENT role)
|
||||
DELETE /api/v1/leads/{id} Delete lead (AGENT role)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Component Templates
|
||||
|
||||
### List Page Template
|
||||
```typescript
|
||||
'use client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Select } from '@/components/ui/select';
|
||||
|
||||
export default function InquiriesPage() {
|
||||
const t = useTranslations('inquiries');
|
||||
const [filters, setFilters] = useState({ page: 1, status: '' });
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['inquiries', filters],
|
||||
queryFn: () => inquiriesApi.list(filters),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">{t('title')}</h1>
|
||||
</div>
|
||||
|
||||
{/* Stats cards */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard label={t('total')} value={data?.total ?? 0} />
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<Select value={filters.status} onChange={(e) => setFilters({...filters, status: e.target.value})}>
|
||||
<option value="">{t('allStatus')}</option>
|
||||
{/* ... status options */}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center"><Spinner /></div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t('name')}</TableHead>
|
||||
<TableHead>{t('status')}</TableHead>
|
||||
<TableHead className="text-right">{t('actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.items.map(item => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell><Badge>{item.status}</Badge></TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm">View</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### API Service Template
|
||||
```typescript
|
||||
// apps/web/lib/inquiries-api.ts
|
||||
import { apiClient } from './api-client';
|
||||
|
||||
export interface InquiryDto {
|
||||
id: string;
|
||||
listingId: string;
|
||||
userId: string;
|
||||
message: string;
|
||||
isRead: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface InquiryListResponse {
|
||||
items: InquiryDto[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export const inquiriesApi = {
|
||||
list: (params: { page?: number; limit?: number; status?: string }) =>
|
||||
apiClient.get<InquiryListResponse>('/inquiries', params),
|
||||
|
||||
getById: (id: string) =>
|
||||
apiClient.get<InquiryDto>(`/inquiries/${id}`),
|
||||
|
||||
markAsRead: (id: string) =>
|
||||
apiClient.patch(`/inquiries/${id}/read`, {}),
|
||||
};
|
||||
```
|
||||
|
||||
### Hook Template
|
||||
```typescript
|
||||
// apps/web/lib/hooks/use-inquiries.ts
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { inquiriesApi } from '@/lib/inquiries-api';
|
||||
|
||||
export const inquiriesKeys = {
|
||||
all: ['inquiries'] as const,
|
||||
list: (params: any) => ['inquiries', 'list', params] as const,
|
||||
detail: (id: string) => ['inquiries', 'detail', id] as const,
|
||||
};
|
||||
|
||||
export function useInquiries(params = {}) {
|
||||
return useQuery({
|
||||
queryKey: inquiriesKeys.list(params),
|
||||
queryFn: () => inquiriesApi.list(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useInquiry(id: string) {
|
||||
return useQuery({
|
||||
queryKey: inquiriesKeys.detail(id),
|
||||
queryFn: () => inquiriesApi.getById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Status Badge Component Template
|
||||
```typescript
|
||||
// apps/web/components/inquiries/inquiry-status-badge.tsx
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
const INQUIRY_STATUSES = {
|
||||
NEW: { label: 'Mới', variant: 'info' as const },
|
||||
READ: { label: 'Đã xem', variant: 'secondary' as const },
|
||||
REPLIED: { label: 'Đã trả lời', variant: 'success' as const },
|
||||
};
|
||||
|
||||
export function InquiryStatusBadge({ status }: { status: string }) {
|
||||
const config = INQUIRY_STATUSES[status as keyof typeof INQUIRY_STATUSES] ?? {
|
||||
label: status,
|
||||
variant: 'outline' as const,
|
||||
};
|
||||
return <Badge variant={config.variant}>{config.label}</Badge>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Translations (i18n)
|
||||
|
||||
Add to `apps/web/messages/vi.json` and `apps/web/messages/en.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"inquiries": {
|
||||
"title": "Quản lý Liên hệ",
|
||||
"subtitle": "Xem và quản lý các liên hệ từ khách hàng",
|
||||
"allStatus": "Tất cả trạng thái",
|
||||
"new": "Mới",
|
||||
"read": "Đã xem",
|
||||
"replied": "Đã trả lời",
|
||||
"total": "Tổng liên hệ",
|
||||
"thisMonth": "Tháng này",
|
||||
"message": "Tin nhắn",
|
||||
"from": "Từ",
|
||||
"date": "Ngày tạo",
|
||||
"markAsRead": "Đánh dấu đã xem"
|
||||
},
|
||||
"leads": {
|
||||
"title": "Quản lý Khách hàng tiềm năng",
|
||||
"subtitle": "Theo dõi và quản lý khách hàng tiềm năng",
|
||||
"name": "Tên khách hàng",
|
||||
"phone": "Số điện thoại",
|
||||
"email": "Email",
|
||||
"source": "Nguồn",
|
||||
"score": "Điểm số",
|
||||
"status": "Trạng thái",
|
||||
"new": "Mới",
|
||||
"contacted": "Đã liên hệ",
|
||||
"qualified": "Đã xác nhận",
|
||||
"negotiating": "Đang thương lượng",
|
||||
"converted": "Chuyển đổi",
|
||||
"lost": "Mất"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage in components:
|
||||
```typescript
|
||||
const t = useTranslations('inquiries');
|
||||
// or
|
||||
const t = useTranslations('leads');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Styling Conventions
|
||||
|
||||
### Color Classes
|
||||
```css
|
||||
/* Status indicators */
|
||||
.success { @apply text-green-600 bg-green-50 }
|
||||
.warning { @apply text-yellow-600 bg-yellow-50 }
|
||||
.info { @apply text-blue-600 bg-blue-50 }
|
||||
.error { @apply text-red-600 bg-red-50 }
|
||||
|
||||
/* Typography */
|
||||
.title { @apply text-2xl font-bold }
|
||||
.subtitle { @apply text-muted-foreground text-sm }
|
||||
.label { @apply text-xs text-muted-foreground uppercase }
|
||||
|
||||
/* Layout */
|
||||
.card-grid { @apply grid gap-4 sm:grid-cols-2 lg:grid-cols-3 }
|
||||
.flex-between { @apply flex items-center justify-between }
|
||||
```
|
||||
|
||||
### Responsive Breakpoints
|
||||
```typescript
|
||||
// Mobile first
|
||||
className="w-full" // Mobile: full width
|
||||
className="sm:w-1/2" // 640px+: 50%
|
||||
className="md:w-1/3" // 768px+: 33%
|
||||
className="lg:grid-cols-3" // 1024px+: 3 columns
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication & Authorization
|
||||
|
||||
### Protected Pages
|
||||
```typescript
|
||||
// pages automatically protected by (dashboard) group
|
||||
// which has JwtAuthGuard applied via middleware or layout
|
||||
|
||||
// For role-specific pages (AGENT only):
|
||||
// Use guard directly or check in component
|
||||
const { user } = useAuthStore();
|
||||
if (!user?.roles.includes('AGENT')) {
|
||||
// redirect or show error
|
||||
}
|
||||
```
|
||||
|
||||
### API Calls with Auth
|
||||
```typescript
|
||||
// Automatically includes:
|
||||
// - httpOnly cookies (JWT)
|
||||
// - CSRF token from XSRF-TOKEN cookie
|
||||
// - X-CSRF-Token header (POST/PATCH/DELETE)
|
||||
|
||||
const { data } = await inquiriesApi.list(); // Auth headers auto-included
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Patterns
|
||||
|
||||
See existing tests in `__tests__` folders for reference:
|
||||
- `apps/web/lib/__tests__/auth-store.spec.ts`
|
||||
- `apps/web/components/ui/__tests__/`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Pre-Build Checklist
|
||||
|
||||
Before creating Inquiry & Lead pages:
|
||||
|
||||
- [ ] Create API service files (`inquiries-api.ts`, `leads-api.ts`)
|
||||
- [ ] Create React Query hooks (`use-inquiries.ts`, `use-leads.ts`)
|
||||
- [ ] Create status badge components
|
||||
- [ ] Add translations to `vi.json` and `en.json`
|
||||
- [ ] Create page components under `(dashboard)` group
|
||||
- [ ] Test API endpoints with backend
|
||||
- [ ] Verify auth guards (JwtAuthGuard, RolesGuard)
|
||||
- [ ] Test pagination with query params
|
||||
- [ ] Test loading/error states
|
||||
- [ ] Test responsive design (mobile/tablet/desktop)
|
||||
- [ ] Add JSDoc comments to reusable functions
|
||||
- [ ] Test dark mode colors
|
||||
|
||||
---
|
||||
|
||||
## 📚 Key Files to Reference
|
||||
|
||||
```
|
||||
REFERENCE PAGES:
|
||||
- apps/web/app/[locale]/(dashboard)/listings/page.tsx ← Best example
|
||||
- apps/web/app/[locale]/(dashboard)/dashboard/page.tsx ← Stats & cards
|
||||
|
||||
REFERENCE COMPONENTS:
|
||||
- apps/web/components/listings/listing-status-badge.tsx ← Status badge pattern
|
||||
- apps/web/components/search/filter-bar.tsx ← Filter pattern
|
||||
- apps/web/components/ui/table.tsx ← Table pattern
|
||||
|
||||
REFERENCE HOOKS:
|
||||
- apps/web/lib/hooks/use-listings.ts ← React Query pattern
|
||||
- apps/web/lib/hooks/use-analytics.ts ← Complex data fetching
|
||||
|
||||
REFERENCE STORES:
|
||||
- apps/web/lib/auth-store.ts ← Async actions pattern
|
||||
- apps/web/lib/comparison-store.ts ← Persistence pattern
|
||||
|
||||
REFERENCE API:
|
||||
- apps/web/lib/listings-api.ts ← API service pattern
|
||||
- apps/web/lib/auth-api.ts ← Auth API pattern
|
||||
|
||||
REFERENCE LAYOUT:
|
||||
- apps/web/app/[locale]/(dashboard)/layout.tsx ← Dashboard nav
|
||||
|
||||
REFERENCE VALIDATION:
|
||||
- apps/web/lib/validations/listings.ts ← Zod schema pattern
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user