israeli-banks-actual-budget-importer 1.9.0 → 1.10.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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.10.0](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/compare/v1.9.0...v1.10.0) (2026-01-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * enhance reconciliation options and update configuration schema ([2d873a5](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/commit/2d873a542915be3d35ac7ab36003d4617d6256c4))
7
+
1
8
  # [1.9.0](https://github.com/tomerh2001/israeli-banks-actual-budget-importer/compare/v1.8.1...v1.9.0) (2026-01-03)
2
9
 
3
10
 
package/README.md CHANGED
@@ -9,22 +9,29 @@ This project provides an importer from Israeli banks (via [israeli-bank-scrapers
9
9
 
10
10
  ## Features
11
11
 
12
- 1. **Multi Bank Support**: Supports all of the institutions that the [israeli-bank-scrapers](https://github.com/eshaham/israeli-bank-scrapers) library covers (Bank Hapoalim, Cal, Leumi, Discount, etc.).
12
+ 1. **Multi Bank Support**
13
+ Supports all of the institutions that the [israeli-bank-scrapers](https://github.com/eshaham/israeli-bank-scrapers) library covers (Bank Hapoalim, Cal, Leumi, Discount, etc.).
13
14
 
14
- 2. **Prevents duplicate transactions** using Actual’s [`imported_id`](https://actualbudget.org/docs/api/reference/#transactions) logic.
15
+ 2. **Prevents duplicate transactions**
16
+ Uses Actual’s [`imported_id`](https://actualbudget.org/docs/api/reference/#transactions) logic.
15
17
 
16
- 3. **Automatic Account Creation**: If the bank account does not exist in Actual, it will be created automatically.
18
+ 3. **Automatic Account Creation**
19
+ If the bank account does not exist in Actual, it will be created automatically.
17
20
 
18
- 4. **Reconciliation:** Optional reconciliation to adjust account balances automatically.
21
+ 4. **Reconciliation**
22
+ Optional reconciliation to adjust account balances automatically.
19
23
 
20
- 5. **Credit Card / Multi-Account Mapping (Targets):** Supports mapping multiple scraped accounts/cards into one Actual account, or mapping each scraped card into its own Actual account (via `targets` and `accounts`).
24
+ 5. **Credit Card / Multi-Account Mapping (Targets)**
25
+ Supports mapping multiple scraped accounts/cards into one Actual account, or mapping each scraped card into its own Actual account (via `targets` and `accounts`).
21
26
 
22
- 6. **Concurrent Processing:** Uses a queue (via [p-queue](https://www.npmjs.com/package/p-queue)) to manage scraping tasks concurrently.
27
+ 6. **Concurrent Processing**
28
+ Uses a queue (via [p-queue](https://www.npmjs.com/package/p-queue)) to manage scraping tasks concurrently.
23
29
 
24
30
  ## Installation
25
31
 
26
32
  ### Docker
27
33
  https://hub.docker.com/r/tomerh2001/israeli-banks-actual-budget-importer
34
+
28
35
  #### Example
29
36
  ```yml
30
37
  services:
@@ -44,18 +51,18 @@ services:
44
51
 
45
52
  ## Configuration
46
53
 
47
- The application configuration is defined using JSON and validated against a schema.
54
+ The application configuration is defined using JSON and validated against a schema.
48
55
  The main configuration file is `config.json`.
49
56
 
50
57
  The configuration has **two independent top-level sections**:
51
- 1. `actual`: Configures the Actual Budget connection.
52
- 2. `banks`: Configures bank scrapers and account mappings.
58
+ 1. `actual` Configures the Actual Budget connection.
59
+ 2. `banks` Configures bank scrapers and account mappings.
53
60
 
54
61
  ---
55
62
 
56
- ### 1) `actual` configuration
63
+ ### 1) `actual` section
57
64
 
58
- This section configures the connection to your Actual Budget server and budget.
65
+ This section configures the connection to your Actual Budget server and budget.
59
66
  It is **always required**, regardless of how you configure banks or targets.
60
67
 
61
68
  ```json
@@ -78,7 +85,7 @@ Nothing in this block changes when using `targets`, credit cards, or multi-accou
78
85
 
79
86
  ---
80
87
 
81
- ### 2) `banks` configuration
88
+ ### 2) `banks` section
82
89
 
83
90
  The `banks` section defines:
84
91
  - Which banks to scrape
@@ -86,11 +93,13 @@ The `banks` section defines:
86
93
  - How scraped accounts/cards are mapped into Actual accounts
87
94
 
88
95
  Each bank entry includes the credentials required by `israeli-bank-scrapers`
89
- (e.g. `userCode`, `username`, `password`, etc.) and supports **multiple mapping modes**.
96
+ (e.g. `userCode`, `username`, `password`, etc.).
97
+
98
+ ---
90
99
 
91
- #### Using `targets` (recommended)
100
+ ### `targets` sub-section
92
101
 
93
- A single bank scrape (for example `visaCal`) may return **multiple accounts/cards**.
102
+ A single bank scrape (for example `visaCal`) may return **multiple accounts/cards**.
94
103
  Different users model these differently in Actual, so the importer supports `targets`.
95
104
 
96
105
  Each **target** represents:
@@ -99,12 +108,21 @@ Each **target** represents:
99
108
 
100
109
  For each target:
101
110
  - Imported transactions = concatenation of transactions from selected cards
102
- - Reconciliation (if enabled) = sum of balances of selected cards
111
+ - Reconciliation (if enabled) = sum of balances of selected cards
103
112
  (only cards with a valid numeric balance are included)
104
113
 
105
114
  ---
106
115
 
107
- #### Example A: One Actual account for all VisaCal cards (consolidated)
116
+ ### Reconciliation behavior
117
+
118
+ - Reconciliation is controlled by the `reconcile` boolean.
119
+ - When `reconcile: true`, **a new reconciliation transaction is created on every run** (no updates, no reconciliation).
120
+ - Existing reconciliation transactions are never modified or reused.
121
+ - If `reconcile` is omitted or set to `false`, no reconciliation transaction is created.
122
+
123
+ ---
124
+
125
+ ### Example A: One Actual account for all VisaCal cards
108
126
 
109
127
  ```json
110
128
  {
@@ -137,7 +155,7 @@ For each target:
137
155
 
138
156
  ---
139
157
 
140
- #### Example B: One Actual account per VisaCal card (separate accounts)
158
+ ### Example B: One Actual account per VisaCal card
141
159
 
142
160
  ```json
143
161
  {
@@ -175,7 +193,7 @@ For each target:
175
193
 
176
194
  ---
177
195
 
178
- #### Example C: Grouped cards into a single Actual account (subset)
196
+ ### Example C: Grouped cards into a single Actual account (subset)
179
197
 
180
198
  ```json
181
199
  {
@@ -210,7 +228,7 @@ For each target:
210
228
 
211
229
  ## Legacy configuration (single Actual account per bank)
212
230
 
213
- This configuration style is **fully supported for backward compatibility**,
231
+ This configuration style is **fully supported for backward compatibility**,
214
232
  but does **not** allow fine-grained control over multiple cards/accounts.
215
233
 
216
234
  It maps all scraped accounts from the bank into a single Actual account.
@@ -261,4 +279,4 @@ This project is open-source. Please see the [LICENSE](./LICENSE) file for licens
261
279
 
262
280
  - **israeli-bank-scrapers:** Thanks to the contributors of the bank scraper libraries.
263
281
  - **Actual App:** For providing a powerful budgeting API.
264
- - **Open-source Community:** Your support and contributions are appreciated.
282
+ - **Open-source Community:** Your support and contributions are appreciated.
@@ -64,6 +64,16 @@
64
64
  }
65
65
  ]
66
66
  },
67
+ "ReconcileOption": {
68
+ "anyOf": [
69
+ {
70
+ "type": "boolean"
71
+ },
72
+ {
73
+ "const": "consolidate"
74
+ }
75
+ ]
76
+ },
67
77
  "ConfigBankTarget": {
68
78
  "additionalProperties": false,
69
79
  "properties": {
@@ -71,7 +81,7 @@
71
81
  "type": "string"
72
82
  },
73
83
  "reconcile": {
74
- "type": "boolean"
84
+ "$ref": "#/definitions/ReconcileOption"
75
85
  },
76
86
  "accounts": {
77
87
  "$ref": "#/definitions/AccountsSelector"
@@ -89,7 +99,7 @@
89
99
  "type": "string"
90
100
  },
91
101
  "reconcile": {
92
- "type": "boolean"
102
+ "$ref": "#/definitions/ReconcileOption"
93
103
  }
94
104
  },
95
105
  "required": [
@@ -126,7 +136,7 @@
126
136
  "type": "string"
127
137
  },
128
138
  "reconcile": {
129
- "type": "boolean"
139
+ "$ref": "#/definitions/ReconcileOption"
130
140
  },
131
141
  "targets": {
132
142
  "type": "array",
@@ -167,7 +177,7 @@
167
177
  "type": "string"
168
178
  },
169
179
  "reconcile": {
170
- "type": "boolean"
180
+ "$ref": "#/definitions/ReconcileOption"
171
181
  },
172
182
  "targets": {
173
183
  "type": "array",
@@ -208,7 +218,7 @@
208
218
  "type": "string"
209
219
  },
210
220
  "reconcile": {
211
- "type": "boolean"
221
+ "$ref": "#/definitions/ReconcileOption"
212
222
  },
213
223
  "targets": {
214
224
  "type": "array",
@@ -252,7 +262,7 @@
252
262
  "type": "string"
253
263
  },
254
264
  "reconcile": {
255
- "type": "boolean"
265
+ "$ref": "#/definitions/ReconcileOption"
256
266
  },
257
267
  "targets": {
258
268
  "type": "array",
@@ -297,7 +307,7 @@
297
307
  "type": "string"
298
308
  },
299
309
  "reconcile": {
300
- "type": "boolean"
310
+ "$ref": "#/definitions/ReconcileOption"
301
311
  },
302
312
  "targets": {
303
313
  "type": "array",
@@ -342,7 +352,7 @@
342
352
  "type": "string"
343
353
  },
344
354
  "reconcile": {
345
- "type": "boolean"
355
+ "$ref": "#/definitions/ReconcileOption"
346
356
  },
347
357
  "targets": {
348
358
  "type": "array",
@@ -390,7 +400,7 @@
390
400
  "type": "string"
391
401
  },
392
402
  "reconcile": {
393
- "type": "boolean"
403
+ "$ref": "#/definitions/ReconcileOption"
394
404
  },
395
405
  "targets": {
396
406
  "type": "array",
@@ -436,7 +446,7 @@
436
446
  "type": "string"
437
447
  },
438
448
  "reconcile": {
439
- "type": "boolean"
449
+ "$ref": "#/definitions/ReconcileOption"
440
450
  },
441
451
  "targets": {
442
452
  "type": "array",
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.9.0",
2
+ "version": "1.10.0",
3
3
  "name": "israeli-banks-actual-budget-importer",
4
4
  "module": "index.ts",
5
5
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "packageManager": "yarn@4.12.0",
27
27
  "dependencies": {
28
- "@actual-app/api": "^25.12.0",
28
+ "@actual-app/api": "^26.1.0",
29
29
  "@tomerh2001/israeli-bank-scrapers": "6.4.0",
30
30
  "cronstrue": "^3.9.0",
31
31
  "lodash": "^4.17.21",
package/src/config.d.ts CHANGED
@@ -17,6 +17,8 @@ export type ConfigActualBudget = {
17
17
  };
18
18
 
19
19
  export type ConfigBanks = Partial<Record<CompanyTypes, ConfigBank>>;
20
+ export type AccountsSelector = string[] | 'all';
21
+ export type ReconcileSelector = boolean | 'consolidate';
20
22
 
21
23
  /**
22
24
  * A single "import target" inside Actual.
@@ -30,8 +32,11 @@ export type ConfigBankTarget = {
30
32
 
31
33
  /**
32
34
  * If true, create/update a reconciliation transaction to match the scraped balance.
35
+ * If 'consolidate', reconcile once per Actual account using the consolidated balance
36
+ * across all selected scraped accounts.
37
+ * If false/undefined, do not reconcile.
33
38
  */
34
- reconcile?: boolean;
39
+ reconcile?: ReconcileSelector;
35
40
 
36
41
  /**
37
42
  * Which scraped accounts (by accountNumber) should be included in this target.
@@ -42,7 +47,7 @@ export type ConfigBankTarget = {
42
47
  * - treat as "all" for import, and for reconciliation use the first usable balance
43
48
  * (you'll refine this in the implementation files).
44
49
  */
45
- accounts?: 'all' | string[];
50
+ accounts?: AccountsSelector;
46
51
  };
47
52
 
48
53
  /**
@@ -61,5 +66,5 @@ export type ConfigBank = ScraperCredentials & {
61
66
  * If targets is provided, these should be ignored by runtime logic.
62
67
  */
63
68
  actualAccountId?: string;
64
- reconcile?: boolean;
69
+ reconcile?: ReconcileSelector;
65
70
  };
@@ -1,3 +1,6 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
2
+ /* eslint-disable no-await-in-loop */
3
+ // Importer.ts
1
4
  /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
5
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
6
  /* eslint-disable @typescript-eslint/naming-convention */
@@ -8,73 +11,18 @@ import {createScraper, type ScraperCredentials} from '@tomerh2001/israeli-bank-s
8
11
  import _ from 'lodash';
9
12
  import moment from 'moment';
10
13
  import actual from '@actual-app/api';
11
- import {type PayeeEntity, type TransactionEntity} from '@actual-app/api/@types/loot-core/src/types/models';
14
+ import {type PayeeEntity} from '@actual-app/api/@types/loot-core/src/types/models';
12
15
  import stdout from 'mute-stdout';
13
- import {type ScrapeTransactionsContext} from './utils.d';
14
-
15
- // If you exported these from your config types file, import them from there instead.
16
- type AccountsSelector = 'all' | string[];
17
- type BankTarget = {
18
- actualAccountId: string;
19
- reconcile?: boolean;
20
- accounts?: AccountsSelector;
21
- };
22
-
23
- function isFiniteNumber(x: unknown): x is number {
24
- return typeof x === 'number' && Number.isFinite(x);
25
- }
26
-
27
- function stripUndefined<T extends Record<string, any>>(object: T): T {
28
- return Object.fromEntries(Object.entries(object).filter(([, v]) => v !== undefined)) as T;
29
- }
30
-
31
- function normalizeTargets(bank: any): BankTarget[] {
32
- // New config: targets[]
33
- if (Array.isArray(bank?.targets) && bank.targets.length > 0) {
34
- return bank.targets
35
- .filter((t: any) => t?.actualAccountId)
36
- .map((t: any) => ({
37
- actualAccountId: t.actualAccountId,
38
- reconcile: Boolean(t.reconcile),
39
- accounts: t.accounts,
40
- }));
41
- }
42
-
43
- // Legacy config: actualAccountId + reconcile
44
- if (bank?.actualAccountId) {
45
- return [{
46
- actualAccountId: bank.actualAccountId,
47
- reconcile: Boolean(bank.reconcile),
48
- // Legacy behavior did not support selecting accounts; treat as "all".
49
- accounts: 'all',
50
- }];
51
- }
52
-
53
- return [];
54
- }
55
-
56
- function selectScraperAccounts(
57
- allAccounts: any[] | undefined,
58
- selector: AccountsSelector | undefined,
59
- ) {
60
- const accounts = allAccounts ?? [];
61
- if (selector === undefined || selector === 'all') {
62
- return accounts;
63
- }
64
-
65
- const set = new Set(selector);
66
- return accounts.filter(a => set.has(String(a.accountNumber)));
67
- }
68
-
69
- function stableImportedId(companyId: string, accountNumber: string | undefined, txn: any) {
70
- // Prefer scraper identifier if present; fall back to a deterministic composite.
71
- const idPart
72
- = txn?.identifier
73
- ?? `${moment(txn?.date).format('YYYY-MM-DD')}:${txn?.chargedAmount}:${txn?.description ?? ''}:${txn?.memo ?? ''}`;
74
-
75
- // AccountNumber is important once multiple cards are aggregated into one Actual account.
76
- return `${companyId}:${accountNumber ?? 'unknown'}:${idPart}`;
77
- }
16
+ import {type ScrapeTransactionsContext} from './importer.d';
17
+ import {
18
+ normalizeTargets,
19
+ selectScraperAccounts,
20
+ stripUndefined,
21
+ stableImportedId,
22
+ isFiniteNumber,
23
+ reconciliationTargetKey,
24
+ uniqueReconciliationImportedId,
25
+ } from './importer.utils';
78
26
 
79
27
  export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTransactionsContext) {
80
28
  function log(status: any, other?: Record<string, unknown>) {
@@ -118,7 +66,7 @@ export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTrans
118
66
 
119
67
  const payees: PayeeEntity[] = await actual.getPayees();
120
68
 
121
- // Process each target independently (supports per-card, per-company, and consolidated).
69
+ // Process each target independently.
122
70
  for (const target of targets) {
123
71
  const selectedAccounts = selectScraperAccounts(result.accounts as any[], target.accounts);
124
72
 
@@ -156,6 +104,7 @@ export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTrans
156
104
  }
157
105
  }
158
106
 
107
+ // Reconcile: boolean-only, and ALWAYS creates a NEW reconciliation txn (no updates).
159
108
  if (!target.reconcile) {
160
109
  continue;
161
110
  }
@@ -184,46 +133,26 @@ export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTrans
184
133
  const currentBalance = actual.utils.integerToAmount(await actual.getAccountBalance(target.actualAccountId));
185
134
  const balanceDiff = scraperBalance - currentBalance;
186
135
 
187
- // Stable imported_id per Actual account so we update the same reconciliation txn each run.
188
- const reconciliationImportedId = `reconciliation-${target.actualAccountId}`;
189
-
190
- const allAccountTxns: TransactionEntity[] = await actual.getTransactions(
191
- target.actualAccountId,
192
- '2000-01-01',
193
- moment().add(1, 'year').format('YYYY-MM-DD'),
194
- );
195
-
196
- const existingReconciliation = allAccountTxns.find(txn => txn.imported_id === reconciliationImportedId);
197
-
198
- if (existingReconciliation && balanceDiff === 0) {
136
+ // If there is no diff, creating a reconciliation txn is typically noise.
137
+ // If you truly want a txn even for 0, remove this guard.
138
+ if (balanceDiff === 0) {
199
139
  log('RECONCILIATION_NOT_NEEDED', {actualAccountId: target.actualAccountId});
200
140
  continue;
201
141
  }
202
142
 
143
+ const targetKey = reconciliationTargetKey(target.accounts, selectedAccounts);
144
+ const reconciliationImportedId = uniqueReconciliationImportedId(target.actualAccountId);
145
+
203
146
  const reconciliationTxn = stripUndefined({
204
147
  account: target.actualAccountId,
205
148
  date: moment().format('YYYY-MM-DD'),
206
149
  amount: actual.utils.amountToInteger(balanceDiff),
207
150
  payee: null, // IMPORTANT: never pass undefined to updateTransaction schema
208
151
  imported_payee: 'Reconciliation',
209
- notes: `Reconciliation from ${currentBalance.toLocaleString()} to ${scraperBalance.toLocaleString()}`,
210
- imported_id: reconciliationImportedId,
152
+ notes: `Reconciliation (${targetKey}) from ${currentBalance.toLocaleString()} to ${scraperBalance.toLocaleString()}`,
153
+ imported_id: reconciliationImportedId, // NEW every run
211
154
  });
212
155
 
213
- if (existingReconciliation) {
214
- stdout.mute();
215
- await actual.updateTransaction(existingReconciliation.id, reconciliationTxn);
216
- stdout.unmute();
217
-
218
- log('RECONCILIATION_UPDATED', {
219
- actualAccountId: target.actualAccountId,
220
- from: currentBalance,
221
- to: scraperBalance,
222
- diff: balanceDiff,
223
- });
224
- continue;
225
- }
226
-
227
156
  stdout.mute();
228
157
  const reconciliationResult = await actual.importTransactions(target.actualAccountId, [reconciliationTxn]);
229
158
  stdout.unmute();
@@ -236,6 +165,7 @@ export async function scrapeAndImportTransactions({companyId, bank}: ScrapeTrans
236
165
  from: currentBalance,
237
166
  to: scraperBalance,
238
167
  diff: balanceDiff,
168
+ importedId: reconciliationImportedId,
239
169
  });
240
170
  }
241
171
  }
@@ -0,0 +1,98 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
5
+
6
+ import moment from 'moment';
7
+ import type {ConfigBankTarget, AccountsSelector} from './config';
8
+
9
+ export function isFiniteNumber(x: unknown): x is number {
10
+ return typeof x === 'number' && Number.isFinite(x);
11
+ }
12
+
13
+ export function stripUndefined<T extends Record<string, any>>(object: T): T {
14
+ return Object.fromEntries(Object.entries(object).filter(([, v]) => v !== undefined)) as T;
15
+ }
16
+
17
+ /**
18
+ * Normalizes bank config into a list of targets.
19
+ *
20
+ * IMPORTANT:
21
+ * - reconcile is boolean-only.
22
+ * - No consolidation modes.
23
+ */
24
+ export function normalizeTargets(bank: any): ConfigBankTarget[] {
25
+ // New config: targets[]
26
+ if (Array.isArray(bank?.targets) && bank.targets.length > 0) {
27
+ return bank.targets
28
+ .filter((t: any) => t?.actualAccountId)
29
+ .map((t: any) => ({
30
+ actualAccountId: t.actualAccountId,
31
+ reconcile: Boolean(t.reconcile),
32
+ accounts: t.accounts as AccountsSelector | undefined,
33
+ }));
34
+ }
35
+
36
+ // Legacy config: actualAccountId + reconcile
37
+ if (bank?.actualAccountId) {
38
+ return [{
39
+ actualAccountId: bank.actualAccountId,
40
+ reconcile: Boolean(bank.reconcile),
41
+ accounts: 'all',
42
+ }];
43
+ }
44
+
45
+ return [];
46
+ }
47
+
48
+ export function selectScraperAccounts(
49
+ allAccounts: any[] | undefined,
50
+ selector: AccountsSelector | undefined,
51
+ ) {
52
+ const accounts = allAccounts ?? [];
53
+ if (selector === undefined || selector === 'all') {
54
+ return accounts;
55
+ }
56
+
57
+ const set = new Set(selector);
58
+ return accounts.filter(a => set.has(String(a.accountNumber)));
59
+ }
60
+
61
+ export function reconciliationTargetKey(selector: AccountsSelector | undefined, selectedAccounts: any[]) {
62
+ // Prefer concrete selected account numbers (deterministic once scrape ran)
63
+ const nums = selectedAccounts
64
+ .map(a => String(a?.accountNumber))
65
+ .filter(Boolean)
66
+ .sort();
67
+
68
+ if (nums.length > 0) {
69
+ return nums.join(',');
70
+ }
71
+
72
+ // Fallback
73
+ if (selector === undefined || selector === 'all') {
74
+ return 'all';
75
+ }
76
+
77
+ return [...selector].map(String).sort().join(',');
78
+ }
79
+
80
+ export function stableImportedId(companyId: string, accountNumber: string | undefined, txn: any) {
81
+ // Prefer scraper identifier if present; fall back to a deterministic composite.
82
+ const idPart = txn?.identifier
83
+ ?? `${moment(txn?.date).format('YYYY-MM-DD')}:${txn?.chargedAmount}:${txn?.description ?? ''}:${txn?.memo ?? ''}`;
84
+
85
+ // AccountNumber is important once multiple cards are aggregated into one Actual account.
86
+ return `${companyId}:${accountNumber ?? 'unknown'}:${idPart}`;
87
+ }
88
+
89
+ /**
90
+ * Generates a unique reconciliation imported_id so a NEW txn is created every run.
91
+ * (No update logic; no consolidation logic.)
92
+ */
93
+ export function uniqueReconciliationImportedId(actualAccountId: string) {
94
+ // Date-based + randomness to avoid collisions if multiple targets reconcile in the same second.
95
+ const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSS');
96
+ const rand = Math.random().toString(16).slice(2, 10);
97
+ return `reconciliation-${actualAccountId}:${ts}:${rand}`;
98
+ }
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ import cronstrue from 'cronstrue';
14
14
  import stdout from 'mute-stdout';
15
15
  import config from '../config.json' assert {type: 'json'};
16
16
  import type {ConfigBank} from './config.d.ts';
17
- import {scrapeAndImportTransactions} from './utils.ts';
17
+ import {scrapeAndImportTransactions} from './importer';
18
18
 
19
19
  let scheduledTask: ScheduledTask;
20
20
 
File without changes