@dizzlkheinz/ynab-mcpb 0.18.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/CLAUDE.md +87 -8
  3. package/bin/ynab-mcp-server.cjs +2 -2
  4. package/bin/ynab-mcp-server.js +3 -3
  5. package/biome.json +39 -0
  6. package/dist/bundle/index.cjs +67 -67
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +27 -27
  9. package/dist/server/YNABMCPServer.d.ts +3 -4
  10. package/dist/server/YNABMCPServer.js +111 -116
  11. package/dist/server/budgetResolver.d.ts +6 -5
  12. package/dist/server/budgetResolver.js +46 -36
  13. package/dist/server/cacheKeys.js +6 -6
  14. package/dist/server/cacheManager.js +14 -11
  15. package/dist/server/completions.d.ts +2 -2
  16. package/dist/server/completions.js +20 -15
  17. package/dist/server/config.d.ts +10 -5
  18. package/dist/server/config.js +24 -7
  19. package/dist/server/deltaCache.d.ts +2 -2
  20. package/dist/server/deltaCache.js +22 -16
  21. package/dist/server/deltaCache.merge.d.ts +2 -2
  22. package/dist/server/diagnostics.d.ts +4 -4
  23. package/dist/server/diagnostics.js +38 -32
  24. package/dist/server/errorHandler.d.ts +5 -12
  25. package/dist/server/errorHandler.js +219 -217
  26. package/dist/server/prompts.d.ts +2 -2
  27. package/dist/server/prompts.js +45 -45
  28. package/dist/server/rateLimiter.js +4 -4
  29. package/dist/server/requestLogger.d.ts +1 -1
  30. package/dist/server/requestLogger.js +40 -35
  31. package/dist/server/resources.d.ts +3 -3
  32. package/dist/server/resources.js +55 -52
  33. package/dist/server/responseFormatter.js +6 -6
  34. package/dist/server/securityMiddleware.d.ts +2 -2
  35. package/dist/server/securityMiddleware.js +22 -20
  36. package/dist/server/serverKnowledgeStore.js +1 -1
  37. package/dist/server/toolRegistry.d.ts +3 -3
  38. package/dist/server/toolRegistry.js +47 -40
  39. package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
  40. package/dist/tools/__tests__/deltaTestUtils.js +2 -2
  41. package/dist/tools/accountTools.d.ts +9 -8
  42. package/dist/tools/accountTools.js +47 -47
  43. package/dist/tools/adapters.d.ts +13 -8
  44. package/dist/tools/adapters.js +21 -11
  45. package/dist/tools/budgetTools.d.ts +8 -7
  46. package/dist/tools/budgetTools.js +22 -22
  47. package/dist/tools/categoryTools.d.ts +9 -8
  48. package/dist/tools/categoryTools.js +68 -59
  49. package/dist/tools/compareTransactions/formatter.d.ts +3 -3
  50. package/dist/tools/compareTransactions/formatter.js +9 -9
  51. package/dist/tools/compareTransactions/index.d.ts +6 -6
  52. package/dist/tools/compareTransactions/index.js +58 -43
  53. package/dist/tools/compareTransactions/matcher.d.ts +1 -1
  54. package/dist/tools/compareTransactions/matcher.js +28 -15
  55. package/dist/tools/compareTransactions/parser.d.ts +2 -2
  56. package/dist/tools/compareTransactions/parser.js +144 -138
  57. package/dist/tools/compareTransactions/types.d.ts +4 -4
  58. package/dist/tools/compareTransactions.d.ts +1 -1
  59. package/dist/tools/compareTransactions.js +1 -1
  60. package/dist/tools/deltaFetcher.d.ts +2 -2
  61. package/dist/tools/deltaFetcher.js +16 -15
  62. package/dist/tools/deltaSupport.d.ts +4 -4
  63. package/dist/tools/deltaSupport.js +35 -41
  64. package/dist/tools/exportTransactions.d.ts +5 -4
  65. package/dist/tools/exportTransactions.js +61 -59
  66. package/dist/tools/monthTools.d.ts +7 -6
  67. package/dist/tools/monthTools.js +31 -29
  68. package/dist/tools/payeeTools.d.ts +7 -6
  69. package/dist/tools/payeeTools.js +28 -28
  70. package/dist/tools/reconcileAdapter.d.ts +2 -2
  71. package/dist/tools/reconcileAdapter.js +21 -11
  72. package/dist/tools/reconciliation/analyzer.d.ts +4 -4
  73. package/dist/tools/reconciliation/analyzer.js +136 -57
  74. package/dist/tools/reconciliation/csvParser.d.ts +3 -3
  75. package/dist/tools/reconciliation/csvParser.js +128 -104
  76. package/dist/tools/reconciliation/executor.d.ts +4 -4
  77. package/dist/tools/reconciliation/executor.js +148 -109
  78. package/dist/tools/reconciliation/index.d.ts +10 -10
  79. package/dist/tools/reconciliation/index.js +96 -83
  80. package/dist/tools/reconciliation/matcher.d.ts +3 -3
  81. package/dist/tools/reconciliation/matcher.js +17 -16
  82. package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
  83. package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
  84. package/dist/tools/reconciliation/recommendationEngine.js +40 -40
  85. package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
  86. package/dist/tools/reconciliation/reportFormatter.js +79 -54
  87. package/dist/tools/reconciliation/signDetector.d.ts +1 -1
  88. package/dist/tools/reconciliation/types.d.ts +19 -16
  89. package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
  90. package/dist/tools/schemas/common.d.ts +1 -1
  91. package/dist/tools/schemas/common.js +1 -1
  92. package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
  93. package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
  94. package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
  95. package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
  96. package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
  97. package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
  98. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
  99. package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
  100. package/dist/tools/schemas/outputs/index.d.ts +14 -14
  101. package/dist/tools/schemas/outputs/index.js +14 -14
  102. package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
  103. package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
  104. package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
  105. package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
  106. package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
  107. package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
  108. package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
  109. package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
  110. package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
  111. package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
  112. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
  113. package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
  114. package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
  115. package/dist/tools/schemas/shared/commonOutputs.js +15 -9
  116. package/dist/tools/transactionReadTools.d.ts +11 -0
  117. package/dist/tools/transactionReadTools.js +202 -0
  118. package/dist/tools/transactionSchemas.d.ts +309 -0
  119. package/dist/tools/transactionSchemas.js +235 -0
  120. package/dist/tools/transactionTools.d.ts +6 -302
  121. package/dist/tools/transactionTools.js +7 -2054
  122. package/dist/tools/transactionUtils.d.ts +31 -0
  123. package/dist/tools/transactionUtils.js +364 -0
  124. package/dist/tools/transactionWriteTools.d.ts +20 -0
  125. package/dist/tools/transactionWriteTools.js +1342 -0
  126. package/dist/tools/utilityTools.d.ts +5 -4
  127. package/dist/tools/utilityTools.js +11 -11
  128. package/dist/types/index.d.ts +7 -7
  129. package/dist/types/index.js +6 -6
  130. package/dist/types/reconciliation.d.ts +1 -1
  131. package/dist/types/toolRegistration.d.ts +14 -12
  132. package/dist/utils/amountUtils.js +1 -1
  133. package/dist/utils/dateUtils.js +4 -4
  134. package/dist/utils/errors.d.ts +3 -3
  135. package/dist/utils/errors.js +4 -4
  136. package/dist/utils/money.d.ts +2 -2
  137. package/dist/utils/money.js +8 -8
  138. package/dist/utils/validationError.d.ts +1 -1
  139. package/dist/utils/validationError.js +1 -1
  140. package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
  141. package/docs/assets/schemas/reconciliation-v2.json +360 -336
  142. package/docs/plans/2025-12-25-transaction-tools-refactor-design.md +211 -0
  143. package/docs/plans/2025-12-25-transaction-tools-refactor.md +905 -0
  144. package/esbuild.config.mjs +53 -50
  145. package/meta.json +12548 -12548
  146. package/package.json +98 -109
  147. package/scripts/analyze-bundle.mjs +33 -30
  148. package/scripts/create-pr-description.js +169 -120
  149. package/scripts/run-all-tests.js +205 -0
  150. package/scripts/run-domain-integration-tests.js +28 -18
  151. package/scripts/run-generate-mcpb.js +19 -17
  152. package/scripts/run-throttled-integration-tests.js +92 -83
  153. package/scripts/test-delta-params.mjs +149 -120
  154. package/scripts/test-recommendations.ts +36 -32
  155. package/scripts/tmpTransaction.ts +80 -43
  156. package/scripts/validate-env.js +98 -91
  157. package/scripts/verify-build.js +78 -76
  158. package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
  159. package/src/__tests__/performance.test.ts +723 -671
  160. package/src/__tests__/setup.ts +442 -395
  161. package/src/__tests__/smoke.e2e.test.ts +41 -39
  162. package/src/__tests__/testRunner.ts +314 -295
  163. package/src/__tests__/testUtils.ts +456 -364
  164. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
  165. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
  166. package/src/index.ts +68 -59
  167. package/src/server/CLAUDE.md +480 -0
  168. package/src/server/YNABMCPServer.ts +821 -794
  169. package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
  170. package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
  171. package/src/server/__tests__/budgetResolver.test.ts +466 -423
  172. package/src/server/__tests__/cacheManager.test.ts +891 -874
  173. package/src/server/__tests__/completions.integration.test.ts +115 -106
  174. package/src/server/__tests__/completions.test.ts +334 -313
  175. package/src/server/__tests__/config.test.ts +98 -86
  176. package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
  177. package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
  178. package/src/server/__tests__/deltaCache.test.ts +946 -759
  179. package/src/server/__tests__/diagnostics.test.ts +825 -792
  180. package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
  181. package/src/server/__tests__/errorHandler.test.ts +402 -397
  182. package/src/server/__tests__/prompts.test.ts +424 -347
  183. package/src/server/__tests__/rateLimiter.test.ts +313 -309
  184. package/src/server/__tests__/requestLogger.test.ts +443 -403
  185. package/src/server/__tests__/resources.template.test.ts +196 -185
  186. package/src/server/__tests__/resources.test.ts +294 -288
  187. package/src/server/__tests__/security.integration.test.ts +487 -421
  188. package/src/server/__tests__/securityMiddleware.test.ts +519 -444
  189. package/src/server/__tests__/server-startup.integration.test.ts +509 -490
  190. package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
  191. package/src/server/__tests__/toolRegistration.test.ts +239 -210
  192. package/src/server/__tests__/toolRegistry.test.ts +907 -845
  193. package/src/server/budgetResolver.ts +221 -181
  194. package/src/server/cacheKeys.ts +6 -6
  195. package/src/server/cacheManager.ts +498 -484
  196. package/src/server/completions.ts +267 -243
  197. package/src/server/config.ts +35 -14
  198. package/src/server/deltaCache.merge.ts +146 -128
  199. package/src/server/deltaCache.ts +352 -309
  200. package/src/server/diagnostics.ts +257 -242
  201. package/src/server/errorHandler.ts +747 -744
  202. package/src/server/prompts.ts +181 -176
  203. package/src/server/rateLimiter.ts +131 -129
  204. package/src/server/requestLogger.ts +350 -322
  205. package/src/server/resources.ts +442 -374
  206. package/src/server/responseFormatter.ts +41 -37
  207. package/src/server/securityMiddleware.ts +223 -205
  208. package/src/server/serverKnowledgeStore.ts +67 -67
  209. package/src/server/toolRegistry.ts +508 -474
  210. package/src/tools/CLAUDE.md +604 -0
  211. package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
  212. package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
  213. package/src/tools/__tests__/accountTools.test.ts +685 -638
  214. package/src/tools/__tests__/adapters.test.ts +142 -108
  215. package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
  216. package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
  217. package/src/tools/__tests__/budgetTools.test.ts +442 -413
  218. package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
  219. package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
  220. package/src/tools/__tests__/categoryTools.test.ts +656 -625
  221. package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
  222. package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
  223. package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
  224. package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
  225. package/src/tools/__tests__/compareTransactions.test.ts +352 -332
  226. package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
  227. package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
  228. package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
  229. package/src/tools/__tests__/deltaSupport.test.ts +211 -184
  230. package/src/tools/__tests__/deltaTestUtils.ts +37 -33
  231. package/src/tools/__tests__/exportTransactions.test.ts +205 -200
  232. package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
  233. package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
  234. package/src/tools/__tests__/monthTools.test.ts +561 -512
  235. package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
  236. package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
  237. package/src/tools/__tests__/payeeTools.test.ts +486 -434
  238. package/src/tools/__tests__/transactionSchemas.test.ts +1204 -0
  239. package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
  240. package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
  241. package/src/tools/__tests__/transactionUtils.test.ts +1016 -0
  242. package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
  243. package/src/tools/__tests__/utilityTools.test.ts +68 -58
  244. package/src/tools/accountTools.ts +293 -271
  245. package/src/tools/adapters.ts +120 -63
  246. package/src/tools/budgetTools.ts +121 -116
  247. package/src/tools/categoryTools.ts +379 -339
  248. package/src/tools/compareTransactions/formatter.ts +131 -119
  249. package/src/tools/compareTransactions/index.ts +249 -214
  250. package/src/tools/compareTransactions/matcher.ts +259 -209
  251. package/src/tools/compareTransactions/parser.ts +517 -487
  252. package/src/tools/compareTransactions/types.ts +38 -38
  253. package/src/tools/compareTransactions.ts +1 -1
  254. package/src/tools/deltaFetcher.ts +281 -260
  255. package/src/tools/deltaSupport.ts +264 -259
  256. package/src/tools/exportTransactions.ts +230 -218
  257. package/src/tools/monthTools.ts +180 -165
  258. package/src/tools/payeeTools.ts +152 -140
  259. package/src/tools/reconcileAdapter.ts +297 -246
  260. package/src/tools/reconciliation/CLAUDE.md +506 -0
  261. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +135 -112
  262. package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -227
  263. package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -335
  264. package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
  265. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
  266. package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
  267. package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
  268. package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
  269. package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
  270. package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
  271. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -986
  272. package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
  273. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -530
  274. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -71
  275. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -58
  276. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
  277. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +58 -43
  278. package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
  279. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
  280. package/src/tools/reconciliation/analyzer.ts +582 -406
  281. package/src/tools/reconciliation/csvParser.ts +656 -609
  282. package/src/tools/reconciliation/executor.ts +1290 -1128
  283. package/src/tools/reconciliation/index.ts +580 -528
  284. package/src/tools/reconciliation/matcher.ts +256 -240
  285. package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
  286. package/src/tools/reconciliation/recommendationEngine.ts +357 -345
  287. package/src/tools/reconciliation/reportFormatter.ts +349 -276
  288. package/src/tools/reconciliation/signDetector.ts +89 -83
  289. package/src/tools/reconciliation/types.ts +164 -153
  290. package/src/tools/reconciliation/ynabAdapter.ts +17 -15
  291. package/src/tools/schemas/CLAUDE.md +546 -0
  292. package/src/tools/schemas/common.ts +1 -1
  293. package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
  294. package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
  295. package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
  296. package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
  297. package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
  298. package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
  299. package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
  300. package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
  301. package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
  302. package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
  303. package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
  304. package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
  305. package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
  306. package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
  307. package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
  308. package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
  309. package/src/tools/schemas/outputs/index.ts +163 -163
  310. package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
  311. package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
  312. package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
  313. package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
  314. package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
  315. package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
  316. package/src/tools/schemas/shared/commonOutputs.ts +27 -19
  317. package/src/tools/toolCategories.ts +114 -114
  318. package/src/tools/transactionReadTools.ts +327 -0
  319. package/src/tools/transactionSchemas.ts +484 -0
  320. package/src/tools/transactionTools.ts +107 -2990
  321. package/src/tools/transactionUtils.ts +621 -0
  322. package/src/tools/transactionWriteTools.ts +2110 -0
  323. package/src/tools/utilityTools.ts +46 -41
  324. package/src/types/CLAUDE.md +477 -0
  325. package/src/types/__tests__/index.test.ts +51 -51
  326. package/src/types/index.ts +43 -39
  327. package/src/types/integration-tests.d.ts +26 -26
  328. package/src/types/reconciliation.ts +29 -29
  329. package/src/types/toolAnnotations.ts +30 -30
  330. package/src/types/toolRegistration.ts +43 -32
  331. package/src/utils/CLAUDE.md +508 -0
  332. package/src/utils/__tests__/dateUtils.test.ts +174 -168
  333. package/src/utils/__tests__/money.test.ts +193 -187
  334. package/src/utils/amountUtils.ts +5 -5
  335. package/src/utils/baseError.ts +5 -5
  336. package/src/utils/dateUtils.ts +29 -26
  337. package/src/utils/errors.ts +14 -14
  338. package/src/utils/money.ts +66 -52
  339. package/src/utils/validationError.ts +1 -1
  340. package/tsconfig.json +29 -29
  341. package/tsconfig.prod.json +16 -16
  342. package/vitest-reporters/split-json-reporter.ts +247 -204
  343. package/vitest.config.ts +99 -95
  344. package/.prettierignore +0 -10
  345. package/.prettierrc.json +0 -10
  346. package/eslint.config.js +0 -49
@@ -1,574 +1,627 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, expect, it } from "vitest";
2
+ import type { LegacyReconciliationResult } from "../executor.js";
2
3
  import {
3
- formatHumanReadableReport,
4
- formatBalanceInfo,
5
- formatTransactionList,
6
- type ReportFormatterOptions,
7
- } from '../reportFormatter.js';
4
+ type ReportFormatterOptions,
5
+ formatBalanceInfo,
6
+ formatHumanReadableReport,
7
+ formatTransactionList,
8
+ } from "../reportFormatter.js";
8
9
  import type {
9
- ReconciliationAnalysis,
10
- BankTransaction,
11
- YNABTransaction,
12
- ReconciliationInsight,
13
- } from '../types.js';
14
- import type { LegacyReconciliationResult } from '../executor.js';
10
+ BankTransaction,
11
+ ReconciliationAnalysis,
12
+ ReconciliationInsight,
13
+ YNABTransaction,
14
+ } from "../types.js";
15
15
 
16
16
  /**
17
17
  * Helper to create MoneyValue for tests
18
18
  */
19
- const makeMoney = (value: number, currency = 'USD') => ({
20
- value_milliunits: Math.round(value * 1000),
21
- value,
22
- value_display: value < 0 ? `-$${Math.abs(value).toFixed(2)}` : `$${value.toFixed(2)}`,
23
- currency,
24
- direction: (value === 0 ? 'balanced' : value > 0 ? 'credit' : 'debit') as
25
- | 'balanced'
26
- | 'credit'
27
- | 'debit',
19
+ const makeMoney = (value: number, currency = "USD") => ({
20
+ value_milliunits: Math.round(value * 1000),
21
+ value,
22
+ value_display:
23
+ value < 0 ? `-$${Math.abs(value).toFixed(2)}` : `$${value.toFixed(2)}`,
24
+ currency,
25
+ direction: (value === 0 ? "balanced" : value > 0 ? "credit" : "debit") as
26
+ | "balanced"
27
+ | "credit"
28
+ | "debit",
28
29
  });
29
30
 
30
31
  /**
31
32
  * Create a minimal reconciliation analysis for testing
32
33
  */
33
34
  const createTestAnalysis = (
34
- overrides?: Partial<ReconciliationAnalysis>,
35
+ overrides?: Partial<ReconciliationAnalysis>,
35
36
  ): ReconciliationAnalysis => ({
36
- success: true,
37
- phase: 'analysis',
38
- summary: {
39
- statement_date_range: '2025-10-01 to 2025-10-31',
40
- bank_transactions_count: 10,
41
- ynab_transactions_count: 12,
42
- auto_matched: 8,
43
- suggested_matches: 1,
44
- unmatched_bank: 1,
45
- unmatched_ynab: 3,
46
- current_cleared_balance: makeMoney(-899.02),
47
- target_statement_balance: makeMoney(-921.24),
48
- discrepancy: makeMoney(22.22),
49
- discrepancy_explanation: 'Statement shows more owed by $22.22',
50
- },
51
- auto_matches: [],
52
- suggested_matches: [],
53
- unmatched_bank: [],
54
- unmatched_ynab: [],
55
- balance_info: {
56
- current_cleared: makeMoney(-899.02),
57
- current_uncleared: makeMoney(-50.0),
58
- current_total: makeMoney(-949.02),
59
- target_statement: makeMoney(-921.24),
60
- discrepancy: makeMoney(22.22),
61
- on_track: false,
62
- },
63
- next_steps: ['Review unmatched transactions', 'Create missing transactions'],
64
- insights: [],
65
- ...overrides,
37
+ success: true,
38
+ phase: "analysis",
39
+ summary: {
40
+ statement_date_range: "2025-10-01 to 2025-10-31",
41
+ bank_transactions_count: 10,
42
+ ynab_transactions_count: 12,
43
+ ynab_in_range_count: 12,
44
+ ynab_outside_range_count: 0,
45
+ auto_matched: 8,
46
+ suggested_matches: 1,
47
+ unmatched_bank: 1,
48
+ unmatched_ynab: 3,
49
+ current_cleared_balance: makeMoney(-899.02),
50
+ target_statement_balance: makeMoney(-921.24),
51
+ discrepancy: makeMoney(22.22),
52
+ discrepancy_explanation: "Statement shows more owed by $22.22",
53
+ },
54
+ auto_matches: [],
55
+ suggested_matches: [],
56
+ unmatched_bank: [],
57
+ unmatched_ynab: [],
58
+ ynab_outside_date_range: [],
59
+ balance_info: {
60
+ current_cleared: makeMoney(-899.02),
61
+ current_uncleared: makeMoney(-50.0),
62
+ current_total: makeMoney(-949.02),
63
+ target_statement: makeMoney(-921.24),
64
+ discrepancy: makeMoney(22.22),
65
+ on_track: false,
66
+ },
67
+ next_steps: ["Review unmatched transactions", "Create missing transactions"],
68
+ insights: [],
69
+ ...overrides,
66
70
  });
67
71
 
68
72
  /**
69
73
  * Create test bank transaction
70
74
  */
71
75
  const createBankTransaction = (
72
- id: string,
73
- amount: number,
74
- payee: string,
75
- date = '2025-10-15',
76
+ id: string,
77
+ amount: number,
78
+ payee: string,
79
+ date = "2025-10-15",
76
80
  ): BankTransaction => ({
77
- id,
78
- date,
79
- amount: Math.round(amount * 1000),
80
- payee,
81
- original_csv_row: 1,
81
+ id,
82
+ date,
83
+ amount: Math.round(amount * 1000),
84
+ payee,
85
+ original_csv_row: 1,
82
86
  });
83
87
 
84
88
  /**
85
89
  * Create test YNAB transaction
86
90
  */
87
91
  const createYNABTransaction = (
88
- id: string,
89
- amount: number,
90
- payee: string,
91
- date = '2025-10-15',
92
+ id: string,
93
+ amount: number,
94
+ payee: string,
95
+ date = "2025-10-15",
92
96
  ): YNABTransaction => ({
93
- id,
94
- date,
95
- amount,
96
- payee_name: payee,
97
- category_name: 'General',
98
- cleared: 'uncleared',
99
- approved: true,
97
+ id,
98
+ date,
99
+ amount,
100
+ payee_name: payee,
101
+ category_name: "General",
102
+ cleared: "uncleared",
103
+ approved: true,
100
104
  });
101
105
 
102
106
  /**
103
107
  * Create test insight
104
108
  */
105
109
  const createInsight = (
106
- id: string,
107
- type: 'repeat_amount' | 'near_match' | 'anomaly',
108
- severity: 'info' | 'warning' | 'critical',
109
- title: string,
110
- description: string,
110
+ id: string,
111
+ type: "repeat_amount" | "near_match" | "anomaly",
112
+ severity: "info" | "warning" | "critical",
113
+ title: string,
114
+ description: string,
111
115
  ): ReconciliationInsight => ({
112
- id,
113
- type,
114
- severity,
115
- title,
116
- description,
116
+ id,
117
+ type,
118
+ severity,
119
+ title,
120
+ description,
117
121
  });
118
122
 
119
123
  /**
120
124
  * Create test execution result
121
125
  */
122
126
  const createExecutionResult = (
123
- overrides?: Partial<LegacyReconciliationResult>,
127
+ overrides?: Partial<LegacyReconciliationResult>,
124
128
  ): LegacyReconciliationResult => ({
125
- summary: {
126
- transactions_created: 0,
127
- transactions_updated: 0,
128
- dates_adjusted: 0,
129
- dry_run: true,
130
- },
131
- account_balance: {
132
- before: {
133
- balance: -899020,
134
- cleared_balance: -899020,
135
- uncleared_balance: 0,
136
- },
137
- after: {
138
- balance: -899020,
139
- cleared_balance: -899020,
140
- uncleared_balance: 0,
141
- },
142
- },
143
- actions_taken: [],
144
- recommendations: [],
145
- ...overrides,
129
+ summary: {
130
+ transactions_created: 0,
131
+ transactions_updated: 0,
132
+ dates_adjusted: 0,
133
+ dry_run: true,
134
+ },
135
+ account_balance: {
136
+ before: {
137
+ balance: -899020,
138
+ cleared_balance: -899020,
139
+ uncleared_balance: 0,
140
+ },
141
+ after: {
142
+ balance: -899020,
143
+ cleared_balance: -899020,
144
+ uncleared_balance: 0,
145
+ },
146
+ },
147
+ actions_taken: [],
148
+ recommendations: [],
149
+ ...overrides,
146
150
  });
147
151
 
148
- describe('reportFormatter', () => {
149
- describe('formatHumanReadableReport', () => {
150
- it('should format a basic report with header and sections', () => {
151
- const analysis = createTestAnalysis();
152
- const options: ReportFormatterOptions = {
153
- accountName: 'Checking Account',
154
- };
155
-
156
- const report = formatHumanReadableReport(analysis, options);
157
-
158
- expect(report).toContain('Checking Account Reconciliation Report');
159
- expect(report).toContain('-'.repeat(60));
160
- expect(report).toContain('Balance Check');
161
- expect(report).toContain('Transaction Analysis');
162
- expect(report).toContain('Recommended Actions');
163
- });
164
-
165
- it('should show statement date range', () => {
166
- const analysis = createTestAnalysis();
167
- const report = formatHumanReadableReport(analysis);
168
-
169
- expect(report).toContain('Statement Period: 2025-10-01 to 2025-10-31');
170
- });
171
-
172
- it('should show balanced status when no discrepancy', () => {
173
- const analysis = createTestAnalysis({
174
- balance_info: {
175
- current_cleared: makeMoney(-921.24),
176
- current_uncleared: makeMoney(0),
177
- current_total: makeMoney(-921.24),
178
- target_statement: makeMoney(-921.24),
179
- discrepancy: makeMoney(0),
180
- on_track: true,
181
- },
182
- summary: {
183
- ...createTestAnalysis().summary,
184
- discrepancy: makeMoney(0),
185
- },
186
- });
187
-
188
- const report = formatHumanReadableReport(analysis);
189
-
190
- expect(report).toContain('Balances match perfectly.');
191
- expect(report).not.toContain('Discrepancy:');
192
- });
193
-
194
- it('should show discrepancy with correct direction when YNAB higher', () => {
195
- const analysis = createTestAnalysis({
196
- balance_info: {
197
- current_cleared: makeMoney(-900.0),
198
- current_uncleared: makeMoney(0),
199
- current_total: makeMoney(-900.0),
200
- target_statement: makeMoney(-920.0),
201
- discrepancy: makeMoney(20.0), // Positive means YNAB higher
202
- on_track: false,
203
- },
204
- summary: {
205
- ...createTestAnalysis().summary,
206
- discrepancy: makeMoney(20.0),
207
- },
208
- });
209
-
210
- const report = formatHumanReadableReport(analysis);
211
-
212
- expect(report).toContain('Discrepancy: $20.00');
213
- expect(report).toContain('YNAB shows MORE than statement');
214
- });
215
-
216
- it('should show discrepancy with correct direction when bank higher', () => {
217
- const analysis = createTestAnalysis({
218
- balance_info: {
219
- current_cleared: makeMoney(-920.0),
220
- current_uncleared: makeMoney(0),
221
- current_total: makeMoney(-920.0),
222
- target_statement: makeMoney(-900.0),
223
- discrepancy: makeMoney(-20.0), // Negative means bank higher
224
- on_track: false,
225
- },
226
- summary: {
227
- ...createTestAnalysis().summary,
228
- discrepancy: makeMoney(-20.0),
229
- },
230
- });
231
-
232
- const report = formatHumanReadableReport(analysis);
233
-
234
- expect(report).toContain('Discrepancy: -$20.00');
235
- expect(report).toContain('Statement shows MORE than YNAB');
236
- });
237
-
238
- it('should show transaction analysis counts', () => {
239
- const analysis = createTestAnalysis();
240
- const report = formatHumanReadableReport(analysis);
241
-
242
- expect(report).toContain('Automatically matched: 8 of 10 transactions');
243
- expect(report).toContain('Suggested matches: 1');
244
- expect(report).toContain('Unmatched bank: 1');
245
- expect(report).toContain('Unmatched YNAB: 3');
246
- });
247
-
248
- it('should list unmatched bank transactions', () => {
249
- const analysis = createTestAnalysis({
250
- unmatched_bank: [
251
- createBankTransaction('bank-1', -22.22, 'EvoCarShare', '2025-10-25'),
252
- createBankTransaction('bank-2', -15.0, 'Coffee Shop', '2025-10-26'),
253
- ],
254
- });
255
-
256
- const report = formatHumanReadableReport(analysis);
257
-
258
- expect(report).toContain('Unmatched bank transactions:');
259
- expect(report).toContain('2025-10-25');
260
- expect(report).toContain('EvoCarShare');
261
- expect(report).toContain('-$22.22');
262
- expect(report).toContain('Coffee Shop');
263
- });
264
-
265
- it('should truncate long unmatched lists', () => {
266
- const unmatchedBank: BankTransaction[] = [];
267
- for (let i = 0; i < 10; i++) {
268
- unmatchedBank.push(createBankTransaction(`bank-${i}`, -10.0, `Payee ${i}`, '2025-10-15'));
269
- }
270
-
271
- const analysis = createTestAnalysis({ unmatched_bank: unmatchedBank });
272
- const report = formatHumanReadableReport(analysis, { maxUnmatchedToShow: 5 });
273
-
274
- expect(report).toContain('... and 5 more');
275
- });
276
-
277
- it('should show suggested matches', () => {
278
- const analysis = createTestAnalysis({
279
- suggested_matches: [
280
- {
281
- bankTransaction: createBankTransaction('bank-1', -60.0, 'Amazon', '2025-10-20'),
282
- ynabTransaction: undefined,
283
- candidates: [],
284
- confidence: 'medium',
285
- confidenceScore: 75,
286
- matchReason: 'amount_and_date_fuzzy_payee',
287
- topConfidence: 75,
288
- },
289
- ],
290
- });
291
-
292
- const report = formatHumanReadableReport(analysis);
293
-
294
- expect(report).toContain('Suggested matches:');
295
- expect(report).toContain('Amazon');
296
- expect(report).toContain('75% confidence');
297
- });
298
-
299
- it('should include insights section when insights present', () => {
300
- const analysis = createTestAnalysis({
301
- insights: [
302
- createInsight(
303
- '1',
304
- 'repeat_amount',
305
- 'critical',
306
- 'Repeated amount detected',
307
- 'Found $22.22 appearing 3 times',
308
- ),
309
- createInsight(
310
- '2',
311
- 'near_match',
312
- 'warning',
313
- 'Near match found',
314
- 'Transaction differs by only $0.50',
315
- ),
316
- ],
317
- });
318
-
319
- const report = formatHumanReadableReport(analysis);
320
-
321
- expect(report).toContain('Key Insights');
322
- expect(report).toContain('[CRITICAL] Repeated amount detected');
323
- expect(report).toContain('[WARN] Near match found');
324
- });
325
-
326
- it('should use correct severity icons', () => {
327
- const analysis = createTestAnalysis({
328
- insights: [
329
- createInsight('1', 'anomaly', 'critical', 'Critical Issue', 'Critical description'),
330
- createInsight('2', 'near_match', 'warning', 'Warning Issue', 'Warning description'),
331
- createInsight('3', 'repeat_amount', 'info', 'Info Issue', 'Info description'),
332
- ],
333
- });
334
-
335
- const report = formatHumanReadableReport(analysis);
336
-
337
- expect(report).toContain('[CRITICAL] Critical Issue');
338
- expect(report).toContain('[WARN] Warning Issue');
339
- expect(report).toContain('[INFO] Info Issue');
340
- });
341
-
342
- it('should truncate insights list', () => {
343
- const insights: ReconciliationInsight[] = [];
344
- for (let i = 0; i < 10; i++) {
345
- insights.push(createInsight(`${i}`, 'anomaly', 'info', `Insight ${i}`, `Description ${i}`));
346
- }
347
-
348
- const analysis = createTestAnalysis({ insights });
349
- const report = formatHumanReadableReport(analysis, { maxInsightsToShow: 3 });
350
-
351
- expect(report).toContain('... and 7 more insights');
352
- });
353
-
354
- it('should include execution section when execution provided', () => {
355
- const analysis = createTestAnalysis();
356
- const execution = createExecutionResult({
357
- summary: {
358
- transactions_created: 2,
359
- transactions_updated: 3,
360
- dates_adjusted: 1,
361
- dry_run: false,
362
- },
363
- });
364
-
365
- const report = formatHumanReadableReport(analysis, {}, execution);
366
-
367
- expect(report).toContain('Execution Summary');
368
- expect(report).toContain('Transactions created: 2');
369
- expect(report).toContain('Transactions updated: 3');
370
- expect(report).toContain('Date adjustments: 1');
371
- expect(report).toContain('Changes applied to YNAB');
372
- });
373
-
374
- it('should show dry run notice when dry run enabled', () => {
375
- const analysis = createTestAnalysis();
376
- const execution = createExecutionResult({
377
- summary: {
378
- transactions_created: 0,
379
- transactions_updated: 0,
380
- dates_adjusted: 0,
381
- dry_run: true,
382
- },
383
- });
384
-
385
- const report = formatHumanReadableReport(analysis, {}, execution);
386
-
387
- expect(report).toContain('NOTE: Dry run only - no YNAB changes were applied.');
388
- });
389
-
390
- it('should show execution recommendations', () => {
391
- const analysis = createTestAnalysis();
392
- const execution = createExecutionResult({
393
- recommendations: [
394
- 'Create transaction for EvoCarShare',
395
- 'Review duplicate entries',
396
- 'Check bank fees',
397
- ],
398
- });
399
-
400
- const report = formatHumanReadableReport(analysis, {}, execution);
401
-
402
- expect(report).toContain('Recommendations:');
403
- expect(report).toContain('Create transaction for EvoCarShare');
404
- expect(report).toContain('Review duplicate entries');
405
- });
406
-
407
- it('should show next steps when no execution', () => {
408
- const analysis = createTestAnalysis({
409
- next_steps: [
410
- 'Create missing transaction for EvoCarShare',
411
- 'Mark 8 transactions as cleared',
412
- ],
413
- });
414
-
415
- const report = formatHumanReadableReport(analysis);
416
-
417
- expect(report).toContain('Recommended Actions');
418
- expect(report).toContain('Create missing transaction for EvoCarShare');
419
- expect(report).toContain('Mark 8 transactions as cleared');
420
- });
421
-
422
- it('should handle empty next steps gracefully', () => {
423
- const analysis = createTestAnalysis({
424
- next_steps: [],
425
- });
426
-
427
- const report = formatHumanReadableReport(analysis);
428
-
429
- expect(report).toContain('No specific actions recommended');
430
- });
431
-
432
- it('should use default account name when not provided', () => {
433
- const analysis = createTestAnalysis();
434
- const report = formatHumanReadableReport(analysis);
435
-
436
- expect(report).toContain('Account Reconciliation Report');
437
- });
438
- });
439
-
440
- describe('formatBalanceInfo', () => {
441
- it('should format balance info correctly', () => {
442
- const balanceInfo = {
443
- current_cleared: makeMoney(-899.02),
444
- current_uncleared: makeMoney(-50.0),
445
- current_total: makeMoney(-949.02),
446
- target_statement: makeMoney(-921.24),
447
- discrepancy: makeMoney(22.22),
448
- on_track: false,
449
- };
450
-
451
- const formatted = formatBalanceInfo(balanceInfo);
452
-
453
- expect(formatted).toContain('Current Cleared: -$899.02');
454
- expect(formatted).toContain('Current Total: -$949.02');
455
- expect(formatted).toContain('Target Statement: -$921.24');
456
- expect(formatted).toContain('Discrepancy: $22.22');
457
- });
458
- });
459
-
460
- describe('formatTransactionList', () => {
461
- it('should format bank transactions', () => {
462
- const transactions = [
463
- createBankTransaction('1', -45.23, 'Shell Gas', '2025-10-15'),
464
- createBankTransaction('2', -60.0, 'Amazon', '2025-10-20'),
465
- ];
466
-
467
- const formatted = formatTransactionList(transactions);
468
-
469
- expect(formatted).toContain('2025-10-15');
470
- expect(formatted).toContain('Shell Gas');
471
- expect(formatted).toContain('-$45.23');
472
- expect(formatted).toContain('Amazon');
473
- });
474
-
475
- it('should format YNAB transactions', () => {
476
- const transactions = [
477
- createYNABTransaction('1', -45230, 'Shell', '2025-10-15'),
478
- createYNABTransaction('2', -60000, 'Amazon', '2025-10-20'),
479
- ];
480
-
481
- const formatted = formatTransactionList(transactions);
482
-
483
- expect(formatted).toContain('2025-10-15');
484
- expect(formatted).toContain('Shell');
485
- expect(formatted).toContain('-$45.23');
486
- expect(formatted).toContain('Amazon');
487
- expect(formatted).toContain('-$60.00');
488
- });
489
-
490
- it('should truncate long lists', () => {
491
- const transactions: BankTransaction[] = [];
492
- for (let i = 0; i < 20; i++) {
493
- transactions.push(createBankTransaction(`${i}`, -10.0, `Payee ${i}`, '2025-10-15'));
494
- }
495
-
496
- const formatted = formatTransactionList(transactions, 5);
497
-
498
- expect(formatted).toContain('... and 15 more');
499
- });
500
-
501
- it('should handle empty list', () => {
502
- const formatted = formatTransactionList([]);
503
- expect(formatted).toBe('');
504
- });
505
- });
506
-
507
- describe('edge cases', () => {
508
- it('should handle negative amounts correctly', () => {
509
- const analysis = createTestAnalysis({
510
- unmatched_bank: [createBankTransaction('1', -123.45, 'Test', '2025-10-15')],
511
- });
512
-
513
- const report = formatHumanReadableReport(analysis);
514
- expect(report).toContain('-$123.45');
515
- });
516
-
517
- it('should handle positive amounts correctly', () => {
518
- const analysis = createTestAnalysis({
519
- unmatched_bank: [createBankTransaction('1', 123.45, 'Refund', '2025-10-15')],
520
- });
521
-
522
- const report = formatHumanReadableReport(analysis);
523
- expect(report).toContain('+$123.45');
524
- });
525
-
526
- it('should handle long payee names gracefully', () => {
527
- const longPayee = 'A'.repeat(100);
528
- const analysis = createTestAnalysis({
529
- unmatched_bank: [createBankTransaction('1', -10.0, longPayee, '2025-10-15')],
530
- });
531
-
532
- const report = formatHumanReadableReport(analysis);
533
- // Should truncate to 40 characters
534
- expect(report).toContain('A'.repeat(40));
535
- expect(report).not.toContain('A'.repeat(50));
536
- });
537
-
538
- it('should handle zero discrepancy', () => {
539
- const analysis = createTestAnalysis({
540
- balance_info: {
541
- current_cleared: makeMoney(-921.24),
542
- current_uncleared: makeMoney(0),
543
- current_total: makeMoney(-921.24),
544
- target_statement: makeMoney(-921.24),
545
- discrepancy: makeMoney(0),
546
- on_track: true,
547
- },
548
- });
549
-
550
- const report = formatHumanReadableReport(analysis);
551
- expect(report).toContain('Balances match perfectly.');
552
- });
553
-
554
- it('should format insight evidence when available', () => {
555
- const analysis = createTestAnalysis({
556
- insights: [
557
- {
558
- id: '1',
559
- type: 'repeat_amount',
560
- severity: 'warning',
561
- title: 'Repeated amount',
562
- description: 'Found duplicates',
563
- evidence: {
564
- transaction_count: 3,
565
- },
566
- },
567
- ],
568
- });
569
-
570
- const report = formatHumanReadableReport(analysis);
571
- expect(report).toContain('Evidence: 3 transactions');
572
- });
573
- });
152
+ describe("reportFormatter", () => {
153
+ describe("formatHumanReadableReport", () => {
154
+ it("should format a basic report with header and sections", () => {
155
+ const analysis = createTestAnalysis();
156
+ const options: ReportFormatterOptions = {
157
+ accountName: "Checking Account",
158
+ };
159
+
160
+ const report = formatHumanReadableReport(analysis, options);
161
+
162
+ expect(report).toContain("Checking Account Reconciliation Report");
163
+ expect(report).toContain("-".repeat(60));
164
+ expect(report).toContain("Balance Check");
165
+ expect(report).toContain("Transaction Analysis");
166
+ expect(report).toContain("Recommended Actions");
167
+ });
168
+
169
+ it("should show statement date range", () => {
170
+ const analysis = createTestAnalysis();
171
+ const report = formatHumanReadableReport(analysis);
172
+
173
+ expect(report).toContain("Statement Period: 2025-10-01 to 2025-10-31");
174
+ });
175
+
176
+ it("should show balanced status when no discrepancy", () => {
177
+ const analysis = createTestAnalysis({
178
+ balance_info: {
179
+ current_cleared: makeMoney(-921.24),
180
+ current_uncleared: makeMoney(0),
181
+ current_total: makeMoney(-921.24),
182
+ target_statement: makeMoney(-921.24),
183
+ discrepancy: makeMoney(0),
184
+ on_track: true,
185
+ },
186
+ summary: {
187
+ ...createTestAnalysis().summary,
188
+ discrepancy: makeMoney(0),
189
+ },
190
+ });
191
+
192
+ const report = formatHumanReadableReport(analysis);
193
+
194
+ expect(report).toContain("Balances match perfectly.");
195
+ expect(report).not.toContain("Discrepancy:");
196
+ });
197
+
198
+ it("should show discrepancy with correct direction when YNAB higher", () => {
199
+ const analysis = createTestAnalysis({
200
+ balance_info: {
201
+ current_cleared: makeMoney(-900.0),
202
+ current_uncleared: makeMoney(0),
203
+ current_total: makeMoney(-900.0),
204
+ target_statement: makeMoney(-920.0),
205
+ discrepancy: makeMoney(20.0), // Positive means YNAB higher
206
+ on_track: false,
207
+ },
208
+ summary: {
209
+ ...createTestAnalysis().summary,
210
+ discrepancy: makeMoney(20.0),
211
+ },
212
+ });
213
+
214
+ const report = formatHumanReadableReport(analysis);
215
+
216
+ expect(report).toContain("Discrepancy: $20.00");
217
+ expect(report).toContain("YNAB shows MORE than statement");
218
+ });
219
+
220
+ it("should show discrepancy with correct direction when bank higher", () => {
221
+ const analysis = createTestAnalysis({
222
+ balance_info: {
223
+ current_cleared: makeMoney(-920.0),
224
+ current_uncleared: makeMoney(0),
225
+ current_total: makeMoney(-920.0),
226
+ target_statement: makeMoney(-900.0),
227
+ discrepancy: makeMoney(-20.0), // Negative means bank higher
228
+ on_track: false,
229
+ },
230
+ summary: {
231
+ ...createTestAnalysis().summary,
232
+ discrepancy: makeMoney(-20.0),
233
+ },
234
+ });
235
+
236
+ const report = formatHumanReadableReport(analysis);
237
+
238
+ expect(report).toContain("Discrepancy: -$20.00");
239
+ expect(report).toContain("Statement shows MORE than YNAB");
240
+ });
241
+
242
+ it("should show transaction analysis counts", () => {
243
+ const analysis = createTestAnalysis();
244
+ const report = formatHumanReadableReport(analysis);
245
+
246
+ expect(report).toContain("Automatically matched: 8 of 10 transactions");
247
+ expect(report).toContain("Suggested matches: 1");
248
+ expect(report).toContain("Unmatched bank: 1");
249
+ expect(report).toContain("Unmatched YNAB: 3");
250
+ });
251
+
252
+ it("should list unmatched bank transactions", () => {
253
+ const analysis = createTestAnalysis({
254
+ unmatched_bank: [
255
+ createBankTransaction("bank-1", -22.22, "EvoCarShare", "2025-10-25"),
256
+ createBankTransaction("bank-2", -15.0, "Coffee Shop", "2025-10-26"),
257
+ ],
258
+ });
259
+
260
+ const report = formatHumanReadableReport(analysis);
261
+
262
+ expect(report).toContain(
263
+ "Missing from YNAB (bank transactions without matches):",
264
+ );
265
+ expect(report).toContain("2025-10-25");
266
+ expect(report).toContain("EvoCarShare");
267
+ expect(report).toContain("-$22.22");
268
+ expect(report).toContain("Coffee Shop");
269
+ });
270
+
271
+ it("should truncate long unmatched lists", () => {
272
+ const unmatchedBank: BankTransaction[] = [];
273
+ for (let i = 0; i < 10; i++) {
274
+ unmatchedBank.push(
275
+ createBankTransaction(`bank-${i}`, -10.0, `Payee ${i}`, "2025-10-15"),
276
+ );
277
+ }
278
+
279
+ const analysis = createTestAnalysis({ unmatched_bank: unmatchedBank });
280
+ const report = formatHumanReadableReport(analysis, {
281
+ maxUnmatchedToShow: 5,
282
+ });
283
+
284
+ expect(report).toContain("... and 5 more");
285
+ });
286
+
287
+ it("should show suggested matches", () => {
288
+ const analysis = createTestAnalysis({
289
+ suggested_matches: [
290
+ {
291
+ bankTransaction: createBankTransaction(
292
+ "bank-1",
293
+ -60.0,
294
+ "Amazon",
295
+ "2025-10-20",
296
+ ),
297
+ ynabTransaction: undefined,
298
+ candidates: [],
299
+ confidence: "medium",
300
+ confidenceScore: 75,
301
+ matchReason: "amount_and_date_fuzzy_payee",
302
+ topConfidence: 75,
303
+ },
304
+ ],
305
+ });
306
+
307
+ const report = formatHumanReadableReport(analysis);
308
+
309
+ expect(report).toContain("Suggested matches:");
310
+ expect(report).toContain("Amazon");
311
+ expect(report).toContain("75% confidence");
312
+ });
313
+
314
+ it("should include insights section when insights present", () => {
315
+ const analysis = createTestAnalysis({
316
+ insights: [
317
+ createInsight(
318
+ "1",
319
+ "repeat_amount",
320
+ "critical",
321
+ "Repeated amount detected",
322
+ "Found $22.22 appearing 3 times",
323
+ ),
324
+ createInsight(
325
+ "2",
326
+ "near_match",
327
+ "warning",
328
+ "Near match found",
329
+ "Transaction differs by only $0.50",
330
+ ),
331
+ ],
332
+ });
333
+
334
+ const report = formatHumanReadableReport(analysis);
335
+
336
+ expect(report).toContain("Key Insights");
337
+ expect(report).toContain("[CRITICAL] Repeated amount detected");
338
+ expect(report).toContain("[WARN] Near match found");
339
+ });
340
+
341
+ it("should use correct severity icons", () => {
342
+ const analysis = createTestAnalysis({
343
+ insights: [
344
+ createInsight(
345
+ "1",
346
+ "anomaly",
347
+ "critical",
348
+ "Critical Issue",
349
+ "Critical description",
350
+ ),
351
+ createInsight(
352
+ "2",
353
+ "near_match",
354
+ "warning",
355
+ "Warning Issue",
356
+ "Warning description",
357
+ ),
358
+ createInsight(
359
+ "3",
360
+ "repeat_amount",
361
+ "info",
362
+ "Info Issue",
363
+ "Info description",
364
+ ),
365
+ ],
366
+ });
367
+
368
+ const report = formatHumanReadableReport(analysis);
369
+
370
+ expect(report).toContain("[CRITICAL] Critical Issue");
371
+ expect(report).toContain("[WARN] Warning Issue");
372
+ expect(report).toContain("[INFO] Info Issue");
373
+ });
374
+
375
+ it("should truncate insights list", () => {
376
+ const insights: ReconciliationInsight[] = [];
377
+ for (let i = 0; i < 10; i++) {
378
+ insights.push(
379
+ createInsight(
380
+ `${i}`,
381
+ "anomaly",
382
+ "info",
383
+ `Insight ${i}`,
384
+ `Description ${i}`,
385
+ ),
386
+ );
387
+ }
388
+
389
+ const analysis = createTestAnalysis({ insights });
390
+ const report = formatHumanReadableReport(analysis, {
391
+ maxInsightsToShow: 3,
392
+ });
393
+
394
+ expect(report).toContain("... and 7 more insights");
395
+ });
396
+
397
+ it("should include execution section when execution provided", () => {
398
+ const analysis = createTestAnalysis();
399
+ const execution = createExecutionResult({
400
+ summary: {
401
+ transactions_created: 2,
402
+ transactions_updated: 3,
403
+ dates_adjusted: 1,
404
+ dry_run: false,
405
+ },
406
+ });
407
+
408
+ const report = formatHumanReadableReport(analysis, {}, execution);
409
+
410
+ expect(report).toContain("Execution Summary");
411
+ expect(report).toContain("Transactions created: 2");
412
+ expect(report).toContain("Transactions updated: 3");
413
+ expect(report).toContain("Date adjustments: 1");
414
+ expect(report).toContain("Changes applied to YNAB");
415
+ });
416
+
417
+ it("should show dry run notice when dry run enabled", () => {
418
+ const analysis = createTestAnalysis();
419
+ const execution = createExecutionResult({
420
+ summary: {
421
+ transactions_created: 0,
422
+ transactions_updated: 0,
423
+ dates_adjusted: 0,
424
+ dry_run: true,
425
+ },
426
+ });
427
+
428
+ const report = formatHumanReadableReport(analysis, {}, execution);
429
+
430
+ expect(report).toContain(
431
+ "NOTE: Dry run only - no YNAB changes were applied.",
432
+ );
433
+ });
434
+
435
+ it("should show execution recommendations", () => {
436
+ const analysis = createTestAnalysis();
437
+ const execution = createExecutionResult({
438
+ recommendations: [
439
+ "Create transaction for EvoCarShare",
440
+ "Review duplicate entries",
441
+ "Check bank fees",
442
+ ],
443
+ });
444
+
445
+ const report = formatHumanReadableReport(analysis, {}, execution);
446
+
447
+ expect(report).toContain("Recommendations:");
448
+ expect(report).toContain("Create transaction for EvoCarShare");
449
+ expect(report).toContain("Review duplicate entries");
450
+ });
451
+
452
+ it("should show next steps when no execution", () => {
453
+ const analysis = createTestAnalysis({
454
+ next_steps: [
455
+ "Create missing transaction for EvoCarShare",
456
+ "Mark 8 transactions as cleared",
457
+ ],
458
+ });
459
+
460
+ const report = formatHumanReadableReport(analysis);
461
+
462
+ expect(report).toContain("Recommended Actions");
463
+ expect(report).toContain("Create missing transaction for EvoCarShare");
464
+ expect(report).toContain("Mark 8 transactions as cleared");
465
+ });
466
+
467
+ it("should handle empty next steps gracefully", () => {
468
+ const analysis = createTestAnalysis({
469
+ next_steps: [],
470
+ });
471
+
472
+ const report = formatHumanReadableReport(analysis);
473
+
474
+ expect(report).toContain("No specific actions recommended");
475
+ });
476
+
477
+ it("should use default account name when not provided", () => {
478
+ const analysis = createTestAnalysis();
479
+ const report = formatHumanReadableReport(analysis);
480
+
481
+ expect(report).toContain("Account Reconciliation Report");
482
+ });
483
+ });
484
+
485
+ describe("formatBalanceInfo", () => {
486
+ it("should format balance info correctly", () => {
487
+ const balanceInfo = {
488
+ current_cleared: makeMoney(-899.02),
489
+ current_uncleared: makeMoney(-50.0),
490
+ current_total: makeMoney(-949.02),
491
+ target_statement: makeMoney(-921.24),
492
+ discrepancy: makeMoney(22.22),
493
+ on_track: false,
494
+ };
495
+
496
+ const formatted = formatBalanceInfo(balanceInfo);
497
+
498
+ expect(formatted).toContain("Current Cleared: -$899.02");
499
+ expect(formatted).toContain("Current Total: -$949.02");
500
+ expect(formatted).toContain("Target Statement: -$921.24");
501
+ expect(formatted).toContain("Discrepancy: $22.22");
502
+ });
503
+ });
504
+
505
+ describe("formatTransactionList", () => {
506
+ it("should format bank transactions", () => {
507
+ const transactions = [
508
+ createBankTransaction("1", -45.23, "Shell Gas", "2025-10-15"),
509
+ createBankTransaction("2", -60.0, "Amazon", "2025-10-20"),
510
+ ];
511
+
512
+ const formatted = formatTransactionList(transactions);
513
+
514
+ expect(formatted).toContain("2025-10-15");
515
+ expect(formatted).toContain("Shell Gas");
516
+ expect(formatted).toContain("-$45.23");
517
+ expect(formatted).toContain("Amazon");
518
+ });
519
+
520
+ it("should format YNAB transactions", () => {
521
+ const transactions = [
522
+ createYNABTransaction("1", -45230, "Shell", "2025-10-15"),
523
+ createYNABTransaction("2", -60000, "Amazon", "2025-10-20"),
524
+ ];
525
+
526
+ const formatted = formatTransactionList(transactions);
527
+
528
+ expect(formatted).toContain("2025-10-15");
529
+ expect(formatted).toContain("Shell");
530
+ expect(formatted).toContain("-$45.23");
531
+ expect(formatted).toContain("Amazon");
532
+ expect(formatted).toContain("-$60.00");
533
+ });
534
+
535
+ it("should truncate long lists", () => {
536
+ const transactions: BankTransaction[] = [];
537
+ for (let i = 0; i < 20; i++) {
538
+ transactions.push(
539
+ createBankTransaction(`${i}`, -10.0, `Payee ${i}`, "2025-10-15"),
540
+ );
541
+ }
542
+
543
+ const formatted = formatTransactionList(transactions, 5);
544
+
545
+ expect(formatted).toContain("... and 15 more");
546
+ });
547
+
548
+ it("should handle empty list", () => {
549
+ const formatted = formatTransactionList([]);
550
+ expect(formatted).toBe("");
551
+ });
552
+ });
553
+
554
+ describe("edge cases", () => {
555
+ it("should handle negative amounts correctly", () => {
556
+ const analysis = createTestAnalysis({
557
+ unmatched_bank: [
558
+ createBankTransaction("1", -123.45, "Test", "2025-10-15"),
559
+ ],
560
+ });
561
+
562
+ const report = formatHumanReadableReport(analysis);
563
+ expect(report).toContain("-$123.45");
564
+ });
565
+
566
+ it("should handle positive amounts correctly", () => {
567
+ const analysis = createTestAnalysis({
568
+ unmatched_bank: [
569
+ createBankTransaction("1", 123.45, "Refund", "2025-10-15"),
570
+ ],
571
+ });
572
+
573
+ const report = formatHumanReadableReport(analysis);
574
+ expect(report).toContain("+$123.45");
575
+ });
576
+
577
+ it("should handle long payee names gracefully", () => {
578
+ const longPayee = "A".repeat(100);
579
+ const analysis = createTestAnalysis({
580
+ unmatched_bank: [
581
+ createBankTransaction("1", -10.0, longPayee, "2025-10-15"),
582
+ ],
583
+ });
584
+
585
+ const report = formatHumanReadableReport(analysis);
586
+ // Should truncate to 40 characters
587
+ expect(report).toContain("A".repeat(40));
588
+ expect(report).not.toContain("A".repeat(50));
589
+ });
590
+
591
+ it("should handle zero discrepancy", () => {
592
+ const analysis = createTestAnalysis({
593
+ balance_info: {
594
+ current_cleared: makeMoney(-921.24),
595
+ current_uncleared: makeMoney(0),
596
+ current_total: makeMoney(-921.24),
597
+ target_statement: makeMoney(-921.24),
598
+ discrepancy: makeMoney(0),
599
+ on_track: true,
600
+ },
601
+ });
602
+
603
+ const report = formatHumanReadableReport(analysis);
604
+ expect(report).toContain("Balances match perfectly.");
605
+ });
606
+
607
+ it("should format insight evidence when available", () => {
608
+ const analysis = createTestAnalysis({
609
+ insights: [
610
+ {
611
+ id: "1",
612
+ type: "repeat_amount",
613
+ severity: "warning",
614
+ title: "Repeated amount",
615
+ description: "Found duplicates",
616
+ evidence: {
617
+ transaction_count: 3,
618
+ },
619
+ },
620
+ ],
621
+ });
622
+
623
+ const report = formatHumanReadableReport(analysis);
624
+ expect(report).toContain("Evidence: 3 transactions");
625
+ });
626
+ });
574
627
  });