diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 43a8ebb..801c5d1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -67,6 +67,19 @@ jobs:
- name: Test
run: pnpm test
+ # GOO-134: API unit-test coverage gate (≥70% stmt/lines/funcs, ≥58% branches → ratcheting to 60 via GOO-180).
+ - name: Test coverage (API)
+ run: pnpm --filter @goodgo/api test:coverage
+
+ - name: Upload coverage report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: api-coverage
+ path: apps/api/coverage
+ if-no-files-found: ignore
+ retention-days: 14
+
- name: Build
run: pnpm build
diff --git a/apps/api/package.json b/apps/api/package.json
index b644ef4..0d58a51 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -9,6 +9,7 @@
"start:prod": "node dist/main",
"lint": "eslint src/",
"test": "vitest run",
+ "test:coverage": "vitest run --coverage",
"test:integration": "vitest run --config vitest.integration.config.ts",
"typecheck": "tsc --noEmit"
},
diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts
index 84143bd..1a34d3b 100644
--- a/apps/api/vitest.config.ts
+++ b/apps/api/vitest.config.ts
@@ -10,6 +10,30 @@ export default defineConfig({
env: {
BCRYPT_ROUNDS: '4',
},
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'text-summary', 'html', 'lcov', 'json-summary'],
+ reportsDirectory: './coverage',
+ include: ['src/**/*.ts'],
+ exclude: [
+ 'src/**/*.spec.ts',
+ 'src/**/*.integration.spec.ts',
+ 'src/**/__tests__/**',
+ 'src/**/*.module.ts',
+ 'src/**/*.dto.ts',
+ 'src/**/index.ts',
+ 'src/main.ts',
+ ],
+ // GOO-134: CI gate thresholds. Branches starts at 58 (no-regression ratchet)
+ // and will be raised to 60 via follow-up GOO-180 (payments/sbv-compliance,
+ // subscriptions/quotas, auth/guards). CTO approval: 8f2b125a.
+ thresholds: {
+ statements: 70,
+ lines: 70,
+ functions: 70,
+ branches: 58,
+ },
+ },
},
resolve: {
alias: {
diff --git a/apps/web/components/chuyen-nhuong/transfer-listing-card.tsx b/apps/web/components/chuyen-nhuong/transfer-listing-card.tsx
index 40c75bf..e6b843c 100644
--- a/apps/web/components/chuyen-nhuong/transfer-listing-card.tsx
+++ b/apps/web/components/chuyen-nhuong/transfer-listing-card.tsx
@@ -56,7 +56,7 @@ export function TransferListingCard({ listing }: TransferListingCardProps) {
{/* Location */}
- {listing.district}, {listing.city}
+ {listing.ward ? `${listing.ward}, ` : ''}{listing.district}, {listing.city}
{/* Price */}
diff --git a/apps/web/components/inquiries/inquiry-detail-dialog.tsx b/apps/web/components/inquiries/inquiry-detail-dialog.tsx
index 835c66b..b93a8f2 100644
--- a/apps/web/components/inquiries/inquiry-detail-dialog.tsx
+++ b/apps/web/components/inquiries/inquiry-detail-dialog.tsx
@@ -13,6 +13,7 @@ import {
} from '@/components/ui/dialog';
import { useMarkInquiryRead } from '@/lib/hooks/use-inquiries';
import type { InquiryReadDto } from '@/lib/inquiries-api';
+import { formatPhone, zaloHref } from '@/lib/phone';
interface InquiryDetailDialogProps {
inquiry: InquiryReadDto | null;
@@ -42,6 +43,8 @@ export function InquiryDetailDialog({ inquiry, open, onOpenChange }: InquiryDeta
minute: '2-digit',
});
+ const phone = inquiry.phone ?? inquiry.userPhone;
+
return (