fix: address settlement display and documentation review findings
Rework Issue 18 / PR 19, source review 76: C11-F01 and D02-F02/D03-F03/D04-F04. Preserve the approved contract and add named display, schema, policy, ownership and signature regressions.
This commit is contained in:
@@ -195,6 +195,53 @@ Review fingerprint binds draft version, target/inclusion state, full valuation/m
|
||||
KRW only aggregation; non-KRW is warned/excluded, never converted. NULL is not zero; no targets yields provisional zero plus completeness and unregistered-liability warning, not a complete close. Existing snapshot rows preserve full frozen evidence, approval, date/mode/difference and actors; substitution, carried and approved-missing flags are orthogonal. `monthly_source_snapshot_id` remains a read-model alias, never a persistence field.
|
||||
# Issue #18 — schema 14 comprehensive settlement additions
|
||||
|
||||
### asset_snapshot_runs cashflow field contract
|
||||
|
||||
All twelve comprehensive-run columns are nullable with no SQL default. This preserves NULL on historical asset-only runs; a new comprehensive write supplies all twelve fields. SQLite reports `id` (TEXT PRIMARY KEY) nullable because DDL omits explicit NOT NULL, although a run logically requires its identifier.
|
||||
|
||||
| Column | Type | Nullability | Default |
|
||||
|---|---|---|---|
|
||||
| `settlement_month` | TEXT | NULL | none |
|
||||
| `currency` | TEXT | NULL | none |
|
||||
| `income_total` | INTEGER | NULL | none |
|
||||
| `expense_total` | INTEGER | NULL | none |
|
||||
| `net_cashflow` | INTEGER | NULL | none |
|
||||
| `total_assets` | INTEGER | NULL | none |
|
||||
| `total_liabilities` | INTEGER | NULL | none |
|
||||
| `net_worth` | INTEGER | NULL | none |
|
||||
| `cashflow_policy_json` | TEXT | NULL | none |
|
||||
| `aggregation_version` | TEXT | NULL | none |
|
||||
| `snapshot_schema_version` | INTEGER | NULL | none |
|
||||
| `source_snapshot_json` | TEXT | NULL | none |
|
||||
|
||||
### settlement_drafts cashflow field contract
|
||||
|
||||
| Column | Type | Nullability | Default |
|
||||
|---|---|---|---|
|
||||
| `cashflow_policy_json` | TEXT | NOT NULL | `'{}'` |
|
||||
| `aggregation_version` | TEXT | NOT NULL | `'comprehensive-v1'` |
|
||||
|
||||
### settlement_cashflow_snapshots cashflow field contract
|
||||
|
||||
| Column | Type | Nullability | Default |
|
||||
|---|---|---|---|
|
||||
| `id` | TEXT | NULL | none |
|
||||
| `settlement_run_id` | TEXT | NOT NULL | none |
|
||||
| `cashflow_kind` | TEXT | NOT NULL | none |
|
||||
| `category` | TEXT | NOT NULL | `''` |
|
||||
| `subcategory` | TEXT | NOT NULL | `''` |
|
||||
| `amount` | INTEGER | NOT NULL | none |
|
||||
| `transaction_count` | INTEGER | NOT NULL | none |
|
||||
| `currency` | TEXT | NOT NULL | none |
|
||||
|
||||
`id` is logically required although SQLite reports no explicit NOT NULL flag for this TEXT primary key. `settlement_run_id` references `asset_snapshot_runs(id)` ON DELETE RESTRICT. The unique key is `(settlement_run_id, cashflow_kind, category, subcategory, currency)`. `transaction_count > 0` and amount/count are integers; `idx_settlement_cashflow_snapshots_run` indexes the run.
|
||||
|
||||
### Comprehensive snapshot evidence inputs
|
||||
|
||||
`source_snapshot_json` stores an object with `transactions` and `assets` arrays. Transaction inputs preserve `id`, `transaction_date`, `type`, original signed `amount`, `currency`, `category`, `subcategory`, `originalcashflow_kind`, applied `cashflow_kind`, `applied_amount`, `decision`, `sourceprovenance`, `category_cashflow_kind` and `category_active`. Refunds keep their original positive amount while the expense contribution in `applied_amount` is negative; unresolved classifications keep `applied_amount=NULL` and block approval. The actual stored alias is `originalcashflow_kind`, not a renamed persistence field.
|
||||
|
||||
Asset inputs preserve `source_id`, `source_type`, `valuation_id`, `valuation_date`, `basis_date`, `currency`, `authority_source`, `liquidity_bucket`, `is_liability`, `selection_mode`, `exclusion_reason`, `status`, `verification_status`, original `amount` and `applied_amount`. Missing/excluded evidence retains NULL applied amounts and its recorded status/reason; fixed included rows remain linked to the same run. The run header's `cashflow_policy_json`, `aggregation_version`, `snapshot_schema_version` and these source inputs together reproduce the original selection/aggregation decision without rereading today's ledger. A legacy asset-only run retains NULL comprehensive fields and is reported as legacy: no historical backfill or invented zero. The existing approval, draft, correction and idempotency links remain unchanged.
|
||||
|
||||
`asset_snapshot_runs` remains the run header. Schema 13→14 adds settlement month, currency, six nullable legacy-compatible INTEGER totals (`income_total`, `expense_total`, `net_cashflow`, `total_assets`, `total_liabilities`, `net_worth`), `cashflow_policy_json`, `aggregation_version`, `snapshot_schema_version`, and `source_snapshot_json`; preserve existing basis/approval/draft/correction/idempotency fields. Old asset-only run totals remain NULL; no current-ledger backfill. Migration is additive/idempotent; old unfinished drafts require fresh comprehensive review/approval.
|
||||
|
||||
`settlement_cashflow_snapshots`: eight columns `id`, `settlement_run_id`, `cashflow_kind`, `category`, `subcategory`, `amount`, `transaction_count`, `currency`; `cashflow_kind` is income/expense/excluded, `amount` signed INTEGER, count positive, category fields NOT NULL (empty string is explicit), FK to `asset_snapshot_runs.id` with RESTRICT, unique `(settlement_run_id,cashflow_kind,category,subcategory,currency)`, and run lookup index. Rows are grouped details for exactly one run.
|
||||
|
||||
@@ -471,3 +471,44 @@ Shared service setter: `set_settlement_cashflow_policy(draft_id,expected_version
|
||||
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.
|
||||
|
||||
### Comprehensive cashflow policy input
|
||||
|
||||
`set_settlement_cashflow_policy(draft_id, expected_version, updated_by, cashflow_policy, db_path=None)` accepts this normalized input (omitted lists normalize to empty lists):
|
||||
|
||||
```json
|
||||
{"savings_categories":[{"category":"Savings","subcategory":"Deposit"}],"category_decisions":[{"category":"Food","subcategory":"Dining","cashflow_kind":"expense","reason":"Reviewed classification"}]}
|
||||
```
|
||||
|
||||
Only `savings_categories` and `category_decisions` are allowed top-level keys; each is a list. Savings entries have exactly `category` and `subcategory`; decision entries have exactly those keys plus `cashflow_kind` and `reason`. Category fields are strings and blank strings are valid. Kinds are `income`, `expense`, or `excluded`; reason must be a nonempty string. Within each list, duplicate category/subcategory pairs are rejected. Normalization sorts each list and supplies omitted lists as empty.
|
||||
|
||||
The setter requires matching `expected_version`; successful mutation increments version and clears prior approval/review fingerprint. Review computes a new fingerprint; approval applies only to the reviewed version/fingerprint. Changed policy or transactions require re-review and re-approval. `aggregation_version` is `comprehensive-v1`; policy editing increments the draft version, clears approval fields and invalidates the prior `review_fingerprint` for approval.
|
||||
|
||||
### Comprehensive settlement response contract
|
||||
|
||||
The six totals are integer root fields in comprehensive review and saved-run reader responses. Legacy readers return NULL for totals they never stored.
|
||||
|
||||
| Field | Type | Presence | Legacy |
|
||||
|---|---|---|---|
|
||||
| `income_total` | integer | required | null |
|
||||
| `expense_total` | integer | required | null |
|
||||
| `net_cashflow` | integer | required | null |
|
||||
| `total_assets` | integer | required | null |
|
||||
| `total_liabilities` | integer | required | null |
|
||||
| `net_worth` | integer | required | null |
|
||||
|
||||
A review response has root-level `review_fingerprint`, `cashflow_policy`, `cashflow_items`, `cashflow_blockers`, and `source_snapshot`. The active draft getter nests its current review under `review`. For a completed draft, `get_settlement_draft` exposes the persisted close response under `final_response`; `finalize_settlement_draft` returns that close response at root, including on idempotent replay. A saved-run reader instead returns the stored snapshot at root with `source: settlement_snapshot`, `run_id`, and six totals. `cashflow_items` is a required list in comprehensive review, close and saved-reader results (empty is valid): each entry has `cashflow_kind` (income/expense/excluded string), `category` and `subcategory` (strings, including blanks), signed integer `amount`, positive integer `transaction_count`, and string `currency`. `cashflow_blockers` is a required review/close list with string `transaction_id` and `reason` entries; successful close requires it to be empty. The saved reader does not synthesize a blocker list from old records. `cashflow_policy` is the normalized policy object for comprehensive responses, but null for the legacy saved reader. `source_snapshot` is the preserved transaction/asset input object for comprehensive responses and an empty object for legacy reads. Version metadata is preserved, not inferred from today's ledger. The following JSON examples show selected fields rather than exhaustive response payloads.
|
||||
|
||||
Successful root review example, including a negative signed expense (refund):
|
||||
|
||||
```json
|
||||
{"success":true,"income_total":120000,"expense_total":-5000,"net_cashflow":125000,"total_assets":900000,"total_liabilities":200000,"net_worth":700000,"review_fingerprint":"<fingerprint>","cashflow_policy":{"savings_categories":[],"category_decisions":[]},"cashflow_items":[{"cashflow_kind":"expense","category":"Food","subcategory":"Dining","amount":-5000,"transaction_count":1,"currency":"KRW"}],"cashflow_blockers":[{"transaction_id":"txn-example","reason":"unknown cashflow classification requires an explicit decision"}],"source_snapshot":{"transactions":[],"assets":[]}}
|
||||
```
|
||||
|
||||
Saved reader root example:
|
||||
|
||||
```json
|
||||
{"success":true,"source":"settlement_snapshot","run_id":"run-example","income_total":120000,"expense_total":-5000,"net_cashflow":125000,"total_assets":900000,"total_liabilities":200000,"net_worth":700000,"cashflow_policy":{"savings_categories":[],"category_decisions":[]},"source_snapshot":{"transactions":[],"assets":[]}}
|
||||
```
|
||||
|
||||
A `validation_error` results when cashflow review blockers must be resolved before approval/finalize. Remediate policy or transaction classification, then re-review and re-approve. Examples use synthetic identifiers, not raw secrets.
|
||||
|
||||
@@ -245,6 +245,10 @@ Never document invented `jarca_ledger_mcp.mcp` or `monthly.py` modules. `transac
|
||||
|
||||
### Shared settlement-draft policies
|
||||
|
||||
| Module | Visibility | Responsibility / caller | Verification |
|
||||
|---|---|---|---|
|
||||
| `jarca_ledger_mcp/application/settlement_cashflow_service.py` | private | `_normalize_cashflow_policy` and `_review_cashflow` provide policy normalization and deterministic aggregation through `settlement_draft_service.py`; not an adapter/public API | `tests/test_settlement_cashflow_policy.py`, `tests/test_comprehensive_settlement_adapters.py` |
|
||||
|
||||
- `jarca_ledger_mcp/application/settlement_draft_service.py` checks one active draft per month for ordinary and correction drafts; SQLite independently enforces it with `idx_settlement_drafts_active_month` on month `WHERE status='active'`. Completed and cancelled history is retained, not deleted to release that slot.
|
||||
- The same application service validates strict month syntax and month-end basis-date equality. Dashboard and MCP cannot supply an alternate date policy.
|
||||
- The shared review service exposes unverified `candidate_amount` with authority amount NULL; no adapter may sum it as verified evidence. A confirmed child requires explicit reselection, fresh review and approval; correction children are not silently followed.
|
||||
|
||||
Reference in New Issue
Block a user