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.
- package/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +280 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/cli.js.map +1 -0
- package/dist/env-file.d.ts +38 -0
- package/dist/env-file.js +40 -0
- package/dist/env-file.js.map +1 -0
- package/dist/extract/csv.d.ts +13 -0
- package/dist/extract/csv.js +60 -0
- package/dist/extract/csv.js.map +1 -0
- package/dist/extract/html-table.d.ts +3 -0
- package/dist/extract/html-table.js +77 -0
- package/dist/extract/html-table.js.map +1 -0
- package/dist/extract/index.d.ts +21 -0
- package/dist/extract/index.js +44 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/extract/pdf.d.ts +47 -0
- package/dist/extract/pdf.js +439 -0
- package/dist/extract/pdf.js.map +1 -0
- package/dist/extract/sniff.d.ts +12 -0
- package/dist/extract/sniff.js +57 -0
- package/dist/extract/sniff.js.map +1 -0
- package/dist/extract/spreadsheetml.d.ts +3 -0
- package/dist/extract/spreadsheetml.js +93 -0
- package/dist/extract/spreadsheetml.js.map +1 -0
- package/dist/extract/types.d.ts +20 -0
- package/dist/extract/types.js +2 -0
- package/dist/extract/types.js.map +1 -0
- package/dist/extract/xlsx.d.ts +2 -0
- package/dist/extract/xlsx.js +64 -0
- package/dist/extract/xlsx.js.map +1 -0
- package/dist/interpret/header.d.ts +24 -0
- package/dist/interpret/header.js +57 -0
- package/dist/interpret/header.js.map +1 -0
- package/dist/interpret/roundtrip.d.ts +29 -0
- package/dist/interpret/roundtrip.js +89 -0
- package/dist/interpret/roundtrip.js.map +1 -0
- package/dist/interpret/rows.d.ts +54 -0
- package/dist/interpret/rows.js +151 -0
- package/dist/interpret/rows.js.map +1 -0
- package/dist/interpret/synonyms.d.ts +27 -0
- package/dist/interpret/synonyms.js +55 -0
- package/dist/interpret/synonyms.js.map +1 -0
- package/dist/interpret/validate.d.ts +27 -0
- package/dist/interpret/validate.js +93 -0
- package/dist/interpret/validate.js.map +1 -0
- package/dist/interpret/values.d.ts +22 -0
- package/dist/interpret/values.js +131 -0
- package/dist/interpret/values.js.map +1 -0
- package/dist/merchants-file.d.ts +14 -0
- package/dist/merchants-file.js +40 -0
- package/dist/merchants-file.js.map +1 -0
- package/dist/narration/merchants.d.ts +46 -0
- package/dist/narration/merchants.js +161 -0
- package/dist/narration/merchants.js.map +1 -0
- package/dist/narration/parse.d.ts +14 -0
- package/dist/narration/parse.js +659 -0
- package/dist/narration/parse.js.map +1 -0
- package/dist/narration/types.d.ts +25 -0
- package/dist/narration/types.js +2 -0
- package/dist/narration/types.js.map +1 -0
- package/dist/out/csv.d.ts +21 -0
- package/dist/out/csv.js +40 -0
- package/dist/out/csv.js.map +1 -0
- package/dist/out/push.d.ts +74 -0
- package/dist/out/push.js +103 -0
- package/dist/out/push.js.map +1 -0
- 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
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
|
package/dist/cli.js.map
ADDED
|
@@ -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;
|