feat: add persistent monthly settlement drafts and approved valuation selection

This commit is contained in:
2026-10-10 02:49:34 +00:00
parent 68212e773b
commit 92f44661c3
37 changed files with 2902 additions and 29 deletions
+30 -1
View File
@@ -424,7 +424,7 @@ Writer/bootstrap explicitly calls `init_db()`. `read_db()` uses SQLite `mode=ro`
## D04 snapshot close output contract
Every successful snapshot close output has these exact top-level 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`. `monthly_source_snapshot_id` is an alias of `snapshot_run_id`, not a persistence field.
Every successful **legacy direct close** output has these exact top-level fields (the transport envelope may additionally include `success`); this field list does not define the additive settlement-draft finalize response: `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`. `monthly_source_snapshot_id` is an alias of `snapshot_run_id`, not a persistence field.
The top-level `provenance` object has these exact nested `provenance` fields: `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.
@@ -433,3 +433,32 @@ The preview input includes basis_date, selection_policy, included/excluded evide
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
`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.