474 lines
37 KiB
Markdown
474 lines
37 KiB
Markdown
# Interface Contract — Dashboard 관리자 인증
|
|
|
|
## 목적과 범위
|
|
|
|
Dashboard 인증 설정, login/cleanup/logout HTTP route, signed cookie와 Streamlit gate의 외부·내부 계약을 정의한다.
|
|
|
|
- 포함 범위: 환경변수, HTTPS form POST, cookie 발급·삭제, fail-closed gate
|
|
- 제외 범위: MCP 인증, OAuth/OIDC, 다중 사용자, DB session/token registry, per-token revocation
|
|
- 사용 주체: browser, Dashboard ASGI server, Streamlit app, 운영 배포
|
|
|
|
## 계약
|
|
|
|
### auth-config — 서버 인증 설정
|
|
|
|
- 책임: 현재 configured credential과 signing secret을 server-only로 제공한다.
|
|
- 종류·프로토콜: process environment
|
|
- 호출 대상·operation: `load_auth_settings()`
|
|
- 버전: 2.0
|
|
- 호출 주체: Dashboard server
|
|
- 인증 방식: 없음
|
|
- 필요 권한: process secret read
|
|
|
|
#### 입력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|---|
|
|
| `JARCA_DASHBOARD_ADMIN_ID` | environment | `string` | 필수 | 없음 | non-empty | configured admin identifier | 민감 |
|
|
| `JARCA_DASHBOARD_ADMIN_PASSWORD` | environment | `string` | 필수 | 없음 | non-empty | configured credential | 민감 |
|
|
| `JARCA_DASHBOARD_SIGNING_SECRET` | environment | `string` | 필수 | 없음 | UTF-8 32 byte 이상 | signing input | 민감 |
|
|
|
|
#### 출력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|
|
|
| `AuthSettings` | server memory | `object` | 성공 시 필수 | current configured values | verifier input; never serialized | 민감 |
|
|
|
|
#### 오류
|
|
|
|
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
|
|---|---|---|---|---|---|
|
|
| `configuration_unavailable` | configuration | missing/empty/read error/short secret | 수정 후 가능 | generic unavailable | no cookie, no DB |
|
|
|
|
### login — `POST /api/auth/login`
|
|
|
|
- 책임: same-origin HTTPS credential을 검증하고 signed cookie를 발급한다.
|
|
- 종류·프로토콜: HTTPS form POST
|
|
- 호출 대상·operation: `/api/auth/login`
|
|
- 버전: 1.0
|
|
- 호출 주체: browser login form
|
|
- 인증 방식: submitted credential, server-only comparison
|
|
- 필요 권한: same-origin HTTPS
|
|
|
|
#### 입력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|---|
|
|
| `admin_id` | form body | `string` | 필수 | 없음 | bounded form value | submitted ID | 민감 |
|
|
| `admin_password` | form body | `string` | 필수 | 없음 | bounded form value | submitted password | 민감 |
|
|
| `remember_me` | form body | `string` | 선택 | off | `on` or absent | choose 30-day TTL | 비민감 |
|
|
| `Origin` | request header | `URL` | 필수 | 없음 | request `Host`와 scheme·hostname·effective port가 같은 HTTPS origin | CSRF origin 검증 | 비민감 |
|
|
|
|
#### 출력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|
|
|
| `Set-Cookie` | response header | `cookie` | success | hardened auth cookie, redacted value | persistent auth | 값은 민감 |
|
|
| `Location` | response header | `string` | success/failure | `/` or generic error path | browser navigation | 비민감 |
|
|
|
|
발급 token은 `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti` claim만 가진다. `sub=dashboard-admin`은 관리자 ID가 아닌 고정 비민감 role이고, token 값과 서명은 민감하므로 browser의 hardened cookie에만 저장한다. 성공한 login 시각에 `iat`와 `exp`를 한 번 고정하며 기본 TTL은 24시간, `remember_me=on`이면 30일이다. 활동, 일반 요청, rerun 또는 인증 복원은 token을 재발급하거나 두 시각을 연장하지 않는다.
|
|
|
|
#### 오류
|
|
|
|
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
|
|---|---|---|---|---|---|
|
|
| `400` | request | malformed/oversized body | 수정 후 가능 | resubmit valid form | no cookie |
|
|
| `403` | origin | non-HTTPS or cross-origin | 수정 후 가능 | use same-origin HTTPS | no cookie |
|
|
| `invalid` | authentication | ID/password mismatch | 가능 | generic error and delay | no cookie |
|
|
| `unavailable` | configuration | invalid server configuration | config fix 후 | generic unavailable | no cookie |
|
|
|
|
### cleanup — `GET /api/auth/cleanup`
|
|
|
|
- 책임: invalid cookie를 matching scope로 삭제한 뒤 unauthenticated navigation을 완료한다.
|
|
- 종류·프로토콜: HTTPS navigation GET
|
|
- 호출 대상·operation: `/api/auth/cleanup`
|
|
- 버전: 1.0
|
|
- 호출 주체: same-origin browser/auth gate
|
|
- 인증 방식: same-origin HTTPS `Referer`; token validity 불필요
|
|
- 필요 권한: same-origin HTTPS
|
|
|
|
#### 입력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|---|
|
|
| `Referer` | request header | `URL` | 필수 | 없음 | request origin과 같은 HTTPS authority | navigation origin 검증 | 비민감 |
|
|
|
|
#### 출력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|
|
|
| `Set-Cookie` | response header | `cookie` | always | same name/path/security, `Max-Age=0` | delete stale cookie | 값은 민감 |
|
|
| `Location` | response header | `string` | success | `/` | unauthenticated page | 비민감 |
|
|
|
|
#### 오류
|
|
|
|
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
|
|---|---|---|---|---|---|
|
|
| `403` | origin | non-HTTPS/cross-origin | 가능 | retry same-origin | no DB |
|
|
|
|
### logout — `POST /api/auth/logout`
|
|
|
|
- 책임: explicit logout으로 browser cookie를 삭제한다.
|
|
- 종류·프로토콜: HTTPS form POST
|
|
- 호출 대상·operation: `/api/auth/logout`
|
|
- 버전: 1.0
|
|
- 호출 주체: authenticated browser
|
|
- 인증 방식: same-origin HTTPS; server-side revocation 없음
|
|
- 필요 권한: same-origin HTTPS
|
|
|
|
#### 입력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|---|
|
|
| `none` | none | `none` | 선택 | 없음 | no credential accepted | explicit logout | 없음 |
|
|
| `Origin` | request header | `URL` | 필수 | 없음 | request `Host`와 scheme·hostname·effective port가 같은 HTTPS origin | CSRF origin 검증 | 비민감 |
|
|
|
|
#### 출력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|
|
|
| `Set-Cookie` | response header | `cookie` | always | matching deletion, `Max-Age=0` | remove browser cookie | 값은 민감 |
|
|
| `Location` | response header | `string` | success | `/` | unauthenticated page | 비민감 |
|
|
|
|
#### 오류
|
|
|
|
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
|
|---|---|---|---|---|---|
|
|
| `403` | origin | non-HTTPS/cross-origin | 가능 | retry same-origin | no DB |
|
|
|
|
### gate — Dashboard 인증 경계
|
|
|
|
- 책임: config and cookie verification before any protected work.
|
|
- 종류·프로토콜: Python/Streamlit call
|
|
- 호출 대상·operation: `require_dashboard_authentication()`
|
|
- 버전: 2.0
|
|
- 호출 주체: `dashboard.app.main()`
|
|
- 인증 방식: current config plus signed cookie
|
|
- 필요 권한: valid token with `sub=dashboard-admin`
|
|
|
|
#### 입력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|---|
|
|
| `auth_cookie` | request cookie | `string` | success path | absent | current configuration으로 서명을 검증할 수 있고 claim이 정확히 `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti`인 token | browser proof | 값과 서명은 민감; `sub`는 비민감 |
|
|
|
|
#### 출력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|
|
|
| `authenticated` | server result | `bool` | always | true/false | gate decision | 비민감 |
|
|
|
|
#### 오류
|
|
|
|
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
|
|---|---|---|---|---|---|
|
|
| `unauthenticated` | authentication | absent/invalid/expired/config failure | login 가능 | cleanup then show login | no protected work |
|
|
|
|
관리자 ID, password 또는 signing secret 변경은 server-only signing key binding을 바꾸므로 기존 token을 모두 무효화하고 cleanup으로 전이한다. DB session, token registry와 per-token revocation은 만들지 않는다.
|
|
|
|
## 호환성
|
|
|
|
- 유지할 기존 호출 방식: authenticated `page` query parameter, page dispatch, gateway와 MCP behavior
|
|
- 버전 변경 정책: REQ-001 revision 2.0; session-only boolean을 signed cookie로 대체
|
|
- 폐기 정책: client-side token storage, DB-backed session, per-token revocation 없음
|
|
- 재시도 계약: credential 실패는 최소 1초 지연; origin/request 오류는 수정 후 재시도
|
|
- 중복 처리 계약: login은 새 token 발급 가능; cleanup/logout deletion은 idempotent
|
|
- 순서 보장: config → cookie verify → cleanup if invalid → DB/page dispatch only if valid
|
|
- 만료 보장: login 성공 시 고정한 `iat`와 `exp`는 활동·일반 요청·rerun·복원에서 변경하거나 연장하지 않음
|
|
|
|
## 예시
|
|
|
|
### example-login — 정상 login
|
|
|
|
- 대상 인터페이스: `login`
|
|
|
|
#### 호출
|
|
|
|
```text
|
|
POST /api/auth/login
|
|
Origin: https://dashboard.example
|
|
admin_id=<submitted>&admin_password=<submitted>&remember_me=on
|
|
```
|
|
|
|
#### 결과
|
|
|
|
```text
|
|
303 Location: /
|
|
Set-Cookie: __Host-jarca_dashboard_auth=<redacted>; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000
|
|
```
|
|
|
|
### example-cleanup — invalid cookie cleanup
|
|
|
|
- 대상 인터페이스: `cleanup`
|
|
|
|
#### 호출
|
|
|
|
```text
|
|
GET /api/auth/cleanup
|
|
Referer: https://dashboard.example/
|
|
```
|
|
|
|
#### 결과
|
|
|
|
```text
|
|
303 Location: /
|
|
Set-Cookie: __Host-jarca_dashboard_auth=; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Path=/
|
|
```
|
|
|
|
## 검증
|
|
|
|
- 정상 계약: authentication/server tests와 loopback HTTPS evidence
|
|
- 실패 계약: wrong credential, invalid configuration, cross-origin, malformed cookie tests
|
|
- 경계 계약: expiry, rotation, oversized token, same-host different-port origin tests
|
|
- 구현·스키마·예시 정합성: `tests/test_dashboard_auth_server.py`, `tests/test_dashboard_authentication_app.py`, `tests/test_dashboard_persistent_authentication.py`, source/document redaction search
|
|
|
|
## Dashboard 메뉴 전환 계약 — REQ-002
|
|
|
|
### dashboard-menu-transition
|
|
|
|
- 책임: authenticated Dashboard에서 공통 shell/background를 유지하고 선택된 page body만 교체한다.
|
|
- 종류·프로토콜: Streamlit rerun, `st.radio` callback, query parameter
|
|
- 호출 대상·operation: `dashboard.app.main()` → `render_sidebar_nav()` → `render_dashboard_body()`
|
|
- 버전: 1.0
|
|
- 호출 주체: authenticated browser
|
|
- 인증 방식: 기존 `gate` 계약 선행; 메뉴는 인증을 우회하지 않음
|
|
- 필요 권한: valid Dashboard auth cookie
|
|
|
|
#### 상태와 입력
|
|
|
|
정확한 menu options/order는 `월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`이다. selected value는 `st.session_state["dashboard_page"]`에 둔다. 직접 query의 `page`가 known option이면 해당 page를, absent/unknown이면 `요약`을 사용한다.
|
|
|
|
#### 출력·불변조건
|
|
|
|
- callback은 `st.query_params["page"]`만 갱신하고 `month`, `account_id`, `filter` 등 sibling key를 보존한다.
|
|
- menu click은 document/path navigation이 아니며 stable `st.empty` body container 안에서 target renderer를 실행한다.
|
|
- 공통 sidebar, refresh control, CSS/background는 유지하고 body만 target content로 대체한다.
|
|
- 느린 target에는 `<target> 불러오는 중…`을 표시한 뒤 renderer 완료 시 placeholder를 제거한다.
|
|
- renderer 예외는 body 안의 `페이지를 불러오지 못했습니다. 다시 시도해 주세요.`와 retry control로 대체하며 예외 상세를 노출하지 않는다.
|
|
- serialized callback이 여러 번 실행되면 마지막 selection이 query와 body의 최종 상태가 된다.
|
|
|
|
#### 오류
|
|
|
|
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
|
|---|---|---|---|---|---|
|
|
| `page_fallback` | request | missing/unknown `page` | 가능 | `요약` 표시 | sibling query 유지 |
|
|
| `renderer_unavailable` | rendering | target renderer exception | 가능 | generic error의 retry 클릭 | previous body 제거, auth/shell 유지 |
|
|
|
|
#### 검증 경계
|
|
|
|
AppTest는 callback, query mapping, synthetic renderer output과 error/loading event를 검증하지만 실제 browser의 document navigation 부재, paint timing, focus/scroll, network request 또는 perceived shell continuity를 증명하지 않는다. 따라서 `REQ-002.03-AC01`은 authenticated real-browser evidence가 필수이며 AppTest는 companion evidence일 뿐이다. history/back-forward와 path URL은 계약에서 제외한다.
|
|
|
|
상세 acceptance target은 `docs/requirements/req-002_dashboard-menu-transition.md`의 `REQ-002.01-AC01`부터 `REQ-002.09-AC01`까지를 따른다.
|
|
|
|
## 계좌 평가·자산 마감 계약 — REQ-003
|
|
|
|
### valuation-service
|
|
|
|
- 책임: `register_account_valuation()`, `list_account_valuations()`, `select_account_valuation()`, `correct_account_valuation()`을 통해 exact/as_of verified leaf와 append-only history를 제공한다.
|
|
- 종류·프로토콜: Python service function; `jarca_ledger_mcp.service` re-export
|
|
- 호출 대상·operation: `jarca_ledger_mcp.application.account_valuation_service`
|
|
- 버전: additive v1
|
|
- 호출 주체: Dashboard accounts gateway, MCP tool, import/account write service
|
|
- 인증 방식: service layer 자체 MCP auth는 주장하지 않음; Dashboard는 기존 `gate` 선행
|
|
- 필요 권한: caller가 DB path와 approval context를 제공
|
|
|
|
#### 입력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|---|
|
|
| `account_id`/`basis_date`/`amount` | function/MCP | `string/string/int` | register 필수 | 없음 | account id, YYYY-MM-DD, exact integer | valuation identity/value | 아니오 |
|
|
| `currency`/`valuation_kind`/`source_type`/`verification_status` | function/MCP | `string` | 선택 | KRW/balance/manual/unverified | canonical enums listed below | 아니오 |
|
|
| `idempotency_key` | function/MCP | `string` | 선택 | `''` | stable caller key | retry identity | 아니오 |
|
|
| `as_of_at`/`timezone`, optional metrics, loan definition | function/MCP | nullable string/int/float | 선택 | NULL | source validation; loan definition required | evidence metrics | 아니오 |
|
|
|
|
#### 출력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|
|
|
| `success` | envelope | `bool` | 항상 | true/false | outcome | 아니오 |
|
|
| `valuation`/`valuations` | result | object/list | 성공 시 | persisted rows | history/evidence | source_ref may be sensitive |
|
|
| `reused`, `carried_forward`, `warning`, `unverified_candidates` | result | bool/string/list | 조건부 | additive | idempotency/selection evidence | 아니오 |
|
|
|
|
#### 오류
|
|
|
|
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
|
|---|---|---|---|---|---|
|
|
| `validation_error` | request | invalid date/type/currency/loan definition | 수정 후 가능 | payload 수정 | no write; no row or cache mutation |
|
|
| `not_found` | lookup | account/valuation missing | 수정 후 가능 | id 확인 | 없음 |
|
|
| `idempotency_conflict` | conflict | same key different payload | 아니오 | new key or inspect | 없음 |
|
|
| `correction_conflict`/`ambiguous_valuation` | conflict | second child/multiple leaves | 수동 검토 | correct evidence | 없음 |
|
|
|
|
### snapshot-close
|
|
|
|
- 책임: `preview_asset_snapshot_close()` is read-only; `close_asset_snapshot()` writes an immutable run and rows.
|
|
- 종류·프로토콜: Python service, Streamlit `assets_gateway`, MCP tools
|
|
- 호출 대상·operation: `jarca_ledger_mcp.application.assets_service`
|
|
- 버전: additive v1
|
|
- 호출 주체: authenticated Dashboard operator or MCP caller
|
|
- 인증 방식: Dashboard existing auth gate; MCP authentication is outside this contract
|
|
- 필요 권한: close conditional approval fields when warnings require it
|
|
|
|
#### 입력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|---|
|
|
| `basis_date`/`selection_policy` | function/MCP/form | `string/string` | basis optional | today/exact | YYYY-MM-DD, exact/as_of | close scope | 아니오 |
|
|
| `idempotency_key`/`correction_reason` | function/MCP/form | `string` | conditional | empty | stable key; reason on re-close | retry/correction | 아니오 |
|
|
| `allow_missing_exclusions`/`allow_as_of_carry_forward` | function/MCP | `bool` | optional | false | explicit flags | conditional approval need | 아니오 |
|
|
| `approved_by`/`approval_reason`/`approval_reference` | function/MCP/form | `string` | conditional | empty | all required together | approval evidence | 아니오 |
|
|
|
|
#### 출력
|
|
|
|
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
|
|---|---|---|---|---|---|---|
|
|
| `source`/`is_closed_snapshot`/`snapshot_run_id` | result | string/bool/string | success | live_authority/snapshot | preview vs close | 아니오 |
|
|
| `close_status`/`selection_policy`/`warnings` | result | string/string/list | success | complete_exact/incomplete_draft/conditional | authority state | 아니오 |
|
|
| `total_assets`/`total_liabilities`/`net_worth` | result | integer | success | exact integer | formula summary | 아니오 |
|
|
|
|
#### 오류
|
|
|
|
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
|
|---|---|---|---|---|---|
|
|
| `conditional_approval_required` | policy | unsafe exclusions/carry-forward without explicit approval | 수정 후 가능 | preview, approve explicitly | 없음 |
|
|
| `idempotency_conflict` | conflict | close key payload differs | 아니오 | inspect run | 없음 |
|
|
| `validation_error` | request | invalid policy or unreasoned correction | 수정 후 가능 | provide reason | no partial rows |
|
|
|
|
#### Canonical valuation values and validation
|
|
|
|
The public contract explicitly supports these exact values: source_type: `manual`, `import`, `account_write`, `reconciliation`, `snapshot`; verification_status: `verified`, `unverified`, `rejected`; valuation_kind: `balance`, `cash_balance`, `total_value`, `loan`, `loan_balance`; representative_liability_definition: `principal_only`, `principal_interest_fees`. These are the currently supported enums, not a delegation to source code.
|
|
|
|
loan accounts require representative_liability_definition for `loan`, `loan_balance`, and `balance` aliases. Those are the only accepted valuation kinds for loan accounts; `cash_balance` and `total_value` do not represent a loan liability and are rejected. legacy valuation-kind aliases remain accepted and map to the canonical purpose; alias acceptance does not broaden the canonical enum set. Invalid values, missing loan definitions, and incompatible definitions return `validation_error` and perform no write. A validation failure returns `validation_error` and performs no write. Converting an existing non-loan account to `loan` is rejected with `validation_error` if any attached valuation lacks an explicit valid definition; the account and valuation history remain unchanged. Conversion with no valuations remains allowed.
|
|
|
|
loan valuations map to liabilities and non-loan valuations map to assets. The output mapping preserves the existing envelopes and signatures: `success` remains the outcome field, failures include `error_code` and `error`, and validation or conflict failures do not partially write.
|
|
|
|
Account current-cache writes keep `last_balance_basis_date` (optional `YYYY-MM-DD`) separate from `last_synced_at` (execution/sync timestamp). `create_ledger_account` keeps the established positional `db_path` argument and adds `last_balance_basis_date` after it as an optional keyword. `update_ledger_account` accepts the same field through its existing keyword-field boundary. A balance with no explicit basis advances only the current display cache, leaves the basis unknown, and creates no historical candidate. A dated older non-loan balance appends historical evidence but cannot roll back a newer current cache. loan cache writes without an explicit liability definition do not append a historical candidate; they preserve current-cache and import behavior without inferring a definition. Import and account writers use deterministic candidate keys.
|
|
|
|
### shared contract and adapter limits
|
|
|
|
All new interfaces retain the existing `success`/`error_code` envelope. `preview_asset_snapshot_close()` and Dashboard render/report are read-only; only explicit register/update/close forms or MCP calls write. MCP tools are `register_account_valuation`, `list_account_valuations`, `select_account_valuation`, `correct_account_valuation`, `preview_asset_snapshot_close`, `close_asset_snapshot`, `get_asset_snapshot_report`, and `get_net_worth_timeseries`; actual decorators are in `jarca_ledger_mcp/adapters/mcp/server.py`. Dashboard uses `accounts_gateway.py`, `assets_gateway.py`, and `page_views/accounts.py`/`assets.py`.
|
|
|
|
## 호환성 추가 규칙 — REQ-003
|
|
|
|
- 유지할 기존 호출 방식: REQ-001 auth config/login/cleanup/logout/gate, REQ-002 exact 10 menu labels/order, `page` query sibling preservation, stable shell/background and browser evidence; existing MCP stdio/streamable-http runtime.
|
|
- 버전 변경 정책: additive fields only; no rename/removal of existing response keys.
|
|
- 폐기 정책: 없음; guessed migration, fake transactions and MCP auth claim are excluded.
|
|
- 재시도 계약: same idempotency payload reuses; changed payload conflicts; DB conflict does not retry blindly.
|
|
- 중복 처리 계약: valuation/run idempotency keys and single correction indexes.
|
|
- 순서 보장: Dashboard auth → DB path → page/gateway; preview → explicit approval → atomic close; selection → report.
|
|
|
|
## 예시 추가
|
|
|
|
### example-valuation — synthetic verified valuation
|
|
|
|
- 대상 인터페이스: `valuation-service`
|
|
|
|
#### 호출
|
|
|
|
```text
|
|
register_account_valuation(account_id="acct-synthetic", basis_date="2026-06-30", amount=1000000, verification_status="verified", idempotency_key="fixture-v1")
|
|
```
|
|
|
|
#### 결과
|
|
|
|
```text
|
|
{"success": true, "reused": false, "valuation": {"basis_date": "2026-06-30", "amount": 1000000, "verification_status": "verified"}}
|
|
```
|
|
|
|
### example-preview — read-only close preview
|
|
|
|
- 대상 인터페이스: `snapshot-close`
|
|
|
|
#### 호출
|
|
|
|
```text
|
|
preview_asset_snapshot_close(basis_date="2026-06-30", selection_policy="exact")
|
|
```
|
|
|
|
#### 결과
|
|
|
|
```text
|
|
{"success": true, "source": "live_authority", "is_closed_snapshot": false, "close_status": "complete_exact", "warnings": []}
|
|
```
|
|
|
|
## 검증 추가
|
|
|
|
- 정상 계약: `tests/test_account_valuations.py`, `tests/test_asset_snapshots.py`, `tests/test_contract_regression.py`
|
|
- 실패 계약: idempotency/correction/conditional approval tests and source validation
|
|
- 경계 계약: exact/as_of, NULL/0, future/unverified candidate, correction/re-close
|
|
- 구현·스키마·예시 정합성: `tests/test_requirement_docs.py`, actual service/MCP/gateway symbols
|
|
|
|
|
|
### canonical implementation signatures and fields
|
|
|
|
The application contracts are these actual signatures (defaults included):
|
|
|
|
```python
|
|
register_account_valuation(account_id: str, basis_date: str, amount: int, currency: str = "KRW", valuation_kind: str = "balance", source_type: str = "manual", source_ref: str = "", verification_status: str = "unverified", memo: str = "", idempotency_key: str = "", as_of_at: str | None = None, timezone: str | None = None, cost_basis: int | None = None, net_contributed: int | None = None, reported_principal_kind: str | None = None, reported_principal_label: str | None = None, profit: int | None = None, return_rate: float | None = None, return_basis: str | None = None, principal: int | None = None, interest: int | None = None, fee: int | None = None, representative_liability_definition: str | None = None, db_path: str | None = None, supersedes_id: str | None = None, correction_reason: str = "") -> dict[str, Any]
|
|
list_account_valuations(account_id: str, include_rejected: bool = True, db_path: str | None = None) -> dict[str, Any]
|
|
select_account_valuation(account_id: str, basis_date: str, as_of: bool = False, currency: str | None = None, valuation_kind: str | None = None, db_path: str | None = None) -> dict[str, Any]
|
|
correct_account_valuation(valuation_id: str, amount: int, correction_reason: str, idempotency_key: str = "", db_path: str | None = None, **fields: Any) -> dict[str, Any]
|
|
preview_asset_snapshot_close(basis_date: str = "", db_path: str | None = None, selection_policy: str = "exact") -> dict[str, Any]
|
|
close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str = "", correction_reason: str = "", db_path: str | None = None, selection_policy: str = "exact", idempotency_key: str = "", allow_missing_exclusions: bool = False, allow_as_of_carry_forward: bool = False, approved_by: str = "", approval_reason: str = "", approval_reference: str = "", preview_fingerprint: str = "") -> dict[str, Any]
|
|
get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None, selection_policy: str = "exact") -> dict[str, Any]
|
|
```
|
|
|
|
Every output is a dict with `success`; failures also have `error_code` and `error`. Successful close responses expose top-level `approval`, `correction`, `warnings`, `excluded_items`, and `carried_items`, plus `snapshot_run_id` and `close_status`. Their nested `provenance` contains `valuation_date`, `age_days`, `carried_forward`, `verification_status`, `evidence_kind`, `approval`, `correction`, `warnings`, and redacted `exclusions`. `last_balance` is an external current comparison, not opening evidence; absent balance_after/opening evidence returns `INCOMPLETE_CALCULATION`. `source_ref` is account-scoped evidence identity even with an explicit key; key/source mismatch is a no-write conflict. Correction requires same parent account/date/currency/purpose, rejects cyclic/self parent, permits one child, and requires reason.
|
|
|
|
### canonical read and provenance rules
|
|
|
|
`monthly_source_snapshot_id` is a read-model alias of `snapshot_run_id`, not a persistence field. The alias and actual valuation date/age/carry/verification/run/approval/correction/warnings/exclusions are exposed through each response's documented top-level fields, `included_items`, and nested `provenance` in close, live, closed, timeseries, and monthly read models. Reconciliation states investment unavailable, loan purpose/definition/ambiguity, and completeness without fake writes.
|
|
|
|
### C13 database boundary
|
|
|
|
Writer/bootstrap explicitly calls `init_db()`. `read_db()` uses SQLite `mode=ro`; missing database returns `db_not_initialized`, while initialized database read failures return `db_error`. Both failures retain the standard `success=false`, `error_code`, `error` envelope without raw SQLite details. Read paths perform no mkdir, no migrate, and no commit. Only first-use write initializes, and Dashboard auth is evaluated before DB access.
|
|
|
|
## D04 snapshot close output contract
|
|
|
|
Every successful **legacy direct close** output retains these required top-level fields as required top-level metadata fields (the transport envelope may additionally include `success`): `snapshot_run_id`, `close_status`, `approval`, `correction`, `warnings`, `excluded_items`, `carried_items`, `provenance`, `valuation_date`, `age_days`, `verification_status`, `evidence_kind`, and `monthly_source_snapshot_id`. This is a required retained metadata-field list, not an exhaustive inventory of all keys in a successful legacy response; additional legacy response keys remain part of the legacy output. The list does not define the additive settlement-draft finalize response. `monthly_source_snapshot_id` is an alias of `snapshot_run_id`, not a persistence field.
|
|
|
|
The top-level `provenance` object retains these required nested `provenance` fields (metadata, not an exhaustive inventory of all nested response keys): `valuation_date`, `age_days`, `carried_forward`, `verification_status`, `evidence_kind`, `approval`, `correction`, `warnings`, `excluded_items`, and `carried_items`. `approval` and `correction` are objects or explicit empty values when not applicable; warnings and item collections are arrays. `valuation_date` is the actual selected evidence date, `age_days` is computed from the requested basis date, and verification/evidence status describe selected evidence. Live, closed, timeseries, and monthly read models preserve these fields and alias semantics.
|
|
|
|
The preview input includes basis_date, selection_policy, included/excluded evidence state hashes, warnings, and candidate_state_hashes. preview output includes preview_fingerprint. Close accepts the exact `preview_fingerprint: str = ""` parameter shown in the signature block and recomputes the same authority fingerprint before opening the write boundary. A mismatch returns `authority_drift` and no partial write.
|
|
|
|
Replay uses replay payload equality: an existing idempotency key replays only when the persisted request fingerprint is equal; a changed payload returns `idempotency_conflict`. The replay response is reconstructed from the persisted run and its evidence, with no replay-only reused fields.
|
|
|
|
`close_metadata` is a recursive close metadata allowlist: only run, approval, correction, warnings, exclusions, carried evidence, and allowlisted evidence fields (`source_type`, `source_id`, `authority_source`, `valuation_id`, dates/statuses and `state_hash`) cross the boundary. `applied_evidence`, `excluded_evidence`, and `carried_evidence` are redacted recursively; unknown nested keys are dropped, and credentials, tokens, cookies, raw account data, and arbitrary source-note payloads are never returned. This is the recursive close_metadata allowlist and redaction contract.
|
|
|
|
## settlement-draft-service — 월정산 초안 API (Issue #15)
|
|
|
|
Common application owner: `jarca_ledger_mcp/application/settlement_draft_service.py`; re-export through `jarca_ledger_mcp.service`; service and gateway signatures below include internal `db_path`, which is omitted from MCP tool inputSchema. All other MCP-exposed parameters retain their documented names, defaults, and requiredness. MCP exposes the same operation names, not the same signatures; Dashboard uses existing gateway layering. Draft finalize adds draft-specific response fields while preserving common aliases; it is not a claim that its response has the legacy direct-close shape. Each returns the existing `success`/`error_code`/`error` envelope. Dashboard auth gate precedes DB access; this adds no MCP authentication claim.
|
|
|
|
```python
|
|
create_settlement_draft(month: str, created_by: str, idempotency_key: str, basis_date: str = "", memo: str = "", correction_of_run_id: str = "", correction_reason: str = "", db_path: str | None = None)
|
|
list_settlement_drafts(month: str = "", db_path: str | None = None)
|
|
get_settlement_draft(draft_id: str, db_path: str | None = None)
|
|
update_settlement_draft(draft_id: str, expected_version: int, updated_by: str, memo: str, db_path: str | None = None)
|
|
refresh_settlement_draft_targets(draft_id: str, expected_version: int, updated_by: str, db_path: str | None = None)
|
|
set_settlement_draft_item(draft_id: str, source_type: str, source_id: str, expected_version: int, selected_by: str, valuation_id: str = "", selection_mode: str = "exact", selection_reason: str = "", included: bool = True, exclusion_reason: str = "", db_path: str | None = None)
|
|
review_settlement_draft(draft_id: str, db_path: str | None = None)
|
|
approve_settlement_draft(draft_id: str, expected_version: int, review_fingerprint: str, approved_by: str, approval_reason: str, approval_reference: str, allow_missing_exclusions: bool = False, allow_carried_before: bool = False, allow_manual_substitute: bool = False, db_path: str | None = None)
|
|
finalize_settlement_draft(draft_id: str, expected_version: int, review_fingerprint: str, idempotency_key: str, db_path: str | None = None)
|
|
cancel_settlement_draft(draft_id: str, expected_version: int, cancelled_by: str, reason: str, db_path: str | None = None)
|
|
```
|
|
|
|
Create validates strict YYYY-MM and month-end date, nonempty actor/key and payload replay equality; same key+payload returns same draft, changed payload or active duplicate conflicts. Versions are positive strict integers (bool/fraction/None rejected); inclusion flags actual bool. `review` is read-only and returns draft/version/fingerprint, items and included/excluded/missing/warnings, approval_required, provisional totals, completeness, substitution and unregistered-liability flags. `candidate_amount` may be shown for unverified evidence but authority amount NULL. Approval binds fingerprint+version and explicit flags. Finalize requires approval even for exact complete data; same request replays the persisted response, conflicting key/payload errors, and failed transaction leaves no partial state.
|
|
|
|
`read_db()` remains SQLite `mode=ro`: missing DB returns `db_not_initialized`, no-create/no mkdir/no migrate/no commit; initialized read failures return `db_error`. Existing auth cookie/logout, MCP transport/scope, menu, query, shell and browser behavior are unchanged. Existing D04 close aliases and recursive metadata allowlist/redaction remain authoritative.
|
|
|
|
### Draft evidence public fields
|
|
|
|
`unregistered_liabilities` is true when the frozen target set contains no registered liability source. It does not assert unknown real-world debt or prove the operator is debt-free. Exclusion and missing evidence remain separate completeness states; neither changes the registration meaning of this flag.
|
|
|
|
`review.included_items`, finalized `items`, asset report `close_metadata.applied_evidence`, timeseries point `applied_evidence`, and monthly `source_coverage.assets.applied_evidence` preserve the same selected-evidence field names: `valuation_date` (actual valuation date), `recorded_at` (original recording timestamp, not the basis date), `date_difference_days = valuation_date - basis_date`, `date_difference_direction` (`after`, `before`, `same`, or `unknown`), and `age_days` (basis-minus-valuation days; negative for a future substitute). A loan additionally preserves `representative_liability_definition`; selection audit includes `selection_mode`, `selection_reason`, `selected_by`, `selected_at`, `item_version`, and draft/fingerprint lineage. These are derived or frozen response fields, not additional database columns.
|
|
|
|
Nested draft `approval` retains `allow_missing_exclusions`, `allow_carried_before`, and `allow_manual_substitute` alongside actor/time/reason/reference. The three approval flags and the three result flags (`contains_substitutions`, `contains_carried`, `has_approved_missing`) are independent. Unverified evidence can be visible as `candidate_amount` but cannot become applied monetary authority; only an explicitly approved missing/excluded row may persist with NULL amount. No lineage default keys are injected into legacy persisted evidence arrays. Recursive allowlisting excludes raw source notes, memos, and unknown nested fields.
|
|
|
|
Executable field-contract regression: `tests/test_settlement_draft_contract_scenarios.py::test_date_metadata_public_contract_names_survive_all_closed_consumers`. Related clause tests cover representative integer precision, transaction readiness independent of asset close, approved unresolved candidates, and the synthetic September pension/loan dates with stale, rollback, and concurrent idempotent retry.
|
|
# Issue #18 additive public contracts (schema 14)
|
|
|
|
Shared service setter: `set_settlement_cashflow_policy(draft_id,expected_version,updated_by,cashflow_policy,db_path=None)`; stored run getter: `get_monthly_settlement_snapshot(month='',run_id='',db_path=None)`. MCP signatures omit internal `db_path`. Draft policy is `draft.cashflow_policy`; policy edits increment version and invalidate approval. Review/finalize results expose fields at response root; active get nests `review`, completed get has root `final_response`. Stored grouped details use `cashflow_kind` (income/expense/excluded), not `kind`.
|
|
|
|
The stored reader recognizes `snapshot_schema_version=1` only with `aggregation_version=comprehensive-v1`. Any non-NULL version metadata requires both recognized versions; a partial pair, unsupported snapshot schema, or absent/unknown aggregation version returns the existing `success=false`, `error_code=db_error`, `error` envelope without authoritative totals/details or DB writes. Legacy NULL schema headers remain legacy reads with unrecorded totals NULL; this does not reinterpret unknown comprehensive versions as legacy.
|
|
|
|
Getter distinguishes immutable stored run/evidence (exact run ID, schema/aggregation version, legacy/partial/missing/excluded indicators) from live current-ledger report. Existing asset-only API inputs/responses/errors/aliases and direct-close meaning remain unchanged and are not comprehensive close. Reads/render are readonly/no-create; authenticated Dashboard gate precedes DB. Existing envelope, stale approval/drift rejection, idempotent persisted replay, conflict, atomic rollback, append-only correction, and privacy contracts remain.
|