@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
@@ -0,0 +1,413 @@
1
+ import Papa from 'papaparse';
2
+ import * as chrono from 'chrono-node';
3
+ import { randomUUID } from 'crypto';
4
+ export const BANK_PRESETS = {
5
+ td: {
6
+ name: 'TD Canada Trust',
7
+ header: false,
8
+ dateColumn: ['0', 'Date'],
9
+ amountColumn: ['Amount'],
10
+ debitColumn: '2',
11
+ creditColumn: '3',
12
+ descriptionColumn: ['1', 'Description'],
13
+ dateFormat: 'MDY',
14
+ },
15
+ rbc: {
16
+ name: 'RBC Royal Bank',
17
+ dateColumn: ['Transaction Date', 'Date'],
18
+ debitColumn: 'Debit',
19
+ creditColumn: 'Credit',
20
+ descriptionColumn: ['Description 1', 'Description', 'Transaction'],
21
+ dateFormat: 'YMD',
22
+ },
23
+ scotiabank: {
24
+ name: 'Scotiabank',
25
+ dateColumn: ['Date', 'Transaction Date'],
26
+ amountColumn: ['Amount'],
27
+ descriptionColumn: ['Description', 'Transaction Details'],
28
+ dateFormat: 'DMY',
29
+ },
30
+ wealthsimple: {
31
+ name: 'Wealthsimple',
32
+ dateColumn: ['Date'],
33
+ amountColumn: ['Amount'],
34
+ descriptionColumn: ['Description', 'Payee'],
35
+ amountMultiplier: 1,
36
+ dateFormat: 'YMD',
37
+ },
38
+ tangerine: {
39
+ name: 'Tangerine',
40
+ dateColumn: ['Date', 'Transaction date'],
41
+ amountColumn: ['Amount'],
42
+ descriptionColumn: ['Name', 'Transaction name', 'Memo'],
43
+ dateFormat: 'MDY',
44
+ },
45
+ };
46
+ function autoDetectFormat(content) {
47
+ const preview = Papa.parse(content, {
48
+ preview: 5,
49
+ header: false,
50
+ skipEmptyLines: true,
51
+ });
52
+ if (preview.errors.length > 0 || preview.data.length === 0)
53
+ return undefined;
54
+ const rows = preview.data;
55
+ const firstRow = rows[0];
56
+ if (!firstRow)
57
+ return undefined;
58
+ const headerMatch = detectPreset(firstRow);
59
+ if (headerMatch) {
60
+ const key = Object.keys(BANK_PRESETS).find((k) => BANK_PRESETS[k] === headerMatch);
61
+ if (key)
62
+ return { preset: key, header: true };
63
+ }
64
+ if (checkTDPattern(rows)) {
65
+ return { preset: 'td', header: false };
66
+ }
67
+ return undefined;
68
+ }
69
+ function checkTDPattern(rows) {
70
+ const validRows = rows.filter((r) => r.length >= 4);
71
+ if (validRows.length === 0)
72
+ return false;
73
+ let matchCount = 0;
74
+ for (const row of validRows) {
75
+ if (!/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(row[0] || ''))
76
+ continue;
77
+ const isDebitNumeric = !row[2] || /^-?[\d,.]+$/.test(row[2]);
78
+ const isCreditNumeric = !row[3] || /^-?[\d,.]+$/.test(row[3]);
79
+ if (isDebitNumeric && isCreditNumeric) {
80
+ matchCount++;
81
+ }
82
+ }
83
+ return matchCount > validRows.length / 2;
84
+ }
85
+ export function parseCSV(content, options = {}) {
86
+ const errors = [];
87
+ const warnings = [];
88
+ const MAX_BYTES = options.maxBytes ?? 10 * 1024 * 1024;
89
+ if (content.length > MAX_BYTES) {
90
+ throw new Error(`File size exceeds limit of ${Math.round(MAX_BYTES / 1024 / 1024)}MB`);
91
+ }
92
+ let detectedPreset = options.preset;
93
+ let detectedHeader = options.header;
94
+ if (!detectedPreset || detectedHeader === undefined) {
95
+ const autoResult = autoDetectFormat(content);
96
+ if (autoResult) {
97
+ if (!detectedPreset) {
98
+ detectedPreset = autoResult.preset;
99
+ }
100
+ if (detectedHeader === undefined && autoResult.header !== undefined) {
101
+ detectedHeader = autoResult.header;
102
+ }
103
+ }
104
+ }
105
+ let hasHeader = true;
106
+ if (detectedHeader !== undefined) {
107
+ hasHeader = detectedHeader;
108
+ }
109
+ else if (detectedPreset) {
110
+ const preset = BANK_PRESETS[detectedPreset];
111
+ if (preset && preset.header !== undefined) {
112
+ hasHeader = preset.header;
113
+ }
114
+ }
115
+ const maxRows = options.maxRows ?? 10000;
116
+ const parsed = Papa.parse(content, {
117
+ header: hasHeader,
118
+ preview: maxRows + (hasHeader ? 1 : 0),
119
+ dynamicTyping: false,
120
+ skipEmptyLines: true,
121
+ transformHeader: (h) => h.trim(),
122
+ });
123
+ if (parsed.errors.length > 0) {
124
+ for (const err of parsed.errors) {
125
+ errors.push({
126
+ row: err.row ?? 0,
127
+ field: 'csv',
128
+ message: err.message,
129
+ rawValue: '',
130
+ });
131
+ }
132
+ }
133
+ const rows = parsed.data;
134
+ let columns = [];
135
+ if (hasHeader) {
136
+ columns = parsed.meta.fields ?? [];
137
+ }
138
+ else {
139
+ const maxLen = rows.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0);
140
+ columns = Array.from({ length: maxLen }, (_, i) => String(i));
141
+ }
142
+ const preset = detectedPreset
143
+ ? BANK_PRESETS[detectedPreset]
144
+ : hasHeader
145
+ ? detectPreset(columns)
146
+ : undefined;
147
+ const dateCandidates = options.columns?.date
148
+ ? [options.columns.date]
149
+ : (preset?.dateColumn ?? ['Date', 'Transaction Date', 'Posted Date']);
150
+ const descCandidates = options.columns?.description
151
+ ? [options.columns.description]
152
+ : (preset?.descriptionColumn ?? ['Description', 'Payee', 'Merchant', 'Name']);
153
+ const dateCol = findColumn(columns, dateCandidates, !hasHeader);
154
+ const descCol = findColumn(columns, descCandidates, !hasHeader);
155
+ let amountCol = null;
156
+ let debitCol = null;
157
+ let creditCol = null;
158
+ if (options.columns?.debit && options.columns?.credit) {
159
+ debitCol = findColumn(columns, [options.columns.debit], !hasHeader);
160
+ creditCol = findColumn(columns, [options.columns.credit], !hasHeader);
161
+ }
162
+ else if (preset?.debitColumn &&
163
+ preset?.creditColumn &&
164
+ !options.columns?.amount &&
165
+ (hasHeader ? !preset?.amountColumn : true)) {
166
+ debitCol = findColumn(columns, [preset.debitColumn], !hasHeader);
167
+ creditCol = findColumn(columns, [preset.creditColumn], !hasHeader);
168
+ }
169
+ else {
170
+ const amountCandidates = options.columns?.amount
171
+ ? [options.columns.amount]
172
+ : (preset?.amountColumn ?? ['Amount', 'CAD$', 'Value']);
173
+ amountCol = findColumn(columns, amountCandidates, !hasHeader);
174
+ }
175
+ if (!dateCol) {
176
+ errors.push({
177
+ row: 0,
178
+ field: 'date',
179
+ message: `Could not identify date column from: ${columns.join(', ')}. Try using preset option (td, rbc, scotiabank, etc.) or specify columns manually with columns.date`,
180
+ rawValue: columns.join(', '),
181
+ });
182
+ }
183
+ if (!amountCol && (!debitCol || !creditCol)) {
184
+ if (!debitCol && !creditCol) {
185
+ errors.push({
186
+ row: 0,
187
+ field: 'amount',
188
+ message: `Could not identify amount column from: ${columns.join(', ')}. Try using preset option or specify columns manually with columns.amount (or columns.debit/credit for split columns)`,
189
+ rawValue: columns.join(', '),
190
+ });
191
+ }
192
+ else if (!debitCol || !creditCol) {
193
+ errors.push({
194
+ row: 0,
195
+ field: 'amount',
196
+ message: `Could not identify debit/credit columns pair from: ${columns.join(', ')}. Found ${debitCol ? 'debit' : 'credit'} but missing ${debitCol ? 'credit' : 'debit'}. Specify both with columns.debit and columns.credit`,
197
+ rawValue: columns.join(', '),
198
+ });
199
+ }
200
+ }
201
+ const transactions = [];
202
+ const dateFormat = options.dateFormat ?? preset?.dateFormat;
203
+ for (let i = 0; i < Math.min(rows.length, maxRows); i++) {
204
+ const row = rows[i];
205
+ if (!row)
206
+ continue;
207
+ const getValue = (colName) => {
208
+ if (!colName)
209
+ return '';
210
+ if (Array.isArray(row)) {
211
+ const idx = parseInt(colName, 10);
212
+ return String(row[idx] ?? '');
213
+ }
214
+ return String(row[colName] ?? '');
215
+ };
216
+ const rowNum = i + (hasHeader ? 2 : 1);
217
+ const rowWarnings = [];
218
+ const rawDate = getValue(dateCol)?.trim() ?? '';
219
+ const parsedDate = parseDate(rawDate, dateFormat);
220
+ if (!parsedDate) {
221
+ errors.push({
222
+ row: rowNum,
223
+ field: 'date',
224
+ message: `Could not parse date: "${rawDate}"`,
225
+ rawValue: rawDate,
226
+ });
227
+ continue;
228
+ }
229
+ const dateStr = formatLocalDate(parsedDate);
230
+ let amountMilliunits;
231
+ let rawAmount;
232
+ if (amountCol) {
233
+ rawAmount = getValue(amountCol)?.trim() ?? '';
234
+ amountMilliunits = dollarStringToMilliunits(rawAmount);
235
+ }
236
+ else if (debitCol && creditCol) {
237
+ const debit = getValue(debitCol)?.trim() ?? '';
238
+ const credit = getValue(creditCol)?.trim() ?? '';
239
+ rawAmount = debit || credit;
240
+ const debitMilliunits = dollarStringToMilliunits(debit);
241
+ const creditMilliunits = dollarStringToMilliunits(credit);
242
+ if (Math.abs(debitMilliunits) > 0 && Math.abs(creditMilliunits) > 0) {
243
+ const warning = `Both Debit (${debit}) and Credit (${credit}) have values - using Debit`;
244
+ rowWarnings.push(warning);
245
+ warnings.push({ row: rowNum, message: warning });
246
+ }
247
+ if (Math.abs(debitMilliunits) > 0) {
248
+ amountMilliunits = -Math.abs(debitMilliunits);
249
+ }
250
+ else if (Math.abs(creditMilliunits) > 0) {
251
+ amountMilliunits = Math.abs(creditMilliunits);
252
+ }
253
+ else {
254
+ amountMilliunits = 0;
255
+ }
256
+ if (debitMilliunits < 0) {
257
+ const warning = `Debit column contains negative value (${debit}) - treating as positive debit`;
258
+ rowWarnings.push(warning);
259
+ warnings.push({ row: rowNum, message: warning });
260
+ }
261
+ }
262
+ else {
263
+ continue;
264
+ }
265
+ if (!Number.isFinite(amountMilliunits)) {
266
+ errors.push({
267
+ row: rowNum,
268
+ field: 'amount',
269
+ message: `Invalid amount: "${rawAmount}"`,
270
+ rawValue: rawAmount,
271
+ });
272
+ continue;
273
+ }
274
+ const multiplier = options.invertAmounts ? -1 : (preset?.amountMultiplier ?? 1);
275
+ amountMilliunits *= multiplier;
276
+ let rawDesc = getValue(descCol)?.trim() ?? '';
277
+ rawDesc = rawDesc
278
+ .replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
279
+ .replace(/[\u202A-\u202E\u2066-\u2069]/g, '')
280
+ .replace(/[\u200B-\u200D\uFEFF]/g, '')
281
+ .replace(/[\u2028-\u2029]/g, '')
282
+ .substring(0, 500);
283
+ transactions.push({
284
+ id: randomUUID(),
285
+ date: dateStr,
286
+ amount: amountMilliunits,
287
+ payee: rawDesc || 'Unknown',
288
+ sourceRow: rowNum,
289
+ raw: {
290
+ date: rawDate,
291
+ amount: rawAmount,
292
+ description: rawDesc,
293
+ },
294
+ ...(rowWarnings.length > 0 && { warnings: rowWarnings }),
295
+ });
296
+ }
297
+ return {
298
+ transactions,
299
+ errors,
300
+ warnings,
301
+ meta: {
302
+ detectedDelimiter: parsed.meta.delimiter || ',',
303
+ detectedColumns: columns,
304
+ totalRows: rows.length,
305
+ validRows: transactions.length,
306
+ skippedRows: rows.length - transactions.length,
307
+ },
308
+ };
309
+ }
310
+ function parseDate(raw, formatHint) {
311
+ if (!raw)
312
+ return null;
313
+ const isoMatch = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
314
+ if (isoMatch) {
315
+ const [, year, month, day] = isoMatch;
316
+ return new Date(Date.UTC(parseInt(year), parseInt(month) - 1, parseInt(day)));
317
+ }
318
+ const numericMatch = raw.match(/^(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})$/);
319
+ if (numericMatch && formatHint) {
320
+ const [, a, b, c] = numericMatch;
321
+ let year, month, day;
322
+ switch (formatHint) {
323
+ case 'YMD':
324
+ year = parseInt(a);
325
+ month = parseInt(b);
326
+ day = parseInt(c);
327
+ break;
328
+ case 'MDY':
329
+ month = parseInt(a);
330
+ day = parseInt(b);
331
+ year = parseInt(c);
332
+ break;
333
+ case 'DMY':
334
+ day = parseInt(a);
335
+ month = parseInt(b);
336
+ year = parseInt(c);
337
+ break;
338
+ }
339
+ if (year < 100)
340
+ year += 2000;
341
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
342
+ return new Date(Date.UTC(year, month - 1, day));
343
+ }
344
+ }
345
+ const parsed = chrono.parseDate(raw);
346
+ if (parsed) {
347
+ return new Date(Date.UTC(parsed.getFullYear(), parsed.getMonth(), parsed.getDate()));
348
+ }
349
+ return null;
350
+ }
351
+ function formatLocalDate(date) {
352
+ const year = date.getUTCFullYear();
353
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
354
+ const day = String(date.getUTCDate()).padStart(2, '0');
355
+ return `${year}-${month}-${day}`;
356
+ }
357
+ function findColumn(available, candidates, exactIndex = false) {
358
+ const candidateList = Array.isArray(candidates) ? candidates : [candidates];
359
+ for (const candidate of candidateList) {
360
+ if (exactIndex) {
361
+ if (available.includes(candidate))
362
+ return candidate;
363
+ }
364
+ else {
365
+ const lower = candidate.toLowerCase();
366
+ const found = available.find((col) => col.toLowerCase() === lower);
367
+ if (found)
368
+ return found;
369
+ }
370
+ }
371
+ if (!exactIndex) {
372
+ for (const candidate of candidateList) {
373
+ const lower = candidate.toLowerCase();
374
+ const found = available.find((col) => col.toLowerCase().includes(lower));
375
+ if (found)
376
+ return found;
377
+ }
378
+ }
379
+ return null;
380
+ }
381
+ function detectPreset(columns) {
382
+ const colSet = new Set(columns.map((c) => c.toLowerCase()));
383
+ if (colSet.has('description 1') || colSet.has('account type')) {
384
+ return BANK_PRESETS['rbc'];
385
+ }
386
+ if (columns.some((c) => c.toLowerCase().includes('cad$'))) {
387
+ return BANK_PRESETS['td'];
388
+ }
389
+ if (colSet.has('date') && colSet.has('description') && colSet.has('amount')) {
390
+ return BANK_PRESETS['td'];
391
+ }
392
+ return undefined;
393
+ }
394
+ const CURRENCY_SYMBOLS = /[$€£¥]/g;
395
+ const CURRENCY_CODES = /\b(CAD|USD|EUR|GBP)\b/gi;
396
+ function dollarStringToMilliunits(str) {
397
+ if (!str)
398
+ return 0;
399
+ let cleaned = str.replace(CURRENCY_SYMBOLS, '').replace(CURRENCY_CODES, '').trim();
400
+ if (cleaned.startsWith('(') && cleaned.endsWith(')')) {
401
+ cleaned = '-' + cleaned.slice(1, -1);
402
+ }
403
+ if (/^-?\d{1,3}(\.\d{3})+,\d{2}$/.test(cleaned)) {
404
+ cleaned = cleaned.replace(/\./g, '').replace(',', '.');
405
+ }
406
+ if (cleaned.includes('.')) {
407
+ cleaned = cleaned.replace(/,/g, '');
408
+ }
409
+ const dollars = parseFloat(cleaned);
410
+ if (!Number.isFinite(dollars))
411
+ return 0;
412
+ return Math.round(dollars * 1000);
413
+ }
@@ -56,6 +56,14 @@ export interface ExecutionResult {
56
56
  bulk_operation_details?: BulkOperationDetails;
57
57
  }
58
58
  export declare function executeReconciliation(options: ExecutionOptions): Promise<ExecutionResult>;
59
+ export interface NormalizedYnabError {
60
+ status?: number;
61
+ name?: string;
62
+ message: string;
63
+ detail?: string;
64
+ }
65
+ export declare function normalizeYnabError(error: unknown): NormalizedYnabError;
66
+ export declare function shouldPropagateYnabError(error: NormalizedYnabError): boolean;
59
67
  declare function buildBalanceReconciliation(args: {
60
68
  ynabAPI: ynab.API;
61
69
  budgetId: string;