india2actual 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.env.example +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +280 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +295 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/env-file.d.ts +38 -0
  8. package/dist/env-file.js +40 -0
  9. package/dist/env-file.js.map +1 -0
  10. package/dist/extract/csv.d.ts +13 -0
  11. package/dist/extract/csv.js +60 -0
  12. package/dist/extract/csv.js.map +1 -0
  13. package/dist/extract/html-table.d.ts +3 -0
  14. package/dist/extract/html-table.js +77 -0
  15. package/dist/extract/html-table.js.map +1 -0
  16. package/dist/extract/index.d.ts +21 -0
  17. package/dist/extract/index.js +44 -0
  18. package/dist/extract/index.js.map +1 -0
  19. package/dist/extract/pdf.d.ts +47 -0
  20. package/dist/extract/pdf.js +439 -0
  21. package/dist/extract/pdf.js.map +1 -0
  22. package/dist/extract/sniff.d.ts +12 -0
  23. package/dist/extract/sniff.js +57 -0
  24. package/dist/extract/sniff.js.map +1 -0
  25. package/dist/extract/spreadsheetml.d.ts +3 -0
  26. package/dist/extract/spreadsheetml.js +93 -0
  27. package/dist/extract/spreadsheetml.js.map +1 -0
  28. package/dist/extract/types.d.ts +20 -0
  29. package/dist/extract/types.js +2 -0
  30. package/dist/extract/types.js.map +1 -0
  31. package/dist/extract/xlsx.d.ts +2 -0
  32. package/dist/extract/xlsx.js +64 -0
  33. package/dist/extract/xlsx.js.map +1 -0
  34. package/dist/interpret/header.d.ts +24 -0
  35. package/dist/interpret/header.js +57 -0
  36. package/dist/interpret/header.js.map +1 -0
  37. package/dist/interpret/roundtrip.d.ts +29 -0
  38. package/dist/interpret/roundtrip.js +89 -0
  39. package/dist/interpret/roundtrip.js.map +1 -0
  40. package/dist/interpret/rows.d.ts +54 -0
  41. package/dist/interpret/rows.js +151 -0
  42. package/dist/interpret/rows.js.map +1 -0
  43. package/dist/interpret/synonyms.d.ts +27 -0
  44. package/dist/interpret/synonyms.js +55 -0
  45. package/dist/interpret/synonyms.js.map +1 -0
  46. package/dist/interpret/validate.d.ts +27 -0
  47. package/dist/interpret/validate.js +93 -0
  48. package/dist/interpret/validate.js.map +1 -0
  49. package/dist/interpret/values.d.ts +22 -0
  50. package/dist/interpret/values.js +131 -0
  51. package/dist/interpret/values.js.map +1 -0
  52. package/dist/merchants-file.d.ts +14 -0
  53. package/dist/merchants-file.js +40 -0
  54. package/dist/merchants-file.js.map +1 -0
  55. package/dist/narration/merchants.d.ts +46 -0
  56. package/dist/narration/merchants.js +161 -0
  57. package/dist/narration/merchants.js.map +1 -0
  58. package/dist/narration/parse.d.ts +14 -0
  59. package/dist/narration/parse.js +659 -0
  60. package/dist/narration/parse.js.map +1 -0
  61. package/dist/narration/types.d.ts +25 -0
  62. package/dist/narration/types.js +2 -0
  63. package/dist/narration/types.js.map +1 -0
  64. package/dist/out/csv.d.ts +21 -0
  65. package/dist/out/csv.js +40 -0
  66. package/dist/out/csv.js.map +1 -0
  67. package/dist/out/push.d.ts +74 -0
  68. package/dist/out/push.js +103 -0
  69. package/dist/out/push.js.map +1 -0
  70. package/package.json +65 -0
package/.env.example ADDED
@@ -0,0 +1,23 @@
1
+ # Real environment variables take precedence over this file, so a one-off
2
+ # override still works:
3
+ #
4
+ # ACTUAL_SYNC_ID=other-budget npx tsx src/cli.ts statement.pdf --push ...
5
+ #
6
+ # None of this is needed to convert a statement to CSV.
7
+
8
+ # --- Encrypted statement PDFs ------------------------------------------------
9
+ STATEMENT_PASSWORD=
10
+
11
+ # --- Pushing into Actual (--push) --------------------------------------------
12
+ # Your Actual sync server, and the password used to sign in to it
13
+ ACTUAL_SERVER_URL=
14
+ ACTUAL_PASSWORD=
15
+
16
+ # The budget to import into: Settings > Advanced > Sync ID
17
+ ACTUAL_SYNC_ID=
18
+
19
+ # Only if this budget uses end-to-end encryption
20
+ ACTUAL_ENCRYPTION_PASSWORD=
21
+
22
+ # Where the API keeps its local copy of the budget. Defaults to ./.actual-cache
23
+ ACTUAL_DATA_DIR=
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emil George
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # india2actual
2
+
3
+ Convert Indian bank statements into [Actual Budget](https://actualbudget.org)
4
+ transactions, with **real merchant names instead of UPI reference strings**.
5
+
6
+ CSV, Excel, HTML-disguised `.xls` and PDF input all work. Every amount is
7
+ cross-checked against the statement's own balance column.
8
+
9
+ ## The problem this solves
10
+
11
+ Actual's CSV importer already handles Indian statements well: lakh grouping
12
+ (`1,23,456.78`), `DD/MM/YYYY` dates and separate Withdrawal/Deposit columns all
13
+ work, and it remembers your column mapping per account.
14
+
15
+ The gap is the narration column, which embeds a unique reference in every
16
+ transaction:
17
+
18
+ ```
19
+ UPI/DR/412345678901/SWIGGY/YESB/swiggy@ybl/Payment
20
+ UPI/DR/419876543210/SWIGGY/YESB/swiggy@ybl/Payment
21
+ ```
22
+
23
+ Map that to Payee and the same merchant becomes two payees. Within months you
24
+ have hundreds of junk payees, spending-by-payee is meaningless, and category
25
+ learning has nothing to work with.
26
+
27
+ This tool reads the structure inside the narration:
28
+
29
+ ```
30
+ UPI/DR/412345678901/SWIGGY/YESB/swiggy@ybl/Payment
31
+ reference name VPA
32
+ ```
33
+
34
+ and produces something importable:
35
+
36
+ | Date | Payee | Notes | Amount |
37
+ | ---------- | -------------- | ---------------------------------------------------- | -------- |
38
+ | 2024-04-01 | Swiggy | `UPI/DR/412345678901/SWIGGY/YESB/swiggy@ybl/Payment` | -450.50 |
39
+ | 2024-04-03 | ATM Withdrawal | `ATW/1234/CASH WDL/BANGALORE` | -2000.00 |
40
+ | 2024-04-04 | DMart | `POS 1234XXXX5678 DMART BANGALORE` | -3250.75 |
41
+ | 2024-04-05 | John Doe | `UPI/412345678903/JOHN DOE/johndoe@oksbi` | 1500.00 |
42
+
43
+ The original narration is always preserved, never discarded.
44
+
45
+ ## Install
46
+
47
+ Needs Node 22 or newer. Nothing to install: `npx` fetches it on first use.
48
+
49
+ ```bash
50
+ npx india2actual statement.csv
51
+ ```
52
+
53
+ Install it properly if you run it often:
54
+
55
+ ```bash
56
+ npm install -g india2actual
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ```bash
62
+ india2actual statement.csv # writes statement.actual.csv
63
+ india2actual statement.pdf --stdout # preview without writing
64
+ ```
65
+
66
+ Then import the generated CSV through Actual's **Import transactions** dialog,
67
+ mapping `Date`, `Payee`, `Notes` and `Amount`. Actual remembers that mapping per
68
+ account, so you only do it once. Leave `Reference` unmapped.
69
+
70
+ Or skip the CSV and [push straight into Actual](#pushing-straight-into-actual).
71
+
72
+ ### Supported input
73
+
74
+ The format is detected by inspecting the file, not by its extension, because
75
+ banks routinely name an HTML table `.xls`.
76
+
77
+ | Actually is | Supported |
78
+ | ----------------------------------- | ----------------------------- |
79
+ | CSV / TSV (delimiter auto-detected) | yes |
80
+ | Excel `.xlsx` (OOXML) | yes |
81
+ | HTML table named `.xls` | yes |
82
+ | Excel 2003 XML (SpreadsheetML) | yes |
83
+ | PDF, including password-protected | yes |
84
+ | Legacy binary `.xls` (OLE2) | no, re-save as `.xlsx` or CSV |
85
+
86
+ Columns are detected from the header row, using the synonyms Indian banks use
87
+ for the same seven fields (date, narration, debit, credit, amount, balance,
88
+ reference). Preamble and footer rows are skipped automatically.
89
+
90
+ ### Options
91
+
92
+ | Option | Purpose |
93
+ | ---------------------------- | ------------------------------------------------------------------------------- |
94
+ | `--out <path>` | Where to write the CSV. Default `<input>.actual.csv`. |
95
+ | `--stdout` | Write to stdout instead of a file. |
96
+ | `--date-order dmy\|mdy\|ymd` | Only affects all-numeric dates, where `01/02/2024` is ambiguous. Default `dmy`. |
97
+ | `--delimiter <char>` | Force the CSV delimiter instead of detecting it. |
98
+ | `--merchants <path>` | Your own merchant rules. See [below](#custom-merchant-rules). |
99
+ | `--env-file <path>` | Read settings from this file instead of `./.env`. |
100
+ | `--push` | Send to Actual via its API instead of writing a CSV. |
101
+ | `--account <name\|id>` | Which Actual account to import into. Required with `--push`. |
102
+ | `--dry-run` | With `--push`, report what would change without writing. |
103
+ | `--force` | Write even if the balance check fails. |
104
+ | `--quiet` | Only report problems. |
105
+
106
+ ### Settings
107
+
108
+ Passwords and server details come from a `.env` file in the current directory,
109
+ never from command-line flags, which would end up in your shell history.
110
+
111
+ ```bash
112
+ cp .env.example .env
113
+ ```
114
+
115
+ Real environment variables take precedence over the file, so a one-off override
116
+ works without editing it:
117
+
118
+ ```bash
119
+ ACTUAL_SYNC_ID=other-budget india2actual statement.pdf --push --account Savings
120
+ ```
121
+
122
+ | Setting | Purpose |
123
+ | ---------------------------- | ----------------------------------------------------- |
124
+ | `STATEMENT_PASSWORD` | Password on an encrypted statement PDF. |
125
+ | `ACTUAL_SERVER_URL` | Your Actual sync server. |
126
+ | `ACTUAL_PASSWORD` | Password for that server. Not the statement password. |
127
+ | `ACTUAL_SYNC_ID` | Budget to import into: Settings > Advanced > Sync ID. |
128
+ | `ACTUAL_ENCRYPTION_PASSWORD` | Only for end-to-end encrypted budgets. |
129
+ | `ACTUAL_DATA_DIR` | Local budget cache. Default `./.actual-cache`. |
130
+
131
+ None of this is needed to convert a statement to CSV, unencrypted PDFs
132
+ included.
133
+
134
+ ## Pushing straight into Actual
135
+
136
+ ```bash
137
+ # Always preview first.
138
+ india2actual statement.pdf --push --account "ICICI Savings" --dry-run
139
+ # [dry run] ICICI Savings: would add 34, would update 0.
140
+
141
+ india2actual statement.pdf --push --account "ICICI Savings"
142
+ ```
143
+
144
+ `--dry-run` maps onto Actual's own preview mode, so nothing is written.
145
+
146
+ The API path does two things the CSV path cannot:
147
+
148
+ - Sets `imported_payee` to the raw narration, which is what Actual's payee
149
+ matching learns from, while `payee_name` gets the cleaned merchant. The CSV
150
+ field mapping has no `imported_payee` slot.
151
+ - Sets `imported_id` from the bank reference, making re-imports of overlapping
152
+ date ranges idempotent.
153
+
154
+ ### Review before you push
155
+
156
+ The tool reads its own output, so you can check and correct the CSV before
157
+ anything reaches your budget:
158
+
159
+ ```bash
160
+ india2actual statement.pdf
161
+ $EDITOR statement.actual.csv
162
+ india2actual statement.actual.csv --push --account "ICICI Savings"
163
+ ```
164
+
165
+ Payees you edited are kept verbatim; converted output is passed through, not
166
+ re-parsed. The `Reference` column survives, so deduplication still works.
167
+
168
+ One caveat: the CSV has no balance column, so a run over converted output
169
+ cannot re-verify the amounts and will say so. The check that matters already ran
170
+ when the CSV was produced.
171
+
172
+ ## The balance check
173
+
174
+ Almost every Indian statement carries a running balance, which makes the parse
175
+ self-verifying: each transaction must equal the change in balance it caused. The
176
+ tool **refuses to write a statement that does not reconcile**:
177
+
178
+ ```
179
+ Balance check FAILED (4/5 rows agree).
180
+ 1 of 5 rows do not agree with the balance column.
181
+ 2024-04-03 "ATW/1234/CASH WDL/BANGALORE": balance moved by -2000.00 but the parsed amount is -9000.00
182
+ Refusing to write a statement that does not reconcile. Re-run with --force to write it anyway.
183
+ ```
184
+
185
+ This catches inverted debit and credit signs, dropped rows and misaligned
186
+ columns. Both date orders are scored, so newest-first exports work too. It
187
+ matters most for PDFs, where the table is reconstructed from the position of
188
+ each piece of text and so is inherently less certain.
189
+
190
+ ## Custom merchant rules
191
+
192
+ The built-in map covers common Indian merchants. Add your own in JSON:
193
+
194
+ ```json
195
+ [
196
+ { "pattern": "^mylocalkirana", "name": "Kirana Store" },
197
+ { "pattern": "^acmecorp", "name": "Acme Payroll" }
198
+ ]
199
+ ```
200
+
201
+ ```bash
202
+ india2actual statement.csv --merchants my-merchants.json
203
+ ```
204
+
205
+ Patterns match a lowercased, punctuation-stripped form of the VPA local-part or
206
+ merchant name, so write them without spaces or dots. Your rules take precedence
207
+ over the built-ins.
208
+
209
+ **Naming recurring mandates.** A NACH narration contains no name, only the
210
+ collecting bank and a mandate reference, followed by a sequence number that
211
+ changes every month. Those collections are grouped under the stable mandate
212
+ reference, for example `NACH ICIC0000000000000001`, so name each one once:
213
+
214
+ ```json
215
+ [{ "pattern": "icic0000000000000001", "name": "Home Loan EMI" }]
216
+ ```
217
+
218
+ **Truncated name fields.** Some banks cut each narration field to about ten
219
+ characters, so one counterparty can arrive as `Mr A N OTHE`, `A N OTHER` or
220
+ `OTHER` depending on the payment route. A rule per variant collapses them.
221
+
222
+ ## Duplicate handling
223
+
224
+ A 12-digit UPI reference (UTR or RRN) found in the narration is emitted in the
225
+ `Reference` column, and becomes Actual's `imported_id` with `--push`.
226
+
227
+ References are deliberately conservative: only exactly-12-digit values are
228
+ accepted, and any value occurring more than once in a file is discarded. Account
229
+ and card numbers appear in narrations at other lengths and are not unique per
230
+ transaction, and a repeated `imported_id` makes Actual treat distinct
231
+ transactions as the same one and drop them. Where no reference is set, Actual's
232
+ own date and amount matching takes over.
233
+
234
+ ## Getting statements out of your bank
235
+
236
+ Prefer internet banking over the mobile app. Bank apps often offer only PDF,
237
+ while the web portal usually offers XLS or CSV for the same account. CSV is the
238
+ most reliable input here and PDF the least, so use a spreadsheet if offered one.
239
+
240
+ ## Limitations
241
+
242
+ - **PDF is the least reliable path.** Reconstructing a table from positioned
243
+ text can break when a bank changes its template. The balance check exists to
244
+ make that loud rather than silent.
245
+ - **Line breaks inside a PDF are ambiguous.** A wrap and a deliberate break are
246
+ not always distinguishable, so a space can appear inside a long reference, or
247
+ be lost between two words. Dates, amounts, payees and the deduplication
248
+ reference do not depend on it.
249
+ - **The merchant map is not authoritative.** Payment aggregators often mask the
250
+ real merchant. You get a consistent payee, which beats one per transaction,
251
+ but not always the actual shop.
252
+ - **No automatic fetching, and there cannot be.** India's Account Aggregator
253
+ framework requires FIU registration and a commercial contract, so a free
254
+ always-on connector like Actual's European bank sync is not possible. This is
255
+ a converter you run on a statement you downloaded.
256
+
257
+ ## Contributing
258
+
259
+ Adding a bank usually means adding column synonyms or narration token shapes,
260
+ both small and contained. Narration strings are the most useful thing to
261
+ contribute: the description column only, with names, account numbers and
262
+ references replaced by realistic fakes.
263
+
264
+ No real statement data belongs in this repository. Fixtures are synthetic.
265
+
266
+ ## Development
267
+
268
+ ```bash
269
+ git clone https://github.com/emilgeo/india2actual.git
270
+ cd india2actual
271
+ npm install
272
+
273
+ npm test
274
+ npm run typecheck
275
+ npm run dev -- statement.csv # run the CLI from source
276
+ ```
277
+
278
+ ## License
279
+
280
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env node
2
+ import { basename, dirname, extname, join } from 'node:path';
3
+ import { argv, cwd, exit, stderr, stdout } from 'node:process';
4
+ import { loadEnvironmentFile, setting } from './env-file.js';
5
+ import { describeFormat, extractTable } from './extract/index.js';
6
+ import { interpretConvertedOutput, isConvertedOutput, } from './interpret/roundtrip.js';
7
+ import { interpretTable } from './interpret/rows.js';
8
+ import { validateBalances } from './interpret/validate.js';
9
+ import { loadMerchantRules } from './merchants-file.js';
10
+ import { toCsv, writeCsv } from './out/csv.js';
11
+ import { pushTransactions } from './out/push.js';
12
+ const USAGE = `
13
+ india2actual — convert Indian bank statements for Actual Budget
14
+
15
+ Usage:
16
+ india2actual <statement-file> [options]
17
+
18
+ Options:
19
+ --out <path> Where to write the normalised CSV.
20
+ Default: <input>.actual.csv next to the input file.
21
+ --stdout Write the CSV to stdout instead of a file.
22
+ --date-order <order> dmy (default), mdy or ymd. Only affects all-numeric
23
+ dates, where 01/02/2024 is genuinely ambiguous.
24
+ --delimiter <char> Force the CSV delimiter instead of detecting it.
25
+ --merchants <path> JSON file of { pattern, name } merchant rules, which
26
+ take precedence over the built-in map.
27
+ --env-file <path> Read settings from this file instead of ./.env.
28
+ --force Write the CSV even if the balance check fails.
29
+ --quiet Only report problems.
30
+ --help Show this message.
31
+
32
+ Pushing straight into Actual (instead of writing a CSV):
33
+ --push Send the transactions to Actual via its API.
34
+ --account <name|id> Which Actual account to import into. Required for --push.
35
+ --dry-run With --push, report what would change without writing.
36
+
37
+ --push needs the API package: npm install @actual-app/api
38
+
39
+ Settings come from a .env file in the current directory, or from real
40
+ environment variables, which take precedence. Never from flags, which would
41
+ end up in your shell history. Copy .env.example to .env to get started.
42
+
43
+ ACTUAL_SERVER_URL e.g. https://actual.example.com
44
+ ACTUAL_PASSWORD your Actual server password
45
+ ACTUAL_SYNC_ID the budget's sync id (Settings > Advanced)
46
+ ACTUAL_ENCRYPTION_PASSWORD only if the budget file is encrypted
47
+ ACTUAL_DATA_DIR local cache dir (default: ./.actual-cache)
48
+ STATEMENT_PASSWORD password for an encrypted statement PDF
49
+ (not the same as ACTUAL_PASSWORD)
50
+
51
+ Input is detected by content, not by extension, because banks routinely name
52
+ HTML tables ".xls". Supported: CSV/TSV, Excel .xlsx, HTML tables, Excel 2003
53
+ XML, and PDF (including password-protected). Legacy binary .xls must be
54
+ re-saved as .xlsx or .csv first.
55
+ `.trim();
56
+ function parseArgs(args) {
57
+ if (!args.length || args.includes('--help') || args.includes('-h')) {
58
+ return null;
59
+ }
60
+ const options = {
61
+ input: '',
62
+ useStdout: false,
63
+ dateOrder: 'dmy',
64
+ force: false,
65
+ quiet: false,
66
+ push: false,
67
+ dryRun: false,
68
+ };
69
+ for (let index = 0; index < args.length; index += 1) {
70
+ const arg = args[index];
71
+ const next = () => {
72
+ const value = args[index + 1];
73
+ if (value === undefined || value.startsWith('--')) {
74
+ throw new Error(`${arg} needs a value`);
75
+ }
76
+ index += 1;
77
+ return value;
78
+ };
79
+ switch (arg) {
80
+ case '--out':
81
+ options.out = next();
82
+ break;
83
+ case '--stdout':
84
+ options.useStdout = true;
85
+ break;
86
+ case '--date-order': {
87
+ const value = next();
88
+ if (value !== 'dmy' && value !== 'mdy' && value !== 'ymd') {
89
+ throw new Error('--date-order must be dmy, mdy or ymd');
90
+ }
91
+ options.dateOrder = value;
92
+ break;
93
+ }
94
+ case '--delimiter':
95
+ options.delimiter = next();
96
+ break;
97
+ case '--merchants':
98
+ options.merchants = next();
99
+ break;
100
+ case '--env-file':
101
+ options.envFile = next();
102
+ break;
103
+ case '--push':
104
+ options.push = true;
105
+ break;
106
+ case '--account':
107
+ options.account = next();
108
+ break;
109
+ case '--dry-run':
110
+ options.dryRun = true;
111
+ break;
112
+ case '--force':
113
+ options.force = true;
114
+ break;
115
+ case '--quiet':
116
+ options.quiet = true;
117
+ break;
118
+ default:
119
+ if (arg === undefined || arg.startsWith('-')) {
120
+ throw new Error(`Unknown option: ${arg}`);
121
+ }
122
+ if (options.input) {
123
+ throw new Error('Only one input file at a time');
124
+ }
125
+ options.input = arg;
126
+ }
127
+ }
128
+ if (!options.input) {
129
+ throw new Error('No input file given');
130
+ }
131
+ if (options.push && !options.account) {
132
+ throw new Error('--push also needs --account <name|id>');
133
+ }
134
+ if (options.dryRun && !options.push) {
135
+ throw new Error('--dry-run only applies to --push');
136
+ }
137
+ return options;
138
+ }
139
+ function defaultOutPath(input) {
140
+ const extension = extname(input);
141
+ const name = basename(input, extension);
142
+ return join(dirname(input), `${name}.actual.csv`);
143
+ }
144
+ async function run(args) {
145
+ const options = parseArgs(args);
146
+ if (!options) {
147
+ stdout.write(`${USAGE}\n`);
148
+ return 0;
149
+ }
150
+ const log = (message) => {
151
+ if (!options.quiet) {
152
+ stderr.write(`${message}\n`);
153
+ }
154
+ };
155
+ // Before anything reads the environment. Both the statement password and the
156
+ // Actual credentials can come from here.
157
+ const envFile = loadEnvironmentFile(options.envFile ?? join(cwd(), '.env'));
158
+ if (envFile.loaded) {
159
+ log(`Loaded environment from ${envFile.path}`);
160
+ }
161
+ else if (options.envFile) {
162
+ // Explicitly asked for, so silence would be wrong.
163
+ throw new Error(`${options.envFile} does not exist`);
164
+ }
165
+ let merchantRules = [];
166
+ if (options.merchants) {
167
+ merchantRules = await loadMerchantRules(options.merchants);
168
+ log(`Loaded ${merchantRules.length} merchant rule(s) from ${options.merchants}`);
169
+ }
170
+ // Resolved before any parsing work so a missing credential or account fails
171
+ // immediately rather than after processing the whole statement.
172
+ const pushConfig = options.push ? pushConfigFromEnv(options) : null;
173
+ // From .env or the environment, never a flag, so it stays out of shell
174
+ // history.
175
+ const pdfPassword = setting('STATEMENT_PASSWORD');
176
+ const { table, format } = await extractTable(options.input, {
177
+ ...(options.delimiter ? { delimiter: options.delimiter } : {}),
178
+ ...(pdfPassword ? { password: pdfPassword } : {}),
179
+ });
180
+ log(`Read ${options.input} as ${describeFormat(format)}`);
181
+ // Our own output is passed through rather than re-parsed, so that payees
182
+ // corrected by hand in the CSV survive.
183
+ const converted = isConvertedOutput(table);
184
+ if (converted) {
185
+ log('Recognised this as already-converted output: payees kept as-is, ' +
186
+ 'narrations kept in Notes.');
187
+ }
188
+ const result = converted
189
+ ? interpretConvertedOutput(table)
190
+ : interpretTable(table, {
191
+ dateOrder: options.dateOrder,
192
+ ...(merchantRules.length ? { merchantRules } : {}),
193
+ });
194
+ if (!result) {
195
+ stderr.write('Could not find a transaction table in this file.\n' +
196
+ 'Expected columns resembling: Date, Narration/Particulars, ' +
197
+ 'Withdrawal/Debit, Deposit/Credit, Balance.\n');
198
+ return 1;
199
+ }
200
+ const { transactions, skipped, header, droppedRefs } = result;
201
+ log(`Header on row ${header.index + 1}; columns: ${Object.entries(header.map)
202
+ .map(([role, column]) => `${role}=${column}`)
203
+ .join(', ')}`);
204
+ log(`Parsed ${transactions.length} transaction(s), skipped ${skipped.length} row(s)`);
205
+ if (droppedRefs) {
206
+ log(`Discarded ${droppedRefs} non-unique reference(s); those rows will rely ` +
207
+ `on Actual's date and amount matching instead.`);
208
+ }
209
+ if (!transactions.length) {
210
+ stderr.write('No transactions were parsed — nothing to write.\n');
211
+ return 1;
212
+ }
213
+ // The balance column is the only independent check we have that the parse is
214
+ // right, so a failure blocks the write unless explicitly overridden.
215
+ const validation = validateBalances(transactions);
216
+ if (validation.status === 'failed') {
217
+ stderr.write(`Balance check FAILED (${validation.matched}/${validation.checked} rows agree).\n`);
218
+ for (const issue of validation.issues) {
219
+ stderr.write(` ${issue}\n`);
220
+ }
221
+ if (!options.force) {
222
+ stderr.write('Refusing to write a statement that does not reconcile. ' +
223
+ 'Re-run with --force to write it anyway.\n');
224
+ return 2;
225
+ }
226
+ stderr.write('Writing anyway because --force was given.\n');
227
+ }
228
+ else if (validation.status === 'passed') {
229
+ log(`Balance check passed (${validation.matched}/${validation.checked} rows, ` +
230
+ `${validation.order} order).`);
231
+ }
232
+ else {
233
+ log(`Balance check skipped: ${validation.issues[0] ?? 'no balance data'}`);
234
+ if (converted) {
235
+ // Said plainly, because it is the one real cost of the round trip: the
236
+ // CSV carries no balance column, so these amounts are not independently
237
+ // verified here. They were when the CSV was produced.
238
+ log('Converted output carries no balance column, so this run cannot ' +
239
+ 're-verify the amounts. Check the balance line from the run that ' +
240
+ 'produced this CSV.');
241
+ }
242
+ }
243
+ if (pushConfig) {
244
+ const result = await pushTransactions(transactions, pushConfig);
245
+ const verb = result.dryRun ? 'would add' : 'added';
246
+ const alsoVerb = result.dryRun ? 'would update' : 'updated';
247
+ stderr.write(`${result.dryRun ? '[dry run] ' : ''}${result.accountName}: ` +
248
+ `${verb} ${result.added}, ${alsoVerb} ${result.updated}.\n`);
249
+ for (const error of result.errors) {
250
+ stderr.write(` error: ${error}\n`);
251
+ }
252
+ return result.errors.length ? 1 : 0;
253
+ }
254
+ if (options.useStdout) {
255
+ stdout.write(toCsv(transactions));
256
+ return 0;
257
+ }
258
+ const outPath = options.out ?? defaultOutPath(options.input);
259
+ await writeCsv(outPath, transactions);
260
+ log(`Wrote ${outPath}`);
261
+ return 0;
262
+ }
263
+ /**
264
+ * Build the push configuration from the environment.
265
+ *
266
+ * Credentials are read from the environment rather than accepted as flags so
267
+ * they do not end up in shell history or process listings.
268
+ */
269
+ function pushConfigFromEnv(options) {
270
+ if (!options.account) {
271
+ throw new Error('--push also needs --account <name|id>');
272
+ }
273
+ const missing = ['ACTUAL_SERVER_URL', 'ACTUAL_PASSWORD', 'ACTUAL_SYNC_ID'].filter(name => !setting(name));
274
+ if (missing.length) {
275
+ throw new Error(`--push needs these settings: ${missing.join(', ')}\n` +
276
+ 'Set them in .env or in the environment. See --help for the full list.');
277
+ }
278
+ const encryptionPassword = setting('ACTUAL_ENCRYPTION_PASSWORD');
279
+ return {
280
+ serverURL: setting('ACTUAL_SERVER_URL'),
281
+ password: setting('ACTUAL_PASSWORD'),
282
+ syncId: setting('ACTUAL_SYNC_ID'),
283
+ dataDir: setting('ACTUAL_DATA_DIR') ?? join(cwd(), '.actual-cache'),
284
+ account: options.account,
285
+ dryRun: options.dryRun,
286
+ ...(encryptionPassword ? { encryptionPassword } : {}),
287
+ };
288
+ }
289
+ run(argv.slice(2))
290
+ .then(code => exit(code))
291
+ .catch((error) => {
292
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
293
+ exit(1);
294
+ });
295
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Load a `.env` file into the environment.
3
+ *
4
+ * Uses Node's built-in loader rather than a dependency, which also fixes the
5
+ * precedence at the behaviour we want: variables already present in the
6
+ * environment are *not* overwritten. So a `.env` supplies the defaults while a
7
+ * one-off `ACTUAL_SYNC_ID=other india2actual ...` still wins, and CI can
8
+ * inject secrets without a file existing at all.
9
+ *
10
+ * Requires Node >= 22 (see `engines` in package.json).
11
+ */
12
+ export type EnvFileResult = {
13
+ loaded: true;
14
+ path: string;
15
+ } | {
16
+ loaded: false;
17
+ path: string;
18
+ reason: 'missing';
19
+ };
20
+ /**
21
+ * A missing file is not an error: the CSV path needs no configuration, so most
22
+ * runs legitimately have no `.env` at all. Anything else — unreadable, a
23
+ * directory, malformed contents — is reported, because it means the user
24
+ * believes they have configured something that is in fact being ignored. That
25
+ * would otherwise surface much later as a confusing "ACTUAL_SERVER_URL is not
26
+ * set".
27
+ */
28
+ export declare function loadEnvironmentFile(path: string): EnvFileResult;
29
+ /**
30
+ * Read a setting, treating blank as absent.
31
+ *
32
+ * `.env.example` ships its keys with empty values for the user to fill in, so
33
+ * a copied-but-unedited file leaves `ACTUAL_DATA_DIR=` set to the empty
34
+ * string. The empty string is not nullish, so a `??` default would not fire
35
+ * and the Actual API would be handed `''` as its data directory. Trimming also
36
+ * catches the trailing space left by `ACTUAL_SYNC_ID= ` .
37
+ */
38
+ export declare function setting(name: string): string | undefined;