Files
portfolio-hub/docs/srs/Migration/req-036_portfolio-ledger-consolidation-migration.md
T
genichinandClaude Opus 4.8 2a19b7f058 fix: ledger write idempotency for TimescaleDB hypertable (BAT-016 rework)
BAT-016's initial implementation passed only against mocked repositories and
broke on a real database. Two defects fixed:

1. insert_ledger_trade used `ON CONFLICT (portfolio_id, order_id, execution_id)
   WHERE order_id IS NOT NULL` but portfolio_ledger is a TimescaleDB hypertable,
   which cannot have that partial unique index (must include the partitioning
   column recorded_at). The statement errored against Postgres. Replaced with
   application-level idempotency: SELECT-before-insert inside the existing
   `SELECT ... FOR UPDATE` portfolio row lock, which serializes concurrent
   same-portfolio writes (race-safe). Duplicate returns current authoritative
   portfolio state (dcy-006 OQ-4 b).

2. `with self._connect() as conn:` closes the shared psycopg3 connection on exit,
   leaving the reused repository connection unusable. Switched to
   `with conn.transaction():` (commits without closing).

Added tests/test_ledger_consolidation_integration.py — real-Postgres integration
tests (new / duplicate / negative-balance) that close the mock-only verification
gap. Full suite: 376 passed.

SDLC: REQ-036/037 CHG-20260605-03 reconcile OQ-7 (ii) (DB partial unique →
application-level idempotency) since the hypertable makes the DB constraint
infeasible. REQ-036/037/038 → Implemented; BAT-016 → release-candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:11:17 +09:00

11 KiB

REQ-036: portfolio_ledger 회계 원장 통합 마이그레이션 (ledger_trades 흡수/DROP)

  • Status: Implemented
  • Type: Migration
  • Priority: High
  • Owner: Justin Ko

Intent

  • 두 ledger 테이블(portfolio_ledger 회계 원장 + ledger_trades broker audit)이 공존하는 구조를 portfolio_ledger 단일 회계 원장으로 통합한다(dcy-006 OQ-6 (iv)).
  • ledger_trades의 broker idempotency/식별 필드를 portfolio_ledger에 흡수하고 ledger_trades 테이블을 삭제해, 신 path(POST /v1/portfolios/{id}/ledger/trades)가 단일 회계 원장에 기록할 수 있는 스키마 기반을 마련한다.

Requirement

  • Alembic 신규 revision을 작성한다. (head는 dcy-005 20260601_0024 다음)
  • portfolio_ledger에 컬럼을 추가한다:
    • ledger_entry_id BIGINT GENERATED BY DEFAULT AS IDENTITY (CHG-20260605-03: hypertable 제약상 단독 PRIMARY KEY 불가 — Identity 컬럼, 비-PK)
    • order_id TEXT NULL, execution_id TEXT NULL (dcy-006 OQ-7 (ii): NULL 허용)
    • side TEXT NOT NULL
    • executed_at TIMESTAMPTZ NOT NULL
  • idempotency는 DB partial UNIQUE index가 아니라 application layer(REQ-037)에서 보장한다 (CHG-20260605-03). portfolio_ledger는 TimescaleDB hypertable(파티션 컬럼 recorded_at)이라 (portfolio_id, order_id, execution_id) partial unique index/PK 생성이 불가능하다(cannot create a unique index without the column recorded_at). 따라서 본 마이그레이션은 broker 식별 컬럼만 추가하고 unique 제약은 만들지 않는다. broker write dedup은 REQ-037의 SELECT ... FOR UPDATE 락 하 SELECT-before-insert로 처리.
  • 수치 컬럼 타입을 DOUBLE PRECISION → NUMERIC(20,8)로 변경한다: price, quantity, fee_amount, tax_amount, cash_balance_after, asset_quantity_after, total_value_after, profit_buffer_after.
  • 기존 action 컬럼을 제거하고 side로 통일한다. backfill 매핑: action='BUY' → side='buy', action='SELL' → side='sell'. 그 외 자유형식 action 값의 처리(R-6)는 마이그레이션 본문에서 분포 확인 후 결정(개발 워크스페이스는 분포 단순).
  • 기존 데이터 backfill: recorded_at → executed_at 복사, order_id/execution_id는 NULL(synthetic 생성 불요).
  • 추가 인덱스 idx_portfolio_ledger_portfolio_executed (portfolio_id, executed_at) 신설. 기존 idx_portfolio_ledger_portfolio_time (portfolio_id, recorded_at DESC)은 유지.
  • ledger_trades 테이블을 DROP한다. (본 워크스페이스 미생성이라 DROP TABLE IF EXISTS로 안전 처리. prod 데이터 보존 필요 시 backfill은 release 단계 별도 검토.)
  • app/repositories/postgres.py의 ensure_schema() 내 portfolio_ledger 정의도 신 스키마와 동일하게 갱신한다.

Acceptance Criteria

  1. Given 신규 Alembic revision 적용 후, When \d portfolio_ledger 확인, Then ledger_entry_id(Identity, 비-PK — CHG-20260605-03), order_id(NULL 허용), execution_id(NULL 허용), side(NOT NULL), executed_at(NOT NULL) 컬럼이 존재한다.
  2. Given AC-1 환경, When information_schema로 수치 컬럼 타입 확인, Then price/quantity/fee_amount/tax_amount/cash_balance_after/asset_quantity_after/total_value_after/profit_buffer_after가 모두 NUMERIC(20,8)이다.
  3. (CHG-20260605-03 재정의) Given AC-1 환경, When portfolio_ledger 인덱스/제약 확인, Then (portfolio_id, order_id, execution_id) 기반 partial unique index/PK는 존재하지 않는다(hypertable 제약). broker write idempotency는 REQ-037 application layer에서 보장되며, 실 DB 회귀(tests/test_ledger_consolidation_integration.py::test_duplicate_broker_write_is_idempotent)로 검증한다.
  4. Given AC-1 환경, When action 컬럼 존재 확인, Then action 컬럼은 존재하지 않고 side 컬럼으로 대체되어 있다.
  5. Given AC-1 환경, When \d ledger_trades 또는 테이블 목록 조회, Then ledger_trades 테이블이 존재하지 않는다.
  6. Given 기존 portfolio_ledger 데이터(있는 경우), When 마이그레이션 적용, Then executed_at은 recorded_at 값으로 채워지고 order_id/execution_id는 NULL이며 side는 기존 action에서 매핑된다.
  7. Given ensure_schema() 단독 실행, When portfolio_ledger 스키마 확인, Then 마이그레이션과 동일한 컬럼/제약/타입이 반영된다.

Impacted Area

  • Docs:
    • docs/data-model.md — portfolio_ledger entity에 신규 컬럼(ledger_entry_id Identity 비-PK, order_id/execution_id NULL, side, executed_at) + 정밀도 NUMERIC(20,8) 명세 추가. idempotency는 application layer(REQ-037)이며 DB partial unique index/PK는 hypertable 제약상 없음을 명시. ledger_trades entity 섹션 제거. 두 ledger 테이블 → 단일 원장 통합 명시.
  • Modules:
    • migrations/versions/ — 신규 revision (예: 20260605_NNNN_portfolio_ledger_consolidation.py). down_revision = 20260601_0024.
    • app/repositories/postgres.py:263-282 — ensure_schema() portfolio_ledger 정의 갱신
  • Tests:
    • 신규/갱신 마이그레이션 회귀 (alembic upgrade head 후 schema 검증)
    • 기존 tests/test_v1_portfolios_ledger.py 등 ledger 의존 테스트가 신 스키마와 정합하도록 갱신 (REQ-037과 연동)

Notes

  • 본 REQ는 dcy-006 FR-9/FR-10/FR-11/FR-12 산출물 + FR-5(data-model.md) 일부 흡수.
  • REQ-037(신 path 회계 책임 + 응답)의 선행 — 본 마이그레이션이 스키마 기반을 제공한다.
  • idempotency(broker write만 dedup, 레거시 write append-only)는 dcy-006 OQ-7 (ii) 결정이나, hypertable 제약으로 DB partial unique가 불가하여 application layer(REQ-037)에서 구현(CHG-20260605-03).
  • R-5(DOUBLE→NUMERIC 정밀도)/R-6(action→side 매핑 누락)/R-7(DROP 트랜잭션)는 prod 마이그레이션 시 별도 검토. 본 워크스페이스는 개발용이라 영향 미미(workspace_purpose 메모리).
  • 별도 REL(REL-003 등)로 분리 배포(dcy-006 A-4).
  • Source: dcy-006 FR-9~12, OQ-6 (iv), OQ-7 (ii).

Change Log

CHG-20260605-04

  • Date: 2026-06-05
  • Author: Justin Ko

Change Summary

  • Status: Approved → Implemented

Intent

  • BAT-016 confirm-batch-verification 완료. portfolio_ledger 통합 마이그레이션 + ensure_schema 갱신 + ledger_trades DROP 구현. AC-1/2/4/5/6/7 검증, AC-3는 CHG-20260605-03 재정의(application-layer idempotency)로 실 DB 통합 테스트(test_duplicate_broker_write_is_idempotent)로 검증. 전체 376 passed.

Acceptance Criteria Delta

  • No change

Impacted Area Delta

  • No change

Delivery Trace Delta

  • Batches:
    • BAT-016 (release-candidate)
  • Releases:
    • REL-003 (예정)

Revalidation Impact

  • Reverification Needed: no
  • Existing Implementation Invalidated: no
  • Status Recommendation: implemented
  • Follow-up Action: REL-003 staging에서 fresh-DB alembic 적용 + DOUBLE→NUMERIC 정밀도/action 분포 최종 검증

Validation Plan

  • 실 DB 통합 테스트 + alembic schema 검증 (완료); prod staging은 release 단계

CHG-20260605-03

  • Date: 2026-06-05
  • Author: Justin Ko

Change Summary

  • AC/Requirement 재정의: portfolio_ledger가 TimescaleDB hypertable임이 BAT-016 verify에서 확인되어, dcy-006 OQ-7 (ii)의 "DB partial UNIQUE index + ledger_entry_id PK"가 물리적으로 불가능(cannot create a unique index without the column recorded_at). idempotency를 application layer(REQ-037 SELECT-before-insert under FOR UPDATE)로 이관. Status는 Approved 유지(구현 진행 중 설계 정합 보정).

Intent

  • 설계(DB 제약 idempotency)와 hypertable 현실의 충돌 해소. 마이그레이션은 broker 컬럼만 추가하고 unique 제약/PK는 생성하지 않으며, dedup은 application layer가 담당하도록 정합화.

Acceptance Criteria Delta

  • Updated:
    • AC-1: ledger_entry_id(PK) → ledger_entry_id(Identity, 비-PK)
    • AC-3: "partial UNIQUE index 존재" → "partial unique index/PK 미존재(hypertable 제약), idempotency는 REQ-037 application layer + 실 DB 통합 테스트로 검증"

Impacted Area Delta

  • Modules: 변경 없음(이미 신규 revision + ensure_schema 지정). 마이그레이션 본문에서 partial unique index 생성 제거 반영.
  • Tests: tests/test_ledger_consolidation_integration.py(실 DB) 추가 — idempotency 검증 이관.

Delivery Trace Delta

  • Batches: BAT-016 (in-delivery)
  • Releases: none (REL-003 등 분리 예정)

Revalidation Impact

  • Reverification Needed: yes (BAT-016 verification은 실 DB 통합 테스트로 재수행)
  • Existing Implementation Invalidated: no (구현이 본 재정의에 맞춰 수정됨)
  • Status Recommendation: keep-current (Approved 유지)
  • Follow-up Action: BAT-016 verification 실 DB evidence 반영

Validation Plan

  • alembic schema 검증(AC-1/2/4/5/6/7) + 실 DB idempotency 통합 테스트(AC-3 대체)

CHG-20260605-02

  • Date: 2026-06-05
  • Author: Justin Ko

Change Summary

  • Status: Proposed → Approved

Intent

  • 승인 게이트 통과: 필수 필드(Status/Type/Priority/Owner/Intent/Requirement/AC/Impacted Area/Change Log) 모두 충족, AC-1~7 구체성(DB schema 검증) 확인 완료, blocker 없음.

Acceptance Criteria Delta

  • No change

Impacted Area Delta

  • No change

Delivery Trace Delta

  • Batches: none (미정)
  • Releases: none (미정 — REL-003 등 분리 예정)

Revalidation Impact

  • Reverification Needed: no
  • Existing Implementation Invalidated: no
  • Status Recommendation: promote-to-approved
  • Follow-up Action: batch 계획 편입 (BAT-016 후보, REQ-037/038 선행)

CHG-20260605-01

  • Date: 2026-06-05
  • Author: Justin Ko

Change Summary

  • 최초 생성 — dcy-006 confirm-discovery 후 draft-req 실행. dcy-006 FR-9~12 산출물.

Intent

  • portfolio_ledger/ledger_trades 두 ledger 테이블을 portfolio_ledger 단일 회계 원장으로 통합하는 스키마 마이그레이션 정의.

Acceptance Criteria Delta

  • Added:
    • AC-1~7 최초 작성

Impacted Area Delta

  • Docs: docs/data-model.md — 최초 지정
  • Modules: migrations/versions/, app/repositories/postgres.py — 최초 지정
  • Tests: 마이그레이션 회귀, tests/test_v1_portfolios_ledger.py — 최초 지정

Delivery Trace Delta

  • Batches: none (미정)
  • Releases: none (미정 — REL-003 등 분리 예정)

Revalidation Impact

  • Reverification Needed: no
  • Existing Implementation Invalidated: no
  • Status Recommendation: keep-current
  • Follow-up Action: confirm-req 후 batch 계획 편입; staging에서 DOUBLE→NUMERIC + action→side backfill dry-run

Validation Plan

  • alembic upgrade head 후 schema 검증(AC-1~7); 개발 DB에서 ensure_schema 정합 확인