@dizzlkheinz/ynab-mcpb 0.18.4 → 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 (343) hide show
  1. package/CLAUDE.md +87 -8
  2. package/bin/ynab-mcp-server.cjs +2 -2
  3. package/bin/ynab-mcp-server.js +3 -3
  4. package/biome.json +39 -0
  5. package/dist/bundle/index.cjs +67 -67
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +27 -27
  8. package/dist/server/YNABMCPServer.d.ts +3 -4
  9. package/dist/server/YNABMCPServer.js +111 -116
  10. package/dist/server/budgetResolver.d.ts +6 -5
  11. package/dist/server/budgetResolver.js +46 -36
  12. package/dist/server/cacheKeys.js +6 -6
  13. package/dist/server/cacheManager.js +14 -11
  14. package/dist/server/completions.d.ts +2 -2
  15. package/dist/server/completions.js +20 -15
  16. package/dist/server/config.d.ts +10 -5
  17. package/dist/server/config.js +24 -7
  18. package/dist/server/deltaCache.d.ts +2 -2
  19. package/dist/server/deltaCache.js +22 -16
  20. package/dist/server/deltaCache.merge.d.ts +2 -2
  21. package/dist/server/diagnostics.d.ts +4 -4
  22. package/dist/server/diagnostics.js +38 -32
  23. package/dist/server/errorHandler.d.ts +5 -12
  24. package/dist/server/errorHandler.js +219 -217
  25. package/dist/server/prompts.d.ts +2 -2
  26. package/dist/server/prompts.js +45 -45
  27. package/dist/server/rateLimiter.js +4 -4
  28. package/dist/server/requestLogger.d.ts +1 -1
  29. package/dist/server/requestLogger.js +40 -35
  30. package/dist/server/resources.d.ts +3 -3
  31. package/dist/server/resources.js +55 -52
  32. package/dist/server/responseFormatter.js +6 -6
  33. package/dist/server/securityMiddleware.d.ts +2 -2
  34. package/dist/server/securityMiddleware.js +22 -20
  35. package/dist/server/serverKnowledgeStore.js +1 -1
  36. package/dist/server/toolRegistry.d.ts +3 -3
  37. package/dist/server/toolRegistry.js +47 -40
  38. package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
  39. package/dist/tools/__tests__/deltaTestUtils.js +2 -2
  40. package/dist/tools/accountTools.d.ts +9 -8
  41. package/dist/tools/accountTools.js +47 -47
  42. package/dist/tools/adapters.d.ts +13 -8
  43. package/dist/tools/adapters.js +21 -11
  44. package/dist/tools/budgetTools.d.ts +8 -7
  45. package/dist/tools/budgetTools.js +22 -22
  46. package/dist/tools/categoryTools.d.ts +9 -8
  47. package/dist/tools/categoryTools.js +68 -59
  48. package/dist/tools/compareTransactions/formatter.d.ts +3 -3
  49. package/dist/tools/compareTransactions/formatter.js +9 -9
  50. package/dist/tools/compareTransactions/index.d.ts +6 -6
  51. package/dist/tools/compareTransactions/index.js +58 -43
  52. package/dist/tools/compareTransactions/matcher.d.ts +1 -1
  53. package/dist/tools/compareTransactions/matcher.js +28 -15
  54. package/dist/tools/compareTransactions/parser.d.ts +2 -2
  55. package/dist/tools/compareTransactions/parser.js +144 -138
  56. package/dist/tools/compareTransactions/types.d.ts +4 -4
  57. package/dist/tools/compareTransactions.d.ts +1 -1
  58. package/dist/tools/compareTransactions.js +1 -1
  59. package/dist/tools/deltaFetcher.d.ts +2 -2
  60. package/dist/tools/deltaFetcher.js +16 -15
  61. package/dist/tools/deltaSupport.d.ts +4 -4
  62. package/dist/tools/deltaSupport.js +35 -41
  63. package/dist/tools/exportTransactions.d.ts +5 -4
  64. package/dist/tools/exportTransactions.js +61 -59
  65. package/dist/tools/monthTools.d.ts +7 -6
  66. package/dist/tools/monthTools.js +31 -29
  67. package/dist/tools/payeeTools.d.ts +7 -6
  68. package/dist/tools/payeeTools.js +28 -28
  69. package/dist/tools/reconcileAdapter.d.ts +2 -2
  70. package/dist/tools/reconcileAdapter.js +19 -12
  71. package/dist/tools/reconciliation/analyzer.d.ts +4 -4
  72. package/dist/tools/reconciliation/analyzer.js +73 -59
  73. package/dist/tools/reconciliation/csvParser.d.ts +3 -3
  74. package/dist/tools/reconciliation/csvParser.js +128 -104
  75. package/dist/tools/reconciliation/executor.d.ts +4 -4
  76. package/dist/tools/reconciliation/executor.js +148 -109
  77. package/dist/tools/reconciliation/index.d.ts +10 -10
  78. package/dist/tools/reconciliation/index.js +96 -83
  79. package/dist/tools/reconciliation/matcher.d.ts +3 -3
  80. package/dist/tools/reconciliation/matcher.js +17 -16
  81. package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
  82. package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
  83. package/dist/tools/reconciliation/recommendationEngine.js +40 -40
  84. package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
  85. package/dist/tools/reconciliation/reportFormatter.js +59 -58
  86. package/dist/tools/reconciliation/signDetector.d.ts +1 -1
  87. package/dist/tools/reconciliation/types.d.ts +16 -16
  88. package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
  89. package/dist/tools/schemas/common.d.ts +1 -1
  90. package/dist/tools/schemas/common.js +1 -1
  91. package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
  92. package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
  93. package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
  94. package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
  95. package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
  96. package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
  97. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
  98. package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
  99. package/dist/tools/schemas/outputs/index.d.ts +14 -14
  100. package/dist/tools/schemas/outputs/index.js +14 -14
  101. package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
  102. package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
  103. package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
  104. package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
  105. package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
  106. package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
  107. package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
  108. package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
  109. package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
  110. package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
  111. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
  112. package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
  113. package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
  114. package/dist/tools/schemas/shared/commonOutputs.js +15 -9
  115. package/dist/tools/transactionReadTools.d.ts +11 -0
  116. package/dist/tools/transactionReadTools.js +202 -0
  117. package/dist/tools/transactionSchemas.d.ts +7 -7
  118. package/dist/tools/transactionSchemas.js +77 -57
  119. package/dist/tools/transactionTools.d.ts +6 -24
  120. package/dist/tools/transactionTools.js +7 -1499
  121. package/dist/tools/transactionUtils.d.ts +6 -6
  122. package/dist/tools/transactionUtils.js +78 -63
  123. package/dist/tools/transactionWriteTools.d.ts +20 -0
  124. package/dist/tools/transactionWriteTools.js +1342 -0
  125. package/dist/tools/utilityTools.d.ts +5 -4
  126. package/dist/tools/utilityTools.js +11 -11
  127. package/dist/types/index.d.ts +7 -7
  128. package/dist/types/index.js +6 -6
  129. package/dist/types/reconciliation.d.ts +1 -1
  130. package/dist/types/toolRegistration.d.ts +14 -12
  131. package/dist/utils/amountUtils.js +1 -1
  132. package/dist/utils/dateUtils.js +4 -4
  133. package/dist/utils/errors.d.ts +3 -3
  134. package/dist/utils/errors.js +4 -4
  135. package/dist/utils/money.d.ts +2 -2
  136. package/dist/utils/money.js +8 -8
  137. package/dist/utils/validationError.d.ts +1 -1
  138. package/dist/utils/validationError.js +1 -1
  139. package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
  140. package/docs/assets/schemas/reconciliation-v2.json +360 -336
  141. package/esbuild.config.mjs +53 -50
  142. package/meta.json +12548 -12548
  143. package/package.json +98 -111
  144. package/scripts/analyze-bundle.mjs +33 -30
  145. package/scripts/create-pr-description.js +169 -120
  146. package/scripts/run-all-tests.js +178 -169
  147. package/scripts/run-domain-integration-tests.js +28 -18
  148. package/scripts/run-generate-mcpb.js +19 -17
  149. package/scripts/run-throttled-integration-tests.js +92 -83
  150. package/scripts/test-delta-params.mjs +149 -120
  151. package/scripts/test-recommendations.ts +36 -32
  152. package/scripts/tmpTransaction.ts +80 -43
  153. package/scripts/validate-env.js +98 -91
  154. package/scripts/verify-build.js +78 -76
  155. package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
  156. package/src/__tests__/performance.test.ts +723 -671
  157. package/src/__tests__/setup.ts +442 -395
  158. package/src/__tests__/smoke.e2e.test.ts +41 -39
  159. package/src/__tests__/testRunner.ts +314 -295
  160. package/src/__tests__/testUtils.ts +456 -364
  161. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
  162. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
  163. package/src/index.ts +68 -59
  164. package/src/server/CLAUDE.md +480 -0
  165. package/src/server/YNABMCPServer.ts +821 -794
  166. package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
  167. package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
  168. package/src/server/__tests__/budgetResolver.test.ts +466 -423
  169. package/src/server/__tests__/cacheManager.test.ts +891 -874
  170. package/src/server/__tests__/completions.integration.test.ts +115 -106
  171. package/src/server/__tests__/completions.test.ts +334 -313
  172. package/src/server/__tests__/config.test.ts +98 -86
  173. package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
  174. package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
  175. package/src/server/__tests__/deltaCache.test.ts +946 -759
  176. package/src/server/__tests__/diagnostics.test.ts +825 -792
  177. package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
  178. package/src/server/__tests__/errorHandler.test.ts +402 -397
  179. package/src/server/__tests__/prompts.test.ts +424 -347
  180. package/src/server/__tests__/rateLimiter.test.ts +313 -309
  181. package/src/server/__tests__/requestLogger.test.ts +443 -403
  182. package/src/server/__tests__/resources.template.test.ts +196 -185
  183. package/src/server/__tests__/resources.test.ts +294 -288
  184. package/src/server/__tests__/security.integration.test.ts +487 -421
  185. package/src/server/__tests__/securityMiddleware.test.ts +519 -444
  186. package/src/server/__tests__/server-startup.integration.test.ts +509 -490
  187. package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
  188. package/src/server/__tests__/toolRegistration.test.ts +239 -210
  189. package/src/server/__tests__/toolRegistry.test.ts +907 -845
  190. package/src/server/budgetResolver.ts +221 -181
  191. package/src/server/cacheKeys.ts +6 -6
  192. package/src/server/cacheManager.ts +498 -484
  193. package/src/server/completions.ts +267 -243
  194. package/src/server/config.ts +35 -14
  195. package/src/server/deltaCache.merge.ts +146 -128
  196. package/src/server/deltaCache.ts +352 -309
  197. package/src/server/diagnostics.ts +257 -242
  198. package/src/server/errorHandler.ts +747 -744
  199. package/src/server/prompts.ts +181 -176
  200. package/src/server/rateLimiter.ts +131 -129
  201. package/src/server/requestLogger.ts +350 -322
  202. package/src/server/resources.ts +442 -374
  203. package/src/server/responseFormatter.ts +41 -37
  204. package/src/server/securityMiddleware.ts +223 -205
  205. package/src/server/serverKnowledgeStore.ts +67 -67
  206. package/src/server/toolRegistry.ts +508 -474
  207. package/src/tools/CLAUDE.md +604 -0
  208. package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
  209. package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
  210. package/src/tools/__tests__/accountTools.test.ts +685 -638
  211. package/src/tools/__tests__/adapters.test.ts +142 -108
  212. package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
  213. package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
  214. package/src/tools/__tests__/budgetTools.test.ts +442 -413
  215. package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
  216. package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
  217. package/src/tools/__tests__/categoryTools.test.ts +656 -625
  218. package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
  219. package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
  220. package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
  221. package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
  222. package/src/tools/__tests__/compareTransactions.test.ts +352 -332
  223. package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
  224. package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
  225. package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
  226. package/src/tools/__tests__/deltaSupport.test.ts +211 -184
  227. package/src/tools/__tests__/deltaTestUtils.ts +37 -33
  228. package/src/tools/__tests__/exportTransactions.test.ts +205 -200
  229. package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
  230. package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
  231. package/src/tools/__tests__/monthTools.test.ts +561 -512
  232. package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
  233. package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
  234. package/src/tools/__tests__/payeeTools.test.ts +486 -434
  235. package/src/tools/__tests__/transactionSchemas.test.ts +1202 -1186
  236. package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
  237. package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
  238. package/src/tools/__tests__/transactionUtils.test.ts +1004 -977
  239. package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
  240. package/src/tools/__tests__/utilityTools.test.ts +68 -58
  241. package/src/tools/accountTools.ts +293 -271
  242. package/src/tools/adapters.ts +120 -63
  243. package/src/tools/budgetTools.ts +121 -116
  244. package/src/tools/categoryTools.ts +379 -339
  245. package/src/tools/compareTransactions/formatter.ts +131 -119
  246. package/src/tools/compareTransactions/index.ts +249 -214
  247. package/src/tools/compareTransactions/matcher.ts +259 -209
  248. package/src/tools/compareTransactions/parser.ts +517 -487
  249. package/src/tools/compareTransactions/types.ts +38 -38
  250. package/src/tools/compareTransactions.ts +1 -1
  251. package/src/tools/deltaFetcher.ts +281 -260
  252. package/src/tools/deltaSupport.ts +264 -259
  253. package/src/tools/exportTransactions.ts +230 -218
  254. package/src/tools/monthTools.ts +180 -165
  255. package/src/tools/payeeTools.ts +152 -140
  256. package/src/tools/reconcileAdapter.ts +297 -252
  257. package/src/tools/reconciliation/CLAUDE.md +506 -0
  258. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +133 -124
  259. package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -230
  260. package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -400
  261. package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
  262. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
  263. package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
  264. package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
  265. package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
  266. package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
  267. package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
  268. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -989
  269. package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
  270. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -533
  271. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -74
  272. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -62
  273. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
  274. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +56 -55
  275. package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
  276. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
  277. package/src/tools/reconciliation/analyzer.ts +564 -504
  278. package/src/tools/reconciliation/csvParser.ts +656 -609
  279. package/src/tools/reconciliation/executor.ts +1290 -1128
  280. package/src/tools/reconciliation/index.ts +580 -528
  281. package/src/tools/reconciliation/matcher.ts +256 -240
  282. package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
  283. package/src/tools/reconciliation/recommendationEngine.ts +357 -345
  284. package/src/tools/reconciliation/reportFormatter.ts +343 -307
  285. package/src/tools/reconciliation/signDetector.ts +89 -83
  286. package/src/tools/reconciliation/types.ts +164 -159
  287. package/src/tools/reconciliation/ynabAdapter.ts +17 -15
  288. package/src/tools/schemas/CLAUDE.md +546 -0
  289. package/src/tools/schemas/common.ts +1 -1
  290. package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
  291. package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
  292. package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
  293. package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
  294. package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
  295. package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
  296. package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
  297. package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
  298. package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
  299. package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
  300. package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
  301. package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
  302. package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
  303. package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
  304. package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
  305. package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
  306. package/src/tools/schemas/outputs/index.ts +163 -163
  307. package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
  308. package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
  309. package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
  310. package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
  311. package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
  312. package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
  313. package/src/tools/schemas/shared/commonOutputs.ts +27 -19
  314. package/src/tools/toolCategories.ts +114 -114
  315. package/src/tools/transactionReadTools.ts +327 -0
  316. package/src/tools/transactionSchemas.ts +322 -291
  317. package/src/tools/transactionTools.ts +84 -2246
  318. package/src/tools/transactionUtils.ts +507 -422
  319. package/src/tools/transactionWriteTools.ts +2110 -0
  320. package/src/tools/utilityTools.ts +46 -41
  321. package/src/types/CLAUDE.md +477 -0
  322. package/src/types/__tests__/index.test.ts +51 -51
  323. package/src/types/index.ts +43 -39
  324. package/src/types/integration-tests.d.ts +26 -26
  325. package/src/types/reconciliation.ts +29 -29
  326. package/src/types/toolAnnotations.ts +30 -30
  327. package/src/types/toolRegistration.ts +43 -32
  328. package/src/utils/CLAUDE.md +508 -0
  329. package/src/utils/__tests__/dateUtils.test.ts +174 -168
  330. package/src/utils/__tests__/money.test.ts +193 -187
  331. package/src/utils/amountUtils.ts +5 -5
  332. package/src/utils/baseError.ts +5 -5
  333. package/src/utils/dateUtils.ts +29 -26
  334. package/src/utils/errors.ts +14 -14
  335. package/src/utils/money.ts +66 -52
  336. package/src/utils/validationError.ts +1 -1
  337. package/tsconfig.json +29 -29
  338. package/tsconfig.prod.json +16 -16
  339. package/vitest-reporters/split-json-reporter.ts +247 -204
  340. package/vitest.config.ts +99 -95
  341. package/.prettierignore +0 -10
  342. package/.prettierrc.json +0 -10
  343. 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;