@dizzlkheinz/ynab-mcpb 0.13.1 → 0.15.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 (207) hide show
  1. package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
  2. package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
  3. package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
  4. package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
  5. package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
  6. package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
  7. package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
  8. package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
  9. package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
  10. package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
  11. package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
  12. package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
  13. package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
  14. package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
  15. package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
  16. package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
  17. package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
  18. package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
  19. package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
  20. package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
  21. package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
  22. package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
  23. package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
  24. package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
  25. package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
  26. package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
  27. package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
  28. package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
  29. package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
  30. package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
  31. package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
  32. package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
  33. package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
  34. package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
  35. package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
  36. package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
  37. package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
  38. package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
  39. package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
  40. package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
  41. package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
  42. package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
  43. package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
  44. package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
  45. package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
  46. package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
  47. package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
  48. package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
  49. package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
  50. package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
  51. package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
  52. package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
  53. package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
  54. package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
  55. package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
  56. package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
  57. package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
  58. package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
  59. package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
  60. package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
  61. package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
  62. package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
  63. package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
  64. package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
  65. package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
  66. package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
  67. package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
  68. package/.github/workflows/publish.yml +3 -3
  69. package/.github/workflows/release.yml +4 -0
  70. package/CHANGELOG.md +75 -0
  71. package/NUL +1 -0
  72. package/dist/bundle/index.cjs +65 -42
  73. package/dist/server/errorHandler.d.ts +2 -0
  74. package/dist/server/errorHandler.js +49 -5
  75. package/dist/tools/reconcileAdapter.js +10 -5
  76. package/dist/tools/reconciliation/analyzer.d.ts +4 -2
  77. package/dist/tools/reconciliation/analyzer.js +120 -404
  78. package/dist/tools/reconciliation/csvParser.d.ts +51 -0
  79. package/dist/tools/reconciliation/csvParser.js +413 -0
  80. package/dist/tools/reconciliation/executor.d.ts +8 -0
  81. package/dist/tools/reconciliation/executor.js +204 -58
  82. package/dist/tools/reconciliation/index.d.ts +7 -7
  83. package/dist/tools/reconciliation/index.js +115 -39
  84. package/dist/tools/reconciliation/matcher.d.ts +24 -3
  85. package/dist/tools/reconciliation/matcher.js +175 -133
  86. package/dist/tools/reconciliation/recommendationEngine.js +22 -18
  87. package/dist/tools/reconciliation/reportFormatter.js +9 -8
  88. package/dist/tools/reconciliation/signDetector.d.ts +2 -0
  89. package/dist/tools/reconciliation/signDetector.js +54 -0
  90. package/dist/tools/reconciliation/types.d.ts +20 -34
  91. package/dist/tools/reconciliation/types.js +1 -7
  92. package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
  93. package/dist/tools/reconciliation/ynabAdapter.js +15 -0
  94. package/dist/types/reconciliation.d.ts +24 -0
  95. package/dist/types/reconciliation.js +1 -0
  96. package/docs/guides/ARCHITECTURE.md +12 -129
  97. package/docs/plans/2025-11-21-v014-hardening.md +153 -0
  98. package/docs/plans/reconciliation-v2-redesign.md +1571 -0
  99. package/package.json +6 -1
  100. package/scripts/test-recommendations.ts +1 -1
  101. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
  102. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
  103. package/src/server/errorHandler.ts +52 -5
  104. package/src/tools/reconcileAdapter.ts +10 -5
  105. package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
  106. package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
  107. package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
  108. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +1 -1
  109. package/src/tools/reconciliation/__tests__/executor.test.ts +88 -61
  110. package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
  111. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
  112. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
  113. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
  114. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
  115. package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
  116. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
  117. package/src/tools/reconciliation/analyzer.ts +174 -545
  118. package/src/tools/reconciliation/csvParser.ts +617 -0
  119. package/src/tools/reconciliation/executor.ts +249 -66
  120. package/src/tools/reconciliation/index.ts +141 -48
  121. package/src/tools/reconciliation/matcher.ts +234 -214
  122. package/src/tools/reconciliation/recommendationEngine.ts +23 -19
  123. package/src/tools/reconciliation/reportFormatter.ts +16 -11
  124. package/src/tools/reconciliation/signDetector.ts +117 -0
  125. package/src/tools/reconciliation/types.ts +39 -61
  126. package/src/tools/reconciliation/ynabAdapter.ts +33 -0
  127. package/src/types/reconciliation.ts +49 -0
  128. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
  129. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
  130. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
  131. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
  132. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
  133. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
  134. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
  135. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
  136. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
  137. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
  138. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
  139. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
  140. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
  141. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
  142. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
  143. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
  144. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
  145. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
  146. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
  147. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
  148. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
  149. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
  150. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
  151. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
  152. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
  153. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
  154. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
  155. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
  156. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
  157. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
  158. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
  159. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
  160. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
  161. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
  162. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
  163. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
  164. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
  165. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
  166. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
  167. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
  168. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
  169. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
  170. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
  171. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
  172. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
  173. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
  174. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
  175. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
  176. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
  177. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
  178. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
  179. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
  180. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
  181. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
  182. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
  183. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
  184. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
  185. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
  186. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
  187. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
  188. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
  189. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
  190. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
  191. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
  192. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
  193. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
  194. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
  195. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
  196. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
  197. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
  198. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
  199. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
  200. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
  201. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
  202. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
  203. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
  204. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
  205. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
  206. package/.gemini/settings.json +0 -8
  207. package/WARP.md +0 -245
@@ -1,10 +1,14 @@
1
1
  import { createHash } from 'crypto';
2
- import { toMilli, toMoneyValue, toMoneyValueFromDecimal, addMilli } from '../../utils/money.js';
2
+ import { YNABAPIError } from '../../server/errorHandler.js';
3
+ import { toMilli, toMoneyValue, addMilli } from '../../utils/money.js';
3
4
  import { generateCorrelationKey, correlateResults, toCorrelationPayload, } from '../transactionTools.js';
4
5
  const MONEY_EPSILON_MILLI = 100;
5
6
  const DEFAULT_TOLERANCE_CENTS = 1;
6
7
  const CENTS_TO_MILLI = 10;
7
8
  const MAX_BULK_CREATE_CHUNK = 100;
9
+ const MAX_BULK_UPDATE_CHUNK = 100;
10
+ const BATCH_DELAY_MS = 200;
11
+ const MAX_MEMO_LENGTH = 500;
8
12
  function chunkArray(array, size) {
9
13
  if (size <= 0) {
10
14
  throw new Error('chunk size must be positive');
@@ -15,6 +19,16 @@ function chunkArray(array, size) {
15
19
  }
16
20
  return chunks;
17
21
  }
22
+ function sleep(ms) {
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
24
+ }
25
+ function truncateMemo(memo) {
26
+ if (!memo)
27
+ return 'Auto-reconciled from bank statement';
28
+ if (memo.length <= MAX_MEMO_LENGTH)
29
+ return memo;
30
+ return memo.substring(0, MAX_MEMO_LENGTH - 3) + '...';
31
+ }
18
32
  function generateBulkImportId(accountId, date, amountMilli, payee) {
19
33
  const normalizedPayee = (payee ?? '').trim().toLowerCase();
20
34
  const raw = `${accountId}|${date}|${amountMilli}|${normalizedPayee}`;
@@ -74,13 +88,13 @@ export async function executeReconciliation(options) {
74
88
  let bulkOperationDetails;
75
89
  if (params.auto_create_transactions && !balanceAligned) {
76
90
  const buildPreparedEntry = (bankTxn) => {
77
- const amountMilli = toMilli(bankTxn.amount);
91
+ const amountMilli = bankTxn.amount;
78
92
  const saveTransaction = {
79
93
  account_id: accountId,
80
94
  amount: amountMilli,
81
95
  date: bankTxn.date,
82
96
  payee_name: bankTxn.payee ?? undefined,
83
- memo: bankTxn.memo ?? 'Auto-reconciled from bank statement',
97
+ memo: truncateMemo(bankTxn.memo),
84
98
  cleared: 'cleared',
85
99
  approved: true,
86
100
  import_id: generateBulkImportId(accountId, bankTxn.date, amountMilli, bankTxn.payee),
@@ -144,12 +158,13 @@ export async function executeReconciliation(options) {
144
158
  bulkOperationDetails.transaction_failures += 1;
145
159
  }
146
160
  const failureReason = ynabError.message || 'Unknown error occurred';
161
+ const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
147
162
  const failureAction = {
148
163
  type: 'create_transaction_failed',
149
164
  transaction: entry.saveTransaction,
150
165
  reason: options.fallbackError
151
- ? `Bulk fallback failed for ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason})`
152
- : `Failed to create transaction ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason})`,
166
+ ? `Bulk fallback failed for ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason}${statusSuffix})`
167
+ : `Failed to create transaction ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason}${statusSuffix})`,
153
168
  correlation_key: entry.correlationKey,
154
169
  };
155
170
  if (options.chunkIndex !== undefined) {
@@ -157,7 +172,7 @@ export async function executeReconciliation(options) {
157
172
  }
158
173
  actions_taken.push(failureAction);
159
174
  if (shouldPropagateYnabError(ynabError)) {
160
- throw attachStatusToError(ynabError);
175
+ throw attachStatusToError(ynabError, error);
161
176
  }
162
177
  }
163
178
  }
@@ -289,13 +304,13 @@ export async function executeReconciliation(options) {
289
304
  bulkOperationDetails.bulk_chunk_failures += 1;
290
305
  if (shouldPropagateYnabError(ynabError)) {
291
306
  bulkOperationDetails.transaction_failures += chunk.length;
292
- throw attachStatusToError(ynabError);
307
+ throw attachStatusToError(ynabError, error);
293
308
  }
294
309
  bulkOperationDetails.sequential_fallbacks += 1;
295
310
  actions_taken.push({
296
311
  type: 'bulk_create_fallback',
297
312
  transaction: null,
298
- reason: `Bulk chunk #${chunkIndex} failed (${failureReason}) - falling back to sequential creation`,
313
+ reason: `Bulk chunk #${chunkIndex} failed (${failureReason}${ynabError.status ? ` (HTTP ${ynabError.status})` : ''}) - falling back to sequential creation`,
299
314
  bulk_chunk_index: chunkIndex,
300
315
  });
301
316
  await processSequentialEntries(chunk, { chunkIndex, fallbackError: ynabError });
@@ -316,13 +331,16 @@ export async function executeReconciliation(options) {
316
331
  const flags = computeUpdateFlags(match, params);
317
332
  if (!flags.needsClearedUpdate && !flags.needsDateUpdate)
318
333
  continue;
319
- if (!match.ynab_transaction)
334
+ if (!match.ynabTransaction)
320
335
  continue;
321
336
  const updatePayload = {
322
- id: match.ynab_transaction.id,
337
+ id: match.ynabTransaction.id,
323
338
  };
339
+ if (match.ynabTransaction.memo) {
340
+ updatePayload.memo = truncateMemo(match.ynabTransaction.memo);
341
+ }
324
342
  if (flags.needsDateUpdate) {
325
- updatePayload.date = match.bank_transaction.date;
343
+ updatePayload.date = match.bankTransaction.date;
326
344
  }
327
345
  if (flags.needsClearedUpdate) {
328
346
  updatePayload.cleared = 'cleared';
@@ -334,15 +352,15 @@ export async function executeReconciliation(options) {
334
352
  actions_taken.push({
335
353
  type: 'update_transaction',
336
354
  transaction: {
337
- transaction_id: match.ynab_transaction.id,
338
- new_date: flags.needsDateUpdate ? match.bank_transaction.date : undefined,
355
+ transaction_id: match.ynabTransaction.id,
356
+ new_date: flags.needsDateUpdate ? match.bankTransaction.date : undefined,
339
357
  cleared: flags.needsClearedUpdate ? 'cleared' : undefined,
340
358
  },
341
359
  reason: `Would update transaction: ${updateReason(match, flags, currencyCode)}`,
342
360
  });
343
361
  if (flags.needsClearedUpdate) {
344
- applyClearedDelta(match.ynab_transaction.amount);
345
- if (recordAlignmentIfNeeded(`clearing ${match.ynab_transaction.id ?? 'transaction'} (dry run)`)) {
362
+ applyClearedDelta(match.ynabTransaction.amount);
363
+ if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id ?? 'transaction'} (dry run)`)) {
346
364
  break;
347
365
  }
348
366
  }
@@ -352,34 +370,75 @@ export async function executeReconciliation(options) {
352
370
  if (flags.needsDateUpdate)
353
371
  summary.dates_adjusted += 1;
354
372
  if (flags.needsClearedUpdate) {
355
- applyClearedDelta(match.ynab_transaction.amount);
356
- if (recordAlignmentIfNeeded(`clearing ${match.ynab_transaction.id}`)) {
373
+ applyClearedDelta(match.ynabTransaction.amount);
374
+ if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id}`)) {
357
375
  break;
358
376
  }
359
377
  }
360
378
  }
361
379
  }
362
380
  if (!params.dry_run && transactionsToUpdate.length > 0) {
363
- const response = await ynabAPI.transactions.updateTransactions(budgetId, {
364
- transactions: transactionsToUpdate,
365
- });
366
- const updatedTransactions = response.data.transactions ?? [];
367
- summary.transactions_updated += updatedTransactions.length;
368
- for (const updatedTransaction of updatedTransactions) {
369
- const match = orderedAutoMatches.find((m) => m.ynab_transaction?.id === updatedTransaction.id);
370
- const flags = match
371
- ? computeUpdateFlags(match, params)
372
- : { needsClearedUpdate: false, needsDateUpdate: false };
373
- actions_taken.push({
374
- type: 'update_transaction',
375
- transaction: updatedTransaction,
376
- reason: `Updated transaction: ${match ? updateReason(match, flags, currencyCode) : 'cleared'}`,
377
- });
381
+ const updateChunks = chunkArray(transactionsToUpdate, MAX_BULK_UPDATE_CHUNK);
382
+ for (let chunkIdx = 0; chunkIdx < updateChunks.length; chunkIdx++) {
383
+ const chunk = updateChunks[chunkIdx];
384
+ try {
385
+ const response = await ynabAPI.transactions.updateTransactions(budgetId, {
386
+ transactions: chunk,
387
+ });
388
+ const updatedTransactions = response.data.transactions ?? [];
389
+ summary.transactions_updated += updatedTransactions.length;
390
+ for (const updatedTransaction of updatedTransactions) {
391
+ const match = orderedAutoMatches.find((m) => m.ynabTransaction?.id === updatedTransaction.id);
392
+ const flags = match
393
+ ? computeUpdateFlags(match, params)
394
+ : { needsClearedUpdate: false, needsDateUpdate: false };
395
+ actions_taken.push({
396
+ type: 'update_transaction',
397
+ transaction: updatedTransaction,
398
+ reason: `Updated transaction: ${match ? updateReason(match, flags, currencyCode) : 'cleared'}`,
399
+ });
400
+ }
401
+ accountSnapshotDirty = true;
402
+ }
403
+ catch (error) {
404
+ const ynabError = normalizeYnabError(error);
405
+ const failureReason = ynabError.message || 'Unknown error occurred';
406
+ const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
407
+ actions_taken.push({
408
+ type: 'batch_update_failed',
409
+ transaction: null,
410
+ reason: `Failed to update chunk ${chunkIdx + 1}/${updateChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
411
+ });
412
+ if (shouldPropagateYnabError(ynabError)) {
413
+ throw attachStatusToError(ynabError, error);
414
+ }
415
+ }
416
+ if (chunkIdx < updateChunks.length - 1) {
417
+ await sleep(BATCH_DELAY_MS);
418
+ }
378
419
  }
379
- accountSnapshotDirty = true;
380
420
  }
381
421
  }
382
422
  const shouldRunSanityPass = params.auto_unclear_missing && !balanceAligned;
423
+ actions_taken.push({
424
+ type: 'diagnostic_step3_entry',
425
+ transaction: null,
426
+ reason: `STEP 3 diagnostics: auto_unclear_missing=${params.auto_unclear_missing}, balanceAligned=${balanceAligned}, shouldRunSanityPass=${shouldRunSanityPass}, orderedUnmatchedYNAB.length=${orderedUnmatchedYNAB.length}`,
427
+ });
428
+ if (orderedUnmatchedYNAB.length > 0) {
429
+ const unmatchedDetails = orderedUnmatchedYNAB.slice(0, 10).map((t) => ({
430
+ id: t.id,
431
+ date: t.date,
432
+ cleared: t.cleared,
433
+ amount: formatDisplay(t.amount, currencyCode),
434
+ payee: t.payee ?? 'Unknown',
435
+ }));
436
+ actions_taken.push({
437
+ type: 'diagnostic_unmatched_ynab',
438
+ transaction: { unmatched_transactions: unmatchedDetails },
439
+ reason: `First ${Math.min(10, orderedUnmatchedYNAB.length)} unmatched YNAB transactions (cleared status and amounts)`,
440
+ });
441
+ }
383
442
  if (shouldRunSanityPass) {
384
443
  const transactionsToUnclear = [];
385
444
  for (const ynabTxn of orderedUnmatchedYNAB) {
@@ -411,19 +470,97 @@ export async function executeReconciliation(options) {
411
470
  }
412
471
  }
413
472
  if (!params.dry_run && transactionsToUnclear.length > 0) {
414
- const response = await ynabAPI.transactions.updateTransactions(budgetId, {
415
- transactions: transactionsToUnclear,
473
+ const unclearChunks = chunkArray(transactionsToUnclear, MAX_BULK_UPDATE_CHUNK);
474
+ for (let chunkIdx = 0; chunkIdx < unclearChunks.length; chunkIdx++) {
475
+ const chunk = unclearChunks[chunkIdx];
476
+ try {
477
+ const response = await ynabAPI.transactions.updateTransactions(budgetId, {
478
+ transactions: chunk,
479
+ });
480
+ const updatedTransactions = response.data.transactions ?? [];
481
+ summary.transactions_updated += updatedTransactions.length;
482
+ for (const updatedTransaction of updatedTransactions) {
483
+ actions_taken.push({
484
+ type: 'update_transaction',
485
+ transaction: updatedTransaction,
486
+ reason: `Marked transaction ${updatedTransaction.id} as uncleared - not found on statement`,
487
+ });
488
+ }
489
+ accountSnapshotDirty = true;
490
+ }
491
+ catch (error) {
492
+ const ynabError = normalizeYnabError(error);
493
+ const failureReason = ynabError.message || 'Unknown error occurred';
494
+ const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
495
+ actions_taken.push({
496
+ type: 'batch_unclear_failed',
497
+ transaction: null,
498
+ reason: `Failed to unclear chunk ${chunkIdx + 1}/${unclearChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
499
+ });
500
+ if (shouldPropagateYnabError(ynabError)) {
501
+ throw attachStatusToError(ynabError, error);
502
+ }
503
+ }
504
+ if (chunkIdx < unclearChunks.length - 1) {
505
+ await sleep(BATCH_DELAY_MS);
506
+ }
507
+ }
508
+ }
509
+ }
510
+ if (balanceAligned && !params.dry_run) {
511
+ const transactionsToReconcile = [];
512
+ for (const match of orderedAutoMatches) {
513
+ if (!match.ynabTransaction)
514
+ continue;
515
+ if (match.ynabTransaction.cleared === 'reconciled')
516
+ continue;
517
+ transactionsToReconcile.push({
518
+ id: match.ynabTransaction.id,
519
+ cleared: 'reconciled',
416
520
  });
417
- const updatedTransactions = response.data.transactions ?? [];
418
- summary.transactions_updated += updatedTransactions.length;
419
- for (const updatedTransaction of updatedTransactions) {
420
- actions_taken.push({
421
- type: 'update_transaction',
422
- transaction: updatedTransaction,
423
- reason: `Marked transaction ${updatedTransaction.id} as uncleared - not found on statement`,
424
- });
521
+ }
522
+ if (transactionsToReconcile.length > 0) {
523
+ const reconcileChunks = chunkArray(transactionsToReconcile, MAX_BULK_UPDATE_CHUNK);
524
+ for (let chunkIdx = 0; chunkIdx < reconcileChunks.length; chunkIdx++) {
525
+ const chunk = reconcileChunks[chunkIdx];
526
+ try {
527
+ const response = await ynabAPI.transactions.updateTransactions(budgetId, {
528
+ transactions: chunk,
529
+ });
530
+ const reconciledTransactions = response.data.transactions ?? [];
531
+ summary.transactions_updated += reconciledTransactions.length;
532
+ for (const reconciledTransaction of reconciledTransactions) {
533
+ const match = orderedAutoMatches.find((m) => m.ynabTransaction?.id === reconciledTransaction.id);
534
+ actions_taken.push({
535
+ type: 'update_transaction',
536
+ transaction: reconciledTransaction,
537
+ reason: `Marked as reconciled: ${match?.bankTransaction.payee ?? 'transaction'} (${formatDisplay(reconciledTransaction.amount, currencyCode)})`,
538
+ });
539
+ }
540
+ accountSnapshotDirty = true;
541
+ }
542
+ catch (error) {
543
+ const ynabError = normalizeYnabError(error);
544
+ const failureReason = ynabError.message || 'Unknown error occurred';
545
+ const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
546
+ actions_taken.push({
547
+ type: 'batch_reconcile_failed',
548
+ transaction: null,
549
+ reason: `Failed to reconcile chunk ${chunkIdx + 1}/${reconcileChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
550
+ });
551
+ if (shouldPropagateYnabError(ynabError)) {
552
+ throw attachStatusToError(ynabError, error);
553
+ }
554
+ }
555
+ if (chunkIdx < reconcileChunks.length - 1) {
556
+ await sleep(BATCH_DELAY_MS);
557
+ }
425
558
  }
426
- accountSnapshotDirty = true;
559
+ actions_taken.push({
560
+ type: 'reconciliation_complete',
561
+ transaction: null,
562
+ reason: `Marked ${transactionsToReconcile.length} matched transaction(s) as reconciled - balance aligned within tolerance`,
563
+ });
427
564
  }
428
565
  }
429
566
  let balance_reconciliation;
@@ -466,8 +603,8 @@ export async function executeReconciliation(options) {
466
603
  }
467
604
  return result;
468
605
  }
469
- const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500]);
470
- function normalizeYnabError(error) {
606
+ const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500, 503]);
607
+ export function normalizeYnabError(error) {
471
608
  const parseStatus = (value) => {
472
609
  if (typeof value === 'number' && Number.isFinite(value))
473
610
  return value;
@@ -479,7 +616,8 @@ function normalizeYnabError(error) {
479
616
  return undefined;
480
617
  };
481
618
  if (error instanceof Error) {
482
- const status = parseStatus(error.status);
619
+ const status = parseStatus(error.status) ??
620
+ parseStatus(error.response?.status);
483
621
  const detailSource = error.detail;
484
622
  const detail = typeof detailSource === 'string' && detailSource.trim().length > 0 ? detailSource : undefined;
485
623
  const result = {
@@ -520,29 +658,37 @@ function normalizeYnabError(error) {
520
658
  }
521
659
  return { message: 'Unknown error occurred' };
522
660
  }
523
- function shouldPropagateYnabError(error) {
661
+ export function shouldPropagateYnabError(error) {
524
662
  return error.status !== undefined && FATAL_YNAB_STATUS_CODES.has(error.status);
525
663
  }
526
- function attachStatusToError(error) {
664
+ function attachStatusToError(error, originalError) {
527
665
  const message = error.message || 'YNAB API error';
528
- const err = new Error(message);
666
+ const isKnownCode = error.status === 400 ||
667
+ error.status === 401 ||
668
+ error.status === 403 ||
669
+ error.status === 404 ||
670
+ error.status === 429 ||
671
+ error.status === 500;
672
+ if (isKnownCode) {
673
+ return new YNABAPIError(error.status, message, originalError);
674
+ }
675
+ const statusFragment = error.status ? ` (HTTP ${error.status})` : '';
676
+ const detailFragment = error.detail && !message.includes(error.detail) ? ` (${error.detail})` : '';
677
+ const err = new Error(`${message}${statusFragment}${detailFragment}`);
529
678
  if (error.status !== undefined) {
530
679
  err.status = error.status;
531
680
  }
532
681
  if (error.name) {
533
682
  err.name = error.name;
534
683
  }
535
- if (error.detail && !message.includes(error.detail)) {
536
- err.message = `${message} (${error.detail})`;
537
- }
538
684
  return err;
539
685
  }
540
686
  function formatDisplay(amount, currency) {
541
- return toMoneyValueFromDecimal(amount, currency).value_display;
687
+ return toMoneyValue(amount, currency).value_display;
542
688
  }
543
689
  function computeUpdateFlags(match, params) {
544
- const ynabTxn = match.ynab_transaction;
545
- const bankTxn = match.bank_transaction;
690
+ const ynabTxn = match.ynabTransaction;
691
+ const bankTxn = match.bankTransaction;
546
692
  if (!ynabTxn) {
547
693
  return { needsClearedUpdate: false, needsDateUpdate: false };
548
694
  }
@@ -556,7 +702,7 @@ function updateReason(match, flags, _currency) {
556
702
  parts.push('marked as cleared');
557
703
  }
558
704
  if (flags.needsDateUpdate) {
559
- parts.push(`date adjusted to ${match.bank_transaction.date}`);
705
+ parts.push(`date adjusted to ${match.bankTransaction.date}`);
560
706
  }
561
707
  return parts.join(', ');
562
708
  }
@@ -677,7 +823,7 @@ function sortByDateDescending(items) {
677
823
  return [...items].sort((a, b) => compareDates(b.date, a.date));
678
824
  }
679
825
  function sortMatchesByBankDateDescending(matches) {
680
- return [...matches].sort((a, b) => compareDates(b.bank_transaction.date, a.bank_transaction.date));
826
+ return [...matches].sort((a, b) => compareDates(b.bankTransaction.date, a.bankTransaction.date));
681
827
  }
682
828
  function compareDates(dateA, dateB) {
683
829
  return toChronoValue(dateA) - toChronoValue(dateB);
@@ -11,16 +11,16 @@ export declare const ReconcileAccountSchema: z.ZodObject<{
11
11
  account_id: z.ZodString;
12
12
  csv_file_path: z.ZodOptional<z.ZodString>;
13
13
  csv_data: z.ZodOptional<z.ZodString>;
14
- csv_format: z.ZodDefault<z.ZodOptional<z.ZodObject<{
15
- date_column: z.ZodDefault<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
14
+ csv_format: z.ZodOptional<z.ZodObject<{
15
+ date_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
16
16
  amount_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
17
17
  debit_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
18
18
  credit_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
19
- description_column: z.ZodDefault<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
20
- date_format: z.ZodDefault<z.ZodOptional<z.ZodString>>;
21
- has_header: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
22
- delimiter: z.ZodDefault<z.ZodOptional<z.ZodString>>;
23
- }, z.core.$strict>>>;
19
+ description_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
20
+ date_format: z.ZodOptional<z.ZodString>;
21
+ has_header: z.ZodOptional<z.ZodBoolean>;
22
+ delimiter: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strict>>;
24
24
  statement_balance: z.ZodNumber;
25
25
  statement_start_date: z.ZodOptional<z.ZodString>;
26
26
  statement_end_date: z.ZodOptional<z.ZodString>;