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