@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.
- 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 +4 -2
- package/dist/tools/reconciliation/analyzer.js +120 -404
- 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 +174 -545
- package/src/tools/reconciliation/csvParser.ts +617 -0
- package/src/tools/reconciliation/executor.ts +249 -66
- package/src/tools/reconciliation/index.ts +141 -48
- 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,269 +1,289 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transaction matching algorithm for reconciliation
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* V2 matcher works natively in milliunits using canonical BankTransaction
|
|
5
|
+
* and NormalizedYNABTransaction types.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
import
|
|
7
|
-
import { DEFAULT_MATCHING_CONFIG } from './types.js';
|
|
8
|
+
import * as fuzz from 'fuzzball';
|
|
8
9
|
import type {
|
|
9
|
-
BankTransaction,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 {
|
|
97
|
+
return {
|
|
98
|
+
bankTransaction: bankTxn,
|
|
99
|
+
bestMatch,
|
|
100
|
+
candidates: candidates.slice(0, 3),
|
|
101
|
+
confidence,
|
|
102
|
+
confidenceScore,
|
|
103
|
+
};
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
const { score, reasons } = calculateMatchScore(bankTxn, ynabTxn, config);
|
|
145
|
+
const scores = calculateScores(bankTxn, ynabTxn, config);
|
|
129
146
|
|
|
130
|
-
|
|
131
|
-
if (score >= 30) {
|
|
147
|
+
if (scores.combined >= config.minimumCandidateScore) {
|
|
132
148
|
candidates.push({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
175
|
+
return 0;
|
|
176
|
+
});
|
|
177
|
+
return candidates;
|
|
181
178
|
}
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
):
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
142
|
+
const bankTxn = match.bankTransaction;
|
|
143
143
|
|
|
144
144
|
// If there's a suggested YNAB transaction, review as possible duplicate
|
|
145
|
-
if (match.
|
|
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.
|
|
150
|
+
confidence: Math.max(0, Math.min(1, match.confidenceScore / 100)),
|
|
151
151
|
message: `Review possible match: ${bankTxn.payee}`,
|
|
152
|
-
reason: match.
|
|
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.
|
|
163
|
+
candidate_ids: [match.ynabTransaction.id],
|
|
164
164
|
bank_transaction: bankTxn,
|
|
165
|
-
suggested_match_id: match.
|
|
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:
|
|
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:
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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,
|