"""Clause-level acceptance uses real synthetic SQLite and public consumers.""" import sqlite3 from concurrent.futures import ThreadPoolExecutor from jarca_ledger_mcp import service as s from jarca_ledger_mcp.db import init_db def ok(result): assert result['success'], result return result def account(path, name, **fields): return ok(s.create_ledger_account(name, db_path=path, **fields))['account']['id'] def value(path, aid, key, when, amount, **fields): return ok(s.register_account_valuation(aid, when, amount, idempotency_key=key, verification_status=fields.pop('verification_status', 'verified'), db_path=path, **fields))['valuation']['id'] def choose(path, draft, aid, vid, **fields): return ok(s.set_settlement_draft_item(draft['id'], 'account', aid, draft['version'], 'synthetic selector', valuation_id=vid, db_path=path, **fields))['draft'] def approve(path, draft, **flags): review = ok(s.review_settlement_draft(draft['id'], db_path=path)) ok(s.approve_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'synthetic reviewer', 'synthetic review reason', 'synthetic reference', db_path=path, **flags)) return review def test_date_metadata_public_contract_names_survive_all_closed_consumers(tmp_path): path = str(tmp_path / 'dates.sqlite3') init_db(path) aid = account(path, 'Synthetic loan', type='loan', liquidity_bucket='loan') vid = value(path, aid, 'loan', '2026-10-07', 40, valuation_kind='loan_balance', representative_liability_definition='principal_only') with sqlite3.connect(path) as c: c.execute("UPDATE account_valuations SET recorded_at='2026-10-08T00:00:00Z' WHERE id=?", (vid,)) d = ok(s.create_settlement_draft('2026-09', 'creator', 'initial', db_path=path))['draft'] d = choose(path, d, aid, vid, selection_mode='manual_substitute', selection_reason='Synthetic late loan statement') review = approve(path, d, allow_manual_substitute=True) final = ok(s.finalize_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'final', db_path=path)) report = ok(s.get_asset_snapshot_report('2026-09-30', db_path=path)) series = ok(s.get_net_worth_timeseries('2026-09-01', '2026-09-30', db_path=path)) monthly = ok(s.get_monthly_settlement_summary('2026-09', db_path=path)) expected = {'valuation_date': '2026-10-07', 'date_difference_days': 7, 'date_difference_direction': 'after', 'age_days': -7, 'recorded_at': '2026-10-08T00:00:00Z', 'representative_liability_definition': 'principal_only'} rows = [review['included_items'][0], final['items'][0], report['close_metadata']['applied_evidence'][0], series['points'][0]['applied_evidence'][0], monthly['source_coverage']['assets']['applied_evidence'][0]] for row in rows: assert {key: row.get(key) for key in expected} == expected for row in rows[1:]: assert row['approval']['allow_manual_substitute'] is True assert row['approval']['allow_missing_exclusions'] is False assert row['approval']['allow_carried_before'] is False def test_representative_integer_precision_no_double_sum_and_scope_rejection(tmp_path): path = str(tmp_path / 'precision.sqlite3') init_db(path) aid = account(path, 'Synthetic pension', type='investment', liquidity_bucket='investment') other = account(path, 'Synthetic other') amount = 9007199254740993 representative = value(path, aid, 'total', '2026-09-30', amount, valuation_kind='total_value') value(path, aid, 'cash', '2026-09-30', 100, valuation_kind='cash_balance') wrong = value(path, other, 'other', '2026-09-30', 10) d = ok(s.create_settlement_draft('2026-09', 'creator', 'initial', db_path=path))['draft'] review = ok(s.review_settlement_draft(d['id'], db_path=path)) item = next(x for x in review['included_items'] if x['source_id'] == aid) assert item['valuation_id'] == representative and type(item['amount']) is int and item['amount'] == amount assert review['totals']['net_worth'] == amount + 10 before = ok(s.get_settlement_draft(d['id'], db_path=path)) bad = s.set_settlement_draft_item(d['id'], 'account', aid, d['version'], 'selector', valuation_id=wrong, db_path=path) assert not bad['success'] and bad['error_code'] == 'validation_error' assert ok(s.get_settlement_draft(d['id'], db_path=path)) == before def test_unverified_substitute_only_finalizes_as_explicit_approved_missing(tmp_path): path = str(tmp_path / 'unverified.sqlite3') init_db(path) aid = account(path, 'Synthetic pension', type='investment', liquidity_bucket='investment') vid = value(path, aid, 'candidate', '2026-10-05', 100, valuation_kind='total_value', verification_status='unverified') d = ok(s.create_settlement_draft('2026-09', 'creator', 'initial', db_path=path))['draft'] d = choose(path, d, aid, vid, selection_mode='manual_substitute', selection_reason='Synthetic review-only candidate') review = ok(s.review_settlement_draft(d['id'], db_path=path)) assert not review['included_items'] and review['missing_items'][0]['amount'] is None bad = s.approve_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'reviewer', 'reason', 'ref', allow_manual_substitute=True, db_path=path) assert not bad['success'] before = s.finalize_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'final', db_path=path) assert not before['success'] review = approve(path, d, allow_missing_exclusions=True) final = ok(s.finalize_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'final', db_path=path)) assert not final['items'] and final['excluded_items'][0]['amount'] is None assert final['excluded_items'][0]['candidate_amount'] == 100 assert final['totals']['net_worth'] == 0 and final['has_approved_missing'] assert not final['contains_substitutions'] and final['close_status'] == 'conditional' def test_transaction_readiness_is_independent_of_asset_draft_close(tmp_path): path = str(tmp_path / 'separation.sqlite3') init_db(path) aid = account(path, 'Synthetic cash') value(path, aid, 'cash', '2026-09-30', 100) tx = ok(s.create_ledger_transaction('2026-09-15', amount=5, account_id=aid, category='미분류', db_path=path)) assert tx['success'] ok(s.upsert_review_decision('transaction', tx['transaction']['id'], status='pending', operator='synthetic reviewer', db_path=path)) before = ok(s.get_monthly_settlement_summary('2026-09', db_path=path)) assert before['unresolved_blocker_count'] > 0 with sqlite3.connect(path) as c: original = c.execute('SELECT * FROM transactions').fetchall() d = ok(s.create_settlement_draft('2026-09', 'creator', 'initial', db_path=path))['draft'] review = ok(s.review_settlement_draft(d['id'], db_path=path)) assert review['totals']['total_assets'] == 100 assert not review['complete'] and review['cashflow_blockers'] blocked = s.approve_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'synthetic reviewer', 'reason', 'ref', db_path=path) assert not blocked['success'] with sqlite3.connect(path) as c: assert c.execute('SELECT COUNT(*) FROM asset_snapshot_runs').fetchone()[0] == 0 policy = {'savings_categories': [], 'category_decisions': [{'category': '미분류', 'subcategory': '', 'cashflow_kind': 'excluded', 'reason': 'synthetic explicit exclusion'}]} updated = s.set_settlement_cashflow_policy(d['id'], d['version'], 'synthetic reviewer', policy, db_path=path) assert updated['success'] d = updated['draft'] assert d['version'] == 2 review = ok(s.review_settlement_draft(d['id'], db_path=path)) assert review['complete'] ok(s.approve_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'synthetic reviewer', 'reason', 'ref', db_path=path)) final = ok(s.finalize_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'final', db_path=path)) after = ok(s.get_monthly_settlement_summary('2026-09', db_path=path)) assert final['totals']['net_worth'] == 100 and after['source_coverage']['assets']['is_closed_snapshot'] assert after['unresolved_blockers'] == before['unresolved_blockers'] and after['unresolved_blocker_count'] > 0 with sqlite3.connect(path) as c: assert c.execute('SELECT * FROM transactions').fetchall() == original def test_synthetic_september_future_pension_loan_late_exact_warnings_stale_rollback_concurrent_retry(tmp_path): path = str(tmp_path / 'september.sqlite3') init_db(path) cash = account(path, 'Synthetic cash') pension = account(path, 'Synthetic pension', type='investment', liquidity_bucket='investment') loan = account(path, 'Synthetic loan', type='loan', liquidity_bucket='loan') rejected = account(path, 'Synthetic rejected') foreign = account(path, 'Synthetic USD', currency='USD') unknown = ok(s.create_manual_asset('Synthetic undated property', None, '', 'real_estate', db_path=path))['manual_asset']['id'] cv = value(path, cash, 'cash', '2026-09-30', 100) pv = value(path, pension, 'pension', '2026-10-05', 120, valuation_kind='total_value') lv = value(path, loan, 'loan', '2026-10-07', 40, valuation_kind='loan_balance', representative_liability_definition='principal_only') value(path, rejected, 'rejected', '2026-09-30', 999, verification_status='rejected') value(path, foreign, 'foreign', '2026-09-30', 77, currency='USD') with sqlite3.connect(path) as c: c.execute("UPDATE account_valuations SET recorded_at='2026-10-10T00:00:00Z' WHERE id=?", (cv,)) d = ok(s.create_settlement_draft('2026-09', 'creator', 'initial', db_path=path))['draft'] first = ok(s.review_settlement_draft(d['id'], db_path=path)) assert first['totals']['net_worth'] == 100 d = choose(path, d, pension, pv, selection_mode='manual_substitute', selection_reason='Synthetic Oct 05 pension') d = choose(path, d, loan, lv, selection_mode='manual_substitute', selection_reason='Synthetic Oct 07 loan') review = approve(path, d, allow_missing_exclusions=True, allow_manual_substitute=True) assert review['totals']['net_worth'] == 180 assert {x['source_id'] for x in review['missing_items']} == {rejected, foreign, unknown} assert all(x['amount'] is None for x in review['missing_items']) assert any(w['code'] == 'non_krw' for w in review['warnings']) with sqlite3.connect(path) as c: c.execute("UPDATE account_valuations SET memo='Synthetic evidence drift' WHERE id=?", (cv,)) stale = s.finalize_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'final', db_path=path) assert not stale['success'] and stale['error_code'] == 'conflict' d = choose(path, d, cash, cv) review = approve(path, d, allow_missing_exclusions=True, allow_manual_substitute=True) with sqlite3.connect(path) as c: c.execute("CREATE TRIGGER synthetic_failure BEFORE INSERT ON account_snapshot BEGIN SELECT RAISE(ABORT, 'synthetic rollback'); END") failed = s.finalize_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'final', db_path=path) assert not failed['success'] and failed['error_code'] == 'db_error' with sqlite3.connect(path) as c: assert c.execute('SELECT COUNT(*) FROM asset_snapshot_runs').fetchone()[0] == 0 assert c.execute('SELECT COUNT(*) FROM account_snapshot').fetchone()[0] == 0 c.execute('DROP TRIGGER synthetic_failure') assert ok(s.get_settlement_draft(d['id'], db_path=path))['draft']['status'] == 'active' def finish(): return s.finalize_settlement_draft(d['id'], d['version'], review['review_fingerprint'], 'final', db_path=path) with ThreadPoolExecutor(max_workers=2) as pool: futures = [pool.submit(finish) for _ in range(2)] results = [ok(f.result()) for f in futures] assert results[0] == results[1] final = results[0] assert final['contains_substitutions'] and final['has_approved_missing'] and not final['contains_carried'] assert final['totals']['net_worth'] == 180 and len(final['items']) == 3 assert {x['valuation_date'] for x in final['items']} == {'2026-09-30', '2026-10-05', '2026-10-07'} assert finish() == final with sqlite3.connect(path) as c: assert c.execute('SELECT COUNT(*) FROM asset_snapshot_runs').fetchone()[0] == 1 assert c.execute('SELECT COUNT(*) FROM account_snapshot').fetchone()[0] == final['inserted_count']