@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
@@ -173,16 +173,17 @@ function formatBankTransactionLine(txn: BankTransaction): string {
173
173
  * Format a suggested match line
174
174
  */
175
175
  function formatSuggestedMatchLine(match: TransactionMatch): string {
176
- const bankTxn = match.bank_transaction;
176
+ const bankTxn = match.bankTransaction;
177
177
  const amountStr = formatAmount(bankTxn.amount);
178
- const confidenceStr = `${match.confidence_score}%`;
178
+ const confidenceStr = `${match.confidenceScore}%`;
179
179
  return ` ${bankTxn.date} - ${bankTxn.payee.substring(0, 35).padEnd(35)} ${amountStr} (${confidenceStr} confidence)`;
180
180
  }
181
181
 
182
182
  /**
183
- * Format an amount for display
183
+ * Format an amount for display (input in milliunits)
184
184
  */
185
- function formatAmount(amount: number): string {
185
+ function formatAmount(amountMilli: number): string {
186
+ const amount = amountMilli / 1000;
186
187
  const sign = amount >= 0 ? '+' : '-';
187
188
  const absAmount = Math.abs(amount);
188
189
  return `${sign}$${absAmount.toFixed(2)}`.padStart(10);
@@ -336,6 +337,8 @@ export function formatBalanceInfo(balance: BalanceInfo): string {
336
337
  /**
337
338
  * Format transaction list (helper for detailed reports)
338
339
  */
340
+ type FormattableYnabTransaction = YNABTransaction & { payee_name?: string | null };
341
+
339
342
  export function formatTransactionList(
340
343
  transactions: BankTransaction[] | YNABTransaction[],
341
344
  maxItems: number = 10,
@@ -344,14 +347,16 @@ export function formatTransactionList(
344
347
  const toShow = transactions.slice(0, maxItems);
345
348
 
346
349
  for (const txn of toShow) {
347
- if ('payee' in txn) {
348
- // Bank transaction
349
- lines.push(formatBankTransactionLine(txn));
350
+ if ('cleared' in txn) {
351
+ // YNAB transaction (normalized)
352
+ const ynabTxn = txn as FormattableYnabTransaction;
353
+ const payee = ynabTxn.payee_name ?? ynabTxn.payee ?? 'Unknown';
354
+ lines.push(
355
+ ` ${ynabTxn.date} - ${payee.substring(0, 40).padEnd(40)} ${formatAmount(ynabTxn.amount)}`,
356
+ );
350
357
  } else {
351
- // YNAB transaction
352
- const amount = txn.amount / 1000; // Convert milliunits to dollars
353
- const payee = txn.payee_name ?? 'Unknown';
354
- lines.push(` ${txn.date} - ${payee.substring(0, 40).padEnd(40)} ${formatAmount(amount)}`);
358
+ // Bank transaction
359
+ lines.push(formatBankTransactionLine(txn as BankTransaction));
355
360
  }
356
361
  }
357
362
 
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Smart sign detection for bank transaction imports
3
+ *
4
+ * Analyzes a sample of bank and YNAB transactions to determine if
5
+ * bank amounts need to be inverted to match YNAB's sign convention.
6
+ */
7
+
8
+ import type { BankTransaction, NormalizedYNABTransaction } from '../../types/reconciliation.js';
9
+
10
+ interface SignMatch {
11
+ bankAmount: number;
12
+ ynabAmount: number;
13
+ oppositeSign: boolean;
14
+ }
15
+
16
+ /**
17
+ * Detects whether bank transaction amounts need to be inverted
18
+ * to match YNAB's sign convention.
19
+ *
20
+ * Algorithm:
21
+ * 1. Find matching transactions based on date/amount proximity
22
+ * 2. For each match, check if signs are opposite
23
+ * 3. If >50% of matches have opposite signs, return true (needs inversion)
24
+ *
25
+ * @param bankTransactions - Raw bank transactions from CSV
26
+ * @param ynabTransactions - Normalized YNAB transactions
27
+ * @returns true if bank amounts should be inverted, false otherwise
28
+ */
29
+ export function detectSignInversion(
30
+ bankTransactions: BankTransaction[],
31
+ ynabTransactions: NormalizedYNABTransaction[],
32
+ ): boolean {
33
+ // Edge cases: empty lists
34
+ if (bankTransactions.length === 0 || ynabTransactions.length === 0) {
35
+ return false; // Conservative default: don't invert
36
+ }
37
+
38
+ // Sample up to 20 transactions for performance
39
+ const sampleSize = Math.min(20, bankTransactions.length);
40
+ const sample = bankTransactions.slice(0, sampleSize);
41
+
42
+ const matches: SignMatch[] = [];
43
+
44
+ // Try to find matches for each bank transaction
45
+ for (const bankTxn of sample) {
46
+ const match = findClosestMatch(bankTxn, ynabTransactions);
47
+ if (match) {
48
+ matches.push(match);
49
+ }
50
+ }
51
+
52
+ // Need at least 1 match to make a determination
53
+ if (matches.length === 0) {
54
+ return false; // Conservative default: don't invert
55
+ }
56
+
57
+ // Count how many matches have opposite signs
58
+ const oppositeSignCount = matches.filter((m) => m.oppositeSign).length;
59
+ const oppositeSignRatio = oppositeSignCount / matches.length;
60
+
61
+ // If more than 50% have opposite signs, inversion is needed
62
+ return oppositeSignRatio > 0.5;
63
+ }
64
+
65
+ /**
66
+ * Find the closest matching YNAB transaction for a bank transaction
67
+ */
68
+ function findClosestMatch(
69
+ bankTxn: BankTransaction,
70
+ ynabTransactions: NormalizedYNABTransaction[],
71
+ ): SignMatch | null {
72
+ const bankDate = new Date(bankTxn.date);
73
+ const bankAbsAmount = Math.abs(bankTxn.amount);
74
+
75
+ let bestMatch: SignMatch | null = null;
76
+ let bestScore = 0;
77
+
78
+ for (const ynabTxn of ynabTransactions) {
79
+ const ynabDate = new Date(ynabTxn.date);
80
+ const ynabAbsAmount = Math.abs(ynabTxn.amount);
81
+
82
+ // Check if amounts match (within tolerance)
83
+ const amountDiff = Math.abs(bankAbsAmount - ynabAbsAmount);
84
+ const amountTolerance = 100; // 10 cents in milliunits
85
+ if (amountDiff > amountTolerance) {
86
+ continue; // Amounts too different
87
+ }
88
+
89
+ // Check date proximity
90
+ const daysDiff = Math.abs(bankDate.getTime() - ynabDate.getTime()) / (1000 * 60 * 60 * 24);
91
+ if (daysDiff > 7) {
92
+ continue; // Dates too far apart
93
+ }
94
+
95
+ // Calculate match score (closer = higher score)
96
+ const amountScore = amountDiff === 0 ? 100 : Math.max(0, 100 - amountDiff / 10);
97
+ const dateScore = daysDiff === 0 ? 100 : Math.max(0, 100 - daysDiff * 10);
98
+ const score = amountScore * 0.7 + dateScore * 0.3;
99
+
100
+ if (score > bestScore) {
101
+ bestScore = score;
102
+
103
+ // Check if signs are opposite
104
+ const bankSign = Math.sign(bankTxn.amount);
105
+ const ynabSign = Math.sign(ynabTxn.amount);
106
+ const oppositeSign = bankSign !== 0 && ynabSign !== 0 && bankSign !== ynabSign;
107
+
108
+ bestMatch = {
109
+ bankAmount: bankTxn.amount,
110
+ ynabAmount: ynabTxn.amount,
111
+ oppositeSign,
112
+ };
113
+ }
114
+ }
115
+
116
+ return bestMatch;
117
+ }
@@ -1,48 +1,28 @@
1
1
  /**
2
2
  * Type definitions for the reconciliation tool
3
3
  * Based on the 2025-10-31 reconciliation redesign specification
4
+ *
5
+ * IMPORTANT UNIT CONVENTION:
6
+ * BankTransaction.amount is in MILLIUNITS (integers) in V2 architecture (formerly dollars).
7
+ * YNABTransaction.amount is in MILLIUNITS (integers).
8
+ * All internal calculations use milliunits to avoid floating-point errors.
4
9
  */
5
10
 
6
11
  import type { MoneyValue } from '../../utils/money.js';
12
+ import type {
13
+ BankTransaction as CanonicalBankTransaction,
14
+ NormalizedYNABTransaction as CanonicalYNABTransaction,
15
+ } from '../../types/reconciliation.js';
16
+
17
+ // Re-export canonical types as the standard types
18
+ export type BankTransaction = CanonicalBankTransaction;
19
+ export type YNABTransaction = CanonicalYNABTransaction;
7
20
 
8
21
  /**
9
22
  * Matching confidence levels
10
23
  */
11
24
  export type MatchConfidence = 'high' | 'medium' | 'low' | 'none';
12
25
 
13
- /**
14
- * Bank transaction parsed from CSV
15
- */
16
- export interface BankTransaction {
17
- /** Generated UUID for tracking */
18
- id: string;
19
- /** Transaction date in YYYY-MM-DD format */
20
- date: string;
21
- /** Amount in dollars */
22
- amount: number;
23
- /** Payee/merchant name */
24
- payee: string;
25
- /** Optional memo/description */
26
- memo?: string;
27
- /** Original CSV row number for debugging */
28
- original_csv_row: number;
29
- }
30
-
31
- /**
32
- * YNAB transaction (simplified from API)
33
- */
34
- export interface YNABTransaction {
35
- id: string;
36
- date: string;
37
- /** Amount in milliunits */
38
- amount: number;
39
- payee_name: string | null;
40
- category_name: string | null;
41
- cleared: 'cleared' | 'uncleared' | 'reconciled';
42
- approved: boolean;
43
- memo?: string | null;
44
- }
45
-
46
26
  /**
47
27
  * Match candidate with confidence score
48
28
  */
@@ -57,21 +37,21 @@ export interface MatchCandidate {
57
37
  * Transaction match result
58
38
  */
59
39
  export interface TransactionMatch {
60
- bank_transaction: BankTransaction;
40
+ bankTransaction: BankTransaction;
61
41
  /** Best matched YNAB transaction (if any) */
62
- ynab_transaction?: YNABTransaction;
42
+ ynabTransaction?: YNABTransaction;
63
43
  /** Alternative candidates for suggested matches */
64
44
  candidates?: MatchCandidate[];
65
45
  /** Confidence level */
66
46
  confidence: MatchConfidence;
67
47
  /** Confidence score 0-100 */
68
- confidence_score: number;
48
+ confidenceScore: number;
69
49
  /** Reason for the match */
70
- match_reason: string;
50
+ matchReason: string;
71
51
  /** Top confidence from candidates */
72
- top_confidence?: number;
52
+ topConfidence?: number;
73
53
  /** Action hint for user */
74
- action_hint?: string;
54
+ actionHint?: string;
75
55
  /** Recommendation text */
76
56
  recommendation?: string;
77
57
  }
@@ -163,31 +143,29 @@ export interface ReconciliationAction {
163
143
  }
164
144
 
165
145
  /**
166
- * Matching algorithm configuration
146
+ * Matching algorithm configuration (V2)
167
147
  */
168
148
  export interface MatchingConfig {
169
- /** Date tolerance in days */
170
- dateToleranceDays: number;
171
- /** Amount tolerance in cents */
172
- amountToleranceCents: number;
173
- /** Description similarity threshold (0-1) */
174
- descriptionSimilarityThreshold: number;
175
- /** Confidence threshold for auto-matching (0-100) */
176
- autoMatchThreshold: number;
177
- /** Confidence threshold for suggestions (0-100) */
178
- suggestionThreshold: number;
179
- }
149
+ weights: {
150
+ amount: number; // Recommended: 0.50
151
+ date: number; // Recommended: 0.15
152
+ payee: number; // Recommended: 0.35
153
+ };
180
154
 
181
- /**
182
- * Default matching configuration (not type-only for use in code)
183
- */
184
- export const DEFAULT_MATCHING_CONFIG = {
185
- dateToleranceDays: 2,
186
- amountToleranceCents: 1,
187
- descriptionSimilarityThreshold: 0.8,
188
- autoMatchThreshold: 90,
189
- suggestionThreshold: 60,
190
- };
155
+ // Tolerances (in MILLIUNITS for amount)
156
+ amountToleranceMilliunits: number; // Default: 10 (1 cent)
157
+ dateToleranceDays: number; // Default: 7
158
+
159
+ // Thresholds
160
+ autoMatchThreshold: number; // Default: 85
161
+ suggestedMatchThreshold: number; // Default: 60
162
+ minimumCandidateScore: number; // Default: 40
163
+
164
+ // Bonuses for perfect matches
165
+ exactAmountBonus: number; // Default: 10
166
+ exactDateBonus: number; // Default: 5
167
+ exactPayeeBonus: number; // Default: 10
168
+ }
191
169
 
192
170
  /**
193
171
  * Parsed CSV data from compareTransactions
@@ -0,0 +1,33 @@
1
+ import type * as ynab from 'ynab';
2
+ import type { NormalizedYNABTransaction } from '../../types/reconciliation.js';
3
+
4
+ /**
5
+ * Convert YNAB SDK transaction to normalized format for matching.
6
+ *
7
+ * This adapter keeps the YNAB SDK dependency isolated from the
8
+ * reconciliation core logic.
9
+ *
10
+ * NOTE: Amount stays in milliunits - no conversion needed since
11
+ * YNAB API already uses milliunits natively.
12
+ */
13
+ export function normalizeYNABTransaction(txn: ynab.TransactionDetail): NormalizedYNABTransaction {
14
+ return {
15
+ id: txn.id,
16
+ date: txn.date,
17
+ amount: txn.amount, // Already in milliunits - no conversion!
18
+ payee: txn.payee_name ?? null,
19
+ memo: txn.memo ?? null,
20
+ categoryName: txn.category_name ?? null,
21
+ cleared: txn.cleared,
22
+ approved: txn.approved,
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Batch convert YNAB transactions.
28
+ */
29
+ export function normalizeYNABTransactions(
30
+ txns: ynab.TransactionDetail[],
31
+ ): NormalizedYNABTransaction[] {
32
+ return txns.map(normalizeYNABTransaction);
33
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Canonical bank transaction type used throughout reconciliation.
3
+ *
4
+ * AMOUNTS ARE IN MILLIUNITS (integers, 1000 = $1.00).
5
+ * This matches YNAB's native format and allows exact integer comparison.
6
+ */
7
+ export interface BankTransaction {
8
+ /** UUID generated for tracking */
9
+ id: string;
10
+ /** ISO date string YYYY-MM-DD */
11
+ date: string;
12
+ /** Amount in MILLIUNITS (negative = outflow, positive = inflow) */
13
+ amount: number;
14
+ /** Merchant/payee name from CSV */
15
+ payee: string;
16
+ /** Optional memo/description */
17
+ memo?: string;
18
+ /** Original CSV row number (1-indexed, after header) */
19
+ sourceRow: number;
20
+ /** Raw values from CSV for debugging */
21
+ raw: {
22
+ date: string;
23
+ amount: string;
24
+ description: string;
25
+ };
26
+ /** Parser warnings (e.g., ambiguous debit/credit) */
27
+ warnings?: string[];
28
+ }
29
+
30
+ /**
31
+ * YNAB transaction normalized for comparison.
32
+ *
33
+ * This interface is intentionally SDK-agnostic. Use the adapter
34
+ * function in ynabAdapter.ts to convert from ynab.TransactionDetail.
35
+ *
36
+ * AMOUNTS ARE IN MILLIUNITS - same as YNAB API native format.
37
+ * No conversion needed from the SDK.
38
+ */
39
+ export interface NormalizedYNABTransaction {
40
+ id: string;
41
+ date: string; // YYYY-MM-DD
42
+ /** Amount in MILLIUNITS (same as YNAB API) */
43
+ amount: number;
44
+ payee: string | null;
45
+ memo: string | null;
46
+ categoryName: string | null;
47
+ cleared: 'cleared' | 'uncleared' | 'reconciled';
48
+ approved: boolean;
49
+ }