@profullstack/coinpay 0.13.0 → 0.16.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 +75 -0
- package/bin/coinpay.js +63 -14
- package/package.json +1 -1
- package/src/finances-commands.js +153 -20
- package/src/finances-reports.d.ts +80 -3
- package/src/finances-reports.js +85 -5
- package/src/mcp.js +117 -3
- package/src/statements-fetch.d.ts +85 -4
- package/src/statements-fetch.js +495 -36
- package/src/statements-throttle.js +158 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,81 @@ All notable changes to `@profullstack/coinpay` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.16.0] - 2026-10-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `coinpay finances due [--days 7]`: what is due, overdue or not posted yet
|
|
12
|
+
across every linked account, projected from each recurring charge's own
|
|
13
|
+
cadence plus statement due days set on cards and loans.
|
|
14
|
+
- `coinpay finances recurring`: every recurring charge with cadence, last and
|
|
15
|
+
next date and monthly cost.
|
|
16
|
+
- `coinpay finances due-day <account-id> <1-31|clear> [--amount N|balance]`:
|
|
17
|
+
a card or loan's statement due day, which no bank feed carries.
|
|
18
|
+
- `coinpay finances daily [show|on|off|send-now]`: the daily summary email
|
|
19
|
+
(due soon, new activity since the last one, balance sheet).
|
|
20
|
+
- MCP: `finance_due_soon`, `finance_recurring`, `finance_summary`,
|
|
21
|
+
`finance_transactions`, `finance_set_due_day`, `finance_daily_summary`.
|
|
22
|
+
- SDK: `getDueSoon`, `getDailySummary`, `setDailySummary`,
|
|
23
|
+
`deleteDailySummary`, `sendDailySummaryNow`, `setAccountDueDay`.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `coinpay mcp` `tools/list` now carries `resultType`, `ttlMs` and
|
|
27
|
+
`cacheScope`; without them Claude Code rejected the whole list
|
|
28
|
+
("connected · tools fetch failed").
|
|
29
|
+
|
|
30
|
+
## [0.15.0] - 2026-10-07
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- Brokerage sources, starting with `webull` (Webull E-Documents). Like the tax
|
|
34
|
+
sources they need no linked account: `coinpay finances statements
|
|
35
|
+
login|assist|fetch webull` and `statements cloud connect|fetch|schedule
|
|
36
|
+
webull`. A new `documents` collector mode keeps dated statements and trade
|
|
37
|
+
confirmations and 1099s listed by tax year (the bank collector skips 1099s).
|
|
38
|
+
Monthly statements and confirmations are filed into the document library as
|
|
39
|
+
category `statement` with their month (a confirmation with its day); 1099,
|
|
40
|
+
1042-S and 5498 forms as category `tax`, type `form`, with the tax year. One
|
|
41
|
+
copy per file; `statements retry` re-files failures. Brokerages are not
|
|
42
|
+
throttled like the agencies. If SimpleFIN ever links a Webull account, it
|
|
43
|
+
keeps the bank path (statements matched to the account).
|
|
44
|
+
- MCP `statements_banks` lists `brokerages`; statements engine exports
|
|
45
|
+
`BROKERAGE_SOURCES`, `brokerageSource`, `isBrokerageSource`, `sourceMode`,
|
|
46
|
+
`classifyBrokerageDocument`, `keepBrokerageDocument`, `COLLECT_DOCS`,
|
|
47
|
+
`OPEN_DOCUMENTS`.
|
|
48
|
+
|
|
49
|
+
## [0.14.0] - 2026-10-05
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- Tax document sources: `ftb` (California FTB, MyFTB), `irs` (IRS Online
|
|
53
|
+
Account) and `irs-business` (IRS Business Tax Account). They need no linked
|
|
54
|
+
bank: `coinpay finances statements login|assist|fetch <source>` and
|
|
55
|
+
`statements cloud connect|fetch|schedule <source>` take them. Notices,
|
|
56
|
+
letters and transcripts are collected in a tax mode (the bank collector
|
|
57
|
+
still skips them) and filed into the document library as category `tax`,
|
|
58
|
+
with tax year, document type and the source key; one copy per file.
|
|
59
|
+
- Attempt throttle for tax sources: at most 2 visits per 30 minutes and 4 per
|
|
60
|
+
day, counted before the visit, lockout pages detected and respected (no
|
|
61
|
+
override), credentials never re-entered. Ledger at
|
|
62
|
+
`~/.coinpay/statements/throttle.json`.
|
|
63
|
+
- `coinpay finances books export --with-documents` (a ZIP with the period's
|
|
64
|
+
tax documents and a manifest) and `books send --with-documents` (to the
|
|
65
|
+
account owner's own address only), within 8 MiB; what does not fit is listed.
|
|
66
|
+
- MCP: `tax_documents_list`, `tax_documents_get`; `statements_banks` lists the
|
|
67
|
+
tax sources with their throttle state. API: `GET /finances/documents?category=`
|
|
68
|
+
and `GET /finances/documents/:id`; SDK `getFinanceDocument`.
|
|
69
|
+
- SDK: `uploadFinanceDocument`, `listFinanceDocuments`; statements engine
|
|
70
|
+
exports `TAX_SOURCES`, `standaloneSources`, `withStandaloneSources`,
|
|
71
|
+
`keepCandidate`, `collectScript`, `classifyTaxDocument`, `keepTaxDocument`
|
|
72
|
+
and the throttle helpers (`evaluateThrottle`, `detectLockout`, …).
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
- `coinpay auth login` is the OAuth 2.1 sign-in the help always advertised
|
|
76
|
+
(`--device` works); `--email/--password` keeps the old password path with a
|
|
77
|
+
deprecation note.
|
|
78
|
+
- `coinpay self update|upgrade|remove|uninstall` now work as listed in the help
|
|
79
|
+
(the top-level forms still do). `coinpay mcp` is listed on its own.
|
|
80
|
+
- `institutionKey` maps `*.ftb.ca.gov` to `ftb` (was `ca`) and `*.irs.gov`
|
|
81
|
+
to `irs`.
|
|
82
|
+
|
|
8
83
|
## [0.13.0] - 2026-10-04
|
|
9
84
|
|
|
10
85
|
### Added
|
package/bin/coinpay.js
CHANGED
|
@@ -53,6 +53,13 @@ function handleSelfManage(action) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** `coinpay self <sub>`: which installer action, or null. */
|
|
57
|
+
function selfAction(sub) {
|
|
58
|
+
if (sub === 'update' || sub === 'upgrade' || sub === 'self-update') return 'update';
|
|
59
|
+
if (sub === 'remove' || sub === 'uninstall') return 'remove';
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
// ANSI colors
|
|
57
64
|
const colors = {
|
|
58
65
|
reset: '\x1b[0m',
|
|
@@ -176,6 +183,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
176
183
|
'no-model',
|
|
177
184
|
'only-uncategorized',
|
|
178
185
|
'no-attach',
|
|
186
|
+
'with-documents',
|
|
179
187
|
'estimate-gaps',
|
|
180
188
|
'plain',
|
|
181
189
|
'no-stream',
|
|
@@ -379,9 +387,13 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
379
387
|
set-url <base-url> Set custom API URL
|
|
380
388
|
show Show current configuration
|
|
381
389
|
|
|
390
|
+
${colors.bright}login${colors.reset} Sign in (OAuth 2.1 in your browser; --device on a machine without one)
|
|
391
|
+
${colors.bright}logout${colors.reset} Forget the saved session
|
|
392
|
+
|
|
382
393
|
${colors.bright}auth${colors.reset}
|
|
383
394
|
register Register new merchant account
|
|
384
|
-
login
|
|
395
|
+
login Same as \`coinpay login\` (OAuth 2.1; --device over SSH).
|
|
396
|
+
--email/--password is the old password sign-in (deprecated)
|
|
385
397
|
me Show current merchant info
|
|
386
398
|
|
|
387
399
|
${colors.bright}payment${colors.reset}
|
|
@@ -491,10 +503,16 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
491
503
|
statements fetch [bank…] Download every new PDF statement from the banks and import it
|
|
492
504
|
(--since YYYY-MM, --max N, --headed, --render S; exits 3 if a bank needs you)
|
|
493
505
|
statements assist <bank> A window: every PDF you download is imported
|
|
506
|
+
statements assist ftb|irs|irs-business Tax sources (no linked bank needed): every
|
|
507
|
+
notice, letter or transcript you download is filed under
|
|
508
|
+
Documents (tax). IRS: use this, locally (ID.me blocks clouds).
|
|
509
|
+
Throttled: 2 visits per 30 min, 4 per day; lockouts respected
|
|
510
|
+
statements login|fetch|assist webull Brokerage (no linked account needed): monthly
|
|
511
|
+
statements, trade confirmations and 1099s are filed under Documents
|
|
494
512
|
statements coverage Which account-months have a statement (--months 12)
|
|
495
513
|
statements runs|local|retry Fetch history, the local archive, re-import what failed
|
|
496
514
|
statements cloud [status] CoinPay cloud fetching (Professional; free for admins)
|
|
497
|
-
statements cloud connect <bank> Sign in once in CoinPay's cloud browser (opens your browser)
|
|
515
|
+
statements cloud connect <bank|ftb|irs|webull> Sign in once in CoinPay's cloud browser (opens your browser)
|
|
498
516
|
statements cloud fetch [bank] [--wait] Fetch now in the cloud; weekly by default
|
|
499
517
|
statements cloud schedule <bank> weekly|off | cloud forget <bank>
|
|
500
518
|
books [queue] Transactions awaiting category review (--status, --scope, --search)
|
|
@@ -503,11 +521,18 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
503
521
|
books categorize Rules, heuristics, then the model over unreviewed rows (--wait)
|
|
504
522
|
books rules [add|delete] Merchant category rules
|
|
505
523
|
books summary Totals by tax category (--period 2026|2026-Q3|2026-08, --scope)
|
|
506
|
-
books export CPA pack (--period, --scope, --format csv|pdf|html|json, --output
|
|
524
|
+
books export CPA pack (--period, --scope, --format csv|pdf|html|json, --output;
|
|
525
|
+
--with-documents: a ZIP with the period's tax documents, up to 8 MiB)
|
|
507
526
|
payloads [list|download <id>] Raw provider responses, exactly as received
|
|
508
527
|
reports send <id> Email a report (--to a@x,b@y, --format pdf,csv, --message, --no-attach)
|
|
509
|
-
books send Email the CPA pack (--to, --period, --scope, --format
|
|
528
|
+
books send Email the CPA pack (--to, --period, --scope, --format,
|
|
529
|
+
--with-documents attaches the period's tax documents within
|
|
530
|
+
8 MiB; only to your own address)
|
|
510
531
|
digest [show|set|off|send-now] Weekly digest email (--days mon,fri --hour 8 --to …)
|
|
532
|
+
due [--days 7] What is due and overdue across every account (--days up to 60)
|
|
533
|
+
recurring Every recurring charge detected, with next date and monthly cost
|
|
534
|
+
due-day <id> <1-31|clear> A card/loan's statement due day (--amount 810.46, or balance)
|
|
535
|
+
daily [show|on|off|send-now] Daily summary email: due soon, new activity, position (--hour 7 --to …)
|
|
511
536
|
|
|
512
537
|
${colors.bright}escrow${colors.reset}
|
|
513
538
|
create Create a new escrow
|
|
@@ -546,14 +571,15 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
546
571
|
logs <business-id> Get webhook logs
|
|
547
572
|
test <business-id> Send test webhook
|
|
548
573
|
|
|
549
|
-
${colors.bright}self${colors.reset}
|
|
574
|
+
${colors.bright}self${colors.reset} (each also works without "self": coinpay update)
|
|
550
575
|
update Update the coinpay CLI to the latest version
|
|
551
|
-
mcp Serve CoinPay finances to an AI agent over MCP (stdio):
|
|
552
|
-
claude mcp add coinpay -- coinpay mcp
|
|
553
576
|
upgrade Alias for update
|
|
554
577
|
remove Uninstall the coinpay CLI
|
|
555
578
|
uninstall Alias for remove
|
|
556
579
|
|
|
580
|
+
${colors.bright}mcp${colors.reset} Serve CoinPay finances to an AI agent over MCP (stdio):
|
|
581
|
+
claude mcp add coinpay -- coinpay mcp
|
|
582
|
+
|
|
557
583
|
${colors.cyan}Wallet Options:${colors.reset}
|
|
558
584
|
--words <12|24> Number of mnemonic words (default: 12)
|
|
559
585
|
--chains <BTC,ETH,...> Chains to derive (default: BTC,ETH,SOL,POL,BCH)
|
|
@@ -2699,13 +2725,20 @@ async function handleAuth(subcommand, args, flags) {
|
|
|
2699
2725
|
case 'login': {
|
|
2700
2726
|
const email = flags.email;
|
|
2701
2727
|
const password = flags.password;
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2728
|
+
|
|
2729
|
+
// `auth login` is the documented way in; it is the same OAuth 2.1 sign-in
|
|
2730
|
+
// as `coinpay login`. Only an explicit --email/--password takes the old path.
|
|
2731
|
+
if (email === undefined && password === undefined) {
|
|
2732
|
+
await handleLogin(flags);
|
|
2706
2733
|
return;
|
|
2707
2734
|
}
|
|
2708
|
-
|
|
2735
|
+
print.warn('Password sign-in (--email/--password) is deprecated; use `coinpay login` (OAuth 2.1) or `coinpay login --device`.');
|
|
2736
|
+
if (!email || !password || email === true || password === true) {
|
|
2737
|
+
print.error('Required: --email <email> --password <password>');
|
|
2738
|
+
print.info('Or sign in without a password: coinpay auth login');
|
|
2739
|
+
process.exit(1);
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2709
2742
|
try {
|
|
2710
2743
|
const client = createUnauthenticatedClient();
|
|
2711
2744
|
const { loginMerchant } = await import('../src/auth.js');
|
|
@@ -4326,7 +4359,12 @@ async function handleFinances(subcommand, args, flags) {
|
|
|
4326
4359
|
case 'statements':
|
|
4327
4360
|
case 'books':
|
|
4328
4361
|
case 'payloads':
|
|
4329
|
-
case 'digest':
|
|
4362
|
+
case 'digest':
|
|
4363
|
+
case 'due':
|
|
4364
|
+
case 'due-soon':
|
|
4365
|
+
case 'recurring':
|
|
4366
|
+
case 'due-day':
|
|
4367
|
+
case 'daily': {
|
|
4330
4368
|
const { client } = financesClient();
|
|
4331
4369
|
const { runFinancesCommand } = await import('../src/finances-commands.js');
|
|
4332
4370
|
const code = await runFinancesCommand(subcommand, args, flags, {
|
|
@@ -4340,7 +4378,7 @@ async function handleFinances(subcommand, args, flags) {
|
|
|
4340
4378
|
|
|
4341
4379
|
default:
|
|
4342
4380
|
print.error(`Unknown finances command: ${subcommand}`);
|
|
4343
|
-
console.log('Usage: coinpay finances [tui|summary|accounts|ledger|connections|sync|connect|disconnect|backfill|jobs|coverage|report|reports|statements|books|payloads|digest]');
|
|
4381
|
+
console.log('Usage: coinpay finances [tui|summary|accounts|ledger|connections|sync|connect|disconnect|backfill|jobs|coverage|report|reports|statements|books|payloads|digest|due|recurring|due-day|daily]');
|
|
4344
4382
|
process.exit(1);
|
|
4345
4383
|
}
|
|
4346
4384
|
}
|
|
@@ -4644,6 +4682,17 @@ async function handleLightning(subcommand, args, flags) {
|
|
|
4644
4682
|
handleSelfManage('update');
|
|
4645
4683
|
break;
|
|
4646
4684
|
|
|
4685
|
+
case 'self': {
|
|
4686
|
+
const action = selfAction(subcommand);
|
|
4687
|
+
if (!action) {
|
|
4688
|
+
print.error(`Unknown self command: ${subcommand || '(none)'}`);
|
|
4689
|
+
print.info('Available: coinpay self update|upgrade|remove|uninstall');
|
|
4690
|
+
process.exit(1);
|
|
4691
|
+
}
|
|
4692
|
+
handleSelfManage(action);
|
|
4693
|
+
break;
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4647
4696
|
case 'remove':
|
|
4648
4697
|
case 'uninstall':
|
|
4649
4698
|
handleSelfManage('remove');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/coinpay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "CoinPay SDK & CLI \u2014 Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/finances-commands.js
CHANGED
|
@@ -146,7 +146,7 @@ async function runCloudAction(rest, flags, { client, emit, progress }) {
|
|
|
146
146
|
const lines = [data.access.allowed ? `CoinPay cloud: ${data.access.message}` : `CoinPay cloud is not available: ${data.access.message}`, ''];
|
|
147
147
|
for (const bank of data.banks) {
|
|
148
148
|
const c = bank.cloud;
|
|
149
|
-
lines.push(`${bank.key} ${bank.name} · ${!c ? 'not connected' : c.state === 'login_needed' ? 'needs sign-in' : c.state}${c ? ` · last fetch ${ago(c.lastFetchAt)}${c.lastStatus ? ` (${c.lastStatus})` : ''} · ${c.schedule}` : ''}`);
|
|
149
|
+
lines.push(`${bank.key} ${bank.name}${bank.kind === 'tax' ? ' (tax documents)' : bank.kind === 'brokerage' ? ' (brokerage documents)' : ''} · ${!c ? 'not connected' : c.state === 'login_needed' ? 'needs sign-in' : c.state}${c ? ` · last fetch ${ago(c.lastFetchAt)}${c.lastStatus ? ` (${c.lastStatus})` : ''} · ${c.schedule}` : ''}`);
|
|
150
150
|
}
|
|
151
151
|
const next = data.banks.find((b) => !b.cloud || b.cloud.state !== 'active');
|
|
152
152
|
if (data.access.allowed && next) lines.push('', `Next: coinpay finances statements cloud connect ${next.key}`);
|
|
@@ -155,8 +155,9 @@ async function runCloudAction(rest, flags, { client, emit, progress }) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
if (sub === 'connect') {
|
|
158
|
-
if (!name) throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances statements cloud connect <bank> [--url <sign-in page>]');
|
|
158
|
+
if (!name) throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances statements cloud connect <bank|ftb|irs|irs-business|webull> [--url <sign-in page>]');
|
|
159
159
|
const data = await api.connectCloudBank(client, { institutionKey: name, url: typeof flags.url === 'string' ? flags.url : undefined });
|
|
160
|
+
if (data.warning) progress(`warning: ${data.warning}`);
|
|
160
161
|
const { siteOrigin, openInBrowser } = await import('./oauth-login.js');
|
|
161
162
|
const viewer = new URL(data.viewerUrl, siteOrigin(client.baseUrl)).toString();
|
|
162
163
|
const opened = flags['no-open'] ? false : openInBrowser(viewer);
|
|
@@ -262,7 +263,8 @@ async function runStatementFetchAction(action, rest, flags, { client, out, emit,
|
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
const bound = await sf.clientApi(client);
|
|
265
|
-
const
|
|
266
|
+
const linked = sf.groupInstitutions(await bound.listAccounts());
|
|
267
|
+
const institutions = sf.withStandaloneSources(linked);
|
|
266
268
|
if (action === 'banks') {
|
|
267
269
|
const state = sf.loadLocal(home);
|
|
268
270
|
let latest = [];
|
|
@@ -271,27 +273,65 @@ async function runStatementFetchAction(action, rest, flags, { client, out, emit,
|
|
|
271
273
|
} catch {
|
|
272
274
|
// An older deployment without the route still lists the banks.
|
|
273
275
|
}
|
|
274
|
-
const rows = institutions.map((i) => ({
|
|
276
|
+
const rows = institutions.map((i) => ({
|
|
277
|
+
...i,
|
|
278
|
+
signedIn: sf.signedIn(i.key, home),
|
|
279
|
+
local: state.institutions[i.key] || {},
|
|
280
|
+
lastRun: latest.find((r) => r.institutionKey === i.key) || null,
|
|
281
|
+
throttle: sf.isTaxSource(i) ? sf.checkLocalThrottle(home, i.key) : null,
|
|
282
|
+
}));
|
|
283
|
+
const banks = rows.filter((b) => !sf.isTaxSource(b) && !sf.isBrokerageSource(b));
|
|
284
|
+
const taxRows = rows.filter((b) => sf.isTaxSource(b));
|
|
285
|
+
const brokerageRows = rows.filter((b) => sf.isBrokerageSource(b));
|
|
286
|
+
const line = (b) => `${b.key} ${b.name} · ${b.signedIn ? `signed in ${ago(b.local.loggedInAt)}` : 'not signed in'} · last fetch ${b.lastRun ? `${b.lastRun.status} ${ago(b.lastRun.finishedAt)}` : 'never'}${b.throttle && !b.throttle.ok ? ` · waiting: ${b.throttle.message}` : ''}`;
|
|
275
287
|
emit({ home, chrome, banks: rows }, [
|
|
276
|
-
...
|
|
277
|
-
...(
|
|
288
|
+
...banks.map((b) => `${line(b)}\n${b.accounts.map((a) => ` ${a.name}${a.last4 ? '' : ' (no last four digits: its statements may need filing by hand)'}`).join('\n')}`),
|
|
289
|
+
...(banks.length ? [] : ['No linked banks.']),
|
|
290
|
+
'',
|
|
291
|
+
'Tax sources (notices, letters, transcripts -> Documents; at most 2 visits per 30 min, 4 per day):',
|
|
292
|
+
...taxRows.map((b) => line(b)),
|
|
293
|
+
'',
|
|
294
|
+
'Brokerages (statements, trade confirmations, 1099s -> Documents):',
|
|
295
|
+
...brokerageRows.map((b) => line(b)),
|
|
296
|
+
...(banks.some((b) => !b.signedIn) ? ['', `Next: coinpay finances statements login ${banks.find((b) => !b.signedIn).key}`] : []),
|
|
278
297
|
...(chrome ? [] : ['', sf.NO_CHROME]),
|
|
279
298
|
].join('\n'));
|
|
280
299
|
return EXIT.OK;
|
|
281
300
|
}
|
|
282
301
|
if (action === 'login' || action === 'assist') {
|
|
283
|
-
if (rest.length !== 1) throw new CliExit(EXIT.INVALID, `Usage: coinpay finances statements ${action} <bank>`);
|
|
302
|
+
if (rest.length !== 1) throw new CliExit(EXIT.INVALID, `Usage: coinpay finances statements ${action} <bank|ftb|irs|irs-business|webull>`);
|
|
284
303
|
if (!chrome) throw new CliExit(EXIT.FAILURE, sf.NO_CHROME);
|
|
285
|
-
const institution = sf.pickInstitution(
|
|
304
|
+
const institution = sf.pickInstitution(linked, rest[0]);
|
|
305
|
+
const tax = sf.isTaxSource(institution);
|
|
306
|
+
const brokerage = sf.isBrokerageSource(institution);
|
|
286
307
|
const state = sf.loadLocal(home);
|
|
287
308
|
const local = (state.institutions[institution.key] ||= {});
|
|
288
309
|
const urls = sf.startUrls(institution, local.start);
|
|
310
|
+
let onPageText;
|
|
311
|
+
if (tax) {
|
|
312
|
+
// Counted before the window opens. --force never lifts a lockout.
|
|
313
|
+
const verdict = sf.takeLocalAttempt(home, institution.key, action);
|
|
314
|
+
if (!verdict.ok) throw new CliExit(EXIT.STRICT, `${institution.name}: not opening, ${verdict.message}`);
|
|
315
|
+
const source = sf.taxSource(institution.key) || {};
|
|
316
|
+
let warned = false;
|
|
317
|
+
onPageText = (text) => {
|
|
318
|
+
const lock = sf.detectLockout(text);
|
|
319
|
+
if (!lock) return;
|
|
320
|
+
const until = sf.lockoutUntil(lock, new Date(), undefined, source.lockoutMinutes);
|
|
321
|
+
sf.recordLocalLockout(home, institution.key, until, `lockout page during ${action}`);
|
|
322
|
+
if (!warned) {
|
|
323
|
+
warned = true;
|
|
324
|
+
progress(`${institution.name} shows a locked account. Stop trying and close the window; CoinPay will not open it again before ${until}.`);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
progress(`${institution.name}: sign in once, by hand. If a code arrives late, do not resubmit the form; close the window and wait. (${sf.TAX_THROTTLE.perWindow} visits per 30 minutes, ${sf.TAX_THROTTLE.perDay} per day.)`);
|
|
328
|
+
}
|
|
289
329
|
const browser = await sf.openBrowser({ chrome, profile: sf.profileDir(institution.key, home), headless: false, force: flags.force === true });
|
|
290
330
|
if (action === 'login') {
|
|
291
331
|
const url = typeof flags.url === 'string' ? flags.url : urls.login;
|
|
292
332
|
if (!url) throw new CliExit(EXIT.INVALID, `CoinPay has no site for ${institution.name}; pass --url with its sign-in page`);
|
|
293
333
|
out(`Opening ${institution.name}. Sign in, go to the page that lists your statements, then close the window.`);
|
|
294
|
-
const last = await sf.loginWindow(browser, url);
|
|
334
|
+
const last = await sf.loginWindow(browser, url, { onPageText });
|
|
295
335
|
await browser.close();
|
|
296
336
|
local.loggedInAt = new Date().toISOString();
|
|
297
337
|
if (last) local.start = last;
|
|
@@ -299,13 +339,22 @@ async function runStatementFetchAction(action, rest, flags, { client, out, emit,
|
|
|
299
339
|
emit({ bank: institution.key, start: local.start || urls.fetch }, last ? `Saved. fetch will start at ${last}` : 'Saved. The window closed on a sign-in page, so fetch starts from the bank\'s known page.');
|
|
300
340
|
return EXIT.OK;
|
|
301
341
|
}
|
|
302
|
-
out(
|
|
342
|
+
out(tax
|
|
343
|
+
? `Opening ${institution.name}. Sign in, open your notices, letters or transcripts, and download each PDF; each is filed under Documents (tax) as it lands. Close the window when done.`
|
|
344
|
+
: brokerage
|
|
345
|
+
? `Opening ${institution.name}. Sign in, open its documents, and download each statement, trade confirmation or 1099; each is filed under Documents as it lands. Close the window when done.`
|
|
346
|
+
: `Opening ${institution.name}. Download each statement you want; each is imported as it lands. Close the window when done.`);
|
|
303
347
|
let kept = 0;
|
|
304
348
|
try {
|
|
305
349
|
await sf.assistWindow(browser, {
|
|
306
350
|
start: urls.fetch,
|
|
351
|
+
onPageText,
|
|
307
352
|
onFile: async (download) => {
|
|
308
|
-
const result =
|
|
353
|
+
const result = tax
|
|
354
|
+
? await sf.keepTaxDocument({ institution, download, state, home, fileDocument: bound.fileDocument, how: 'assist' })
|
|
355
|
+
: brokerage
|
|
356
|
+
? await sf.keepBrokerageDocument({ institution, download, state, home, fileDocument: bound.fileDocument, how: 'assist' })
|
|
357
|
+
: await sf.keepStatement({ institution, download, state, home, importStatement: bound.importStatement, how: 'assist' });
|
|
309
358
|
if (result.status === 'imported') kept += 1;
|
|
310
359
|
progress(` ${result.status}: ${result.entry ? result.entry.path : result.name}${result.entry && result.entry.importError ? ` (${result.entry.importError})` : ''}`);
|
|
311
360
|
sf.saveLocal(state, home);
|
|
@@ -314,7 +363,7 @@ async function runStatementFetchAction(action, rest, flags, { client, out, emit,
|
|
|
314
363
|
} finally {
|
|
315
364
|
await browser.close();
|
|
316
365
|
}
|
|
317
|
-
emit({ bank: institution.key, imported: kept }, `${kept} imported`);
|
|
366
|
+
emit({ bank: institution.key, imported: kept }, tax ? `${kept} filed under Documents (tax)` : brokerage ? `${kept} filed under Documents` : `${kept} imported`);
|
|
318
367
|
return EXIT.OK;
|
|
319
368
|
}
|
|
320
369
|
// fetch
|
|
@@ -325,7 +374,7 @@ async function runStatementFetchAction(action, rest, flags, { client, out, emit,
|
|
|
325
374
|
const render = flags.render !== undefined ? Number(flags.render) : 25;
|
|
326
375
|
if (!Number.isFinite(render) || render < 3 || render > 300) throw new CliExit(EXIT.INVALID, '--render is seconds, 3 to 300');
|
|
327
376
|
const results = await sf.runStatementFetch({ api: bound, banks: rest, since, max, renderMs: render * 1000, headless: flags.headed !== true, chrome, home, log: progress, force: flags.force === true });
|
|
328
|
-
emit({ banks: results }, results.map((r) => `${r.bank}: ${r.status}, ${r.imported} imported, ${r.duplicates} already had
|
|
377
|
+
emit({ banks: results }, results.map((r) => `${r.bank}: ${r.status}, ${r.imported} ${r.kind === 'bank' ? 'imported' : 'filed'}, ${r.duplicates} already had${r.kind !== 'bank' ? '' : `, ${r.unmatched} unmatched`}${r.message ? ` (${r.message})` : ''}`).join('\n'));
|
|
329
378
|
return results.every((r) => r.status === 'ok' && r.failed === 0) ? EXIT.OK : EXIT.STRICT;
|
|
330
379
|
} catch (err) {
|
|
331
380
|
if (err instanceof sf.StatementFetchError) throw new CliExit(EXIT.INVALID, err.message);
|
|
@@ -625,7 +674,7 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
|
|
|
625
674
|
return EXIT.OK;
|
|
626
675
|
}
|
|
627
676
|
if (STATEMENT_FETCH_ACTIONS.has(action)) return await runStatementFetchAction(action, args.slice(1), flags, { client, out, emit, progress });
|
|
628
|
-
throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances statements [import <file>|list|get <id>|download <id>|reconcile <id>|delete <id>|banks|login <bank>|fetch [bank…]|assist <bank>|coverage|runs|local|retry]');
|
|
677
|
+
throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances statements [import <file>|list|get <id>|download <id>|reconcile <id>|delete <id>|banks|login <bank>|fetch [bank…]|assist <bank>|coverage|runs|local|retry|cloud …] (tax sources: ftb, irs, irs-business; brokerages: webull)');
|
|
629
678
|
}
|
|
630
679
|
|
|
631
680
|
case 'books': {
|
|
@@ -698,7 +747,7 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
|
|
|
698
747
|
return EXIT.OK;
|
|
699
748
|
}
|
|
700
749
|
if (action === 'send') {
|
|
701
|
-
if (typeof flags.to !== 'string') throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances books send --to cpa@x.com,me@x.com --period 2026 [--scope business] [--format pdf,csv] [--message "…"]');
|
|
750
|
+
if (typeof flags.to !== 'string') throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances books send --to cpa@x.com,me@x.com --period 2026 [--scope business] [--format pdf,csv] [--message "…"] [--with-documents (your own address only)]');
|
|
702
751
|
const period = typeof flags.period === 'string' ? flags.period : null;
|
|
703
752
|
const from = typeof flags.from === 'string' ? flags.from : undefined;
|
|
704
753
|
const toDate = typeof flags['to-date'] === 'string' ? flags['to-date'] : undefined;
|
|
@@ -710,8 +759,12 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
|
|
|
710
759
|
formats: flags.format ? listFlag(flags.format) : undefined,
|
|
711
760
|
message: typeof flags.message === 'string' ? flags.message : null,
|
|
712
761
|
attach: flags['no-attach'] ? false : true,
|
|
762
|
+
withDocuments: flags['with-documents'] === true,
|
|
713
763
|
});
|
|
714
|
-
|
|
764
|
+
const docs = data.documents
|
|
765
|
+
? ` Tax documents attached: ${data.documents.attached.length ? data.documents.attached.join(', ') : 'none'}.${data.documents.skipped.length ? ` Not attached (in CoinPay under Documents): ${data.documents.skipped.map((d) => `${d.title} (${d.reason})`).join(', ')}.` : ''}`
|
|
766
|
+
: '';
|
|
767
|
+
emit(data, `Sent to ${data.sent.join(', ') || 'nobody'}${data.failed.length ? `; failed: ${data.failed.map((f) => `${f.to} (${f.error})`).join(', ')}` : ''}. ${data.rows} rows, ${data.unreviewed} unreviewed.${docs} Link (expires ${data.linkExpiresAt.slice(0, 10)}): ${data.linkUrl}`);
|
|
715
768
|
return data.sent.length > 0 ? EXIT.OK : EXIT.FAILURE;
|
|
716
769
|
}
|
|
717
770
|
if (action === 'summary' || action === 'export') {
|
|
@@ -734,10 +787,16 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
|
|
|
734
787
|
return EXIT.OK;
|
|
735
788
|
}
|
|
736
789
|
const format = typeof flags.format === 'string' ? flags.format.toLowerCase() : 'csv';
|
|
737
|
-
const
|
|
738
|
-
const
|
|
790
|
+
const withDocuments = flags['with-documents'] === true;
|
|
791
|
+
const file = await api.exportBooks(client, { period: period || undefined, from, to, timezone, scope, format, withDocuments });
|
|
792
|
+
const output = typeof flags.output === 'string' ? flags.output : file.filename || `books-${period || 'range'}.${withDocuments ? 'zip' : format}`;
|
|
739
793
|
const path = writeDownload(output, Buffer.from(file.bytes), { overwrite: flags.overwrite === true });
|
|
740
|
-
|
|
794
|
+
const attachedDocs = Number(file.headers['x-documents-attached'] || 0);
|
|
795
|
+
const skippedDocs = Number(file.headers['x-documents-skipped'] || 0);
|
|
796
|
+
emit(
|
|
797
|
+
{ output: path, bytes: file.bytes.length, unreviewed: Number(file.headers['x-unreviewed-rows'] || 0), ...(withDocuments ? { documentsAttached: attachedDocs, documentsSkipped: skippedDocs } : {}) },
|
|
798
|
+
`Wrote ${path} (${file.bytes.length} bytes; ${file.headers['x-unreviewed-rows'] || 0} unreviewed rows)${withDocuments ? `. ${attachedDocs} tax document(s) inside${skippedDocs ? `, ${skippedDocs} left out to stay under 8 MiB (listed in MANIFEST.txt)` : ''}` : ''}`,
|
|
799
|
+
);
|
|
741
800
|
return EXIT.OK;
|
|
742
801
|
}
|
|
743
802
|
throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances books [queue|confirm <id>|confirm-all --yes|categorize|rules [add|delete]|summary|export|send]');
|
|
@@ -787,6 +846,80 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
|
|
|
787
846
|
throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances digest [show|set --days mon,fri --hour 8 --to a@x,b@y [--scope business]|off|delete|send-now]');
|
|
788
847
|
}
|
|
789
848
|
|
|
849
|
+
case 'due':
|
|
850
|
+
case 'due-soon':
|
|
851
|
+
case 'recurring': {
|
|
852
|
+
const days = flags.days !== undefined ? Number(flags.days) : undefined;
|
|
853
|
+
if (days !== undefined && (!Number.isInteger(days) || days < 0 || days > 60)) throw new CliExit(EXIT.INVALID, '--days must be 0 to 60');
|
|
854
|
+
const d = await api.getDueSoon(client, { days, timezone: typeof flags.timezone === 'string' ? flags.timezone : undefined });
|
|
855
|
+
const cur = d.currency || 'USD';
|
|
856
|
+
const money = (n) => new Intl.NumberFormat('en-US', { style: 'currency', currency: cur }).format(n);
|
|
857
|
+
if (subcommand === 'recurring') {
|
|
858
|
+
if (json) { out(JSON.stringify(d.recurring, null, 2)); return EXIT.OK; }
|
|
859
|
+
const rows = [...d.recurring.charges].sort((a, b) => a.nextExpected.localeCompare(b.nextExpected));
|
|
860
|
+
out(`${rows.length} recurring charges · ${money(d.recurring.monthlyTotal)}/mo${d.recurring.monthlyDebtService ? ` (${money(d.recurring.monthlyDebtService)} debt service)` : ''}`);
|
|
861
|
+
for (const c of rows) {
|
|
862
|
+
out(` next ${c.nextExpected.slice(0, 10)} ${c.cadence.padEnd(9)} ${money(c.amount).padStart(11)} ${c.payee} · ${c.accountName}${c.isDebtService ? ' · debt' : ''}`);
|
|
863
|
+
}
|
|
864
|
+
return EXIT.OK;
|
|
865
|
+
}
|
|
866
|
+
if (json) { out(JSON.stringify(d, null, 2)); return EXIT.OK; }
|
|
867
|
+
const when = (i) => i.status === 'paid' ? 'paid' : i.status === 'awaiting' ? 'not yet?' : i.daysUntil === 0 ? 'today' : i.daysUntil === 1 ? 'tomorrow' : i.daysUntil < 0 ? `${-i.daysUntil}d LATE` : `in ${i.daysUntil}d`;
|
|
868
|
+
const amt = (i) => i.amount !== null ? money(i.amount) : i.owed !== null ? `bal ${money(i.owed)}` : 'statement';
|
|
869
|
+
out(`${d.today} (${d.timezone}) · ${money(d.totals.due)} due in ${d.horizonDays} days${d.totals.overdue ? ` · ${money(d.totals.overdue)} OVERDUE` : ''}${d.totals.unpriced ? ` · +${d.totals.unpriced} statement balance(s)` : ''}`);
|
|
870
|
+
if (d.items.length === 0 && d.paid.length === 0) out(' Nothing due.');
|
|
871
|
+
for (const i of [...d.items, ...d.paid]) {
|
|
872
|
+
out(` ${i.dueOn} ${when(i).padEnd(9)} ${amt(i).padStart(14)} ${i.payee} · ${i.accountName}${i.source === 'scheduled' ? ' · statement due' : ''}`);
|
|
873
|
+
}
|
|
874
|
+
if (d.missed.length) out(` Not seen lately: ${d.missed.slice(0, 8).map((m) => `${m.payee} (expected ${m.expectedOn})`).join(', ')}`);
|
|
875
|
+
return EXIT.OK;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
case 'due-day': {
|
|
879
|
+
const id = args[0];
|
|
880
|
+
const value = args[1];
|
|
881
|
+
if (!id || value === undefined) throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances due-day <account-id> <1-31|clear> [--amount 810.46|--amount balance] (ids: coinpay finances accounts)');
|
|
882
|
+
const day = value === 'clear' || value === 'off' ? null : Number(value);
|
|
883
|
+
if (day !== null && (!Number.isInteger(day) || day < 1 || day > 31)) throw new CliExit(EXIT.INVALID, 'The due day must be 1 to 31, or clear');
|
|
884
|
+
const amount = flags.amount === undefined ? undefined : flags.amount === 'balance' || flags.amount === true ? null : String(flags.amount);
|
|
885
|
+
const account = await api.setAccountDueDay(client, id, { day, amount });
|
|
886
|
+
emit({ account }, day === null ? `Cleared the due day on ${account.name}.` : `${account.name}: due on day ${day}${account.payment_due_amount != null ? `, ${account.payment_due_amount}` : ', statement balance'}.`);
|
|
887
|
+
return EXIT.OK;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
case 'daily': {
|
|
891
|
+
const action = flags.off ? 'off' : args[0] || 'show';
|
|
892
|
+
const describe = (s) => s
|
|
893
|
+
? `Daily summary ${s.active ? 'on' : 'off'} · ${String(s.hour).padStart(2, '0')}:00 ${s.timezone} · to ${s.recipients.join(', ')}${s.nextRunAt ? ` · next ${s.nextRunAt}` : ''}${s.lastSentAt ? ` · last ${s.lastSentAt}` : ''}`
|
|
894
|
+
: 'No daily summary configured. Turn it on with: coinpay finances daily on --hour 7 --to you@example.com';
|
|
895
|
+
if (action === 'show') {
|
|
896
|
+
const s = await api.getDailySummary(client);
|
|
897
|
+
emit({ schedule: s }, describe(s));
|
|
898
|
+
return EXIT.OK;
|
|
899
|
+
}
|
|
900
|
+
if (action === 'on' || action === 'set') {
|
|
901
|
+
const s = await api.setDailySummary(client, {
|
|
902
|
+
hour: flags.hour !== undefined ? Number(flags.hour) : undefined,
|
|
903
|
+
timezone: typeof flags.timezone === 'string' ? flags.timezone : undefined,
|
|
904
|
+
recipients: flags.to ? listFlag(flags.to) : undefined,
|
|
905
|
+
active: true,
|
|
906
|
+
});
|
|
907
|
+
emit({ schedule: s }, describe(s));
|
|
908
|
+
return EXIT.OK;
|
|
909
|
+
}
|
|
910
|
+
if (action === 'off' || action === 'delete') {
|
|
911
|
+
const data = await api.deleteDailySummary(client);
|
|
912
|
+
emit(data, 'Daily summary turned off.');
|
|
913
|
+
return EXIT.OK;
|
|
914
|
+
}
|
|
915
|
+
if (action === 'send-now') {
|
|
916
|
+
const data = await api.sendDailySummaryNow(client);
|
|
917
|
+
emit(data, `Sent to ${data.sent.join(', ') || 'nobody'} (${data.due} due, ${data.overdue} overdue)${data.failed.length ? `; failed: ${data.failed.map((f) => `${f.to} (${f.error})`).join(', ')}` : ''}`);
|
|
918
|
+
return data.sent.length > 0 ? EXIT.OK : EXIT.FAILURE;
|
|
919
|
+
}
|
|
920
|
+
throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances daily [show|on --hour 7 --to a@x|off|send-now]');
|
|
921
|
+
}
|
|
922
|
+
|
|
790
923
|
case 'payloads': {
|
|
791
924
|
const action = args[0] || 'list';
|
|
792
925
|
if (action === 'list') {
|
|
@@ -822,4 +955,4 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
|
|
|
822
955
|
}
|
|
823
956
|
}
|
|
824
957
|
|
|
825
|
-
export const EXTENDED_SUBCOMMANDS = ['connect', 'disconnect', 'consent', 'backfill', 'jobs', 'coverage', 'report', 'reports', 'statements', 'books', 'payloads', 'digest'];
|
|
958
|
+
export const EXTENDED_SUBCOMMANDS = ['connect', 'disconnect', 'consent', 'backfill', 'jobs', 'coverage', 'report', 'reports', 'statements', 'books', 'payloads', 'digest', 'due', 'due-soon', 'recurring', 'due-day', 'daily'];
|
|
@@ -316,7 +316,43 @@ export function getBooksSummary(
|
|
|
316
316
|
client: CoinPayClient,
|
|
317
317
|
options?: PeriodSelection & { scope?: 'business' | 'personal' | 'all'; rows?: boolean },
|
|
318
318
|
): Promise<{ period: Record<string, unknown>; scope: string; lines: BooksSummaryLine[]; totals: Array<{ currency: string; income: string; expenses: string; net: string; excluded: string }>; rows: number; unreviewed: number; uncategorized: number; notice: string; transactions?: BooksRow[] }>;
|
|
319
|
-
export function exportBooks(client: CoinPayClient, options?: PeriodSelection & { scope?: 'business' | 'personal' | 'all'; format?: 'csv' | 'pdf' | 'html' | 'json' }): Promise<BinaryResponse>;
|
|
319
|
+
export function exportBooks(client: CoinPayClient, options?: PeriodSelection & { scope?: 'business' | 'personal' | 'all'; format?: 'csv' | 'pdf' | 'html' | 'json'; withDocuments?: boolean }): Promise<BinaryResponse>;
|
|
320
|
+
|
|
321
|
+
export interface FinanceDocument {
|
|
322
|
+
id: string;
|
|
323
|
+
title: string;
|
|
324
|
+
category: 'report' | 'statement' | 'tax' | 'invoice' | 'other';
|
|
325
|
+
periodLabel: string | null;
|
|
326
|
+
notes: string | null;
|
|
327
|
+
filename: string | null;
|
|
328
|
+
contentType: string;
|
|
329
|
+
bytes: number;
|
|
330
|
+
source: 'upload' | 'api' | 'fetch' | 'cloud';
|
|
331
|
+
institutionKey: string | null;
|
|
332
|
+
taxYear: number | null;
|
|
333
|
+
docType: string | null;
|
|
334
|
+
uploadedBy: string | null;
|
|
335
|
+
createdAt: string;
|
|
336
|
+
downloadUrl: string;
|
|
337
|
+
}
|
|
338
|
+
export function uploadFinanceDocument(
|
|
339
|
+
client: CoinPayClient,
|
|
340
|
+
options: {
|
|
341
|
+
file: Uint8Array | Blob;
|
|
342
|
+
filename?: string;
|
|
343
|
+
title?: string;
|
|
344
|
+
category?: 'report' | 'statement' | 'tax' | 'invoice' | 'other';
|
|
345
|
+
periodLabel?: string | null;
|
|
346
|
+
notes?: string | null;
|
|
347
|
+
contentType?: string;
|
|
348
|
+
source?: 'fetch';
|
|
349
|
+
institutionKey?: string;
|
|
350
|
+
taxYear?: number | null;
|
|
351
|
+
docType?: string | null;
|
|
352
|
+
},
|
|
353
|
+
): Promise<{ document: FinanceDocument; duplicate: boolean }>;
|
|
354
|
+
export function listFinanceDocuments(client: CoinPayClient, options?: { category?: FinanceDocument['category']; limit?: number }): Promise<FinanceDocument[]>;
|
|
355
|
+
export function getFinanceDocument(client: CoinPayClient, documentId: string): Promise<FinanceDocument>;
|
|
320
356
|
export function listFinancePayloads(client: CoinPayClient, options?: { connectionId?: string; limit?: number; offset?: number }): Promise<{ payloads: Array<Record<string, unknown>>; total: number }>;
|
|
321
357
|
export function downloadFinancePayload(client: CoinPayClient, payloadId: string): Promise<BinaryResponse>;
|
|
322
358
|
|
|
@@ -350,8 +386,8 @@ export function sendFinanceReportEmail(
|
|
|
350
386
|
): Promise<SendOutcome>;
|
|
351
387
|
export function sendBooksEmail(
|
|
352
388
|
client: CoinPayClient,
|
|
353
|
-
options: { to: string | string[]; period?: string; from?: string; toDate?: string; timezone?: string; scope?: 'business' | 'personal' | 'all'; formats?: string[]; message?: string | null; attach?: boolean; expiresInDays?: number },
|
|
354
|
-
): Promise<SendOutcome & { unreviewed: number; rows: number }>;
|
|
389
|
+
options: { to: string | string[]; period?: string; from?: string; toDate?: string; timezone?: string; scope?: 'business' | 'personal' | 'all'; formats?: string[]; message?: string | null; attach?: boolean; expiresInDays?: number; withDocuments?: boolean },
|
|
390
|
+
): Promise<SendOutcome & { unreviewed: number; rows: number; documents?: { attached: string[]; skipped: Array<{ title: string; bytes: number; reason: string }> } }>;
|
|
355
391
|
export function getWeeklyDigest(client: CoinPayClient): Promise<WeeklyDigest | null>;
|
|
356
392
|
export function setWeeklyDigest(
|
|
357
393
|
client: CoinPayClient,
|
|
@@ -359,3 +395,44 @@ export function setWeeklyDigest(
|
|
|
359
395
|
): Promise<WeeklyDigest>;
|
|
360
396
|
export function deleteWeeklyDigest(client: CoinPayClient): Promise<{ success: boolean }>;
|
|
361
397
|
export function sendWeeklyDigestNow(client: CoinPayClient): Promise<SendOutcome>;
|
|
398
|
+
|
|
399
|
+
export interface DueItem {
|
|
400
|
+
source: 'projected' | 'scheduled';
|
|
401
|
+
payee: string;
|
|
402
|
+
amount: number | null;
|
|
403
|
+
owed: number | null;
|
|
404
|
+
cadence: string;
|
|
405
|
+
dueOn: string;
|
|
406
|
+
daysUntil: number;
|
|
407
|
+
status: 'overdue' | 'awaiting' | 'today' | 'soon' | 'paid';
|
|
408
|
+
accountId: string;
|
|
409
|
+
accountName: string;
|
|
410
|
+
scope: 'business' | 'personal';
|
|
411
|
+
isDebtService: boolean;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export interface DueSoon {
|
|
415
|
+
timezone: string;
|
|
416
|
+
today: string;
|
|
417
|
+
horizonDays: number;
|
|
418
|
+
items: DueItem[];
|
|
419
|
+
paid: DueItem[];
|
|
420
|
+
missed: Array<{ payee: string; amount: number; expectedOn: string; lastSeen: string; accountName: string }>;
|
|
421
|
+
totals: { due: number; overdue: number; unpriced: number };
|
|
422
|
+
recurring: {
|
|
423
|
+
charges: Array<{ payee: string; amount: number; cadence: string; occurrences: number; lastSeen: string; nextExpected: string; monthlyEquivalent: number; scope: 'business' | 'personal'; isDebtService: boolean; accountId: string; accountName: string }>;
|
|
424
|
+
monthlyTotal: number;
|
|
425
|
+
monthlyDebtService: number;
|
|
426
|
+
};
|
|
427
|
+
currency: string | null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function getDueSoon(client: CoinPayClient, opts?: { days?: number; timezone?: string }): Promise<DueSoon>;
|
|
431
|
+
export function getDailySummary(client: CoinPayClient): Promise<WeeklyDigest | null>;
|
|
432
|
+
export function setDailySummary(
|
|
433
|
+
client: CoinPayClient,
|
|
434
|
+
opts?: { weekdays?: number[]; hour?: number; timezone?: string; recipients?: string[]; active?: boolean },
|
|
435
|
+
): Promise<WeeklyDigest>;
|
|
436
|
+
export function deleteDailySummary(client: CoinPayClient): Promise<{ success: boolean }>;
|
|
437
|
+
export function sendDailySummaryNow(client: CoinPayClient): Promise<{ sent: string[]; failed: Array<{ to: string; error: string }>; due: number; overdue: number }>;
|
|
438
|
+
export function setAccountDueDay(client: CoinPayClient, accountId: string, opts: { day: number | null; amount?: number | string | null }): Promise<Record<string, unknown>>;
|