feat: deliver schema 15 monthly settlement lifecycle
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
from jarca_ledger_mcp.application.monthly_settlement_service import (
|
||||
create_monthly_settlement,
|
||||
link_account_snapshot,
|
||||
list_account_snapshots_for_settlement,
|
||||
)
|
||||
from jarca_ledger_mcp.infrastructure.sqlite.migrations import init_db
|
||||
|
||||
|
||||
def _account(conn, ident="acct-1", name="실제계정", currency="KRW"):
|
||||
conn.execute(
|
||||
"INSERT INTO accounts(id,name,currency,created_at,updated_at) VALUES(?,?,?,?,?)",
|
||||
(ident, name, currency, "2026-10-01T00:00:00Z", "2026-10-01T00:00:00Z"),
|
||||
)
|
||||
|
||||
|
||||
def _snapshot(conn, ident, *, account="acct-1", basis_date="2026-10-31", currency="KRW", settlement=None, amount=1234):
|
||||
conn.execute(
|
||||
"""INSERT INTO account_snapshot
|
||||
(id,basis_date,source_type,source_id,name,amount,currency,liquidity_bucket,
|
||||
monthly_settlement_id,valuation_id,verification_status,created_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(ident, basis_date, "account", account, "원본 스냅샷명", amount, currency, "cash", settlement,
|
||||
"valuation-1", "unverified", "2026-10-31T00:00:00Z"),
|
||||
)
|
||||
|
||||
|
||||
def _ledger(tmp_path):
|
||||
path = tmp_path / "link.sqlite3"
|
||||
init_db(str(path))
|
||||
with sqlite3.connect(path) as conn:
|
||||
draft = create_monthly_settlement("2026-10", db_path=str(path))["settlement_id"]
|
||||
conn.execute("UPDATE monthly_settlement SET basis_date='2026-10-31' WHERE id=?", (draft,))
|
||||
_account(conn)
|
||||
conn.commit()
|
||||
return path, draft
|
||||
|
||||
|
||||
def test_link_updates_only_monthly_fk_and_preserves_snapshot_fields(tmp_path):
|
||||
path, settlement = _ledger(tmp_path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
_snapshot(conn, "candidate")
|
||||
before = conn.execute("SELECT * FROM account_snapshot WHERE id='candidate'").fetchone()
|
||||
columns = [row[1] for row in conn.execute("PRAGMA table_info(account_snapshot)")]
|
||||
result = link_account_snapshot("candidate", settlement, db_path=str(path))
|
||||
assert result["success"] is True
|
||||
with sqlite3.connect(path) as conn:
|
||||
after = conn.execute("SELECT * FROM account_snapshot WHERE id='candidate'").fetchone()
|
||||
assert len(before) == len(after) == len(columns)
|
||||
changed = {column: (old, new) for column, old, new in zip(columns, before, after) if old != new}
|
||||
assert changed == {"monthly_settlement_id": (None, settlement)}
|
||||
|
||||
|
||||
def test_actual_read_function_returns_unlinked_candidates_in_exact_order(tmp_path):
|
||||
path, settlement = _ledger(tmp_path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
_snapshot(conn, "old", basis_date="2026-09-30", amount=1)
|
||||
_snapshot(conn, "exact-old-created", basis_date="2026-10-31", amount=2)
|
||||
_snapshot(conn, "exact-new-created", basis_date="2026-10-31", amount=3)
|
||||
conn.execute("UPDATE account_snapshot SET created_at='2026-10-31T02:00:00Z' WHERE id='exact-old-created'")
|
||||
conn.execute("UPDATE account_snapshot SET created_at='2026-10-31T03:00:00Z' WHERE id='exact-new-created'")
|
||||
conn.commit()
|
||||
result = list_account_snapshots_for_settlement(settlement, db_path=str(path))
|
||||
row = result["snapshots"][0]
|
||||
assert [item["snapshot_id"] for item in row["unlinked_candidates"]] == ["exact-new-created", "exact-old-created", "old"]
|
||||
|
||||
@pytest.mark.parametrize("status", ["approved", "closed"])
|
||||
def test_link_rejects_approved_and_closed(tmp_path, status):
|
||||
path, settlement = _ledger(tmp_path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
_snapshot(conn, "candidate")
|
||||
conn.execute("""UPDATE monthly_settlement SET status=?, approved_by='test',
|
||||
approved_at='2026-10-31T00:00:00Z', approved_version=1,
|
||||
approved_fingerprint='test', closed_by='test', closed_at='2026-10-31T00:00:00Z' WHERE id=?""", (status, settlement))
|
||||
conn.commit()
|
||||
result = link_account_snapshot("candidate", settlement, db_path=str(path))
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "invalid_settlement_status"
|
||||
|
||||
|
||||
def test_link_rejects_unrelated_month_and_already_linked_snapshot(tmp_path):
|
||||
path, settlement = _ledger(tmp_path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
other = create_monthly_settlement("2026-11", db_path=str(path))["settlement_id"]
|
||||
_snapshot(conn, "linked-other", settlement=other)
|
||||
_snapshot(conn, "linked-this", settlement=settlement)
|
||||
conn.commit()
|
||||
assert link_account_snapshot("linked-other", settlement, db_path=str(path))["error_code"] == "already_linked"
|
||||
assert link_account_snapshot("linked-this", settlement, db_path=str(path))["error_code"] == "already_linked"
|
||||
|
||||
|
||||
def test_basis_date_mismatch_requires_explicit_true(tmp_path):
|
||||
path, settlement = _ledger(tmp_path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
_snapshot(conn, "mismatch", basis_date="2026-09-30")
|
||||
conn.commit()
|
||||
rejected = link_account_snapshot("mismatch", settlement, db_path=str(path))
|
||||
assert rejected["error_code"] == "basis_date_mismatch"
|
||||
accepted = link_account_snapshot("mismatch", settlement, allow_basis_date_mismatch=True, db_path=str(path))
|
||||
assert accepted["success"] is True
|
||||
|
||||
|
||||
def test_link_rejects_account_currency_mismatch(tmp_path):
|
||||
path, settlement = _ledger(tmp_path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
_snapshot(conn, "bad-currency", currency="USD")
|
||||
conn.commit()
|
||||
result = link_account_snapshot("bad-currency", settlement, db_path=str(path))
|
||||
assert result["error_code"] == "validation_error"
|
||||
Reference in New Issue
Block a user