@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,6 +1,6 @@
1
- import { YNABAPIError } from '../../server/errorHandler.js';
2
- import { toMilli, toMoneyValue, addMilli } from '../../utils/money.js';
3
- import { generateCorrelationKey, correlateResults, toCorrelationPayload, } from '../transactionTools.js';
1
+ import { YNABAPIError, YNABErrorCode } from "../../server/errorHandler.js";
2
+ import { addMilli, toMilli, toMoneyValue } from "../../utils/money.js";
3
+ import { correlateResults, generateCorrelationKey, toCorrelationPayload, } from "../transactionTools.js";
4
4
  const MONEY_EPSILON_MILLI = 100;
5
5
  const DEFAULT_TOLERANCE_CENTS = 1;
6
6
  const CENTS_TO_MILLI = 10;
@@ -10,7 +10,7 @@ const BATCH_DELAY_MS = 200;
10
10
  const MAX_MEMO_LENGTH = 500;
11
11
  function chunkArray(array, size) {
12
12
  if (size <= 0) {
13
- throw new Error('chunk size must be positive');
13
+ throw new Error("chunk size must be positive");
14
14
  }
15
15
  const chunks = [];
16
16
  for (let i = 0; i < array.length; i += size) {
@@ -23,10 +23,10 @@ function sleep(ms) {
23
23
  }
24
24
  function truncateMemo(memo) {
25
25
  if (!memo)
26
- return 'Auto-reconciled from bank statement';
26
+ return "Auto-reconciled from bank statement";
27
27
  if (memo.length <= MAX_MEMO_LENGTH)
28
28
  return memo;
29
- return memo.substring(0, MAX_MEMO_LENGTH - 3) + '...';
29
+ return `${memo.substring(0, MAX_MEMO_LENGTH - 3)}...`;
30
30
  }
31
31
  function parseISODate(dateStr) {
32
32
  if (!dateStr)
@@ -46,8 +46,10 @@ function resolveStatementWindow(params, analysisDateRange) {
46
46
  window.end = end;
47
47
  return window;
48
48
  }
49
- if (analysisDateRange && analysisDateRange.includes(' to ')) {
50
- const [rawStart, rawEnd] = analysisDateRange.split(' to ').map((part) => part.trim());
49
+ if (analysisDateRange?.includes(" to ")) {
50
+ const [rawStart, rawEnd] = analysisDateRange
51
+ .split(" to ")
52
+ .map((part) => part.trim());
51
53
  const parsedStart = parseISODate(rawStart);
52
54
  const parsedEnd = parseISODate(rawEnd);
53
55
  if (parsedStart || parsedEnd) {
@@ -106,7 +108,8 @@ export async function executeReconciliation(options) {
106
108
  let accountSnapshotDirty = false;
107
109
  const statementTargetMilli = resolveStatementBalanceMilli(analysis.balance_info, params.statement_balance);
108
110
  let clearedDeltaMilli = addMilli(initialAccount.cleared_balance ?? 0, -statementTargetMilli);
109
- const balanceToleranceMilli = Math.max(0, params.amount_tolerance_cents ?? DEFAULT_TOLERANCE_CENTS) * CENTS_TO_MILLI;
111
+ const balanceToleranceMilli = Math.max(0, params.amount_tolerance_cents ?? DEFAULT_TOLERANCE_CENTS) *
112
+ CENTS_TO_MILLI;
110
113
  let balanceAligned = false;
111
114
  const applyClearedDelta = (delta) => {
112
115
  if (delta === 0)
@@ -123,7 +126,7 @@ export async function executeReconciliation(options) {
123
126
  const deltaDisplay = toMoneyValue(clearedDeltaMilli, currencyCode).value_display;
124
127
  const toleranceDisplay = toMoneyValue(balanceToleranceMilli, currencyCode).value_display;
125
128
  actions_taken.push({
126
- type: 'balance_checkpoint',
129
+ type: "balance_checkpoint",
127
130
  transaction: null,
128
131
  reason: `Cleared delta ${deltaDisplay} within ±${toleranceDisplay} after ${trigger} - halting newest-to-oldest pass`,
129
132
  });
@@ -132,7 +135,7 @@ export async function executeReconciliation(options) {
132
135
  }
133
136
  return false;
134
137
  };
135
- recordAlignmentIfNeeded('initial balance check', { log: false });
138
+ recordAlignmentIfNeeded("initial balance check", { log: false });
136
139
  const orderedUnmatchedBank = params.auto_create_transactions
137
140
  ? sortByDateDescending(analysis.unmatched_bank)
138
141
  : [];
@@ -151,7 +154,7 @@ export async function executeReconciliation(options) {
151
154
  date: bankTxn.date,
152
155
  payee_name: bankTxn.payee ?? undefined,
153
156
  memo: truncateMemo(bankTxn.memo),
154
- cleared: 'cleared',
157
+ cleared: "cleared",
155
158
  approved: true,
156
159
  };
157
160
  const correlationKey = generateCorrelationKey(toCorrelationPayload(saveTransaction));
@@ -166,9 +169,9 @@ export async function executeReconciliation(options) {
166
169
  const { entry, createdTxn, chunkIndex, prefix } = args;
167
170
  summary.transactions_created += 1;
168
171
  const action = {
169
- type: 'create_transaction',
172
+ type: "create_transaction",
170
173
  transaction: createdTxn,
171
- reason: `${prefix ?? 'Created missing transaction'}: ${entry.bankTransaction.payee ?? 'Unknown'} (${formatDisplay(entry.bankTransaction.amount, currencyCode)})`,
174
+ reason: `${prefix ?? "Created missing transaction"}: ${entry.bankTransaction.payee ?? "Unknown"} (${formatDisplay(entry.bankTransaction.amount, currencyCode)})`,
172
175
  correlation_key: entry.correlationKey,
173
176
  };
174
177
  if (chunkIndex !== undefined) {
@@ -193,8 +196,8 @@ export async function executeReconciliation(options) {
193
196
  entry,
194
197
  createdTxn: createdTransaction,
195
198
  prefix: options.fallbackError
196
- ? 'Created missing transaction after bulk fallback'
197
- : 'Created missing transaction',
199
+ ? "Created missing transaction after bulk fallback"
200
+ : "Created missing transaction",
198
201
  };
199
202
  if (options.chunkIndex !== undefined) {
200
203
  recordArgs.chunkIndex = options.chunkIndex;
@@ -205,8 +208,8 @@ export async function executeReconciliation(options) {
205
208
  completedOperations += 1;
206
209
  await reportProgress(`Created ${completedOperations} of ${totalOperations} transactions`);
207
210
  const trigger = options.chunkIndex
208
- ? `creating ${entry.bankTransaction.payee ?? 'missing transaction'} (chunk ${options.chunkIndex})`
209
- : `creating ${entry.bankTransaction.payee ?? 'missing transaction'}`;
211
+ ? `creating ${entry.bankTransaction.payee ?? "missing transaction"} (chunk ${options.chunkIndex})`
212
+ : `creating ${entry.bankTransaction.payee ?? "missing transaction"}`;
210
213
  recordAlignmentIfNeeded(trigger);
211
214
  }
212
215
  catch (error) {
@@ -214,14 +217,16 @@ export async function executeReconciliation(options) {
214
217
  if (bulkOperationDetails) {
215
218
  bulkOperationDetails.transaction_failures += 1;
216
219
  }
217
- const failureReason = ynabError.message || 'Unknown error occurred';
218
- const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
220
+ const failureReason = ynabError.message || "Unknown error occurred";
221
+ const statusSuffix = ynabError.status
222
+ ? ` (HTTP ${ynabError.status})`
223
+ : "";
219
224
  const failureAction = {
220
- type: 'create_transaction_failed',
225
+ type: "create_transaction_failed",
221
226
  transaction: entry.saveTransaction,
222
227
  reason: options.fallbackError
223
- ? `Bulk fallback failed for ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason}${statusSuffix})`
224
- : `Failed to create transaction ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason}${statusSuffix})`,
228
+ ? `Bulk fallback failed for ${entry.bankTransaction.payee ?? "Unknown"} (${failureReason}${statusSuffix})`
229
+ : `Failed to create transaction ${entry.bankTransaction.payee ?? "Unknown"} (${failureReason}${statusSuffix})`,
225
230
  correlation_key: entry.correlationKey,
226
231
  };
227
232
  if (options.chunkIndex !== undefined) {
@@ -233,12 +238,17 @@ export async function executeReconciliation(options) {
233
238
  }
234
239
  }
235
240
  }
236
- if (bulkOperationDetails && options.fallbackError && sequentialAttempts > 0) {
241
+ if (bulkOperationDetails &&
242
+ options.fallbackError &&
243
+ sequentialAttempts > 0) {
237
244
  bulkOperationDetails.sequential_attempts =
238
245
  (bulkOperationDetails.sequential_attempts ?? 0) + sequentialAttempts;
239
246
  }
240
247
  };
241
248
  const processBulkChunk = async (chunk, chunkIndex) => {
249
+ if (!bulkOperationDetails) {
250
+ throw new Error("Bulk operation details not initialized");
251
+ }
242
252
  const bulkDetails = bulkOperationDetails;
243
253
  const payload = chunk.map((entry) => entry.saveTransaction);
244
254
  const response = await ynabAPI.transactions.createTransactions(budgetId, {
@@ -258,7 +268,7 @@ export async function executeReconciliation(options) {
258
268
  const entry = chunk[result.request_index];
259
269
  if (!entry)
260
270
  continue;
261
- if (result.status === 'created') {
271
+ if (result.status === "created") {
262
272
  const createdTransaction = result.transaction_id
263
273
  ? (transactionMap.get(result.transaction_id) ?? null)
264
274
  : null;
@@ -266,21 +276,21 @@ export async function executeReconciliation(options) {
266
276
  entry,
267
277
  createdTxn: createdTransaction,
268
278
  chunkIndex,
269
- prefix: 'Created missing transaction via bulk',
279
+ prefix: "Created missing transaction via bulk",
270
280
  });
271
281
  accountSnapshotDirty = true;
272
282
  applyClearedDelta(entry.amountMilli);
273
- recordAlignmentIfNeeded(`creating ${entry.bankTransaction.payee ?? 'missing transaction'} via bulk chunk ${chunkIndex}`);
283
+ recordAlignmentIfNeeded(`creating ${entry.bankTransaction.payee ?? "missing transaction"} via bulk chunk ${chunkIndex}`);
274
284
  }
275
- else if (result.status === 'duplicate') {
285
+ else if (result.status === "duplicate") {
276
286
  bulkDetails.duplicates_detected += 1;
277
287
  actions_taken.push({
278
- type: 'create_transaction_duplicate',
288
+ type: "create_transaction_duplicate",
279
289
  transaction: {
280
290
  transaction_id: result.transaction_id ?? null,
281
291
  import_id: entry.saveTransaction.import_id,
282
292
  },
283
- reason: `Duplicate import detected for ${entry.bankTransaction.payee ?? 'Unknown'} (import_id ${entry.saveTransaction.import_id})`,
293
+ reason: `Duplicate import detected for ${entry.bankTransaction.payee ?? "Unknown"} (import_id ${entry.saveTransaction.import_id})`,
284
294
  bulk_chunk_index: chunkIndex,
285
295
  correlation_key: result.correlation_key,
286
296
  duplicate: true,
@@ -289,9 +299,10 @@ export async function executeReconciliation(options) {
289
299
  else {
290
300
  bulkDetails.transaction_failures += 1;
291
301
  actions_taken.push({
292
- type: 'create_transaction_failed',
302
+ type: "create_transaction_failed",
293
303
  transaction: entry.saveTransaction,
294
- reason: result.error ?? `Bulk create failed for ${entry.bankTransaction.payee ?? 'Unknown'}`,
304
+ reason: result.error ??
305
+ `Bulk create failed for ${entry.bankTransaction.payee ?? "Unknown"}`,
295
306
  bulk_chunk_index: chunkIndex,
296
307
  correlation_key: result.correlation_key,
297
308
  });
@@ -305,13 +316,13 @@ export async function executeReconciliation(options) {
305
316
  const entry = buildPreparedEntry(bankTxn);
306
317
  summary.transactions_created += 1;
307
318
  actions_taken.push({
308
- type: 'create_transaction',
319
+ type: "create_transaction",
309
320
  transaction: entry.saveTransaction,
310
- reason: `Would create missing transaction: ${bankTxn.payee ?? 'Unknown'} (${formatDisplay(bankTxn.amount, currencyCode)})`,
321
+ reason: `Would create missing transaction: ${bankTxn.payee ?? "Unknown"} (${formatDisplay(bankTxn.amount, currencyCode)})`,
311
322
  correlation_key: entry.correlationKey,
312
323
  });
313
324
  applyClearedDelta(entry.amountMilli);
314
- recordAlignmentIfNeeded(`creating ${bankTxn.payee ?? 'missing transaction'}`);
325
+ recordAlignmentIfNeeded(`creating ${bankTxn.payee ?? "missing transaction"}`);
315
326
  }
316
327
  }
317
328
  else if (orderedUnmatchedBank.length >= 2) {
@@ -359,7 +370,7 @@ export async function executeReconciliation(options) {
359
370
  }
360
371
  catch (error) {
361
372
  const ynabError = normalizeYnabError(error);
362
- const failureReason = ynabError.message || 'unknown error';
373
+ const failureReason = ynabError.message || "unknown error";
363
374
  bulkOperationDetails.bulk_chunk_failures += 1;
364
375
  if (shouldPropagateYnabError(ynabError)) {
365
376
  bulkOperationDetails.transaction_failures += chunk.length;
@@ -367,12 +378,15 @@ export async function executeReconciliation(options) {
367
378
  }
368
379
  bulkOperationDetails.sequential_fallbacks += 1;
369
380
  actions_taken.push({
370
- type: 'bulk_create_fallback',
381
+ type: "bulk_create_fallback",
371
382
  transaction: null,
372
- reason: `Bulk chunk #${chunkIndex} failed (${failureReason}${ynabError.status ? ` (HTTP ${ynabError.status})` : ''}) - falling back to sequential creation`,
383
+ reason: `Bulk chunk #${chunkIndex} failed (${failureReason}${ynabError.status ? ` (HTTP ${ynabError.status})` : ""}) - falling back to sequential creation`,
373
384
  bulk_chunk_index: chunkIndex,
374
385
  });
375
- await processSequentialEntries(chunk, { chunkIndex, fallbackError: ynabError });
386
+ await processSequentialEntries(chunk, {
387
+ chunkIndex,
388
+ fallbackError: ynabError,
389
+ });
376
390
  }
377
391
  }
378
392
  }
@@ -402,24 +416,26 @@ export async function executeReconciliation(options) {
402
416
  updatePayload.date = match.bankTransaction.date;
403
417
  }
404
418
  if (flags.needsClearedUpdate) {
405
- updatePayload.cleared = 'cleared';
419
+ updatePayload.cleared = "cleared";
406
420
  }
407
421
  if (params.dry_run) {
408
422
  summary.transactions_updated += 1;
409
423
  if (flags.needsDateUpdate)
410
424
  summary.dates_adjusted += 1;
411
425
  actions_taken.push({
412
- type: 'update_transaction',
426
+ type: "update_transaction",
413
427
  transaction: {
414
428
  transaction_id: match.ynabTransaction.id,
415
- new_date: flags.needsDateUpdate ? match.bankTransaction.date : undefined,
416
- cleared: flags.needsClearedUpdate ? 'cleared' : undefined,
429
+ new_date: flags.needsDateUpdate
430
+ ? match.bankTransaction.date
431
+ : undefined,
432
+ cleared: flags.needsClearedUpdate ? "cleared" : undefined,
417
433
  },
418
434
  reason: `Would update transaction: ${updateReason(match, flags, currencyCode)}`,
419
435
  });
420
436
  if (flags.needsClearedUpdate) {
421
437
  applyClearedDelta(match.ynabTransaction.amount);
422
- if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id ?? 'transaction'} (dry run)`)) {
438
+ if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id ?? "transaction"} (dry run)`)) {
423
439
  break;
424
440
  }
425
441
  }
@@ -440,6 +456,8 @@ export async function executeReconciliation(options) {
440
456
  const updateChunks = chunkArray(transactionsToUpdate, MAX_BULK_UPDATE_CHUNK);
441
457
  for (let chunkIdx = 0; chunkIdx < updateChunks.length; chunkIdx++) {
442
458
  const chunk = updateChunks[chunkIdx];
459
+ if (!chunk)
460
+ continue;
443
461
  try {
444
462
  const response = await ynabAPI.transactions.updateTransactions(budgetId, {
445
463
  transactions: chunk,
@@ -452,9 +470,9 @@ export async function executeReconciliation(options) {
452
470
  ? computeUpdateFlags(match, params)
453
471
  : { needsClearedUpdate: false, needsDateUpdate: false };
454
472
  actions_taken.push({
455
- type: 'update_transaction',
473
+ type: "update_transaction",
456
474
  transaction: updatedTransaction,
457
- reason: `Updated transaction: ${match ? updateReason(match, flags, currencyCode) : 'cleared'}`,
475
+ reason: `Updated transaction: ${match ? updateReason(match, flags, currencyCode) : "cleared"}`,
458
476
  });
459
477
  }
460
478
  accountSnapshotDirty = true;
@@ -463,10 +481,12 @@ export async function executeReconciliation(options) {
463
481
  }
464
482
  catch (error) {
465
483
  const ynabError = normalizeYnabError(error);
466
- const failureReason = ynabError.message || 'Unknown error occurred';
467
- const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
484
+ const failureReason = ynabError.message || "Unknown error occurred";
485
+ const statusSuffix = ynabError.status
486
+ ? ` (HTTP ${ynabError.status})`
487
+ : "";
468
488
  actions_taken.push({
469
- type: 'batch_update_failed',
489
+ type: "batch_update_failed",
470
490
  transaction: null,
471
491
  reason: `Failed to update chunk ${chunkIdx + 1}/${updateChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
472
492
  });
@@ -482,7 +502,7 @@ export async function executeReconciliation(options) {
482
502
  }
483
503
  const shouldRunSanityPass = params.auto_unclear_missing && !balanceAligned;
484
504
  actions_taken.push({
485
- type: 'diagnostic_step3_entry',
505
+ type: "diagnostic_step3_entry",
486
506
  transaction: null,
487
507
  reason: `STEP 3 diagnostics: auto_unclear_missing=${params.auto_unclear_missing}, balanceAligned=${balanceAligned}, shouldRunSanityPass=${shouldRunSanityPass}, orderedUnmatchedYNAB.length=${orderedUnmatchedYNAB.length}`,
488
508
  });
@@ -492,26 +512,28 @@ export async function executeReconciliation(options) {
492
512
  date: t.date,
493
513
  cleared: t.cleared,
494
514
  amount: formatDisplay(t.amount, currencyCode),
495
- payee: t.payee ?? 'Unknown',
515
+ payee: t.payee ?? "Unknown",
496
516
  }));
497
517
  actions_taken.push({
498
- type: 'diagnostic_unmatched_ynab',
499
- transaction: { unmatched_transactions: unmatchedDetails },
518
+ type: "diagnostic_unmatched_ynab",
519
+ transaction: {
520
+ unmatched_transactions: unmatchedDetails,
521
+ },
500
522
  reason: `First ${Math.min(10, orderedUnmatchedYNAB.length)} unmatched YNAB transactions (cleared status and amounts)`,
501
523
  });
502
524
  }
503
525
  if (shouldRunSanityPass) {
504
526
  const transactionsToUnclear = [];
505
527
  for (const ynabTxn of orderedUnmatchedYNAB) {
506
- if (ynabTxn.cleared !== 'cleared')
528
+ if (ynabTxn.cleared !== "cleared")
507
529
  continue;
508
530
  if (balanceAligned)
509
531
  break;
510
532
  if (params.dry_run) {
511
533
  summary.transactions_updated += 1;
512
534
  actions_taken.push({
513
- type: 'update_transaction',
514
- transaction: { transaction_id: ynabTxn.id, cleared: 'uncleared' },
535
+ type: "update_transaction",
536
+ transaction: { transaction_id: ynabTxn.id, cleared: "uncleared" },
515
537
  reason: `Would mark transaction ${ynabTxn.id} as uncleared - not present on statement`,
516
538
  });
517
539
  applyClearedDelta(-ynabTxn.amount);
@@ -522,7 +544,7 @@ export async function executeReconciliation(options) {
522
544
  else {
523
545
  transactionsToUnclear.push({
524
546
  id: ynabTxn.id,
525
- cleared: 'uncleared',
547
+ cleared: "uncleared",
526
548
  });
527
549
  applyClearedDelta(-ynabTxn.amount);
528
550
  if (recordAlignmentIfNeeded(`unclearing ${ynabTxn.id}`)) {
@@ -534,6 +556,8 @@ export async function executeReconciliation(options) {
534
556
  const unclearChunks = chunkArray(transactionsToUnclear, MAX_BULK_UPDATE_CHUNK);
535
557
  for (let chunkIdx = 0; chunkIdx < unclearChunks.length; chunkIdx++) {
536
558
  const chunk = unclearChunks[chunkIdx];
559
+ if (!chunk)
560
+ continue;
537
561
  try {
538
562
  const response = await ynabAPI.transactions.updateTransactions(budgetId, {
539
563
  transactions: chunk,
@@ -542,7 +566,7 @@ export async function executeReconciliation(options) {
542
566
  summary.transactions_updated += updatedTransactions.length;
543
567
  for (const updatedTransaction of updatedTransactions) {
544
568
  actions_taken.push({
545
- type: 'update_transaction',
569
+ type: "update_transaction",
546
570
  transaction: updatedTransaction,
547
571
  reason: `Marked transaction ${updatedTransaction.id} as uncleared - not found on statement`,
548
572
  });
@@ -553,10 +577,12 @@ export async function executeReconciliation(options) {
553
577
  }
554
578
  catch (error) {
555
579
  const ynabError = normalizeYnabError(error);
556
- const failureReason = ynabError.message || 'Unknown error occurred';
557
- const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
580
+ const failureReason = ynabError.message || "Unknown error occurred";
581
+ const statusSuffix = ynabError.status
582
+ ? ` (HTTP ${ynabError.status})`
583
+ : "";
558
584
  actions_taken.push({
559
- type: 'batch_unclear_failed',
585
+ type: "batch_unclear_failed",
560
586
  transaction: null,
561
587
  reason: `Failed to unclear chunk ${chunkIdx + 1}/${unclearChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
562
588
  });
@@ -575,17 +601,19 @@ export async function executeReconciliation(options) {
575
601
  for (const match of orderedAutoMatches) {
576
602
  if (!match.ynabTransaction)
577
603
  continue;
578
- if (match.ynabTransaction.cleared === 'reconciled')
604
+ if (match.ynabTransaction.cleared === "reconciled")
579
605
  continue;
580
606
  transactionsToReconcile.push({
581
607
  id: match.ynabTransaction.id,
582
- cleared: 'reconciled',
608
+ cleared: "reconciled",
583
609
  });
584
610
  }
585
611
  if (transactionsToReconcile.length > 0) {
586
612
  const reconcileChunks = chunkArray(transactionsToReconcile, MAX_BULK_UPDATE_CHUNK);
587
613
  for (let chunkIdx = 0; chunkIdx < reconcileChunks.length; chunkIdx++) {
588
614
  const chunk = reconcileChunks[chunkIdx];
615
+ if (!chunk)
616
+ continue;
589
617
  try {
590
618
  const response = await ynabAPI.transactions.updateTransactions(budgetId, {
591
619
  transactions: chunk,
@@ -595,19 +623,21 @@ export async function executeReconciliation(options) {
595
623
  for (const reconciledTransaction of reconciledTransactions) {
596
624
  const match = orderedAutoMatches.find((m) => m.ynabTransaction?.id === reconciledTransaction.id);
597
625
  actions_taken.push({
598
- type: 'update_transaction',
626
+ type: "update_transaction",
599
627
  transaction: reconciledTransaction,
600
- reason: `Marked as reconciled: ${match?.bankTransaction.payee ?? 'transaction'} (${formatDisplay(reconciledTransaction.amount, currencyCode)})`,
628
+ reason: `Marked as reconciled: ${match?.bankTransaction.payee ?? "transaction"} (${formatDisplay(reconciledTransaction.amount, currencyCode)})`,
601
629
  });
602
630
  }
603
631
  accountSnapshotDirty = true;
604
632
  }
605
633
  catch (error) {
606
634
  const ynabError = normalizeYnabError(error);
607
- const failureReason = ynabError.message || 'Unknown error occurred';
608
- const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
635
+ const failureReason = ynabError.message || "Unknown error occurred";
636
+ const statusSuffix = ynabError.status
637
+ ? ` (HTTP ${ynabError.status})`
638
+ : "";
609
639
  actions_taken.push({
610
- type: 'batch_reconcile_failed',
640
+ type: "batch_reconcile_failed",
611
641
  transaction: null,
612
642
  reason: `Failed to reconcile chunk ${chunkIdx + 1}/${reconcileChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
613
643
  });
@@ -620,7 +650,7 @@ export async function executeReconciliation(options) {
620
650
  }
621
651
  }
622
652
  actions_taken.push({
623
- type: 'reconciliation_complete',
653
+ type: "reconciliation_complete",
624
654
  transaction: null,
625
655
  reason: `Marked ${transactionsToReconcile.length} matched transaction(s) as reconciled - balance aligned within tolerance`,
626
656
  });
@@ -640,7 +670,9 @@ export async function executeReconciliation(options) {
640
670
  if (!params.dry_run && accountSnapshotDirty) {
641
671
  afterAccount = await refreshAccountSnapshot(ynabAPI, budgetId, accountId);
642
672
  }
643
- const balanceChangeMilli = params.dry_run || !accountSnapshotDirty ? 0 : afterAccount.balance - initialAccount.balance;
673
+ const balanceChangeMilli = params.dry_run || !accountSnapshotDirty
674
+ ? 0
675
+ : afterAccount.balance - initialAccount.balance;
644
676
  const recommendations = buildRecommendations({
645
677
  summary,
646
678
  params,
@@ -661,7 +693,8 @@ export async function executeReconciliation(options) {
661
693
  result.balance_reconciliation = balance_reconciliation;
662
694
  }
663
695
  if (bulkOperationDetails) {
664
- bulkOperationDetails.failed_transactions = bulkOperationDetails.transaction_failures;
696
+ bulkOperationDetails.failed_transactions =
697
+ bulkOperationDetails.transaction_failures;
665
698
  result.bulk_operation_details = bulkOperationDetails;
666
699
  }
667
700
  return result;
@@ -669,9 +702,9 @@ export async function executeReconciliation(options) {
669
702
  const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500, 503]);
670
703
  export function normalizeYnabError(error) {
671
704
  const parseStatus = (value) => {
672
- if (typeof value === 'number' && Number.isFinite(value))
705
+ if (typeof value === "number" && Number.isFinite(value))
673
706
  return value;
674
- if (typeof value === 'string') {
707
+ if (typeof value === "string") {
675
708
  const numeric = Number(value);
676
709
  if (Number.isFinite(numeric))
677
710
  return numeric;
@@ -682,10 +715,12 @@ export function normalizeYnabError(error) {
682
715
  const status = parseStatus(error.status) ??
683
716
  parseStatus(error.response?.status);
684
717
  const detailSource = error.detail;
685
- const detail = typeof detailSource === 'string' && detailSource.trim().length > 0 ? detailSource : undefined;
718
+ const detail = typeof detailSource === "string" && detailSource.trim().length > 0
719
+ ? detailSource
720
+ : undefined;
686
721
  const result = {
687
722
  name: error.name,
688
- message: error.message || 'Unknown error occurred',
723
+ message: error.message || "Unknown error occurred",
689
724
  };
690
725
  if (status !== undefined)
691
726
  result.status = status;
@@ -693,18 +728,21 @@ export function normalizeYnabError(error) {
693
728
  result.detail = detail;
694
729
  return result;
695
730
  }
696
- if (error && typeof error === 'object') {
731
+ if (error && typeof error === "object") {
697
732
  const errObj = error.error ?? error;
698
- const status = parseStatus(errObj.id ?? errObj.status);
733
+ const status = parseStatus(errObj.id ??
734
+ errObj.status);
699
735
  const detailCandidate = errObj.detail ??
700
736
  errObj.message ??
701
737
  errObj.name;
702
- const detail = typeof detailCandidate === 'string' && detailCandidate.trim().length > 0
738
+ const detail = typeof detailCandidate === "string" && detailCandidate.trim().length > 0
703
739
  ? detailCandidate
704
740
  : undefined;
705
741
  const message = detail ??
706
- (typeof errObj === 'string' && errObj.trim().length > 0 ? errObj : 'Unknown error occurred');
707
- const name = typeof errObj.name === 'string'
742
+ (typeof errObj === "string" && errObj.trim().length > 0
743
+ ? errObj
744
+ : "Unknown error occurred");
745
+ const name = typeof errObj.name === "string"
708
746
  ? errObj.name
709
747
  : undefined;
710
748
  const result = { message };
@@ -716,27 +754,27 @@ export function normalizeYnabError(error) {
716
754
  result.detail = detail;
717
755
  return result;
718
756
  }
719
- if (typeof error === 'string') {
757
+ if (typeof error === "string") {
720
758
  return { message: error };
721
759
  }
722
- return { message: 'Unknown error occurred' };
760
+ return { message: "Unknown error occurred" };
723
761
  }
724
762
  export function shouldPropagateYnabError(error) {
725
- return error.status !== undefined && FATAL_YNAB_STATUS_CODES.has(error.status);
763
+ return (error.status !== undefined && FATAL_YNAB_STATUS_CODES.has(error.status));
726
764
  }
727
765
  function attachStatusToError(error, originalError) {
728
- const message = error.message || 'YNAB API error';
729
- const isKnownCode = error.status === 400 ||
730
- error.status === 401 ||
731
- error.status === 403 ||
732
- error.status === 404 ||
733
- error.status === 429 ||
734
- error.status === 500;
766
+ const message = error.message || "YNAB API error";
767
+ const isKnownCode = error.status === YNABErrorCode.BAD_REQUEST ||
768
+ error.status === YNABErrorCode.UNAUTHORIZED ||
769
+ error.status === YNABErrorCode.FORBIDDEN ||
770
+ error.status === YNABErrorCode.NOT_FOUND ||
771
+ error.status === YNABErrorCode.TOO_MANY_REQUESTS ||
772
+ error.status === YNABErrorCode.INTERNAL_SERVER_ERROR;
735
773
  if (isKnownCode) {
736
774
  return new YNABAPIError(error.status, message, originalError);
737
775
  }
738
- const statusFragment = error.status ? ` (HTTP ${error.status})` : '';
739
- const detailFragment = error.detail && !message.includes(error.detail) ? ` (${error.detail})` : '';
776
+ const statusFragment = error.status ? ` (HTTP ${error.status})` : "";
777
+ const detailFragment = error.detail && !message.includes(error.detail) ? ` (${error.detail})` : "";
740
778
  const err = new Error(`${message}${statusFragment}${detailFragment}`);
741
779
  if (error.status !== undefined) {
742
780
  err.status = error.status;
@@ -755,26 +793,26 @@ function computeUpdateFlags(match, params) {
755
793
  if (!ynabTxn) {
756
794
  return { needsClearedUpdate: false, needsDateUpdate: false };
757
795
  }
758
- const needsClearedUpdate = Boolean(params.auto_update_cleared_status && ynabTxn.cleared !== 'cleared');
796
+ const needsClearedUpdate = Boolean(params.auto_update_cleared_status && ynabTxn.cleared !== "cleared");
759
797
  const needsDateUpdate = Boolean(params.auto_adjust_dates && ynabTxn.date !== bankTxn.date);
760
798
  return { needsClearedUpdate, needsDateUpdate };
761
799
  }
762
800
  function updateReason(match, flags, _currency) {
763
801
  const parts = [];
764
802
  if (flags.needsClearedUpdate) {
765
- parts.push('marked as cleared');
803
+ parts.push("marked as cleared");
766
804
  }
767
805
  if (flags.needsDateUpdate) {
768
806
  parts.push(`date adjusted to ${match.bankTransaction.date}`);
769
807
  }
770
- return parts.join(', ');
808
+ return parts.join(", ");
771
809
  }
772
810
  async function buildBalanceReconciliation(args) {
773
811
  const { ynabAPI, budgetId, accountId, statementDate, statementBalance } = args;
774
812
  const ynabMilli = await clearedBalanceAsOf(ynabAPI, budgetId, accountId, statementDate);
775
813
  const bankMilli = toMilli(statementBalance);
776
814
  const discrepancy = bankMilli - ynabMilli;
777
- const status = discrepancy === 0 ? 'PERFECTLY_RECONCILED' : 'DISCREPANCY_FOUND';
815
+ const status = discrepancy === 0 ? "PERFECTLY_RECONCILED" : "DISCREPANCY_FOUND";
778
816
  const precision_calculations = {
779
817
  bank_statement_balance_milliunits: bankMilli,
780
818
  ynab_calculated_balance_milliunits: ynabMilli,
@@ -800,7 +838,7 @@ async function buildBalanceReconciliation(args) {
800
838
  async function clearedBalanceAsOf(api, budgetId, accountId, dateISO) {
801
839
  const response = await api.transactions.getTransactionsByAccount(budgetId, accountId);
802
840
  const asOf = new Date(dateISO);
803
- const cleared = response.data.transactions.filter((txn) => txn.cleared === 'cleared' && new Date(txn.date) <= asOf);
841
+ const cleared = response.data.transactions.filter((txn) => txn.cleared === "cleared" && new Date(txn.date) <= asOf);
804
842
  const sum = cleared.reduce((acc, txn) => addMilli(acc, txn.amount ?? 0), 0);
805
843
  return sum;
806
844
  }
@@ -821,13 +859,13 @@ function buildLikelyCauses(discrepancyMilli) {
821
859
  const abs = Math.abs(discrepancyMilli);
822
860
  if (abs % 1000 === 0 || abs % 500 === 0) {
823
861
  causes.push({
824
- cause_type: 'bank_fee',
825
- description: 'Round amount suggests a bank fee or interest adjustment.',
862
+ cause_type: "bank_fee",
863
+ description: "Round amount suggests a bank fee or interest adjustment.",
826
864
  confidence: 0.8,
827
865
  amount_milliunits: discrepancyMilli,
828
866
  suggested_resolution: discrepancyMilli < 0
829
- ? 'Create bank fee transaction and mark cleared'
830
- : 'Record interest income',
867
+ ? "Create bank fee transaction and mark cleared"
868
+ : "Record interest income",
831
869
  evidence: [],
832
870
  });
833
871
  }
@@ -835,7 +873,7 @@ function buildLikelyCauses(discrepancyMilli) {
835
873
  ? {
836
874
  confidence_level: Math.max(...causes.map((cause) => cause.confidence)),
837
875
  likely_causes: causes,
838
- risk_assessment: 'LOW',
876
+ risk_assessment: "LOW",
839
877
  }
840
878
  : undefined;
841
879
  }
@@ -849,13 +887,13 @@ function buildRecommendations(args) {
849
887
  recommendations.push(`Consider enabling auto_create_transactions to automatically create ${analysis.summary.unmatched_bank} missing transaction(s)`);
850
888
  }
851
889
  if (!params.auto_adjust_dates && analysis.auto_matches.length > 0) {
852
- recommendations.push('Consider enabling auto_adjust_dates to align YNAB dates with bank statement dates');
890
+ recommendations.push("Consider enabling auto_adjust_dates to align YNAB dates with bank statement dates");
853
891
  }
854
892
  if (analysis.summary.unmatched_ynab > 0) {
855
893
  recommendations.push(`${analysis.summary.unmatched_ynab} transaction(s) exist in YNAB but not on the bank statement — review for duplicates or pending items`);
856
894
  }
857
895
  if (params.dry_run) {
858
- recommendations.push('Dry run only — re-run with dry_run=false to apply these changes');
896
+ recommendations.push("Dry run only — re-run with dry_run=false to apply these changes");
859
897
  }
860
898
  if (Math.abs(balanceChangeMilli) > MONEY_EPSILON_MILLI) {
861
899
  recommendations.push(`Account balance changed by ${toMoneyValue(balanceChangeMilli, currencyCode).value_display} during reconciliation`);
@@ -863,7 +901,7 @@ function buildRecommendations(args) {
863
901
  return recommendations;
864
902
  }
865
903
  function resolveStatementBalanceMilli(balanceInfo, provided) {
866
- if (typeof provided === 'number' && Number.isFinite(provided)) {
904
+ if (typeof provided === "number" && Number.isFinite(provided)) {
867
905
  return toMilli(provided);
868
906
  }
869
907
  return (extractMoneyValue(balanceInfo?.target_statement) ??
@@ -871,13 +909,14 @@ function resolveStatementBalanceMilli(balanceInfo, provided) {
871
909
  0);
872
910
  }
873
911
  function extractMoneyValue(value) {
874
- if (typeof value === 'number' && Number.isFinite(value)) {
912
+ if (typeof value === "number" && Number.isFinite(value)) {
875
913
  return toMilli(value);
876
914
  }
877
915
  if (value &&
878
- typeof value === 'object' &&
879
- 'value_milliunits' in value &&
880
- typeof value.value_milliunits === 'number') {
916
+ typeof value === "object" &&
917
+ "value_milliunits" in value &&
918
+ typeof value.value_milliunits ===
919
+ "number") {
881
920
  return value.value_milliunits;
882
921
  }
883
922
  return undefined;