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에서 갱신.
|
||||
@@ -0,0 +1,157 @@
|
||||
# BAT-001 Implementation
|
||||
|
||||
- Status: implemented
|
||||
- Batch: BAT-001
|
||||
|
||||
## Plan Summary
|
||||
|
||||
design.md에서 확정된 결정 사항을 기반으로 아래 범위를 구현한다.
|
||||
|
||||
1. app/core/encryption.py 신규 — Fernet 암복호화 유틸리티
|
||||
2. app/core/config.py — CREDENTIAL_ENCRYPTION_KEY 필드 추가 및 require_credential_encryption_key() 메서드
|
||||
3. Alembic migration 신규 — 4개 스키마 변경 (IF NOT EXISTS 패턴)
|
||||
4. app/domain/models.py — AuthToken.runtime_id, TokenPrincipal.runtime_id, BrokerCredential, LedgerTrade 추가
|
||||
5. app/services/auth_service.py — service-token 발급 시 UUID v4 runtime_id 생성·저장
|
||||
6. app/cli/commands/service_tokens.py — 응답/출력에 runtime_id 포함
|
||||
7. app/core/auth.py — AuthContext.runtime_id 추가, _resolve_auth_context에서 전달
|
||||
8. app/api/health.py, app/schemas/v1.py — HealthResponse 스키마 적용
|
||||
9. app/api/v1_assets.py — GET /v1/assets 구현
|
||||
10. app/api/v1_user_info.py — GET /v1/user_info 구현 (R-4: 404 반환)
|
||||
11. app/api/v1_ledger.py — POST /v1/assets/{asset_id}/ledger/trades 구현
|
||||
12. app/repositories/postgres.py — broker_credentials, ledger_trades, runtime_id 쿼리 추가
|
||||
13. app/main.py — /v1 라우터 등록
|
||||
14. tests/ — 테스트 파일 작성 및 기존 테스트 확장
|
||||
|
||||
## Changed Files
|
||||
|
||||
- app/core/encryption.py (신규)
|
||||
- app/core/config.py (수정: credential_encryption_key 필드, require_credential_encryption_key 메서드, model_validator import)
|
||||
- app/core/auth.py (수정: AuthContext.runtime_id, _resolve_auth_context runtime_id 전달)
|
||||
- migrations/versions/20260502_0014_v1_api_schema.py (신규)
|
||||
- app/domain/models.py (수정: AuthToken.runtime_id, TokenPrincipal.runtime_id, Portfolio.market, BrokerCredential, LedgerTrade)
|
||||
- app/services/auth_service.py (수정: uuid import, runtime_id 생성·전달)
|
||||
- app/cli/commands/service_tokens.py (수정: ServiceTokenIssueCliResponse에 runtime_id 포함)
|
||||
- app/api/health.py (수정: HealthResponse 스키마 적용)
|
||||
- app/schemas/auth.py (수정: ServiceTokenIssueCliResponse.runtime_id 추가)
|
||||
- app/schemas/v1.py (신규: AssetsResponse, UserInfoResponse, LedgerTradeRequest/Response)
|
||||
- app/api/v1_assets.py (신규)
|
||||
- app/api/v1_user_info.py (신규)
|
||||
- app/api/v1_ledger.py (신규)
|
||||
- app/repositories/postgres.py (수정: _SELECT_AUTH_TOKEN runtime_id, _SELECT_PORTFOLIO market, _row_to_auth_token runtime_id, _row_to_portfolio market, create_auth_token runtime_id, get_token_principal runtime_id, broker_credentials CRUD, ledger_trades CRUD, list_portfolios_with_strategies)
|
||||
- app/main.py (수정: v1 라우터 3개 등록)
|
||||
- tests/test_app.py (수정: test_healthcheck 응답 필드 검증 업데이트)
|
||||
- tests/test_auth.py (수정: _AuthRepo.create_auth_token runtime_id 파라미터 추가)
|
||||
- tests/test_cli.py (수정: FakeRepo.create_auth_token runtime_id 파라미터 추가)
|
||||
- tests/test_v1.py (신규: health/assets/user_info/ledger 9개 테스트)
|
||||
|
||||
## Execution Log
|
||||
|
||||
1. [x] app/core/encryption.py 신규 구현
|
||||
2. [x] app/core/config.py CREDENTIAL_ENCRYPTION_KEY 추가 및 require_credential_encryption_key() 메서드
|
||||
3. [x] Alembic migration 파일 작성 (20260502_0014_v1_api_schema.py)
|
||||
4. [x] app/domain/models.py 모델 필드/클래스 추가
|
||||
5. [x] auth_service.py runtime_id 생성·저장 구현
|
||||
6. [x] service_tokens.py runtime_id 출력 추가
|
||||
7. [x] app/core/auth.py AuthContext.runtime_id 추가
|
||||
8. [x] app/api/health.py HealthResponse 스키마 적용
|
||||
9. [x] app/api/v1_assets.py 구현
|
||||
10. [x] app/api/v1_user_info.py 구현 (runtime_id 없는 구형 토큰 → HTTP 404)
|
||||
11. [x] app/api/v1_ledger.py 구현 (idempotent)
|
||||
12. [x] app/repositories/postgres.py 쿼리 추가
|
||||
13. [x] app/main.py /v1 라우터 등록
|
||||
14. [x] 테스트 작성 및 전체 테스트 스위트 통과 (88 passed)
|
||||
|
||||
## Validation
|
||||
|
||||
- [x] pytest 88개 전체 통과 (2026-05-02)
|
||||
- [ ] alembic upgrade head 성공 (로컬 Docker Compose 환경 — DB 없이 코드 검증만 완료)
|
||||
- [x] GET /health 응답에 service/version/checked_at(+09:00 KST) 포함 확인 (테스트)
|
||||
- [x] GET /v1/assets 200 응답 확인 (테스트)
|
||||
- [x] GET /v1/user_info runtime_id 없는 토큰 → 404 확인 (테스트)
|
||||
- [x] POST /v1/assets/{asset_id}/ledger/trades 중복 → created=false 확인 (테스트)
|
||||
|
||||
## Design Decisions (confirmed during implementation)
|
||||
|
||||
- CREDENTIAL_ENCRYPTION_KEY: Settings 생성 시 validator 없음. `require_credential_encryption_key()` 메서드 호출 시 RuntimeError 발생 (lazy validation)
|
||||
- R-4 (구형 service-token runtime_id 없음): HTTP 404, detail="runtime_id not assigned to this token"
|
||||
- disabled_reason: is_active=False 시 "account_inactive" 고정값
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- (해소) R1: CREDENTIAL_ENCRYPTION_KEY — lazy validation으로 처리. 테스트 환경 unblocked.
|
||||
|
||||
|
||||
design.md에서 확정된 결정 사항을 기반으로 아래 범위를 구현한다.
|
||||
|
||||
1. app/core/encryption.py 신규 — Fernet 암복호화 유틸리티
|
||||
2. app/core/config.py — CREDENTIAL_ENCRYPTION_KEY 필드 추가 및 startup validation
|
||||
3. Alembic migration 신규 — 4개 스키마 변경 (IF NOT EXISTS 패턴)
|
||||
4. app/domain/models.py — AuthToken.runtime_id, BrokerCredential, LedgerTrade 추가
|
||||
5. app/services/auth_service.py — service-token 발급 시 UUID v4 runtime_id 생성·저장
|
||||
6. app/api/auth_routes.py, app/cli/commands/service_tokens.py — 응답/출력에 runtime_id 포함
|
||||
7. app/api/health.py, app/schemas/ — HealthResponse 스키마 적용
|
||||
8. app/api/v1_assets.py — GET /v1/assets 구현
|
||||
9. app/api/v1_user_info.py — GET /v1/user_info 구현 (R-4: 404 반환)
|
||||
10. app/api/v1_ledger.py — POST /v1/assets/{asset_id}/ledger/trades 구현
|
||||
11. app/repositories/postgres.py — broker_credentials, ledger_trades, runtime_id 쿼리 추가
|
||||
12. app/main.py — /v1 라우터 등록
|
||||
13. 테스트 파일 작성 및 기존 테스트 확장
|
||||
|
||||
## Changed Files
|
||||
|
||||
- app/core/encryption.py (신규)
|
||||
- app/core/config.py (수정)
|
||||
- migrations/versions/20260502_0014_v1_api_schema.py (신규)
|
||||
- app/domain/models.py (수정)
|
||||
- app/services/auth_service.py (수정)
|
||||
- app/api/auth_routes.py (수정)
|
||||
- app/cli/commands/service_tokens.py (수정)
|
||||
- app/api/health.py (수정)
|
||||
- app/schemas/ (수정: HealthResponse 추가, v1 스키마 추가)
|
||||
- app/api/v1_assets.py (신규)
|
||||
- app/api/v1_user_info.py (신규)
|
||||
- app/api/v1_ledger.py (신규)
|
||||
- app/repositories/postgres.py (수정)
|
||||
- app/main.py (수정)
|
||||
- tests/test_health.py (신규 또는 test_app.py 확장)
|
||||
- tests/test_auth.py (확장)
|
||||
- tests/test_v1_assets.py (신규)
|
||||
- tests/test_v1_user_info.py (신규)
|
||||
- tests/test_v1_ledger.py (신규)
|
||||
|
||||
## Execution Log
|
||||
|
||||
_(구현 진행 시 단계별 기록)_
|
||||
|
||||
1. [ ] app/core/encryption.py 신규 구현
|
||||
2. [ ] app/core/config.py CREDENTIAL_ENCRYPTION_KEY 추가 및 startup validation
|
||||
3. [ ] Alembic migration 파일 작성 및 로컬 upgrade head 성공 확인
|
||||
4. [ ] app/domain/models.py 모델 필드/클래스 추가
|
||||
5. [ ] auth_service.py runtime_id 생성·저장 구현
|
||||
6. [ ] auth_routes.py, service_tokens.py runtime_id 출력 추가
|
||||
7. [ ] app/api/health.py HealthResponse 스키마 적용
|
||||
8. [ ] app/api/v1_assets.py 구현
|
||||
9. [ ] app/api/v1_user_info.py 구현
|
||||
10. [ ] app/api/v1_ledger.py 구현
|
||||
11. [ ] app/repositories/postgres.py 쿼리 추가
|
||||
12. [ ] app/main.py /v1 라우터 등록
|
||||
13. [ ] 테스트 작성 및 전체 테스트 스위트 통과
|
||||
|
||||
## Validation
|
||||
|
||||
_(구현 완료 시 기록)_
|
||||
|
||||
- [ ] pytest 전체 통과
|
||||
- [ ] CREDENTIAL_ENCRYPTION_KEY 없이 기동 시 RuntimeError 확인
|
||||
- [ ] alembic upgrade head 성공 (로컬 Docker Compose 환경)
|
||||
- [ ] GET /health 응답에 service/version/checked_at 포함 확인
|
||||
- [ ] GET /v1/assets 200 응답 확인
|
||||
- [ ] GET /v1/user_info runtime_id 없는 토큰 → 404 확인
|
||||
- [ ] POST /v1/assets/{asset_id}/ledger/trades 중복 → 200+duplicate 확인
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
_(구현 전 상태 — 진행 중 업데이트)_
|
||||
|
||||
- R1: CREDENTIAL_ENCRYPTION_KEY 테스트 픽스처 처리 방식 확정 필요
|
||||
- R2: psycopg UniqueViolation import 경로 확인 필요 (psycopg v3 기준)
|
||||
@@ -0,0 +1,67 @@
|
||||
# BAT-001 Index
|
||||
|
||||
- Status: in-delivery
|
||||
- Batch: BAT-001
|
||||
- Profile: standard
|
||||
- Created: 2026-05-02
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-004 | GET /health 응답 스키마 확장 | Interface | High |
|
||||
| REQ-005 | service-token 발급 시 runtime_id 동시 생성 | Interface | High |
|
||||
| REQ-006 | GET /v1/assets 엔드포인트 구현 | Interface | High |
|
||||
| REQ-007 | GET /v1/user_info 엔드포인트 구현 | Interface | High |
|
||||
| REQ-008 | POST /v1/assets/{asset_id}/ledger/trades 구현 | Interface | High |
|
||||
| REQ-009 | v1 API DB 스키마 migration 추가 | Migration | High |
|
||||
| REQ-010 | credential_payload Fernet 암호화 및 startup validation | Non-Functional | High |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- dcy-002: trex-basic Consumer-Driven Contract 반영
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- GET /health 응답 확장 (service, version, checked_at 필드 추가)
|
||||
- service-token 발급 시 runtime_id UUID v4 동시 생성 및 auth_tokens 저장
|
||||
- GET /v1/assets 엔드포인트 신규 구현 (portfolio → asset 뷰 변환)
|
||||
- GET /v1/user_info 엔드포인트 신규 구현 (runtime_id, trading_enabled, brokers[] 복호화 반환)
|
||||
- POST /v1/assets/{asset_id}/ledger/trades 엔드포인트 신규 구현 (idempotent 체결 기록)
|
||||
- Alembic migration: portfolios.market 컬럼, auth_tokens.runtime_id 컬럼, broker_credentials 테이블, ledger_trades 테이블 추가
|
||||
- Fernet 암복호화 유틸리티 신규 구현 및 startup validation
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs: docs/project-structure.md, docs/runtime-flows.md (design 완료 후 갱신)
|
||||
- Modules:
|
||||
- app/api/health.py (수정)
|
||||
- app/api/auth_routes.py (수정)
|
||||
- app/api/v1_assets.py (신규)
|
||||
- app/api/v1_user_info.py (신규)
|
||||
- app/api/v1_ledger.py (신규)
|
||||
- app/main.py (수정: /v1 라우터 등록)
|
||||
- app/cli/commands/service_tokens.py (수정)
|
||||
- app/core/config.py (수정: CREDENTIAL_ENCRYPTION_KEY 추가)
|
||||
- app/core/encryption.py (신규: Fernet 유틸리티)
|
||||
- app/domain/models.py (수정: AuthToken.runtime_id, BrokerCredential, LedgerTrade 추가)
|
||||
- app/services/auth_service.py (수정)
|
||||
- app/services/ (신규 서비스 메서드)
|
||||
- app/schemas/ (신규 v1 스키마)
|
||||
- app/repositories/postgres.py (수정: 신규 테이블 쿼리)
|
||||
- migrations/versions/ (신규 migration 파일)
|
||||
- Tests:
|
||||
- tests/test_app.py 또는 tests/test_health.py (신규/확장)
|
||||
- tests/test_auth.py (확장)
|
||||
- tests/test_v1_assets.py (신규)
|
||||
- tests/test_v1_user_info.py (신규)
|
||||
- tests/test_v1_ledger.py (신규)
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | draft |
|
||||
| Design | [design.md](./design.md) | draft |
|
||||
| Implementation | [implementation.md](./implementation.md) | draft |
|
||||
| Verification | [verification.md](./verification.md) | draft |
|
||||
@@ -0,0 +1,83 @@
|
||||
# BAT-001 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-001
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-004: GET /health 응답 스키마 확장
|
||||
- REQ-005: service-token 발급 시 runtime_id 동시 생성
|
||||
- REQ-006: GET /v1/assets 엔드포인트 구현
|
||||
- REQ-007: GET /v1/user_info 엔드포인트 구현
|
||||
- REQ-008: POST /v1/assets/{asset_id}/ledger/trades 구현
|
||||
- REQ-009: v1 API DB 스키마 migration 추가
|
||||
- REQ-010: credential_payload Fernet 암호화 및 startup validation
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
구현 선행 조건과 의존성 방향을 기준으로 아래 순서로 진행한다.
|
||||
|
||||
### Out of Scope
|
||||
- REQ-001 (Testing — dcy-001 범위, 별도 batch 예정)
|
||||
- REQ-002 (Migration 정책 문서 — 기 반영됨)
|
||||
- REQ-003 (Documentation — dcy-001 runbook, 별도 처리)
|
||||
- REQ-011 (Documentation — 본 batch 구현 완료 후 별도 task)
|
||||
- broker_credentials 키 로테이션 절차
|
||||
- disabled_reason 동적 계산 (이번 반복은 고정값 "account_inactive")
|
||||
- GET /v1/assets 페이지네이션
|
||||
|
||||
### 1단계: 기반 레이어 (의존 없음)
|
||||
- REQ-010: app/core/encryption.py 신규 구현, app/core/config.py에 CREDENTIAL_ENCRYPTION_KEY startup validation 추가
|
||||
- REQ-009: Alembic migration 파일 작성 (portfolios.market, auth_tokens.runtime_id, broker_credentials, ledger_trades), app/domain/models.py 모델 필드/클래스 추가
|
||||
|
||||
### 2단계: 인증 확장 (REQ-009/010 완료 후)
|
||||
- REQ-005: app/cli/commands/service_tokens.py, app/api/auth_routes.py, app/services/auth_service.py에 runtime_id UUID v4 생성·저장 추가
|
||||
|
||||
### 3단계: v1 엔드포인트 (REQ-009/010/005 완료 후)
|
||||
- REQ-004: app/api/health.py 응답 스키마 확장 (service, version, checked_at 추가), HealthResponse 스키마 추가
|
||||
- REQ-006: app/api/v1_assets.py 신규, 서비스·스키마·레포지터리 메서드 추가, app/main.py /v1 라우터 등록
|
||||
- REQ-007: app/api/v1_user_info.py 신규, BrokerCredential 조회·복호화 서비스·스키마 추가
|
||||
- REQ-008: app/api/v1_ledger.py 신규, LedgerTrade 저장·중복 감지 서비스·스키마 추가
|
||||
|
||||
### 4단계: 테스트
|
||||
- 각 엔드포인트 단위 테스트 작성 및 기존 test_auth.py 확장
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- REQ-009 migration이 REQ-005, 006, 007, 008 구현의 선행 조건 (컬럼·테이블 존재 필요)
|
||||
- REQ-010 암호화 유틸리티가 REQ-007 credential_payload 복호화의 선행 조건
|
||||
- REQ-005 runtime_id 발급이 REQ-007 GET /v1/user_info runtime_id 반환의 선행 조건
|
||||
- app/main.py /v1 라우터 등록은 REQ-006/007/008 세 라우터를 한 번에 처리
|
||||
- External:
|
||||
- CREDENTIAL_ENCRYPTION_KEY 환경변수 (운영 환경 설정 필요)
|
||||
- Docker Compose local-test 환경에서 Alembic upgrade head 선행 필요
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason:
|
||||
- /v1 라우터 구조 및 app/main.py 등록 방식 결정 필요 (prefix, router 분리 또는 통합)
|
||||
- GET /v1/user_info의 R-4 처리 방식 결정 필요 (runtime_id 없는 구형 service-token: 404 또는 즉시 생성)
|
||||
- disabled_reason 고정값 방식 확정 필요 ("account_inactive" 등)
|
||||
- Fernet startup validation 위치 확정 필요 (FastAPI lifespan vs Settings 초기화)
|
||||
- broker_credentials 테이블 상세 컬럼 타입 확정 필요 (NUMERIC vs FLOAT, TIMESTAMPTZ 등)
|
||||
- runtime_id 포맷 확정 필요 (UUID v4 권장, 설계 단계 확정)
|
||||
|
||||
## Milestones
|
||||
|
||||
1. design.md confirmed — 모든 Design Gate 결정 사항 해소
|
||||
2. REQ-009 migration 구현 완료 및 로컬 DB upgrade head 성공
|
||||
3. REQ-010 Fernet 유틸리티 구현 완료 및 startup validation 동작 확인
|
||||
4. REQ-005 runtime_id 발급 구현 완료 및 단위 테스트 통과
|
||||
5. REQ-004/006/007/008 엔드포인트 구현 완료
|
||||
6. 전체 테스트 스위트 통과
|
||||
7. verification.md confirmed
|
||||
|
||||
## Risks
|
||||
|
||||
- R1: GET /v1/user_info R-4 처리(기존 runtime_id 없는 토큰) 미결 — 설계 단계에서 반드시 확정. 404 선택 시 기존 service-token 무효화 영향 범위 검토 필요.
|
||||
- R2: Fernet 키 없는 환경에서 테스트 실행 실패 가능 — 테스트 픽스처에서 환경변수 mock 처리 필요.
|
||||
- R3: broker_credentials 컬럼 타입(특히 numeric 정밀도)을 migration 작성 전 확정하지 않으면 후속 변경 migration이 늘어남.
|
||||
- R4: /v1 라우터 prefix 방식이 기존 /portfolios, /auth 등 라우터와 충돌하지 않는지 app/main.py 구조 확인 필요.
|
||||
@@ -0,0 +1,187 @@
|
||||
# BAT-001 Verification
|
||||
|
||||
- Status: draft
|
||||
- Batch: BAT-001
|
||||
|
||||
## 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 경로 전용 테스트 없음 |
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
## Batch Register
|
||||
|
||||
| BAT ID | Profile | Status | Included REQ | Discovery | Folder |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| BAT-001 | standard | in-delivery | REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010 | dcy-002 | [bat-001_20260502_trex-basic-v1-api-contract](./bat-001_20260502_trex-basic-v1-api-contract/) |
|
||||
Reference in New Issue
Block a user