@profullstack/coinpay 0.8.0 → 0.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 +77 -0
- package/README.md +136 -3
- package/bin/coinpay.js +1057 -41
- package/package.json +37 -9
- package/src/card-payments.js +23 -14
- package/src/client.d.ts +10 -0
- package/src/client.js +131 -2
- package/src/finances-commands.js +423 -0
- package/src/finances-report.js +458 -0
- package/src/finances-reports.d.ts +243 -0
- package/src/finances-reports.js +257 -0
- package/src/finances-tui.js +1019 -0
- package/src/finances.d.ts +365 -0
- package/src/finances.js +511 -0
- package/src/index.d.ts +4 -0
- package/src/index.js +38 -0
- package/src/invoices.d.ts +1 -0
- package/src/invoices.js +14 -1
- package/src/lightning.js +8 -3
- package/src/pdf.js +151 -0
- package/src/swap.js +36 -5
- package/src/taskmarket.js +329 -0
- package/src/wallet.js +54 -24
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { CoinPayClient } from './client.js';
|
|
2
|
+
|
|
3
|
+
export type ProviderCoverage = 'unknown' | 'partial' | 'available_window_fetched';
|
|
4
|
+
export type ReconciliationStatus = 'not_attempted' | 'unavailable' | 'mismatch' | 'user_reconciled';
|
|
5
|
+
export type ReportFormat = 'pdf' | 'html' | 'csv' | 'json';
|
|
6
|
+
export type ReportStatus = 'queued' | 'generating' | 'ready' | 'failed' | 'superseded' | 'deleted';
|
|
7
|
+
export type JobStatus = 'queued' | 'running' | 'waiting_for_budget' | 'partial' | 'failed' | 'completed' | 'cancelled';
|
|
8
|
+
export type JobKind = 'backfill' | 'refresh' | 'scheduled_sync' | 'report';
|
|
9
|
+
|
|
10
|
+
/** Thrown by every call here when the server answers with its structured error body. */
|
|
11
|
+
export interface FinanceApiError extends Error {
|
|
12
|
+
code?: string;
|
|
13
|
+
status?: number;
|
|
14
|
+
retryable?: boolean;
|
|
15
|
+
jobId?: string;
|
|
16
|
+
reportId?: string;
|
|
17
|
+
retryAfter?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FinanceJob {
|
|
21
|
+
id: string;
|
|
22
|
+
kind: JobKind;
|
|
23
|
+
connectionId: string | null;
|
|
24
|
+
status: JobStatus;
|
|
25
|
+
params: Record<string, unknown>;
|
|
26
|
+
progress: Record<string, unknown>;
|
|
27
|
+
result: Record<string, unknown> | null;
|
|
28
|
+
errorCode: string | null;
|
|
29
|
+
errorMessage: string | null;
|
|
30
|
+
attempts: number;
|
|
31
|
+
nextAttemptAt: string | null;
|
|
32
|
+
cancelRequested: boolean;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
startedAt: string | null;
|
|
35
|
+
finishedAt: string | null;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FinanceReportPeriod {
|
|
40
|
+
kind: 'month' | 'quarter' | 'custom';
|
|
41
|
+
selector: string;
|
|
42
|
+
label: string;
|
|
43
|
+
timezone: string;
|
|
44
|
+
start: string;
|
|
45
|
+
end: string;
|
|
46
|
+
effectiveEnd: string;
|
|
47
|
+
periodToDate: boolean;
|
|
48
|
+
cutoff: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FinanceReportTotals {
|
|
52
|
+
currency: string;
|
|
53
|
+
credits: string;
|
|
54
|
+
debits: string;
|
|
55
|
+
net: string;
|
|
56
|
+
rows: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FinanceReport {
|
|
60
|
+
id: string;
|
|
61
|
+
revision: number;
|
|
62
|
+
supersedesReportId: string | null;
|
|
63
|
+
supersededByReportId: string | null;
|
|
64
|
+
jobId: string | null;
|
|
65
|
+
period: FinanceReportPeriod;
|
|
66
|
+
scope: 'all' | 'business' | 'personal';
|
|
67
|
+
accountIds: string[];
|
|
68
|
+
includeHidden: boolean;
|
|
69
|
+
includePending: boolean;
|
|
70
|
+
strict: boolean;
|
|
71
|
+
status: ReportStatus;
|
|
72
|
+
local_export_complete: boolean | null;
|
|
73
|
+
provider_coverage: ProviderCoverage | null;
|
|
74
|
+
reconciliation_status: ReconciliationStatus;
|
|
75
|
+
datasetHash: string | null;
|
|
76
|
+
rowCount: number | null;
|
|
77
|
+
pendingCount: number | null;
|
|
78
|
+
totals: FinanceReportTotals[] | null;
|
|
79
|
+
warnings: string[];
|
|
80
|
+
rendererVersion: string | null;
|
|
81
|
+
errorCode: string | null;
|
|
82
|
+
errorMessage: string | null;
|
|
83
|
+
artifacts: Array<{ format: ReportFormat; bytes: number; sha256: string }>;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
generatedAt: string | null;
|
|
86
|
+
notice: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface BinaryResponse {
|
|
90
|
+
bytes: Uint8Array;
|
|
91
|
+
contentType: string;
|
|
92
|
+
filename: string | null;
|
|
93
|
+
sha256: string | null;
|
|
94
|
+
headers: Record<string, string>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface FinanceStatement {
|
|
98
|
+
id: string;
|
|
99
|
+
accountId: string;
|
|
100
|
+
institutionLabel: string | null;
|
|
101
|
+
label: string;
|
|
102
|
+
cycle: 'monthly' | 'quarterly' | 'custom';
|
|
103
|
+
periodStart: string;
|
|
104
|
+
periodEnd: string;
|
|
105
|
+
timezone: string;
|
|
106
|
+
notes: string | null;
|
|
107
|
+
originalFilename: string | null;
|
|
108
|
+
contentType: string;
|
|
109
|
+
bytes: number;
|
|
110
|
+
sha256: string;
|
|
111
|
+
scanState: 'quarantined' | 'clean' | 'rejected';
|
|
112
|
+
scanReason: string | null;
|
|
113
|
+
provenance: string;
|
|
114
|
+
verified: false;
|
|
115
|
+
reconciliation: FinanceReconciliation | null;
|
|
116
|
+
createdAt: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface FinanceReconciliation {
|
|
120
|
+
id: string;
|
|
121
|
+
statementId: string;
|
|
122
|
+
reportId: string;
|
|
123
|
+
accountId: string;
|
|
124
|
+
currency: string;
|
|
125
|
+
entered: { opening: string; closing: string; credits: string | null; debits: string | null };
|
|
126
|
+
signConvention: 'as_stated' | 'liability_positive';
|
|
127
|
+
normalized: { opening: string; closing: string };
|
|
128
|
+
expectedClosing: string;
|
|
129
|
+
difference: string;
|
|
130
|
+
state: 'matched' | 'mismatch' | 'unavailable' | 'user_reconciled' | 'invalidated';
|
|
131
|
+
acknowledged: boolean;
|
|
132
|
+
datasetHash: string | null;
|
|
133
|
+
version: number;
|
|
134
|
+
createdAt: string;
|
|
135
|
+
invalidatedAt: string | null;
|
|
136
|
+
bankVerified: false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface AccountCoverage {
|
|
140
|
+
accountId: string;
|
|
141
|
+
name: string | null;
|
|
142
|
+
org_name: string | null;
|
|
143
|
+
currency: string | null;
|
|
144
|
+
coverage: ProviderCoverage;
|
|
145
|
+
fraction: number;
|
|
146
|
+
gaps: Array<{ start: string; end: string }>;
|
|
147
|
+
warnings: string[];
|
|
148
|
+
capped: boolean;
|
|
149
|
+
rowsRejected: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface PeriodSelection {
|
|
153
|
+
period?: string;
|
|
154
|
+
from?: string;
|
|
155
|
+
to?: string;
|
|
156
|
+
timezone?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function connectSimpleFin(
|
|
160
|
+
client: CoinPayClient,
|
|
161
|
+
options: { setupToken: string; label?: string; protocolVersion?: 1 | 2; idempotencyKey?: string },
|
|
162
|
+
): Promise<{ connection: Record<string, unknown>; replayed?: boolean }>;
|
|
163
|
+
export function disconnectFinanceConnection(client: CoinPayClient, connectionId: string): Promise<{ connection: Record<string, unknown>; note: string }>;
|
|
164
|
+
export function describeFinanceConnection(
|
|
165
|
+
client: CoinPayClient,
|
|
166
|
+
connectionId: string,
|
|
167
|
+
): Promise<{ connection: Record<string, unknown>; deletionWouldRemove: { accounts: number; transactions: number; statements: number; reports: number } }>;
|
|
168
|
+
export function deleteFinanceConnection(client: CoinPayClient, connectionId: string, options: { confirm: true }): Promise<{ success: boolean; removed: Record<string, number> }>;
|
|
169
|
+
export function setFinanceSyncConsent(client: CoinPayClient, connectionId: string, dailySync: boolean): Promise<{ connection: Record<string, unknown> }>;
|
|
170
|
+
|
|
171
|
+
export function createBackfillJob(
|
|
172
|
+
client: CoinPayClient,
|
|
173
|
+
options: PeriodSelection & { connectionId?: string; idempotencyKey?: string },
|
|
174
|
+
): Promise<{ jobs: FinanceJob[]; job: FinanceJob; timezone?: { timezone: string; source: 'request' | 'saved' } }>;
|
|
175
|
+
export function createRefreshJob(client: CoinPayClient, options: { connectionId: string; days?: number; idempotencyKey?: string }): Promise<{ jobs: FinanceJob[]; job: FinanceJob }>;
|
|
176
|
+
export function getFinanceJob(client: CoinPayClient, jobId: string): Promise<FinanceJob>;
|
|
177
|
+
export function listFinanceJobs(client: CoinPayClient, options?: { limit?: number; connectionId?: string }): Promise<FinanceJob[]>;
|
|
178
|
+
export function cancelFinanceJob(client: CoinPayClient, jobId: string): Promise<FinanceJob>;
|
|
179
|
+
export function waitForFinanceJob(
|
|
180
|
+
client: CoinPayClient,
|
|
181
|
+
jobId: string,
|
|
182
|
+
options?: { intervalMs?: number; timeoutMs?: number; onProgress?: (job: FinanceJob) => void; sleep?: (ms: number) => Promise<void> },
|
|
183
|
+
): Promise<FinanceJob>;
|
|
184
|
+
|
|
185
|
+
export function getFinanceCoverage(
|
|
186
|
+
client: CoinPayClient,
|
|
187
|
+
options: PeriodSelection & { accountIds?: string[]; includeHidden?: boolean },
|
|
188
|
+
): Promise<{ period: FinanceReportPeriod; provider_coverage: ProviderCoverage; accounts: AccountCoverage[] }>;
|
|
189
|
+
|
|
190
|
+
export function createFinanceReport(
|
|
191
|
+
client: CoinPayClient,
|
|
192
|
+
options: PeriodSelection & {
|
|
193
|
+
accountIds?: string[];
|
|
194
|
+
scope?: 'all' | 'business' | 'personal';
|
|
195
|
+
includeHidden?: boolean;
|
|
196
|
+
includePendingAppendix?: boolean;
|
|
197
|
+
formats?: ReportFormat[];
|
|
198
|
+
strict?: boolean;
|
|
199
|
+
idempotencyKey?: string;
|
|
200
|
+
},
|
|
201
|
+
): Promise<{ report: FinanceReport; job: FinanceJob }>;
|
|
202
|
+
export function listFinanceReports(client: CoinPayClient, options?: { limit?: number; offset?: number }): Promise<{ reports: FinanceReport[]; total: number }>;
|
|
203
|
+
export function getFinanceReport(client: CoinPayClient, reportId: string): Promise<{ report: FinanceReport; job: FinanceJob | null }>;
|
|
204
|
+
export function deleteFinanceReport(client: CoinPayClient, reportId: string): Promise<{ success: boolean; note: string }>;
|
|
205
|
+
export function waitForFinanceReport(
|
|
206
|
+
client: CoinPayClient,
|
|
207
|
+
reportId: string,
|
|
208
|
+
options?: { intervalMs?: number; timeoutMs?: number; onProgress?: (data: { report: FinanceReport; job: FinanceJob | null }) => void; sleep?: (ms: number) => Promise<void> },
|
|
209
|
+
): Promise<{ report: FinanceReport; job: FinanceJob | null }>;
|
|
210
|
+
export function downloadFinanceReport(client: CoinPayClient, reportId: string, options?: { format?: ReportFormat }): Promise<BinaryResponse>;
|
|
211
|
+
|
|
212
|
+
export function importFinanceStatement(
|
|
213
|
+
client: CoinPayClient,
|
|
214
|
+
options: PeriodSelection & {
|
|
215
|
+
file: Uint8Array | Blob;
|
|
216
|
+
filename?: string;
|
|
217
|
+
accountId: string;
|
|
218
|
+
institutionLabel?: string;
|
|
219
|
+
cycle?: 'monthly' | 'quarterly' | 'custom';
|
|
220
|
+
notes?: string;
|
|
221
|
+
},
|
|
222
|
+
): Promise<{ statement: FinanceStatement; duplicateOf: string | null; note: string }>;
|
|
223
|
+
export function listFinanceStatements(
|
|
224
|
+
client: CoinPayClient,
|
|
225
|
+
options?: PeriodSelection & { accountId?: string; limit?: number },
|
|
226
|
+
): Promise<FinanceStatement[]>;
|
|
227
|
+
export function getFinanceStatement(client: CoinPayClient, statementId: string): Promise<{ statement: FinanceStatement; reconciliations: FinanceReconciliation[] }>;
|
|
228
|
+
export function downloadFinanceStatement(client: CoinPayClient, statementId: string): Promise<BinaryResponse>;
|
|
229
|
+
export function deleteFinanceStatement(client: CoinPayClient, statementId: string): Promise<{ success: boolean; note: string }>;
|
|
230
|
+
export function reconcileFinanceStatement(
|
|
231
|
+
client: CoinPayClient,
|
|
232
|
+
statementId: string,
|
|
233
|
+
options: {
|
|
234
|
+
reportId: string;
|
|
235
|
+
opening: string;
|
|
236
|
+
closing: string;
|
|
237
|
+
credits?: string;
|
|
238
|
+
debits?: string;
|
|
239
|
+
currency: string;
|
|
240
|
+
signConvention?: 'as_stated' | 'liability_positive';
|
|
241
|
+
acknowledge?: boolean;
|
|
242
|
+
},
|
|
243
|
+
): Promise<FinanceReconciliation>;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finance reports, sync jobs, coverage and the statement library.
|
|
3
|
+
*
|
|
4
|
+
* Every function takes a `CoinPayClient` authenticated with a merchant
|
|
5
|
+
* session. Nothing here computes a total: the server freezes each report's
|
|
6
|
+
* rows and sums them exactly, and these calls fetch what it produced. A
|
|
7
|
+
* download returns bytes, never a JSON-wrapped blob.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function query(params) {
|
|
11
|
+
const search = new URLSearchParams();
|
|
12
|
+
for (const [key, value] of Object.entries(params)) {
|
|
13
|
+
if (value === undefined || value === null || value === '') continue;
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
for (const v of value) search.append(key, String(v));
|
|
16
|
+
} else {
|
|
17
|
+
search.set(key, String(value));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const s = search.toString();
|
|
21
|
+
return s ? `?${s}` : '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function headersWith(idempotencyKey) {
|
|
25
|
+
return idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Surface the server's structured error `{ error: { code, message } }` as an Error with `.code`. */
|
|
29
|
+
async function call(client, endpoint, options = {}) {
|
|
30
|
+
try {
|
|
31
|
+
return await client.request(endpoint, options);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const body = err && err.response && err.response.error;
|
|
34
|
+
if (body && typeof body === 'object') {
|
|
35
|
+
const wrapped = new Error(body.message || err.message);
|
|
36
|
+
wrapped.code = body.code;
|
|
37
|
+
wrapped.status = err.status;
|
|
38
|
+
wrapped.retryable = body.retryable === true;
|
|
39
|
+
wrapped.jobId = body.jobId;
|
|
40
|
+
wrapped.reportId = body.reportId;
|
|
41
|
+
wrapped.retryAfter = body.retryAfter;
|
|
42
|
+
wrapped.response = err.response;
|
|
43
|
+
throw wrapped;
|
|
44
|
+
}
|
|
45
|
+
if (err && err.response && typeof err.response.code === 'string' && !err.code) err.code = err.response.code;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Connections ──
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Claim a SimpleFIN setup token. The token is single-use; the server checks
|
|
54
|
+
* everything it can before spending it and says plainly when the outcome is
|
|
55
|
+
* unknown (`err.code === 'claim_outcome_unknown'`).
|
|
56
|
+
*/
|
|
57
|
+
export async function connectSimpleFin(client, { setupToken, label, protocolVersion, idempotencyKey } = {}) {
|
|
58
|
+
if (!setupToken || !String(setupToken).trim()) throw new Error('A setup token is required');
|
|
59
|
+
return call(client, '/finances/connections', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: headersWith(idempotencyKey),
|
|
62
|
+
body: JSON.stringify({ setupToken: String(setupToken).trim(), label, protocolVersion }),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Stop future syncs and remove the credential; history is kept. */
|
|
67
|
+
export async function disconnectFinanceConnection(client, connectionId) {
|
|
68
|
+
return call(client, `/finances/connections/${encodeURIComponent(connectionId)}/disconnect`, { method: 'POST', body: '{}' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What deleting a connection would remove. */
|
|
72
|
+
export async function describeFinanceConnection(client, connectionId) {
|
|
73
|
+
return call(client, `/finances/connections/${encodeURIComponent(connectionId)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Delete a connection and everything under it. `confirm` must be true. */
|
|
77
|
+
export async function deleteFinanceConnection(client, connectionId, { confirm = false } = {}) {
|
|
78
|
+
if (!confirm) throw new Error('Deleting a connection removes its account data; pass { confirm: true }');
|
|
79
|
+
return call(client, `/finances/connections/${encodeURIComponent(connectionId)}?confirm=yes`, {
|
|
80
|
+
method: 'DELETE',
|
|
81
|
+
headers: { 'X-Confirm-Delete': 'yes' },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Opt a connection in or out of a once-daily background sync. */
|
|
86
|
+
export async function setFinanceSyncConsent(client, connectionId, dailySync) {
|
|
87
|
+
return call(client, `/finances/connections/${encodeURIComponent(connectionId)}/consent`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: JSON.stringify({ dailySync: dailySync === true }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Jobs ──
|
|
94
|
+
|
|
95
|
+
/** Queue a period backfill (`{ period: '2026-Q2' }` or `{ from, to }`). Answers 202 with the job(s). */
|
|
96
|
+
export async function createBackfillJob(client, { connectionId, period, from, to, timezone, idempotencyKey } = {}) {
|
|
97
|
+
return call(client, '/finances/sync-jobs', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: headersWith(idempotencyKey),
|
|
100
|
+
body: JSON.stringify({ kind: 'backfill', connectionId, period, from, to, timezone }),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Queue a background rolling refresh of one connection. */
|
|
105
|
+
export async function createRefreshJob(client, { connectionId, days, idempotencyKey } = {}) {
|
|
106
|
+
return call(client, '/finances/sync-jobs', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: headersWith(idempotencyKey),
|
|
109
|
+
body: JSON.stringify({ kind: 'refresh', connectionId, days }),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function getFinanceJob(client, jobId) {
|
|
114
|
+
const data = await call(client, `/finances/jobs/${encodeURIComponent(jobId)}`);
|
|
115
|
+
return data.job;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function listFinanceJobs(client, { limit, connectionId } = {}) {
|
|
119
|
+
const data = await call(client, `/finances/jobs${query({ limit, connection: connectionId })}`);
|
|
120
|
+
return data.jobs || [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function cancelFinanceJob(client, jobId) {
|
|
124
|
+
const data = await call(client, `/finances/jobs/${encodeURIComponent(jobId)}/cancel`, { method: 'POST', body: '{}' });
|
|
125
|
+
return data.job;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const TERMINAL_JOB = new Set(['completed', 'failed', 'cancelled', 'partial']);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Poll a job until it reaches a terminal state or `waiting_for_budget`
|
|
132
|
+
* (which can take hours to clear and is reported, not waited out).
|
|
133
|
+
*/
|
|
134
|
+
export async function waitForFinanceJob(client, jobId, { intervalMs = 3000, timeoutMs = 30 * 60 * 1000, onProgress, sleep } = {}) {
|
|
135
|
+
const started = Date.now();
|
|
136
|
+
const pause = sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
137
|
+
for (;;) {
|
|
138
|
+
const job = await getFinanceJob(client, jobId);
|
|
139
|
+
if (onProgress) onProgress(job);
|
|
140
|
+
if (TERMINAL_JOB.has(job.status) || job.status === 'waiting_for_budget') return job;
|
|
141
|
+
if (Date.now() - started > timeoutMs) return job;
|
|
142
|
+
await pause(intervalMs);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Coverage ──
|
|
147
|
+
|
|
148
|
+
export async function getFinanceCoverage(client, { period, from, to, timezone, accountIds, includeHidden } = {}) {
|
|
149
|
+
return call(client, `/finances/coverage${query({ period, from, to, timezone, account: accountIds, hidden: includeHidden ? 1 : undefined })}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Reports ──
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create an immutable report revision. Answers 202 with `{ report, job }`;
|
|
156
|
+
* use `waitForFinanceReport` to block until it is ready.
|
|
157
|
+
*/
|
|
158
|
+
export async function createFinanceReport(client, {
|
|
159
|
+
period, from, to, timezone, accountIds, scope, includeHidden, includePendingAppendix, formats, strict, idempotencyKey,
|
|
160
|
+
} = {}) {
|
|
161
|
+
return call(client, '/finances/reports', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: headersWith(idempotencyKey),
|
|
164
|
+
body: JSON.stringify({ period, from, to, timezone, accountIds, scope, includeHidden, includePendingAppendix, formats, strict }),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function listFinanceReports(client, { limit, offset } = {}) {
|
|
169
|
+
return call(client, `/finances/reports${query({ limit, offset })}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function getFinanceReport(client, reportId) {
|
|
173
|
+
return call(client, `/finances/reports/${encodeURIComponent(reportId)}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function deleteFinanceReport(client, reportId) {
|
|
177
|
+
return call(client, `/finances/reports/${encodeURIComponent(reportId)}`, { method: 'DELETE' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const TERMINAL_REPORT = new Set(['ready', 'failed', 'superseded', 'deleted']);
|
|
181
|
+
|
|
182
|
+
export async function waitForFinanceReport(client, reportId, { intervalMs = 3000, timeoutMs = 30 * 60 * 1000, onProgress, sleep } = {}) {
|
|
183
|
+
const started = Date.now();
|
|
184
|
+
const pause = sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
185
|
+
for (;;) {
|
|
186
|
+
const data = await getFinanceReport(client, reportId);
|
|
187
|
+
if (onProgress) onProgress(data);
|
|
188
|
+
if (TERMINAL_REPORT.has(data.report.status)) return data;
|
|
189
|
+
if (data.job && data.job.status === 'waiting_for_budget') return data;
|
|
190
|
+
if (Date.now() - started > timeoutMs) return data;
|
|
191
|
+
await pause(intervalMs);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Download one format of a ready report. Resolves to
|
|
197
|
+
* `{ bytes: Uint8Array, contentType, filename, sha256, headers }`.
|
|
198
|
+
*/
|
|
199
|
+
export async function downloadFinanceReport(client, reportId, { format = 'pdf' } = {}) {
|
|
200
|
+
return client.requestBinary(`/finances/reports/${encodeURIComponent(reportId)}/download${query({ format })}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Statements ──
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Import an original PDF. `file` is a Uint8Array/Buffer/Blob of the bytes;
|
|
207
|
+
* nothing is read out of it into the ledger.
|
|
208
|
+
*/
|
|
209
|
+
export async function importFinanceStatement(client, {
|
|
210
|
+
file, filename = 'statement.pdf', accountId, period, from, to, timezone, institutionLabel, cycle, notes,
|
|
211
|
+
} = {}) {
|
|
212
|
+
if (!file) throw new Error('A PDF file is required');
|
|
213
|
+
if (!accountId) throw new Error('accountId is required');
|
|
214
|
+
const form = new FormData();
|
|
215
|
+
const blob = file instanceof Blob ? file : new Blob([file], { type: 'application/pdf' });
|
|
216
|
+
form.append('file', blob, filename);
|
|
217
|
+
form.append('accountId', accountId);
|
|
218
|
+
if (period) form.append('period', period);
|
|
219
|
+
if (from) form.append('from', from);
|
|
220
|
+
if (to) form.append('to', to);
|
|
221
|
+
if (timezone) form.append('timezone', timezone);
|
|
222
|
+
if (institutionLabel) form.append('institutionLabel', institutionLabel);
|
|
223
|
+
if (cycle) form.append('cycle', cycle);
|
|
224
|
+
if (notes) form.append('notes', notes);
|
|
225
|
+
return client.requestForm('/finances/statements', form);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function listFinanceStatements(client, { accountId, period, from, to, timezone, limit } = {}) {
|
|
229
|
+
const data = await call(client, `/finances/statements${query({ account: accountId, period, from, to, timezone, limit })}`);
|
|
230
|
+
return data.statements || [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function getFinanceStatement(client, statementId) {
|
|
234
|
+
return call(client, `/finances/statements/${encodeURIComponent(statementId)}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function downloadFinanceStatement(client, statementId) {
|
|
238
|
+
return client.requestBinary(`/finances/statements/${encodeURIComponent(statementId)}/download`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function deleteFinanceStatement(client, statementId) {
|
|
242
|
+
return call(client, `/finances/statements/${encodeURIComponent(statementId)}`, { method: 'DELETE' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Enter a statement's balances and check them against one report revision.
|
|
247
|
+
* Amounts are decimal strings in the account currency.
|
|
248
|
+
*/
|
|
249
|
+
export async function reconcileFinanceStatement(client, statementId, {
|
|
250
|
+
reportId, opening, closing, credits, debits, currency, signConvention, acknowledge,
|
|
251
|
+
} = {}) {
|
|
252
|
+
const data = await call(client, `/finances/statements/${encodeURIComponent(statementId)}/reconciliations`, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
body: JSON.stringify({ reportId, opening, closing, credits, debits, currency, signConvention, acknowledge }),
|
|
255
|
+
});
|
|
256
|
+
return data.reconciliation;
|
|
257
|
+
}
|