feat(V1-API): implement TREX-Basic consumer API contract with encryption and persistence
- Add V1 API endpoints: assets, ledger, user-info - Implement Fernet-based credential payload encryption (req-010) - Extend health response schema with runtime info (req-004) - Add service token runtime ID issuance (req-005) - Implement database schema migration for V1 API (req-009) - Add comprehensive test coverage for V1 endpoints - Update SRS with 10 new requirements (REQ-004 through REQ-011) - Create batch planning, design, and verification for BAT-001 - Integrate review-discovery StagePilot skill - Update documentation indices and baseline specs
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# BAT-001 Design
|
||||
|
||||
- Status: draft
|
||||
- Batch: BAT-001
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
trex-basic Consumer-Driven Contract를 충족하기 위해 4개 계층에 걸쳐 변경한다.
|
||||
|
||||
1. **DB 레이어** (REQ-009): Alembic migration 1개 추가로 4개 스키마 변경
|
||||
2. **보안 레이어** (REQ-010): `app/core/encryption.py` 신규 — Fernet 암복호화 유틸리티, `app/core/config.py` startup validation
|
||||
3. **인증 확장** (REQ-005): service-token 발급 시 UUID v4 runtime_id 동시 생성
|
||||
4. **v1 API 레이어** (REQ-004, 006, 007, 008): `/v1` prefix APIRouter 신규, 기존 `/health` 응답 확장
|
||||
|
||||
## Changed Areas
|
||||
|
||||
- Docs:
|
||||
- docs/project-structure.md (신규 모듈 app/core/encryption.py, app/api/v1_*.py 반영)
|
||||
- docs/runtime-flows.md (v1 API 요청 흐름, runtime_id 발급 흐름 추가)
|
||||
- Modules:
|
||||
- app/core/encryption.py (신규)
|
||||
- app/core/config.py (CREDENTIAL_ENCRYPTION_KEY 필드 + startup validation)
|
||||
- app/domain/models.py (AuthToken.runtime_id, BrokerCredential, LedgerTrade 추가)
|
||||
- app/api/health.py (HealthResponse 스키마 적용, checked_at/service/version 필드 추가)
|
||||
- app/api/auth_routes.py (service-token 발급 응답에 runtime_id 포함)
|
||||
- app/api/v1_assets.py (신규)
|
||||
- app/api/v1_user_info.py (신규)
|
||||
- app/api/v1_ledger.py (신규)
|
||||
- app/main.py (v1 router include 추가)
|
||||
- app/cli/commands/service_tokens.py (runtime_id 출력 추가)
|
||||
- app/services/auth_service.py (runtime_id 생성·저장 로직)
|
||||
- app/schemas/ (HealthResponse, v1 Assets/UserInfo/Ledger 스키마 추가)
|
||||
- app/repositories/postgres.py (broker_credentials, ledger_trades, runtime_id 쿼리 추가)
|
||||
- migrations/versions/ (신규 migration 파일)
|
||||
- Interfaces:
|
||||
- GET /health: 응답 필드 추가 (service, version, checked_at) — 기존 status 필드 유지로 하위 호환
|
||||
- POST /auth/service-tokens (또는 동등 경로): 응답에 runtime_id 추가
|
||||
- GET /v1/assets: 신규
|
||||
- GET /v1/user_info: 신규
|
||||
- POST /v1/assets/{asset_id}/ledger/trades: 신규
|
||||
|
||||
## Key Decisions
|
||||
|
||||
1. **v1 라우터 구조**: `app/api/v1_*.py` 파일 3개를 각각 별도 모듈로 작성하고 `app/main.py`에서 `prefix="/v1"`로 일괄 등록한다. 기존 `/portfolios`, `/auth`, `/users` 라우터와 prefix가 겹치지 않는다.
|
||||
|
||||
2. **R-4 처리 (runtime_id 없는 구형 service-token)**: GET /v1/user_info 호출 시 해당 토큰에 runtime_id가 없으면 **HTTP 404** (`detail: "runtime_id not assigned to this token"`)를 반환한다. 즉시 생성 방식은 채택하지 않는다 — 기존 토큰 소유자에게 재발급을 요구하는 것이 명시적이고 안전하다.
|
||||
|
||||
3. **disabled_reason**: `users.is_active = false`인 경우 고정값 `"account_inactive"`를 반환한다. `is_active = true`인 경우 `disabled_reason`은 `null`로 반환한다.
|
||||
|
||||
4. **Fernet startup validation 위치**: `app/core/config.py`의 `Settings` 클래스 `model_post_init` 또는 FastAPI `lifespan` 시작 시 수행한다. `CREDENTIAL_ENCRYPTION_KEY`가 없으면 `RuntimeError`를 발생시켜 기동을 중단한다. 테스트 환경에서는 `CREDENTIAL_ENCRYPTION_KEY` 환경변수를 픽스처로 주입한다.
|
||||
|
||||
5. **runtime_id 포맷**: `str(uuid.uuid4())` — 하이픈 포함 UUID v4 문자열 (예: `"550e8400-e29b-41d4-a716-446655440000"`).
|
||||
|
||||
6. **broker_credentials 컬럼 타입**:
|
||||
- `user_id`: INTEGER NOT NULL (users.user_id FK)
|
||||
- `broker`: TEXT NOT NULL
|
||||
- `account_ref`: TEXT NOT NULL
|
||||
- `encrypted_payload`: TEXT NOT NULL
|
||||
- `credential_version`: INTEGER NOT NULL DEFAULT 1
|
||||
- `last_rotated_at`: TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
- PRIMARY KEY: `(user_id, broker)`
|
||||
|
||||
7. **ledger_trades 컬럼 타입**:
|
||||
- `ledger_entry_id`: BIGSERIAL PRIMARY KEY
|
||||
- `asset_id`: INTEGER NOT NULL (portfolios.portfolio_id 참조, FK 선택적)
|
||||
- `order_id`: TEXT NOT NULL
|
||||
- `execution_id`: TEXT NOT NULL
|
||||
- `side`: TEXT NOT NULL (buy/sell)
|
||||
- `quantity`: NUMERIC(20, 8) NOT NULL
|
||||
- `price`: NUMERIC(20, 8) NOT NULL
|
||||
- `fee_amount`: NUMERIC(20, 8) NOT NULL DEFAULT 0
|
||||
- `tax_amount`: NUMERIC(20, 8) NOT NULL DEFAULT 0
|
||||
- `executed_at`: TIMESTAMPTZ NOT NULL
|
||||
- `recorded_at`: TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
- UNIQUE: `(asset_id, order_id, execution_id)`
|
||||
|
||||
8. **앱 버전**: `app/core/config.py`의 `Settings.app_version` 값을 `GET /health` 응답의 `version` 필드로 사용한다. 현재 값: `"0.1.0"`.
|
||||
|
||||
9. **checked_at 타임존**: KST (UTC+9) ISO 8601 문자열로 반환한다. `datetime.now(timezone(timedelta(hours=9))).isoformat()`.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- `strategy_params` 파싱 실패(StrategySet.config가 null이거나 유효하지 않은 JSON): `{}` 폴백, 에러 전파 없음.
|
||||
- `ledger_trades` 중복 삽입 시 DB unique constraint 위반: psycopg `UniqueViolation` 캐치 → 기존 행 조회 → payload 비교 → 동일이면 200+duplicate, 다르면 409.
|
||||
- `broker_credentials`가 없는 사용자의 GET /v1/user_info: `brokers: []` 반환.
|
||||
- `CREDENTIAL_ENCRYPTION_KEY` 환경변수 없이 테스트 실행 시: 테스트 픽스처에서 `Fernet.generate_key()`로 임시 키 주입.
|
||||
|
||||
## Architecture Impact
|
||||
|
||||
- both (project-structure + runtime-flows)
|
||||
|
||||
## Reference Doc Update Plan
|
||||
|
||||
- project-structure.md: 신규 모듈(app/core/encryption.py, app/api/v1_assets.py, app/api/v1_user_info.py, app/api/v1_ledger.py) 반영 필요. implementation 완료 후 REQ-011에서 갱신.
|
||||
- runtime-flows.md: v1 API 요청 처리 흐름(service-token 인증 → 서비스 → DB), runtime_id 발급 흐름 추가 필요. implementation 완료 후 REQ-011에서 갱신.
|
||||
Reference in New Issue
Block a user