@dizzlkheinz/ynab-mcpb 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
  2. package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
  3. package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
  4. package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
  5. package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
  6. package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
  7. package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
  8. package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
  9. package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
  10. package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
  11. package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
  12. package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
  13. package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
  14. package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
  15. package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
  16. package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
  17. package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
  18. package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
  19. package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
  20. package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
  21. package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
  22. package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
  23. package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
  24. package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
  25. package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
  26. package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
  27. package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
  28. package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
  29. package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
  30. package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
  31. package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
  32. package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
  33. package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
  34. package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
  35. package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
  36. package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
  37. package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
  38. package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
  39. package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
  40. package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
  41. package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
  42. package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
  43. package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
  44. package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
  45. package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
  46. package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
  47. package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
  48. package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
  49. package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
  50. package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
  51. package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
  52. package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
  53. package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
  54. package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
  55. package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
  56. package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
  57. package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
  58. package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
  59. package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
  60. package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
  61. package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
  62. package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
  63. package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
  64. package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
  65. package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
  66. package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
  67. package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
  68. package/.github/workflows/publish.yml +3 -3
  69. package/.github/workflows/release.yml +4 -0
  70. package/CHANGELOG.md +75 -0
  71. package/NUL +1 -0
  72. package/dist/bundle/index.cjs +65 -42
  73. package/dist/server/errorHandler.d.ts +2 -0
  74. package/dist/server/errorHandler.js +49 -5
  75. package/dist/tools/reconcileAdapter.js +10 -5
  76. package/dist/tools/reconciliation/analyzer.d.ts +4 -2
  77. package/dist/tools/reconciliation/analyzer.js +120 -404
  78. package/dist/tools/reconciliation/csvParser.d.ts +51 -0
  79. package/dist/tools/reconciliation/csvParser.js +413 -0
  80. package/dist/tools/reconciliation/executor.d.ts +8 -0
  81. package/dist/tools/reconciliation/executor.js +204 -58
  82. package/dist/tools/reconciliation/index.d.ts +7 -7
  83. package/dist/tools/reconciliation/index.js +115 -39
  84. package/dist/tools/reconciliation/matcher.d.ts +24 -3
  85. package/dist/tools/reconciliation/matcher.js +175 -133
  86. package/dist/tools/reconciliation/recommendationEngine.js +22 -18
  87. package/dist/tools/reconciliation/reportFormatter.js +9 -8
  88. package/dist/tools/reconciliation/signDetector.d.ts +2 -0
  89. package/dist/tools/reconciliation/signDetector.js +54 -0
  90. package/dist/tools/reconciliation/types.d.ts +20 -34
  91. package/dist/tools/reconciliation/types.js +1 -7
  92. package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
  93. package/dist/tools/reconciliation/ynabAdapter.js +15 -0
  94. package/dist/types/reconciliation.d.ts +24 -0
  95. package/dist/types/reconciliation.js +1 -0
  96. package/docs/guides/ARCHITECTURE.md +12 -129
  97. package/docs/plans/2025-11-21-v014-hardening.md +153 -0
  98. package/docs/plans/reconciliation-v2-redesign.md +1571 -0
  99. package/package.json +6 -1
  100. package/scripts/test-recommendations.ts +1 -1
  101. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
  102. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
  103. package/src/server/errorHandler.ts +52 -5
  104. package/src/tools/reconcileAdapter.ts +10 -5
  105. package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
  106. package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
  107. package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
  108. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +1 -1
  109. package/src/tools/reconciliation/__tests__/executor.test.ts +88 -61
  110. package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
  111. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
  112. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
  113. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
  114. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
  115. package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
  116. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
  117. package/src/tools/reconciliation/analyzer.ts +174 -545
  118. package/src/tools/reconciliation/csvParser.ts +617 -0
  119. package/src/tools/reconciliation/executor.ts +249 -66
  120. package/src/tools/reconciliation/index.ts +141 -48
  121. package/src/tools/reconciliation/matcher.ts +234 -214
  122. package/src/tools/reconciliation/recommendationEngine.ts +23 -19
  123. package/src/tools/reconciliation/reportFormatter.ts +16 -11
  124. package/src/tools/reconciliation/signDetector.ts +117 -0
  125. package/src/tools/reconciliation/types.ts +39 -61
  126. package/src/tools/reconciliation/ynabAdapter.ts +33 -0
  127. package/src/types/reconciliation.ts +49 -0
  128. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
  129. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
  130. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
  131. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
  132. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
  133. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
  134. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
  135. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
  136. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
  137. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
  138. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
  139. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
  140. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
  141. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
  142. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
  143. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
  144. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
  145. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
  146. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
  147. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
  148. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
  149. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
  150. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
  151. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
  152. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
  153. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
  154. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
  155. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
  156. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
  157. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
  158. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
  159. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
  160. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
  161. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
  162. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
  163. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
  164. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
  165. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
  166. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
  167. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
  168. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
  169. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
  170. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
  171. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
  172. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
  173. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
  174. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
  175. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
  176. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
  177. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
  178. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
  179. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
  180. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
  181. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
  182. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
  183. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
  184. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
  185. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
  186. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
  187. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
  188. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
  189. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
  190. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
  191. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
  192. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
  193. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
  194. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
  195. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
  196. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
  197. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
  198. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
  199. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
  200. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
  201. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
  202. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
  203. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
  204. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
  205. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
  206. package/.gemini/settings.json +0 -8
  207. package/WARP.md +0 -245
@@ -1,269 +1,289 @@
1
1
  /**
2
2
  * Transaction matching algorithm for reconciliation
3
- * Implements confidence-based matching with auto-match and suggestion tiers
3
+ *
4
+ * V2 matcher works natively in milliunits using canonical BankTransaction
5
+ * and NormalizedYNABTransaction types.
4
6
  */
5
7
 
6
- import { normalizedMatch, payeeSimilarity } from './payeeNormalizer.js';
7
- import { DEFAULT_MATCHING_CONFIG } from './types.js';
8
+ import * as fuzz from 'fuzzball';
8
9
  import type {
9
- BankTransaction,
10
- YNABTransaction,
11
- TransactionMatch,
12
- MatchCandidate,
13
- MatchingConfig,
14
- } from './types.js';
15
-
16
- /**
17
- * Check if two amounts match within tolerance
18
- */
19
- function amountsMatch(bankAmount: number, ynabAmount: number, toleranceCents: number): boolean {
20
- // Convert YNAB milliunits to dollars
21
- const ynabDollars = ynabAmount / 1000;
22
-
23
- // Round to avoid floating point precision issues
24
- const difference = Math.round(Math.abs(bankAmount - ynabDollars) * 100) / 100;
25
- const toleranceDollars = toleranceCents / 100;
26
-
27
- return difference <= toleranceDollars;
10
+ BankTransaction as CanonicalBankTransaction,
11
+ NormalizedYNABTransaction,
12
+ } from '../../types/reconciliation.js';
13
+ import { type MatchingConfig } from './types.js';
14
+
15
+ export type { MatchingConfig };
16
+
17
+ export interface MatchCandidate {
18
+ ynabTransaction: NormalizedYNABTransaction;
19
+ scores: {
20
+ amount: number; // 0-100
21
+ date: number; // 0-100
22
+ payee: number; // 0-100
23
+ combined: number; // Weighted combination
24
+ };
25
+ matchReasons: string[];
28
26
  }
29
27
 
30
- /**
31
- * Check if two dates match within tolerance
32
- */
33
- function datesMatch(date1: string, date2: string, toleranceDays: number): boolean {
34
- const d1 = new Date(date1);
35
- const d2 = new Date(date2);
36
-
37
- const diffMs = Math.abs(d1.getTime() - d2.getTime());
38
- const diffDays = diffMs / (1000 * 60 * 60 * 24);
39
-
40
- return diffDays <= toleranceDays;
28
+ export interface MatchResult {
29
+ bankTransaction: CanonicalBankTransaction;
30
+ bestMatch: MatchCandidate | null;
31
+ candidates: MatchCandidate[]; // Top 3
32
+ confidence: 'high' | 'medium' | 'low' | 'none';
33
+ confidenceScore: number;
41
34
  }
42
35
 
43
- /**
44
- * Calculate match confidence score between bank and YNAB transaction
45
- * Returns score 0-100 and match reasons
46
- */
47
- function calculateMatchScore(
48
- bankTxn: BankTransaction,
49
- ynabTxn: YNABTransaction,
50
- config: MatchingConfig,
51
- ): { score: number; reasons: string[] } {
52
- const reasons: string[] = [];
53
- let score = 0;
54
-
55
- // Amount match (40% weight) - REQUIRED
56
- const amountMatch = amountsMatch(bankTxn.amount, ynabTxn.amount, config.amountToleranceCents);
57
- if (!amountMatch) {
58
- return { score: 0, reasons: ['Amount does not match'] };
59
- }
60
- score += 40;
61
- reasons.push('Amount matches');
62
-
63
- // Date match (40% weight)
64
- const dateWithinTolerance = datesMatch(bankTxn.date, ynabTxn.date, config.dateToleranceDays);
65
- if (dateWithinTolerance) {
66
- score += 40;
67
- const daysDiff = Math.abs(
68
- (new Date(bankTxn.date).getTime() - new Date(ynabTxn.date).getTime()) / (1000 * 60 * 60 * 24),
69
- );
70
- if (daysDiff === 0) {
71
- reasons.push('Exact date match');
72
- } else {
73
- reasons.push(`Date within ${Math.round(daysDiff)} days`);
74
- }
36
+ export const DEFAULT_CONFIG: MatchingConfig = {
37
+ weights: {
38
+ amount: 0.5,
39
+ date: 0.15,
40
+ payee: 0.35,
41
+ },
42
+ amountToleranceMilliunits: 10, // 1 cent
43
+ dateToleranceDays: 7,
44
+ autoMatchThreshold: 85,
45
+ suggestedMatchThreshold: 60,
46
+ minimumCandidateScore: 40,
47
+ exactAmountBonus: 10,
48
+ exactDateBonus: 5,
49
+ exactPayeeBonus: 10,
50
+ };
51
+
52
+ export function normalizeConfig(config?: MatchingConfig): MatchingConfig {
53
+ if (!config) {
54
+ return { ...DEFAULT_CONFIG };
75
55
  }
76
56
 
77
- // Payee match (20% weight)
78
- const payeeScore = payeeSimilarity(bankTxn.payee, ynabTxn.payee_name);
57
+ return {
58
+ weights: config.weights ?? DEFAULT_CONFIG.weights,
59
+ amountToleranceMilliunits:
60
+ config.amountToleranceMilliunits ?? DEFAULT_CONFIG.amountToleranceMilliunits,
61
+ dateToleranceDays: config.dateToleranceDays ?? DEFAULT_CONFIG.dateToleranceDays,
62
+ autoMatchThreshold: config.autoMatchThreshold ?? DEFAULT_CONFIG.autoMatchThreshold,
63
+ suggestedMatchThreshold:
64
+ config.suggestedMatchThreshold ?? DEFAULT_CONFIG.suggestedMatchThreshold,
65
+ minimumCandidateScore: config.minimumCandidateScore ?? DEFAULT_CONFIG.minimumCandidateScore,
66
+ exactAmountBonus: config.exactAmountBonus ?? DEFAULT_CONFIG.exactAmountBonus,
67
+ exactDateBonus: config.exactDateBonus ?? DEFAULT_CONFIG.exactDateBonus,
68
+ exactPayeeBonus: config.exactPayeeBonus ?? DEFAULT_CONFIG.exactPayeeBonus,
69
+ };
70
+ }
79
71
 
80
- if (normalizedMatch(bankTxn.payee, ynabTxn.payee_name)) {
81
- score += 20;
82
- reasons.push('Payee exact match');
83
- } else if (payeeScore >= 95) {
84
- score += 15;
85
- reasons.push(`Payee highly similar (${Math.round(payeeScore)}%)`);
86
- } else if (payeeScore >= 80) {
87
- score += 10;
88
- reasons.push(`Payee similar (${Math.round(payeeScore)}%)`);
89
- } else if (payeeScore >= 60) {
90
- score += 6;
91
- reasons.push(`Payee somewhat similar (${Math.round(payeeScore)}%)`);
72
+ function matchSingle(
73
+ bankTxn: CanonicalBankTransaction,
74
+ ynabTransactions: NormalizedYNABTransaction[],
75
+ usedIds: Set<string>,
76
+ configInput: MatchingConfig | undefined,
77
+ ): MatchResult {
78
+ const config = normalizeConfig(configInput);
79
+
80
+ const candidates = findCandidates(bankTxn, ynabTransactions, usedIds, config);
81
+
82
+ const bestMatch = candidates.length > 0 ? candidates[0]! : null;
83
+ const confidenceScore = bestMatch?.scores.combined ?? 0;
84
+
85
+ let confidence: MatchResult['confidence'];
86
+ if (confidenceScore >= config.autoMatchThreshold) {
87
+ confidence = 'high';
88
+ if (bestMatch) usedIds.add(bestMatch.ynabTransaction.id);
89
+ } else if (confidenceScore >= config.suggestedMatchThreshold) {
90
+ confidence = 'medium';
91
+ } else if (confidenceScore >= config.minimumCandidateScore) {
92
+ confidence = 'low';
93
+ } else {
94
+ confidence = 'none';
92
95
  }
93
96
 
94
- return { score: Math.round(score), reasons };
97
+ return {
98
+ bankTransaction: bankTxn,
99
+ bestMatch,
100
+ candidates: candidates.slice(0, 3),
101
+ confidence,
102
+ confidenceScore,
103
+ };
95
104
  }
96
105
 
97
- /**
98
- * Priority scoring for YNAB transactions
99
- * Uncleared transactions get higher priority than cleared ones
100
- */
101
- function getPriority(ynabTxn: YNABTransaction): number {
102
- // Uncleared transactions are expecting bank confirmation
103
- if (ynabTxn.cleared === 'uncleared') return 10;
104
- if (ynabTxn.cleared === 'cleared') return 5;
105
- if (ynabTxn.cleared === 'reconciled') return 1;
106
- return 0;
106
+ export function findMatches(
107
+ bankTransactions: CanonicalBankTransaction[],
108
+ ynabTransactions: NormalizedYNABTransaction[],
109
+ config?: MatchingConfig,
110
+ ): MatchResult[] {
111
+ const usedYnabIds = new Set<string>();
112
+ const results: MatchResult[] = [];
113
+
114
+ for (const bankTxn of bankTransactions) {
115
+ results.push(matchSingle(bankTxn, ynabTransactions, usedYnabIds, config));
116
+ }
117
+
118
+ return results;
107
119
  }
108
120
 
109
- /**
110
- * Find all matching candidates for a bank transaction
111
- */
112
- function findMatchCandidates(
113
- bankTxn: BankTransaction,
114
- ynabTransactions: YNABTransaction[],
121
+ function findCandidates(
122
+ bankTxn: CanonicalBankTransaction,
123
+ ynabTransactions: NormalizedYNABTransaction[],
115
124
  usedIds: Set<string>,
116
125
  config: MatchingConfig,
117
126
  ): MatchCandidate[] {
118
127
  const candidates: MatchCandidate[] = [];
119
128
 
120
129
  for (const ynabTxn of ynabTransactions) {
121
- // Skip already matched transactions
122
130
  if (usedIds.has(ynabTxn.id)) continue;
123
131
 
124
- // Skip opposite-signed transactions (refunds vs purchases)
125
- if (bankTxn.amount > 0 !== ynabTxn.amount > 0) continue;
132
+ // Sign check - both must be same sign (or both zero)
133
+ const bankSign = Math.sign(bankTxn.amount);
134
+ const ynabSign = Math.sign(ynabTxn.amount);
135
+ if (bankSign !== ynabSign && bankSign !== 0 && ynabSign !== 0) {
136
+ continue;
137
+ }
138
+
139
+ const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
140
+ if (amountDiff > config.amountToleranceMilliunits) {
141
+ // Outside configured amount tolerance - treat as no candidate
142
+ continue;
143
+ }
126
144
 
127
- // Calculate match score
128
- const { score, reasons } = calculateMatchScore(bankTxn, ynabTxn, config);
145
+ const scores = calculateScores(bankTxn, ynabTxn, config);
129
146
 
130
- // Only include candidates with minimum score
131
- if (score >= 30) {
147
+ if (scores.combined >= config.minimumCandidateScore) {
132
148
  candidates.push({
133
- ynab_transaction: ynabTxn,
134
- confidence: score,
135
- match_reason: reasons.join(', '),
136
- explanation: buildExplanation(bankTxn, ynabTxn, score, reasons),
149
+ ynabTransaction: ynabTxn,
150
+ scores,
151
+ matchReasons: buildMatchReasons(scores, config),
137
152
  });
138
153
  }
139
154
  }
140
155
 
141
- // Sort by confidence (desc), then priority (desc), then date proximity
142
156
  candidates.sort((a, b) => {
143
- if (b.confidence !== a.confidence) {
144
- return b.confidence - a.confidence;
157
+ const scoreDiff = b.scores.combined - a.scores.combined;
158
+ if (scoreDiff !== 0) {
159
+ return scoreDiff;
145
160
  }
146
- const priorityDiff = getPriority(b.ynab_transaction) - getPriority(a.ynab_transaction);
147
- if (priorityDiff !== 0) return priorityDiff;
148
-
149
- // Date proximity as tiebreaker
150
- const dateProximityA = Math.abs(
151
- new Date(bankTxn.date).getTime() - new Date(a.ynab_transaction.date).getTime(),
152
- );
153
- const dateProximityB = Math.abs(
154
- new Date(bankTxn.date).getTime() - new Date(b.ynab_transaction.date).getTime(),
155
- );
156
- return dateProximityA - dateProximityB;
157
- });
158
161
 
159
- return candidates;
160
- }
162
+ const aUncleared = a.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
163
+ const bUncleared = b.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
164
+ if (aUncleared !== bUncleared) {
165
+ return bUncleared - aUncleared;
166
+ }
161
167
 
162
- /**
163
- * Build human-readable explanation for a match
164
- */
165
- function buildExplanation(
166
- _bankTxn: BankTransaction,
167
- ynabTxn: YNABTransaction,
168
- score: number,
169
- reasons: string[],
170
- ): string {
171
- const parts: string[] = [];
172
-
173
- parts.push(`Match confidence: ${score}%`);
174
- parts.push(reasons.join(', '));
175
-
176
- if (ynabTxn.cleared === 'uncleared') {
177
- parts.push('(Uncleared - awaiting confirmation)');
178
- }
168
+ const bankTime = new Date(bankTxn.date).getTime();
169
+ const aDiff = Math.abs(bankTime - new Date(a.ynabTransaction.date).getTime());
170
+ const bDiff = Math.abs(bankTime - new Date(b.ynabTransaction.date).getTime());
171
+ if (aDiff !== bDiff) {
172
+ return aDiff - bDiff;
173
+ }
179
174
 
180
- return parts.join(' | ');
175
+ return 0;
176
+ });
177
+ return candidates;
181
178
  }
182
179
 
183
- /**
184
- * Find best match for a single bank transaction
185
- */
186
- export function findBestMatch(
187
- bankTxn: BankTransaction,
188
- ynabTransactions: YNABTransaction[],
189
- usedIds: Set<string>,
180
+ function calculateScores(
181
+ bankTxn: CanonicalBankTransaction,
182
+ ynabTxn: NormalizedYNABTransaction,
190
183
  config: MatchingConfig,
191
- ): TransactionMatch {
192
- const candidates = findMatchCandidates(bankTxn, ynabTransactions, usedIds, config);
193
-
194
- if (candidates.length === 0) {
195
- // No match found
196
- return {
197
- bank_transaction: bankTxn,
198
- confidence: 'none',
199
- confidence_score: 0,
200
- match_reason: 'No matching transaction found in YNAB',
201
- action_hint: 'add_to_ynab',
202
- recommendation: 'This transaction appears on bank statement but not in YNAB',
203
- };
184
+ ): MatchCandidate['scores'] {
185
+ // Amount score - now using INTEGER comparison (milliunits)
186
+ const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
187
+ let amountScore: number;
188
+
189
+ if (amountDiff === 0) {
190
+ // Exact integer match - no floating point issues!
191
+ amountScore = 100;
192
+ } else if (amountDiff <= config.amountToleranceMilliunits) {
193
+ amountScore = 95;
194
+ } else if (amountDiff <= 1000) {
195
+ // Within $1
196
+ amountScore = 80 - (amountDiff / 1000) * 20;
197
+ } else {
198
+ amountScore = Math.max(0, 60 - (amountDiff / 1000) * 5);
204
199
  }
205
200
 
206
- const bestCandidate = candidates[0]!; // Safe: we checked candidates.length > 0
207
- const bestScore = bestCandidate.confidence;
208
-
209
- // HIGH confidence: Auto-match candidate (≥90%)
210
- if (bestScore >= config.autoMatchThreshold) {
211
- return {
212
- bank_transaction: bankTxn,
213
- ynab_transaction: bestCandidate.ynab_transaction,
214
- confidence: 'high',
215
- confidence_score: bestScore,
216
- match_reason: bestCandidate.match_reason,
217
- };
201
+ // Date score
202
+ const bankDate = new Date(bankTxn.date);
203
+ const ynabDate = new Date(ynabTxn.date);
204
+ const daysDiff = Math.abs(bankDate.getTime() - ynabDate.getTime()) / (1000 * 60 * 60 * 24);
205
+ let dateScore: number;
206
+
207
+ if (daysDiff < 0.5) {
208
+ dateScore = 100;
209
+ } else if (daysDiff <= 1) {
210
+ dateScore = 95;
211
+ } else if (daysDiff <= config.dateToleranceDays) {
212
+ dateScore = 90 - (daysDiff - 1) * (40 / config.dateToleranceDays);
213
+ } else {
214
+ dateScore = Math.max(0, 50 - (daysDiff - config.dateToleranceDays) * 5);
218
215
  }
219
216
 
220
- // MEDIUM confidence: Suggested match (60-89%)
221
- if (bestScore >= config.suggestionThreshold) {
222
- return {
223
- bank_transaction: bankTxn,
224
- ynab_transaction: bestCandidate.ynab_transaction,
225
- candidates: candidates.slice(0, 3), // Top 3 candidates
226
- confidence: 'medium',
227
- confidence_score: bestScore,
228
- match_reason: bestCandidate.match_reason,
229
- top_confidence: bestScore,
230
- action_hint: 'review_and_choose',
231
- };
232
- }
217
+ // Payee score using fuzzball
218
+ const payeeScore = calculatePayeeScore(bankTxn.payee, ynabTxn.payee);
219
+
220
+ // Combined score with weights
221
+ let combined =
222
+ amountScore * config.weights.amount +
223
+ dateScore * config.weights.date +
224
+ payeeScore * config.weights.payee;
225
+
226
+ // Apply bonuses
227
+ if (amountScore === 100) combined += config.exactAmountBonus;
228
+ if (dateScore === 100) combined += config.exactDateBonus;
229
+ if (payeeScore >= 95) combined += config.exactPayeeBonus;
230
+
231
+ combined = Math.min(100, combined);
233
232
 
234
- // LOW confidence: Show as possible match but don't auto-suggest (30-59%)
235
233
  return {
236
- bank_transaction: bankTxn,
237
- candidates: candidates.slice(0, 3),
238
- confidence: 'low',
239
- confidence_score: bestScore,
240
- match_reason: 'Low confidence match',
241
- top_confidence: bestScore,
242
- action_hint: 'review_or_add_new',
243
- recommendation: 'Consider reviewing candidates or adding as new transaction',
234
+ amount: Math.round(amountScore),
235
+ date: Math.round(dateScore),
236
+ payee: Math.round(payeeScore),
237
+ combined: Math.round(combined),
244
238
  };
245
239
  }
246
240
 
247
- /**
248
- * Find matches for all bank transactions
249
- */
250
- export function findMatches(
251
- bankTransactions: BankTransaction[],
252
- ynabTransactions: YNABTransaction[],
253
- config: MatchingConfig = DEFAULT_MATCHING_CONFIG as MatchingConfig,
254
- ): TransactionMatch[] {
255
- const matches: TransactionMatch[] = [];
256
- const usedIds = new Set<string>();
241
+ function calculatePayeeScore(bankPayee: string, ynabPayee: string | null): number {
242
+ if (!ynabPayee) return 30;
257
243
 
258
- for (const bankTxn of bankTransactions) {
259
- const match = findBestMatch(bankTxn, ynabTransactions, usedIds, config);
260
- matches.push(match);
244
+ const scores = [
245
+ fuzz.token_set_ratio(bankPayee, ynabPayee),
246
+ fuzz.token_sort_ratio(bankPayee, ynabPayee),
247
+ fuzz.partial_ratio(bankPayee, ynabPayee),
248
+ fuzz.WRatio(bankPayee, ynabPayee),
249
+ ];
261
250
 
262
- // Mark high-confidence matches as used to prevent duplicate matching
263
- if (match.confidence === 'high' && match.ynab_transaction) {
264
- usedIds.add(match.ynab_transaction.id);
265
- }
251
+ return Math.max(...scores);
252
+ }
253
+
254
+ function buildMatchReasons(scores: MatchCandidate['scores'], config: MatchingConfig): string[] {
255
+ const reasons: string[] = [];
256
+
257
+ if (scores.amount === 100) {
258
+ reasons.push('Exact amount match');
259
+ } else if (scores.amount >= 95) {
260
+ reasons.push('Amount within tolerance');
261
+ }
262
+
263
+ if (scores.date === 100) {
264
+ reasons.push('Same date');
265
+ } else if (scores.date >= 90) {
266
+ reasons.push('Date within 1-2 days');
267
+ } else if (scores.date >= 50) {
268
+ reasons.push(`Date within ${config.dateToleranceDays} days`);
266
269
  }
267
270
 
268
- return matches;
271
+ if (scores.payee >= 95) {
272
+ reasons.push('Payee exact match');
273
+ } else if (scores.payee >= 80) {
274
+ reasons.push('Payee highly similar');
275
+ } else if (scores.payee >= 60) {
276
+ reasons.push('Payee somewhat similar');
277
+ }
278
+
279
+ return reasons;
280
+ }
281
+
282
+ export function findBestMatch(
283
+ bankTransaction: CanonicalBankTransaction,
284
+ ynabTransactions: NormalizedYNABTransaction[],
285
+ usedYnabIds: Set<string> = new Set<string>(),
286
+ config?: MatchingConfig,
287
+ ): MatchResult {
288
+ return matchSingle(bankTransaction, ynabTransactions, usedYnabIds, config);
269
289
  }
@@ -11,7 +11,7 @@ import type {
11
11
  BankTransaction,
12
12
  YNABTransaction,
13
13
  } from './types.js';
14
- import { toMoneyValueFromDecimal, fromMilli, toMilli } from '../../utils/money.js';
14
+ import { toMoneyValue, toMoneyValueFromDecimal, fromMilli } from '../../utils/money.js';
15
15
 
16
16
  const RECOMMENDATION_VERSION = '1.0';
17
17
 
@@ -139,17 +139,17 @@ function createSuggestedMatchRecommendation(
139
139
  match: TransactionMatch,
140
140
  context: RecommendationContext,
141
141
  ): CreateTransactionRecommendation | ReviewDuplicateRecommendation | ManualReviewRecommendation {
142
- const bankTxn = match.bank_transaction;
142
+ const bankTxn = match.bankTransaction;
143
143
 
144
144
  // If there's a suggested YNAB transaction, review as possible duplicate
145
- if (match.ynab_transaction && match.confidence !== 'none') {
145
+ if (match.ynabTransaction && match.confidence !== 'none') {
146
146
  return {
147
147
  id: randomUUID(),
148
148
  action_type: 'review_duplicate',
149
149
  priority: 'high',
150
- confidence: Math.max(0, Math.min(1, match.confidence_score / 100)),
150
+ confidence: Math.max(0, Math.min(1, match.confidenceScore / 100)),
151
151
  message: `Review possible match: ${bankTxn.payee}`,
152
- reason: match.match_reason,
152
+ reason: match.matchReason,
153
153
  estimated_impact: toMoneyValueFromDecimal(
154
154
  0,
155
155
  context.analysis.balance_info.current_cleared.currency,
@@ -160,16 +160,15 @@ function createSuggestedMatchRecommendation(
160
160
  created_at: new Date().toISOString(),
161
161
  },
162
162
  parameters: {
163
- candidate_ids: [match.ynab_transaction.id],
163
+ candidate_ids: [match.ynabTransaction.id],
164
164
  bank_transaction: bankTxn,
165
- suggested_match_id: match.ynab_transaction.id,
165
+ suggested_match_id: match.ynabTransaction.id,
166
166
  },
167
167
  };
168
168
  }
169
169
 
170
170
  // Check for combination matches (multiple YNAB transactions that together match the bank transaction)
171
- const isCombinationMatch =
172
- match.match_reason === 'combination_match' || (match.candidates?.length ?? 0) > 1;
171
+ const isCombinationMatch = (match.candidates?.length ?? 0) > 1;
173
172
 
174
173
  if (isCombinationMatch) {
175
174
  return createCombinationReviewRecommendation(match, context);
@@ -179,7 +178,7 @@ function createSuggestedMatchRecommendation(
179
178
  const parameters: CreateTransactionRecommendation['parameters'] = {
180
179
  account_id: context.account_id,
181
180
  date: bankTxn.date,
182
- amount: toMilli(bankTxn.amount), // Convert dollars to milliunits for create_transaction
181
+ amount: bankTxn.amount, // Amount is already milliunits
183
182
  payee_name: bankTxn.payee,
184
183
  cleared: 'cleared',
185
184
  approved: true,
@@ -196,7 +195,7 @@ function createSuggestedMatchRecommendation(
196
195
  confidence: CONFIDENCE.CREATE_EXACT_MATCH,
197
196
  message: `Create transaction for ${bankTxn.payee}`,
198
197
  reason: `This transaction exactly matches your discrepancy`,
199
- estimated_impact: toMoneyValueFromDecimal(
198
+ estimated_impact: toMoneyValue(
200
199
  bankTxn.amount,
201
200
  context.analysis.balance_info.current_cleared.currency,
202
201
  ),
@@ -216,7 +215,7 @@ function createCombinationReviewRecommendation(
216
215
  match: TransactionMatch,
217
216
  context: RecommendationContext,
218
217
  ): ManualReviewRecommendation {
219
- const bankTxn = match.bank_transaction;
218
+ const bankTxn = match.bankTransaction;
220
219
  const candidateIds = match.candidates?.map((candidate) => candidate.ynab_transaction.id) ?? [];
221
220
 
222
221
  // Calculate total amount from candidates for context (convert from milliunits to decimal)
@@ -247,7 +246,7 @@ function createCombinationReviewRecommendation(
247
246
  metadata: {
248
247
  version: RECOMMENDATION_VERSION,
249
248
  created_at: new Date().toISOString(),
250
- bank_transaction_amount: toMoneyValueFromDecimal(
249
+ bank_transaction_amount: toMoneyValue(
251
250
  bankTxn.amount,
252
251
  context.analysis.balance_info.current_cleared.currency,
253
252
  ),
@@ -269,8 +268,8 @@ function createCombinationReviewRecommendation(
269
268
  source: 'ynab' as const,
270
269
  id,
271
270
  description:
272
- match.candidates?.find((c) => c.ynab_transaction.id === id)?.ynab_transaction
273
- .payee_name ?? 'Unknown',
271
+ match.candidates?.find((c) => c.ynab_transaction.id === id)?.ynab_transaction.payee ??
272
+ 'Unknown',
274
273
  })),
275
274
  ],
276
275
  },
@@ -388,7 +387,12 @@ function processUnmatchedTransactions(context: RecommendationContext): Actionabl
388
387
  }
389
388
 
390
389
  // Suggested matches → review as potential duplicates or auto-match
391
- for (const match of context.analysis.suggested_matches) {
390
+ const matchesForReview = [
391
+ ...context.analysis.suggested_matches,
392
+ ...context.analysis.auto_matches,
393
+ ];
394
+
395
+ for (const match of matchesForReview) {
392
396
  recommendations.push(createSuggestedMatchRecommendation(match, context));
393
397
  }
394
398
 
@@ -420,7 +424,7 @@ function createUnmatchedBankRecommendation(
420
424
  const parameters: CreateTransactionRecommendation['parameters'] = {
421
425
  account_id: context.account_id,
422
426
  date: txn.date,
423
- amount: toMilli(txn.amount), // Convert dollars to milliunits for create_transaction
427
+ amount: txn.amount, // Amount is already milliunits
424
428
  payee_name: txn.payee,
425
429
  cleared: 'cleared',
426
430
  approved: true,
@@ -437,7 +441,7 @@ function createUnmatchedBankRecommendation(
437
441
  confidence: CONFIDENCE.UNMATCHED_BANK,
438
442
  message: `Create missing transaction: ${txn.payee}`,
439
443
  reason: 'Transaction appears on bank statement but not in YNAB',
440
- estimated_impact: toMoneyValueFromDecimal(
444
+ estimated_impact: toMoneyValue(
441
445
  txn.amount,
442
446
  context.analysis.balance_info.current_cleared.currency,
443
447
  ),
@@ -471,7 +475,7 @@ function createUpdateClearedRecommendation(
471
475
  action_type: 'update_cleared',
472
476
  priority: 'low',
473
477
  confidence: CONFIDENCE.UPDATE_CLEARED,
474
- message: `Mark transaction as cleared: ${txn.payee_name || 'Unknown'}`,
478
+ message: `Mark transaction as cleared: ${txn.payee || 'Unknown'}`,
475
479
  reason: 'Transaction exists in YNAB but not yet cleared',
476
480
  estimated_impact: toMoneyValueFromDecimal(
477
481
  0,