from __future__ import annotations from datetime import date from pathlib import Path import unittest from bank_importer.parser import UnknownTemplateError, detect_header, parse_directory ROOT = Path(__file__).resolve().parents[1] SAMPLES = ROOT / "流水模板" class StatementParserTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.batches = parse_directory(SAMPLES) cls.by_file = {batch.source_file.name: batch for batch in cls.batches} def test_all_current_bank_samples_are_recognized(self) -> None: expected = { "中信银行账户流水.xlsx": ("中信银行", 16, 4), "中国农业银行账户流水.xls": ("中国农业银行", 3, 4), "中国工商银行账户流水.xlsx": ("中国工商银行", 2, 5), "中国建设银行账户流水.xls": ("中国建设银行", 1, 1), "河南农商银行账户流水.xlsx": ("河南农商银行", 11, 1), "郑州银行账户流水.xls": ("郑州银行", 7, 5), } self.assertEqual(set(expected), set(self.by_file)) for filename, (bank, header_row, transaction_count) in expected.items(): with self.subTest(filename=filename): batch = self.by_file[filename] self.assertEqual(bank, batch.bank_name) self.assertEqual(header_row, batch.header_row) self.assertEqual(transaction_count, len(batch.transactions)) def test_known_statement_periods_are_extracted(self) -> None: expected = { "中信银行账户流水.xlsx": (date(2026, 4, 26), date(2026, 5, 23)), "中国农业银行账户流水.xls": (date(2025, 11, 26), date(2025, 12, 27)), "河南农商银行账户流水.xlsx": (date(2026, 2, 24), date(2026, 3, 28)), "郑州银行账户流水.xls": (date(2026, 2, 24), date(2026, 3, 27)), } for filename, period in expected.items(): with self.subTest(filename=filename): batch = self.by_file[filename] self.assertEqual(period, (batch.period_start, batch.period_end)) def test_normalized_transactions_have_one_cash_direction(self) -> None: for batch in self.batches: for transaction in batch.transactions: with self.subTest( filename=batch.source_file.name, row=transaction.source_row ): self.assertNotEqual(transaction.income > 0, transaction.expense > 0) def test_current_samples_reconcile_by_running_balance(self) -> None: for batch in self.batches: with self.subTest(filename=batch.source_file.name): self.assertEqual((), batch.warnings) def test_column_order_does_not_affect_header_detection(self) -> None: header = ( "余额", "对方单位名称", "贷方发生额", "交易时间", "本方账号", "借方发生额", "凭证号", ) template, row_index, columns = detect_header((header,)) self.assertEqual("icbc-history-detail-v1", template.template_id) self.assertEqual(0, row_index) self.assertEqual(3, columns["transaction_at"]) def test_unknown_header_is_rejected(self) -> None: with self.assertRaises(UnknownTemplateError) as raised: detect_header((("日期", "金额", "备注"),)) message = str(raised.exception) self.assertIn("未识别到受支持的银行表头", message) self.assertIn("第 1 行", message) self.assertIn("日期、金额、备注", message) if __name__ == "__main__": unittest.main()