swiftshopr-payments 1.0.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.
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Payments API Module
3
+ * Handles payment sessions, transfers, and status checks
4
+ */
5
+
6
+ const { SwiftShoprError } = require('./utils/http');
7
+
8
+ /**
9
+ * UUID validation regex
10
+ */
11
+ const UUID_REGEX =
12
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
+
14
+ /**
15
+ * Ethereum address validation regex
16
+ */
17
+ const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
18
+
19
+ /**
20
+ * Validate payment session parameters
21
+ */
22
+ function validateSessionParams(params) {
23
+ const errors = [];
24
+
25
+ if (
26
+ !params.amount ||
27
+ typeof params.amount !== 'number' ||
28
+ params.amount <= 0
29
+ ) {
30
+ errors.push('amount must be a positive number');
31
+ }
32
+
33
+ if (params.amount > 10000) {
34
+ errors.push('amount cannot exceed $10,000 per transaction');
35
+ }
36
+
37
+ if (
38
+ params.destinationAddress &&
39
+ !ETH_ADDRESS_REGEX.test(params.destinationAddress)
40
+ ) {
41
+ errors.push('destinationAddress must be a valid Ethereum address');
42
+ }
43
+
44
+ if (!params.storeId && !params.destinationAddress) {
45
+ errors.push('Either storeId or destinationAddress is required');
46
+ }
47
+
48
+ if (errors.length > 0) {
49
+ throw new SwiftShoprError('VALIDATION_ERROR', errors.join('; '), {
50
+ errors,
51
+ });
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Validate transfer parameters
57
+ */
58
+ function validateTransferParams(params) {
59
+ const errors = [];
60
+
61
+ if (
62
+ !params.amount ||
63
+ typeof params.amount !== 'number' ||
64
+ params.amount <= 0
65
+ ) {
66
+ errors.push('amount must be a positive number');
67
+ }
68
+
69
+ if (params.amount > 10000) {
70
+ errors.push('amount cannot exceed $10,000 per transaction');
71
+ }
72
+
73
+ if (
74
+ !params.userWalletAddress ||
75
+ !ETH_ADDRESS_REGEX.test(params.userWalletAddress)
76
+ ) {
77
+ errors.push('userWalletAddress must be a valid Ethereum address');
78
+ }
79
+
80
+ if (
81
+ params.destinationAddress &&
82
+ !ETH_ADDRESS_REGEX.test(params.destinationAddress)
83
+ ) {
84
+ errors.push('destinationAddress must be a valid Ethereum address');
85
+ }
86
+
87
+ if (!params.storeId && !params.destinationAddress) {
88
+ errors.push('Either storeId or destinationAddress is required');
89
+ }
90
+
91
+ if (errors.length > 0) {
92
+ throw new SwiftShoprError('VALIDATION_ERROR', errors.join('; '), {
93
+ errors,
94
+ });
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Create Payments API instance
100
+ */
101
+ function createPaymentsAPI(http) {
102
+ return {
103
+ /**
104
+ * Create an onramp session (Add Funds button)
105
+ * User will be directed to Coinbase to purchase USDC
106
+ *
107
+ * @param {Object} params
108
+ * @param {number} params.amount - Amount in USD
109
+ * @param {string} [params.orderId] - Your order reference
110
+ * @param {string} [params.storeId] - Store ID (resolves wallet automatically)
111
+ * @param {string} [params.destinationAddress] - Retailer wallet address
112
+ * @param {string} [params.paymentMethod] - ACH_BANK_ACCOUNT, CARD, etc.
113
+ * @returns {Promise<Object>} Session with onramp_url
114
+ */
115
+ async createSession(params) {
116
+ validateSessionParams(params);
117
+
118
+ const body = {
119
+ paymentAmount: params.amount.toString(),
120
+ paymentCurrency: 'USD',
121
+ paymentMethod: params.paymentMethod || 'ACH_BANK_ACCOUNT',
122
+ ...(params.orderId && { orderId: params.orderId }),
123
+ ...(params.storeId && { storeId: params.storeId }),
124
+ ...(params.destinationAddress && {
125
+ destinationAddress: params.destinationAddress,
126
+ }),
127
+ ...(params.metadata && { metadata: params.metadata }),
128
+ };
129
+
130
+ const response = await http.post('/api/v1/sdk/onramp/session', body, {
131
+ idempotencyKey: params.idempotencyKey,
132
+ });
133
+
134
+ return {
135
+ intentId: response.intent_id,
136
+ sessionId: response.session_id,
137
+ orderId: response.order_id,
138
+ quoteId: response.quote_id,
139
+ onrampUrl: response.onramp_url,
140
+ expiresAt: response.expires_at,
141
+ branding: response.branding || null,
142
+ };
143
+ },
144
+
145
+ /**
146
+ * Create a direct transfer (Purchase button)
147
+ * For users who already have USDC balance
148
+ *
149
+ * @param {Object} params
150
+ * @param {number} params.amount - Amount in USD
151
+ * @param {string} params.userWalletAddress - User's wallet address
152
+ * @param {string} [params.orderId] - Your order reference
153
+ * @param {string} [params.storeId] - Store ID (resolves wallet automatically)
154
+ * @param {string} [params.destinationAddress] - Retailer wallet address
155
+ * @returns {Promise<Object>} Transfer instructions
156
+ */
157
+ async createTransfer(params) {
158
+ validateTransferParams(params);
159
+
160
+ const body = {
161
+ amount: params.amount.toString(),
162
+ userWalletAddress: params.userWalletAddress,
163
+ ...(params.orderId && { orderId: params.orderId }),
164
+ ...(params.storeId && { storeId: params.storeId }),
165
+ ...(params.destinationAddress && {
166
+ destinationAddress: params.destinationAddress,
167
+ }),
168
+ ...(params.metadata && { metadata: params.metadata }),
169
+ };
170
+
171
+ const response = await http.post('/api/v1/sdk/onramp/transfer', body, {
172
+ idempotencyKey: params.idempotencyKey,
173
+ });
174
+
175
+ return {
176
+ intentId: response.intent_id,
177
+ orderId: response.order_id,
178
+ status: response.status,
179
+ transfer: {
180
+ to: response.transfer.to,
181
+ amount: response.transfer.amount,
182
+ asset: response.transfer.asset,
183
+ network: response.transfer.network,
184
+ chainId: response.transfer.chain_id,
185
+ },
186
+ expiresAt: response.expires_at,
187
+ branding: response.branding || null,
188
+ };
189
+ },
190
+
191
+ /**
192
+ * Get payment status by intent ID
193
+ *
194
+ * @param {string} intentId - Payment intent ID (UUID)
195
+ * @returns {Promise<Object>} Payment status
196
+ */
197
+ async getStatus(intentId) {
198
+ if (!intentId) {
199
+ throw new SwiftShoprError('VALIDATION_ERROR', 'intentId is required');
200
+ }
201
+
202
+ const response = await http.get(
203
+ `/api/v1/sdk/onramp/payments/${intentId}/status`,
204
+ );
205
+
206
+ return {
207
+ intentId: response.intent_id,
208
+ orderId: response.order_id,
209
+ status: response.status,
210
+ amount: parseFloat(response.amount_usd),
211
+ storeId: response.store_id,
212
+ destinationAddress: response.destination_address,
213
+ txHash: response.tx_hash || null,
214
+ explorerUrl: response.explorer_url || null,
215
+ confirmedAt: response.confirmed_at || null,
216
+ createdAt: response.created_at,
217
+ expiresAt: response.expires_at,
218
+ isExpired: response.is_expired,
219
+ refund: response.refund || null,
220
+ };
221
+ },
222
+
223
+ /**
224
+ * Get payment status by order ID
225
+ *
226
+ * @param {string} orderId - Your order reference
227
+ * @returns {Promise<Object>} Payment status
228
+ */
229
+ async getStatusByOrderId(orderId) {
230
+ if (!orderId) {
231
+ throw new SwiftShoprError('VALIDATION_ERROR', 'orderId is required');
232
+ }
233
+
234
+ const response = await http.get(
235
+ `/api/v1/sdk/onramp/payments/${encodeURIComponent(orderId)}/status?by=order`,
236
+ );
237
+
238
+ return {
239
+ intentId: response.intent_id,
240
+ orderId: response.order_id,
241
+ status: response.status,
242
+ amount: parseFloat(response.amount_usd),
243
+ storeId: response.store_id,
244
+ destinationAddress: response.destination_address,
245
+ txHash: response.tx_hash || null,
246
+ explorerUrl: response.explorer_url || null,
247
+ confirmedAt: response.confirmed_at || null,
248
+ createdAt: response.created_at,
249
+ expiresAt: response.expires_at,
250
+ isExpired: response.is_expired,
251
+ refund: response.refund || null,
252
+ };
253
+ },
254
+
255
+ /**
256
+ * Poll for payment completion
257
+ * Useful for waiting for webhook confirmation
258
+ *
259
+ * @param {string} intentId - Payment intent ID
260
+ * @param {Object} [options]
261
+ * @param {number} [options.timeout=300000] - Max wait time (default 5 min)
262
+ * @param {number} [options.interval=2000] - Poll interval (default 2s)
263
+ * @param {Function} [options.onStatusChange] - Callback on status change
264
+ * @returns {Promise<Object>} Final payment status
265
+ */
266
+ async waitForCompletion(intentId, options = {}) {
267
+ const timeout = options.timeout || 300000;
268
+ const interval = options.interval || 2000;
269
+ const { onStatusChange } = options;
270
+
271
+ const startTime = Date.now();
272
+ let lastStatus = null;
273
+
274
+ while (Date.now() - startTime < timeout) {
275
+ const status = await this.getStatus(intentId);
276
+
277
+ if (lastStatus !== status.status) {
278
+ lastStatus = status.status;
279
+ if (onStatusChange) {
280
+ onStatusChange(status);
281
+ }
282
+ }
283
+
284
+ if (status.status === 'completed' || status.status === 'COMPLETED') {
285
+ return status;
286
+ }
287
+
288
+ if (status.status === 'failed' || status.status === 'FAILED') {
289
+ throw new SwiftShoprError('PAYMENT_FAILED', 'Payment failed', status);
290
+ }
291
+
292
+ if (status.status === 'canceled' || status.status === 'CANCELED') {
293
+ throw new SwiftShoprError(
294
+ 'PAYMENT_CANCELED',
295
+ 'Payment was canceled',
296
+ status,
297
+ );
298
+ }
299
+
300
+ if (status.isExpired) {
301
+ throw new SwiftShoprError(
302
+ 'PAYMENT_EXPIRED',
303
+ 'Payment expired',
304
+ status,
305
+ );
306
+ }
307
+
308
+ await new Promise((resolve) => setTimeout(resolve, interval));
309
+ }
310
+
311
+ throw new SwiftShoprError(
312
+ 'TIMEOUT',
313
+ `Payment did not complete within ${timeout}ms`,
314
+ );
315
+ },
316
+ };
317
+ }
318
+
319
+ module.exports = { createPaymentsAPI };
package/src/refunds.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Refunds API Module
3
+ * Handles refund requests and status tracking
4
+ */
5
+
6
+ const { SwiftShoprError } = require('./utils/http');
7
+
8
+ /**
9
+ * UUID validation regex
10
+ */
11
+ const UUID_REGEX =
12
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
+
14
+ /**
15
+ * Validate refund parameters
16
+ */
17
+ function validateRefundParams(params) {
18
+ const errors = [];
19
+
20
+ if (!params.intentId) {
21
+ errors.push('intentId is required');
22
+ } else if (!UUID_REGEX.test(params.intentId)) {
23
+ errors.push('intentId must be a valid UUID');
24
+ }
25
+
26
+ if (params.amount !== undefined) {
27
+ if (typeof params.amount !== 'number' || params.amount <= 0) {
28
+ errors.push('amount must be a positive number');
29
+ }
30
+ }
31
+
32
+ if (errors.length > 0) {
33
+ throw new SwiftShoprError('VALIDATION_ERROR', errors.join('; '), {
34
+ errors,
35
+ });
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Create Refunds API instance
41
+ */
42
+ function createRefundsAPI(http) {
43
+ return {
44
+ /**
45
+ * Request a refund for a completed payment
46
+ *
47
+ * @param {Object} params
48
+ * @param {string} params.intentId - Original payment intent ID
49
+ * @param {number} [params.amount] - Refund amount (optional, defaults to full refund)
50
+ * @param {string} [params.reason] - Reason for refund
51
+ * @returns {Promise<Object>} Refund details with transfer instructions
52
+ */
53
+ async create(params) {
54
+ validateRefundParams(params);
55
+
56
+ const body = {
57
+ intent_id: params.intentId,
58
+ ...(params.amount && { amount: params.amount }),
59
+ ...(params.reason && { reason: params.reason }),
60
+ };
61
+
62
+ const response = await http.post('/api/v1/sdk/onramp/refund', body, {
63
+ idempotencyKey: params.idempotencyKey,
64
+ });
65
+
66
+ if (!response.success) {
67
+ throw new SwiftShoprError(
68
+ response.error || 'REFUND_ERROR',
69
+ response.message || 'Refund request failed',
70
+ response,
71
+ );
72
+ }
73
+
74
+ return {
75
+ id: response.refund.id,
76
+ intentId: response.refund.intent_id,
77
+ orderId: response.refund.order_id,
78
+ status: response.refund.status,
79
+ amount: response.refund.amount,
80
+ originalAmount: response.refund.original_amount,
81
+ isPartial: response.refund.is_partial,
82
+ reason: response.refund.reason,
83
+ createdAt: response.refund.created_at,
84
+ instructions: {
85
+ message: response.instructions.message,
86
+ transfer: {
87
+ to: response.instructions.transfer.to,
88
+ amount: response.instructions.transfer.amount,
89
+ asset: response.instructions.transfer.asset,
90
+ network: response.instructions.transfer.network,
91
+ chainId: response.instructions.transfer.chain_id,
92
+ },
93
+ fromWallet: response.instructions.from_wallet,
94
+ },
95
+ };
96
+ },
97
+
98
+ /**
99
+ * Get refund status by refund ID
100
+ *
101
+ * @param {string} refundId - Refund ID (UUID)
102
+ * @returns {Promise<Object>} Refund status
103
+ */
104
+ async get(refundId) {
105
+ if (!refundId) {
106
+ throw new SwiftShoprError('VALIDATION_ERROR', 'refundId is required');
107
+ }
108
+
109
+ const response = await http.get(`/api/v1/sdk/onramp/refund/${refundId}`);
110
+
111
+ if (!response.success) {
112
+ throw new SwiftShoprError(
113
+ response.error || 'REFUND_ERROR',
114
+ response.message || 'Failed to get refund status',
115
+ response,
116
+ );
117
+ }
118
+
119
+ return {
120
+ id: response.refund.id,
121
+ intentId: response.refund.intent_id,
122
+ orderId: response.refund.order_id,
123
+ amount: response.refund.amount,
124
+ reason: response.refund.reason,
125
+ status: response.refund.status,
126
+ txHash: response.refund.tx_hash,
127
+ explorerUrl: response.refund.explorer_url,
128
+ fromAddress: response.refund.from_address,
129
+ toAddress: response.refund.to_address,
130
+ createdAt: response.refund.created_at,
131
+ completedAt: response.refund.completed_at,
132
+ };
133
+ },
134
+
135
+ /**
136
+ * List all refunds for a payment intent
137
+ *
138
+ * @param {string} intentId - Payment intent ID (UUID)
139
+ * @returns {Promise<Array>} List of refunds
140
+ */
141
+ async listForPayment(intentId) {
142
+ if (!intentId) {
143
+ throw new SwiftShoprError('VALIDATION_ERROR', 'intentId is required');
144
+ }
145
+
146
+ const response = await http.get(
147
+ `/api/v1/sdk/onramp/payments/${intentId}/refunds`,
148
+ );
149
+
150
+ return response.map((refund) => ({
151
+ id: refund.id,
152
+ amount: refund.amount,
153
+ reason: refund.reason,
154
+ status: refund.status,
155
+ txHash: refund.tx_hash,
156
+ explorerUrl: refund.explorer_url,
157
+ createdAt: refund.created_at,
158
+ completedAt: refund.completed_at,
159
+ }));
160
+ },
161
+
162
+ /**
163
+ * Poll for refund completion
164
+ *
165
+ * @param {string} refundId - Refund ID
166
+ * @param {Object} [options]
167
+ * @param {number} [options.timeout=600000] - Max wait time (default 10 min)
168
+ * @param {number} [options.interval=5000] - Poll interval (default 5s)
169
+ * @param {Function} [options.onStatusChange] - Callback on status change
170
+ * @returns {Promise<Object>} Final refund status
171
+ */
172
+ async waitForCompletion(refundId, options = {}) {
173
+ const timeout = options.timeout || 600000;
174
+ const interval = options.interval || 5000;
175
+ const { onStatusChange } = options;
176
+
177
+ const startTime = Date.now();
178
+ let lastStatus = null;
179
+
180
+ while (Date.now() - startTime < timeout) {
181
+ const refund = await this.get(refundId);
182
+
183
+ if (lastStatus !== refund.status) {
184
+ lastStatus = refund.status;
185
+ if (onStatusChange) {
186
+ onStatusChange(refund);
187
+ }
188
+ }
189
+
190
+ if (refund.status === 'completed') {
191
+ return refund;
192
+ }
193
+
194
+ if (refund.status === 'failed') {
195
+ throw new SwiftShoprError('REFUND_FAILED', 'Refund failed', refund);
196
+ }
197
+
198
+ await new Promise((resolve) => setTimeout(resolve, interval));
199
+ }
200
+
201
+ throw new SwiftShoprError(
202
+ 'TIMEOUT',
203
+ `Refund did not complete within ${timeout}ms`,
204
+ );
205
+ },
206
+ };
207
+ }
208
+
209
+ module.exports = { createRefundsAPI };