Compare commits
10 Commits
2c97f99214
...
e5f7acf7da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f7acf7da | ||
|
|
b93c28fa01 | ||
|
|
ccfc176e40 | ||
|
|
f373f7b1e2 | ||
|
|
1ebdc5f0b3 | ||
|
|
a9fa214544 | ||
|
|
db0fe8b9b7 | ||
|
|
25420720e7 | ||
|
|
1617921993 | ||
|
|
50b2eea4a2 |
6
.env.ci
Normal file
6
.env.ci
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Port mappings for CI containers — offset from dev defaults to avoid conflicts.
|
||||||
|
# Docker Compose reads this file via `env_file` in docker-compose.ci.yml.
|
||||||
|
DB_PORT=5433
|
||||||
|
REDIS_PORT=6380
|
||||||
|
TYPESENSE_PORT=8109
|
||||||
|
MINIO_PORT=9002
|
||||||
41
.env.test
41
.env.test
@@ -1,23 +1,34 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# GoodGo Platform — Test Environment Variables
|
# GoodGo Platform — Test Environment Variables
|
||||||
# Used by E2E tests (Playwright globalSetup loads this automatically)
|
# Used by E2E tests (Playwright globalSetup loads this automatically)
|
||||||
|
#
|
||||||
|
# These values MUST match docker-compose.ci.yml service credentials.
|
||||||
|
# Ports use CI_* offsets to avoid conflicts with dev containers.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# Test database — separate from development DB for isolation
|
# Test database — matches docker-compose.ci.yml postgres service
|
||||||
DATABASE_URL=postgresql://goodgo:goodgo_secret@localhost:5432/goodgo_test?schema=public
|
# Port 5433 avoids conflict with dev postgres on 5432
|
||||||
|
DATABASE_URL=postgresql://goodgo:goodgo_test_secret@localhost:5433/goodgo_test?schema=public
|
||||||
|
|
||||||
# Services (same as dev, adjust if your test infra differs)
|
# Redis — matches docker-compose.ci.yml redis service
|
||||||
REDIS_URL=redis://localhost:6379
|
# Port 6380 avoids conflict with dev redis on 6379
|
||||||
|
REDIS_URL=redis://localhost:6380
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6380
|
||||||
|
|
||||||
|
# Typesense — matches docker-compose.ci.yml typesense service
|
||||||
|
# Port 8109 avoids conflict with dev typesense on 8108
|
||||||
TYPESENSE_HOST=localhost
|
TYPESENSE_HOST=localhost
|
||||||
TYPESENSE_PORT=8108
|
TYPESENSE_PORT=8109
|
||||||
TYPESENSE_PROTOCOL=http
|
TYPESENSE_PROTOCOL=http
|
||||||
TYPESENSE_API_KEY=ts_dev_key_change_me
|
TYPESENSE_API_KEY=ts_ci_key
|
||||||
|
|
||||||
# MinIO
|
# MinIO — matches docker-compose.ci.yml minio service
|
||||||
|
# Port 9002 avoids conflict with dev minio on 9000
|
||||||
MINIO_ENDPOINT=localhost
|
MINIO_ENDPOINT=localhost
|
||||||
MINIO_PORT=9000
|
MINIO_PORT=9002
|
||||||
MINIO_ACCESS_KEY=test_minio_user
|
MINIO_ACCESS_KEY=ci_minio_user
|
||||||
MINIO_SECRET_KEY=test_minio_secret_key_32chars!!
|
MINIO_SECRET_KEY=ci_minio_secret_key_32chars!!
|
||||||
MINIO_BUCKET=goodgo-uploads
|
MINIO_BUCKET=goodgo-uploads
|
||||||
|
|
||||||
# Auth (deterministic secrets for test reproducibility)
|
# Auth (deterministic secrets for test reproducibility)
|
||||||
@@ -27,6 +38,16 @@ JWT_EXPIRES_IN=15m
|
|||||||
JWT_REFRESH_EXPIRES_IN=7d
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
NODE_ENV=test
|
NODE_ENV=test
|
||||||
|
|
||||||
|
# Server ports — offset to avoid conflicts with dev
|
||||||
|
API_PORT=3011
|
||||||
|
WEB_PORT=3010
|
||||||
|
API_BASE_URL=http://localhost:3011/api/v1/
|
||||||
|
WEB_BASE_URL=http://localhost:3010
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3011/api/v1
|
||||||
|
|
||||||
|
# CORS — allow web test origin
|
||||||
|
CORS_ORIGINS=http://localhost:3010,http://localhost:3000
|
||||||
|
|
||||||
# Bcrypt (fast rounds for test — production uses 12+)
|
# Bcrypt (fast rounds for test — production uses 12+)
|
||||||
BCRYPT_ROUNDS=4
|
BCRYPT_ROUNDS=4
|
||||||
|
|
||||||
|
|||||||
44
.github/workflows/deploy.yml
vendored
44
.github/workflows/deploy.yml
vendored
@@ -197,9 +197,11 @@ jobs:
|
|||||||
DEPLOY_KEY: ${{ secrets.STAGING_SSH_KEY }}
|
DEPLOY_KEY: ${{ secrets.STAGING_SSH_KEY }}
|
||||||
IMAGE_TAG: ${{ github.sha }}
|
IMAGE_TAG: ${{ github.sha }}
|
||||||
run: |
|
run: |
|
||||||
# Copy production compose and deploy
|
# Copy production compose, monitoring, and infra configs
|
||||||
scp -i ~/.ssh/deploy_key docker-compose.prod.yml "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/"
|
scp -i ~/.ssh/deploy_key docker-compose.prod.yml "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/"
|
||||||
scp -i ~/.ssh/deploy_key -r monitoring/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/monitoring/"
|
scp -i ~/.ssh/deploy_key -r monitoring/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/monitoring/"
|
||||||
|
scp -i ~/.ssh/deploy_key -r infra/pgbouncer/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/infra/pgbouncer/"
|
||||||
|
scp -i ~/.ssh/deploy_key -r scripts/backup/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/scripts/backup/"
|
||||||
|
|
||||||
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << DEPLOY_SCRIPT
|
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << DEPLOY_SCRIPT
|
||||||
cd ~/goodgo
|
cd ~/goodgo
|
||||||
@@ -218,12 +220,29 @@ jobs:
|
|||||||
docker compose -f docker-compose.prod.yml up -d --no-deps --wait ai-services
|
docker compose -f docker-compose.prod.yml up -d --no-deps --wait ai-services
|
||||||
|
|
||||||
# Run database migrations
|
# Run database migrations
|
||||||
docker compose -f docker-compose.prod.yml exec api npx prisma migrate deploy
|
docker compose -f docker-compose.prod.yml exec -T api npx prisma migrate deploy
|
||||||
|
|
||||||
# Cleanup old images
|
# Cleanup old images
|
||||||
docker image prune -f
|
docker image prune -f
|
||||||
DEPLOY_SCRIPT
|
DEPLOY_SCRIPT
|
||||||
|
|
||||||
|
- name: Sync Nginx configs
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.STAGING_HOST }}
|
||||||
|
DEPLOY_USER: ${{ secrets.STAGING_USER }}
|
||||||
|
DEPLOY_KEY: ${{ secrets.STAGING_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
scp -i ~/.ssh/deploy_key infra/nginx/*.conf \
|
||||||
|
"$DEPLOY_USER@$DEPLOY_HOST:/tmp/goodgo-nginx/"
|
||||||
|
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << 'NGINX_SCRIPT'
|
||||||
|
sudo mkdir -p /tmp/goodgo-nginx
|
||||||
|
sudo cp /tmp/goodgo-nginx/*.conf /etc/nginx/sites-available/ 2>/dev/null || true
|
||||||
|
for conf in /etc/nginx/sites-available/*goodgo*; do
|
||||||
|
[ -f "$conf" ] && sudo ln -sf "$conf" /etc/nginx/sites-enabled/
|
||||||
|
done
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
NGINX_SCRIPT
|
||||||
|
|
||||||
- name: Verify staging deployment
|
- name: Verify staging deployment
|
||||||
env:
|
env:
|
||||||
STAGING_URL: ${{ secrets.STAGING_URL }}
|
STAGING_URL: ${{ secrets.STAGING_URL }}
|
||||||
@@ -372,8 +391,11 @@ jobs:
|
|||||||
chmod 600 ~/.ssh/deploy_key
|
chmod 600 ~/.ssh/deploy_key
|
||||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
# Copy configs
|
||||||
scp -i ~/.ssh/deploy_key docker-compose.prod.yml "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/"
|
scp -i ~/.ssh/deploy_key docker-compose.prod.yml "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/"
|
||||||
scp -i ~/.ssh/deploy_key -r monitoring/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/monitoring/"
|
scp -i ~/.ssh/deploy_key -r monitoring/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/monitoring/"
|
||||||
|
scp -i ~/.ssh/deploy_key -r infra/pgbouncer/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/infra/pgbouncer/"
|
||||||
|
scp -i ~/.ssh/deploy_key -r scripts/backup/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/scripts/backup/"
|
||||||
|
|
||||||
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << DEPLOY_SCRIPT
|
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << DEPLOY_SCRIPT
|
||||||
cd ~/goodgo
|
cd ~/goodgo
|
||||||
@@ -389,11 +411,27 @@ jobs:
|
|||||||
docker compose -f docker-compose.prod.yml up -d --no-deps --wait web
|
docker compose -f docker-compose.prod.yml up -d --no-deps --wait web
|
||||||
docker compose -f docker-compose.prod.yml up -d --no-deps --wait ai-services
|
docker compose -f docker-compose.prod.yml up -d --no-deps --wait ai-services
|
||||||
|
|
||||||
docker compose -f docker-compose.prod.yml exec api npx prisma migrate deploy
|
docker compose -f docker-compose.prod.yml exec -T api npx prisma migrate deploy
|
||||||
|
|
||||||
docker image prune -f
|
docker image prune -f
|
||||||
DEPLOY_SCRIPT
|
DEPLOY_SCRIPT
|
||||||
|
|
||||||
|
- name: Sync Nginx configs (production)
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.PRODUCTION_HOST }}
|
||||||
|
DEPLOY_USER: ${{ secrets.PRODUCTION_USER }}
|
||||||
|
DEPLOY_KEY: ${{ secrets.PRODUCTION_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
scp -i ~/.ssh/deploy_key infra/nginx/*.conf \
|
||||||
|
"$DEPLOY_USER@$DEPLOY_HOST:/tmp/goodgo-nginx/"
|
||||||
|
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << 'NGINX_SCRIPT'
|
||||||
|
sudo cp /tmp/goodgo-nginx/*.conf /etc/nginx/sites-available/ 2>/dev/null || true
|
||||||
|
for conf in /etc/nginx/sites-available/*goodgo*; do
|
||||||
|
[ -f "$conf" ] && sudo ln -sf "$conf" /etc/nginx/sites-enabled/
|
||||||
|
done
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
NGINX_SCRIPT
|
||||||
|
|
||||||
- name: Verify production deployment
|
- name: Verify production deployment
|
||||||
env:
|
env:
|
||||||
PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }}
|
PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }}
|
||||||
|
|||||||
25
.github/workflows/e2e.yml
vendored
25
.github/workflows/e2e.yml
vendored
@@ -71,9 +71,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DATABASE_URL: postgresql://goodgo:goodgo_test_secret@localhost:5432/goodgo_test
|
DATABASE_URL: postgresql://goodgo:goodgo_test_secret@localhost:5432/goodgo_test
|
||||||
REDIS_URL: redis://localhost:6379
|
REDIS_URL: redis://localhost:6379
|
||||||
|
REDIS_HOST: localhost
|
||||||
|
REDIS_PORT: 6379
|
||||||
TYPESENSE_URL: http://localhost:8108
|
TYPESENSE_URL: http://localhost:8108
|
||||||
TYPESENSE_HOST: localhost
|
TYPESENSE_HOST: localhost
|
||||||
TYPESENSE_PORT: 8108
|
TYPESENSE_PORT: 8108
|
||||||
|
TYPESENSE_PROTOCOL: http
|
||||||
TYPESENSE_API_KEY: ts_ci_key
|
TYPESENSE_API_KEY: ts_ci_key
|
||||||
MINIO_ENDPOINT: localhost
|
MINIO_ENDPOINT: localhost
|
||||||
MINIO_PORT: 9000
|
MINIO_PORT: 9000
|
||||||
@@ -81,12 +84,28 @@ jobs:
|
|||||||
MINIO_SECRET_KEY: ${{ vars.CI_MINIO_SECRET_KEY || 'ci_minio_secret_key_32chars!!' }}
|
MINIO_SECRET_KEY: ${{ vars.CI_MINIO_SECRET_KEY || 'ci_minio_secret_key_32chars!!' }}
|
||||||
MINIO_BUCKET: goodgo-uploads
|
MINIO_BUCKET: goodgo-uploads
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
JWT_SECRET: e2e-test-jwt-secret-key
|
CI: true
|
||||||
JWT_REFRESH_SECRET: e2e-test-refresh-secret-key
|
# API and Web ports for Playwright webServer
|
||||||
|
API_PORT: 3001
|
||||||
|
WEB_PORT: 3000
|
||||||
|
API_BASE_URL: http://localhost:3001/api/v1/
|
||||||
|
WEB_BASE_URL: http://localhost:3000
|
||||||
|
NEXT_PUBLIC_API_URL: http://localhost:3001/api/v1
|
||||||
|
JWT_SECRET: e2e-test-jwt-secret-key-minimum-32-chars-long-enough
|
||||||
|
JWT_REFRESH_SECRET: e2e-test-refresh-secret-key-minimum-32-chars-ok
|
||||||
|
JWT_EXPIRES_IN: 15m
|
||||||
|
JWT_REFRESH_EXPIRES_IN: 7d
|
||||||
|
BCRYPT_ROUNDS: 4
|
||||||
VNPAY_TMN_CODE: TESTCODE
|
VNPAY_TMN_CODE: TESTCODE
|
||||||
VNPAY_HASH_SECRET: TESTHASHSECRET
|
VNPAY_HASH_SECRET: TESTHASHSECRETTESTHASHSECRETTEST
|
||||||
VNPAY_URL: https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
|
VNPAY_URL: https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
|
||||||
VNPAY_RETURN_URL: http://localhost:3000/payment/return
|
VNPAY_RETURN_URL: http://localhost:3000/payment/return
|
||||||
|
GOOGLE_CLIENT_ID: test-google-client-id
|
||||||
|
GOOGLE_CLIENT_SECRET: test-google-client-secret
|
||||||
|
GOOGLE_CALLBACK_URL: http://localhost:3001/api/v1/auth/google/callback
|
||||||
|
ZALO_APP_ID: test-zalo-app-id
|
||||||
|
ZALO_APP_SECRET: test-zalo-app-secret
|
||||||
|
ZALO_CALLBACK_URL: http://localhost:3001/api/v1/auth/zalo/callback
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
291
AUDIT_INDEX.md
291
AUDIT_INDEX.md
@@ -1,291 +0,0 @@
|
|||||||
# GoodGo Platform AI — Audit Reports Index
|
|
||||||
**Generated**: 2026-04-11 | **Status**: Wave 10 (Active Development)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
### 📋 Main Audit Reports
|
|
||||||
1. **[COMPREHENSIVE_AUDIT_2026-04-11.md](COMPREHENSIVE_AUDIT_2026-04-11.md)** (768 lines)
|
|
||||||
- Complete codebase analysis with all 10 required sections
|
|
||||||
- Detailed module inventory, architecture breakdown, metrics
|
|
||||||
- Strengths, weaknesses, and actionable recommendations
|
|
||||||
|
|
||||||
2. **[AUDIT_SUMMARY_2026-04-11.txt](AUDIT_SUMMARY_2026-04-11.txt)** (Quick Reference)
|
|
||||||
- Executive summary with key metrics and scores
|
|
||||||
- Visual breakdown of codebase structure
|
|
||||||
- Priority recommendations at a glance
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Audit Scope (All 10 Requirements Covered)
|
|
||||||
|
|
||||||
### ✅ 1. Top-Level Structure
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 1
|
|
||||||
- **Coverage**: All root directories, 10 config files, monorepo setup
|
|
||||||
- **Status**: Complete
|
|
||||||
|
|
||||||
### ✅ 2. Apps/API Module Analysis
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 2
|
|
||||||
- **Coverage**: 16 API modules, layer analysis, 788 TypeScript files, 229 tests
|
|
||||||
- **Findings**: 13 full-stack modules, 3 incomplete (health, metrics, mcp)
|
|
||||||
|
|
||||||
### ✅ 3. Apps/Web Frontend
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 3
|
|
||||||
- **Coverage**: 28 routes across 4 layout groups, 66 components, 16,568 LOC
|
|
||||||
- **Findings**: Full Next.js 14 implementation, limited unit tests (6 only)
|
|
||||||
|
|
||||||
### ✅ 4. Prisma Database Layer
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 4
|
|
||||||
- **Coverage**: 21 models, 18 enums, 12 migrations, 78 indexes
|
|
||||||
- **Findings**: Production-ready schema with GDPR compliance, audit logging
|
|
||||||
|
|
||||||
### ✅ 5. Shared Libraries (libs/)
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 5
|
|
||||||
- **Coverage**: AI services (21 Python files), MCP servers (12 TypeScript files)
|
|
||||||
- **Findings**: AI services minimal, MCP servers are stubs needing implementation
|
|
||||||
|
|
||||||
### ✅ 6. E2E Testing
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 6
|
|
||||||
- **Coverage**: 31 Playwright specs (16 API, 15 Web), test organization
|
|
||||||
- **Findings**: Good E2E coverage, global setup/teardown configured
|
|
||||||
|
|
||||||
### ✅ 7. Configuration Files
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 7
|
|
||||||
- **Coverage**: 10 root config files, 178-line .env.example, Docker stacks
|
|
||||||
- **Findings**: Comprehensive configuration documentation
|
|
||||||
|
|
||||||
### ✅ 8. Test Coverage Analysis
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 8
|
|
||||||
- **Coverage**: 745 total test files breakdown by layer and module
|
|
||||||
- **Findings**: 229 API tests, 6 web tests, 31 E2E specs
|
|
||||||
|
|
||||||
### ✅ 9. Documentation
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 9
|
|
||||||
- **Coverage**: 89 core docs + 81 audit reports in docs/audits/
|
|
||||||
- **Findings**: Comprehensive documentation trail
|
|
||||||
|
|
||||||
### ✅ 10. CI/CD Pipeline
|
|
||||||
- **File**: COMPREHENSIVE_AUDIT_2026-04-11.md, Section 10
|
|
||||||
- **Coverage**: 7 GitHub Actions workflows, 13-service Docker stack
|
|
||||||
- **Findings**: Production-ready DevOps, Kubernetes-ready
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Findings Summary
|
|
||||||
|
|
||||||
### 📊 Codebase Metrics
|
|
||||||
```
|
|
||||||
Total Lines of Code: 76,402 LOC
|
|
||||||
├─ API Backend: 23,926 LOC (31%)
|
|
||||||
├─ Web Frontend: 16,568 LOC (22%)
|
|
||||||
├─ Test Files: ~34,100 LOC (45%)
|
|
||||||
├─ MCP Servers: 984 LOC (1%)
|
|
||||||
└─ AI Services: 824 LOC (1%)
|
|
||||||
|
|
||||||
TypeScript Files: 1,038
|
|
||||||
Test Files: 745
|
|
||||||
Documentation: 89 files + 81 audits
|
|
||||||
Git Commits: 203
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🏗️ Architecture Summary
|
|
||||||
- **16 NestJS API modules** (13 full-stack with ADIP layers)
|
|
||||||
- **28 Next.js routes** (public, auth, dashboard, admin)
|
|
||||||
- **21 Prisma models** (comprehensive domain model)
|
|
||||||
- **12 database migrations** (schema evolution tracked)
|
|
||||||
- **7 GitHub Actions workflows** (CI/CD complete)
|
|
||||||
|
|
||||||
### 📈 Quality Scores
|
|
||||||
| Aspect | Score | Status |
|
|
||||||
|--------|-------|--------|
|
|
||||||
| Architecture | 9/10 | ✅ Excellent |
|
|
||||||
| Code Quality | 8/10 | ✅ Good |
|
|
||||||
| Test Coverage | 7/10 | ⚠️ Needs web tests |
|
|
||||||
| Documentation | 8/10 | ✅ Comprehensive |
|
|
||||||
| CI/CD | 9/10 | ✅ Excellent |
|
|
||||||
| Database | 9/10 | ✅ Excellent |
|
|
||||||
| Error Handling | 8/10 | ⚠️ Some gaps |
|
|
||||||
| Performance | 8/10 | ✅ Good |
|
|
||||||
| Security | 7/10 | ⚠️ Add MFA |
|
|
||||||
| DevOps | 9/10 | ✅ Excellent |
|
|
||||||
| **OVERALL** | **8.2/10** | **✅ Production-Ready** |
|
|
||||||
|
|
||||||
### 🎯 Key Strengths
|
|
||||||
1. ✅ Mature DDD + CQRS architecture
|
|
||||||
2. ✅ 76K LOC of real implementation
|
|
||||||
3. ✅ 745+ test files (229 API, 31 E2E)
|
|
||||||
4. ✅ Modern tech stack (NestJS 11, Next.js 14, PostgreSQL 16)
|
|
||||||
5. ✅ Strong DevOps (Docker, K8s, GitHub Actions)
|
|
||||||
6. ✅ Excellent documentation (89 docs + 81 audits)
|
|
||||||
7. ✅ Type-safe TypeScript (strict mode)
|
|
||||||
8. ✅ 21 models with 78 indexes (optimized)
|
|
||||||
|
|
||||||
### ⚠️ Areas for Improvement
|
|
||||||
1. ⚠️ Incomplete modules (3): health, metrics, mcp
|
|
||||||
2. ⚠️ Web unit tests: only 6 (needs 50% coverage)
|
|
||||||
3. ⚠️ MCP servers: stubs only (~50 lines each)
|
|
||||||
4. ⚠️ Error handling: some CQRS handlers incomplete
|
|
||||||
5. ⚠️ Security: add field encryption, MFA, rate limiting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendations Priority Matrix
|
|
||||||
|
|
||||||
### 🔴 High Priority (DO NOW) — 30-40 hours
|
|
||||||
1. **Complete incomplete modules** (health, metrics, mcp)
|
|
||||||
- Implement full ADIP layers for health/metrics
|
|
||||||
- Real MCP server implementations
|
|
||||||
- Effort: 5-10 hours
|
|
||||||
|
|
||||||
2. **Expand web unit tests to 50% coverage**
|
|
||||||
- Focus on critical components (auth, listings, search)
|
|
||||||
- Effort: 10-15 hours
|
|
||||||
|
|
||||||
3. **Audit & complete error handling**
|
|
||||||
- Review remaining CQRS handlers
|
|
||||||
- Ensure consistent error responses
|
|
||||||
- Effort: 5 hours
|
|
||||||
|
|
||||||
### 🟡 Medium Priority (DO SOON) — 40-60 hours
|
|
||||||
1. **Add field-level encryption** (PII, payments)
|
|
||||||
2. **Implement API rate limiting** (per-endpoint quotas)
|
|
||||||
3. **Add OpenTelemetry tracing** (distributed tracing)
|
|
||||||
4. **Expand monitoring dashboards** (Grafana)
|
|
||||||
5. **Performance optimization** (query analysis)
|
|
||||||
|
|
||||||
### 🟢 Low Priority (DO LATER) — Future phases
|
|
||||||
1. GraphQL API (optional)
|
|
||||||
2. Mobile app (React Native/Flutter)
|
|
||||||
3. Advanced ML features
|
|
||||||
4. Multi-tenant support
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Development Status
|
|
||||||
|
|
||||||
### Current Milestone: Wave 10 (Beta Phase)
|
|
||||||
- **MVP Phase**: ✅ COMPLETE (Core modules, DDD architecture)
|
|
||||||
- **Beta Phase**: 🔄 IN PROGRESS (Testing, refinement, monitoring)
|
|
||||||
- **Production Phase**: ⏳ READY (Pending validation)
|
|
||||||
- **Scale Phase**: 📋 PLANNED
|
|
||||||
|
|
||||||
### Recent Progress (Last 10 commits)
|
|
||||||
- ✅ Added comprehensive alerting rules (Alertmanager)
|
|
||||||
- ✅ K6 load testing coverage expanded
|
|
||||||
- ✅ Error handling added to 51 CQRS handlers
|
|
||||||
- ✅ Login endpoint fixed (prevented 500 errors)
|
|
||||||
- ✅ Email alert templates for saved searches
|
|
||||||
- ✅ Unit tests added for MCP, Inquiries, Leads modules
|
|
||||||
|
|
||||||
### Development Velocity
|
|
||||||
- 203 total commits on master
|
|
||||||
- ~2 commits/day average
|
|
||||||
- Consistent feature delivery & bug fixes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment Status
|
|
||||||
|
|
||||||
### Ready for:
|
|
||||||
✅ **MVP Launch** — All core features implemented
|
|
||||||
✅ **Staging Deployment** — Full CI/CD pipeline configured
|
|
||||||
⏳ **Production** — Pending final validation & load testing
|
|
||||||
|
|
||||||
### Infrastructure Status
|
|
||||||
✅ Local development (docker-compose.yml, 13 services)
|
|
||||||
✅ CI environment (docker-compose.ci.yml)
|
|
||||||
✅ Production stack (docker-compose.prod.yml)
|
|
||||||
✅ Kubernetes manifests (infra/)
|
|
||||||
✅ Monitoring (Prometheus + Grafana)
|
|
||||||
✅ Backup/restore (pg-backup + verification)
|
|
||||||
✅ Load testing (K6 suite)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Technology Stack Summary
|
|
||||||
|
|
||||||
| Layer | Technology | Version |
|
|
||||||
|-------|-----------|---------|
|
|
||||||
| Backend | NestJS | 11 |
|
|
||||||
| Frontend | Next.js | 14 |
|
|
||||||
| Runtime | Node.js | 22+ |
|
|
||||||
| Database | PostgreSQL | 16 + PostGIS 3.4 |
|
|
||||||
| Search | Typesense | 27 |
|
|
||||||
| Cache | Redis | 7 |
|
|
||||||
| Storage | MinIO | Latest |
|
|
||||||
| AI/ML | FastAPI | + XGBoost |
|
|
||||||
| Testing | Playwright | 1.59 |
|
|
||||||
| Testing | Vitest | Latest |
|
|
||||||
| CI/CD | GitHub Actions | - |
|
|
||||||
| Monitoring | Prometheus/Grafana | Latest |
|
|
||||||
| Package Manager | pnpm | 10.27.0 |
|
|
||||||
| Build Tool | Turbo | 2.9.4 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Use These Reports
|
|
||||||
|
|
||||||
### For Project Managers
|
|
||||||
- Read: **AUDIT_SUMMARY_2026-04-11.txt** (quick overview)
|
|
||||||
- Then: **COMPREHENSIVE_AUDIT_2026-04-11.md** sections 1, 8-10
|
|
||||||
|
|
||||||
### For Developers
|
|
||||||
- Read: **COMPREHENSIVE_AUDIT_2026-04-11.md** entire document
|
|
||||||
- Reference: **AUDIT_SUMMARY_2026-04-11.txt** for quick stats
|
|
||||||
|
|
||||||
### For Architects
|
|
||||||
- Focus: Sections 1-5, 7 of comprehensive audit
|
|
||||||
- Review: Module completeness, architecture patterns
|
|
||||||
|
|
||||||
### For QA/Testers
|
|
||||||
- Focus: Sections 6, 8 of comprehensive audit
|
|
||||||
- Review: Test coverage, E2E test organization
|
|
||||||
|
|
||||||
### For DevOps/Infrastructure
|
|
||||||
- Focus: Sections 7, 10 of comprehensive audit
|
|
||||||
- Review: CI/CD workflows, Docker stack, monitoring
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
### In Repository
|
|
||||||
- `docs/architecture.md` — Detailed system design
|
|
||||||
- `docs/api-endpoints.md` — REST API reference
|
|
||||||
- `docs/api-error-codes.md` — Error handling guide
|
|
||||||
- `docs/deployment.md` — Production deployment guide
|
|
||||||
- `IMPLEMENTATION_PLAN.md` — Remaining work
|
|
||||||
- `PROJECT_TRACKER.md` — Development roadmap
|
|
||||||
- `docs/audits/` — 81 specialized audit reports
|
|
||||||
|
|
||||||
### Key Files
|
|
||||||
- `README.md` — Project overview & quick start
|
|
||||||
- `CONTRIBUTING.md` — Development conventions
|
|
||||||
- `CHANGELOG.md` — Version history
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Audit Verification Checklist
|
|
||||||
|
|
||||||
- [x] Top-level structure reviewed (all root directories)
|
|
||||||
- [x] apps/api module analysis complete (16 modules, 788 files)
|
|
||||||
- [x] apps/web frontend mapped (28 routes, 66 components)
|
|
||||||
- [x] prisma schema analyzed (21 models, 12 migrations)
|
|
||||||
- [x] libs/ libraries reviewed (AI + MCP servers)
|
|
||||||
- [x] E2E testing evaluated (31 Playwright specs)
|
|
||||||
- [x] Configuration files documented (10 root configs)
|
|
||||||
- [x] Test coverage analyzed (745 total files)
|
|
||||||
- [x] Documentation surveyed (89 docs + 81 audits)
|
|
||||||
- [x] CI/CD pipeline reviewed (7 workflows, 13 services)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Audit Conducted**: 2026-04-11
|
|
||||||
**Status**: ✅ COMPLETE
|
|
||||||
**Quality Score**: 8.2/10 (Production-Ready)
|
|
||||||
**Next Review**: Recommend after Wave 10 completion
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*For questions or clarifications, refer to the comprehensive audit document or contact the development team.*
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
# Quick Reference: Pricing/Subscription/Payment System
|
|
||||||
|
|
||||||
## Files at a Glance
|
|
||||||
|
|
||||||
### 🎨 Frontend
|
|
||||||
|
|
||||||
| File | Purpose | Status |
|
|
||||||
|------|---------|--------|
|
|
||||||
| `apps/web/app/[locale]/(public)/pricing/page.tsx` | Main pricing page | ✅ Complete |
|
|
||||||
| `apps/web/lib/subscription-api.ts` | Subscription API client | ✅ Complete |
|
|
||||||
| `apps/web/lib/payment-api.ts` | Payment API client | ✅ Complete |
|
|
||||||
| `apps/web/lib/hooks/use-subscription.ts` | Subscription hooks | ✅ Complete |
|
|
||||||
| `apps/web/lib/hooks/use-payments.ts` | Payment hooks | ✅ Complete |
|
|
||||||
| `apps/web/app/.../dashboard/payments/page.tsx` | Payment history | ✅ Complete |
|
|
||||||
|
|
||||||
### 🔧 Backend
|
|
||||||
|
|
||||||
| Directory | Purpose | Status |
|
|
||||||
|-----------|---------|--------|
|
|
||||||
| `apps/api/src/modules/subscriptions/` | Subscription CQRS module | ✅ Complete |
|
|
||||||
| `apps/api/src/modules/payments/` | Payment CQRS module | ✅ Complete |
|
|
||||||
| `apps/api/src/modules/payments/infrastructure/services/` | Payment gateways (VNPay, MoMo, ZaloPay) | ✅ Complete |
|
|
||||||
|
|
||||||
### 📦 Database
|
|
||||||
|
|
||||||
| Model | Fields | Relationships |
|
|
||||||
|-------|--------|---|
|
|
||||||
| `Plan` | id, tier (unique), name, prices, features, isActive | 1→M Subscription |
|
|
||||||
| `Subscription` | id, userId (unique), planId, status, periods, cancelledAt | M←1 Plan, 1←1 User |
|
|
||||||
| `Payment` | id, userId, provider, type, amountVND, status, providerTxId, idempotencyKey | M←1 User |
|
|
||||||
| `UsageRecord` | id, subscriptionId, metric, count, periods | M←1 Subscription |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key API Endpoints
|
|
||||||
|
|
||||||
### Plans (Public)
|
|
||||||
```
|
|
||||||
GET /subscriptions/plans
|
|
||||||
GET /subscriptions/plans/:tier
|
|
||||||
```
|
|
||||||
|
|
||||||
### Subscriptions (Auth Required)
|
|
||||||
```
|
|
||||||
POST /subscriptions # Create new
|
|
||||||
PUT /subscriptions/upgrade # Upgrade
|
|
||||||
DELETE /subscriptions # Cancel
|
|
||||||
GET /subscriptions/quota/:metric # Check quota
|
|
||||||
POST /subscriptions/usage # Record usage
|
|
||||||
GET /subscriptions/billing # View history
|
|
||||||
```
|
|
||||||
|
|
||||||
### Payments (Auth + Webhook)
|
|
||||||
```
|
|
||||||
POST /payments # Create payment → returns paymentUrl
|
|
||||||
POST /payments/callback/:provider # Webhook from gateway
|
|
||||||
GET /payments/:id # Check status
|
|
||||||
GET /payments # List transactions
|
|
||||||
POST /payments/:id/refund # Refund (admin)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Type Definitions
|
|
||||||
|
|
||||||
### Frontend Types
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// From subscription-api.ts
|
|
||||||
interface PlanDto {
|
|
||||||
id: string;
|
|
||||||
tier: string; // FREE, AGENT_PRO, INVESTOR, ENTERPRISE
|
|
||||||
name: string;
|
|
||||||
priceMonthlyVND: string; // In VND
|
|
||||||
priceYearlyVND: string; // In VND
|
|
||||||
maxListings: number;
|
|
||||||
maxSavedSearches: number;
|
|
||||||
features: Record<string, boolean | number | string>;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateSubscriptionResult {
|
|
||||||
subscriptionId: string;
|
|
||||||
planTier: string;
|
|
||||||
status: string; // ACTIVE, PAST_DUE, CANCELLED, EXPIRED
|
|
||||||
currentPeriodStart: string; // ISO datetime
|
|
||||||
currentPeriodEnd: string; // ISO datetime
|
|
||||||
}
|
|
||||||
|
|
||||||
// From payment-api.ts
|
|
||||||
interface CreatePaymentPayload {
|
|
||||||
provider: 'VNPAY' | 'MOMO' | 'ZALOPAY' | 'BANK_TRANSFER';
|
|
||||||
type: 'SUBSCRIPTION' | 'LISTING_FEE' | 'DEPOSIT' | 'FEATURED_LISTING';
|
|
||||||
amountVND: number; // 1 to 100,000,000,000
|
|
||||||
description: string;
|
|
||||||
returnUrl: string; // Redirect after payment
|
|
||||||
idempotencyKey?: string; // Prevent duplicates
|
|
||||||
transactionId?: string; // External transaction ID
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreatePaymentResult {
|
|
||||||
paymentId: string;
|
|
||||||
paymentUrl: string; // Redirect user here
|
|
||||||
providerTxId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PaymentStatusDto {
|
|
||||||
id: string;
|
|
||||||
provider: string;
|
|
||||||
type: string;
|
|
||||||
amountVND: string;
|
|
||||||
status: string; // PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED
|
|
||||||
providerTxId: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Use in Frontend
|
|
||||||
|
|
||||||
### Get Plans
|
|
||||||
```typescript
|
|
||||||
import { usePlans } from '@/lib/hooks/use-subscription';
|
|
||||||
|
|
||||||
export function MyComponent() {
|
|
||||||
const { data: plans, isLoading } = usePlans();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{isLoading ? 'Loading...' : plans?.map(plan => <div>{plan.name}</div>)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Payment
|
|
||||||
```typescript
|
|
||||||
import { paymentApi } from '@/lib/payment-api';
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
const createPaymentMutation = useMutation({
|
|
||||||
mutationFn: (payload) => paymentApi.createPayment(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
// When user clicks "Pay Now"
|
|
||||||
const handlePayment = async (planTier: string, provider: 'VNPAY' | 'MOMO' | 'ZALOPAY') => {
|
|
||||||
const result = await createPaymentMutation.mutateAsync({
|
|
||||||
provider,
|
|
||||||
type: 'SUBSCRIPTION',
|
|
||||||
amountVND: 499000,
|
|
||||||
description: `Subscription to ${planTier}`,
|
|
||||||
returnUrl: `${window.location.origin}/payment-return`,
|
|
||||||
idempotencyKey: crypto.randomUUID(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Redirect to payment gateway
|
|
||||||
window.location = result.paymentUrl;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Payment Status (on return page)
|
|
||||||
```typescript
|
|
||||||
import { paymentApi } from '@/lib/payment-api';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export function PaymentReturnPage() {
|
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
|
||||||
const paymentId = searchParams.get('paymentId');
|
|
||||||
const [status, setStatus] = useState<string>('loading');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!paymentId) return;
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
const payment = await paymentApi.getPaymentStatus(paymentId);
|
|
||||||
|
|
||||||
if (payment.status === 'COMPLETED') {
|
|
||||||
// Create subscription
|
|
||||||
await subscriptionApi.createSubscription('AGENT_PRO', 'monthly');
|
|
||||||
setStatus('success');
|
|
||||||
// Redirect to dashboard
|
|
||||||
window.location = '/dashboard';
|
|
||||||
} else if (payment.status === 'FAILED') {
|
|
||||||
setStatus('failed');
|
|
||||||
} else {
|
|
||||||
// Poll again in 2 seconds
|
|
||||||
setTimeout(poll, 2000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
poll();
|
|
||||||
}, [paymentId]);
|
|
||||||
|
|
||||||
return <div>{status === 'loading' ? 'Processing payment...' : status}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Subscription
|
|
||||||
```typescript
|
|
||||||
import { subscriptionApi } from '@/lib/subscription-api';
|
|
||||||
|
|
||||||
const result = await subscriptionApi.createSubscription('AGENT_PRO', 'monthly');
|
|
||||||
console.log(result);
|
|
||||||
// {
|
|
||||||
// subscriptionId: 'cuid...',
|
|
||||||
// planTier: 'AGENT_PRO',
|
|
||||||
// status: 'ACTIVE',
|
|
||||||
// currentPeriodStart: '2024-04-12T...',
|
|
||||||
// currentPeriodEnd: '2024-05-12T...'
|
|
||||||
// }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Use in Backend
|
|
||||||
|
|
||||||
### Create Plan (Admin)
|
|
||||||
```sql
|
|
||||||
INSERT INTO "Plan" (id, tier, name, "priceMonthlyVND", "priceYearlyVND", "maxListings", features, "isActive")
|
|
||||||
VALUES (
|
|
||||||
'cuid123',
|
|
||||||
'AGENT_PRO',
|
|
||||||
'Agent Pro',
|
|
||||||
499000,
|
|
||||||
4990000,
|
|
||||||
50,
|
|
||||||
'{"analytics": true, "aiValuation": true}',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Payment (via API)
|
|
||||||
```
|
|
||||||
POST /payments
|
|
||||||
Authorization: Bearer <jwt_token>
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"provider": "VNPAY",
|
|
||||||
"type": "SUBSCRIPTION",
|
|
||||||
"amountVND": 499000,
|
|
||||||
"description": "Agent Pro - Monthly",
|
|
||||||
"returnUrl": "https://goodgo.vn/payment-return",
|
|
||||||
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
"paymentId": "cuid456",
|
|
||||||
"paymentUrl": "https://sandbox.vnpayment.vn/paymentv2/vpcpay.html?...",
|
|
||||||
"providerTxId": "cuid456"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handle Payment Callback (Webhook)
|
|
||||||
```
|
|
||||||
POST /payments/callback/vnpay?vnp_TxnRef=cuid456&vnp_ResponseCode=00&vnp_SecureHash=...
|
|
||||||
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
"orderId": "cuid456",
|
|
||||||
"isSuccess": true,
|
|
||||||
"status": "COMPLETED"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Subscription (via API)
|
|
||||||
```
|
|
||||||
POST /subscriptions
|
|
||||||
Authorization: Bearer <jwt_token>
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"planTier": "AGENT_PRO",
|
|
||||||
"billingCycle": "monthly"
|
|
||||||
}
|
|
||||||
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
"subscriptionId": "cuid789",
|
|
||||||
"planTier": "AGENT_PRO",
|
|
||||||
"status": "ACTIVE",
|
|
||||||
"currentPeriodStart": "2024-04-12T...",
|
|
||||||
"currentPeriodEnd": "2024-05-12T..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pricing Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
FREE (0 VND)
|
|
||||||
├── 3 listings
|
|
||||||
├── 5 saved searches
|
|
||||||
└── Basic features
|
|
||||||
|
|
||||||
AGENT_PRO (499,000 VND/month | 4,990,000/year = -17%)
|
|
||||||
├── 50 listings
|
|
||||||
├── 30 saved searches
|
|
||||||
├── Analytics
|
|
||||||
├── AI Valuation
|
|
||||||
├── Priority support
|
|
||||||
└── Lead management
|
|
||||||
|
|
||||||
INVESTOR (999,000 VND/month | 9,990,000/year = -17%)
|
|
||||||
├── 20 listings
|
|
||||||
├── 100 saved searches
|
|
||||||
├── Analytics
|
|
||||||
├── AI Valuation
|
|
||||||
├── Market reports
|
|
||||||
├── Price alerts
|
|
||||||
└── Portfolio tracking
|
|
||||||
|
|
||||||
ENTERPRISE (4,990,000 VND/month | 49,900,000/year = -17%)
|
|
||||||
├── Unlimited listings
|
|
||||||
├── Unlimited searches
|
|
||||||
├── All INVESTOR features
|
|
||||||
├── API access
|
|
||||||
├── White label
|
|
||||||
└── Dedicated support
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend (.env)
|
|
||||||
VNPAY_TMN_CODE=your_tmn_code
|
|
||||||
VNPAY_HASH_SECRET=your_hash_secret
|
|
||||||
VNPAY_BASE_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
|
|
||||||
VNPAY_API_URL=https://sandbox.vnpayment.vn/merchant_webapi/api/transaction
|
|
||||||
|
|
||||||
MOMO_PARTNER_CODE=your_partner_code
|
|
||||||
MOMO_ACCESS_KEY=your_access_key
|
|
||||||
MOMO_SECRET_KEY=your_secret_key
|
|
||||||
MOMO_ENDPOINT=https://test-payment.momo.vn/v2/gateway/api
|
|
||||||
|
|
||||||
ZALOPAY_APP_ID=your_app_id
|
|
||||||
ZALOPAY_KEY1=your_key1
|
|
||||||
ZALOPAY_KEY2=your_key2
|
|
||||||
ZALOPAY_ENDPOINT=https://sandbox.zalopay.com.vn
|
|
||||||
|
|
||||||
# Frontend (.env.local)
|
|
||||||
NEXT_PUBLIC_APP_URL=https://goodgo.vn
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Credentials
|
|
||||||
|
|
||||||
### VNPay Sandbox
|
|
||||||
```
|
|
||||||
Terminal: 0
|
|
||||||
Account: 0968323286
|
|
||||||
Password: 123456
|
|
||||||
Card: 9704198526191432198
|
|
||||||
OTP: 123456
|
|
||||||
```
|
|
||||||
|
|
||||||
### MoMo Sandbox
|
|
||||||
```
|
|
||||||
Phone: 0987654321
|
|
||||||
Password: 123456
|
|
||||||
OTP: 123456
|
|
||||||
```
|
|
||||||
|
|
||||||
### ZaloPay Sandbox
|
|
||||||
```
|
|
||||||
Phone: 0987654321
|
|
||||||
OTP: 123456
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Errors
|
|
||||||
|
|
||||||
| Error | Cause | Solution |
|
|
||||||
|-------|-------|----------|
|
|
||||||
| `ConflictException: User already has active subscription` | User trying to create 2nd subscription | Check existing subscription first |
|
|
||||||
| `ValidationException: Số tiền phải lớn hơn 0` | Amount is 0 or negative | Ensure amount > 0 |
|
|
||||||
| `NotFoundException: Plan not found` | Plan tier doesn't exist in DB | Check plan is created and isActive=true |
|
|
||||||
| `Payment gateway failed` | Payment gateway credentials wrong | Verify ENV vars |
|
|
||||||
| `Cannot complete payment in status X` | Payment already completed/failed | Check idempotencyKey |
|
|
||||||
| `Idempotency check failed` | Same idempotencyKey used twice | Generate unique UUID each time |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Debugging Checklist
|
|
||||||
|
|
||||||
- [ ] Check payment provider credentials in .env
|
|
||||||
- [ ] Verify idempotencyKey is unique per request
|
|
||||||
- [ ] Ensure amountVND matches plan price
|
|
||||||
- [ ] Check returnUrl is publicly accessible
|
|
||||||
- [ ] Verify JWT token is valid when calling protected endpoints
|
|
||||||
- [ ] Check payment status with `GET /payments/:id`
|
|
||||||
- [ ] Review payment provider logs/dashboard
|
|
||||||
- [ ] Test with sandbox credentials first
|
|
||||||
- [ ] Verify callback signature matches gateway requirements
|
|
||||||
- [ ] Check subscription was created after successful payment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Detailed Audit: `PRICING_CHECKOUT_AUDIT.md`
|
|
||||||
- Summary: `PRICING_AUDIT_SUMMARY.md`
|
|
||||||
- Pricing Page: `apps/web/app/[locale]/(public)/pricing/page.tsx`
|
|
||||||
- Subscriptions Module: `apps/api/src/modules/subscriptions/`
|
|
||||||
- Payments Module: `apps/api/src/modules/payments/`
|
|
||||||
- Schema: `prisma/schema.prisma` (lines 451-514)
|
|
||||||
|
|
||||||
259
SEED_GENERATION_SCRIPT.ts
Normal file
259
SEED_GENERATION_SCRIPT.ts
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* GoodGo Platform - Seed User Generation Script
|
||||||
|
*
|
||||||
|
* Creates seed users with full login capability (passwords + PII hashing)
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* export FIELD_ENCRYPTION_KEY='hex-encoded-32-byte-key'
|
||||||
|
* npx tsx scripts/seed-with-auth.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import { PrismaClient, UserRole, KYCStatus } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Configuration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface SeedUserConfig {
|
||||||
|
id: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
fullName: string;
|
||||||
|
password: string;
|
||||||
|
role: UserRole;
|
||||||
|
kycStatus: KYCStatus;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEED_USERS: SeedUserConfig[] = [
|
||||||
|
{
|
||||||
|
id: 'seed-admin-001',
|
||||||
|
phone: '0900000001',
|
||||||
|
email: 'admin@goodgo.vn',
|
||||||
|
fullName: 'Admin GoodGo',
|
||||||
|
password: 'AdminPassword123',
|
||||||
|
role: UserRole.ADMIN,
|
||||||
|
kycStatus: 'VERIFIED',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'seed-agent-001',
|
||||||
|
phone: '0900000002',
|
||||||
|
email: 'agent.nguyen@goodgo.vn',
|
||||||
|
fullName: 'Nguyễn Văn An',
|
||||||
|
password: 'AgentPassword123',
|
||||||
|
role: UserRole.AGENT,
|
||||||
|
kycStatus: 'VERIFIED',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'seed-seller-001',
|
||||||
|
phone: '0900000005',
|
||||||
|
email: 'seller.pham@gmail.com',
|
||||||
|
fullName: 'Phạm Đức Dũng',
|
||||||
|
password: 'SellerPassword123',
|
||||||
|
role: UserRole.SELLER,
|
||||||
|
kycStatus: 'VERIFIED',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'seed-buyer-001',
|
||||||
|
phone: '0900000004',
|
||||||
|
email: 'buyer.le@gmail.com',
|
||||||
|
fullName: 'Lê Minh Cường',
|
||||||
|
password: 'BuyerPassword123',
|
||||||
|
role: UserRole.BUYER,
|
||||||
|
kycStatus: 'NONE',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helper Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize Vietnamese phone number to +84... format
|
||||||
|
*/
|
||||||
|
function normalizeVietnamPhone(phone: string): string {
|
||||||
|
const cleaned = phone.replace(/[\s.-]/g, '');
|
||||||
|
if (cleaned.startsWith('+84')) return cleaned;
|
||||||
|
if (cleaned.startsWith('84')) return `+${cleaned}`;
|
||||||
|
if (cleaned.startsWith('0')) return `+84${cleaned.slice(1)}`;
|
||||||
|
throw new Error(`Invalid phone format: ${phone}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive HMAC key from encryption key (same as field-encryption.ts)
|
||||||
|
*/
|
||||||
|
function deriveHmacKey(encryptionKeyHex: string): Buffer {
|
||||||
|
return crypto.hkdfSync(
|
||||||
|
'sha256',
|
||||||
|
Buffer.from(encryptionKeyHex, 'hex'),
|
||||||
|
Buffer.alloc(0),
|
||||||
|
Buffer.from('goodgo-field-hash', 'utf8'),
|
||||||
|
32,
|
||||||
|
) as unknown as Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute HMAC-SHA256 hash for searchable fields
|
||||||
|
*/
|
||||||
|
function computeHash(value: string, hmacKey: Buffer): string {
|
||||||
|
const normalized = value.toLowerCase().trim();
|
||||||
|
return crypto.createHmac('sha256', hmacKey).update(normalized).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash password with bcrypt
|
||||||
|
*/
|
||||||
|
async function hashPassword(password: string): Promise<string> {
|
||||||
|
if (password.length < 8) {
|
||||||
|
throw new Error('Password must be at least 8 characters');
|
||||||
|
}
|
||||||
|
return bcrypt.hash(password, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Main Seeding Function
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async function seedUsersWithAuth() {
|
||||||
|
const encryptionKey = process.env['FIELD_ENCRYPTION_KEY'];
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new Error('FIELD_ENCRYPTION_KEY environment variable is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hmacKey = deriveHmacKey(encryptionKey);
|
||||||
|
const stats = {
|
||||||
|
created: 0,
|
||||||
|
skipped: 0,
|
||||||
|
errors: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🌱 Seeding users with authentication...\n');
|
||||||
|
|
||||||
|
for (const userConfig of SEED_USERS) {
|
||||||
|
try {
|
||||||
|
// Check if user already exists
|
||||||
|
const existing = await prisma.user.findUnique({
|
||||||
|
where: { id: userConfig.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
console.log(`⏭️ Skipping ${userConfig.fullName} (already exists)`);
|
||||||
|
stats.skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Normalize phone
|
||||||
|
const normalizedPhone = normalizeVietnamPhone(userConfig.phone);
|
||||||
|
|
||||||
|
// 2. Compute hashes
|
||||||
|
const phoneHash = computeHash(normalizedPhone, hmacKey);
|
||||||
|
const emailHash = computeHash(userConfig.email, hmacKey);
|
||||||
|
|
||||||
|
// 3. Hash password
|
||||||
|
const passwordHash = await hashPassword(userConfig.password);
|
||||||
|
|
||||||
|
// 4. Create user
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
id: userConfig.id,
|
||||||
|
phone: normalizedPhone,
|
||||||
|
phoneHash,
|
||||||
|
email: userConfig.email,
|
||||||
|
emailHash,
|
||||||
|
passwordHash,
|
||||||
|
fullName: userConfig.fullName,
|
||||||
|
role: userConfig.role,
|
||||||
|
kycStatus: userConfig.kycStatus,
|
||||||
|
isActive: userConfig.isActive,
|
||||||
|
totpEnabled: false,
|
||||||
|
totpBackupCodes: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Created ${user.fullName} (${user.role})`);
|
||||||
|
console.log(` 📞 Phone: ${normalizedPhone}`);
|
||||||
|
console.log(` 📧 Email: ${user.email}`);
|
||||||
|
console.log(` 🔑 Can login with password: ${userConfig.password}\n`);
|
||||||
|
|
||||||
|
stats.created++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`❌ Error creating ${userConfig.fullName}:`,
|
||||||
|
error instanceof Error ? error.message : error,
|
||||||
|
);
|
||||||
|
stats.errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('📊 Seed Summary');
|
||||||
|
console.log(` Created: ${stats.created}`);
|
||||||
|
console.log(` Skipped: ${stats.skipped}`);
|
||||||
|
console.log(` Errors: ${stats.errors}`);
|
||||||
|
|
||||||
|
if (stats.errors === 0 && stats.created > 0) {
|
||||||
|
console.log('\n✅ Seed completed successfully!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test Login Function (optional)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a created user can actually log in
|
||||||
|
*/
|
||||||
|
async function testLogin(userId: string, password: string): Promise<boolean> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || !user.passwordHash) {
|
||||||
|
console.error('User not found or has no password');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await bcrypt.compare(password, user.passwordHash);
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CLI Entry Point
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
await seedUsersWithAuth();
|
||||||
|
|
||||||
|
// Optionally test login
|
||||||
|
const adminUser = SEED_USERS.find((u) => u.role === UserRole.ADMIN);
|
||||||
|
if (adminUser) {
|
||||||
|
console.log('\n🔐 Testing login...');
|
||||||
|
const loginWorks = await testLogin(adminUser.id, adminUser.password);
|
||||||
|
if (loginWorks) {
|
||||||
|
console.log(`✅ Login test passed for ${adminUser.fullName}`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Login test failed for ${adminUser.fullName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { seedUsersWithAuth, testLogin };
|
||||||
@@ -55,17 +55,17 @@ import { AppController } from './app.controller';
|
|||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
ttl: 60_000,
|
ttl: 60_000,
|
||||||
limit: process.env['NODE_ENV'] === 'test' ? 10_000 : 60,
|
limit: process.env['NODE_ENV'] === 'test' || process.env['NODE_ENV'] === 'development' ? 10_000 : 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'auth',
|
name: 'auth',
|
||||||
ttl: 60_000,
|
ttl: 60_000,
|
||||||
limit: process.env['NODE_ENV'] === 'test' ? 10_000 : 10,
|
limit: process.env['NODE_ENV'] === 'test' || process.env['NODE_ENV'] === 'development' ? 10_000 : 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'payment-callback',
|
name: 'payment-callback',
|
||||||
ttl: 60_000,
|
ttl: 60_000,
|
||||||
limit: process.env['NODE_ENV'] === 'test' ? 10_000 : 20,
|
limit: process.env['NODE_ENV'] === 'test' || process.env['NODE_ENV'] === 'development' ? 10_000 : 20,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ const isTest = process.env['NODE_ENV'] === 'test';
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const integrations: any[] = [];
|
const integrations: any[] = [];
|
||||||
if (!isTest) {
|
if (!isTest) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/consistent-type-imports
|
try {
|
||||||
const { nodeProfilingIntegration } = require('@sentry/profiling-node') as typeof import('@sentry/profiling-node');
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/consistent-type-imports
|
||||||
integrations.push(nodeProfilingIntegration());
|
const { nodeProfilingIntegration } = require('@sentry/profiling-node') as typeof import('@sentry/profiling-node');
|
||||||
|
integrations.push(nodeProfilingIntegration());
|
||||||
|
} catch {
|
||||||
|
// Native CPU profiler binary not available — skip profiling gracefully.
|
||||||
|
console.warn('[Sentry] Profiling skipped — native module not available');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import './instrument';
|
import './instrument';
|
||||||
|
|
||||||
|
// BigInt cannot be serialized by JSON.stringify by default.
|
||||||
|
// Polyfill toJSON so Express/NestJS can serialize Prisma BigInt fields.
|
||||||
|
(BigInt.prototype as any).toJSON = function () {
|
||||||
|
return this.toString();
|
||||||
|
};
|
||||||
|
|
||||||
import { RequestMethod, ValidationPipe } from '@nestjs/common';
|
import { RequestMethod, ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { type PlanTier } from '@prisma/client';
|
import { PlanTier } from '@prisma/client';
|
||||||
import { DomainException, NotFoundException, ValidationException, type PrismaService, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, ValidationException, PrismaService, LoggerService } from '@modules/shared';
|
||||||
import { SUBSCRIPTION_REPOSITORY, type ISubscriptionRepository } from '@modules/subscriptions';
|
import { SUBSCRIPTION_REPOSITORY, ISubscriptionRepository } from '@modules/subscriptions';
|
||||||
import { SubscriptionAdjustedEvent } from '../../../domain/events/subscription-adjusted.event';
|
import { SubscriptionAdjustedEvent } from '../../../domain/events/subscription-adjusted.event';
|
||||||
import { AdjustSubscriptionCommand } from './adjust-subscription.command';
|
import { AdjustSubscriptionCommand } from './adjust-subscription.command';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '@modules/auth';
|
import { USER_REPOSITORY, IUserRepository } from '@modules/auth';
|
||||||
import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, ValidationException, LoggerService } from '@modules/shared';
|
||||||
import { KycApprovedEvent } from '../../../domain/events/kyc-approved.event';
|
import { KycApprovedEvent } from '../../../domain/events/kyc-approved.event';
|
||||||
import { ApproveKycCommand } from './approve-kyc.command';
|
import { ApproveKycCommand } from './approve-kyc.command';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings';
|
import { LISTING_REPOSITORY, IListingRepository } from '@modules/listings';
|
||||||
import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, ValidationException, LoggerService } from '@modules/shared';
|
||||||
import { ListingApprovedEvent } from '../../../domain/events/listing-approved.event';
|
import { ListingApprovedEvent } from '../../../domain/events/listing-approved.event';
|
||||||
import { ApproveListingCommand } from './approve-listing.command';
|
import { ApproveListingCommand } from './approve-listing.command';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '@modules/auth';
|
import { USER_REPOSITORY, IUserRepository } from '@modules/auth';
|
||||||
import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, ValidationException, LoggerService } from '@modules/shared';
|
||||||
import { UserBannedEvent } from '../../../domain/events/user-banned.event';
|
import { UserBannedEvent } from '../../../domain/events/user-banned.event';
|
||||||
import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event';
|
import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event';
|
||||||
import { BanUserCommand } from './ban-user.command';
|
import { BanUserCommand } from './ban-user.command';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings';
|
import { LISTING_REPOSITORY, IListingRepository } from '@modules/listings';
|
||||||
import { DomainException, ValidationException, type LoggerService } from '@modules/shared';
|
import { DomainException, ValidationException, LoggerService } from '@modules/shared';
|
||||||
import { ListingApprovedEvent } from '../../../domain/events/listing-approved.event';
|
import { ListingApprovedEvent } from '../../../domain/events/listing-approved.event';
|
||||||
import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event';
|
import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event';
|
||||||
import { BulkModerateListingsCommand } from './bulk-moderate-listings.command';
|
import { BulkModerateListingsCommand } from './bulk-moderate-listings.command';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '@modules/auth';
|
import { USER_REPOSITORY, IUserRepository } from '@modules/auth';
|
||||||
import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, ValidationException, LoggerService } from '@modules/shared';
|
||||||
import { KycRejectedEvent } from '../../../domain/events/kyc-rejected.event';
|
import { KycRejectedEvent } from '../../../domain/events/kyc-rejected.event';
|
||||||
import { RejectKycCommand } from './reject-kyc.command';
|
import { RejectKycCommand } from './reject-kyc.command';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings';
|
import { LISTING_REPOSITORY, IListingRepository } from '@modules/listings';
|
||||||
import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, ValidationException, LoggerService } from '@modules/shared';
|
||||||
import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event';
|
import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event';
|
||||||
import { RejectListingCommand } from './reject-listing.command';
|
import { RejectListingCommand } from './reject-listing.command';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '@modules/auth';
|
import { USER_REPOSITORY, IUserRepository } from '@modules/auth';
|
||||||
import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, ValidationException, LoggerService } from '@modules/shared';
|
||||||
import { UserBannedEvent } from '../../../domain/events/user-banned.event';
|
import { UserBannedEvent } from '../../../domain/events/user-banned.event';
|
||||||
import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event';
|
import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event';
|
||||||
import { UpdateUserStatusCommand } from './update-user-status.command';
|
import { UpdateUserStatusCommand } from './update-user-status.command';
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { type LoggerService } from '@modules/shared';
|
import { LoggerService } from '@modules/shared';
|
||||||
import { type KycApprovedEvent } from '../../domain/events/kyc-approved.event';
|
import { KycApprovedEvent } from '../../domain/events/kyc-approved.event';
|
||||||
import { type KycRejectedEvent } from '../../domain/events/kyc-rejected.event';
|
import { KycRejectedEvent } from '../../domain/events/kyc-rejected.event';
|
||||||
import { type ListingApprovedEvent } from '../../domain/events/listing-approved.event';
|
import { ListingApprovedEvent } from '../../domain/events/listing-approved.event';
|
||||||
import { type ListingRejectedEvent } from '../../domain/events/listing-rejected.event';
|
import { ListingRejectedEvent } from '../../domain/events/listing-rejected.event';
|
||||||
import { type SubscriptionAdjustedEvent } from '../../domain/events/subscription-adjusted.event';
|
import { SubscriptionAdjustedEvent } from '../../domain/events/subscription-adjusted.event';
|
||||||
import { type UserBannedEvent } from '../../domain/events/user-banned.event';
|
import { UserBannedEvent } from '../../domain/events/user-banned.event';
|
||||||
import { type UserUnbannedEvent } from '../../domain/events/user-unbanned.event';
|
import { UserUnbannedEvent } from '../../domain/events/user-unbanned.event';
|
||||||
import {
|
import {
|
||||||
AUDIT_LOG_REPOSITORY,
|
AUDIT_LOG_REPOSITORY,
|
||||||
type IAuditLogRepository,
|
IAuditLogRepository,
|
||||||
} from '../../domain/repositories/audit-log.repository';
|
} from '../../domain/repositories/audit-log.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type CommandBus } from '@nestjs/cqrs';
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { SendNotificationCommand } from '@modules/notifications';
|
import { SendNotificationCommand } from '@modules/notifications';
|
||||||
import { type LoggerService, type PrismaService } from '@modules/shared';
|
import { LoggerService, PrismaService } from '@modules/shared';
|
||||||
import { type UserBannedEvent } from '../../domain/events/user-banned.event';
|
import { UserBannedEvent } from '../../domain/events/user-banned.event';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserBannedListener {
|
export class UserBannedListener {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { type UserDeactivatedEvent } from '@modules/auth';
|
import { UserDeactivatedEvent } from '@modules/auth';
|
||||||
import { type LoggerService, type PrismaService } from '@modules/shared';
|
import { LoggerService, PrismaService } from '@modules/shared';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserDeactivatedListener {
|
export class UserDeactivatedListener {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
AUDIT_LOG_REPOSITORY,
|
AUDIT_LOG_REPOSITORY,
|
||||||
type IAuditLogRepository,
|
IAuditLogRepository,
|
||||||
type AuditLogListResult,
|
type AuditLogListResult,
|
||||||
} from '../../../domain/repositories/audit-log.repository';
|
} from '../../../domain/repositories/audit-log.repository';
|
||||||
import { GetAuditLogsQuery } from './get-audit-logs.query';
|
import { GetAuditLogsQuery } from './get-audit-logs.query';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type DashboardStats } from '../../../domain/repositories/admin-query.repository';
|
import { ADMIN_QUERY_REPOSITORY, IAdminQueryRepository, DashboardStats } from '../../../domain/repositories/admin-query.repository';
|
||||||
import { GetDashboardStatsQuery } from './get-dashboard-stats.query';
|
import { GetDashboardStatsQuery } from './get-dashboard-stats.query';
|
||||||
|
|
||||||
@QueryHandler(GetDashboardStatsQuery)
|
@QueryHandler(GetDashboardStatsQuery)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type KycQueueResult } from '../../../domain/repositories/admin-query.repository';
|
import { ADMIN_QUERY_REPOSITORY, IAdminQueryRepository, KycQueueResult } from '../../../domain/repositories/admin-query.repository';
|
||||||
import { GetKycQueueQuery } from './get-kyc-queue.query';
|
import { GetKycQueueQuery } from './get-kyc-queue.query';
|
||||||
|
|
||||||
@QueryHandler(GetKycQueueQuery)
|
@QueryHandler(GetKycQueueQuery)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type ModerationQueueResult } from '../../../domain/repositories/admin-query.repository';
|
import { ADMIN_QUERY_REPOSITORY, IAdminQueryRepository, ModerationQueueResult } from '../../../domain/repositories/admin-query.repository';
|
||||||
import { GetModerationQueueQuery } from './get-moderation-queue.query';
|
import { GetModerationQueueQuery } from './get-moderation-queue.query';
|
||||||
|
|
||||||
@QueryHandler(GetModerationQueueQuery)
|
@QueryHandler(GetModerationQueueQuery)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type RevenueStatsItem } from '../../../domain/repositories/admin-query.repository';
|
import { ADMIN_QUERY_REPOSITORY, IAdminQueryRepository, RevenueStatsItem } from '../../../domain/repositories/admin-query.repository';
|
||||||
import { GetRevenueStatsQuery } from './get-revenue-stats.query';
|
import { GetRevenueStatsQuery } from './get-revenue-stats.query';
|
||||||
|
|
||||||
@QueryHandler(GetRevenueStatsQuery)
|
@QueryHandler(GetRevenueStatsQuery)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, NotFoundException, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, LoggerService } from '@modules/shared';
|
||||||
import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type UserDetail } from '../../../domain/repositories/admin-query.repository';
|
import { ADMIN_QUERY_REPOSITORY, IAdminQueryRepository, UserDetail } from '../../../domain/repositories/admin-query.repository';
|
||||||
import { GetUserDetailQuery } from './get-user-detail.query';
|
import { GetUserDetailQuery } from './get-user-detail.query';
|
||||||
|
|
||||||
@QueryHandler(GetUserDetailQuery)
|
@QueryHandler(GetUserDetailQuery)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type UserListResult } from '../../../domain/repositories/admin-query.repository';
|
import { ADMIN_QUERY_REPOSITORY, IAdminQueryRepository, UserListResult } from '../../../domain/repositories/admin-query.repository';
|
||||||
import { GetUsersQuery } from './get-users.query';
|
import { GetUsersQuery } from './get-users.query';
|
||||||
|
|
||||||
@QueryHandler(GetUsersQuery)
|
@QueryHandler(GetUsersQuery)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class KycApprovedEvent implements DomainEvent {
|
export class KycApprovedEvent implements DomainEvent {
|
||||||
readonly eventName = 'kyc.approved';
|
readonly eventName = 'kyc.approved';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class KycRejectedEvent implements DomainEvent {
|
export class KycRejectedEvent implements DomainEvent {
|
||||||
readonly eventName = 'kyc.rejected';
|
readonly eventName = 'kyc.rejected';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class ListingApprovedEvent implements DomainEvent {
|
export class ListingApprovedEvent implements DomainEvent {
|
||||||
readonly eventName = 'listing.approved_by_admin';
|
readonly eventName = 'listing.approved_by_admin';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class ListingRejectedEvent implements DomainEvent {
|
export class ListingRejectedEvent implements DomainEvent {
|
||||||
readonly eventName = 'listing.rejected_by_admin';
|
readonly eventName = 'listing.rejected_by_admin';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class SubscriptionAdjustedEvent implements DomainEvent {
|
export class SubscriptionAdjustedEvent implements DomainEvent {
|
||||||
readonly eventName = 'subscription.adjusted_by_admin';
|
readonly eventName = 'subscription.adjusted_by_admin';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class UserBannedEvent implements DomainEvent {
|
export class UserBannedEvent implements DomainEvent {
|
||||||
readonly eventName = 'user.banned';
|
readonly eventName = 'user.banned';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class UserUnbannedEvent implements DomainEvent {
|
export class UserUnbannedEvent implements DomainEvent {
|
||||||
readonly eventName = 'user.unbanned';
|
readonly eventName = 'user.unbanned';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository } from './admin-query.repository';
|
export { ADMIN_QUERY_REPOSITORY, IAdminQueryRepository } from './admin-query.repository';
|
||||||
export type {
|
export type {
|
||||||
ModerationQueueItem,
|
ModerationQueueItem,
|
||||||
ModerationQueueResult,
|
ModerationQueueResult,
|
||||||
@@ -9,7 +9,7 @@ export type {
|
|||||||
} from './admin-query.repository';
|
} from './admin-query.repository';
|
||||||
export {
|
export {
|
||||||
AUDIT_LOG_REPOSITORY,
|
AUDIT_LOG_REPOSITORY,
|
||||||
type IAuditLogRepository,
|
IAuditLogRepository,
|
||||||
type AuditLogEntry,
|
type AuditLogEntry,
|
||||||
type AuditLogListResult,
|
type AuditLogListResult,
|
||||||
type CreateAuditLogInput,
|
type CreateAuditLogInput,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export { ListingApprovedEvent } from './domain/events/listing-approved.event';
|
|||||||
export { ListingRejectedEvent } from './domain/events/listing-rejected.event';
|
export { ListingRejectedEvent } from './domain/events/listing-rejected.event';
|
||||||
export {
|
export {
|
||||||
AUDIT_LOG_REPOSITORY,
|
AUDIT_LOG_REPOSITORY,
|
||||||
type IAuditLogRepository,
|
IAuditLogRepository,
|
||||||
type AuditLogEntry,
|
type AuditLogEntry,
|
||||||
type AuditLogListResult,
|
type AuditLogListResult,
|
||||||
} from './domain/repositories/audit-log.repository';
|
} from './domain/repositories/audit-log.repository';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
type DashboardStats,
|
type DashboardStats,
|
||||||
type RevenueStatsItem,
|
type RevenueStatsItem,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type Prisma, type UserRole } from '@prisma/client';
|
import { Prisma, UserRole } from '@prisma/client';
|
||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
type UserListResult,
|
type UserListResult,
|
||||||
type UserDetail,
|
type UserDetail,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
type IAdminQueryRepository,
|
IAdminQueryRepository,
|
||||||
type ModerationQueueResult,
|
type ModerationQueueResult,
|
||||||
type DashboardStats,
|
type DashboardStats,
|
||||||
type RevenueStatsItem,
|
type RevenueStatsItem,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type AdminAction, type AuditTargetType, type Prisma } from '@prisma/client';
|
import { AdminAction, AuditTargetType, Prisma } from '@prisma/client';
|
||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
type IAuditLogRepository,
|
IAuditLogRepository,
|
||||||
type AuditLogEntry,
|
type AuditLogEntry,
|
||||||
type AuditLogListResult,
|
type AuditLogListResult,
|
||||||
type CreateAuditLogInput,
|
type CreateAuditLogInput,
|
||||||
|
|||||||
@@ -6,30 +6,30 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
|
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||||
import { type JwtPayload, CurrentUser, Roles, JwtAuthGuard, RolesGuard } from '@modules/auth';
|
import { JwtPayload, CurrentUser, Roles, JwtAuthGuard, RolesGuard } from '@modules/auth';
|
||||||
import { ApproveKycCommand } from '../../application/commands/approve-kyc/approve-kyc.command';
|
import { ApproveKycCommand } from '../../application/commands/approve-kyc/approve-kyc.command';
|
||||||
import { type ApproveKycResult } from '../../application/commands/approve-kyc/approve-kyc.handler';
|
import { ApproveKycResult } from '../../application/commands/approve-kyc/approve-kyc.handler';
|
||||||
import { ApproveListingCommand } from '../../application/commands/approve-listing/approve-listing.command';
|
import { ApproveListingCommand } from '../../application/commands/approve-listing/approve-listing.command';
|
||||||
import { type ApproveListingResult } from '../../application/commands/approve-listing/approve-listing.handler';
|
import { ApproveListingResult } from '../../application/commands/approve-listing/approve-listing.handler';
|
||||||
import { BulkModerateListingsCommand } from '../../application/commands/bulk-moderate-listings/bulk-moderate-listings.command';
|
import { BulkModerateListingsCommand } from '../../application/commands/bulk-moderate-listings/bulk-moderate-listings.command';
|
||||||
import { type BulkModerateResult } from '../../application/commands/bulk-moderate-listings/bulk-moderate-listings.handler';
|
import { BulkModerateResult } from '../../application/commands/bulk-moderate-listings/bulk-moderate-listings.handler';
|
||||||
import { RejectKycCommand } from '../../application/commands/reject-kyc/reject-kyc.command';
|
import { RejectKycCommand } from '../../application/commands/reject-kyc/reject-kyc.command';
|
||||||
import { type RejectKycResult } from '../../application/commands/reject-kyc/reject-kyc.handler';
|
import { RejectKycResult } from '../../application/commands/reject-kyc/reject-kyc.handler';
|
||||||
import { RejectListingCommand } from '../../application/commands/reject-listing/reject-listing.command';
|
import { RejectListingCommand } from '../../application/commands/reject-listing/reject-listing.command';
|
||||||
import { type RejectListingResult } from '../../application/commands/reject-listing/reject-listing.handler';
|
import { RejectListingResult } from '../../application/commands/reject-listing/reject-listing.handler';
|
||||||
import { GetKycQueueQuery } from '../../application/queries/get-kyc-queue/get-kyc-queue.query';
|
import { GetKycQueueQuery } from '../../application/queries/get-kyc-queue/get-kyc-queue.query';
|
||||||
import { GetModerationQueueQuery } from '../../application/queries/get-moderation-queue/get-moderation-queue.query';
|
import { GetModerationQueueQuery } from '../../application/queries/get-moderation-queue/get-moderation-queue.query';
|
||||||
import {
|
import {
|
||||||
type ModerationQueueResult,
|
type ModerationQueueResult,
|
||||||
type KycQueueResult,
|
type KycQueueResult,
|
||||||
} from '../../domain/repositories/admin-query.repository';
|
} from '../../domain/repositories/admin-query.repository';
|
||||||
import { type ApproveKycDto } from '../dto/approve-kyc.dto';
|
import { ApproveKycDto } from '../dto/approve-kyc.dto';
|
||||||
import { type ApproveListingDto } from '../dto/approve-listing.dto';
|
import { ApproveListingDto } from '../dto/approve-listing.dto';
|
||||||
import { type BulkModerateDto } from '../dto/bulk-moderate.dto';
|
import { BulkModerateDto } from '../dto/bulk-moderate.dto';
|
||||||
import { type RejectKycDto } from '../dto/reject-kyc.dto';
|
import { RejectKycDto } from '../dto/reject-kyc.dto';
|
||||||
import { type RejectListingDto } from '../dto/reject-listing.dto';
|
import { RejectListingDto } from '../dto/reject-listing.dto';
|
||||||
|
|
||||||
@ApiTags('admin')
|
@ApiTags('admin')
|
||||||
@ApiBearerAuth('JWT')
|
@ApiBearerAuth('JWT')
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
|
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||||
import { type JwtPayload, CurrentUser, Roles, JwtAuthGuard, RolesGuard } from '@modules/auth';
|
import { JwtPayload, CurrentUser, Roles, JwtAuthGuard, RolesGuard } from '@modules/auth';
|
||||||
import { AdjustSubscriptionCommand } from '../../application/commands/adjust-subscription/adjust-subscription.command';
|
import { AdjustSubscriptionCommand } from '../../application/commands/adjust-subscription/adjust-subscription.command';
|
||||||
import { type AdjustSubscriptionResult } from '../../application/commands/adjust-subscription/adjust-subscription.handler';
|
import { AdjustSubscriptionResult } from '../../application/commands/adjust-subscription/adjust-subscription.handler';
|
||||||
import { BanUserCommand } from '../../application/commands/ban-user/ban-user.command';
|
import { BanUserCommand } from '../../application/commands/ban-user/ban-user.command';
|
||||||
import { type BanUserResult } from '../../application/commands/ban-user/ban-user.handler';
|
import { BanUserResult } from '../../application/commands/ban-user/ban-user.handler';
|
||||||
import { UpdateUserStatusCommand } from '../../application/commands/update-user-status/update-user-status.command';
|
import { UpdateUserStatusCommand } from '../../application/commands/update-user-status/update-user-status.command';
|
||||||
import { type UpdateUserStatusResult } from '../../application/commands/update-user-status/update-user-status.handler';
|
import { UpdateUserStatusResult } from '../../application/commands/update-user-status/update-user-status.handler';
|
||||||
import { GetAuditLogsQuery } from '../../application/queries/get-audit-logs/get-audit-logs.query';
|
import { GetAuditLogsQuery } from '../../application/queries/get-audit-logs/get-audit-logs.query';
|
||||||
import { GetDashboardStatsQuery } from '../../application/queries/get-dashboard-stats/get-dashboard-stats.query';
|
import { GetDashboardStatsQuery } from '../../application/queries/get-dashboard-stats/get-dashboard-stats.query';
|
||||||
import { GetRevenueStatsQuery } from '../../application/queries/get-revenue-stats/get-revenue-stats.query';
|
import { GetRevenueStatsQuery } from '../../application/queries/get-revenue-stats/get-revenue-stats.query';
|
||||||
@@ -28,13 +28,13 @@ import {
|
|||||||
type UserListResult,
|
type UserListResult,
|
||||||
type UserDetail,
|
type UserDetail,
|
||||||
} from '../../domain/repositories/admin-query.repository';
|
} from '../../domain/repositories/admin-query.repository';
|
||||||
import { type AuditLogListResult } from '../../domain/repositories/audit-log.repository';
|
import { AuditLogListResult } from '../../domain/repositories/audit-log.repository';
|
||||||
import { type AdjustSubscriptionDto } from '../dto/adjust-subscription.dto';
|
import { AdjustSubscriptionDto } from '../dto/adjust-subscription.dto';
|
||||||
import { type BanUserDto } from '../dto/ban-user.dto';
|
import { BanUserDto } from '../dto/ban-user.dto';
|
||||||
import { type GetAuditLogsQueryDto } from '../dto/get-audit-logs-query.dto';
|
import { GetAuditLogsQueryDto } from '../dto/get-audit-logs-query.dto';
|
||||||
import { type GetUsersQueryDto } from '../dto/get-users-query.dto';
|
import { GetUsersQueryDto } from '../dto/get-users-query.dto';
|
||||||
import { type RevenueStatsDto } from '../dto/revenue-stats.dto';
|
import { RevenueStatsDto } from '../dto/revenue-stats.dto';
|
||||||
import { type UpdateUserStatusDto } from '../dto/update-user-status.dto';
|
import { UpdateUserStatusDto } from '../dto/update-user-status.dto';
|
||||||
|
|
||||||
@ApiTags('admin')
|
@ApiTags('admin')
|
||||||
@ApiBearerAuth('JWT')
|
@ApiBearerAuth('JWT')
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
AGENT_REPOSITORY,
|
AGENT_REPOSITORY,
|
||||||
type IAgentRepository,
|
IAgentRepository,
|
||||||
} from '../../../domain/repositories/agent.repository';
|
} from '../../../domain/repositories/agent.repository';
|
||||||
import { QualityScoreCalculator } from '../../../domain/services/quality-score.service';
|
import { QualityScoreCalculator } from '../../../domain/services/quality-score.service';
|
||||||
import { QualityScore } from '../../../domain/value-objects/quality-score.vo';
|
import { QualityScore } from '../../../domain/value-objects/quality-score.vo';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type CommandBus } from '@nestjs/cqrs';
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { type LoggerService } from '@modules/shared';
|
import { LoggerService } from '@modules/shared';
|
||||||
import { RecalculateQualityScoreCommand } from '../commands/recalculate-quality-score/recalculate-quality-score.command';
|
import { RecalculateQualityScoreCommand } from '../commands/recalculate-quality-score/recalculate-quality-score.command';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, NotFoundException, type LoggerService } from '@modules/shared';
|
import { DomainException, NotFoundException, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
AGENT_REPOSITORY,
|
AGENT_REPOSITORY,
|
||||||
type AgentDashboardData,
|
type AgentDashboardData,
|
||||||
type IAgentRepository,
|
IAgentRepository,
|
||||||
} from '../../../domain/repositories/agent.repository';
|
} from '../../../domain/repositories/agent.repository';
|
||||||
import { GetAgentDashboardQuery } from './get-agent-dashboard.query';
|
import { GetAgentDashboardQuery } from './get-agent-dashboard.query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
AGENT_REPOSITORY,
|
AGENT_REPOSITORY,
|
||||||
type AgentPublicProfileData,
|
type AgentPublicProfileData,
|
||||||
type IAgentRepository,
|
IAgentRepository,
|
||||||
} from '../../../domain/repositories/agent.repository';
|
} from '../../../domain/repositories/agent.repository';
|
||||||
import { GetAgentPublicProfileQuery } from './get-agent-public-profile.query';
|
import { GetAgentPublicProfileQuery } from './get-agent-public-profile.query';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AggregateRoot } from '@modules/shared';
|
import { AggregateRoot } from '@modules/shared';
|
||||||
import { QualityScoreUpdatedEvent } from '../events/quality-score-updated.event';
|
import { QualityScoreUpdatedEvent } from '../events/quality-score-updated.event';
|
||||||
import { type QualityScore } from '../value-objects/quality-score.vo';
|
import { QualityScore } from '../value-objects/quality-score.vo';
|
||||||
|
|
||||||
export interface AgentProps {
|
export interface AgentProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class QualityScoreUpdatedEvent implements DomainEvent {
|
export class QualityScoreUpdatedEvent implements DomainEvent {
|
||||||
readonly eventName = 'agent.quality_score_updated';
|
readonly eventName = 'agent.quality_score_updated';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type AgentEntity } from '../entities/agent.entity';
|
import { AgentEntity } from '../entities/agent.entity';
|
||||||
|
|
||||||
export const AGENT_REPOSITORY = Symbol('AGENT_REPOSITORY');
|
export const AGENT_REPOSITORY = Symbol('AGENT_REPOSITORY');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
AGENT_REPOSITORY,
|
AGENT_REPOSITORY,
|
||||||
type IAgentRepository,
|
IAgentRepository,
|
||||||
type AgentDashboardData,
|
type AgentDashboardData,
|
||||||
type AgentPublicProfileData,
|
type AgentPublicProfileData,
|
||||||
type AgentPublicListingItem,
|
type AgentPublicListingItem,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export { QualityScoreUpdatedEvent } from './domain/events/quality-score-updated.
|
|||||||
export { QualityScoreCalculator } from './domain/services/quality-score.service';
|
export { QualityScoreCalculator } from './domain/services/quality-score.service';
|
||||||
export {
|
export {
|
||||||
AGENT_REPOSITORY,
|
AGENT_REPOSITORY,
|
||||||
type IAgentRepository,
|
IAgentRepository,
|
||||||
type AgentDashboardData,
|
type AgentDashboardData,
|
||||||
type AgentPublicProfileData,
|
type AgentPublicProfileData,
|
||||||
type AgentPublicListingItem,
|
type AgentPublicListingItem,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
type AgentPublicProfileData,
|
type AgentPublicProfileData,
|
||||||
type AgentPublicListingItem,
|
type AgentPublicListingItem,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import { AgentEntity } from '../../domain/entities/agent.entity';
|
import { AgentEntity } from '../../domain/entities/agent.entity';
|
||||||
import {
|
import {
|
||||||
type AgentDashboardData,
|
type AgentDashboardData,
|
||||||
type AgentPublicProfileData,
|
type AgentPublicProfileData,
|
||||||
type IAgentRepository,
|
IAgentRepository,
|
||||||
type QualityScoreInputData,
|
type QualityScoreInputData,
|
||||||
} from '../../domain/repositories/agent.repository';
|
} from '../../domain/repositories/agent.repository';
|
||||||
import { QualityScore } from '../../domain/value-objects/quality-score.vo';
|
import { QualityScore } from '../../domain/value-objects/quality-score.vo';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller, Get, NotFoundException, Param, Post, UseGuards } from '@nestjs/common';
|
import { Controller, Get, NotFoundException, Param, Post, UseGuards } from '@nestjs/common';
|
||||||
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
|
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||||
import {
|
import {
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { RecalculateQualityScoreCommand } from '../../application/commands/recalculate-quality-score/recalculate-quality-score.command';
|
import { RecalculateQualityScoreCommand } from '../../application/commands/recalculate-quality-score/recalculate-quality-score.command';
|
||||||
import { GetAgentDashboardQuery } from '../../application/queries/get-agent-dashboard/get-agent-dashboard.query';
|
import { GetAgentDashboardQuery } from '../../application/queries/get-agent-dashboard/get-agent-dashboard.query';
|
||||||
import { GetAgentPublicProfileQuery } from '../../application/queries/get-agent-public-profile/get-agent-public-profile.query';
|
import { GetAgentPublicProfileQuery } from '../../application/queries/get-agent-public-profile/get-agent-public-profile.query';
|
||||||
import { type AgentDashboardData, type AgentPublicProfileData } from '../../domain/repositories/agent.repository';
|
import { AgentDashboardData, AgentPublicProfileData } from '../../domain/repositories/agent.repository';
|
||||||
|
|
||||||
@ApiTags('agents')
|
@ApiTags('agents')
|
||||||
@Controller('agents')
|
@Controller('agents')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
|
|
||||||
export class GenerateReportCommand {
|
export class GenerateReportCommand {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
MARKET_INDEX_REPOSITORY,
|
MARKET_INDEX_REPOSITORY,
|
||||||
type IMarketIndexRepository,
|
IMarketIndexRepository,
|
||||||
type MarketReportResult,
|
type MarketReportResult,
|
||||||
} from '../../../domain/repositories/market-index.repository';
|
} from '../../../domain/repositories/market-index.repository';
|
||||||
import { GenerateReportCommand } from './generate-report.command';
|
import { GenerateReportCommand } from './generate-report.command';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService } from '@modules/shared';
|
import { DomainException, LoggerService } from '@modules/shared';
|
||||||
import { TrackEventCommand } from './track-event.command';
|
import { TrackEventCommand } from './track-event.command';
|
||||||
|
|
||||||
export interface TrackEventResult {
|
export interface TrackEventResult {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
|
|
||||||
export class UpdateMarketIndexCommand {
|
export class UpdateMarketIndexCommand {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type CacheService, CachePrefix, type LoggerService } from '@modules/shared';
|
import { DomainException, CacheService, CachePrefix, LoggerService } from '@modules/shared';
|
||||||
import { MarketIndexEntity } from '../../../domain/entities/market-index.entity';
|
import { MarketIndexEntity } from '../../../domain/entities/market-index.entity';
|
||||||
import {
|
import {
|
||||||
MARKET_INDEX_REPOSITORY,
|
MARKET_INDEX_REPOSITORY,
|
||||||
type IMarketIndexRepository,
|
IMarketIndexRepository,
|
||||||
} from '../../../domain/repositories/market-index.repository';
|
} from '../../../domain/repositories/market-index.repository';
|
||||||
import { UpdateMarketIndexCommand } from './update-market-index.command';
|
import { UpdateMarketIndexCommand } from './update-market-index.command';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Inject } from '@nestjs/common';
|
import { Inject } from '@nestjs/common';
|
||||||
import { EventsHandler, type IEventHandler, type CommandBus } from '@nestjs/cqrs';
|
import { EventsHandler, IEventHandler, CommandBus } from '@nestjs/cqrs';
|
||||||
import { ListingCreatedEvent, ModerateListingCommand } from '@modules/listings';
|
import { ListingCreatedEvent, ModerateListingCommand } from '@modules/listings';
|
||||||
import { type PrismaService, type LoggerService } from '@modules/shared';
|
import { PrismaService, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
AI_SERVICE_CLIENT,
|
AI_SERVICE_CLIENT,
|
||||||
type IAiServiceClient,
|
IAiServiceClient,
|
||||||
} from '../../infrastructure/services/ai-service.client';
|
} from '../../infrastructure/services/ai-service.client';
|
||||||
|
|
||||||
const AUTO_REJECT_THRESHOLD = 0.8;
|
const AUTO_REJECT_THRESHOLD = 0.8;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type CacheService, CachePrefix, CacheTTL, Cacheable, type LoggerService } from '@modules/shared';
|
import { DomainException, CacheService, CachePrefix, CacheTTL, Cacheable, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
MARKET_INDEX_REPOSITORY,
|
MARKET_INDEX_REPOSITORY,
|
||||||
type IMarketIndexRepository,
|
IMarketIndexRepository,
|
||||||
type DistrictStatsResult,
|
type DistrictStatsResult,
|
||||||
} from '../../../domain/repositories/market-index.repository';
|
} from '../../../domain/repositories/market-index.repository';
|
||||||
import { GetDistrictStatsQuery } from './get-district-stats.query';
|
import { GetDistrictStatsQuery } from './get-district-stats.query';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, CacheService, CachePrefix, CacheTTL, type LoggerService } from '@modules/shared';
|
import { DomainException, CacheService, CachePrefix, CacheTTL, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
MARKET_INDEX_REPOSITORY,
|
MARKET_INDEX_REPOSITORY,
|
||||||
type IMarketIndexRepository,
|
IMarketIndexRepository,
|
||||||
type HeatmapDataPoint,
|
type HeatmapDataPoint,
|
||||||
} from '../../../domain/repositories/market-index.repository';
|
} from '../../../domain/repositories/market-index.repository';
|
||||||
import { GetHeatmapQuery } from './get-heatmap.query';
|
import { GetHeatmapQuery } from './get-heatmap.query';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, CacheService, CachePrefix, CacheTTL, type LoggerService } from '@modules/shared';
|
import { DomainException, CacheService, CachePrefix, CacheTTL, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
MARKET_INDEX_REPOSITORY,
|
MARKET_INDEX_REPOSITORY,
|
||||||
type IMarketIndexRepository,
|
IMarketIndexRepository,
|
||||||
type MarketReportResult,
|
type MarketReportResult,
|
||||||
} from '../../../domain/repositories/market-index.repository';
|
} from '../../../domain/repositories/market-index.repository';
|
||||||
import { GetMarketReportQuery } from './get-market-report.query';
|
import { GetMarketReportQuery } from './get-market-report.query';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
|
|
||||||
export class GetMarketReportQuery {
|
export class GetMarketReportQuery {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, CacheService, CachePrefix, CacheTTL, type LoggerService } from '@modules/shared';
|
import { DomainException, CacheService, CachePrefix, CacheTTL, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
MARKET_INDEX_REPOSITORY,
|
MARKET_INDEX_REPOSITORY,
|
||||||
type IMarketIndexRepository,
|
IMarketIndexRepository,
|
||||||
type PriceTrendPoint,
|
type PriceTrendPoint,
|
||||||
} from '../../../domain/repositories/market-index.repository';
|
} from '../../../domain/repositories/market-index.repository';
|
||||||
import { GetPriceTrendQuery } from './get-price-trend.query';
|
import { GetPriceTrendQuery } from './get-price-trend.query';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
|
|
||||||
export class GetPriceTrendQuery {
|
export class GetPriceTrendQuery {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, CacheService, CachePrefix, CacheTTL, type LoggerService } from '@modules/shared';
|
import { DomainException, CacheService, CachePrefix, CacheTTL, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
AVM_SERVICE,
|
AVM_SERVICE,
|
||||||
type IAVMService,
|
IAVMService,
|
||||||
type ValuationResult,
|
type ValuationResult,
|
||||||
} from '../../../domain/services/avm-service';
|
} from '../../../domain/services/avm-service';
|
||||||
import { GetValuationQuery } from './get-valuation.query';
|
import { GetValuationQuery } from './get-valuation.query';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
|
|
||||||
export class GetValuationQuery {
|
export class GetValuationQuery {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
import { AggregateRoot } from '@modules/shared';
|
import { AggregateRoot } from '@modules/shared';
|
||||||
import { MarketIndexUpdatedEvent } from '../events/market-index-updated.event';
|
import { MarketIndexUpdatedEvent } from '../events/market-index-updated.event';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DomainEvent } from '@modules/shared';
|
import { DomainEvent } from '@modules/shared';
|
||||||
|
|
||||||
export class MarketIndexUpdatedEvent implements DomainEvent {
|
export class MarketIndexUpdatedEvent implements DomainEvent {
|
||||||
readonly eventName = 'market-index.updated';
|
readonly eventName = 'market-index.updated';
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { MARKET_INDEX_REPOSITORY, type IMarketIndexRepository, type MarketReportResult, type HeatmapDataPoint, type PriceTrendPoint, type DistrictStatsResult } from './market-index.repository';
|
export { MARKET_INDEX_REPOSITORY, IMarketIndexRepository, type MarketReportResult, type HeatmapDataPoint, type PriceTrendPoint, type DistrictStatsResult } from './market-index.repository';
|
||||||
export { VALUATION_REPOSITORY, type IValuationRepository } from './valuation.repository';
|
export { VALUATION_REPOSITORY, IValuationRepository } from './valuation.repository';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
import { type MarketIndexEntity } from '../entities/market-index.entity';
|
import { MarketIndexEntity } from '../entities/market-index.entity';
|
||||||
|
|
||||||
export const MARKET_INDEX_REPOSITORY = Symbol('MARKET_INDEX_REPOSITORY');
|
export const MARKET_INDEX_REPOSITORY = Symbol('MARKET_INDEX_REPOSITORY');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type ValuationEntity } from '../entities/valuation.entity';
|
import { ValuationEntity } from '../entities/valuation.entity';
|
||||||
|
|
||||||
export const VALUATION_REPOSITORY = Symbol('VALUATION_REPOSITORY');
|
export const VALUATION_REPOSITORY = Symbol('VALUATION_REPOSITORY');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
|
|
||||||
export const AVM_SERVICE = Symbol('AVM_SERVICE');
|
export const AVM_SERVICE = Symbol('AVM_SERVICE');
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { AVM_SERVICE, type IAVMService, type AVMParams, type ValuationResult, type Comparable } from './avm-service';
|
export { AVM_SERVICE, IAVMService, type AVMParams, type ValuationResult, type Comparable } from './avm-service';
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { AnalyticsModule } from './analytics.module';
|
export { AnalyticsModule } from './analytics.module';
|
||||||
export { MARKET_INDEX_REPOSITORY, type IMarketIndexRepository } from './domain/repositories/market-index.repository';
|
export { MARKET_INDEX_REPOSITORY, IMarketIndexRepository } from './domain/repositories/market-index.repository';
|
||||||
export { VALUATION_REPOSITORY, type IValuationRepository } from './domain/repositories/valuation.repository';
|
export { VALUATION_REPOSITORY, IValuationRepository } from './domain/repositories/valuation.repository';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type MarketIndex as PrismaMarketIndex, type PropertyType } from '@prisma/client';
|
import { MarketIndex as PrismaMarketIndex, PropertyType } from '@prisma/client';
|
||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import { MarketIndexEntity, type MarketIndexProps } from '../../domain/entities/market-index.entity';
|
import { MarketIndexEntity, MarketIndexProps } from '../../domain/entities/market-index.entity';
|
||||||
import {
|
import {
|
||||||
type IMarketIndexRepository,
|
IMarketIndexRepository,
|
||||||
type MarketReportResult,
|
type MarketReportResult,
|
||||||
type HeatmapDataPoint,
|
type HeatmapDataPoint,
|
||||||
type PriceTrendPoint,
|
type PriceTrendPoint,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type Prisma, type Valuation as PrismaValuation } from '@prisma/client';
|
import { Prisma, Valuation as PrismaValuation } from '@prisma/client';
|
||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import { ValuationEntity, type ValuationProps } from '../../domain/entities/valuation.entity';
|
import { ValuationEntity, ValuationProps } from '../../domain/entities/valuation.entity';
|
||||||
import { type IValuationRepository } from '../../domain/repositories/valuation.repository';
|
import { IValuationRepository } from '../../domain/repositories/valuation.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaValuationRepository implements IValuationRepository {
|
export class PrismaValuationRepository implements IValuationRepository {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type LoggerService } from '@modules/shared';
|
import { LoggerService } from '@modules/shared';
|
||||||
|
|
||||||
export interface AiPredictRequest {
|
export interface AiPredictRequest {
|
||||||
area: number;
|
area: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
import { type Comparable } from '../../domain/services/avm-service';
|
import { Comparable } from '../../domain/services/avm-service';
|
||||||
|
|
||||||
const DEFAULT_RADIUS_METERS = 2000;
|
const DEFAULT_RADIUS_METERS = 2000;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { type PrismaService, type LoggerService } from '@modules/shared';
|
import { PrismaService, LoggerService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
type IAVMService,
|
IAVMService,
|
||||||
type AVMParams,
|
type AVMParams,
|
||||||
type ValuationResult,
|
type ValuationResult,
|
||||||
type Comparable,
|
type Comparable,
|
||||||
} from '../../domain/services/avm-service';
|
} from '../../domain/services/avm-service';
|
||||||
import {
|
import {
|
||||||
AI_SERVICE_CLIENT,
|
AI_SERVICE_CLIENT,
|
||||||
type IAiServiceClient,
|
IAiServiceClient,
|
||||||
type AiPredictRequest,
|
type AiPredictRequest,
|
||||||
} from './ai-service.client';
|
} from './ai-service.client';
|
||||||
import { type PrismaAVMService } from './prisma-avm.service';
|
import { PrismaAVMService } from './prisma-avm.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HttpAVMService implements IAVMService {
|
export class HttpAVMService implements IAVMService {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type CommandBus } from '@nestjs/cqrs';
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import { PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
import { type PrismaService, type LoggerService } from '@modules/shared';
|
import { PrismaService, LoggerService } from '@modules/shared';
|
||||||
import { UpdateMarketIndexCommand } from '../../application/commands/update-market-index/update-market-index.command';
|
import { UpdateMarketIndexCommand } from '../../application/commands/update-market-index/update-market-index.command';
|
||||||
|
|
||||||
interface MarketStats {
|
interface MarketStats {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { type PropertyType } from '@prisma/client';
|
import { PropertyType } from '@prisma/client';
|
||||||
import { type PrismaService } from '@modules/shared';
|
import { PrismaService } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
type IAVMService,
|
IAVMService,
|
||||||
type AVMParams,
|
type AVMParams,
|
||||||
type ValuationResult,
|
type ValuationResult,
|
||||||
type Comparable,
|
type Comparable,
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { type QueryBus } from '@nestjs/cqrs';
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { JwtAuthGuard } from '@modules/auth';
|
import { JwtAuthGuard } from '@modules/auth';
|
||||||
import { RequireQuota, QuotaGuard } from '@modules/subscriptions';
|
import { RequireQuota, QuotaGuard } from '@modules/subscriptions';
|
||||||
import { type DistrictStatsDto } from '../../application/queries/get-district-stats/get-district-stats.handler';
|
import { DistrictStatsDto } from '../../application/queries/get-district-stats/get-district-stats.handler';
|
||||||
import { GetDistrictStatsQuery } from '../../application/queries/get-district-stats/get-district-stats.query';
|
import { GetDistrictStatsQuery } from '../../application/queries/get-district-stats/get-district-stats.query';
|
||||||
import { type HeatmapDto } from '../../application/queries/get-heatmap/get-heatmap.handler';
|
import { HeatmapDto } from '../../application/queries/get-heatmap/get-heatmap.handler';
|
||||||
import { GetHeatmapQuery } from '../../application/queries/get-heatmap/get-heatmap.query';
|
import { GetHeatmapQuery } from '../../application/queries/get-heatmap/get-heatmap.query';
|
||||||
import { type MarketReportDto } from '../../application/queries/get-market-report/get-market-report.handler';
|
import { MarketReportDto } from '../../application/queries/get-market-report/get-market-report.handler';
|
||||||
import { GetMarketReportQuery } from '../../application/queries/get-market-report/get-market-report.query';
|
import { GetMarketReportQuery } from '../../application/queries/get-market-report/get-market-report.query';
|
||||||
import { type PriceTrendDto } from '../../application/queries/get-price-trend/get-price-trend.handler';
|
import { PriceTrendDto } from '../../application/queries/get-price-trend/get-price-trend.handler';
|
||||||
import { GetPriceTrendQuery } from '../../application/queries/get-price-trend/get-price-trend.query';
|
import { GetPriceTrendQuery } from '../../application/queries/get-price-trend/get-price-trend.query';
|
||||||
import { type ValuationDto } from '../../application/queries/get-valuation/get-valuation.handler';
|
import { ValuationDto } from '../../application/queries/get-valuation/get-valuation.handler';
|
||||||
import { GetValuationQuery } from '../../application/queries/get-valuation/get-valuation.query';
|
import { GetValuationQuery } from '../../application/queries/get-valuation/get-valuation.query';
|
||||||
import { type GetDistrictStatsDto } from '../dto/get-district-stats.dto';
|
import { GetDistrictStatsDto } from '../dto/get-district-stats.dto';
|
||||||
import { type GetHeatmapDto } from '../dto/get-heatmap.dto';
|
import { GetHeatmapDto } from '../dto/get-heatmap.dto';
|
||||||
import { type GetMarketReportDto } from '../dto/get-market-report.dto';
|
import { GetMarketReportDto } from '../dto/get-market-report.dto';
|
||||||
import { type GetPriceTrendDto } from '../dto/get-price-trend.dto';
|
import { GetPriceTrendDto } from '../dto/get-price-trend.dto';
|
||||||
import { type GetValuationDto } from '../dto/get-valuation.dto';
|
import { GetValuationDto } from '../dto/get-valuation.dto';
|
||||||
|
|
||||||
@ApiTags('analytics')
|
@ApiTags('analytics')
|
||||||
@Controller('analytics')
|
@Controller('analytics')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { type LoggerService, type PrismaService, DomainException, NotFoundException, ValidationException } from '@modules/shared';
|
import { LoggerService, PrismaService, DomainException, NotFoundException, ValidationException } from '@modules/shared';
|
||||||
import { CancelUserDeletionCommand } from './cancel-user-deletion.command';
|
import { CancelUserDeletionCommand } from './cancel-user-deletion.command';
|
||||||
|
|
||||||
@CommandHandler(CancelUserDeletionCommand)
|
@CommandHandler(CancelUserDeletionCommand)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService, UnauthorizedException, ValidationException } from '@modules/shared';
|
import { DomainException, LoggerService, UnauthorizedException, ValidationException } from '@modules/shared';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
import { USER_REPOSITORY, IUserRepository } from '../../../domain/repositories/user.repository';
|
||||||
import { type MfaService } from '../../../infrastructure/services/mfa.service';
|
import { MfaService } from '../../../infrastructure/services/mfa.service';
|
||||||
import { DisableMfaCommand } from './disable-mfa.command';
|
import { DisableMfaCommand } from './disable-mfa.command';
|
||||||
|
|
||||||
@CommandHandler(DisableMfaCommand)
|
@CommandHandler(DisableMfaCommand)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { type LoggerService, type PrismaService, DomainException, NotFoundException } from '@modules/shared';
|
import { LoggerService, PrismaService, DomainException, NotFoundException } from '@modules/shared';
|
||||||
import { ExportUserDataCommand } from './export-user-data.command';
|
import { ExportUserDataCommand } from './export-user-data.command';
|
||||||
|
|
||||||
export interface UserDataExport {
|
export interface UserDataExport {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { type LoggerService, type PrismaService, DomainException, NotFoundException } from '@modules/shared';
|
import { LoggerService, PrismaService, DomainException, NotFoundException } from '@modules/shared';
|
||||||
import { ForceDeleteUserCommand } from './force-delete-user.command';
|
import { ForceDeleteUserCommand } from './force-delete-user.command';
|
||||||
|
|
||||||
@CommandHandler(ForceDeleteUserCommand)
|
@CommandHandler(ForceDeleteUserCommand)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { createId } from '@paralleldrive/cuid2';
|
import { createId } from '@paralleldrive/cuid2';
|
||||||
import { type LoggerService, DomainException } from '@modules/shared';
|
import { LoggerService, DomainException } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
MFA_CHALLENGE_REPOSITORY,
|
MFA_CHALLENGE_REPOSITORY,
|
||||||
type IMfaChallengeRepository,
|
IMfaChallengeRepository,
|
||||||
} from '../../../domain/repositories/mfa-challenge.repository';
|
} from '../../../domain/repositories/mfa-challenge.repository';
|
||||||
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
|
import { TokenService, TokenPair } from '../../../infrastructure/services/token.service';
|
||||||
import { LoginUserCommand } from './login-user.command';
|
import { LoginUserCommand } from './login-user.command';
|
||||||
|
|
||||||
const MFA_CHALLENGE_TTL_MINUTES = 5;
|
const MFA_CHALLENGE_TTL_MINUTES = 5;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type CommandBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, CommandBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { type LoggerService, type PrismaService, DomainException } from '@modules/shared';
|
import { LoggerService, PrismaService, DomainException } from '@modules/shared';
|
||||||
import { ForceDeleteUserCommand } from '../force-delete-user/force-delete-user.command';
|
import { ForceDeleteUserCommand } from '../force-delete-user/force-delete-user.command';
|
||||||
import { ProcessScheduledDeletionsCommand } from './process-scheduled-deletions.command';
|
import { ProcessScheduledDeletionsCommand } from './process-scheduled-deletions.command';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { type LoggerService, DomainException, UnauthorizedException } from '@modules/shared';
|
import { LoggerService, DomainException, UnauthorizedException } from '@modules/shared';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
import { USER_REPOSITORY, IUserRepository } from '../../../domain/repositories/user.repository';
|
||||||
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
|
import { TokenService, TokenPair } from '../../../infrastructure/services/token.service';
|
||||||
import { RefreshTokenCommand } from './refresh-token.command';
|
import { RefreshTokenCommand } from './refresh-token.command';
|
||||||
|
|
||||||
@CommandHandler(RefreshTokenCommand)
|
@CommandHandler(RefreshTokenCommand)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { createId } from '@paralleldrive/cuid2';
|
import { createId } from '@paralleldrive/cuid2';
|
||||||
import { ConflictException, DomainException, type LoggerService, ValidationException } from '@modules/shared';
|
import { ConflictException, DomainException, LoggerService, ValidationException } from '@modules/shared';
|
||||||
import { UserEntity } from '../../../domain/entities/user.entity';
|
import { UserEntity } from '../../../domain/entities/user.entity';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
import { USER_REPOSITORY, IUserRepository } from '../../../domain/repositories/user.repository';
|
||||||
import { Email } from '../../../domain/value-objects/email.vo';
|
import { Email } from '../../../domain/value-objects/email.vo';
|
||||||
import { HashedPassword } from '../../../domain/value-objects/hashed-password.vo';
|
import { HashedPassword } from '../../../domain/value-objects/hashed-password.vo';
|
||||||
import { Phone } from '../../../domain/value-objects/phone.vo';
|
import { Phone } from '../../../domain/value-objects/phone.vo';
|
||||||
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
|
import { TokenService, TokenPair } from '../../../infrastructure/services/token.service';
|
||||||
import { RegisterUserCommand } from './register-user.command';
|
import { RegisterUserCommand } from './register-user.command';
|
||||||
|
|
||||||
@CommandHandler(RegisterUserCommand)
|
@CommandHandler(RegisterUserCommand)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { type LoggerService, type PrismaService, DomainException, NotFoundException, ValidationException } from '@modules/shared';
|
import { LoggerService, PrismaService, DomainException, NotFoundException, ValidationException } from '@modules/shared';
|
||||||
import { RequestUserDeletionCommand } from './request-user-deletion.command';
|
import { RequestUserDeletionCommand } from './request-user-deletion.command';
|
||||||
|
|
||||||
const DELETION_GRACE_PERIOD_DAYS = 30;
|
const DELETION_GRACE_PERIOD_DAYS = 30;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService, ValidationException } from '@modules/shared';
|
import { DomainException, LoggerService, ValidationException } from '@modules/shared';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
import { USER_REPOSITORY, IUserRepository } from '../../../domain/repositories/user.repository';
|
||||||
import { type MfaService, type MfaSetupResult } from '../../../infrastructure/services/mfa.service';
|
import { MfaService, MfaSetupResult } from '../../../infrastructure/services/mfa.service';
|
||||||
import { SetupMfaCommand } from './setup-mfa.command';
|
import { SetupMfaCommand } from './setup-mfa.command';
|
||||||
|
|
||||||
export interface SetupMfaResultDto {
|
export interface SetupMfaResultDto {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService, UnauthorizedException } from '@modules/shared';
|
import { DomainException, LoggerService, UnauthorizedException } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
MFA_CHALLENGE_REPOSITORY,
|
MFA_CHALLENGE_REPOSITORY,
|
||||||
type IMfaChallengeRepository,
|
IMfaChallengeRepository,
|
||||||
} from '../../../domain/repositories/mfa-challenge.repository';
|
} from '../../../domain/repositories/mfa-challenge.repository';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
import { USER_REPOSITORY, IUserRepository } from '../../../domain/repositories/user.repository';
|
||||||
import { type MfaService } from '../../../infrastructure/services/mfa.service';
|
import { MfaService } from '../../../infrastructure/services/mfa.service';
|
||||||
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
|
import { TokenService, TokenPair } from '../../../infrastructure/services/token.service';
|
||||||
import { UseBackupCodeCommand } from './use-backup-code.command';
|
import { UseBackupCodeCommand } from './use-backup-code.command';
|
||||||
|
|
||||||
@CommandHandler(UseBackupCodeCommand)
|
@CommandHandler(UseBackupCodeCommand)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService, NotFoundException, CacheService, CachePrefix } from '@modules/shared';
|
import { DomainException, LoggerService, NotFoundException, CacheService, CachePrefix } from '@modules/shared';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
import { USER_REPOSITORY, IUserRepository } from '../../../domain/repositories/user.repository';
|
||||||
import { VerifyKycCommand } from './verify-kyc.command';
|
import { VerifyKycCommand } from './verify-kyc.command';
|
||||||
|
|
||||||
@CommandHandler(VerifyKycCommand)
|
@CommandHandler(VerifyKycCommand)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService, UnauthorizedException } from '@modules/shared';
|
import { DomainException, LoggerService, UnauthorizedException } from '@modules/shared';
|
||||||
import {
|
import {
|
||||||
MFA_CHALLENGE_REPOSITORY,
|
MFA_CHALLENGE_REPOSITORY,
|
||||||
type IMfaChallengeRepository,
|
IMfaChallengeRepository,
|
||||||
} from '../../../domain/repositories/mfa-challenge.repository';
|
} from '../../../domain/repositories/mfa-challenge.repository';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
import { USER_REPOSITORY, IUserRepository } from '../../../domain/repositories/user.repository';
|
||||||
import { type MfaService } from '../../../infrastructure/services/mfa.service';
|
import { MfaService } from '../../../infrastructure/services/mfa.service';
|
||||||
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
|
import { TokenService, TokenPair } from '../../../infrastructure/services/token.service';
|
||||||
import { VerifyMfaChallengeCommand } from './verify-mfa-challenge.command';
|
import { VerifyMfaChallengeCommand } from './verify-mfa-challenge.command';
|
||||||
|
|
||||||
@CommandHandler(VerifyMfaChallengeCommand)
|
@CommandHandler(VerifyMfaChallengeCommand)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { DomainException, type LoggerService, ValidationException } from '@modules/shared';
|
import { DomainException, LoggerService, ValidationException } from '@modules/shared';
|
||||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
import { USER_REPOSITORY, IUserRepository } from '../../../domain/repositories/user.repository';
|
||||||
import { type MfaService } from '../../../infrastructure/services/mfa.service';
|
import { MfaService } from '../../../infrastructure/services/mfa.service';
|
||||||
import { VerifyMfaSetupCommand } from './verify-mfa-setup.command';
|
import { VerifyMfaSetupCommand } from './verify-mfa-setup.command';
|
||||||
|
|
||||||
export interface VerifyMfaSetupResultDto {
|
export interface VerifyMfaSetupResultDto {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||||
import { type PrismaService, DomainException, type LoggerService } from '@modules/shared';
|
import { PrismaService, DomainException, LoggerService } from '@modules/shared';
|
||||||
import { GetAgentByUserIdQuery } from './get-agent-by-user-id.query';
|
import { GetAgentByUserIdQuery } from './get-agent-by-user-id.query';
|
||||||
|
|
||||||
export interface AgentDto {
|
export interface AgentDto {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user