@profullstack/coinpay 0.6.11 → 0.9.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 +39 -0
- package/README.md +189 -0
- package/bin/coinpay.js +1869 -93
- package/package.json +41 -9
- package/src/auth.js +2 -1
- package/src/card-payments.js +23 -14
- package/src/client.d.ts +52 -0
- package/src/client.js +93 -9
- package/src/finances-tui.js +1005 -0
- package/src/finances.d.ts +333 -0
- package/src/finances.js +445 -0
- package/src/index.d.ts +37 -1
- package/src/index.js +67 -0
- package/src/invoices.d.ts +78 -0
- package/src/invoices.js +250 -0
- package/src/lightning.js +8 -3
- package/src/swap.js +36 -5
- package/src/taskmarket.js +329 -0
- package/src/tokens.d.ts +14 -0
- package/src/tokens.js +34 -0
- package/src/wallet.d.ts +11 -0
- package/src/wallet.js +283 -25
- package/src/webhooks.js +37 -3
- package/src/x402-browser.js +318 -0
- package/src/x402-pay-server.js +272 -0
- package/src/x402-v2.js +416 -0
- package/src/x402.js +164 -19
package/src/finances.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finances — the merchant's money in one place.
|
|
3
|
+
*
|
|
4
|
+
* Read-side wrappers over the CoinPay API for everything that has a dollar
|
|
5
|
+
* sign on it: linked bank and card accounts (SimpleFIN / Plaid), the ledger,
|
|
6
|
+
* crypto payments, card payments, escrows, invoices and payouts. Plus
|
|
7
|
+
* `buildFinanceSnapshot`, a pure function that turns those responses into the
|
|
8
|
+
* headline numbers a dashboard wants (earnings, commission paid, refunds,
|
|
9
|
+
* cashflow, outstanding invoices, escrow held).
|
|
10
|
+
*
|
|
11
|
+
* Every call needs the merchant session token (`coinpay login`), not a
|
|
12
|
+
* business API key: the bank-data routes deliberately refuse API keys.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const CRYPTO_SUCCESS = new Set(['confirmed', 'completed', 'forwarded', 'forwarding']);
|
|
16
|
+
const CRYPTO_FAILED = new Set(['failed', 'expired', 'forwarding_failed', 'settle_failed', 'settlement_failed']);
|
|
17
|
+
const CRYPTO_PENDING = new Set(['pending', 'detected', 'confirming']);
|
|
18
|
+
const CARD_SUCCESS = new Set(['completed', 'succeeded']);
|
|
19
|
+
const CARD_FAILED = new Set(['failed', 'canceled', 'cancelled', 'requires_payment_method']);
|
|
20
|
+
const CARD_REFUNDED = new Set(['refunded', 'partially_refunded']);
|
|
21
|
+
|
|
22
|
+
function query(params) {
|
|
23
|
+
const search = new URLSearchParams();
|
|
24
|
+
for (const [key, value] of Object.entries(params)) {
|
|
25
|
+
if (value === undefined || value === null || value === '') continue;
|
|
26
|
+
search.set(key, String(value));
|
|
27
|
+
}
|
|
28
|
+
const s = search.toString();
|
|
29
|
+
return s ? `?${s}` : '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Map a day window onto the analytics `period` values the API understands. */
|
|
33
|
+
export function periodForDays(days) {
|
|
34
|
+
if (!Number.isFinite(days) || days <= 0) return 'all';
|
|
35
|
+
if (days <= 7) return '7d';
|
|
36
|
+
if (days <= 30) return '30d';
|
|
37
|
+
if (days <= 90) return '90d';
|
|
38
|
+
if (days <= 365) return '1y';
|
|
39
|
+
return 'all';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Bank & card accounts (SimpleFIN / Plaid) ──
|
|
43
|
+
|
|
44
|
+
/** Balance sheet + cashflow headline. `days` sets the cashflow window. */
|
|
45
|
+
export async function getFinanceSummary(client, { days = 30, includeHidden = false } = {}) {
|
|
46
|
+
const data = await client.request(`/finances/summary${query({ days, hidden: includeHidden ? 1 : undefined })}`);
|
|
47
|
+
return data.summary;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Every linked account with its current balance. */
|
|
51
|
+
export async function listFinanceAccounts(client, { includeHidden = false } = {}) {
|
|
52
|
+
const data = await client.request(`/finances/accounts${query({ hidden: includeHidden ? 1 : undefined })}`);
|
|
53
|
+
return data.accounts || [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The ledger, newest first. Returns `{ rows, total, limit, offset }`. */
|
|
57
|
+
export async function listFinanceTransactions(client, filters = {}) {
|
|
58
|
+
return client.request(
|
|
59
|
+
`/finances/transactions${query({
|
|
60
|
+
account: filters.accountId,
|
|
61
|
+
search: filters.search,
|
|
62
|
+
category: filters.category,
|
|
63
|
+
start: filters.startDate,
|
|
64
|
+
end: filters.endDate,
|
|
65
|
+
pending: filters.includePending === false ? 0 : undefined,
|
|
66
|
+
limit: filters.limit,
|
|
67
|
+
offset: filters.offset,
|
|
68
|
+
})}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Linked institutions and their last sync outcome. */
|
|
73
|
+
export async function listFinanceConnections(client) {
|
|
74
|
+
return client.request('/finances/connections');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Pull fresh balances and transactions from the bank bridge. This spends
|
|
79
|
+
* request budget (SimpleFIN allows ~24/day per connection), so it is a
|
|
80
|
+
* deliberate action, never something a screen does on its own.
|
|
81
|
+
*/
|
|
82
|
+
export async function syncFinances(client, { days, connectionId } = {}) {
|
|
83
|
+
return client.request('/finances/sync', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: JSON.stringify({ days, connectionId }),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Payments, cards, payouts ──
|
|
90
|
+
|
|
91
|
+
/** Merchant dashboard headline (plan, commission rate, recent payments). */
|
|
92
|
+
export async function getDashboardStats(client, { businessId } = {}) {
|
|
93
|
+
return client.request(`/dashboard/stats${query({ business_id: businessId })}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Crypto + card analytics for a period (`7d`, `30d`, `90d`, `1y`, `all`). */
|
|
97
|
+
export async function getFinanceAnalytics(client, { period = '30d', businessId } = {}) {
|
|
98
|
+
const data = await client.request(`/stripe/analytics${query({ period, business_id: businessId })}`);
|
|
99
|
+
return data.analytics;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Crypto payments across every business the merchant can see. */
|
|
103
|
+
export async function listCryptoPayments(client, { limit = 100, offset = 0, status, businessId, dateFrom, dateTo } = {}) {
|
|
104
|
+
return client.request(`/payments${query({ limit, offset, status, business_id: businessId, date_from: dateFrom, date_to: dateTo })}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Card (Stripe) charges across every business the merchant can see. */
|
|
108
|
+
export async function listCardTransactions(client, { limit = 100, offset = 0, status, businessId, dateFrom, dateTo } = {}) {
|
|
109
|
+
return client.request(`/stripe/transactions${query({ limit, offset, status, business_id: businessId, date_from: dateFrom, date_to: dateTo })}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Card payouts to the merchant's bank. */
|
|
113
|
+
export async function listCardPayouts(client, { limit = 50, offset = 0, status } = {}) {
|
|
114
|
+
return client.request(`/stripe/payouts${query({ limit, offset, status })}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Snapshot ──
|
|
118
|
+
|
|
119
|
+
function num(value) {
|
|
120
|
+
const n = typeof value === 'number' ? value : parseFloat(value ?? '');
|
|
121
|
+
return Number.isFinite(n) ? n : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function within(dateLike, since) {
|
|
125
|
+
if (!since) return true;
|
|
126
|
+
const t = new Date(dateLike || 0).getTime();
|
|
127
|
+
return Number.isFinite(t) && t >= since.getTime();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function round2(n) {
|
|
131
|
+
return Math.round(n * 100) / 100;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** USD value of a crypto payment's fee, pro-rated from the crypto amounts. */
|
|
135
|
+
export function cryptoFeeUsd(payment) {
|
|
136
|
+
const fee = num(payment.fee_amount);
|
|
137
|
+
const crypto = num(payment.amount_crypto ?? payment.crypto_amount);
|
|
138
|
+
const usd = num(payment.amount_usd ?? payment.amount);
|
|
139
|
+
if (fee > 0 && crypto > 0 && usd > 0) return (fee / crypto) * usd;
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Turn raw API responses into the numbers a dashboard shows.
|
|
145
|
+
*
|
|
146
|
+
* Pure: no I/O, so it is unit-testable and the TUI can re-run it on every
|
|
147
|
+
* refresh without touching the network. Any input may be missing (a route
|
|
148
|
+
* that failed) and the corresponding section is then empty rather than the
|
|
149
|
+
* whole snapshot failing.
|
|
150
|
+
*/
|
|
151
|
+
export function buildFinanceSnapshot(raw = {}, { days = 30, now = new Date() } = {}) {
|
|
152
|
+
const since = Number.isFinite(days) && days > 0 ? new Date(now.getTime() - days * 86400000) : null;
|
|
153
|
+
const errors = { ...(raw.errors || {}) };
|
|
154
|
+
|
|
155
|
+
// ── Crypto payments ──
|
|
156
|
+
const payments = raw.payments?.payments || [];
|
|
157
|
+
const windowPayments = payments.filter((p) => within(p.created_at, since));
|
|
158
|
+
const cryptoOk = windowPayments.filter((p) => CRYPTO_SUCCESS.has(String(p.status).toLowerCase()));
|
|
159
|
+
const cryptoFailed = windowPayments.filter((p) => CRYPTO_FAILED.has(String(p.status).toLowerCase()));
|
|
160
|
+
const cryptoPending = windowPayments.filter((p) => CRYPTO_PENDING.has(String(p.status).toLowerCase()));
|
|
161
|
+
const crypto = {
|
|
162
|
+
volumeUsd: round2(cryptoOk.reduce((s, p) => s + num(p.amount_usd), 0)),
|
|
163
|
+
feesUsd: round2(cryptoOk.reduce((s, p) => s + cryptoFeeUsd(p), 0)),
|
|
164
|
+
successful: cryptoOk.length,
|
|
165
|
+
failed: cryptoFailed.length,
|
|
166
|
+
pending: cryptoPending.length,
|
|
167
|
+
total: windowPayments.length,
|
|
168
|
+
byChain: {},
|
|
169
|
+
// The list is a page; the server-side analytics below cover the full window.
|
|
170
|
+
partial: Boolean(raw.payments?.pagination?.has_more),
|
|
171
|
+
};
|
|
172
|
+
for (const p of cryptoOk) {
|
|
173
|
+
const chain = p.currency || 'unknown';
|
|
174
|
+
crypto.byChain[chain] = round2((crypto.byChain[chain] || 0) + num(p.amount_usd));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Card payments ──
|
|
178
|
+
const cardTx = raw.cardTransactions?.transactions || [];
|
|
179
|
+
const windowCards = cardTx.filter((t) => within(t.created_at, since));
|
|
180
|
+
const cardOk = windowCards.filter((t) => CARD_SUCCESS.has(String(t.status).toLowerCase()));
|
|
181
|
+
const cardFailed = windowCards.filter((t) => CARD_FAILED.has(String(t.status).toLowerCase()));
|
|
182
|
+
const cardRefunded = windowCards.filter((t) => CARD_REFUNDED.has(String(t.status).toLowerCase()));
|
|
183
|
+
const cents = (t, key) => num(t[key]) / 100;
|
|
184
|
+
const card = {
|
|
185
|
+
volumeUsd: round2(cardOk.reduce((s, t) => s + cents(t, 'amount_cents'), 0)),
|
|
186
|
+
platformFeesUsd: round2(cardOk.reduce((s, t) => s + cents(t, 'platform_fee_amount'), 0)),
|
|
187
|
+
stripeFeesUsd: round2(cardOk.reduce((s, t) => s + cents(t, 'stripe_fee_amount'), 0)),
|
|
188
|
+
netUsd: round2(cardOk.reduce((s, t) => s + cents(t, 'net_to_merchant'), 0)),
|
|
189
|
+
refundedUsd: round2(cardRefunded.reduce((s, t) => s + cents(t, 'amount_cents'), 0)),
|
|
190
|
+
successful: cardOk.length,
|
|
191
|
+
failed: cardFailed.length,
|
|
192
|
+
refunded: cardRefunded.length,
|
|
193
|
+
total: windowCards.length,
|
|
194
|
+
partial: Boolean(raw.cardTransactions?.pagination?.has_more),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// ── Server-side analytics (complete over the window, unlike the pages above) ──
|
|
198
|
+
const analytics = raw.analytics || null;
|
|
199
|
+
const series = (analytics?.series?.points || []).map((pt) => ({
|
|
200
|
+
label: pt.label,
|
|
201
|
+
volumeUsd: num(pt.total_volume_usd),
|
|
202
|
+
cryptoUsd: num(pt.crypto_volume_usd),
|
|
203
|
+
cardUsd: num(pt.card_volume_usd),
|
|
204
|
+
commissionUsd: num(pt.total_commission_usd),
|
|
205
|
+
count: num(pt.total_count),
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
const grossVolumeUsd = analytics
|
|
209
|
+
? num(analytics.combined?.total_volume_usd)
|
|
210
|
+
: round2(crypto.volumeUsd + card.volumeUsd);
|
|
211
|
+
const commissionUsd = analytics
|
|
212
|
+
? num(analytics.combined?.total_fees_usd)
|
|
213
|
+
: round2(crypto.feesUsd + card.platformFeesUsd);
|
|
214
|
+
|
|
215
|
+
// ── Escrow ──
|
|
216
|
+
const escrows = raw.escrows?.escrows || raw.escrows?.data || (Array.isArray(raw.escrows) ? raw.escrows : []);
|
|
217
|
+
const escrowStatus = (e) => String(e.status || '').toLowerCase();
|
|
218
|
+
const escrowRefunded = escrows.filter((e) => escrowStatus(e) === 'refunded' && within(e.settled_at || e.refunded_at || e.updated_at, since));
|
|
219
|
+
const escrowHeld = escrows.filter((e) => ['funded', 'disputed', 'pending_release'].includes(escrowStatus(e)));
|
|
220
|
+
const escrowReleased = escrows.filter((e) => ['released', 'settled', 'completed'].includes(escrowStatus(e)) && within(e.settled_at || e.released_at || e.updated_at, since));
|
|
221
|
+
const escrowFeeUsd = (e) => {
|
|
222
|
+
if (!e.fee_tx_hash) return 0;
|
|
223
|
+
const fee = num(e.fee_amount);
|
|
224
|
+
const amount = num(e.amount);
|
|
225
|
+
return fee > 0 && amount > 0 ? (fee / amount) * num(e.amount_usd) : 0;
|
|
226
|
+
};
|
|
227
|
+
const escrow = {
|
|
228
|
+
heldUsd: round2(escrowHeld.reduce((s, e) => s + num(e.amount_usd), 0)),
|
|
229
|
+
held: escrowHeld.length,
|
|
230
|
+
releasedUsd: round2(escrowReleased.reduce((s, e) => s + num(e.amount_usd), 0)),
|
|
231
|
+
released: escrowReleased.length,
|
|
232
|
+
refundedUsd: round2(escrowRefunded.reduce((s, e) => s + num(e.amount_usd), 0)),
|
|
233
|
+
refunded: escrowRefunded.length,
|
|
234
|
+
feesUsd: round2(escrowReleased.reduce((s, e) => s + escrowFeeUsd(e), 0)),
|
|
235
|
+
total: escrows.length,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// ── Invoices ──
|
|
239
|
+
const invoices = raw.invoices?.invoices || raw.invoices?.data || (Array.isArray(raw.invoices) ? raw.invoices : []);
|
|
240
|
+
const invStatus = (i) => String(i.status || '').toLowerCase();
|
|
241
|
+
const isOverdue = (i) => ['sent', 'overdue'].includes(invStatus(i)) && i.due_date && new Date(i.due_date).getTime() < now.getTime();
|
|
242
|
+
const invoiceTotals = { draft: 0, outstanding: 0, overdue: 0, paid: 0, cancelled: 0 };
|
|
243
|
+
const invoiceCounts = { draft: 0, outstanding: 0, overdue: 0, paid: 0, cancelled: 0 };
|
|
244
|
+
for (const inv of invoices) {
|
|
245
|
+
const amount = num(inv.amount);
|
|
246
|
+
const status = invStatus(inv);
|
|
247
|
+
let bucket;
|
|
248
|
+
if (status === 'draft') bucket = 'draft';
|
|
249
|
+
else if (status === 'paid') {
|
|
250
|
+
if (!within(inv.paid_at || inv.updated_at, since)) continue;
|
|
251
|
+
bucket = 'paid';
|
|
252
|
+
} else if (status === 'cancelled' || status === 'canceled') bucket = 'cancelled';
|
|
253
|
+
else if (isOverdue(inv)) bucket = 'overdue';
|
|
254
|
+
else bucket = 'outstanding';
|
|
255
|
+
invoiceTotals[bucket] = round2(invoiceTotals[bucket] + amount);
|
|
256
|
+
invoiceCounts[bucket] += 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Payouts (card rail → bank) ──
|
|
260
|
+
const payouts = raw.payouts?.payouts || [];
|
|
261
|
+
const windowPayouts = payouts.filter((p) => within(p.created_at, since));
|
|
262
|
+
const payout = {
|
|
263
|
+
paidUsd: round2(windowPayouts.filter((p) => String(p.status).toLowerCase() === 'paid').reduce((s, p) => s + num(p.amount_cents) / 100, 0)),
|
|
264
|
+
pendingUsd: round2(windowPayouts.filter((p) => ['pending', 'in_transit'].includes(String(p.status).toLowerCase())).reduce((s, p) => s + num(p.amount_cents) / 100, 0)),
|
|
265
|
+
count: windowPayouts.length,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// ── Bank & cards ──
|
|
269
|
+
const summary = raw.summary || null;
|
|
270
|
+
const accounts = (raw.accounts || []).slice().sort((a, b) => Math.abs(num(b.display_balance ?? b.balance)) - Math.abs(num(a.display_balance ?? a.balance)));
|
|
271
|
+
const primary = summary?.totals?.find((t) => t.currency === summary.primaryCurrency) || summary?.totals?.[0] || null;
|
|
272
|
+
const bank = {
|
|
273
|
+
currency: summary?.primaryCurrency || primary?.currency || 'USD',
|
|
274
|
+
assets: round2(num(primary?.assets)),
|
|
275
|
+
liabilities: round2(num(primary?.liabilities)),
|
|
276
|
+
net: round2(num(primary?.net)),
|
|
277
|
+
accountCount: summary?.accountCount ?? accounts.length,
|
|
278
|
+
hiddenCount: summary?.hiddenCount ?? 0,
|
|
279
|
+
transactionCount: summary?.transactionCount ?? 0,
|
|
280
|
+
cashflow: {
|
|
281
|
+
moneyIn: round2(num(summary?.cashflow?.moneyIn)),
|
|
282
|
+
moneyOut: round2(num(summary?.cashflow?.moneyOut)),
|
|
283
|
+
net: round2(num(summary?.cashflow?.net)),
|
|
284
|
+
transactions: summary?.cashflow?.transactions ?? 0,
|
|
285
|
+
},
|
|
286
|
+
byKind: summary?.byKind || [],
|
|
287
|
+
byInstitution: (summary?.byInstitution || []).slice().sort((a, b) => (b.assets + b.liabilities) - (a.assets + a.liabilities)),
|
|
288
|
+
topCategories: summary?.topCategories || [],
|
|
289
|
+
newestTransaction: summary?.newestTransaction || null,
|
|
290
|
+
accounts,
|
|
291
|
+
creditCards: accounts.filter((a) => (a.effective_kind || a.kind) === 'credit'),
|
|
292
|
+
connections: raw.connections?.connections || [],
|
|
293
|
+
plaidEnabled: Boolean(raw.connections?.plaidEnabled),
|
|
294
|
+
ledger: raw.transactions?.rows || [],
|
|
295
|
+
ledgerTotal: raw.transactions?.total ?? 0,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const refundsUsd = round2(card.refundedUsd + escrow.refundedUsd);
|
|
299
|
+
const stats = raw.stats || null;
|
|
300
|
+
|
|
301
|
+
// Debt against income. Computed server-side, because it needs six months of
|
|
302
|
+
// rows and the ledger this snapshot carries is one page of the window.
|
|
303
|
+
// Absent when the summary route failed, and the screen says so rather than
|
|
304
|
+
// drawing zeros.
|
|
305
|
+
const position = summary?.position || null;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
generatedAt: now.toISOString(),
|
|
309
|
+
windowDays: days,
|
|
310
|
+
plan: stats?.plan || null,
|
|
311
|
+
businesses: stats?.businesses || [],
|
|
312
|
+
earnings: {
|
|
313
|
+
grossVolumeUsd: round2(grossVolumeUsd),
|
|
314
|
+
cryptoVolumeUsd: analytics ? num(analytics.crypto?.total_volume_usd) : crypto.volumeUsd,
|
|
315
|
+
cardVolumeUsd: analytics ? num(analytics.card?.total_volume_usd) : card.volumeUsd,
|
|
316
|
+
commissionUsd: round2(commissionUsd),
|
|
317
|
+
stripeFeesUsd: card.stripeFeesUsd,
|
|
318
|
+
refundsUsd,
|
|
319
|
+
netUsd: round2(grossVolumeUsd - commissionUsd - card.stripeFeesUsd - refundsUsd),
|
|
320
|
+
transactions: analytics ? num(analytics.combined?.successful_transactions) : crypto.successful + card.successful,
|
|
321
|
+
failed: analytics ? num(analytics.combined?.failed_transactions) : crypto.failed + card.failed,
|
|
322
|
+
failureRate: analytics ? num(analytics.combined?.failure_rate) : 0,
|
|
323
|
+
},
|
|
324
|
+
series,
|
|
325
|
+
crypto,
|
|
326
|
+
card,
|
|
327
|
+
escrow,
|
|
328
|
+
invoices: { totals: invoiceTotals, counts: invoiceCounts, rows: invoices },
|
|
329
|
+
payout,
|
|
330
|
+
bank,
|
|
331
|
+
position,
|
|
332
|
+
recent: {
|
|
333
|
+
payments: payments.slice(0, 50),
|
|
334
|
+
cards: cardTx.slice(0, 50),
|
|
335
|
+
escrows: escrows.slice(0, 50),
|
|
336
|
+
},
|
|
337
|
+
errors,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Fetch everything and build a snapshot. Routes fail independently: a 500
|
|
343
|
+
* from one of them lands in `snapshot.errors[name]` and its section is empty.
|
|
344
|
+
*/
|
|
345
|
+
export async function collectFinanceSnapshot(client, { days = 30, limit = 100, businessId } = {}) {
|
|
346
|
+
const since = Number.isFinite(days) && days > 0 ? new Date(Date.now() - days * 86400000).toISOString() : undefined;
|
|
347
|
+
const sources = {
|
|
348
|
+
summary: () => getFinanceSummary(client, { days }),
|
|
349
|
+
accounts: () => listFinanceAccounts(client),
|
|
350
|
+
transactions: () => listFinanceTransactions(client, { limit, startDate: since }),
|
|
351
|
+
connections: () => listFinanceConnections(client),
|
|
352
|
+
stats: () => getDashboardStats(client, { businessId }),
|
|
353
|
+
analytics: () => getFinanceAnalytics(client, { period: periodForDays(days), businessId }),
|
|
354
|
+
payments: () => listCryptoPayments(client, { limit, businessId, dateFrom: since }),
|
|
355
|
+
cardTransactions: () => listCardTransactions(client, { limit, businessId, dateFrom: since }),
|
|
356
|
+
// Raw route on purpose: the SDK's listEscrows camel-cases fields and the
|
|
357
|
+
// snapshot reads the API's snake_case shape like every other source.
|
|
358
|
+
escrows: () => client.request(`/escrow${query({ limit })}`),
|
|
359
|
+
invoices: () => client.request('/invoices'),
|
|
360
|
+
payouts: () => listCardPayouts(client, { limit }),
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const names = Object.keys(sources);
|
|
364
|
+
const settled = await Promise.allSettled(names.map((name) => sources[name]()));
|
|
365
|
+
const raw = { errors: {} };
|
|
366
|
+
settled.forEach((result, i) => {
|
|
367
|
+
if (result.status === 'fulfilled') raw[names[i]] = result.value;
|
|
368
|
+
else raw.errors[names[i]] = result.reason?.message || String(result.reason);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// One failing source is a hole in the picture; every source failing means
|
|
372
|
+
// the API or the session is gone, and a page of zeros would be a lie.
|
|
373
|
+
if (Object.keys(raw.errors).length === names.length) {
|
|
374
|
+
const first = raw.errors[names[0]];
|
|
375
|
+
throw new Error(`Could not load finances: ${first}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return buildFinanceSnapshot(raw, { days });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Subscribe to the merchant's live payment stream (server-sent events).
|
|
383
|
+
*
|
|
384
|
+
* Takes the API base URL and the merchant session token explicitly (the
|
|
385
|
+
* client keeps its key private). `onEvent` receives parsed events (`connected`, `heartbeat`, `payment_*`).
|
|
386
|
+
* Returns a function that closes the stream. Reconnects itself with backoff
|
|
387
|
+
* until closed, so a dropped connection is a gap, not the end.
|
|
388
|
+
*/
|
|
389
|
+
export function subscribeToPayments({ baseUrl, token, onEvent, onStatus, businessId } = {}) {
|
|
390
|
+
if (!baseUrl || !token) throw new Error('subscribeToPayments needs baseUrl and token');
|
|
391
|
+
baseUrl = baseUrl.replace(/\/$/, '');
|
|
392
|
+
let closed = false;
|
|
393
|
+
let controller = null;
|
|
394
|
+
let attempt = 0;
|
|
395
|
+
|
|
396
|
+
const status = (s, detail) => { try { onStatus?.(s, detail); } catch { /* listener error */ } };
|
|
397
|
+
|
|
398
|
+
async function connect() {
|
|
399
|
+
while (!closed) {
|
|
400
|
+
controller = new AbortController();
|
|
401
|
+
try {
|
|
402
|
+
status('connecting');
|
|
403
|
+
const res = await fetch(`${baseUrl}/realtime/payments${query({ businessId })}`, {
|
|
404
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' },
|
|
405
|
+
signal: controller.signal,
|
|
406
|
+
});
|
|
407
|
+
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
408
|
+
attempt = 0;
|
|
409
|
+
status('connected');
|
|
410
|
+
const reader = res.body.getReader();
|
|
411
|
+
const decoder = new TextDecoder();
|
|
412
|
+
let buffer = '';
|
|
413
|
+
for (;;) {
|
|
414
|
+
const { value, done } = await reader.read();
|
|
415
|
+
if (done) break;
|
|
416
|
+
buffer += decoder.decode(value, { stream: true });
|
|
417
|
+
let idx;
|
|
418
|
+
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
419
|
+
const chunk = buffer.slice(0, idx);
|
|
420
|
+
buffer = buffer.slice(idx + 2);
|
|
421
|
+
const data = chunk.split('\n').filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).join('\n');
|
|
422
|
+
if (!data) continue;
|
|
423
|
+
try { onEvent?.(JSON.parse(data)); } catch { /* not JSON */ }
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (!closed) status('disconnected');
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (closed) return;
|
|
429
|
+
status('error', err?.message || String(err));
|
|
430
|
+
}
|
|
431
|
+
if (closed) return;
|
|
432
|
+
attempt += 1;
|
|
433
|
+
const delay = Math.min(30000, 1000 * 2 ** Math.min(attempt, 5));
|
|
434
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
void connect();
|
|
439
|
+
|
|
440
|
+
return () => {
|
|
441
|
+
closed = true;
|
|
442
|
+
try { controller?.abort(); } catch { /* already gone */ }
|
|
443
|
+
status('closed');
|
|
444
|
+
};
|
|
445
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -12,9 +12,22 @@ export type {
|
|
|
12
12
|
PaymentParams,
|
|
13
13
|
ListPaymentsParams,
|
|
14
14
|
WaitForPaymentOptions,
|
|
15
|
-
CreateBusinessParams
|
|
15
|
+
CreateBusinessParams,
|
|
16
|
+
SupportedCoinsParams,
|
|
17
|
+
SupportedCoin,
|
|
18
|
+
SupportedToken,
|
|
19
|
+
SupportedCoinsResponse,
|
|
20
|
+
TokensResponse,
|
|
16
21
|
} from './client.js';
|
|
17
22
|
|
|
23
|
+
export {
|
|
24
|
+
getTokens,
|
|
25
|
+
getSupportedCoins,
|
|
26
|
+
} from './tokens.js';
|
|
27
|
+
export type {
|
|
28
|
+
TokenDiscoveryParams,
|
|
29
|
+
} from './tokens.js';
|
|
30
|
+
|
|
18
31
|
// Payment exports
|
|
19
32
|
export {
|
|
20
33
|
createPayment,
|
|
@@ -31,6 +44,27 @@ export type {
|
|
|
31
44
|
ListPaymentsFnParams
|
|
32
45
|
} from './payments.js';
|
|
33
46
|
|
|
47
|
+
// Invoice exports
|
|
48
|
+
export {
|
|
49
|
+
createInvoice,
|
|
50
|
+
getInvoice,
|
|
51
|
+
listInvoices,
|
|
52
|
+
updateInvoice,
|
|
53
|
+
deleteInvoice,
|
|
54
|
+
publishInvoice,
|
|
55
|
+
sendInvoice,
|
|
56
|
+
getInvoicePaymentData,
|
|
57
|
+
InvoiceStatus,
|
|
58
|
+
} from './invoices.js';
|
|
59
|
+
export type {
|
|
60
|
+
Invoice,
|
|
61
|
+
InvoiceStatusType,
|
|
62
|
+
CreateInvoiceParams,
|
|
63
|
+
ListInvoicesParams,
|
|
64
|
+
UpdateInvoiceParams,
|
|
65
|
+
ListInvoicesResult,
|
|
66
|
+
} from './invoices.js';
|
|
67
|
+
|
|
34
68
|
// Webhook exports
|
|
35
69
|
export {
|
|
36
70
|
verifyWebhookSignature,
|
|
@@ -113,3 +147,5 @@ export type {
|
|
|
113
147
|
|
|
114
148
|
import { CoinPayClient } from './client.js';
|
|
115
149
|
export default CoinPayClient;
|
|
150
|
+
|
|
151
|
+
export * from './finances.js';
|
package/src/index.js
CHANGED
|
@@ -46,6 +46,11 @@ import {
|
|
|
46
46
|
PaymentStatus,
|
|
47
47
|
FiatCurrency,
|
|
48
48
|
} from './payments.js';
|
|
49
|
+
import {
|
|
50
|
+
getTokens,
|
|
51
|
+
getSupportedCoins,
|
|
52
|
+
getSupportedCoins as getSupportedPaymentCoins,
|
|
53
|
+
} from './tokens.js';
|
|
49
54
|
import {
|
|
50
55
|
verifyWebhookSignature,
|
|
51
56
|
generateWebhookSignature,
|
|
@@ -66,6 +71,19 @@ import {
|
|
|
66
71
|
authenticateEscrow,
|
|
67
72
|
} from './escrow.js';
|
|
68
73
|
|
|
74
|
+
// Invoice exports
|
|
75
|
+
import {
|
|
76
|
+
createInvoice,
|
|
77
|
+
getInvoice,
|
|
78
|
+
listInvoices,
|
|
79
|
+
updateInvoice,
|
|
80
|
+
deleteInvoice,
|
|
81
|
+
publishInvoice,
|
|
82
|
+
sendInvoice,
|
|
83
|
+
getInvoicePaymentData,
|
|
84
|
+
InvoiceStatus,
|
|
85
|
+
} from './invoices.js';
|
|
86
|
+
|
|
69
87
|
// Wallet exports
|
|
70
88
|
import {
|
|
71
89
|
WalletClient,
|
|
@@ -156,12 +174,30 @@ import {
|
|
|
156
174
|
verifyX402Payment,
|
|
157
175
|
settleX402Payment,
|
|
158
176
|
buildPaymentRequired,
|
|
177
|
+
expectedForProof,
|
|
159
178
|
convertUsdToAssetAmount,
|
|
160
179
|
PAYMENT_METHODS,
|
|
161
180
|
USDC_CONTRACTS,
|
|
162
181
|
CHAIN_IDS,
|
|
163
182
|
} from './x402.js';
|
|
164
183
|
|
|
184
|
+
import {
|
|
185
|
+
getFinanceSummary,
|
|
186
|
+
listFinanceAccounts,
|
|
187
|
+
listFinanceTransactions,
|
|
188
|
+
listFinanceConnections,
|
|
189
|
+
syncFinances,
|
|
190
|
+
getDashboardStats,
|
|
191
|
+
getFinanceAnalytics,
|
|
192
|
+
listCryptoPayments,
|
|
193
|
+
listCardTransactions,
|
|
194
|
+
listCardPayouts,
|
|
195
|
+
buildFinanceSnapshot,
|
|
196
|
+
collectFinanceSnapshot,
|
|
197
|
+
subscribeToPayments,
|
|
198
|
+
periodForDays,
|
|
199
|
+
} from './finances.js';
|
|
200
|
+
|
|
165
201
|
export {
|
|
166
202
|
// Client
|
|
167
203
|
CoinPayClient,
|
|
@@ -170,6 +206,9 @@ export {
|
|
|
170
206
|
createPayment,
|
|
171
207
|
getPayment,
|
|
172
208
|
listPayments,
|
|
209
|
+
getTokens,
|
|
210
|
+
getSupportedCoins,
|
|
211
|
+
getSupportedPaymentCoins,
|
|
173
212
|
|
|
174
213
|
// Escrow functions
|
|
175
214
|
createEscrow,
|
|
@@ -182,6 +221,17 @@ export {
|
|
|
182
221
|
waitForEscrow,
|
|
183
222
|
authenticateEscrow,
|
|
184
223
|
|
|
224
|
+
// Invoice functions
|
|
225
|
+
createInvoice,
|
|
226
|
+
getInvoice,
|
|
227
|
+
listInvoices,
|
|
228
|
+
updateInvoice,
|
|
229
|
+
deleteInvoice,
|
|
230
|
+
publishInvoice,
|
|
231
|
+
sendInvoice,
|
|
232
|
+
getInvoicePaymentData,
|
|
233
|
+
InvoiceStatus,
|
|
234
|
+
|
|
185
235
|
// Wallet
|
|
186
236
|
WalletClient,
|
|
187
237
|
WalletChain,
|
|
@@ -253,6 +303,22 @@ export {
|
|
|
253
303
|
// Lightning
|
|
254
304
|
LightningClient,
|
|
255
305
|
|
|
306
|
+
// Finances (bank feeds, earnings, commission, refunds)
|
|
307
|
+
getFinanceSummary,
|
|
308
|
+
listFinanceAccounts,
|
|
309
|
+
listFinanceTransactions,
|
|
310
|
+
listFinanceConnections,
|
|
311
|
+
syncFinances,
|
|
312
|
+
getDashboardStats,
|
|
313
|
+
getFinanceAnalytics,
|
|
314
|
+
listCryptoPayments,
|
|
315
|
+
listCardTransactions,
|
|
316
|
+
listCardPayouts,
|
|
317
|
+
buildFinanceSnapshot,
|
|
318
|
+
collectFinanceSnapshot,
|
|
319
|
+
subscribeToPayments,
|
|
320
|
+
periodForDays,
|
|
321
|
+
|
|
256
322
|
// Auth
|
|
257
323
|
registerMerchant,
|
|
258
324
|
loginMerchant,
|
|
@@ -263,6 +329,7 @@ export {
|
|
|
263
329
|
verifyX402Payment,
|
|
264
330
|
settleX402Payment,
|
|
265
331
|
buildPaymentRequired,
|
|
332
|
+
expectedForProof,
|
|
266
333
|
convertUsdToAssetAmount,
|
|
267
334
|
PAYMENT_METHODS,
|
|
268
335
|
USDC_CONTRACTS,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CoinPayClient } from './client.js';
|
|
2
|
+
|
|
3
|
+
export const InvoiceStatus: {
|
|
4
|
+
readonly DRAFT: 'draft';
|
|
5
|
+
readonly SENT: 'sent';
|
|
6
|
+
readonly PAID: 'paid';
|
|
7
|
+
readonly OVERDUE: 'overdue';
|
|
8
|
+
readonly CANCELLED: 'cancelled';
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type InvoiceStatusType = (typeof InvoiceStatus)[keyof typeof InvoiceStatus];
|
|
12
|
+
|
|
13
|
+
export interface Invoice {
|
|
14
|
+
id: string;
|
|
15
|
+
invoiceNumber?: string;
|
|
16
|
+
businessId?: string;
|
|
17
|
+
clientId?: string;
|
|
18
|
+
currency?: string;
|
|
19
|
+
amount?: number;
|
|
20
|
+
cryptoCurrency?: string;
|
|
21
|
+
cryptoAmount?: string | number;
|
|
22
|
+
status?: InvoiceStatusType;
|
|
23
|
+
dueDate?: string;
|
|
24
|
+
notes?: string;
|
|
25
|
+
walletId?: string;
|
|
26
|
+
merchantWalletAddress?: string;
|
|
27
|
+
paymentAddress?: string;
|
|
28
|
+
stripeCheckoutUrl?: string;
|
|
29
|
+
paidAt?: string;
|
|
30
|
+
sentAt?: string;
|
|
31
|
+
createdAt?: string;
|
|
32
|
+
updatedAt?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CreateInvoiceParams {
|
|
36
|
+
businessId?: string;
|
|
37
|
+
clientId?: string;
|
|
38
|
+
currency: string;
|
|
39
|
+
amount: number;
|
|
40
|
+
cryptoCurrency?: string;
|
|
41
|
+
dueDate?: string;
|
|
42
|
+
notes?: string;
|
|
43
|
+
walletId?: string;
|
|
44
|
+
merchantWalletAddress?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ListInvoicesParams {
|
|
48
|
+
businessId?: string;
|
|
49
|
+
status?: InvoiceStatusType;
|
|
50
|
+
clientId?: string;
|
|
51
|
+
dateFrom?: string;
|
|
52
|
+
dateTo?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UpdateInvoiceParams {
|
|
56
|
+
amount?: number;
|
|
57
|
+
currency?: string;
|
|
58
|
+
cryptoCurrency?: string;
|
|
59
|
+
dueDate?: string;
|
|
60
|
+
notes?: string;
|
|
61
|
+
clientId?: string;
|
|
62
|
+
walletId?: string;
|
|
63
|
+
merchantWalletAddress?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ListInvoicesResult {
|
|
67
|
+
invoices: Invoice[];
|
|
68
|
+
total?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createInvoice(client: CoinPayClient, options: CreateInvoiceParams): Promise<Invoice>;
|
|
72
|
+
export function listInvoices(client: CoinPayClient, filters?: ListInvoicesParams): Promise<ListInvoicesResult>;
|
|
73
|
+
export function getInvoice(client: CoinPayClient, id: string): Promise<Invoice>;
|
|
74
|
+
export function updateInvoice(client: CoinPayClient, id: string, updates: UpdateInvoiceParams): Promise<Invoice>;
|
|
75
|
+
export function deleteInvoice(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
|
|
76
|
+
export function publishInvoice(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
|
|
77
|
+
export function sendInvoice(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
|
|
78
|
+
export function getInvoicePaymentData(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
|