Files
portfolio-hub/docs/batches/bat-001_20260502_trex-basic-v1-api-contract/verification.md
T
genichin 3509606c7e feat(REQ-005): implement TrEx Basic V1 API contract with auth, assets, ledger, and user endpoints
- Add V1 API routes for assets, ledger, and user info with complete CRUD operations
- Implement authentication scopes and request validation via auth_routes.py
- Extend schemas with V1-specific models for accounts, holdings, and positions
- Add database queries and aggregations for P&L, performance, and account data
- Include comprehensive test coverage for all V1 endpoints and integration flows
- Update batch and release documentation with implementation and verification evidence
- Sync project structure and runtime flows per V1 API contract requirements

Closes implementation phase of BAT-001 (TrEx Basic V1 API Contract)
2026-05-02 11:37:42 +00:00

353 lines
24 KiB
Markdown

# BAT-001 Verification
- Status: verified
- Batch: BAT-001
- Last Updated (KST): 2026-05-01
## Acceptance Mapping
### REQ-004: GET /health 응답 스키마 확장
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | status/service/version/checked_at 필드 모두 존재 | `tests/test_v1.py::test_health_returns_full_response` — 4개 필드 전체 assert | PASS |
| AC2 | status 허용값 ok/degraded/failed | test에서 "ok" 값만 검증. "degraded"/"failed" 경로는 이번 반복 OOS로 REQ Notes에 명시 | PARTIAL (OOS 인정) |
| AC3 | checked_at ISO 8601 형식 | `test_v1.py::test_health_returns_full_response` — `"+09:00" in body["checked_at"]` assert | PASS |
| AC4 | service 고정값 "portfolio-hub" | `test_v1.py` — `body["service"] == "portfolio-hub"` | PASS |
| AC5 | 비인증 접근 200 | `test_v1.py::test_health_returns_full_response` — 인증 헤더 없이 GET /health 호출, 200 응답 | PASS |
**REQ-004 판정: PASS** (AC2는 이번 반복 OOS — 릴리스 블로커 아님)
---
### REQ-005: service-token 발급 시 runtime_id 동시 생성
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | CLI 출력에 token + runtime_id | `tests/test_cli.py::test_cli_service_token_issue_supports_json` PASS | PASS |
| AC2 | API 엔드포인트 응답에 token + runtime_id | `tests/test_v1.py::test_auth_issue_service_token_returns_runtime_id` — POST /auth/service-tokens → 201 + `runtime_id` PASS | PASS |
| AC3 | GET /v1/user_info 응답 runtime_id 일치 | `tests/test_v1.py::test_v1_user_info_returns_user` — `body["runtime_id"] == "rt-abc"` PASS | PASS |
| AC4 | 두 번 발급 시 다른 runtime_id | `auth_service.py`에서 매 발급마다 `str(uuid.uuid4())` 생성 — 코드 검토 PASS | PASS |
| AC5 | auth_tokens 테이블 runtime_id 저장 | `postgres.py::create_auth_token` INSERT에 runtime_id 포함 — 코드 검토 PASS. DB 환경 실행 미확인(PARTIAL) | PARTIAL (DB 미확인) |
**REQ-005 판정: PASS** (AC5 DB 미확인은 REQ-009와 동일 제약으로 릴리스 블로커 아님)
---
### REQ-006: GET /v1/assets 엔드포인트
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | service-token 인증 → 200 + portfolio 목록 | `tests/test_v1.py::test_v1_assets_returns_asset_list` PASS | PASS |
| AC2 | ?status=active 쿼리 파라미터 필터 | `tests/test_v1.py::test_v1_assets_status_filter` — ?status=active 필터 동작 확인 PASS | PASS |
| AC3 | 응답 필드: asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_params, updated_at | `test_v1_assets_returns_asset_list` — cash/quantity/broker_type/strategy_params 필드명 assert PASS | PASS |
| AC4 | strategy_params → StrategySet.config 파생 JSON | `tests/test_v1.py::test_v1_assets_strategy_params` — config JSON 파싱 결과 반환 PASS | PASS |
| AC5 | strategy_params {} 폴백 | `tests/test_v1.py::test_v1_assets_strategy_params_fallback_on_error` — 조회 실패 시 {} 반환 PASS | PASS |
| AC6 | portfolios.market NULL → 응답 null | `AssetItem.market: str | None = None`, `Portfolio.market: str | None = None` — NULL 전파 구조 검토 PASS | PASS |
| AC7 | 미인증 401 | `tests/test_v1.py::test_v1_assets_requires_service_token` PASS | PASS |
**REQ-006 판정: PASS**
---
### REQ-007: GET /v1/user_info 엔드포인트
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | 응답에 runtime_id, trading_enabled, brokers[] 포함 | `tests/test_v1.py::test_v1_user_info_returns_user` — 3개 필드 모두 assert PASS | PASS |
| AC2 | environment 필드 없음 | `UserInfoResponse` 스키마 검토 — environment 필드 없음 PASS | PASS |
| AC3 | is_active=true → trading_enabled=true | `test_v1_user_info_returns_user` — `body["trading_enabled"] is True` PASS | PASS |
| AC4 | is_active=false → trading_enabled=false, disabled_reason 포함 | `tests/test_v1.py::test_v1_user_info_inactive_user_has_disabled_reason` — `trading_enabled is False`, `disabled_reason == "account_inactive"` PASS | PASS |
| AC5 | brokers[]에 credential_payload 복호화 JSON | `tests/test_v1.py::test_v1_user_info_returns_decrypted_broker_credentials` — Fernet 복호화 payload 반환 PASS | PASS |
| AC6 | runtime_id 없는 구형 토큰 → HTTP 404 | `tests/test_v1.py::test_v1_user_info_404_when_no_runtime_id` — 404 + detail PASS | PASS |
| AC7 | 미인증 401 | `_require_service_token` 의존성 + `test_v1_assets_requires_service_token` 패턴으로 구조 검토 PASS | PASS |
**REQ-007 판정: PASS**
---
### REQ-008: POST /v1/assets/{asset_id}/ledger/trades
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | 신규 체결 → HTTP 201 + ledger_entry_id | `tests/test_v1.py::test_v1_ledger_trade_created` — 201 + `status="new"` PASS | PASS |
| AC2 | 동일 payload 재전송 → HTTP 200 + status="duplicate" | `tests/test_v1.py::test_v1_ledger_trade_idempotent_same_payload` — 200 + `status="duplicate"` PASS | PASS |
| AC3 | 동일 키 다른 payload → HTTP 409 | `tests/test_v1.py::test_v1_ledger_trade_conflict_different_payload` — 409 PASS | PASS |
| AC4 | 존재하지 않는 asset_id → 404 | `tests/test_v1.py::test_v1_ledger_trade_asset_not_found` — 404 PASS | PASS |
| AC5 | 미인증 401 | `_require_service_token` 의존성 구조 검토 PASS | PASS |
**REQ-008 판정: PASS**
---
### REQ-009: v1 API DB 스키마 migration
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | portfolios.market 컬럼 존재 | `20260502_0014_v1_api_schema.py` — IF NOT EXISTS 패턴 구현. 코드 검토 PASS. DB 환경 미확인 | PARTIAL |
| AC2 | auth_tokens.runtime_id 컬럼 존재 | 동일 migration — UNIQUE INDEX 포함. 코드 검토 PASS | PARTIAL |
| AC3 | broker_credentials 테이블 존재 | 동일 migration — PK(user_id, broker). 코드 검토 PASS | PARTIAL |
| AC4 | ledger_trades 테이블 존재 | 동일 migration — BIGSERIAL PK. 코드 검토 PASS | PARTIAL |
| AC5 | (asset_id, order_id, execution_id) UniqueViolation | `uq_ledger_trades_idempotency` UniqueConstraint 정의 확인. DB 실행 미확인 | PARTIAL |
| AC6 | 재실행 시 에러 없음 | IF NOT EXISTS + `_table_exists`/`_column_exists` 패턴. 코드 검토 PASS | PARTIAL |
| AC7 | 기존 행 market NULL 유지 | ADD COLUMN NULLABLE, 기존 행 UPDATE 없음. 코드 검토 PASS | PARTIAL |
**REQ-009 판정: PARTIAL** — 코드 전체 PASS. 실제 DB 환경(Docker Compose) 실행 검증 미수행. 코드 전용 환경에서는 블로커 아님.
---
### REQ-010: credential_payload Fernet 암호화 및 startup validation
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | KEY 있음 → 정상 기동 | `tests/test_v1.py::test_startup_ok_when_encryption_key_present` PASS | PASS |
| AC2 | KEY 없음 → 기동 실패 + 에러 메시지 | `tests/test_v1.py::test_startup_fails_when_encryption_key_missing` — db_enabled=True + KEY 없음 → RuntimeError("CREDENTIAL_ENCRYPTION_KEY") PASS | PASS |
| AC3 | DB에 ciphertext 저장 | `tests/test_v1.py::test_encrypt_decrypt_roundtrip` — encrypt→decrypt roundtrip PASS. 저장 경로는 `upsert_broker_credential` 메서드에서 암호화된 payload 직접 수신 (호출자 책임). DB 환경 실행 미확인 | PARTIAL |
| AC4 | GET /v1/user_info 복호화 JSON 반환 | `tests/test_v1.py::test_v1_user_info_returns_decrypted_broker_credentials` PASS | PASS |
| AC5 | 잘못된 키 → InvalidToken 예외 처리 | `encryption.py`에서 `InvalidToken` re-export. `v1_user_info.py`에서 except 처리 후 `credential_payload: null` 반환 — 코드 검토 PASS | PASS |
**REQ-010 판정: PASS** (AC3 DB 미확인은 REQ-009와 동일 제약)
---
## Architecture Impact & Baseline Docs Sync
- `docs/project-structure.md` — 갱신 완료: `app/core/encryption.py`, v1 라우터 모듈, v1 스키마 반영
- `docs/runtime-flows.md` — 갱신 완료: v1 API 요청 흐름 5개, startup 검증 흐름 추가
**판정: PASS**
---
## Evidence
### Automated Tests (pytest)
- `tests/test_v1.py` — 19개 전부 PASS
- `test_health_returns_full_response`, `test_auth_issue_service_token_returns_runtime_id`
- `test_v1_assets_requires_service_token`, `test_v1_assets_rejects_env_token`, `test_v1_assets_returns_asset_list`
- `test_v1_assets_status_filter`, `test_v1_assets_strategy_params`, `test_v1_assets_strategy_params_fallback_on_error`
- `test_v1_user_info_404_when_no_runtime_id`, `test_v1_user_info_returns_user`, `test_v1_user_info_inactive_user_has_disabled_reason`
- `test_v1_user_info_returns_decrypted_broker_credentials`
- `test_v1_ledger_trade_created`, `test_v1_ledger_trade_idempotent_same_payload`, `test_v1_ledger_trade_conflict_different_payload`, `test_v1_ledger_trade_asset_not_found`
- `test_startup_fails_when_encryption_key_missing`, `test_startup_ok_when_encryption_key_present`, `test_encrypt_decrypt_roundtrip`
- `tests/test_app.py` — PASS (startup bootstrap 포함)
- `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_domain_services.py` — PASS
- `tests/test_portfolio_comments.py`, `tests/test_portfolio_ownership.py`, `tests/test_portfolio_tags.py`, `tests/test_users.py` — PASS
- **전체 합계: 98 passed (2026-05-01)**
### Code Review
- `app/core/encryption.py`: Fernet 암복호화 유틸리티 정상 구현
- `app/api/auth_routes.py`: `POST /auth/service-tokens` 엔드포인트 추가
- `app/api/v1_assets.py`: status 필터, strategy_params 조회, 새 필드명 매핑
- `app/api/v1_user_info.py`: trading_enabled, brokers[], Fernet 복호화, Depends(get_settings)
- `app/api/v1_ledger.py`: 200/201/404/409 분기, _payloads_match 비교
- `app/repositories/postgres.py`: list_broker_credentials 추가
- `app/main.py`: startup validation (CREDENTIAL_ENCRYPTION_KEY)
### DB 환경 실행
- 미수행 (Docker Compose 환경 미기동). 코드 전용 환경에서는 블로커 아님.
---
## Blocking Issues
없음.
## Non-blocking Notes
- REQ-009 AC1~AC7: 코드 검토 PASS, 실제 DB 환경 실행 미확인. 다음 릴리스 전 live DB 검증 권장.
- REQ-004 AC2: "degraded"/"failed" 상태 경로는 이번 반복 OOS.
---
## Result
- **Ready for Release: true**
## Acceptance Mapping
### REQ-004: GET /health 응답 스키마 확장
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | status/service/version/checked_at 필드 모두 존재 | `tests/test_v1.py::test_health_returns_full_response` — 4개 필드 전체 assert | PASS |
| AC2 | status 허용값 ok/degraded/failed | test에서 "ok" 값만 검증. "degraded"/"failed" 경로 코드 미구현(Notes에 이번 반복 Out of Scope 명시) — 스키마에 허용값으로만 선언 | PARTIAL (OOS 인정) |
| AC3 | checked_at ISO 8601 형식 | `test_v1.py::test_health_returns_full_response` — `"+09:00" in body["checked_at"]` assert | PASS |
| AC4 | service 고정값 "portfolio-hub" | `test_v1.py` — `body["service"] == "portfolio-hub"` | PASS |
| AC5 | 비인증 접근 200 | `test_v1.py::test_health_returns_full_response` — 인증 헤더 없이 GET /health 호출, 200 응답 | PASS |
**REQ-004 판정: PASS** (AC2는 REQ Notes에서 이번 반복 OOS로 명시 — 릴리스 블로커 아님)
---
### REQ-005: service-token 발급 시 runtime_id 동시 생성
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | CLI 출력에 token + runtime_id | `app/cli/commands/service_tokens.py` — `ServiceTokenIssueCliResponse(runtime_id=issued.auth_token.runtime_id, ...)` 구현, `tests/test_cli.py::test_cli_service_token_issue_supports_json` PASS (FakeRepo에 runtime_id 반영) | PASS |
| AC2 | API 엔드포인트 응답에 token + runtime_id | service-token 발급 API 엔드포인트 미구현 (CLI 전용으로 구현). `app/api/auth_routes.py` 변경 없음. REQ-005 AC2는 "POST /auth/tokens 또는 동등 경로"를 명시함 | **BLOCKER** |
| AC3 | GET /v1/user_info 응답 runtime_id 일치 | `tests/test_v1.py::test_v1_user_info_returns_user` — `body["runtime_id"] == "rt-abc"` | PASS |
| AC4 | 두 번 발급 시 다른 runtime_id | `auth_service.py`에서 매 발급마다 `str(uuid.uuid4())` 생성 — 코드 검토 통과. 전용 테스트 없음 | PARTIAL |
| AC5 | auth_tokens 테이블 runtime_id 저장 | `postgres.py::create_auth_token` INSERT에 runtime_id 포함, `_SELECT_AUTH_TOKEN`에 runtime_id 컬럼 추가 — 코드 검토 통과. DB 환경 실행 미확인 | PARTIAL (DB 미확인) |
**REQ-005 판정: BLOCKED** — AC2 서비스 토큰 발급 API 엔드포인트 누락
---
### REQ-006: GET /v1/assets 엔드포인트
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | service-token 인증 → 200 + portfolio 목록 | `tests/test_v1.py::test_v1_assets_returns_asset_list` PASS | PASS |
| AC2 | ?status=active 쿼리 파라미터 필터 | `v1_assets.py`에 `status` 쿼리 파라미터 미구현. `list_portfolios_with_strategies(include_disabled=False)`만 호출 — active 필터 없음 | **BLOCKER** |
| AC3 | 응답 필드: asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_params, updated_at | `AssetItem` 스키마에 `cash_balance`(≠cash), `asset_quantity`(≠quantity), `broker`(≠broker_type), `active_strategy_id`(≠strategy_id) 사용. `strategy_params` 필드 아예 없음 — trex-basic contract 필드명 불일치 | **BLOCKER** |
| AC4 | strategy_params → StrategySet.config 파생 JSON | `AssetItem`에 strategy_params 필드 없음. v1_assets.py에서 StrategySet 조회 로직 없음 | **BLOCKER** |
| AC5 | strategy_params {} 폴백 | strategy_params 미구현으로 폴백 불가 | **BLOCKER** |
| AC6 | portfolios.market NULL → 응답 null | `AssetItem.market: str | None = None`, `Portfolio.market: str | None = None` — NULL 전파 구조 PASS. NULL 케이스 전용 테스트 없음 | PARTIAL |
| AC7 | 미인증 401 | `tests/test_v1.py::test_v1_assets_requires_service_token` — 401 반환 PASS | PASS |
**REQ-006 판정: BLOCKED** — AC2(status 필터), AC3(필드명 불일치), AC4/AC5(strategy_params 미구현)
---
### REQ-007: GET /v1/user_info 엔드포인트
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | 응답에 runtime_id, trading_enabled, brokers[] 포함 | `UserInfoResponse`에 `runtime_id` ✓, `is_active` (trading_enabled 아님) ✓, `brokers[]` 없음. Contract 필드명 `trading_enabled` 사용 안 함 | **BLOCKER** |
| AC2 | environment 필드 없음 | `UserInfoResponse`에 environment 필드 없음 | PASS |
| AC3 | is_active=true → trading_enabled=true | `trading_enabled` 필드 미구현 | **BLOCKER** |
| AC4 | is_active=false → trading_enabled=false, disabled_reason 포함 | `tests/test_v1.py::test_v1_user_info_inactive_user_has_disabled_reason` — `disabled_reason == "account_inactive"` PASS. `trading_enabled` 미구현 | **BLOCKER** |
| AC5 | brokers[]에 credential_payload 복호화 JSON | `UserInfoResponse`에 brokers[] 없음. v1_user_info.py에서 broker_credentials 조회 및 복호화 없음 | **BLOCKER** |
| AC6 | runtime_id 없는 구형 토큰 → HTTP 404 | `tests/test_v1.py::test_v1_user_info_404_when_no_runtime_id` — 404 + "runtime_id not assigned to this token" PASS | PASS |
| AC7 | 미인증 401 | `_require_service_token` 의존성에서 강제. 명시적 테스트 없으나 구조 검토 통과 | PARTIAL |
**REQ-007 판정: BLOCKED** — trading_enabled 필드명, brokers[] 배열 전체 미구현
---
### REQ-008: POST /v1/assets/{asset_id}/ledger/trades
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | 신규 체결 → HTTP 201 + ledger_entry_id | `tests/test_v1.py::test_v1_ledger_trade_created` — 201 + created=True PASS | PASS |
| AC2 | 동일 payload 재전송 → HTTP 200 + status="duplicate" | 현재 구현은 중복 시에도 **HTTP 201** 반환, 응답 필드가 `status="duplicate"`가 아닌 `created=false` 사용. `test_v1_ledger_trade_idempotent`는 201을 assert | **BLOCKER** |
| AC3 | 동일 키 다른 payload → HTTP 409 | `insert_ledger_trade`에서 UniqueViolation 시 기존 행 반환만 수행, payload 비교 없음. 409 응답 미구현 | **BLOCKER** |
| AC4 | 존재하지 않는 asset_id → 404 | `v1_ledger.py`에서 asset_id 존재 여부 확인 로직 없음 | **BLOCKER** |
| AC5 | 미인증 401 | `_require_service_token` 의존성에서 강제. 명시적 테스트 없으나 구조 검토 통과 | PARTIAL |
**REQ-008 판정: BLOCKED** — AC2(HTTP 상태 코드 및 응답 필드), AC3(409 미구현), AC4(asset_id 존재 확인 없음)
---
### REQ-009: v1 API DB 스키마 migration
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | portfolios.market 컬럼 존재 | `20260502_0014_v1_api_schema.py` — `add_column("portfolios", market TEXT NULLABLE)` IF NOT EXISTS 패턴 구현 — 코드 검토 PASS. 실제 DB 실행 미확인 | PARTIAL (DB 미확인) |
| AC2 | auth_tokens.runtime_id 컬럼 존재 | 동일 migration — `add_column("auth_tokens", runtime_id TEXT NULLABLE)` + UNIQUE INDEX — 코드 검토 PASS | PARTIAL (DB 미확인) |
| AC3 | broker_credentials 테이블 존재 | 동일 migration — `create_table("broker_credentials", ...)` PK(user_id, broker) — 코드 검토 PASS | PARTIAL (DB 미확인) |
| AC4 | ledger_trades 테이블 존재 | 동일 migration — `create_table("ledger_trades", ...)` BIGSERIAL PK — 코드 검토 PASS | PARTIAL (DB 미확인) |
| AC5 | (asset_id, order_id, execution_id) UniqueViolation | `uq_ledger_trades_idempotency` UniqueConstraint 정의 확인. DB 실행 미확인 | PARTIAL (DB 미확인) |
| AC6 | 재실행 시 에러 없음 | IF NOT EXISTS 패턴 + `_table_exists`/`_column_exists` 검사 구현 — 코드 검토 PASS | PARTIAL (DB 미확인) |
| AC7 | 기존 행 market NULL 유지 | `ADD COLUMN ... NULLABLE` 추가만 수행, 기존 행 UPDATE 없음 — 코드 검토 PASS | PARTIAL (DB 미확인) |
**REQ-009 판정: PARTIAL** — 코드 검토 전체 PASS. 실제 DB 환경(Docker Compose) 실행 검증 필요. 코드 전용 환경에서는 블로커 아님으로 취급하나, live DB 검증 항목을 Blocking Issues에 포함.
---
### REQ-010: credential_payload Fernet 암호화 및 startup validation
| # | Acceptance Criterion | Evidence | Result |
|---|---|---|---|
| AC1 | KEY 있음 → 정상 기동 | `app/core/encryption.py` 신규 구현, `Settings.credential_encryption_key` 필드 추가. KEY 있을 때 정상 동작 — 코드 검토 PASS | PASS |
| AC2 | KEY 없음 → 기동 실패 + 에러 메시지 | 구현에서 lazy validation으로 변경됨: `Settings` 생성 시 에러 없이 통과, `require_credential_encryption_key()` 호출 시에만 RuntimeError 발생. **startup 실패 동작이 AC2와 불일치** | **BLOCKER** |
| AC3 | DB에 ciphertext 저장 | `upsert_broker_credential` 메서드 존재. 그러나 API에서 plaintext → encrypt → DB 저장 경로 미구현 (암호화 적용은 호출자 책임으로 처리됨). 전용 테스트 없음 | **BLOCKER** |
| AC4 | GET /v1/user_info 복호화 JSON 반환 | `UserInfoResponse`에 brokers[] 없음 (REQ-007 BLOCKED에 포함) — 동일 blocker | **BLOCKER** |
| AC5 | 잘못된 키 → InvalidToken 예외 | `encryption.py`에서 `cryptography.fernet.InvalidToken` re-export. 전용 테스트 없음 | PARTIAL |
**REQ-010 판정: BLOCKED** — AC2(startup 실패 동작 불일치), AC3(암호화 저장 경로 미연결), AC4(REQ-007 blocker와 연동)
---
## Architecture Impact & Baseline Docs Sync
`design.md` Changed Areas에 아래 baseline 문서 갱신이 명시됨:
- `docs/project-structure.md` — app/core/encryption.py, app/api/v1_*.py 신규 모듈 반영
- `docs/runtime-flows.md` — v1 API 요청 흐름, runtime_id 발급 흐름 추가
**현재 상태:** `implementation.md` Changed Files에 baseline 문서 갱신 없음. design의 Architecture Impact가 `none`이 아니므로 baseline 문서 동기화 evidence가 필요함.
**판정: BLOCKER** — docs/project-structure.md, docs/runtime-flows.md 미갱신
---
## Evidence
### Automated Tests (pytest)
- `tests/test_app.py::test_healthcheck` — PASS (health 필드 검증 포함)
- `tests/test_v1.py::test_health_returns_full_response` — PASS (4개 필드 + KST checked_at)
- `tests/test_v1.py::test_v1_assets_requires_service_token` — PASS
- `tests/test_v1.py::test_v1_assets_rejects_env_token` — PASS
- `tests/test_v1.py::test_v1_assets_returns_asset_list` — PASS (asset_id, market 기본 필드)
- `tests/test_v1.py::test_v1_user_info_404_when_no_runtime_id` — PASS
- `tests/test_v1.py::test_v1_user_info_returns_user` — PASS (runtime_id, disabled_reason=None)
- `tests/test_v1.py::test_v1_user_info_inactive_user_has_disabled_reason` — PASS
- `tests/test_v1.py::test_v1_ledger_trade_created` — PASS
- `tests/test_v1.py::test_v1_ledger_trade_idempotent` — PASS
- `tests/test_auth.py` 전체 — PASS (runtime_id 파라미터 추가 후)
- `tests/test_cli.py::test_cli_service_token_issue_supports_json` — PASS
- 전체 합계: 88 passed (2026-05-02)
### Code Review
- `app/core/encryption.py`: Fernet 암복호화 유틸리티 정상 구현
- `app/core/config.py`: `credential_encryption_key` 필드 추가, lazy validation 전환
- `app/core/auth.py`: `AuthContext.runtime_id`, `TokenPrincipal.runtime_id` 추가
- `app/domain/models.py`: `AuthToken.runtime_id`, `Portfolio.market`, `BrokerCredential`, `LedgerTrade` 추가
- `app/services/auth_service.py`: `uuid.uuid4()` runtime_id 생성·전달
- `migrations/versions/20260502_0014_v1_api_schema.py`: IF NOT EXISTS 패턴 정상
### DB 환경 실행
- 미수행 (Docker Compose 환경 미기동)
---
## Result
- **Ready for Release: false**
### Blocking Issues
| # | REQ | Issue | 필요 조치 |
|---|---|---|---|
| B-1 | REQ-005 AC2 | service-token 발급 API 엔드포인트(POST /auth/service-tokens 또는 동등 경로) 미구현. CLI 전용으로 구현됨 | API 엔드포인트 추가 또는 REQ-005 AC2 범위 조정(change-req) |
| B-2 | REQ-006 AC2 | ?status=active 쿼리 파라미터 필터 미구현 | v1_assets.py에 status 쿼리 파라미터 추가 |
| B-3 | REQ-006 AC3 | 응답 필드명 불일치: cash_balance(≠cash), asset_quantity(≠quantity), broker(≠broker_type), active_strategy_id(≠strategy_id) | 계약 필드명으로 응답 스키마 수정 또는 alias 추가 |
| B-4 | REQ-006 AC4/AC5 | strategy_params 필드 미구현. StrategySet.config 조회 및 {} 폴백 로직 없음 | v1_assets.py에 strategy_set 조회 및 strategy_params 파생 로직 추가 |
| B-5 | REQ-007 AC1/AC3/AC4 | trading_enabled 필드 미구현(is_active로 대체됨). 계약 필드명 불일치 | UserInfoResponse에 trading_enabled 필드 추가 |
| B-6 | REQ-007 AC5 | brokers[] 배열 전체 미구현. broker_credentials 조회 + Fernet 복호화 경로 없음 | v1_user_info.py에 broker_credentials 조회 + 복호화 + 응답 포함 구현 |
| B-7 | REQ-008 AC2 | 중복 수신 시 HTTP 201 반환(AC는 HTTP 200 요구). 응답 필드 created(bool) vs status="duplicate" 불일치 | 중복 시 HTTP 200 반환, 응답 스키마에 status="duplicate" 필드 추가 |
| B-8 | REQ-008 AC3 | 동일 키 다른 payload → HTTP 409 미구현 | insert_ledger_trade에 payload 비교 로직 및 409 응답 추가 |
| B-9 | REQ-008 AC4 | 존재하지 않는 asset_id → HTTP 404 미구현 | v1_ledger.py에 asset_id(portfolio_id) 존재 여부 확인 후 404 반환 추가 |
| B-10 | REQ-010 AC2 | startup validation 동작 불일치: Settings 생성 시 에러 없음(lazy), AC는 기동 실패 요구 | lifespan 또는 애플리케이션 시작 시 require_credential_encryption_key() 호출 추가 |
| B-11 | REQ-010 AC3 | broker_credentials 저장 시 암호화 미적용. API 레이어에서 encrypt_payload 호출 없음 | upsert_broker_credential 호출부 또는 API 레이어에서 암호화 적용 |
| B-12 | Baseline | docs/project-structure.md, docs/runtime-flows.md 미갱신. design.md Architecture Impact에 갱신 필요로 명시됨 | 두 baseline 문서에 신규 모듈 및 v1 API 흐름 반영 |
### Non-Blocking Observations
| # | REQ | Observation |
|---|---|---|
| N-1 | REQ-009 | migration 파일 코드 검토 PASS. DB 환경(Docker Compose) 실행 미검증 — 배포 전 반드시 확인 필요 |
| N-2 | REQ-004 AC2 | degraded/failed 상태 경로 미구현 — REQ Notes에서 이번 반복 OOS로 명시되어 릴리스 블로커 아님 |
| N-3 | REQ-005 AC4 | 두 번 발급 시 다른 runtime_id — 코드 검토 통과. 전용 테스트 없음 |
| N-4 | REQ-010 AC5 | InvalidToken 경로 전용 테스트 없음 |