@dizzlkheinz/ynab-mcpb 0.13.1 → 0.15.1

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 +8 -2
  77. package/dist/tools/reconciliation/analyzer.js +127 -409
  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 +191 -550
  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 +148 -54
  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,312 +1,58 @@
1
- import { randomUUID } from 'crypto';
2
- import * as bankParser from '../compareTransactions/parser.js';
3
- import { findMatches } from './matcher.js';
4
- import { DEFAULT_MATCHING_CONFIG } from './types.js';
5
- import { toMoneyValueFromDecimal } from '../../utils/money.js';
1
+ import { parseCSV } from './csvParser.js';
2
+ import { findMatches, normalizeConfig, DEFAULT_CONFIG } from './matcher.js';
3
+ import { normalizeYNABTransactions } from './ynabAdapter.js';
4
+ import { toMoneyValue } from '../../utils/money.js';
6
5
  import { generateRecommendations } from './recommendationEngine.js';
7
- function convertYNABTransaction(apiTxn) {
8
- return {
9
- id: apiTxn.id,
10
- date: apiTxn.date,
11
- amount: apiTxn.amount,
12
- payee_name: apiTxn.payee_name || null,
13
- category_name: apiTxn.category_name || null,
14
- cleared: apiTxn.cleared,
15
- approved: apiTxn.approved,
16
- memo: apiTxn.memo || null,
6
+ function mapToTransactionMatch(result) {
7
+ const candidates = result.candidates.map((c) => ({
8
+ ynab_transaction: c.ynabTransaction,
9
+ confidence: c.scores.combined,
10
+ match_reason: c.matchReasons.join(', '),
11
+ explanation: c.matchReasons.join(', '),
12
+ }));
13
+ const match = {
14
+ bankTransaction: result.bankTransaction,
15
+ candidates,
16
+ confidence: result.confidence,
17
+ confidenceScore: result.confidenceScore,
18
+ matchReason: result.bestMatch?.matchReasons.join(', ') ?? 'No match found',
19
+ actionHint: result.confidence === 'high' ? 'approve' : 'review',
17
20
  };
18
- }
19
- const FALLBACK_CSV_FORMAT = {
20
- date_column: 'Date',
21
- amount_column: 'Amount',
22
- description_column: 'Description',
23
- date_format: 'MM/DD/YYYY',
24
- has_header: true,
25
- delimiter: ',',
26
- };
27
- const ENABLE_COMBINATION_MATCHING = true;
28
- const DAYS_IN_MS = 24 * 60 * 60 * 1000;
29
- function toDollars(milliunits) {
30
- return milliunits / 1000;
31
- }
32
- function amountTolerance(config) {
33
- const toleranceCents = config.amountToleranceCents ?? DEFAULT_MATCHING_CONFIG.amountToleranceCents ?? 1;
34
- return Math.max(0, toleranceCents) / 100;
35
- }
36
- function dateTolerance(config) {
37
- return config.dateToleranceDays ?? DEFAULT_MATCHING_CONFIG.dateToleranceDays ?? 2;
38
- }
39
- function daysBetween(dateA, dateB) {
40
- const a = new Date(`${dateA}T00:00:00Z`).getTime();
41
- const b = new Date(`${dateB}T00:00:00Z`).getTime();
42
- if (Number.isNaN(a) || Number.isNaN(b))
43
- return Number.POSITIVE_INFINITY;
44
- return Math.abs(a - b) / DAYS_IN_MS;
45
- }
46
- function withinDateTolerance(bankDate, ynabTxns, toleranceDays) {
47
- return ynabTxns.every((txn) => daysBetween(bankDate, txn.date) <= toleranceDays);
48
- }
49
- function hasMatchingSign(bankAmount, ynabTxns) {
50
- const bankSign = Math.sign(bankAmount);
51
- const sumSign = Math.sign(ynabTxns.reduce((sum, txn) => sum + toDollars(txn.amount), 0));
52
- return bankSign === sumSign || Math.abs(bankAmount) === 0;
53
- }
54
- function computeCombinationConfidence(diff, tolerance, legCount) {
55
- const safeTolerance = tolerance > 0 ? tolerance : 0.01;
56
- const ratio = diff / safeTolerance;
57
- let base = legCount === 2 ? 75 : 70;
58
- if (ratio <= 0.25) {
59
- base += 5;
60
- }
61
- else if (ratio <= 0.5) {
62
- base += 3;
63
- }
64
- else if (ratio >= 0.9) {
65
- base -= 5;
66
- }
67
- return Math.max(65, Math.min(80, Math.round(base)));
68
- }
69
- function formatDifference(diff) {
70
- return formatCurrency(diff);
71
- }
72
- function findCombinationMatches(unmatchedBank, unmatchedYNAB, config) {
73
- if (!ENABLE_COMBINATION_MATCHING || unmatchedBank.length === 0 || unmatchedYNAB.length === 0) {
74
- return { matches: [], insights: [] };
75
- }
76
- const tolerance = amountTolerance(config);
77
- const toleranceDays = dateTolerance(config);
78
- const matches = [];
79
- const insights = [];
80
- const seenCombinations = new Set();
81
- for (const bankTxn of unmatchedBank) {
82
- const viableYnab = unmatchedYNAB.filter((txn) => hasMatchingSign(bankTxn.amount, [txn]));
83
- if (viableYnab.length < 2)
84
- continue;
85
- const evaluated = [];
86
- const addIfValid = (combo) => {
87
- const sum = combo.reduce((acc, txn) => acc + toDollars(txn.amount), 0);
88
- const diff = Math.abs(sum - bankTxn.amount);
89
- if (diff > tolerance)
90
- return;
91
- if (!withinDateTolerance(bankTxn.date, combo, toleranceDays))
92
- return;
93
- if (!hasMatchingSign(bankTxn.amount, combo))
94
- return;
95
- evaluated.push({ txns: combo, diff, sum });
96
- };
97
- const n = viableYnab.length;
98
- for (let i = 0; i < n - 1; i++) {
99
- for (let j = i + 1; j < n; j++) {
100
- addIfValid([viableYnab[i], viableYnab[j]]);
101
- }
102
- }
103
- if (n >= 3) {
104
- for (let i = 0; i < n - 2; i++) {
105
- for (let j = i + 1; j < n - 1; j++) {
106
- for (let k = j + 1; k < n; k++) {
107
- addIfValid([viableYnab[i], viableYnab[j], viableYnab[k]]);
108
- }
109
- }
110
- }
111
- }
112
- if (evaluated.length === 0)
113
- continue;
114
- evaluated.sort((a, b) => a.diff - b.diff);
115
- const recordedSizes = new Set();
116
- for (const combo of evaluated) {
117
- if (recordedSizes.has(combo.txns.length))
118
- continue;
119
- const comboIds = combo.txns.map((txn) => txn.id).sort();
120
- const key = `${bankTxn.id}|${comboIds.join('+')}`;
121
- if (seenCombinations.has(key))
122
- continue;
123
- seenCombinations.add(key);
124
- recordedSizes.add(combo.txns.length);
125
- const score = computeCombinationConfidence(combo.diff, tolerance, combo.txns.length);
126
- const candidateConfidence = Math.max(60, score - 5);
127
- const descriptionTotal = formatCurrency(combo.sum);
128
- const diffLabel = formatDifference(combo.diff);
129
- matches.push({
130
- bank_transaction: bankTxn,
131
- confidence: 'medium',
132
- confidence_score: score,
133
- match_reason: 'combination_match',
134
- top_confidence: score,
135
- candidates: combo.txns.map((txn) => ({
136
- ynab_transaction: txn,
137
- confidence: candidateConfidence,
138
- match_reason: 'combination_component',
139
- explanation: `Part of combination totaling ${descriptionTotal} (difference ${diffLabel}).`,
140
- })),
141
- action_hint: 'review_combination',
142
- recommendation: `Combination of ${combo.txns.length} YNAB transactions totals ${descriptionTotal} versus ` +
143
- `${formatCurrency(bankTxn.amount)} on the bank statement.`,
144
- });
145
- const insightId = `combination-${bankTxn.id}-${comboIds.join('+')}`;
146
- insights.push({
147
- id: insightId,
148
- type: 'combination_match',
149
- severity: 'info',
150
- title: `Combination of ${combo.txns.length} transactions matches ${formatCurrency(bankTxn.amount)}`,
151
- description: `${combo.txns.length} YNAB transactions totaling ${descriptionTotal} align with ` +
152
- `${formatCurrency(bankTxn.amount)} from ${bankTxn.payee}. Difference ${diffLabel}.`,
153
- evidence: {
154
- bank_transaction_id: bankTxn.id,
155
- bank_amount: bankTxn.amount,
156
- ynab_transaction_ids: comboIds,
157
- ynab_amounts_milliunits: combo.txns.map((txn) => txn.amount),
158
- combination_size: combo.txns.length,
159
- difference: combo.diff,
160
- },
161
- });
162
- }
163
- }
164
- return { matches, insights };
165
- }
166
- function isParsedCSVData(result) {
167
- return (typeof result === 'object' &&
168
- result !== null &&
169
- !Array.isArray(result) &&
170
- 'transactions' in result);
171
- }
172
- function normalizeDate(value) {
173
- if (value instanceof Date) {
174
- return value.toISOString().split('T')[0];
175
- }
176
- if (typeof value === 'string') {
177
- const trimmed = value.trim();
178
- if (!trimmed)
179
- return trimmed;
180
- const parsed = new Date(trimmed);
181
- if (!Number.isNaN(parsed.getTime())) {
182
- return parsed.toISOString().split('T')[0];
183
- }
184
- return trimmed;
185
- }
186
- return new Date().toISOString().split('T')[0];
187
- }
188
- function normalizeAmount(record) {
189
- const raw = record['amount'];
190
- if (typeof raw === 'number') {
191
- if (record['date'] instanceof Date || 'raw_amount' in record || 'raw_date' in record) {
192
- return Math.round(raw) / 1000;
193
- }
194
- return raw;
195
- }
196
- if (typeof raw === 'string') {
197
- const cleaned = raw.replace(/[$,\s]/g, '');
198
- const parsed = Number.parseFloat(cleaned);
199
- return Number.isFinite(parsed) ? parsed : 0;
200
- }
201
- return 0;
202
- }
203
- function normalizePayee(record) {
204
- const candidates = [record['payee'], record['description'], record['memo']];
205
- for (const candidate of candidates) {
206
- if (typeof candidate === 'string' && candidate.trim()) {
207
- return candidate.trim();
208
- }
209
- }
210
- return 'Unknown Payee';
211
- }
212
- function determineRow(record, index) {
213
- if (typeof record['original_csv_row'] === 'number') {
214
- return record['original_csv_row'];
215
- }
216
- if (typeof record['row_number'] === 'number') {
217
- return record['row_number'];
218
- }
219
- return index + 1;
220
- }
221
- function convertParserRecord(record, index) {
222
- const data = typeof record === 'object' && record !== null ? record : {};
223
- const dateValue = normalizeDate(data['date']);
224
- const amountValue = normalizeAmount(data);
225
- const payeeValue = normalizePayee(data);
226
- const memoValue = typeof data['memo'] === 'string' && data['memo'].trim() ? data['memo'].trim() : undefined;
227
- const originalRow = determineRow(data, index);
228
- const transaction = {
229
- id: randomUUID(),
230
- date: dateValue,
231
- amount: amountValue,
232
- payee: payeeValue,
233
- original_csv_row: originalRow,
234
- };
235
- if (memoValue !== undefined) {
236
- transaction.memo = memoValue;
237
- }
238
- return transaction;
239
- }
240
- function parseBankStatement(csvContent, csvFilePath) {
241
- const content = csvFilePath ? bankParser.readCSVFile(csvFilePath) : csvContent;
242
- let format = FALLBACK_CSV_FORMAT;
243
- let autoDetect;
244
- try {
245
- autoDetect = bankParser
246
- .autoDetectCSVFormat;
247
- }
248
- catch {
249
- autoDetect = undefined;
250
- }
251
- if (typeof autoDetect === 'function') {
252
- try {
253
- format = autoDetect(content);
254
- }
255
- catch {
256
- format = FALLBACK_CSV_FORMAT;
257
- }
21
+ if (result.bestMatch) {
22
+ match.ynabTransaction = result.bestMatch.ynabTransaction;
258
23
  }
259
- const rawResult = bankParser.parseBankCSV(content, format);
260
- const records = isParsedCSVData(rawResult) ? rawResult.transactions : rawResult;
261
- return records.map(convertParserRecord);
262
- }
263
- function categorizeMatches(matches) {
264
- const autoMatches = [];
265
- const suggestedMatches = [];
266
- const unmatchedBank = [];
267
- for (const match of matches) {
268
- if (match.confidence === 'high') {
269
- autoMatches.push(match);
270
- }
271
- else if (match.confidence === 'medium') {
272
- suggestedMatches.push(match);
273
- }
274
- else {
275
- unmatchedBank.push(match.bank_transaction);
276
- }
24
+ if (result.candidates[0]) {
25
+ match.topConfidence = result.candidates[0].scores.combined;
277
26
  }
278
- return { autoMatches, suggestedMatches, unmatchedBank };
279
- }
280
- function findUnmatchedYNAB(ynabTransactions, matches) {
281
- const matchedIds = new Set();
282
- for (const match of matches) {
283
- if (match.ynab_transaction) {
284
- matchedIds.add(match.ynab_transaction.id);
285
- }
27
+ if (result.confidence === 'none') {
28
+ match.recommendation = 'This bank transaction is not in YNAB. Consider adding it.';
286
29
  }
287
- return ynabTransactions.filter((txn) => !matchedIds.has(txn.id));
30
+ return match;
288
31
  }
289
- function calculateBalances(ynabTransactions, statementBalance, currency) {
290
- let clearedBalance = 0;
291
- let unclearedBalance = 0;
32
+ function calculateBalances(ynabTransactions, statementBalanceDecimal, currency, accountSnapshot) {
33
+ let computedCleared = 0;
34
+ let computedUncleared = 0;
292
35
  for (const txn of ynabTransactions) {
293
- const amount = txn.amount / 1000;
36
+ const amount = txn.amount;
294
37
  if (txn.cleared === 'cleared' || txn.cleared === 'reconciled') {
295
- clearedBalance += amount;
38
+ computedCleared += amount;
296
39
  }
297
40
  else {
298
- unclearedBalance += amount;
41
+ computedUncleared += amount;
299
42
  }
300
43
  }
301
- const totalBalance = clearedBalance + unclearedBalance;
302
- const discrepancy = clearedBalance - statementBalance;
44
+ const clearedBalance = accountSnapshot?.cleared_balance ?? computedCleared;
45
+ const unclearedBalance = accountSnapshot?.uncleared_balance ?? computedUncleared;
46
+ const totalBalance = accountSnapshot?.balance ?? clearedBalance + unclearedBalance;
47
+ const statementBalanceMilli = Math.round(statementBalanceDecimal * 1000);
48
+ const discrepancy = clearedBalance - statementBalanceMilli;
303
49
  return {
304
- current_cleared: toMoneyValueFromDecimal(clearedBalance, currency),
305
- current_uncleared: toMoneyValueFromDecimal(unclearedBalance, currency),
306
- current_total: toMoneyValueFromDecimal(totalBalance, currency),
307
- target_statement: toMoneyValueFromDecimal(statementBalance, currency),
308
- discrepancy: toMoneyValueFromDecimal(discrepancy, currency),
309
- on_track: Math.abs(discrepancy) < 0.01,
50
+ current_cleared: toMoneyValue(clearedBalance, currency),
51
+ current_uncleared: toMoneyValue(unclearedBalance, currency),
52
+ current_total: toMoneyValue(totalBalance, currency),
53
+ target_statement: toMoneyValue(statementBalanceMilli, currency),
54
+ discrepancy: toMoneyValue(discrepancy, currency),
55
+ on_track: Math.abs(discrepancy) < 10,
310
56
  };
311
57
  }
312
58
  function generateSummary(bankTransactions, ynabTransactions, autoMatches, suggestedMatches, unmatchedBank, unmatchedYNAB, balances) {
@@ -363,23 +109,23 @@ function generateNextSteps(summary) {
363
109
  }
364
110
  return steps;
365
111
  }
366
- function formatCurrency(amount) {
112
+ function formatCurrency(amountMilli, currency = 'USD') {
367
113
  const formatter = new Intl.NumberFormat('en-US', {
368
114
  style: 'currency',
369
- currency: 'USD',
115
+ currency: currency,
370
116
  minimumFractionDigits: 2,
371
117
  maximumFractionDigits: 2,
372
118
  });
373
- return formatter.format(amount);
119
+ return formatter.format(amountMilli / 1000);
374
120
  }
375
- function repeatAmountInsights(unmatchedBank) {
121
+ function repeatAmountInsights(unmatchedBank, currency = 'USD') {
376
122
  const insights = [];
377
123
  if (unmatchedBank.length === 0) {
378
124
  return insights;
379
125
  }
380
126
  const frequency = new Map();
381
127
  for (const txn of unmatchedBank) {
382
- const key = txn.amount.toFixed(2);
128
+ const key = txn.amount;
383
129
  const entry = frequency.get(key) ?? { amount: txn.amount, txns: [] };
384
130
  entry.txns.push(txn);
385
131
  frequency.set(key, entry);
@@ -392,72 +138,29 @@ function repeatAmountInsights(unmatchedBank) {
392
138
  }
393
139
  const top = repeated[0];
394
140
  insights.push({
395
- id: `repeat-${top.amount.toFixed(2)}`,
141
+ id: `repeat-${top.amount}`,
396
142
  type: 'repeat_amount',
397
143
  severity: top.txns.length >= 4 ? 'critical' : 'warning',
398
- title: `${top.txns.length} unmatched transactions at ${formatCurrency(top.amount)}`,
399
- description: `The bank statement shows ${top.txns.length} unmatched transaction(s) at ${formatCurrency(top.amount)}. ` +
144
+ title: `${top.txns.length} unmatched transactions at ${formatCurrency(top.amount, currency)}`,
145
+ description: `The bank statement shows ${top.txns.length} unmatched transaction(s) at ${formatCurrency(top.amount, currency)}. ` +
400
146
  'Repeated amounts are usually the quickest wins — reconcile these first.',
401
147
  evidence: {
402
148
  amount: top.amount,
403
149
  occurrences: top.txns.length,
404
150
  dates: top.txns.map((txn) => txn.date),
405
- csv_rows: top.txns.map((txn) => txn.original_csv_row),
151
+ csv_rows: top.txns.map((txn) => txn.sourceRow),
406
152
  },
407
153
  });
408
154
  return insights;
409
155
  }
410
- function nearMatchInsights(matches, config) {
156
+ function anomalyInsights(balances) {
411
157
  const insights = [];
412
- for (const match of matches) {
413
- if (!match.candidates || match.candidates.length === 0)
414
- continue;
415
- if (match.confidence === 'high')
416
- continue;
417
- const topCandidate = match.candidates[0];
418
- const score = topCandidate.confidence;
419
- const highSignal = (match.confidence === 'medium' && score >= config.autoMatchThreshold - 5) ||
420
- (match.confidence === 'low' && score >= config.suggestionThreshold) ||
421
- (match.confidence === 'none' && score >= config.suggestionThreshold);
422
- if (!highSignal)
423
- continue;
424
- const bankTxn = match.bank_transaction;
425
- const ynabTxn = topCandidate.ynab_transaction;
426
- insights.push({
427
- id: `near-${bankTxn.id}`,
428
- type: 'near_match',
429
- severity: score >= config.autoMatchThreshold ? 'warning' : 'info',
430
- title: `${formatCurrency(bankTxn.amount)} nearly matches ${formatCurrency(ynabTxn.amount / 1000)}`,
431
- description: `Bank transaction on ${bankTxn.date} (${formatCurrency(bankTxn.amount)}) nearly matches ` +
432
- `${ynabTxn.payee_name ?? 'unknown payee'} on ${ynabTxn.date}. Confidence ${score}% — review and confirm.`,
433
- evidence: {
434
- bank_transaction: {
435
- id: bankTxn.id,
436
- date: bankTxn.date,
437
- amount: bankTxn.amount,
438
- payee: bankTxn.payee,
439
- },
440
- candidate: {
441
- id: ynabTxn.id,
442
- date: ynabTxn.date,
443
- amount_milliunits: ynabTxn.amount,
444
- payee_name: ynabTxn.payee_name,
445
- confidence: score,
446
- reasons: topCandidate.match_reason,
447
- },
448
- },
449
- });
450
- }
451
- return insights.slice(0, 3);
452
- }
453
- function anomalyInsights(summary, balances) {
454
- const insights = [];
455
- const discrepancyAbs = Math.abs(balances.discrepancy.value);
456
- if (discrepancyAbs >= 1) {
158
+ const discrepancyAbs = Math.abs(balances.discrepancy.value_milliunits);
159
+ if (discrepancyAbs >= 1000) {
457
160
  insights.push({
458
161
  id: 'balance-gap',
459
162
  type: 'anomaly',
460
- severity: discrepancyAbs >= 100 ? 'critical' : 'warning',
163
+ severity: discrepancyAbs >= 100000 ? 'critical' : 'warning',
461
164
  title: `Cleared balance off by ${balances.discrepancy.value_display}`,
462
165
  description: `YNAB cleared balance is ${balances.current_cleared.value_display} but the statement expects ` +
463
166
  `${balances.target_statement.value_display}. Focus on closing this gap.`,
@@ -468,22 +171,9 @@ function anomalyInsights(summary, balances) {
468
171
  },
469
172
  });
470
173
  }
471
- if (summary.unmatched_bank >= 5) {
472
- insights.push({
473
- id: 'bulk-missing-bank',
474
- type: 'anomaly',
475
- severity: summary.unmatched_bank >= 10 ? 'critical' : 'warning',
476
- title: `${summary.unmatched_bank} bank transactions still unmatched`,
477
- description: `There are ${summary.unmatched_bank} bank transactions without a match. ` +
478
- 'Consider bulk importing or reviewing by date sequence.',
479
- evidence: {
480
- unmatched_bank: summary.unmatched_bank,
481
- },
482
- });
483
- }
484
174
  return insights;
485
175
  }
486
- function detectInsights(matches, unmatchedBank, summary, balances, config) {
176
+ function detectInsights(unmatchedBank, _summary, balances, currency, csvErrors = [], csvWarnings = []) {
487
177
  const insights = [];
488
178
  const seen = new Set();
489
179
  const addUnique = (insight) => {
@@ -492,62 +182,90 @@ function detectInsights(matches, unmatchedBank, summary, balances, config) {
492
182
  seen.add(insight.id);
493
183
  insights.push(insight);
494
184
  };
495
- for (const insight of repeatAmountInsights(unmatchedBank)) {
496
- addUnique(insight);
185
+ if (csvErrors.length > 0) {
186
+ addUnique({
187
+ id: 'csv-parse-errors',
188
+ type: 'anomaly',
189
+ severity: csvErrors.length >= 5 ? 'critical' : 'warning',
190
+ title: `${csvErrors.length} CSV parsing error(s)`,
191
+ description: csvErrors
192
+ .slice(0, 3)
193
+ .map((e) => `Row ${e.row}: ${e.message}`)
194
+ .join('; ') + (csvErrors.length > 3 ? ` (+${csvErrors.length - 3} more)` : ''),
195
+ evidence: {
196
+ error_count: csvErrors.length,
197
+ errors: csvErrors.slice(0, 5),
198
+ },
199
+ });
200
+ }
201
+ if (csvWarnings.length > 0) {
202
+ addUnique({
203
+ id: 'csv-parse-warnings',
204
+ type: 'anomaly',
205
+ severity: 'info',
206
+ title: `${csvWarnings.length} CSV parsing warning(s)`,
207
+ description: csvWarnings
208
+ .slice(0, 3)
209
+ .map((w) => `Row ${w.row}: ${w.message}`)
210
+ .join('; ') + (csvWarnings.length > 3 ? ` (+${csvWarnings.length - 3} more)` : ''),
211
+ evidence: {
212
+ warning_count: csvWarnings.length,
213
+ warnings: csvWarnings.slice(0, 5),
214
+ },
215
+ });
497
216
  }
498
- for (const insight of nearMatchInsights(matches, config)) {
217
+ for (const insight of repeatAmountInsights(unmatchedBank, currency)) {
499
218
  addUnique(insight);
500
219
  }
501
- for (const insight of anomalyInsights(summary, balances)) {
220
+ for (const insight of anomalyInsights(balances)) {
502
221
  addUnique(insight);
503
222
  }
504
223
  return insights.slice(0, 5);
505
224
  }
506
- function mergeInsights(base, additional) {
507
- if (additional.length === 0) {
508
- return base;
509
- }
510
- const seen = new Set(base.map((insight) => insight.id));
511
- const merged = [...base];
512
- for (const insight of additional) {
513
- if (seen.has(insight.id))
514
- continue;
515
- seen.add(insight.id);
516
- merged.push(insight);
517
- }
518
- return merged.slice(0, 5);
519
- }
520
- export function analyzeReconciliation(csvContent, csvFilePath, ynabTransactions, statementBalance, config = DEFAULT_MATCHING_CONFIG, currency = 'USD', accountId, budgetId, invertBankAmounts = false) {
521
- let bankTransactions = parseBankStatement(csvContent, csvFilePath);
522
- if (invertBankAmounts) {
523
- bankTransactions = bankTransactions.map((txn) => ({
524
- ...txn,
525
- amount: -txn.amount,
526
- }));
527
- }
528
- const convertedYNABTxns = ynabTransactions.map(convertYNABTransaction);
529
- const matches = findMatches(bankTransactions, convertedYNABTxns, config);
530
- const { autoMatches, suggestedMatches, unmatchedBank } = categorizeMatches(matches);
531
- const unmatchedYNAB = findUnmatchedYNAB(convertedYNABTxns, matches);
532
- let combinationMatches = [];
533
- let combinationInsights = [];
534
- if (ENABLE_COMBINATION_MATCHING) {
535
- const combinationResult = findCombinationMatches(unmatchedBank, unmatchedYNAB, config);
536
- combinationMatches = combinationResult.matches;
537
- combinationInsights = combinationResult.insights;
225
+ export function analyzeReconciliation(csvContentOrParsed, _csvFilePath, ynabTransactions, statementBalance, config = DEFAULT_CONFIG, currency = 'USD', accountId, budgetId, invertBankAmounts = false, csvOptions, accountSnapshot) {
226
+ let parseResult;
227
+ if (typeof csvContentOrParsed === 'string') {
228
+ parseResult = parseCSV(csvContentOrParsed, {
229
+ ...csvOptions,
230
+ invertAmounts: invertBankAmounts,
231
+ });
538
232
  }
539
- const enrichedSuggestedMatches = [...suggestedMatches, ...combinationMatches];
540
- const balances = calculateBalances(convertedYNABTxns, statementBalance, currency);
541
- const summary = generateSummary(bankTransactions, convertedYNABTxns, autoMatches, enrichedSuggestedMatches, unmatchedBank, unmatchedYNAB, balances);
233
+ else {
234
+ parseResult = csvContentOrParsed;
235
+ }
236
+ const newBankTransactions = parseResult.transactions;
237
+ const csvParseErrors = parseResult.errors;
238
+ const csvParseWarnings = parseResult.warnings;
239
+ const newYNABTransactions = normalizeYNABTransactions(ynabTransactions);
240
+ const normalizedConfig = normalizeConfig(config);
241
+ const newMatches = findMatches(newBankTransactions, newYNABTransactions, normalizedConfig);
242
+ const matches = newMatches.map(mapToTransactionMatch);
243
+ const autoMatches = matches.filter((m) => m.confidence === 'high');
244
+ const autoMatchedYnabIds = new Set();
245
+ autoMatches.forEach((m) => {
246
+ if (m.ynabTransaction)
247
+ autoMatchedYnabIds.add(m.ynabTransaction.id);
248
+ });
249
+ const suggestedMatches = matches.filter((m) => m.confidence === 'medium' &&
250
+ (!m.ynabTransaction || !autoMatchedYnabIds.has(m.ynabTransaction.id)));
251
+ const unmatchedBankMatches = matches.filter((m) => m.confidence === 'low' || m.confidence === 'none');
252
+ const unmatchedBank = unmatchedBankMatches.map((m) => m.bankTransaction);
253
+ const matchedYnabIds = new Set();
254
+ matches.forEach((m) => {
255
+ if (m.ynabTransaction)
256
+ matchedYnabIds.add(m.ynabTransaction.id);
257
+ });
258
+ const unmatchedYNAB = newYNABTransactions.filter((t) => !matchedYnabIds.has(t.id));
259
+ const balances = calculateBalances(newYNABTransactions, statementBalance, currency, accountSnapshot);
260
+ const summary = generateSummary(matches.map((m) => m.bankTransaction), newYNABTransactions, autoMatches, suggestedMatches, unmatchedBank, unmatchedYNAB, balances);
542
261
  const nextSteps = generateNextSteps(summary);
543
- const baseInsights = detectInsights(matches, unmatchedBank, summary, balances, config);
544
- const insights = mergeInsights(baseInsights, combinationInsights);
262
+ const insights = detectInsights(unmatchedBank, summary, balances, currency, csvParseErrors, csvParseWarnings);
545
263
  const analysis = {
546
264
  success: true,
547
265
  phase: 'analysis',
548
266
  summary,
549
267
  auto_matches: autoMatches,
550
- suggested_matches: enrichedSuggestedMatches,
268
+ suggested_matches: suggestedMatches,
551
269
  unmatched_bank: unmatchedBank,
552
270
  unmatched_ynab: unmatchedYNAB,
553
271
  balance_info: balances,
@@ -559,7 +277,7 @@ export function analyzeReconciliation(csvContent, csvFilePath, ynabTransactions,
559
277
  account_id: accountId,
560
278
  budget_id: budgetId,
561
279
  analysis,
562
- matching_config: config,
280
+ matching_config: normalizedConfig,
563
281
  });
564
282
  analysis.recommendations = recommendations;
565
283
  }
@@ -0,0 +1,51 @@
1
+ import type { BankTransaction } from '../../types/reconciliation.js';
2
+ export interface CSVParseResult {
3
+ transactions: BankTransaction[];
4
+ errors: ParseError[];
5
+ warnings: ParseWarning[];
6
+ meta: {
7
+ detectedDelimiter: string;
8
+ detectedColumns: string[];
9
+ totalRows: number;
10
+ validRows: number;
11
+ skippedRows: number;
12
+ };
13
+ }
14
+ export interface ParseError {
15
+ row: number;
16
+ field: string;
17
+ message: string;
18
+ rawValue: string;
19
+ }
20
+ export interface ParseWarning {
21
+ row: number;
22
+ message: string;
23
+ }
24
+ export interface BankPreset {
25
+ name: string;
26
+ dateColumn: string | string[];
27
+ amountColumn?: string | string[];
28
+ debitColumn?: string;
29
+ creditColumn?: string;
30
+ descriptionColumn: string | string[];
31
+ amountMultiplier?: number;
32
+ dateFormat?: 'YMD' | 'MDY' | 'DMY';
33
+ header?: boolean;
34
+ }
35
+ export declare const BANK_PRESETS: Record<string, BankPreset>;
36
+ export interface ParseCSVOptions {
37
+ preset?: string;
38
+ invertAmounts?: boolean;
39
+ columns?: {
40
+ date?: string;
41
+ amount?: string;
42
+ debit?: string;
43
+ credit?: string;
44
+ description?: string;
45
+ };
46
+ dateFormat?: 'YMD' | 'MDY' | 'DMY';
47
+ header?: boolean;
48
+ maxRows?: number;
49
+ maxBytes?: number;
50
+ }
51
+ export declare function parseCSV(content: string, options?: ParseCSVOptions): CSVParseResult;