@profullstack/coinpay 0.6.6 → 0.6.8

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/bin/coinpay.js CHANGED
@@ -119,6 +119,13 @@ function createClient() {
119
119
  return new CoinPayClient({ apiKey, baseUrl: getBaseUrl() });
120
120
  }
121
121
 
122
+ /**
123
+ * Create unauthenticated client for auth operations
124
+ */
125
+ function createUnauthenticatedClient() {
126
+ return new CoinPayClient({ baseUrl: getBaseUrl() });
127
+ }
128
+
122
129
  /**
123
130
  * Parse command line arguments
124
131
  */
@@ -171,6 +178,11 @@ ${colors.cyan}Commands:${colors.reset}
171
178
  set-url <base-url> Set custom API URL
172
179
  show Show current configuration
173
180
 
181
+ ${colors.bright}auth${colors.reset}
182
+ register Register new merchant account
183
+ login Login to merchant account
184
+ me Show current merchant info
185
+
174
186
  ${colors.bright}payment${colors.reset}
175
187
  create Create a new payment
176
188
  get <id> Get payment details
@@ -208,6 +220,11 @@ ${colors.cyan}Commands:${colors.reset}
208
220
  status <swap-id> Check swap status
209
221
  history Swap history
210
222
 
223
+ ${colors.bright}payout${colors.reset}
224
+ create Create a payout to your Stripe account
225
+ list List payouts
226
+ get <id> Get payout details
227
+
211
228
  ${colors.bright}card${colors.reset}
212
229
  create Create a card payment via Stripe
213
230
  get <id> Get card payment status
@@ -236,6 +253,8 @@ ${colors.cyan}Commands:${colors.reset}
236
253
  badge [did] Get embeddable reputation badge URL
237
254
  verify <id> Verify a credential
238
255
  revocations List revoked credentials
256
+ did claim Claim (auto-generate) a new DID
257
+ did me Show your current DID
239
258
  issuer register Register platform issuer (--name, --domain)
240
259
  issuer list List your platform issuers
241
260
  issuer rotate Rotate issuer API key (--id)
@@ -1593,9 +1612,249 @@ async function handleEscrow(subcommand, args, flags) {
1593
1612
  break;
1594
1613
  }
1595
1614
 
1615
+ case 'series': {
1616
+ const seriesCmd = args[0];
1617
+ const seriesId = args[1];
1618
+
1619
+ switch (seriesCmd) {
1620
+ case 'create': {
1621
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1622
+ const ask = (q) => new Promise((r) => rl.question(q, r));
1623
+
1624
+ try {
1625
+ const businessId = flags['business-id'] || await ask('Business ID: ');
1626
+ const paymentMethod = flags['payment-method'] || await ask('Payment method (crypto/card): ');
1627
+ const amount = flags.amount || await ask('Amount (cents for card, satoshis-equiv for crypto): ');
1628
+ const currency = flags.currency || await ask('Currency [USD]: ') || 'USD';
1629
+ const interval = flags.interval || await ask('Interval (weekly/biweekly/monthly): ');
1630
+ const maxPeriods = flags['max-periods'] || await ask('Max periods (blank=infinite): ') || undefined;
1631
+ const customerEmail = flags.email || await ask('Customer email (optional): ') || undefined;
1632
+ const description = flags.description || await ask('Description (optional): ') || undefined;
1633
+
1634
+ const params = {
1635
+ business_id: businessId,
1636
+ payment_method: paymentMethod,
1637
+ amount: parseInt(amount),
1638
+ currency,
1639
+ interval,
1640
+ max_periods: maxPeriods ? parseInt(maxPeriods) : undefined,
1641
+ customer_email: customerEmail,
1642
+ description,
1643
+ };
1644
+
1645
+ if (paymentMethod === 'crypto') {
1646
+ params.coin = flags.coin || await ask('Coin (BTC/ETH/SOL/etc): ');
1647
+ params.beneficiary_address = flags.beneficiary || await ask('Beneficiary address: ');
1648
+ params.depositor_address = flags.depositor || await ask('Depositor address: ');
1649
+ } else {
1650
+ params.stripe_account_id = flags['stripe-account'] || await ask('Stripe account ID: ');
1651
+ }
1652
+
1653
+ rl.close();
1654
+
1655
+ const series = await client.createEscrowSeries(params);
1656
+ print.success(`Escrow series created: ${series.id}`);
1657
+ print.info(` Status: ${series.status}`);
1658
+ print.info(` Interval: ${series.interval}`);
1659
+ print.info(` Amount: ${series.amount} ${series.currency}`);
1660
+ if (flags.json) print.json(series);
1661
+ } catch (e) {
1662
+ rl.close();
1663
+ throw e;
1664
+ }
1665
+ break;
1666
+ }
1667
+
1668
+ case 'list': {
1669
+ const businessId = flags['business-id'];
1670
+ if (!businessId) { print.error('--business-id required'); process.exit(1); }
1671
+ const result = await client.listEscrowSeries(businessId, flags.status);
1672
+ print.info(`Series (${result.series.length}):`);
1673
+ for (const s of result.series) {
1674
+ console.log(` ${s.id} | ${s.status} | ${s.payment_method} | ${s.interval} | ${s.amount} ${s.currency}`);
1675
+ }
1676
+ if (flags.json) print.json(result);
1677
+ break;
1678
+ }
1679
+
1680
+ case 'get': {
1681
+ if (!seriesId) { print.error('Series ID required'); process.exit(1); }
1682
+ const result = await client.getEscrowSeries(seriesId);
1683
+ print.success(`Series ${result.series.id}`);
1684
+ print.info(` Status: ${result.series.status}`);
1685
+ print.info(` Method: ${result.series.payment_method}`);
1686
+ print.info(` Amount: ${result.series.amount} ${result.series.currency}`);
1687
+ print.info(` Interval: ${result.series.interval}`);
1688
+ print.info(` Periods: ${result.series.periods_completed}${result.series.max_periods ? '/' + result.series.max_periods : ''}`);
1689
+ print.info(` Next charge: ${result.series.next_charge_at}`);
1690
+ print.info(` Crypto escrows: ${result.escrows.crypto.length}`);
1691
+ print.info(` Stripe escrows: ${result.escrows.stripe.length}`);
1692
+ if (flags.json) print.json(result);
1693
+ break;
1694
+ }
1695
+
1696
+ case 'pause': {
1697
+ if (!seriesId) { print.error('Series ID required'); process.exit(1); }
1698
+ await client.updateEscrowSeries(seriesId, { status: 'paused' });
1699
+ print.success(`Series ${seriesId} paused`);
1700
+ break;
1701
+ }
1702
+
1703
+ case 'resume': {
1704
+ if (!seriesId) { print.error('Series ID required'); process.exit(1); }
1705
+ await client.updateEscrowSeries(seriesId, { status: 'active' });
1706
+ print.success(`Series ${seriesId} resumed`);
1707
+ break;
1708
+ }
1709
+
1710
+ case 'cancel': {
1711
+ if (!seriesId) { print.error('Series ID required'); process.exit(1); }
1712
+ await client.cancelEscrowSeries(seriesId);
1713
+ print.success(`Series ${seriesId} cancelled`);
1714
+ break;
1715
+ }
1716
+
1717
+ default:
1718
+ print.error(`Unknown series command: ${seriesCmd}`);
1719
+ print.info('Available: create, list, get, pause, resume, cancel');
1720
+ process.exit(1);
1721
+ }
1722
+ break;
1723
+ }
1724
+
1596
1725
  default:
1597
1726
  print.error(`Unknown escrow command: ${subcommand}`);
1598
- print.info('Available: create, get, list, release, refund, dispute, events, auth');
1727
+ print.info('Available: create, get, list, release, refund, dispute, events, auth, series');
1728
+ process.exit(1);
1729
+ }
1730
+ }
1731
+
1732
+ /**
1733
+ * Auth commands
1734
+ */
1735
+ async function handleAuth(subcommand, args, flags) {
1736
+ const config = loadConfig();
1737
+
1738
+ switch (subcommand) {
1739
+ case 'register': {
1740
+ const email = flags.email;
1741
+ const password = flags.password;
1742
+ const name = flags.name;
1743
+
1744
+ if (!email || !password) {
1745
+ print.error('Required: --email <email> --password <password>');
1746
+ print.info('Example: coinpay auth register --email user@example.com --password mysecret --name "My Company"');
1747
+ return;
1748
+ }
1749
+
1750
+ try {
1751
+ const client = createUnauthenticatedClient();
1752
+ const { registerMerchant } = await import('../src/auth.js');
1753
+ const result = await registerMerchant(client, { email, password, name });
1754
+
1755
+ if (result.success) {
1756
+ // Store the JWT token in config
1757
+ config.jwtToken = result.token;
1758
+ saveConfig(config);
1759
+
1760
+ print.success('Registration successful!');
1761
+ print.info(`Merchant ID: ${result.merchant.id}`);
1762
+ print.info(`Email: ${result.merchant.email}`);
1763
+ if (result.merchant.name) {
1764
+ print.info(`Name: ${result.merchant.name}`);
1765
+ }
1766
+ print.success('JWT token saved to config');
1767
+ } else {
1768
+ print.error(result.error || 'Registration failed');
1769
+ }
1770
+
1771
+ if (flags.json) {
1772
+ print.json(result);
1773
+ }
1774
+ } catch (error) {
1775
+ print.error(error.message);
1776
+ }
1777
+ break;
1778
+ }
1779
+
1780
+ case 'login': {
1781
+ const email = flags.email;
1782
+ const password = flags.password;
1783
+
1784
+ if (!email || !password) {
1785
+ print.error('Required: --email <email> --password <password>');
1786
+ print.info('Example: coinpay auth login --email user@example.com --password mysecret');
1787
+ return;
1788
+ }
1789
+
1790
+ try {
1791
+ const client = createUnauthenticatedClient();
1792
+ const { loginMerchant } = await import('../src/auth.js');
1793
+ const result = await loginMerchant(client, { email, password });
1794
+
1795
+ if (result.success) {
1796
+ // Store the JWT token in config
1797
+ config.jwtToken = result.token;
1798
+ saveConfig(config);
1799
+
1800
+ print.success('Login successful!');
1801
+ print.info(`Merchant ID: ${result.merchant.id}`);
1802
+ print.info(`Email: ${result.merchant.email}`);
1803
+ if (result.merchant.name) {
1804
+ print.info(`Name: ${result.merchant.name}`);
1805
+ }
1806
+ print.success('JWT token saved to config');
1807
+ } else {
1808
+ print.error(result.error || 'Login failed');
1809
+ }
1810
+
1811
+ if (flags.json) {
1812
+ print.json(result);
1813
+ }
1814
+ } catch (error) {
1815
+ print.error(error.message);
1816
+ }
1817
+ break;
1818
+ }
1819
+
1820
+ case 'me': {
1821
+ try {
1822
+ // Check if we have a JWT token
1823
+ const jwtToken = config.jwtToken;
1824
+ if (!jwtToken) {
1825
+ print.error('Not authenticated. Run: coinpay auth login');
1826
+ return;
1827
+ }
1828
+
1829
+ // Create client with JWT token instead of API key
1830
+ const client = new CoinPayClient({ apiKey: jwtToken, baseUrl: getBaseUrl() });
1831
+ const { getMe } = await import('../src/auth.js');
1832
+ const result = await getMe(client);
1833
+
1834
+ print.success('Current merchant:');
1835
+ print.info(`ID: ${result.id}`);
1836
+ print.info(`Email: ${result.email}`);
1837
+ if (result.name) {
1838
+ print.info(`Name: ${result.name}`);
1839
+ }
1840
+ print.info(`Created: ${result.created_at}`);
1841
+
1842
+ if (flags.json) {
1843
+ print.json(result);
1844
+ }
1845
+ } catch (error) {
1846
+ if (error.message.includes('401') || error.message.includes('403')) {
1847
+ print.error('Authentication failed. Please run: coinpay auth login');
1848
+ } else {
1849
+ print.error(error.message);
1850
+ }
1851
+ }
1852
+ break;
1853
+ }
1854
+
1855
+ default:
1856
+ print.error(`Unknown auth command: ${subcommand}`);
1857
+ print.info('Available: register, login, me');
1599
1858
  process.exit(1);
1600
1859
  }
1601
1860
  }
@@ -1781,6 +2040,21 @@ async function handleReputation(subcommand, args, flags) {
1781
2040
  print.error(result.error || 'Failed to link DID');
1782
2041
  }
1783
2042
 
2043
+ if (flags.json) print.json(result);
2044
+ } else if (didSubcommand === 'me') {
2045
+ // Show current DID
2046
+ const { getMyDid } = await import('../src/reputation.js');
2047
+ const result = await getMyDid(client);
2048
+
2049
+ if (result.did) {
2050
+ print.success(`Your DID: ${result.did}`);
2051
+ console.log(` Public Key: ${result.public_key}`);
2052
+ console.log(` Verified: ${result.verified}`);
2053
+ console.log(` Created: ${result.created_at}`);
2054
+ } else {
2055
+ print.info('No DID found. Run: coinpay reputation did claim');
2056
+ }
2057
+
1784
2058
  if (flags.json) print.json(result);
1785
2059
  } else {
1786
2060
  // Default: show current DID
@@ -2003,6 +2277,74 @@ async function handleReputation(subcommand, args, flags) {
2003
2277
  }
2004
2278
  }
2005
2279
 
2280
+ /**
2281
+ * Payout commands
2282
+ */
2283
+ async function handlePayout(subcommand, args, flags) {
2284
+ const client = createClient();
2285
+
2286
+ switch (subcommand) {
2287
+ case 'create': {
2288
+ const amount = parseInt(flags.amount);
2289
+ if (!amount || amount <= 0) {
2290
+ print.error('--amount is required (in cents, e.g., 5000 for $50.00)');
2291
+ print.info('Example: coinpay payout create --amount 5000 --description "Weekly payout"');
2292
+ process.exit(1);
2293
+ }
2294
+
2295
+ const result = await client.createPayout({
2296
+ amount,
2297
+ currency: flags.currency || 'usd',
2298
+ description: flags.description,
2299
+ metadata: flags.metadata ? JSON.parse(flags.metadata) : undefined,
2300
+ });
2301
+
2302
+ print.success('Payout created');
2303
+ print.json(result);
2304
+ break;
2305
+ }
2306
+
2307
+ case 'list': {
2308
+ const result = await client.listPayouts({
2309
+ status: flags.status,
2310
+ dateFrom: flags['date-from'] || flags.dateFrom,
2311
+ dateTo: flags['date-to'] || flags.dateTo,
2312
+ limit: flags.limit ? parseInt(flags.limit) : undefined,
2313
+ offset: flags.offset ? parseInt(flags.offset) : undefined,
2314
+ });
2315
+
2316
+ if (result.payouts && result.payouts.length > 0) {
2317
+ console.log(`\n${colors.bright}Payouts${colors.reset} (${result.pagination?.total || result.payouts.length} total)\n`);
2318
+ for (const p of result.payouts) {
2319
+ const statusColor = p.status === 'paid' ? colors.green : p.status === 'failed' ? colors.red : colors.yellow;
2320
+ console.log(` ${p.id} ${statusColor}${p.status}${colors.reset} $${p.amount_usd || ((p.amount_cents || 0) / 100).toFixed(2)} ${new Date(p.created_at).toLocaleDateString()}`);
2321
+ }
2322
+ console.log();
2323
+ } else {
2324
+ print.info('No payouts found');
2325
+ }
2326
+ break;
2327
+ }
2328
+
2329
+ case 'get': {
2330
+ const id = args[0];
2331
+ if (!id) {
2332
+ print.error('Usage: coinpay payout get <id>');
2333
+ process.exit(1);
2334
+ }
2335
+
2336
+ const result = await client.getPayout(id);
2337
+ print.json(result);
2338
+ break;
2339
+ }
2340
+
2341
+ default:
2342
+ print.error(`Unknown payout command: ${subcommand}`);
2343
+ print.info('Available: create, list, get');
2344
+ process.exit(1);
2345
+ }
2346
+ }
2347
+
2006
2348
  /**
2007
2349
  * Card (Stripe) commands
2008
2350
  */
@@ -2258,10 +2600,18 @@ async function main() {
2258
2600
  await handleWebhook(subcommand, args, flags);
2259
2601
  break;
2260
2602
 
2603
+ case 'payout':
2604
+ await handlePayout(subcommand, args, flags);
2605
+ break;
2606
+
2261
2607
  case 'card':
2262
2608
  await handleCard(subcommand, args, flags);
2263
2609
  break;
2264
2610
 
2611
+ case 'auth':
2612
+ await handleAuth(subcommand, args, flags);
2613
+ break;
2614
+
2265
2615
  case 'reputation':
2266
2616
  await handleReputation(subcommand, args, flags);
2267
2617
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "CoinPay SDK & CLI — Accept cryptocurrency payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet and swap support",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/auth.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Auth SDK Module Types
3
+ */
4
+
5
+ import { CoinPayClient } from './client.js';
6
+
7
+ /**
8
+ * Merchant object returned from auth operations
9
+ */
10
+ export interface Merchant {
11
+ id: string;
12
+ email: string;
13
+ name?: string;
14
+ created_at: string;
15
+ updated_at?: string;
16
+ [key: string]: any;
17
+ }
18
+
19
+ /**
20
+ * Auth response for register/login
21
+ */
22
+ export interface AuthResponse {
23
+ success: boolean;
24
+ merchant: Merchant;
25
+ token: string;
26
+ }
27
+
28
+ /**
29
+ * Register params
30
+ */
31
+ export interface RegisterParams {
32
+ email: string;
33
+ password: string;
34
+ name?: string;
35
+ }
36
+
37
+ /**
38
+ * Login params
39
+ */
40
+ export interface LoginParams {
41
+ email: string;
42
+ password: string;
43
+ }
44
+
45
+ /**
46
+ * Register a new merchant account
47
+ * @param client - CoinPay client (can be unauthenticated)
48
+ * @param params - Registration parameters
49
+ * @returns Promise with registration response
50
+ */
51
+ export function registerMerchant(
52
+ client: CoinPayClient,
53
+ params: RegisterParams
54
+ ): Promise<AuthResponse>;
55
+
56
+ /**
57
+ * Login to merchant account
58
+ * @param client - CoinPay client (can be unauthenticated)
59
+ * @param params - Login parameters
60
+ * @returns Promise with login response
61
+ */
62
+ export function loginMerchant(
63
+ client: CoinPayClient,
64
+ params: LoginParams
65
+ ): Promise<AuthResponse>;
66
+
67
+ /**
68
+ * Get current authenticated merchant info
69
+ * @param client - Authenticated CoinPay client
70
+ * @returns Promise with merchant information
71
+ */
72
+ export function getMe(client: CoinPayClient): Promise<Merchant>;
package/src/auth.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Auth SDK Module
3
+ *
4
+ * Authentication functions for registration, login, and merchant management.
5
+ *
6
+ * @example
7
+ * import { CoinPayClient } from '@profullstack/coinpay';
8
+ * import { registerMerchant, loginMerchant, getMe } from '@profullstack/coinpay/auth';
9
+ */
10
+
11
+ /**
12
+ * Register a new merchant account
13
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client (can be unauthenticated)
14
+ * @param {Object} params
15
+ * @param {string} params.email - Merchant email
16
+ * @param {string} params.password - Account password
17
+ * @param {string} [params.name] - Optional merchant name
18
+ * @returns {Promise<Object>} { success: boolean, merchant: Object, token: string }
19
+ */
20
+ export async function registerMerchant(client, { email, password, name }) {
21
+ return client.requestUnauthenticated('/auth/register', {
22
+ method: 'POST',
23
+ body: JSON.stringify({
24
+ email,
25
+ password,
26
+ name,
27
+ }),
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Login to merchant account
33
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client (can be unauthenticated)
34
+ * @param {Object} params
35
+ * @param {string} params.email - Merchant email
36
+ * @param {string} params.password - Account password
37
+ * @returns {Promise<Object>} { success: boolean, merchant: Object, token: string }
38
+ */
39
+ export async function loginMerchant(client, { email, password }) {
40
+ return client.requestUnauthenticated('/auth/login', {
41
+ method: 'POST',
42
+ body: JSON.stringify({
43
+ email,
44
+ password,
45
+ }),
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Get current authenticated merchant info
51
+ * @param {import('./client.js').CoinPayClient} client - Authenticated CoinPay client
52
+ * @returns {Promise<Object>} Merchant information
53
+ */
54
+ export async function getMe(client) {
55
+ return client.request('/auth/me', {
56
+ method: 'POST',
57
+ });
58
+ }
@@ -285,6 +285,128 @@ export function calculateCardPaymentFees(amountCents, tier = 'free') {
285
285
  };
286
286
  }
287
287
 
288
+ /**
289
+ * Create a card escrow payment
290
+ *
291
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
292
+ * @param {string} businessId - Business ID
293
+ * @param {number} amountUSD - Amount in USD (will be converted to cents)
294
+ * @param {string} description - Payment description
295
+ * @param {Object} [metadata={}] - Custom metadata
296
+ * @returns {Promise<Object>} Escrow payment session with checkout URL
297
+ *
298
+ * @example
299
+ * import { createCardEscrow } from '@profullstack/coinpay/card-payments';
300
+ *
301
+ * const escrow = await createCardEscrow(client, 'business-id', 100, 'Service payment', {
302
+ * orderId: '123',
303
+ * serviceType: 'web-design'
304
+ * });
305
+ *
306
+ * // Redirect customer to: escrow.checkout_url
307
+ */
308
+ export async function createCardEscrow(client, businessId, amountUSD, description, metadata = {}) {
309
+ // Convert USD to cents
310
+ const amountCents = Math.round(amountUSD * 100);
311
+
312
+ return client.request('POST', '/api/stripe/payments/create', {
313
+ businessId,
314
+ amount: amountCents,
315
+ currency: 'usd',
316
+ description,
317
+ metadata,
318
+ escrowMode: true,
319
+ });
320
+ }
321
+
322
+ /**
323
+ * List card escrows
324
+ *
325
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
326
+ * @param {Object} [options={}] - Filtering options
327
+ * @param {string} [options.businessId] - Filter by business ID
328
+ * @param {string} [options.status] - Filter by status
329
+ * @param {number} [options.limit] - Maximum number of results
330
+ * @param {number} [options.offset] - Results offset
331
+ * @returns {Promise<Object>} List of card escrows
332
+ *
333
+ * @example
334
+ * const escrows = await listCardEscrows(client, {
335
+ * businessId: 'business-id',
336
+ * status: 'pending'
337
+ * });
338
+ */
339
+ export async function listCardEscrows(client, options = {}) {
340
+ const queryParams = new URLSearchParams();
341
+ if (options.businessId) queryParams.set('businessId', options.businessId);
342
+ if (options.status) queryParams.set('status', options.status);
343
+ if (options.limit) queryParams.set('limit', options.limit.toString());
344
+ if (options.offset) queryParams.set('offset', options.offset.toString());
345
+
346
+ const queryString = queryParams.toString();
347
+ const url = `/api/stripe/escrows${queryString ? `?${queryString}` : ''}`;
348
+
349
+ return client.request('GET', url);
350
+ }
351
+
352
+ /**
353
+ * Release a card escrow
354
+ *
355
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
356
+ * @param {string} escrowId - Escrow ID to release
357
+ * @returns {Promise<Object>} Release result
358
+ *
359
+ * @example
360
+ * const result = await releaseCardEscrow(client, 'escrow-123');
361
+ */
362
+ export async function releaseCardEscrow(client, escrowId) {
363
+ return client.request('POST', '/api/stripe/escrow/release', {
364
+ escrowId,
365
+ });
366
+ }
367
+
368
+ /**
369
+ * Refund a card escrow
370
+ *
371
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
372
+ * @param {string} escrowId - Escrow ID to refund
373
+ * @param {Object} [options={}] - Refund options
374
+ * @param {number} [options.amount] - Partial refund amount in cents
375
+ * @param {string} [options.reason] - Refund reason
376
+ * @returns {Promise<Object>} Refund result
377
+ *
378
+ * @example
379
+ * // Full refund
380
+ * const result = await refundCardEscrow(client, 'escrow-123');
381
+ *
382
+ * // Partial refund
383
+ * const result = await refundCardEscrow(client, 'escrow-123', {
384
+ * amount: 2500, // $25.00 in cents
385
+ * reason: 'partial_delivery'
386
+ * });
387
+ */
388
+ export async function refundCardEscrow(client, escrowId, options = {}) {
389
+ return client.request('POST', '/api/stripe/escrow/refund', {
390
+ escrowId,
391
+ ...options,
392
+ });
393
+ }
394
+
395
+ /**
396
+ * Get card escrow status and transaction details
397
+ *
398
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
399
+ * @param {string} escrowId - Escrow/transaction ID
400
+ * @returns {Promise<Object>} Transaction details with escrow status
401
+ *
402
+ * @example
403
+ * const status = await getCardEscrowStatus(client, 'escrow-123');
404
+ * console.log(status.escrow_status); // 'pending', 'released', 'refunded'
405
+ */
406
+ export async function getCardEscrowStatus(client, escrowId) {
407
+ return client.request('GET', `/api/stripe/transactions/${escrowId}`);
408
+ }
409
+
288
410
  /**
289
411
  * Default export with all convenience functions
290
412
  */
@@ -295,4 +417,9 @@ export default {
295
417
  getPaymentMethodSupport,
296
418
  formatCardAmount,
297
419
  calculateCardPaymentFees,
420
+ createCardEscrow,
421
+ listCardEscrows,
422
+ releaseCardEscrow,
423
+ refundCardEscrow,
424
+ getCardEscrowStatus,
298
425
  };
package/src/client.js CHANGED
@@ -16,15 +16,11 @@ export class CoinPayClient {
16
16
  /**
17
17
  * Create a new CoinPay client
18
18
  * @param {Object} options - Client options
19
- * @param {string} options.apiKey - Your CoinPay API key
19
+ * @param {string} [options.apiKey] - Your CoinPay API key (optional for auth operations)
20
20
  * @param {string} [options.baseUrl] - API base URL (default: https://coinpayportal.com/api)
21
21
  * @param {number} [options.timeout] - Request timeout in ms (default: 30000)
22
22
  */
23
- constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, timeout = 30000 }) {
24
- if (!apiKey) {
25
- throw new Error('API key is required');
26
- }
27
-
23
+ constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, timeout = 30000 } = {}) {
28
24
  this.#apiKey = apiKey;
29
25
  this.#baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
30
26
  this.#timeout = timeout;
@@ -37,6 +33,10 @@ export class CoinPayClient {
37
33
  * @returns {Promise<Object>} API response
38
34
  */
39
35
  async request(endpoint, options = {}) {
36
+ if (!this.#apiKey) {
37
+ throw new Error('API key is required for authenticated requests. Use requestUnauthenticated() for registration/login.');
38
+ }
39
+
40
40
  const url = `${this.#baseUrl}${endpoint}`;
41
41
  const controller = new AbortController();
42
42
  const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
@@ -72,6 +72,47 @@ export class CoinPayClient {
72
72
  }
73
73
  }
74
74
 
75
+ /**
76
+ * Make an unauthenticated API request (for registration/login)
77
+ * @param {string} endpoint - API endpoint
78
+ * @param {Object} [options] - Fetch options
79
+ * @returns {Promise<Object>} API response
80
+ */
81
+ async requestUnauthenticated(endpoint, options = {}) {
82
+ const url = `${this.#baseUrl}${endpoint}`;
83
+ const controller = new AbortController();
84
+ const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
85
+
86
+ try {
87
+ const response = await fetch(url, {
88
+ ...options,
89
+ signal: controller.signal,
90
+ headers: {
91
+ 'Content-Type': 'application/json',
92
+ ...options.headers,
93
+ },
94
+ });
95
+
96
+ const data = await response.json();
97
+
98
+ if (!response.ok) {
99
+ const error = new Error(data.error || `HTTP ${response.status}`);
100
+ error.status = response.status;
101
+ error.response = data;
102
+ throw error;
103
+ }
104
+
105
+ return data;
106
+ } catch (error) {
107
+ if (error.name === 'AbortError') {
108
+ throw new Error(`Request timeout after ${this.#timeout}ms`);
109
+ }
110
+ throw error;
111
+ } finally {
112
+ clearTimeout(timeoutId);
113
+ }
114
+ }
115
+
75
116
  /**
76
117
  * Create a new payment
77
118
  *
@@ -523,6 +564,43 @@ export class CoinPayClient {
523
564
  return authenticateEscrow(this, escrowId, token);
524
565
  }
525
566
 
567
+ // ── Stripe Payout Methods ───────────────────────────────
568
+
569
+ /**
570
+ * Create a payout to your connected Stripe account
571
+ *
572
+ * @param {Object} params - Payout parameters
573
+ * @param {number} params.amount - Amount in cents
574
+ * @param {string} [params.currency='usd'] - Currency code
575
+ * @param {string} [params.description] - Payout description
576
+ * @param {Object} [params.metadata] - Custom metadata
577
+ * @returns {Promise<Object>} Created payout
578
+ */
579
+ async createPayout(params) {
580
+ const { createPayout } = await import('./payouts.js');
581
+ return createPayout(this, params);
582
+ }
583
+
584
+ /**
585
+ * List payouts
586
+ * @param {Object} [filters] - Filter options
587
+ * @returns {Promise<Object>} Payouts list with pagination
588
+ */
589
+ async listPayouts(filters) {
590
+ const { listPayouts } = await import('./payouts.js');
591
+ return listPayouts(this, filters);
592
+ }
593
+
594
+ /**
595
+ * Get payout by ID
596
+ * @param {string} payoutId - Payout ID
597
+ * @returns {Promise<Object>} Payout details
598
+ */
599
+ async getPayout(payoutId) {
600
+ const { getPayout } = await import('./payouts.js');
601
+ return getPayout(this, payoutId);
602
+ }
603
+
526
604
  // ── Stripe Card Payment Methods ────────────────────────
527
605
 
528
606
  /**
@@ -685,6 +763,64 @@ export class CoinPayClient {
685
763
  }),
686
764
  });
687
765
  }
766
+ // ── Escrow Series (Recurring) Methods ────────────────────
767
+
768
+ /**
769
+ * Create a recurring escrow series
770
+ * @param {Object} params
771
+ * @returns {Promise<Object>} Created series
772
+ */
773
+ async createEscrowSeries(params) {
774
+ return this.request('/escrow/series', {
775
+ method: 'POST',
776
+ body: JSON.stringify(params),
777
+ });
778
+ }
779
+
780
+ /**
781
+ * List escrow series for a business
782
+ * @param {string} businessId
783
+ * @param {string} [status]
784
+ * @returns {Promise<Object>}
785
+ */
786
+ async listEscrowSeries(businessId, status) {
787
+ const params = new URLSearchParams({ business_id: businessId });
788
+ if (status) params.set('status', status);
789
+ return this.request(`/escrow/series?${params.toString()}`);
790
+ }
791
+
792
+ /**
793
+ * Get escrow series detail with child escrows
794
+ * @param {string} id
795
+ * @returns {Promise<Object>}
796
+ */
797
+ async getEscrowSeries(id) {
798
+ return this.request(`/escrow/series/${id}`);
799
+ }
800
+
801
+ /**
802
+ * Update escrow series (pause/resume/cancel, update amount/interval)
803
+ * @param {string} id
804
+ * @param {Object} params - { status?, amount?, interval? }
805
+ * @returns {Promise<Object>}
806
+ */
807
+ async updateEscrowSeries(id, params) {
808
+ return this.request(`/escrow/series/${id}`, {
809
+ method: 'PATCH',
810
+ body: JSON.stringify(params),
811
+ });
812
+ }
813
+
814
+ /**
815
+ * Cancel an escrow series
816
+ * @param {string} id
817
+ * @returns {Promise<Object>}
818
+ */
819
+ async cancelEscrowSeries(id) {
820
+ return this.request(`/escrow/series/${id}`, {
821
+ method: 'DELETE',
822
+ });
823
+ }
688
824
  }
689
825
 
690
826
  export default CoinPayClient;
package/src/index.js CHANGED
@@ -88,8 +88,18 @@ import {
88
88
  getBadgeUrl,
89
89
  verifyCredential,
90
90
  getRevocationList,
91
+ claimDid,
92
+ linkDid,
93
+ getMyDid,
91
94
  } from './reputation.js';
92
95
 
96
+ // Auth exports
97
+ import {
98
+ registerMerchant,
99
+ loginMerchant,
100
+ getMe,
101
+ } from './auth.js';
102
+
93
103
  import {
94
104
  SwapClient,
95
105
  SwapCoins,
@@ -101,6 +111,42 @@ import {
101
111
  getSwapHistory,
102
112
  } from './swap.js';
103
113
 
114
+ // Payouts
115
+ import {
116
+ createPayout,
117
+ listPayouts,
118
+ getPayout,
119
+ formatPayoutAmount,
120
+ } from './payouts.js';
121
+
122
+ // Card payments
123
+ import {
124
+ createQuickCardPayment,
125
+ waitForStripeOnboarding,
126
+ createCardPaymentWithOnboardingCheck,
127
+ getPaymentMethodSupport,
128
+ formatCardAmount,
129
+ calculateCardPaymentFees,
130
+ createCardEscrow,
131
+ listCardEscrows,
132
+ releaseCardEscrow,
133
+ refundCardEscrow,
134
+ getCardEscrowStatus,
135
+ } from './card-payments.js';
136
+
137
+ // Subscriptions
138
+ import {
139
+ createSubscriptionPlan,
140
+ listSubscriptionPlans,
141
+ subscribeCustomer,
142
+ cancelSubscription,
143
+ listSubscriptions,
144
+ getSubscription,
145
+ formatSubscriptionAmount,
146
+ PlanInterval,
147
+ SubscriptionStatus,
148
+ } from './subscriptions.js';
149
+
104
150
  export {
105
151
  // Client
106
152
  CoinPayClient,
@@ -140,6 +186,36 @@ export {
140
186
  getSwapStatus,
141
187
  getSwapHistory,
142
188
 
189
+ // Payouts
190
+ createPayout,
191
+ listPayouts,
192
+ getPayout,
193
+ formatPayoutAmount,
194
+
195
+ // Card payments
196
+ createQuickCardPayment,
197
+ waitForStripeOnboarding,
198
+ createCardPaymentWithOnboardingCheck,
199
+ getPaymentMethodSupport,
200
+ formatCardAmount,
201
+ calculateCardPaymentFees,
202
+ createCardEscrow,
203
+ listCardEscrows,
204
+ releaseCardEscrow,
205
+ refundCardEscrow,
206
+ getCardEscrowStatus,
207
+
208
+ // Subscriptions
209
+ createSubscriptionPlan,
210
+ listSubscriptionPlans,
211
+ subscribeCustomer,
212
+ cancelSubscription,
213
+ listSubscriptions,
214
+ getSubscription,
215
+ formatSubscriptionAmount,
216
+ PlanInterval,
217
+ SubscriptionStatus,
218
+
143
219
  // Constants
144
220
  Blockchain,
145
221
  Cryptocurrency, // Deprecated, use Blockchain
@@ -150,8 +226,19 @@ export {
150
226
  submitReceipt,
151
227
  getReputation,
152
228
  getCredential,
229
+ getCredentials,
230
+ getReceipts,
231
+ getBadgeUrl,
153
232
  verifyCredential,
154
233
  getRevocationList,
234
+ claimDid,
235
+ linkDid,
236
+ getMyDid,
237
+
238
+ // Auth
239
+ registerMerchant,
240
+ loginMerchant,
241
+ getMe,
155
242
 
156
243
  // Webhook utilities
157
244
  verifyWebhookSignature,
package/src/payouts.js ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * CoinPay Payouts - Functions for Stripe Connect payouts
3
+ *
4
+ * Manage payouts to connected Stripe accounts (merchants).
5
+ */
6
+
7
+ /**
8
+ * Create a payout to a connected Stripe account
9
+ *
10
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
11
+ * @param {Object} params - Payout parameters
12
+ * @param {number} params.amount - Amount in cents (e.g., 5000 = $50.00)
13
+ * @param {string} [params.currency='usd'] - Currency code
14
+ * @param {string} [params.description] - Payout description
15
+ * @param {Object} [params.metadata] - Custom metadata
16
+ * @returns {Promise<Object>} Created payout
17
+ *
18
+ * @example
19
+ * const payout = await createPayout(client, {
20
+ * amount: 5000,
21
+ * currency: 'usd',
22
+ * description: 'Weekly payout'
23
+ * });
24
+ */
25
+ export async function createPayout(client, { amount, currency = 'usd', description, metadata }) {
26
+ return client.request('/stripe/payouts', {
27
+ method: 'POST',
28
+ body: JSON.stringify({ amount, currency, description, metadata }),
29
+ });
30
+ }
31
+
32
+ /**
33
+ * List payouts for the authenticated merchant
34
+ *
35
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
36
+ * @param {Object} [filters] - Filter options
37
+ * @param {string} [filters.status] - Filter by status (pending, paid, failed, canceled)
38
+ * @param {string} [filters.dateFrom] - Filter from date (ISO string)
39
+ * @param {string} [filters.dateTo] - Filter to date (ISO string)
40
+ * @param {number} [filters.limit=50] - Number of results
41
+ * @param {number} [filters.offset=0] - Pagination offset
42
+ * @returns {Promise<Object>} List of payouts with pagination
43
+ */
44
+ export async function listPayouts(client, filters = {}) {
45
+ const params = new URLSearchParams();
46
+ if (filters.status) params.set('status', filters.status);
47
+ if (filters.dateFrom) params.set('date_from', filters.dateFrom);
48
+ if (filters.dateTo) params.set('date_to', filters.dateTo);
49
+ if (filters.limit) params.set('limit', String(filters.limit));
50
+ if (filters.offset) params.set('offset', String(filters.offset));
51
+
52
+ const qs = params.toString();
53
+ return client.request(`/stripe/payouts${qs ? '?' + qs : ''}`);
54
+ }
55
+
56
+ /**
57
+ * Get a specific payout by ID
58
+ *
59
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
60
+ * @param {string} payoutId - Payout ID
61
+ * @returns {Promise<Object>} Payout details
62
+ */
63
+ export async function getPayout(client, payoutId) {
64
+ return client.request(`/stripe/payouts/${payoutId}`);
65
+ }
66
+
67
+ /**
68
+ * Format payout amount from cents to display string
69
+ *
70
+ * @param {number} amountCents - Amount in cents
71
+ * @param {string} [currency='usd'] - Currency code
72
+ * @returns {string} Formatted amount (e.g., "$50.00")
73
+ */
74
+ export function formatPayoutAmount(amountCents, currency = 'usd') {
75
+ const amount = (amountCents / 100).toFixed(2);
76
+ const symbols = { usd: '$', eur: '€', gbp: '£' };
77
+ const symbol = symbols[currency.toLowerCase()] || currency.toUpperCase() + ' ';
78
+ return `${symbol}${amount}`;
79
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * CoinPay Subscriptions - Stripe subscription billing integration
3
+ *
4
+ * Provides functions for managing subscription plans and customer subscriptions
5
+ * through Stripe's subscription billing system.
6
+ */
7
+
8
+ /**
9
+ * Subscription plan intervals
10
+ */
11
+ export const PlanInterval = {
12
+ DAILY: 'day',
13
+ WEEKLY: 'week',
14
+ MONTHLY: 'month',
15
+ YEARLY: 'year',
16
+ };
17
+
18
+ /**
19
+ * Subscription statuses
20
+ */
21
+ export const SubscriptionStatus = {
22
+ ACTIVE: 'active',
23
+ PAST_DUE: 'past_due',
24
+ CANCELED: 'canceled',
25
+ INCOMPLETE: 'incomplete',
26
+ INCOMPLETE_EXPIRED: 'incomplete_expired',
27
+ TRIALING: 'trialing',
28
+ UNPAID: 'unpaid',
29
+ PAUSED: 'paused',
30
+ };
31
+
32
+ /**
33
+ * Create a subscription plan (Stripe Price + Product)
34
+ *
35
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
36
+ * @param {Object} params - Plan parameters
37
+ * @param {string} params.businessId - Business ID
38
+ * @param {string} params.name - Plan name
39
+ * @param {string} [params.description] - Plan description
40
+ * @param {number} params.amount - Amount in cents
41
+ * @param {string} [params.currency='usd'] - Currency code
42
+ * @param {string} [params.interval='month'] - Billing interval (day, week, month, year)
43
+ * @param {number} [params.intervalCount=1] - Number of intervals between billings
44
+ * @param {number} [params.trialDays] - Trial period in days
45
+ * @param {Object} [params.metadata] - Custom metadata
46
+ * @returns {Promise<Object>} Created plan
47
+ *
48
+ * @example
49
+ * const plan = await createSubscriptionPlan(client, {
50
+ * businessId: 'biz-123',
51
+ * name: 'Pro Monthly',
52
+ * amount: 2999,
53
+ * interval: 'month',
54
+ * });
55
+ */
56
+ export async function createSubscriptionPlan(client, params) {
57
+ const {
58
+ businessId,
59
+ name,
60
+ description,
61
+ amount,
62
+ currency = 'usd',
63
+ interval = 'month',
64
+ intervalCount = 1,
65
+ trialDays,
66
+ metadata = {},
67
+ } = params;
68
+
69
+ if (!businessId || !name || !amount) {
70
+ throw new Error('businessId, name, and amount are required');
71
+ }
72
+
73
+ return client.request('/stripe/subscriptions/plans', {
74
+ method: 'POST',
75
+ body: JSON.stringify({
76
+ businessId,
77
+ name,
78
+ description,
79
+ amount,
80
+ currency,
81
+ interval,
82
+ intervalCount,
83
+ trialDays,
84
+ metadata,
85
+ }),
86
+ });
87
+ }
88
+
89
+ /**
90
+ * List subscription plans for a business
91
+ *
92
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
93
+ * @param {string} businessId - Business ID
94
+ * @param {Object} [options] - Query options
95
+ * @param {number} [options.limit=20] - Max results
96
+ * @param {boolean} [options.active] - Filter by active status
97
+ * @returns {Promise<Object>} List of plans
98
+ */
99
+ export async function listSubscriptionPlans(client, businessId, options = {}) {
100
+ const params = new URLSearchParams({ businessId });
101
+ if (options.limit) params.set('limit', String(options.limit));
102
+ if (options.active !== undefined) params.set('active', String(options.active));
103
+
104
+ return client.request(`/stripe/subscriptions/plans?${params}`);
105
+ }
106
+
107
+ /**
108
+ * Subscribe a customer to a plan
109
+ *
110
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
111
+ * @param {Object} params - Subscription parameters
112
+ * @param {string} params.planId - Plan/Price ID
113
+ * @param {string} params.customerEmail - Customer email
114
+ * @param {string} [params.customerId] - Existing Stripe customer ID
115
+ * @param {string} [params.paymentMethodId] - Payment method ID
116
+ * @param {string} [params.successUrl] - Success redirect URL
117
+ * @param {string} [params.cancelUrl] - Cancel redirect URL
118
+ * @param {Object} [params.metadata] - Custom metadata
119
+ * @returns {Promise<Object>} Subscription or checkout session
120
+ *
121
+ * @example
122
+ * const sub = await subscribeCustomer(client, {
123
+ * planId: 'price_123',
124
+ * customerEmail: 'customer@example.com',
125
+ * successUrl: 'https://mysite.com/success',
126
+ * cancelUrl: 'https://mysite.com/cancel',
127
+ * });
128
+ * // Redirect to sub.checkout_url
129
+ */
130
+ export async function subscribeCustomer(client, params) {
131
+ const {
132
+ planId,
133
+ customerEmail,
134
+ customerId,
135
+ paymentMethodId,
136
+ successUrl,
137
+ cancelUrl,
138
+ metadata = {},
139
+ } = params;
140
+
141
+ if (!planId || (!customerEmail && !customerId)) {
142
+ throw new Error('planId and either customerEmail or customerId are required');
143
+ }
144
+
145
+ return client.request('/stripe/subscriptions', {
146
+ method: 'POST',
147
+ body: JSON.stringify({
148
+ planId,
149
+ customerEmail,
150
+ customerId,
151
+ paymentMethodId,
152
+ successUrl,
153
+ cancelUrl,
154
+ metadata,
155
+ }),
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Cancel a subscription
161
+ *
162
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
163
+ * @param {string} subscriptionId - Subscription ID
164
+ * @param {Object} [options] - Cancellation options
165
+ * @param {boolean} [options.immediately=false] - Cancel immediately vs at period end
166
+ * @returns {Promise<Object>} Cancelled subscription
167
+ */
168
+ export async function cancelSubscription(client, subscriptionId, options = {}) {
169
+ if (!subscriptionId) {
170
+ throw new Error('subscriptionId is required');
171
+ }
172
+
173
+ return client.request(`/stripe/subscriptions/${encodeURIComponent(subscriptionId)}`, {
174
+ method: 'DELETE',
175
+ body: JSON.stringify({
176
+ immediately: options.immediately || false,
177
+ }),
178
+ });
179
+ }
180
+
181
+ /**
182
+ * List subscriptions
183
+ *
184
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
185
+ * @param {Object} [options] - Query options
186
+ * @param {string} [options.businessId] - Filter by business
187
+ * @param {string} [options.customerId] - Filter by customer
188
+ * @param {string} [options.status] - Filter by status
189
+ * @param {number} [options.limit=20] - Max results
190
+ * @param {number} [options.offset=0] - Pagination offset
191
+ * @returns {Promise<Object>} List of subscriptions
192
+ */
193
+ export async function listSubscriptions(client, options = {}) {
194
+ const params = new URLSearchParams();
195
+ if (options.businessId) params.set('businessId', options.businessId);
196
+ if (options.customerId) params.set('customerId', options.customerId);
197
+ if (options.status) params.set('status', options.status);
198
+ if (options.limit) params.set('limit', String(options.limit));
199
+ if (options.offset) params.set('offset', String(options.offset));
200
+
201
+ return client.request(`/stripe/subscriptions?${params}`);
202
+ }
203
+
204
+ /**
205
+ * Get subscription status/details
206
+ *
207
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client instance
208
+ * @param {string} subscriptionId - Subscription ID
209
+ * @returns {Promise<Object>} Subscription details
210
+ */
211
+ export async function getSubscription(client, subscriptionId) {
212
+ if (!subscriptionId) {
213
+ throw new Error('subscriptionId is required');
214
+ }
215
+
216
+ return client.request(`/stripe/subscriptions/${encodeURIComponent(subscriptionId)}`);
217
+ }
218
+
219
+ /**
220
+ * Format subscription amount for display
221
+ *
222
+ * @param {number} amount - Amount in cents
223
+ * @param {string} [currency='usd'] - Currency code
224
+ * @param {string} [interval='month'] - Billing interval
225
+ * @returns {string} Formatted amount (e.g., "$29.99/month")
226
+ */
227
+ export function formatSubscriptionAmount(amount, currency = 'usd', interval = 'month') {
228
+ const dollars = (amount / 100).toFixed(2);
229
+ const symbol = currency.toLowerCase() === 'usd' ? '$' : currency.toUpperCase() + ' ';
230
+ return `${symbol}${dollars}/${interval}`;
231
+ }