@dizzlkheinz/ynab-mcpb 0.18.4 → 0.19.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/CLAUDE.md +87 -8
- package/bin/ynab-mcp-server.cjs +2 -2
- package/bin/ynab-mcp-server.js +3 -3
- package/biome.json +39 -0
- package/dist/bundle/index.cjs +67 -67
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -27
- package/dist/server/YNABMCPServer.d.ts +3 -4
- package/dist/server/YNABMCPServer.js +111 -116
- package/dist/server/budgetResolver.d.ts +6 -5
- package/dist/server/budgetResolver.js +46 -36
- package/dist/server/cacheKeys.js +6 -6
- package/dist/server/cacheManager.js +14 -11
- package/dist/server/completions.d.ts +2 -2
- package/dist/server/completions.js +20 -15
- package/dist/server/config.d.ts +10 -5
- package/dist/server/config.js +24 -7
- package/dist/server/deltaCache.d.ts +2 -2
- package/dist/server/deltaCache.js +22 -16
- package/dist/server/deltaCache.merge.d.ts +2 -2
- package/dist/server/diagnostics.d.ts +4 -4
- package/dist/server/diagnostics.js +38 -32
- package/dist/server/errorHandler.d.ts +5 -12
- package/dist/server/errorHandler.js +219 -217
- package/dist/server/prompts.d.ts +2 -2
- package/dist/server/prompts.js +45 -45
- package/dist/server/rateLimiter.js +4 -4
- package/dist/server/requestLogger.d.ts +1 -1
- package/dist/server/requestLogger.js +40 -35
- package/dist/server/resources.d.ts +3 -3
- package/dist/server/resources.js +55 -52
- package/dist/server/responseFormatter.js +6 -6
- package/dist/server/securityMiddleware.d.ts +2 -2
- package/dist/server/securityMiddleware.js +22 -20
- package/dist/server/serverKnowledgeStore.js +1 -1
- package/dist/server/toolRegistry.d.ts +3 -3
- package/dist/server/toolRegistry.js +47 -40
- package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
- package/dist/tools/__tests__/deltaTestUtils.js +2 -2
- package/dist/tools/accountTools.d.ts +9 -8
- package/dist/tools/accountTools.js +47 -47
- package/dist/tools/adapters.d.ts +13 -8
- package/dist/tools/adapters.js +21 -11
- package/dist/tools/budgetTools.d.ts +8 -7
- package/dist/tools/budgetTools.js +22 -22
- package/dist/tools/categoryTools.d.ts +9 -8
- package/dist/tools/categoryTools.js +68 -59
- package/dist/tools/compareTransactions/formatter.d.ts +3 -3
- package/dist/tools/compareTransactions/formatter.js +9 -9
- package/dist/tools/compareTransactions/index.d.ts +6 -6
- package/dist/tools/compareTransactions/index.js +58 -43
- package/dist/tools/compareTransactions/matcher.d.ts +1 -1
- package/dist/tools/compareTransactions/matcher.js +28 -15
- package/dist/tools/compareTransactions/parser.d.ts +2 -2
- package/dist/tools/compareTransactions/parser.js +144 -138
- package/dist/tools/compareTransactions/types.d.ts +4 -4
- package/dist/tools/compareTransactions.d.ts +1 -1
- package/dist/tools/compareTransactions.js +1 -1
- package/dist/tools/deltaFetcher.d.ts +2 -2
- package/dist/tools/deltaFetcher.js +16 -15
- package/dist/tools/deltaSupport.d.ts +4 -4
- package/dist/tools/deltaSupport.js +35 -41
- package/dist/tools/exportTransactions.d.ts +5 -4
- package/dist/tools/exportTransactions.js +61 -59
- package/dist/tools/monthTools.d.ts +7 -6
- package/dist/tools/monthTools.js +31 -29
- package/dist/tools/payeeTools.d.ts +7 -6
- package/dist/tools/payeeTools.js +28 -28
- package/dist/tools/reconcileAdapter.d.ts +2 -2
- package/dist/tools/reconcileAdapter.js +19 -12
- package/dist/tools/reconciliation/analyzer.d.ts +4 -4
- package/dist/tools/reconciliation/analyzer.js +73 -59
- package/dist/tools/reconciliation/csvParser.d.ts +3 -3
- package/dist/tools/reconciliation/csvParser.js +128 -104
- package/dist/tools/reconciliation/executor.d.ts +4 -4
- package/dist/tools/reconciliation/executor.js +148 -109
- package/dist/tools/reconciliation/index.d.ts +10 -10
- package/dist/tools/reconciliation/index.js +96 -83
- package/dist/tools/reconciliation/matcher.d.ts +3 -3
- package/dist/tools/reconciliation/matcher.js +17 -16
- package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
- package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
- package/dist/tools/reconciliation/recommendationEngine.js +40 -40
- package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
- package/dist/tools/reconciliation/reportFormatter.js +59 -58
- package/dist/tools/reconciliation/signDetector.d.ts +1 -1
- package/dist/tools/reconciliation/types.d.ts +16 -16
- package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
- package/dist/tools/schemas/common.d.ts +1 -1
- package/dist/tools/schemas/common.js +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
- package/dist/tools/schemas/outputs/index.d.ts +14 -14
- package/dist/tools/schemas/outputs/index.js +14 -14
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
- package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
- package/dist/tools/schemas/shared/commonOutputs.js +15 -9
- package/dist/tools/transactionReadTools.d.ts +11 -0
- package/dist/tools/transactionReadTools.js +202 -0
- package/dist/tools/transactionSchemas.d.ts +7 -7
- package/dist/tools/transactionSchemas.js +77 -57
- package/dist/tools/transactionTools.d.ts +6 -24
- package/dist/tools/transactionTools.js +7 -1499
- package/dist/tools/transactionUtils.d.ts +6 -6
- package/dist/tools/transactionUtils.js +78 -63
- package/dist/tools/transactionWriteTools.d.ts +20 -0
- package/dist/tools/transactionWriteTools.js +1342 -0
- package/dist/tools/utilityTools.d.ts +5 -4
- package/dist/tools/utilityTools.js +11 -11
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.js +6 -6
- package/dist/types/reconciliation.d.ts +1 -1
- package/dist/types/toolRegistration.d.ts +14 -12
- package/dist/utils/amountUtils.js +1 -1
- package/dist/utils/dateUtils.js +4 -4
- package/dist/utils/errors.d.ts +3 -3
- package/dist/utils/errors.js +4 -4
- package/dist/utils/money.d.ts +2 -2
- package/dist/utils/money.js +8 -8
- package/dist/utils/validationError.d.ts +1 -1
- package/dist/utils/validationError.js +1 -1
- package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
- package/docs/assets/schemas/reconciliation-v2.json +360 -336
- package/esbuild.config.mjs +53 -50
- package/meta.json +12548 -12548
- package/package.json +98 -111
- package/scripts/analyze-bundle.mjs +33 -30
- package/scripts/create-pr-description.js +169 -120
- package/scripts/run-all-tests.js +178 -169
- package/scripts/run-domain-integration-tests.js +28 -18
- package/scripts/run-generate-mcpb.js +19 -17
- package/scripts/run-throttled-integration-tests.js +92 -83
- package/scripts/test-delta-params.mjs +149 -120
- package/scripts/test-recommendations.ts +36 -32
- package/scripts/tmpTransaction.ts +80 -43
- package/scripts/validate-env.js +98 -91
- package/scripts/verify-build.js +78 -76
- package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
- package/src/__tests__/performance.test.ts +723 -671
- package/src/__tests__/setup.ts +442 -395
- package/src/__tests__/smoke.e2e.test.ts +41 -39
- package/src/__tests__/testRunner.ts +314 -295
- package/src/__tests__/testUtils.ts +456 -364
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
- package/src/index.ts +68 -59
- package/src/server/CLAUDE.md +480 -0
- package/src/server/YNABMCPServer.ts +821 -794
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
- package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
- package/src/server/__tests__/budgetResolver.test.ts +466 -423
- package/src/server/__tests__/cacheManager.test.ts +891 -874
- package/src/server/__tests__/completions.integration.test.ts +115 -106
- package/src/server/__tests__/completions.test.ts +334 -313
- package/src/server/__tests__/config.test.ts +98 -86
- package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
- package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
- package/src/server/__tests__/deltaCache.test.ts +946 -759
- package/src/server/__tests__/diagnostics.test.ts +825 -792
- package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
- package/src/server/__tests__/errorHandler.test.ts +402 -397
- package/src/server/__tests__/prompts.test.ts +424 -347
- package/src/server/__tests__/rateLimiter.test.ts +313 -309
- package/src/server/__tests__/requestLogger.test.ts +443 -403
- package/src/server/__tests__/resources.template.test.ts +196 -185
- package/src/server/__tests__/resources.test.ts +294 -288
- package/src/server/__tests__/security.integration.test.ts +487 -421
- package/src/server/__tests__/securityMiddleware.test.ts +519 -444
- package/src/server/__tests__/server-startup.integration.test.ts +509 -490
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
- package/src/server/__tests__/toolRegistration.test.ts +239 -210
- package/src/server/__tests__/toolRegistry.test.ts +907 -845
- package/src/server/budgetResolver.ts +221 -181
- package/src/server/cacheKeys.ts +6 -6
- package/src/server/cacheManager.ts +498 -484
- package/src/server/completions.ts +267 -243
- package/src/server/config.ts +35 -14
- package/src/server/deltaCache.merge.ts +146 -128
- package/src/server/deltaCache.ts +352 -309
- package/src/server/diagnostics.ts +257 -242
- package/src/server/errorHandler.ts +747 -744
- package/src/server/prompts.ts +181 -176
- package/src/server/rateLimiter.ts +131 -129
- package/src/server/requestLogger.ts +350 -322
- package/src/server/resources.ts +442 -374
- package/src/server/responseFormatter.ts +41 -37
- package/src/server/securityMiddleware.ts +223 -205
- package/src/server/serverKnowledgeStore.ts +67 -67
- package/src/server/toolRegistry.ts +508 -474
- package/src/tools/CLAUDE.md +604 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
- package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
- package/src/tools/__tests__/accountTools.test.ts +685 -638
- package/src/tools/__tests__/adapters.test.ts +142 -108
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
- package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
- package/src/tools/__tests__/budgetTools.test.ts +442 -413
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
- package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
- package/src/tools/__tests__/categoryTools.test.ts +656 -625
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
- package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
- package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
- package/src/tools/__tests__/compareTransactions.test.ts +352 -332
- package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
- package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
- package/src/tools/__tests__/deltaSupport.test.ts +211 -184
- package/src/tools/__tests__/deltaTestUtils.ts +37 -33
- package/src/tools/__tests__/exportTransactions.test.ts +205 -200
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
- package/src/tools/__tests__/monthTools.test.ts +561 -512
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
- package/src/tools/__tests__/payeeTools.test.ts +486 -434
- package/src/tools/__tests__/transactionSchemas.test.ts +1202 -1186
- package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
- package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
- package/src/tools/__tests__/transactionUtils.test.ts +1004 -977
- package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
- package/src/tools/__tests__/utilityTools.test.ts +68 -58
- package/src/tools/accountTools.ts +293 -271
- package/src/tools/adapters.ts +120 -63
- package/src/tools/budgetTools.ts +121 -116
- package/src/tools/categoryTools.ts +379 -339
- package/src/tools/compareTransactions/formatter.ts +131 -119
- package/src/tools/compareTransactions/index.ts +249 -214
- package/src/tools/compareTransactions/matcher.ts +259 -209
- package/src/tools/compareTransactions/parser.ts +517 -487
- package/src/tools/compareTransactions/types.ts +38 -38
- package/src/tools/compareTransactions.ts +1 -1
- package/src/tools/deltaFetcher.ts +281 -260
- package/src/tools/deltaSupport.ts +264 -259
- package/src/tools/exportTransactions.ts +230 -218
- package/src/tools/monthTools.ts +180 -165
- package/src/tools/payeeTools.ts +152 -140
- package/src/tools/reconcileAdapter.ts +297 -252
- package/src/tools/reconciliation/CLAUDE.md +506 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +133 -124
- package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -230
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -400
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
- package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
- package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -989
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -533
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -74
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -62
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +56 -55
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
- package/src/tools/reconciliation/analyzer.ts +564 -504
- package/src/tools/reconciliation/csvParser.ts +656 -609
- package/src/tools/reconciliation/executor.ts +1290 -1128
- package/src/tools/reconciliation/index.ts +580 -528
- package/src/tools/reconciliation/matcher.ts +256 -240
- package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
- package/src/tools/reconciliation/recommendationEngine.ts +357 -345
- package/src/tools/reconciliation/reportFormatter.ts +343 -307
- package/src/tools/reconciliation/signDetector.ts +89 -83
- package/src/tools/reconciliation/types.ts +164 -159
- package/src/tools/reconciliation/ynabAdapter.ts +17 -15
- package/src/tools/schemas/CLAUDE.md +546 -0
- package/src/tools/schemas/common.ts +1 -1
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
- package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
- package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
- package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
- package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
- package/src/tools/schemas/outputs/index.ts +163 -163
- package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
- package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
- package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
- package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
- package/src/tools/schemas/shared/commonOutputs.ts +27 -19
- package/src/tools/toolCategories.ts +114 -114
- package/src/tools/transactionReadTools.ts +327 -0
- package/src/tools/transactionSchemas.ts +322 -291
- package/src/tools/transactionTools.ts +84 -2246
- package/src/tools/transactionUtils.ts +507 -422
- package/src/tools/transactionWriteTools.ts +2110 -0
- package/src/tools/utilityTools.ts +46 -41
- package/src/types/CLAUDE.md +477 -0
- package/src/types/__tests__/index.test.ts +51 -51
- package/src/types/index.ts +43 -39
- package/src/types/integration-tests.d.ts +26 -26
- package/src/types/reconciliation.ts +29 -29
- package/src/types/toolAnnotations.ts +30 -30
- package/src/types/toolRegistration.ts +43 -32
- package/src/utils/CLAUDE.md +508 -0
- package/src/utils/__tests__/dateUtils.test.ts +174 -168
- package/src/utils/__tests__/money.test.ts +193 -187
- package/src/utils/amountUtils.ts +5 -5
- package/src/utils/baseError.ts +5 -5
- package/src/utils/dateUtils.ts +29 -26
- package/src/utils/errors.ts +14 -14
- package/src/utils/money.ts +66 -52
- package/src/utils/validationError.ts +1 -1
- package/tsconfig.json +29 -29
- package/tsconfig.prod.json +16 -16
- package/vitest-reporters/split-json-reporter.ts +247 -204
- package/vitest.config.ts +99 -95
- package/.prettierignore +0 -10
- package/.prettierrc.json +0 -10
- package/eslint.config.js +0 -49
|
@@ -1,524 +1,554 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { parse
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parse } from "csv-parse/sync";
|
|
3
|
+
import { parse as parseDateFns } from "date-fns";
|
|
4
|
+
import { toMilli } from "../../utils/money.js";
|
|
5
|
+
import type { Milli } from "../../utils/money.js";
|
|
6
|
+
import type { BankTransaction, CSVFormat } from "./types.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Parse date string using date-fns for better reliability
|
|
10
10
|
*/
|
|
11
11
|
export function parseDate(dateStr: string, format: string): Date {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
12
|
+
const cleanDate = dateStr.trim();
|
|
13
|
+
|
|
14
|
+
// Map our format strings to date-fns format patterns
|
|
15
|
+
const formatMap: Record<string, string> = {
|
|
16
|
+
"MM/DD/YYYY": "MM/dd/yyyy",
|
|
17
|
+
"M/D/YYYY": "M/d/yyyy",
|
|
18
|
+
"DD/MM/YYYY": "dd/MM/yyyy",
|
|
19
|
+
"D/M/YYYY": "d/M/yyyy",
|
|
20
|
+
"YYYY-MM-DD": "yyyy-MM-dd",
|
|
21
|
+
"MM-DD-YYYY": "MM-dd-yyyy",
|
|
22
|
+
"MMM dd, yyyy": "MMM dd, yyyy",
|
|
23
|
+
"MMM d, yyyy": "MMM d, yyyy",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const dateFnsFormat = formatMap[format];
|
|
27
|
+
if (dateFnsFormat) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = parseDateFns(cleanDate, dateFnsFormat, new Date());
|
|
30
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Fall through to generic parsing
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback to native Date parsing for any unrecognized formats
|
|
39
|
+
const parsed = new Date(cleanDate);
|
|
40
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
41
|
+
throw new Error(`Unable to parse date: ${dateStr} with format: ${format}`);
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Convert dollar amount to milliunits
|
|
48
48
|
*/
|
|
49
49
|
export function amountToMilliunits(amountStr: string): Milli {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
const cleaned = amountStr.replace(/[$,\s]/g, "").trim();
|
|
51
|
+
let s = cleaned;
|
|
52
|
+
let neg = false;
|
|
53
|
+
if (s.startsWith("(") && s.endsWith(")")) {
|
|
54
|
+
neg = true;
|
|
55
|
+
s = s.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
if (s.startsWith("+")) s = s.slice(1);
|
|
58
|
+
|
|
59
|
+
const n = Number(s);
|
|
60
|
+
if (Number.isNaN(n) || !Number.isFinite(n)) {
|
|
61
|
+
throw new Error(`Invalid amount value: "${amountStr}" (cleaned: "${s}")`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return toMilli(neg ? -n : n);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Check if a string looks like a date
|
|
69
69
|
*/
|
|
70
70
|
function isDateLike(str: string): boolean {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
if (!str) return false;
|
|
72
|
+
// Common date patterns
|
|
73
|
+
const datePatterns = [
|
|
74
|
+
/^\d{1,2}\/\d{1,2}\/\d{4}$/, // MM/DD/YYYY
|
|
75
|
+
/^\d{4}-\d{1,2}-\d{1,2}$/, // YYYY-MM-DD
|
|
76
|
+
/^\d{1,2}-\d{1,2}-\d{4}$/, // MM-DD-YYYY
|
|
77
|
+
/^[A-Za-z]{3}\s+\d{1,2},\s+\d{4}$/, // MMM dd, yyyy (e.g., "Sep 18, 2025")
|
|
78
|
+
];
|
|
79
|
+
return datePatterns.some((pattern) => pattern.test(str.trim()));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Detect date format from a sample date string
|
|
84
84
|
*/
|
|
85
85
|
export function detectDateFormat(dateStr: string | undefined): string {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
86
|
+
if (!dateStr) return "MM/DD/YYYY";
|
|
87
|
+
const cleaned = dateStr.trim();
|
|
88
|
+
|
|
89
|
+
if (cleaned.includes("/")) {
|
|
90
|
+
return "MM/DD/YYYY";
|
|
91
|
+
}
|
|
92
|
+
if (cleaned.includes("-")) {
|
|
93
|
+
if (/^\d{4}-/.test(cleaned)) {
|
|
94
|
+
return "YYYY-MM-DD";
|
|
95
|
+
}
|
|
96
|
+
return "MM-DD-YYYY";
|
|
97
|
+
}
|
|
98
|
+
if (/^[A-Za-z]{3}\s+\d{1,2},\s+\d{4}$/.test(cleaned)) {
|
|
99
|
+
// Detect "Sep 18, 2025" format
|
|
100
|
+
return "MMM dd, yyyy";
|
|
101
|
+
}
|
|
102
|
+
return "MM/DD/YYYY";
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
/**
|
|
105
106
|
* Detect the most likely delimiter by evaluating candidates across sample lines
|
|
106
107
|
*/
|
|
107
108
|
function detectDelimiter(lines: string[]): string {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
109
|
+
const candidates = [",", ";", "\t", "|"];
|
|
110
|
+
const sampleLines = lines.slice(0, 3).filter((line) => line.trim()); // Use first 2-3 non-empty lines
|
|
111
|
+
|
|
112
|
+
if (sampleLines.length === 0) {
|
|
113
|
+
return ","; // Default fallback
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let bestDelimiter = ",";
|
|
117
|
+
let bestScore = -1;
|
|
118
|
+
|
|
119
|
+
for (const delimiter of candidates) {
|
|
120
|
+
let score = 0;
|
|
121
|
+
const columnCounts: number[] = [];
|
|
122
|
+
let parseFailed = false;
|
|
123
|
+
|
|
124
|
+
for (const line of sampleLines) {
|
|
125
|
+
try {
|
|
126
|
+
const rows = parse(line, {
|
|
127
|
+
delimiter,
|
|
128
|
+
quote: '"',
|
|
129
|
+
escape: '"',
|
|
130
|
+
skip_empty_lines: true,
|
|
131
|
+
trim: true,
|
|
132
|
+
relax_column_count: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// rows should be an array with one row (since we're parsing one line)
|
|
136
|
+
if (rows && rows.length > 0 && rows[0]) {
|
|
137
|
+
const columns = Array.isArray(rows[0])
|
|
138
|
+
? rows[0]
|
|
139
|
+
: Object.values(rows[0]);
|
|
140
|
+
columnCounts.push(columns.length);
|
|
141
|
+
} else {
|
|
142
|
+
// If parsing failed or returned empty, fall back to simple split
|
|
143
|
+
const columns = line.split(delimiter);
|
|
144
|
+
columnCounts.push(columns.length);
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// If csv-parse fails, fall back to simple split method
|
|
148
|
+
parseFailed = true;
|
|
149
|
+
const columns = line.split(delimiter);
|
|
150
|
+
columnCounts.push(columns.length);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check consistency: all lines should have the same column count
|
|
155
|
+
if (columnCounts.length > 1) {
|
|
156
|
+
const firstCount = columnCounts[0];
|
|
157
|
+
if (firstCount === undefined) continue;
|
|
158
|
+
const isConsistent = columnCounts.every((count) => count === firstCount);
|
|
159
|
+
|
|
160
|
+
if (isConsistent && firstCount > 1) {
|
|
161
|
+
// Score based on column count (more columns = better, up to a reasonable limit)
|
|
162
|
+
score = Math.min(firstCount, 10); // Cap at 10 to avoid excessive weight
|
|
163
|
+
|
|
164
|
+
// Bonus points for common delimiters
|
|
165
|
+
if (delimiter === ",") score += 0.5;
|
|
166
|
+
if (delimiter === ";") score += 0.3;
|
|
167
|
+
|
|
168
|
+
// Bonus points if csv-parse succeeded (indicates proper CSV format)
|
|
169
|
+
if (!parseFailed) score += 0.2;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (score > bestScore) {
|
|
174
|
+
bestScore = score;
|
|
175
|
+
bestDelimiter = delimiter;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return bestDelimiter;
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
/**
|
|
180
183
|
* Analyze header names to detect column purposes
|
|
181
184
|
*/
|
|
182
185
|
function analyzeHeaders(headers: string[]): {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
dateColumn: string | null;
|
|
187
|
+
amountColumn: string | null;
|
|
188
|
+
descriptionColumn: string | null;
|
|
189
|
+
debitColumn: string | null;
|
|
190
|
+
creditColumn: string | null;
|
|
188
191
|
} {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
192
|
+
const datePattern = /^(date|trans.*date|transaction.*date|post.*date|dt)$/i;
|
|
193
|
+
const amountPattern = /^(amount|amt|dollar.*amount|transaction.*amount)$/i;
|
|
194
|
+
const descriptionPattern =
|
|
195
|
+
/^(description|desc|memo|transaction.*description|payee|merchant)$/i;
|
|
196
|
+
const debitPattern = /^(debit|debits|withdrawal|withdrawals|out|outgoing)$/i;
|
|
197
|
+
const creditPattern = /^(credit|credits|deposit|deposits|in|incoming)$/i;
|
|
198
|
+
|
|
199
|
+
let dateColumn: string | null = null;
|
|
200
|
+
let amountColumn: string | null = null;
|
|
201
|
+
let descriptionColumn: string | null = null;
|
|
202
|
+
let debitColumn: string | null = null;
|
|
203
|
+
let creditColumn: string | null = null;
|
|
204
|
+
|
|
205
|
+
for (const header of headers) {
|
|
206
|
+
const cleanHeader = header.trim();
|
|
207
|
+
|
|
208
|
+
if (datePattern.test(cleanHeader)) {
|
|
209
|
+
dateColumn = cleanHeader;
|
|
210
|
+
} else if (amountPattern.test(cleanHeader)) {
|
|
211
|
+
amountColumn = cleanHeader;
|
|
212
|
+
} else if (descriptionPattern.test(cleanHeader)) {
|
|
213
|
+
descriptionColumn = cleanHeader;
|
|
214
|
+
} else if (debitPattern.test(cleanHeader)) {
|
|
215
|
+
debitColumn = cleanHeader;
|
|
216
|
+
} else if (creditPattern.test(cleanHeader)) {
|
|
217
|
+
creditColumn = cleanHeader;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
dateColumn,
|
|
223
|
+
amountColumn,
|
|
224
|
+
descriptionColumn,
|
|
225
|
+
debitColumn,
|
|
226
|
+
creditColumn,
|
|
227
|
+
};
|
|
218
228
|
}
|
|
219
229
|
|
|
220
230
|
/**
|
|
221
231
|
* Auto-detect CSV format by analyzing the first few rows
|
|
222
232
|
*/
|
|
223
233
|
export function autoDetectCSVFormat(csvContent: string): CSVFormat {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
234
|
+
const linesRaw = csvContent.trim().split("\n").slice(0, 3);
|
|
235
|
+
if (linesRaw.length === 0) {
|
|
236
|
+
throw new Error("CSV file is empty");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Safely handle the first line - check if it exists and is not empty after trimming
|
|
240
|
+
const firstLineRaw = linesRaw[0];
|
|
241
|
+
if (!firstLineRaw || !firstLineRaw.trim()) {
|
|
242
|
+
throw new Error("CSV file contains empty first line");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Detect delimiter across sample lines
|
|
246
|
+
const delimiter = detectDelimiter(linesRaw);
|
|
247
|
+
|
|
248
|
+
const firstLine = firstLineRaw.split(delimiter);
|
|
249
|
+
const hasHeader = !isDateLike(firstLine[0] || "");
|
|
250
|
+
|
|
251
|
+
// Check for separate debit/credit columns by looking for empty cells pattern
|
|
252
|
+
let hasDebitCredit = false;
|
|
253
|
+
if (linesRaw.length > 1) {
|
|
254
|
+
const dataLines = hasHeader ? linesRaw.slice(1) : linesRaw;
|
|
255
|
+
hasDebitCredit = dataLines.some((line) => {
|
|
256
|
+
const cols = line.split(delimiter);
|
|
257
|
+
// Look for pattern: amount in col2 OR col3, but not both
|
|
258
|
+
return (
|
|
259
|
+
cols.length >= 4 &&
|
|
260
|
+
((cols[2]?.trim() && !cols[3]?.trim()) ||
|
|
261
|
+
(!cols[2]?.trim() && cols[3]?.trim()))
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (hasHeader) {
|
|
267
|
+
const {
|
|
268
|
+
dateColumn,
|
|
269
|
+
amountColumn,
|
|
270
|
+
descriptionColumn,
|
|
271
|
+
debitColumn,
|
|
272
|
+
creditColumn,
|
|
273
|
+
} = analyzeHeaders(firstLine);
|
|
274
|
+
|
|
275
|
+
const safe = (v?: string) => (v?.trim() ? v : undefined);
|
|
276
|
+
|
|
277
|
+
if (hasDebitCredit && debitColumn && creditColumn) {
|
|
278
|
+
const dateCol = safe(dateColumn ?? undefined) ?? safe(firstLine[0]);
|
|
279
|
+
if (!dateCol)
|
|
280
|
+
throw new Error("Unable to detect date column name from header");
|
|
281
|
+
const descCol =
|
|
282
|
+
safe(descriptionColumn ?? undefined) ?? safe(firstLine[1]);
|
|
283
|
+
if (!descCol)
|
|
284
|
+
throw new Error("Unable to detect description column name from header");
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
date_column: dateCol,
|
|
288
|
+
description_column: descCol,
|
|
289
|
+
debit_column: debitColumn,
|
|
290
|
+
credit_column: creditColumn,
|
|
291
|
+
date_format: detectDateFormat(linesRaw[1]?.split(delimiter)[0]),
|
|
292
|
+
has_header: hasHeader,
|
|
293
|
+
delimiter: delimiter,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const dateCol = safe(dateColumn ?? undefined) ?? safe(firstLine[0]);
|
|
297
|
+
if (!dateCol)
|
|
298
|
+
throw new Error("Unable to detect date column name from header");
|
|
299
|
+
const amountCol = safe(amountColumn ?? undefined) ?? safe(firstLine[1]);
|
|
300
|
+
if (!amountCol)
|
|
301
|
+
throw new Error("Unable to detect amount column name from header");
|
|
302
|
+
const descCol =
|
|
303
|
+
safe(descriptionColumn ?? undefined) ??
|
|
304
|
+
safe(firstLine.length >= 3 ? firstLine[2] : firstLine[1]);
|
|
305
|
+
if (!descCol)
|
|
306
|
+
throw new Error("Unable to detect description column name from header");
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
date_column: dateCol,
|
|
310
|
+
amount_column: amountCol,
|
|
311
|
+
description_column: descCol,
|
|
312
|
+
date_format: detectDateFormat(linesRaw[1]?.split(delimiter)[0]),
|
|
313
|
+
has_header: hasHeader,
|
|
314
|
+
delimiter: delimiter,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (hasDebitCredit && firstLine.length >= 4) {
|
|
318
|
+
return {
|
|
319
|
+
date_column: 0,
|
|
320
|
+
description_column: 1,
|
|
321
|
+
debit_column: 2,
|
|
322
|
+
credit_column: 3,
|
|
323
|
+
date_format: detectDateFormat(firstLine[0]),
|
|
324
|
+
has_header: hasHeader,
|
|
325
|
+
delimiter: delimiter,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
date_column: 0,
|
|
330
|
+
amount_column: 1,
|
|
331
|
+
description_column: firstLine.length >= 3 ? 2 : 1,
|
|
332
|
+
date_format: detectDateFormat(firstLine[0]),
|
|
333
|
+
has_header: hasHeader,
|
|
334
|
+
delimiter: delimiter,
|
|
335
|
+
};
|
|
317
336
|
}
|
|
318
337
|
|
|
319
338
|
/**
|
|
320
339
|
* Automatically fix common CSV issues like unquoted dates with commas
|
|
321
340
|
*/
|
|
322
341
|
function preprocessCSV(csvContent: string, format: CSVFormat): string {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
342
|
+
// Check if we're dealing with MMM dd, yyyy format dates that might need quoting
|
|
343
|
+
if (
|
|
344
|
+
format.date_format?.includes("MMM") &&
|
|
345
|
+
format.date_format?.includes(",")
|
|
346
|
+
) {
|
|
347
|
+
const lines = csvContent.split("\n");
|
|
348
|
+
const fixedLines = lines.map((line, index) => {
|
|
349
|
+
// Skip header row
|
|
350
|
+
if (format.has_header && index === 0) return line;
|
|
351
|
+
if (!line.trim()) return line;
|
|
352
|
+
|
|
353
|
+
// Check if this line has unquoted dates (more commas than expected)
|
|
354
|
+
const parts = line.split(format.delimiter);
|
|
355
|
+
const expectedColumns = format.has_header
|
|
356
|
+
? lines[0]?.split(format.delimiter).length || 3
|
|
357
|
+
: 3;
|
|
358
|
+
|
|
359
|
+
if (parts.length > expectedColumns) {
|
|
360
|
+
// Check if we have a date pattern split across first two parts (like "Sep 18, 2025")
|
|
361
|
+
const potentialDate = parts.slice(0, 2).join(",");
|
|
362
|
+
if (/^[A-Za-z]{3}\s+\d{1,2},\s+\d{4}/.test(potentialDate)) {
|
|
363
|
+
// This looks like "Sep 18, 2025" - quote it
|
|
364
|
+
const dateField = parts.slice(0, 2).join(","); // "Sep 18, 2025"
|
|
365
|
+
const remainingFields = parts.slice(2);
|
|
366
|
+
return `"${dateField}"${format.delimiter}${remainingFields.join(format.delimiter)}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return line;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return fixedLines.join("\n");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return csvContent;
|
|
353
377
|
}
|
|
354
378
|
|
|
355
379
|
/**
|
|
356
380
|
* Parse CSV data into bank transactions
|
|
357
381
|
*/
|
|
358
382
|
export function parseBankCSV(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
383
|
+
csvContent: string,
|
|
384
|
+
format: CSVFormat,
|
|
385
|
+
options: { debug?: boolean } = {},
|
|
362
386
|
): BankTransaction[] {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
387
|
+
// Preprocess CSV to fix common issues like unquoted dates
|
|
388
|
+
const processedCSV = preprocessCSV(csvContent, format);
|
|
389
|
+
|
|
390
|
+
const records = parse(processedCSV, {
|
|
391
|
+
delimiter: format.delimiter,
|
|
392
|
+
columns: format.has_header,
|
|
393
|
+
skip_empty_lines: true,
|
|
394
|
+
trim: true,
|
|
395
|
+
quote: '"',
|
|
396
|
+
escape: '"',
|
|
397
|
+
relax_column_count: true,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const transactions: BankTransaction[] = [];
|
|
401
|
+
|
|
402
|
+
for (let i = 0; i < records.length; i++) {
|
|
403
|
+
const record = records[i];
|
|
404
|
+
const rowNumber = format.has_header ? i + 2 : i + 1; // Account for header row
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
let rawDate: string;
|
|
408
|
+
let rawAmount: string;
|
|
409
|
+
let description: string;
|
|
410
|
+
|
|
411
|
+
if (format.has_header) {
|
|
412
|
+
// Record is an object when using headers
|
|
413
|
+
const recordObj = record as unknown as Record<string, string>;
|
|
414
|
+
rawDate = recordObj[format.date_column as string] || "";
|
|
415
|
+
|
|
416
|
+
if (format.amount_column) {
|
|
417
|
+
rawAmount = recordObj[format.amount_column as string] || "";
|
|
418
|
+
} else if (
|
|
419
|
+
format.debit_column !== undefined &&
|
|
420
|
+
format.credit_column !== undefined
|
|
421
|
+
) {
|
|
422
|
+
const debitVal = recordObj[format.debit_column as string] || "";
|
|
423
|
+
const creditVal = recordObj[format.credit_column as string] || "";
|
|
424
|
+
// Convert: debits negative, credits positive
|
|
425
|
+
// Check if debit has a value and is non-zero
|
|
426
|
+
const debitNum = Number.parseFloat(debitVal.replace(/[^\d.-]/g, ""));
|
|
427
|
+
const creditNum = Number.parseFloat(
|
|
428
|
+
creditVal.replace(/[^\d.-]/g, ""),
|
|
429
|
+
);
|
|
430
|
+
if (!Number.isNaN(debitNum) && debitNum !== 0) {
|
|
431
|
+
rawAmount = `-${debitVal}`;
|
|
432
|
+
} else if (!Number.isNaN(creditNum) && creditNum !== 0) {
|
|
433
|
+
rawAmount = creditVal;
|
|
434
|
+
} else {
|
|
435
|
+
rawAmount = "0";
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
throw new Error("No amount column configuration found");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
description = recordObj[format.description_column as string] || "";
|
|
442
|
+
} else {
|
|
443
|
+
// Record is an array when not using headers, so use column indices
|
|
444
|
+
const recordArray = record as string[];
|
|
445
|
+
const dateIndex =
|
|
446
|
+
typeof format.date_column === "number"
|
|
447
|
+
? format.date_column
|
|
448
|
+
: Number.parseInt(format.date_column, 10);
|
|
449
|
+
const descIndex =
|
|
450
|
+
typeof format.description_column === "number"
|
|
451
|
+
? format.description_column
|
|
452
|
+
: Number.parseInt(format.description_column, 10);
|
|
453
|
+
|
|
454
|
+
// Validate indices are valid numbers (fallback to defaults if invalid)
|
|
455
|
+
const safeDateIndex = Number.isNaN(dateIndex) ? 0 : dateIndex;
|
|
456
|
+
const safeDescIndex = Number.isNaN(descIndex) ? 2 : descIndex;
|
|
457
|
+
|
|
458
|
+
rawDate = recordArray[safeDateIndex] || "";
|
|
459
|
+
|
|
460
|
+
if (format.amount_column !== undefined) {
|
|
461
|
+
const amountIndex =
|
|
462
|
+
typeof format.amount_column === "number"
|
|
463
|
+
? format.amount_column
|
|
464
|
+
: Number.parseInt(format.amount_column, 10);
|
|
465
|
+
const safeAmountIndex = Number.isNaN(amountIndex) ? 1 : amountIndex;
|
|
466
|
+
rawAmount = recordArray[safeAmountIndex] || "";
|
|
467
|
+
} else if (
|
|
468
|
+
format.debit_column !== undefined &&
|
|
469
|
+
format.credit_column !== undefined
|
|
470
|
+
) {
|
|
471
|
+
const debitIndex =
|
|
472
|
+
typeof format.debit_column === "number"
|
|
473
|
+
? format.debit_column
|
|
474
|
+
: Number.parseInt(format.debit_column, 10);
|
|
475
|
+
const creditIndex =
|
|
476
|
+
typeof format.credit_column === "number"
|
|
477
|
+
? format.credit_column
|
|
478
|
+
: Number.parseInt(format.credit_column, 10);
|
|
479
|
+
|
|
480
|
+
const debitVal = recordArray[debitIndex] || "";
|
|
481
|
+
const creditVal = recordArray[creditIndex] || "";
|
|
482
|
+
|
|
483
|
+
// Convert: debits negative, credits positive
|
|
484
|
+
// Check if debit has a value and is non-zero
|
|
485
|
+
const debitNum = Number.parseFloat(debitVal.replace(/[^\d.-]/g, ""));
|
|
486
|
+
const creditNum = Number.parseFloat(
|
|
487
|
+
creditVal.replace(/[^\d.-]/g, ""),
|
|
488
|
+
);
|
|
489
|
+
if (!Number.isNaN(debitNum) && debitNum !== 0) {
|
|
490
|
+
rawAmount = `-${debitVal}`;
|
|
491
|
+
} else if (!Number.isNaN(creditNum) && creditNum !== 0) {
|
|
492
|
+
rawAmount = creditVal;
|
|
493
|
+
} else {
|
|
494
|
+
rawAmount = "0";
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
throw new Error("No amount column configuration found");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
description = recordArray[safeDescIndex] || "";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!rawDate || !rawAmount) {
|
|
504
|
+
if (options.debug) {
|
|
505
|
+
console.warn(`Skipping row ${rowNumber}: missing date or amount`);
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const date = parseDate(rawDate, format.date_format);
|
|
511
|
+
let amount: Milli;
|
|
512
|
+
try {
|
|
513
|
+
amount = amountToMilliunits(rawAmount);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (options.debug) {
|
|
516
|
+
console.warn(
|
|
517
|
+
`Skipping row ${rowNumber}: ${error instanceof Error ? error.message : "Invalid amount"}`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
transactions.push({
|
|
524
|
+
date,
|
|
525
|
+
amount,
|
|
526
|
+
description: description.trim(),
|
|
527
|
+
raw_amount: rawAmount,
|
|
528
|
+
raw_date: rawDate,
|
|
529
|
+
row_number: rowNumber,
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (options.debug) {
|
|
533
|
+
console.warn(`Error parsing row ${rowNumber}:`, error);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return transactions;
|
|
509
539
|
}
|
|
510
540
|
|
|
511
541
|
/**
|
|
512
542
|
* Read CSV file safely with error handling
|
|
513
543
|
*/
|
|
514
544
|
export function readCSVFile(filePath: string): string {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
545
|
+
try {
|
|
546
|
+
return readFileSync(filePath, "utf-8");
|
|
547
|
+
} catch (error) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
`Unable to read CSV file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
522
552
|
}
|
|
523
553
|
|
|
524
554
|
/**
|
|
@@ -526,32 +556,32 @@ export function readCSVFile(filePath: string): string {
|
|
|
526
556
|
* Returns min and max dates in YYYY-MM-DD format
|
|
527
557
|
*/
|
|
528
558
|
export function extractDateRangeFromCSV(
|
|
529
|
-
|
|
530
|
-
|
|
559
|
+
csvContent: string,
|
|
560
|
+
format: CSVFormat,
|
|
531
561
|
): { minDate: string; maxDate: string } {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
562
|
+
const transactions = parseBankCSV(csvContent, format);
|
|
563
|
+
|
|
564
|
+
if (transactions.length === 0) {
|
|
565
|
+
throw new Error("No transactions found in CSV");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Extract all dates (already Date objects from parseBankCSV)
|
|
569
|
+
const dates = transactions.map((txn) => txn.date.getTime());
|
|
570
|
+
|
|
571
|
+
// Find min and max
|
|
572
|
+
const minDateObj = new Date(Math.min(...dates));
|
|
573
|
+
const maxDateObj = new Date(Math.max(...dates));
|
|
574
|
+
|
|
575
|
+
// Convert to YYYY-MM-DD format
|
|
576
|
+
const toYYYYMMDD = (date: Date): string => {
|
|
577
|
+
const year = date.getFullYear();
|
|
578
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
579
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
580
|
+
return `${year}-${month}-${day}`;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
minDate: toYYYYMMDD(minDateObj),
|
|
585
|
+
maxDate: toYYYYMMDD(maxDateObj),
|
|
586
|
+
};
|
|
557
587
|
}
|