@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.
- package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
- package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
- package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
- package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
- package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
- package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
- package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
- package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
- package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
- package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
- package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
- package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
- package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
- package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
- package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
- package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
- package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
- package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
- package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
- package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
- package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
- package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
- package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
- package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
- package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
- package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
- package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
- package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
- package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
- package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
- package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
- package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
- package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
- package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
- package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
- package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
- package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
- package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
- package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
- package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
- package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
- package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
- package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
- package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
- package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
- package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
- package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
- package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
- package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
- package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
- package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
- package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
- package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
- package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
- package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
- package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
- package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
- package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
- package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
- package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
- package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
- package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
- package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
- package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
- package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
- package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
- package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/release.yml +4 -0
- package/CHANGELOG.md +75 -0
- package/NUL +1 -0
- package/dist/bundle/index.cjs +65 -42
- package/dist/server/errorHandler.d.ts +2 -0
- package/dist/server/errorHandler.js +49 -5
- package/dist/tools/reconcileAdapter.js +10 -5
- package/dist/tools/reconciliation/analyzer.d.ts +8 -2
- package/dist/tools/reconciliation/analyzer.js +127 -409
- package/dist/tools/reconciliation/csvParser.d.ts +51 -0
- package/dist/tools/reconciliation/csvParser.js +413 -0
- package/dist/tools/reconciliation/executor.d.ts +8 -0
- package/dist/tools/reconciliation/executor.js +204 -58
- package/dist/tools/reconciliation/index.d.ts +7 -7
- package/dist/tools/reconciliation/index.js +115 -39
- package/dist/tools/reconciliation/matcher.d.ts +24 -3
- package/dist/tools/reconciliation/matcher.js +175 -133
- package/dist/tools/reconciliation/recommendationEngine.js +22 -18
- package/dist/tools/reconciliation/reportFormatter.js +9 -8
- package/dist/tools/reconciliation/signDetector.d.ts +2 -0
- package/dist/tools/reconciliation/signDetector.js +54 -0
- package/dist/tools/reconciliation/types.d.ts +20 -34
- package/dist/tools/reconciliation/types.js +1 -7
- package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
- package/dist/tools/reconciliation/ynabAdapter.js +15 -0
- package/dist/types/reconciliation.d.ts +24 -0
- package/dist/types/reconciliation.js +1 -0
- package/docs/guides/ARCHITECTURE.md +12 -129
- package/docs/plans/2025-11-21-v014-hardening.md +153 -0
- package/docs/plans/reconciliation-v2-redesign.md +1571 -0
- package/package.json +6 -1
- package/scripts/test-recommendations.ts +1 -1
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
- package/src/server/errorHandler.ts +52 -5
- package/src/tools/reconcileAdapter.ts +10 -5
- package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +1 -1
- package/src/tools/reconciliation/__tests__/executor.test.ts +88 -61
- package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
- package/src/tools/reconciliation/analyzer.ts +191 -550
- package/src/tools/reconciliation/csvParser.ts +617 -0
- package/src/tools/reconciliation/executor.ts +249 -66
- package/src/tools/reconciliation/index.ts +148 -54
- package/src/tools/reconciliation/matcher.ts +234 -214
- package/src/tools/reconciliation/recommendationEngine.ts +23 -19
- package/src/tools/reconciliation/reportFormatter.ts +16 -11
- package/src/tools/reconciliation/signDetector.ts +117 -0
- package/src/tools/reconciliation/types.ts +39 -61
- package/src/tools/reconciliation/ynabAdapter.ts +33 -0
- package/src/types/reconciliation.ts +49 -0
- package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
- package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
- package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
- package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
- package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
- package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
- package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
- package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
- package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
- package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
- package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
- package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
- package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
- package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
- package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
- package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
- package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
- package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
- package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
- package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
- package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
- package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
- package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
- package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
- package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
- package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
- package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
- package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
- package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
- package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
- package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
- package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
- package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
- package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
- package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
- package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
- package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
- package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
- package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
- package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
- package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
- package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
- package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
- package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
- package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
- package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
- package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
- package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
- package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
- package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
- package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
- package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
- package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
- package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
- package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
- package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
- package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
- package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
- package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
- package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
- package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
- package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
- package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
- package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
- package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
- package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
- package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
- package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
- package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
- package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
- package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
- package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
- package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
- package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
- package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
- package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
- package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
- package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
- package/.gemini/settings.json +0 -8
- package/WARP.md +0 -245
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
1
2
|
import { z } from 'zod/v4';
|
|
2
3
|
import { withToolErrorHandling } from '../../types/index.js';
|
|
3
4
|
import { analyzeReconciliation } from './analyzer.js';
|
|
4
5
|
import { buildReconciliationPayload } from '../reconcileAdapter.js';
|
|
5
6
|
import { executeReconciliation, } from './executor.js';
|
|
6
7
|
import { responseFormatter } from '../../server/responseFormatter.js';
|
|
7
|
-
import {
|
|
8
|
+
import { parseCSV } from './csvParser.js';
|
|
8
9
|
import { resolveDeltaFetcherArgs } from '../deltaSupport.js';
|
|
10
|
+
import { detectSignInversion } from './signDetector.js';
|
|
11
|
+
import { normalizeYNABTransactions } from './ynabAdapter.js';
|
|
9
12
|
export { analyzeReconciliation } from './analyzer.js';
|
|
10
13
|
export { findMatches, findBestMatch } from './matcher.js';
|
|
11
14
|
export { normalizePayee, normalizedMatch, fuzzyMatch, payeeSimilarity } from './payeeNormalizer.js';
|
|
@@ -38,25 +41,17 @@ export const ReconcileAccountSchema = z
|
|
|
38
41
|
csv_data: z.string().optional(),
|
|
39
42
|
csv_format: z
|
|
40
43
|
.object({
|
|
41
|
-
date_column: z.union([z.string(), z.number()]).optional()
|
|
44
|
+
date_column: z.union([z.string(), z.number()]).optional(),
|
|
42
45
|
amount_column: z.union([z.string(), z.number()]).optional(),
|
|
43
46
|
debit_column: z.union([z.string(), z.number()]).optional(),
|
|
44
47
|
credit_column: z.union([z.string(), z.number()]).optional(),
|
|
45
|
-
description_column: z.union([z.string(), z.number()]).optional()
|
|
46
|
-
date_format: z.string().optional()
|
|
47
|
-
has_header: z.boolean().optional()
|
|
48
|
-
delimiter: z.string().optional()
|
|
48
|
+
description_column: z.union([z.string(), z.number()]).optional(),
|
|
49
|
+
date_format: z.string().optional(),
|
|
50
|
+
has_header: z.boolean().optional(),
|
|
51
|
+
delimiter: z.string().optional(),
|
|
49
52
|
})
|
|
50
53
|
.strict()
|
|
51
|
-
.optional()
|
|
52
|
-
.default(() => ({
|
|
53
|
-
date_column: 'Date',
|
|
54
|
-
amount_column: 'Amount',
|
|
55
|
-
description_column: 'Description',
|
|
56
|
-
date_format: 'MM/DD/YYYY',
|
|
57
|
-
has_header: true,
|
|
58
|
-
delimiter: ',',
|
|
59
|
-
})),
|
|
54
|
+
.optional(),
|
|
60
55
|
statement_balance: z.number({
|
|
61
56
|
message: 'Statement balance is required and must be a number',
|
|
62
57
|
}),
|
|
@@ -65,9 +60,9 @@ export const ReconcileAccountSchema = z
|
|
|
65
60
|
statement_date: z.string().optional(),
|
|
66
61
|
expected_bank_balance: z.number().optional(),
|
|
67
62
|
as_of_timezone: z.string().optional(),
|
|
68
|
-
date_tolerance_days: z.number().min(0).max(7).optional().default(
|
|
63
|
+
date_tolerance_days: z.number().min(0).max(7).optional().default(7),
|
|
69
64
|
amount_tolerance_cents: z.number().min(0).max(100).optional().default(1),
|
|
70
|
-
auto_match_threshold: z.number().min(0).max(100).optional().default(
|
|
65
|
+
auto_match_threshold: z.number().min(0).max(100).optional().default(85),
|
|
71
66
|
suggestion_threshold: z.number().min(0).max(100).optional().default(60),
|
|
72
67
|
amount_tolerance: z.number().min(0).max(1).optional(),
|
|
73
68
|
auto_create_transactions: z.boolean().optional().default(false),
|
|
@@ -95,11 +90,19 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
95
90
|
const forceFullRefresh = params.force_full_refresh ?? true;
|
|
96
91
|
return await withToolErrorHandling(async () => {
|
|
97
92
|
const config = {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
weights: {
|
|
94
|
+
amount: 0.5,
|
|
95
|
+
date: 0.15,
|
|
96
|
+
payee: 0.35,
|
|
97
|
+
},
|
|
98
|
+
dateToleranceDays: params.date_tolerance_days ?? 5,
|
|
99
|
+
amountToleranceMilliunits: (params.amount_tolerance_cents ?? 1) * 10,
|
|
100
|
+
autoMatchThreshold: params.auto_match_threshold ?? 90,
|
|
101
|
+
suggestedMatchThreshold: params.suggestion_threshold ?? 60,
|
|
102
|
+
minimumCandidateScore: 40,
|
|
103
|
+
exactAmountBonus: 10,
|
|
104
|
+
exactDateBonus: 5,
|
|
105
|
+
exactPayeeBonus: 10,
|
|
103
106
|
};
|
|
104
107
|
const accountResult = forceFullRefresh
|
|
105
108
|
? await deltaFetcher.fetchAccountsFull(params.budget_id)
|
|
@@ -125,28 +128,70 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
125
128
|
: params.statement_balance;
|
|
126
129
|
const budgetResponse = await ynabAPI.budgets.getBudgetById(params.budget_id);
|
|
127
130
|
const currencyCode = budgetResponse.data.budget?.currency_format?.iso_code ?? 'USD';
|
|
131
|
+
const dateFormat = mapCsvDateFormatToHint(params.csv_format?.date_format);
|
|
132
|
+
const csvOptions = {
|
|
133
|
+
columns: {
|
|
134
|
+
...(params.csv_format?.date_column !== undefined && {
|
|
135
|
+
date: String(params.csv_format.date_column),
|
|
136
|
+
}),
|
|
137
|
+
...(params.csv_format?.amount_column !== undefined && {
|
|
138
|
+
amount: String(params.csv_format.amount_column),
|
|
139
|
+
}),
|
|
140
|
+
...(params.csv_format?.debit_column !== undefined && {
|
|
141
|
+
debit: String(params.csv_format.debit_column),
|
|
142
|
+
}),
|
|
143
|
+
...(params.csv_format?.credit_column !== undefined && {
|
|
144
|
+
credit: String(params.csv_format.credit_column),
|
|
145
|
+
}),
|
|
146
|
+
...(params.csv_format?.description_column !== undefined && {
|
|
147
|
+
description: String(params.csv_format.description_column),
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
...(dateFormat && { dateFormat }),
|
|
151
|
+
...(params.csv_format?.has_header !== undefined && {
|
|
152
|
+
header: params.csv_format.has_header,
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
let csvContent = params.csv_data ?? '';
|
|
156
|
+
if (!csvContent && params.csv_file_path) {
|
|
157
|
+
try {
|
|
158
|
+
csvContent = await fs.readFile(params.csv_file_path, 'utf8');
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const message = error instanceof Error && error.message
|
|
162
|
+
? error.message
|
|
163
|
+
: 'Unknown error while reading CSV file';
|
|
164
|
+
throw new Error(`Failed to read CSV file at path ${params.csv_file_path}: ${message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
128
167
|
let sinceDate;
|
|
168
|
+
let parseResult;
|
|
129
169
|
if (params.statement_start_date) {
|
|
130
170
|
sinceDate = new Date(params.statement_start_date);
|
|
131
171
|
}
|
|
132
172
|
else {
|
|
133
173
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
parseResult = parseCSV(csvContent, {
|
|
175
|
+
...csvOptions,
|
|
176
|
+
invertAmounts: shouldInvertBankAmounts,
|
|
177
|
+
});
|
|
178
|
+
if (parseResult.transactions.length > 0) {
|
|
179
|
+
const dates = parseResult.transactions
|
|
180
|
+
.map((t) => new Date(t.date).getTime())
|
|
181
|
+
.filter((t) => !isNaN(t));
|
|
182
|
+
if (dates.length > 0) {
|
|
183
|
+
const minTime = Math.min(...dates);
|
|
184
|
+
const minDateObj = new Date(minTime);
|
|
185
|
+
minDateObj.setDate(minDateObj.getDate() - 7);
|
|
186
|
+
sinceDate = minDateObj;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
194
|
+
}
|
|
150
195
|
}
|
|
151
196
|
catch {
|
|
152
197
|
sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
@@ -157,6 +202,21 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
157
202
|
? await deltaFetcher.fetchTransactionsByAccountFull(params.budget_id, params.account_id, sinceDateString)
|
|
158
203
|
: await deltaFetcher.fetchTransactionsByAccount(params.budget_id, params.account_id, sinceDateString);
|
|
159
204
|
const ynabTransactions = transactionsResult.data;
|
|
205
|
+
let finalInvertAmounts = shouldInvertBankAmounts;
|
|
206
|
+
if (params.invert_bank_amounts === undefined && csvContent) {
|
|
207
|
+
const rawParseResult = parseCSV(csvContent, {
|
|
208
|
+
...csvOptions,
|
|
209
|
+
invertAmounts: false,
|
|
210
|
+
});
|
|
211
|
+
if (rawParseResult.transactions.length > 0 && ynabTransactions.length > 0) {
|
|
212
|
+
const normalizedYNAB = normalizeYNABTransactions(ynabTransactions);
|
|
213
|
+
const needsInversion = detectSignInversion(rawParseResult.transactions, normalizedYNAB);
|
|
214
|
+
finalInvertAmounts = needsInversion;
|
|
215
|
+
if (needsInversion !== shouldInvertBankAmounts && parseResult) {
|
|
216
|
+
parseResult = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
160
220
|
const auditMetadata = {
|
|
161
221
|
data_freshness: getDataFreshness(transactionsResult, forceFullRefresh),
|
|
162
222
|
data_source: getAuditDataSource(transactionsResult, forceFullRefresh),
|
|
@@ -170,12 +230,12 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
170
230
|
delta_merge_applied: transactionsResult.usedDelta,
|
|
171
231
|
},
|
|
172
232
|
};
|
|
173
|
-
const analysis = analyzeReconciliation(params.csv_data || params.csv_file_path || '', params.csv_file_path, ynabTransactions, adjustedStatementBalance, config, currencyCode, params.account_id, params.budget_id, shouldInvertBankAmounts);
|
|
174
233
|
const initialAccount = {
|
|
175
234
|
balance: accountData.balance,
|
|
176
235
|
cleared_balance: accountData.cleared_balance,
|
|
177
236
|
uncleared_balance: accountData.uncleared_balance,
|
|
178
237
|
};
|
|
238
|
+
const analysis = analyzeReconciliation(parseResult ?? csvContent, params.csv_file_path, ynabTransactions, adjustedStatementBalance, config, currencyCode, params.account_id, params.budget_id, finalInvertAmounts, csvOptions, initialAccount);
|
|
179
239
|
let executionData;
|
|
180
240
|
const wantsBalanceVerification = Boolean(params.statement_date);
|
|
181
241
|
const shouldExecute = params.auto_create_transactions ||
|
|
@@ -222,6 +282,22 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
222
282
|
};
|
|
223
283
|
}, 'ynab:reconcile_account', 'analyzing account reconciliation');
|
|
224
284
|
}
|
|
285
|
+
function mapCsvDateFormatToHint(format) {
|
|
286
|
+
if (!format) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
const normalized = format.toUpperCase().replace(/[^YMD]/g, '');
|
|
290
|
+
if (normalized === 'YYYYMMDD' || normalized === 'YYMMDD' || normalized === 'YMD') {
|
|
291
|
+
return 'YMD';
|
|
292
|
+
}
|
|
293
|
+
if (normalized === 'MMDDYYYY' || normalized === 'MDY') {
|
|
294
|
+
return 'MDY';
|
|
295
|
+
}
|
|
296
|
+
if (normalized === 'DDMMYYYY' || normalized === 'DMY') {
|
|
297
|
+
return 'DMY';
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
225
301
|
function mapCsvFormatForPayload(format) {
|
|
226
302
|
if (!format) {
|
|
227
303
|
return undefined;
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
import type { BankTransaction
|
|
2
|
-
|
|
3
|
-
export
|
|
1
|
+
import type { BankTransaction as CanonicalBankTransaction, NormalizedYNABTransaction } from '../../types/reconciliation.js';
|
|
2
|
+
import { type MatchingConfig } from './types.js';
|
|
3
|
+
export type { MatchingConfig };
|
|
4
|
+
export interface MatchCandidate {
|
|
5
|
+
ynabTransaction: NormalizedYNABTransaction;
|
|
6
|
+
scores: {
|
|
7
|
+
amount: number;
|
|
8
|
+
date: number;
|
|
9
|
+
payee: number;
|
|
10
|
+
combined: number;
|
|
11
|
+
};
|
|
12
|
+
matchReasons: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface MatchResult {
|
|
15
|
+
bankTransaction: CanonicalBankTransaction;
|
|
16
|
+
bestMatch: MatchCandidate | null;
|
|
17
|
+
candidates: MatchCandidate[];
|
|
18
|
+
confidence: 'high' | 'medium' | 'low' | 'none';
|
|
19
|
+
confidenceScore: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_CONFIG: MatchingConfig;
|
|
22
|
+
export declare function normalizeConfig(config?: MatchingConfig): MatchingConfig;
|
|
23
|
+
export declare function findMatches(bankTransactions: CanonicalBankTransaction[], ynabTransactions: NormalizedYNABTransaction[], config?: MatchingConfig): MatchResult[];
|
|
24
|
+
export declare function findBestMatch(bankTransaction: CanonicalBankTransaction, ynabTransactions: NormalizedYNABTransaction[], usedYnabIds?: Set<string>, config?: MatchingConfig): MatchResult;
|
|
@@ -1,160 +1,202 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!amountMatch) {
|
|
21
|
-
return { score: 0, reasons: ['Amount does not match'] };
|
|
22
|
-
}
|
|
23
|
-
score += 40;
|
|
24
|
-
reasons.push('Amount matches');
|
|
25
|
-
const dateWithinTolerance = datesMatch(bankTxn.date, ynabTxn.date, config.dateToleranceDays);
|
|
26
|
-
if (dateWithinTolerance) {
|
|
27
|
-
score += 40;
|
|
28
|
-
const daysDiff = Math.abs((new Date(bankTxn.date).getTime() - new Date(ynabTxn.date).getTime()) / (1000 * 60 * 60 * 24));
|
|
29
|
-
if (daysDiff === 0) {
|
|
30
|
-
reasons.push('Exact date match');
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
reasons.push(`Date within ${Math.round(daysDiff)} days`);
|
|
34
|
-
}
|
|
1
|
+
import * as fuzz from 'fuzzball';
|
|
2
|
+
export const DEFAULT_CONFIG = {
|
|
3
|
+
weights: {
|
|
4
|
+
amount: 0.5,
|
|
5
|
+
date: 0.15,
|
|
6
|
+
payee: 0.35,
|
|
7
|
+
},
|
|
8
|
+
amountToleranceMilliunits: 10,
|
|
9
|
+
dateToleranceDays: 7,
|
|
10
|
+
autoMatchThreshold: 85,
|
|
11
|
+
suggestedMatchThreshold: 60,
|
|
12
|
+
minimumCandidateScore: 40,
|
|
13
|
+
exactAmountBonus: 10,
|
|
14
|
+
exactDateBonus: 5,
|
|
15
|
+
exactPayeeBonus: 10,
|
|
16
|
+
};
|
|
17
|
+
export function normalizeConfig(config) {
|
|
18
|
+
if (!config) {
|
|
19
|
+
return { ...DEFAULT_CONFIG };
|
|
35
20
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
return {
|
|
22
|
+
weights: config.weights ?? DEFAULT_CONFIG.weights,
|
|
23
|
+
amountToleranceMilliunits: config.amountToleranceMilliunits ?? DEFAULT_CONFIG.amountToleranceMilliunits,
|
|
24
|
+
dateToleranceDays: config.dateToleranceDays ?? DEFAULT_CONFIG.dateToleranceDays,
|
|
25
|
+
autoMatchThreshold: config.autoMatchThreshold ?? DEFAULT_CONFIG.autoMatchThreshold,
|
|
26
|
+
suggestedMatchThreshold: config.suggestedMatchThreshold ?? DEFAULT_CONFIG.suggestedMatchThreshold,
|
|
27
|
+
minimumCandidateScore: config.minimumCandidateScore ?? DEFAULT_CONFIG.minimumCandidateScore,
|
|
28
|
+
exactAmountBonus: config.exactAmountBonus ?? DEFAULT_CONFIG.exactAmountBonus,
|
|
29
|
+
exactDateBonus: config.exactDateBonus ?? DEFAULT_CONFIG.exactDateBonus,
|
|
30
|
+
exactPayeeBonus: config.exactPayeeBonus ?? DEFAULT_CONFIG.exactPayeeBonus,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function matchSingle(bankTxn, ynabTransactions, usedIds, configInput) {
|
|
34
|
+
const config = normalizeConfig(configInput);
|
|
35
|
+
const candidates = findCandidates(bankTxn, ynabTransactions, usedIds, config);
|
|
36
|
+
const bestMatch = candidates.length > 0 ? candidates[0] : null;
|
|
37
|
+
const confidenceScore = bestMatch?.scores.combined ?? 0;
|
|
38
|
+
let confidence;
|
|
39
|
+
if (confidenceScore >= config.autoMatchThreshold) {
|
|
40
|
+
confidence = 'high';
|
|
41
|
+
if (bestMatch)
|
|
42
|
+
usedIds.add(bestMatch.ynabTransaction.id);
|
|
40
43
|
}
|
|
41
|
-
else if (
|
|
42
|
-
|
|
43
|
-
reasons.push(`Payee highly similar (${Math.round(payeeScore)}%)`);
|
|
44
|
+
else if (confidenceScore >= config.suggestedMatchThreshold) {
|
|
45
|
+
confidence = 'medium';
|
|
44
46
|
}
|
|
45
|
-
else if (
|
|
46
|
-
|
|
47
|
-
reasons.push(`Payee similar (${Math.round(payeeScore)}%)`);
|
|
47
|
+
else if (confidenceScore >= config.minimumCandidateScore) {
|
|
48
|
+
confidence = 'low';
|
|
48
49
|
}
|
|
49
|
-
else
|
|
50
|
-
|
|
51
|
-
reasons.push(`Payee somewhat similar (${Math.round(payeeScore)}%)`);
|
|
50
|
+
else {
|
|
51
|
+
confidence = 'none';
|
|
52
52
|
}
|
|
53
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
bankTransaction: bankTxn,
|
|
55
|
+
bestMatch,
|
|
56
|
+
candidates: candidates.slice(0, 3),
|
|
57
|
+
confidence,
|
|
58
|
+
confidenceScore,
|
|
59
|
+
};
|
|
54
60
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return 0;
|
|
61
|
+
export function findMatches(bankTransactions, ynabTransactions, config) {
|
|
62
|
+
const usedYnabIds = new Set();
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const bankTxn of bankTransactions) {
|
|
65
|
+
results.push(matchSingle(bankTxn, ynabTransactions, usedYnabIds, config));
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
63
68
|
}
|
|
64
|
-
function
|
|
69
|
+
function findCandidates(bankTxn, ynabTransactions, usedIds, config) {
|
|
65
70
|
const candidates = [];
|
|
66
71
|
for (const ynabTxn of ynabTransactions) {
|
|
67
72
|
if (usedIds.has(ynabTxn.id))
|
|
68
73
|
continue;
|
|
69
|
-
|
|
74
|
+
const bankSign = Math.sign(bankTxn.amount);
|
|
75
|
+
const ynabSign = Math.sign(ynabTxn.amount);
|
|
76
|
+
if (bankSign !== ynabSign && bankSign !== 0 && ynabSign !== 0) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
|
|
80
|
+
if (amountDiff > config.amountToleranceMilliunits) {
|
|
70
81
|
continue;
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
}
|
|
83
|
+
const scores = calculateScores(bankTxn, ynabTxn, config);
|
|
84
|
+
if (scores.combined >= config.minimumCandidateScore) {
|
|
73
85
|
candidates.push({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
explanation: buildExplanation(bankTxn, ynabTxn, score, reasons),
|
|
86
|
+
ynabTransaction: ynabTxn,
|
|
87
|
+
scores,
|
|
88
|
+
matchReasons: buildMatchReasons(scores, config),
|
|
78
89
|
});
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
candidates.sort((a, b) => {
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
const scoreDiff = b.scores.combined - a.scores.combined;
|
|
94
|
+
if (scoreDiff !== 0) {
|
|
95
|
+
return scoreDiff;
|
|
96
|
+
}
|
|
97
|
+
const aUncleared = a.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
|
|
98
|
+
const bUncleared = b.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
|
|
99
|
+
if (aUncleared !== bUncleared) {
|
|
100
|
+
return bUncleared - aUncleared;
|
|
101
|
+
}
|
|
102
|
+
const bankTime = new Date(bankTxn.date).getTime();
|
|
103
|
+
const aDiff = Math.abs(bankTime - new Date(a.ynabTransaction.date).getTime());
|
|
104
|
+
const bDiff = Math.abs(bankTime - new Date(b.ynabTransaction.date).getTime());
|
|
105
|
+
if (aDiff !== bDiff) {
|
|
106
|
+
return aDiff - bDiff;
|
|
84
107
|
}
|
|
85
|
-
|
|
86
|
-
if (priorityDiff !== 0)
|
|
87
|
-
return priorityDiff;
|
|
88
|
-
const dateProximityA = Math.abs(new Date(bankTxn.date).getTime() - new Date(a.ynab_transaction.date).getTime());
|
|
89
|
-
const dateProximityB = Math.abs(new Date(bankTxn.date).getTime() - new Date(b.ynab_transaction.date).getTime());
|
|
90
|
-
return dateProximityA - dateProximityB;
|
|
108
|
+
return 0;
|
|
91
109
|
});
|
|
92
110
|
return candidates;
|
|
93
111
|
}
|
|
94
|
-
function
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
export function findBestMatch(bankTxn, ynabTransactions, usedIds, config) {
|
|
104
|
-
const candidates = findMatchCandidates(bankTxn, ynabTransactions, usedIds, config);
|
|
105
|
-
if (candidates.length === 0) {
|
|
106
|
-
return {
|
|
107
|
-
bank_transaction: bankTxn,
|
|
108
|
-
confidence: 'none',
|
|
109
|
-
confidence_score: 0,
|
|
110
|
-
match_reason: 'No matching transaction found in YNAB',
|
|
111
|
-
action_hint: 'add_to_ynab',
|
|
112
|
-
recommendation: 'This transaction appears on bank statement but not in YNAB',
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
const bestCandidate = candidates[0];
|
|
116
|
-
const bestScore = bestCandidate.confidence;
|
|
117
|
-
if (bestScore >= config.autoMatchThreshold) {
|
|
118
|
-
return {
|
|
119
|
-
bank_transaction: bankTxn,
|
|
120
|
-
ynab_transaction: bestCandidate.ynab_transaction,
|
|
121
|
-
confidence: 'high',
|
|
122
|
-
confidence_score: bestScore,
|
|
123
|
-
match_reason: bestCandidate.match_reason,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
if (bestScore >= config.suggestionThreshold) {
|
|
127
|
-
return {
|
|
128
|
-
bank_transaction: bankTxn,
|
|
129
|
-
ynab_transaction: bestCandidate.ynab_transaction,
|
|
130
|
-
candidates: candidates.slice(0, 3),
|
|
131
|
-
confidence: 'medium',
|
|
132
|
-
confidence_score: bestScore,
|
|
133
|
-
match_reason: bestCandidate.match_reason,
|
|
134
|
-
top_confidence: bestScore,
|
|
135
|
-
action_hint: 'review_and_choose',
|
|
136
|
-
};
|
|
112
|
+
function calculateScores(bankTxn, ynabTxn, config) {
|
|
113
|
+
const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
|
|
114
|
+
let amountScore;
|
|
115
|
+
if (amountDiff === 0) {
|
|
116
|
+
amountScore = 100;
|
|
117
|
+
}
|
|
118
|
+
else if (amountDiff <= config.amountToleranceMilliunits) {
|
|
119
|
+
amountScore = 95;
|
|
137
120
|
}
|
|
121
|
+
else if (amountDiff <= 1000) {
|
|
122
|
+
amountScore = 80 - (amountDiff / 1000) * 20;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
amountScore = Math.max(0, 60 - (amountDiff / 1000) * 5);
|
|
126
|
+
}
|
|
127
|
+
const bankDate = new Date(bankTxn.date);
|
|
128
|
+
const ynabDate = new Date(ynabTxn.date);
|
|
129
|
+
const daysDiff = Math.abs(bankDate.getTime() - ynabDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
130
|
+
let dateScore;
|
|
131
|
+
if (daysDiff < 0.5) {
|
|
132
|
+
dateScore = 100;
|
|
133
|
+
}
|
|
134
|
+
else if (daysDiff <= 1) {
|
|
135
|
+
dateScore = 95;
|
|
136
|
+
}
|
|
137
|
+
else if (daysDiff <= config.dateToleranceDays) {
|
|
138
|
+
dateScore = 90 - (daysDiff - 1) * (40 / config.dateToleranceDays);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
dateScore = Math.max(0, 50 - (daysDiff - config.dateToleranceDays) * 5);
|
|
142
|
+
}
|
|
143
|
+
const payeeScore = calculatePayeeScore(bankTxn.payee, ynabTxn.payee);
|
|
144
|
+
let combined = amountScore * config.weights.amount +
|
|
145
|
+
dateScore * config.weights.date +
|
|
146
|
+
payeeScore * config.weights.payee;
|
|
147
|
+
if (amountScore === 100)
|
|
148
|
+
combined += config.exactAmountBonus;
|
|
149
|
+
if (dateScore === 100)
|
|
150
|
+
combined += config.exactDateBonus;
|
|
151
|
+
if (payeeScore >= 95)
|
|
152
|
+
combined += config.exactPayeeBonus;
|
|
153
|
+
combined = Math.min(100, combined);
|
|
138
154
|
return {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
match_reason: 'Low confidence match',
|
|
144
|
-
top_confidence: bestScore,
|
|
145
|
-
action_hint: 'review_or_add_new',
|
|
146
|
-
recommendation: 'Consider reviewing candidates or adding as new transaction',
|
|
155
|
+
amount: Math.round(amountScore),
|
|
156
|
+
date: Math.round(dateScore),
|
|
157
|
+
payee: Math.round(payeeScore),
|
|
158
|
+
combined: Math.round(combined),
|
|
147
159
|
};
|
|
148
160
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
function calculatePayeeScore(bankPayee, ynabPayee) {
|
|
162
|
+
if (!ynabPayee)
|
|
163
|
+
return 30;
|
|
164
|
+
const scores = [
|
|
165
|
+
fuzz.token_set_ratio(bankPayee, ynabPayee),
|
|
166
|
+
fuzz.token_sort_ratio(bankPayee, ynabPayee),
|
|
167
|
+
fuzz.partial_ratio(bankPayee, ynabPayee),
|
|
168
|
+
fuzz.WRatio(bankPayee, ynabPayee),
|
|
169
|
+
];
|
|
170
|
+
return Math.max(...scores);
|
|
171
|
+
}
|
|
172
|
+
function buildMatchReasons(scores, config) {
|
|
173
|
+
const reasons = [];
|
|
174
|
+
if (scores.amount === 100) {
|
|
175
|
+
reasons.push('Exact amount match');
|
|
176
|
+
}
|
|
177
|
+
else if (scores.amount >= 95) {
|
|
178
|
+
reasons.push('Amount within tolerance');
|
|
179
|
+
}
|
|
180
|
+
if (scores.date === 100) {
|
|
181
|
+
reasons.push('Same date');
|
|
182
|
+
}
|
|
183
|
+
else if (scores.date >= 90) {
|
|
184
|
+
reasons.push('Date within 1-2 days');
|
|
185
|
+
}
|
|
186
|
+
else if (scores.date >= 50) {
|
|
187
|
+
reasons.push(`Date within ${config.dateToleranceDays} days`);
|
|
158
188
|
}
|
|
159
|
-
|
|
189
|
+
if (scores.payee >= 95) {
|
|
190
|
+
reasons.push('Payee exact match');
|
|
191
|
+
}
|
|
192
|
+
else if (scores.payee >= 80) {
|
|
193
|
+
reasons.push('Payee highly similar');
|
|
194
|
+
}
|
|
195
|
+
else if (scores.payee >= 60) {
|
|
196
|
+
reasons.push('Payee somewhat similar');
|
|
197
|
+
}
|
|
198
|
+
return reasons;
|
|
199
|
+
}
|
|
200
|
+
export function findBestMatch(bankTransaction, ynabTransactions, usedYnabIds = new Set(), config) {
|
|
201
|
+
return matchSingle(bankTransaction, ynabTransactions, usedYnabIds, config);
|
|
160
202
|
}
|