@mandateops/audit 0.0.0-stage → 0.2.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/LICENSE +55 -0
- package/README.md +33 -2
- package/dist/bun.d.ts +13 -0
- package/dist/bun.js +28 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +532 -0
- package/dist/providers/common.d.ts +9 -0
- package/dist/providers/common.js +8 -0
- package/dist/providers/crypto.d.ts +36 -0
- package/dist/providers/crypto.js +188 -0
- package/dist/providers/fiat.d.ts +106 -0
- package/dist/providers/fiat.js +594 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/types.d.ts +122 -0
- package/dist/providers/types.js +1 -0
- package/dist/reading.d.ts +117 -0
- package/dist/reading.js +280 -0
- package/package.json +64 -4
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { CREATED_AT, CREATED_UNIX, undocumented } from "./common.js";
|
|
2
|
+
/** The currencies these formats are tried with; any other is refused. */
|
|
3
|
+
const CURRENCIES = [
|
|
4
|
+
"AUD",
|
|
5
|
+
"BHD",
|
|
6
|
+
"BRL",
|
|
7
|
+
"CAD",
|
|
8
|
+
"CHF",
|
|
9
|
+
"DKK",
|
|
10
|
+
"EUR",
|
|
11
|
+
"GBP",
|
|
12
|
+
"GHS",
|
|
13
|
+
"HKD",
|
|
14
|
+
"HUF",
|
|
15
|
+
"INR",
|
|
16
|
+
"JPY",
|
|
17
|
+
"KES",
|
|
18
|
+
"KRW",
|
|
19
|
+
"KWD",
|
|
20
|
+
"MXN",
|
|
21
|
+
"NGN",
|
|
22
|
+
"NOK",
|
|
23
|
+
"NZD",
|
|
24
|
+
"OMR",
|
|
25
|
+
"PLN",
|
|
26
|
+
"SEK",
|
|
27
|
+
"SGD",
|
|
28
|
+
"TWD",
|
|
29
|
+
"USD",
|
|
30
|
+
"ZAR",
|
|
31
|
+
];
|
|
32
|
+
/** ISO 4217 minor digits, as the runtime's own currency data states them. */
|
|
33
|
+
const minorDigitsOf = (currency) => new Intl.NumberFormat("en", {
|
|
34
|
+
style: "currency",
|
|
35
|
+
currency,
|
|
36
|
+
}).resolvedOptions().maximumFractionDigits ?? 0;
|
|
37
|
+
const CURRENCY_DIGITS = Object.fromEntries(CURRENCIES.map((currency) => [currency, minorDigitsOf(currency)]));
|
|
38
|
+
const DECIMAL = { scaleTerm: "currency", scales: CURRENCY_DIGITS };
|
|
39
|
+
/**
|
|
40
|
+
* `POST /transfer`, from Paystack's published OpenAPI description. The amount
|
|
41
|
+
* is in kobo or pesewas, and a reference is lowercase alphanumeric with `-` and
|
|
42
|
+
* `_`. Reuse of a reference is unverified: its docs both reject one and advise
|
|
43
|
+
* retrying with it. `currency` is optional there, defaulting to NGN.
|
|
44
|
+
*/
|
|
45
|
+
export const paystack = (terms) => ({
|
|
46
|
+
path: "/transfer",
|
|
47
|
+
key: { field: "reference", shape: "lowercase_slug" },
|
|
48
|
+
fields: {
|
|
49
|
+
amount: "amount",
|
|
50
|
+
currency: "currency",
|
|
51
|
+
destination: "recipient",
|
|
52
|
+
},
|
|
53
|
+
defaults: { currency: "NGN" },
|
|
54
|
+
fixed: terms,
|
|
55
|
+
replies: {
|
|
56
|
+
paid: {
|
|
57
|
+
status: 200,
|
|
58
|
+
body: {
|
|
59
|
+
status: true,
|
|
60
|
+
message: "Transfer has been queued",
|
|
61
|
+
data: {
|
|
62
|
+
amount: "{{amount:number}}",
|
|
63
|
+
currency: "{{currency}}",
|
|
64
|
+
reference: "{{key}}",
|
|
65
|
+
source: "balance",
|
|
66
|
+
status: "success",
|
|
67
|
+
transfer_code: "TRF_{{effectId}}",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
declined: {
|
|
72
|
+
status: 400,
|
|
73
|
+
body: { status: false, message: "Transfer declined" },
|
|
74
|
+
},
|
|
75
|
+
conflict: {
|
|
76
|
+
status: 400,
|
|
77
|
+
body: { status: false, message: "Duplicate Transaction Reference" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const stripe = (path, terms, paid) => ({
|
|
82
|
+
path,
|
|
83
|
+
encoding: "form",
|
|
84
|
+
key: { header: "idempotency-key", maxLength: 255 },
|
|
85
|
+
fields: {
|
|
86
|
+
amount: "amount",
|
|
87
|
+
currency: "currency",
|
|
88
|
+
destination: "destination",
|
|
89
|
+
},
|
|
90
|
+
fixed: terms,
|
|
91
|
+
replies: {
|
|
92
|
+
paid: {
|
|
93
|
+
status: 200,
|
|
94
|
+
body: {
|
|
95
|
+
amount: "{{amount:number}}",
|
|
96
|
+
created: CREATED_UNIX,
|
|
97
|
+
currency: "{{currency}}",
|
|
98
|
+
livemode: false,
|
|
99
|
+
...paid,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
// The status of both failures is unverified.
|
|
103
|
+
declined: {
|
|
104
|
+
status: 400,
|
|
105
|
+
body: {
|
|
106
|
+
error: {
|
|
107
|
+
type: "invalid_request_error",
|
|
108
|
+
code: "balance_insufficient",
|
|
109
|
+
message: "Insufficient funds",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
conflict: {
|
|
114
|
+
status: 400,
|
|
115
|
+
body: {
|
|
116
|
+
error: {
|
|
117
|
+
type: "idempotency_error",
|
|
118
|
+
message: "Keys can only be used with the same parameters",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
/** `POST /v1/payouts`. A request without `destination` is refused here. */
|
|
125
|
+
export const stripePayouts = (terms) => stripe("/v1/payouts", terms, {
|
|
126
|
+
id: "po_{{effectId}}",
|
|
127
|
+
object: "payout",
|
|
128
|
+
arrival_date: CREATED_UNIX,
|
|
129
|
+
automatic: false,
|
|
130
|
+
method: "standard",
|
|
131
|
+
reconciliation_status: "not_applicable",
|
|
132
|
+
source_type: "card",
|
|
133
|
+
status: "pending",
|
|
134
|
+
type: "bank_account",
|
|
135
|
+
});
|
|
136
|
+
/** `POST /v1/transfers`, to a connected account. */
|
|
137
|
+
export const stripeTransfers = (terms) => stripe("/v1/transfers", terms, {
|
|
138
|
+
id: "tr_{{effectId}}",
|
|
139
|
+
object: "transfer",
|
|
140
|
+
amount_reversed: 0,
|
|
141
|
+
destination: "{{destination}}",
|
|
142
|
+
metadata: {},
|
|
143
|
+
reversals: { object: "list", data: [], has_more: false },
|
|
144
|
+
reversed: false,
|
|
145
|
+
});
|
|
146
|
+
/**
|
|
147
|
+
* `POST /v3/transfers`. Keyed on the unique `reference`, as v3 coverage of
|
|
148
|
+
* `X-Idempotency-Key` and v3 reference reuse are unverified. The amount is a
|
|
149
|
+
* required integer whose unit is not stated, though samples look major; it is
|
|
150
|
+
* read as given. A synchronous refusal is unverified. The destination is
|
|
151
|
+
* `bank_account`, so a run authorises the two joined.
|
|
152
|
+
*/
|
|
153
|
+
export const flutterwave = (terms) => ({
|
|
154
|
+
path: "/v3/transfers",
|
|
155
|
+
key: { field: "reference" },
|
|
156
|
+
fields: { amount: "amount", currency: "currency" },
|
|
157
|
+
joined: { destination: ["account_bank", "account_number"] },
|
|
158
|
+
fixed: terms,
|
|
159
|
+
replies: {
|
|
160
|
+
paid: {
|
|
161
|
+
status: 200,
|
|
162
|
+
body: {
|
|
163
|
+
status: "success",
|
|
164
|
+
message: "Transfer Queued Successfully",
|
|
165
|
+
data: {
|
|
166
|
+
id: "{{effectId}}",
|
|
167
|
+
currency: "{{currency}}",
|
|
168
|
+
amount: "{{amount:number}}",
|
|
169
|
+
status: "NEW",
|
|
170
|
+
reference: "{{key}}",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
declined: undocumented(400),
|
|
175
|
+
conflict: undocumented(400),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
/**
|
|
179
|
+
* `POST /transfers` of the Transfers API, to a transfer instrument. A refusal
|
|
180
|
+
* is a 202, as the published specification shows, and 422 is its validation
|
|
181
|
+
* status. A processed retry gets the first response; a reused key with a
|
|
182
|
+
* different body is unverified.
|
|
183
|
+
*/
|
|
184
|
+
export const adyen = (terms) => ({
|
|
185
|
+
path: "/transfers",
|
|
186
|
+
credential: { header: "x-api-key", prefix: "" },
|
|
187
|
+
key: { header: "idempotency-key" },
|
|
188
|
+
fields: {
|
|
189
|
+
account: "balanceAccountId",
|
|
190
|
+
amount: "amount.value",
|
|
191
|
+
currency: "amount.currency",
|
|
192
|
+
destination: "counterparty.transferInstrumentId",
|
|
193
|
+
},
|
|
194
|
+
fixed: terms,
|
|
195
|
+
replies: {
|
|
196
|
+
paid: {
|
|
197
|
+
status: 202,
|
|
198
|
+
body: {
|
|
199
|
+
id: "{{effectId}}",
|
|
200
|
+
amount: { value: "{{amount:number}}", currency: "{{currency}}" },
|
|
201
|
+
category: "bank",
|
|
202
|
+
counterparty: { transferInstrumentId: "{{destination}}" },
|
|
203
|
+
status: "authorised",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
declined: {
|
|
207
|
+
status: 202,
|
|
208
|
+
body: {
|
|
209
|
+
amount: { value: "{{amount:number}}", currency: "{{currency}}" },
|
|
210
|
+
category: "bank",
|
|
211
|
+
counterparty: { transferInstrumentId: "{{destination}}" },
|
|
212
|
+
status: "refused",
|
|
213
|
+
reason: "notEnoughBalance",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
conflict: undocumented(422),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
/**
|
|
220
|
+
* `POST /transfers` between entities; the sample names no currency. Idempotency
|
|
221
|
+
* is documented for this route: only 2xx replies are cached, and a concurrent
|
|
222
|
+
* repeat gets 409. The field names come from non-canonical sources, and the
|
|
223
|
+
* amount's unit, the credential scheme and all failures are unverified.
|
|
224
|
+
*/
|
|
225
|
+
export const checkout = (terms) => ({
|
|
226
|
+
path: "/transfers",
|
|
227
|
+
key: { header: "cko-idempotency-key" },
|
|
228
|
+
fields: {
|
|
229
|
+
account: "source.id",
|
|
230
|
+
amount: "source.amount",
|
|
231
|
+
destination: "destination.id",
|
|
232
|
+
},
|
|
233
|
+
fixed: terms,
|
|
234
|
+
replies: {
|
|
235
|
+
paid: {
|
|
236
|
+
status: 201,
|
|
237
|
+
body: {
|
|
238
|
+
id: "tra_{{effectId}}",
|
|
239
|
+
status: "pending",
|
|
240
|
+
_links: { self: { href: "/transfers/tra_{{effectId}}" } },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
declined: undocumented(422),
|
|
244
|
+
conflict: undocumented(422),
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
/** PayPal writes these whole, whatever ISO 4217 says. */
|
|
248
|
+
const PAYPAL_DIGITS = { ...CURRENCY_DIGITS, HUF: 0, TWD: 0 };
|
|
249
|
+
/**
|
|
250
|
+
* `POST /v1/payments/payouts`, one item per batch. An email receiver is not an
|
|
251
|
+
* identifier the lab can hold. A repeated batch id is rejected with a link to
|
|
252
|
+
* the original batch, within 30 days; that status is unverified. Insufficient
|
|
253
|
+
* funds is documented per item, so a synchronous refusal is unverified.
|
|
254
|
+
*/
|
|
255
|
+
export const paypal = (terms) => ({
|
|
256
|
+
path: "/v1/payments/payouts",
|
|
257
|
+
key: { field: "sender_batch_header.sender_batch_id" },
|
|
258
|
+
singleEntry: ["items"],
|
|
259
|
+
fields: {
|
|
260
|
+
amount: "items.0.amount.value",
|
|
261
|
+
currency: "items.0.amount.currency",
|
|
262
|
+
destination: "items.0.receiver",
|
|
263
|
+
},
|
|
264
|
+
fixed: terms,
|
|
265
|
+
amount: { scaleTerm: "currency", scales: PAYPAL_DIGITS },
|
|
266
|
+
replay: "duplicate_reply",
|
|
267
|
+
replies: {
|
|
268
|
+
paid: {
|
|
269
|
+
status: 201,
|
|
270
|
+
body: {
|
|
271
|
+
batch_header: {
|
|
272
|
+
payout_batch_id: "{{effectId}}",
|
|
273
|
+
batch_status: "PENDING",
|
|
274
|
+
sender_batch_header: { sender_batch_id: "{{key}}" },
|
|
275
|
+
time_created: CREATED_AT,
|
|
276
|
+
},
|
|
277
|
+
links: [],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
declined: undocumented(400),
|
|
281
|
+
conflict: undocumented(400),
|
|
282
|
+
duplicate: {
|
|
283
|
+
status: 400,
|
|
284
|
+
body: {
|
|
285
|
+
links: [{ href: "/v1/payments/payouts/{{effectId}}", rel: "self" }],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
/**
|
|
291
|
+
* `POST /v1/payouts`. A repeated key returns the first response; a changed body
|
|
292
|
+
* is unverified. Low balance is a payout state, so a refusal is a reply whose
|
|
293
|
+
* status says so, and every HTTP status here is unverified.
|
|
294
|
+
*/
|
|
295
|
+
export const razorpayx = (terms) => ({
|
|
296
|
+
path: "/v1/payouts",
|
|
297
|
+
credential: { basic: true },
|
|
298
|
+
key: { header: "x-payout-idempotency" },
|
|
299
|
+
fields: {
|
|
300
|
+
account: "account_number",
|
|
301
|
+
amount: "amount",
|
|
302
|
+
currency: "currency",
|
|
303
|
+
destination: "fund_account_id",
|
|
304
|
+
},
|
|
305
|
+
fixed: terms,
|
|
306
|
+
replies: {
|
|
307
|
+
paid: {
|
|
308
|
+
status: 200,
|
|
309
|
+
body: {
|
|
310
|
+
id: "pout_{{effectId}}",
|
|
311
|
+
entity: "payout",
|
|
312
|
+
amount: "{{amount:number}}",
|
|
313
|
+
currency: "{{currency}}",
|
|
314
|
+
status: "queued",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
declined: {
|
|
318
|
+
status: 200,
|
|
319
|
+
body: {
|
|
320
|
+
id: "pout_{{payment}}",
|
|
321
|
+
entity: "payout",
|
|
322
|
+
amount: "{{amount:number}}",
|
|
323
|
+
currency: "{{currency}}",
|
|
324
|
+
status: "rejected",
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
conflict: undocumented(400),
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
const HAL_JSON = { "content-type": "application/hal+json" };
|
|
331
|
+
const omitCurrency = ({ currency: _currency, ...rest }) => rest;
|
|
332
|
+
const mollieError = (status, detail) => ({
|
|
333
|
+
status,
|
|
334
|
+
body: { status, title: detail, detail },
|
|
335
|
+
headers: HAL_JSON,
|
|
336
|
+
});
|
|
337
|
+
/**
|
|
338
|
+
* `POST /v2/payouts`, to the balance's configured bank account. `balanceId` is
|
|
339
|
+
* optional; `account` is what its absence means. Omitting `amount` pays out the
|
|
340
|
+
* whole balance, which the suite states as `wholeBalance`. The reply's
|
|
341
|
+
* `statusReason` is left out, as its codes are unverified.
|
|
342
|
+
*/
|
|
343
|
+
export const molliePayouts = ({ account, wholeBalance, ...terms }) => ({
|
|
344
|
+
path: "/v2/payouts",
|
|
345
|
+
key: { header: "idempotency-key" },
|
|
346
|
+
fields: {
|
|
347
|
+
account: "balanceId",
|
|
348
|
+
amount: "amount.value",
|
|
349
|
+
currency: "amount.currency",
|
|
350
|
+
},
|
|
351
|
+
defaults: { account, currency: terms.currency },
|
|
352
|
+
amountDefault: wholeBalance,
|
|
353
|
+
fixed: omitCurrency(terms),
|
|
354
|
+
amount: DECIMAL,
|
|
355
|
+
replies: {
|
|
356
|
+
paid: {
|
|
357
|
+
status: 201,
|
|
358
|
+
headers: HAL_JSON,
|
|
359
|
+
body: {
|
|
360
|
+
resource: "payout",
|
|
361
|
+
id: "{{effectId}}",
|
|
362
|
+
status: "requested",
|
|
363
|
+
createdAt: CREATED_AT,
|
|
364
|
+
mode: "test",
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
declined: mollieError(422, "Insufficient funds"),
|
|
368
|
+
conflict: mollieError(400, "Idempotency key reused"),
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
/**
|
|
372
|
+
* `POST /api/merchants/{merchantId}/payouts`, one payout per request. Failures
|
|
373
|
+
* are documented as `{ title, message, code }`; which code fits each refusal is
|
|
374
|
+
* unverified, so their bodies stay empty. The merchant is the account, and the
|
|
375
|
+
* destination is `branchCode--accountNumber`. A payout without `payoutId` has no
|
|
376
|
+
* idempotency, so each such request is its own payment.
|
|
377
|
+
*/
|
|
378
|
+
export const peach = (terms) => ({
|
|
379
|
+
path: "/api/merchants/:merchantId/payouts",
|
|
380
|
+
key: { field: "payouts.0.payoutId", optional: true },
|
|
381
|
+
singleEntry: ["payouts"],
|
|
382
|
+
fields: {
|
|
383
|
+
account: ":merchantId",
|
|
384
|
+
amount: "payouts.0.amount",
|
|
385
|
+
currency: "payouts.0.currency",
|
|
386
|
+
},
|
|
387
|
+
joined: {
|
|
388
|
+
destination: ["payouts.0.branchCode", "payouts.0.accountNumber"],
|
|
389
|
+
},
|
|
390
|
+
fixed: terms,
|
|
391
|
+
replay: "duplicate_reply",
|
|
392
|
+
replies: {
|
|
393
|
+
paid: {
|
|
394
|
+
status: 201,
|
|
395
|
+
body: {
|
|
396
|
+
payoutRequestId: "{{effectId}}",
|
|
397
|
+
created: CREATED_AT,
|
|
398
|
+
payouts: [
|
|
399
|
+
{ payoutId: "{{key}}", status: "pending", created: CREATED_AT },
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
declined: undocumented(400),
|
|
404
|
+
conflict: undocumented(400),
|
|
405
|
+
duplicate: undocumented(400),
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
/**
|
|
409
|
+
* `POST /v2/disbursements`, ZAR only. The amount is read as decimal, and a
|
|
410
|
+
* repeated nonce gets 409. Insufficient funds is documented as a later `error`
|
|
411
|
+
* status, so a refusal is a reply in that status. The destination is
|
|
412
|
+
* `bankId--accountNumber`.
|
|
413
|
+
*/
|
|
414
|
+
export const stitch = (terms) => ({
|
|
415
|
+
path: "/v2/disbursements",
|
|
416
|
+
key: { field: "nonce" },
|
|
417
|
+
fields: {
|
|
418
|
+
amount: "amount.quantity",
|
|
419
|
+
currency: "amount.currency",
|
|
420
|
+
},
|
|
421
|
+
joined: { destination: ["beneficiary.bankId", "beneficiary.accountNumber"] },
|
|
422
|
+
fixed: terms,
|
|
423
|
+
amount: { scaleTerm: "currency", scales: { ZAR: 2 } },
|
|
424
|
+
replay: "duplicate_reply",
|
|
425
|
+
replies: {
|
|
426
|
+
paid: {
|
|
427
|
+
status: 201,
|
|
428
|
+
body: { id: "{{effectId}}", status: "pending", createdAt: CREATED_AT },
|
|
429
|
+
},
|
|
430
|
+
declined: {
|
|
431
|
+
status: 201,
|
|
432
|
+
body: { id: "{{payment}}", status: "error", createdAt: CREATED_AT },
|
|
433
|
+
},
|
|
434
|
+
conflict: undocumented(409),
|
|
435
|
+
duplicate: undocumented(409),
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
/**
|
|
439
|
+
* `POST /pay`, from `account_id` to a counterparty's account, per Revolut's
|
|
440
|
+
* OpenAPI description. A reused `request_id` makes no second transaction and
|
|
441
|
+
* answers the documented 400 for two weeks. The id is at most 40 characters.
|
|
442
|
+
*/
|
|
443
|
+
export const revolut = (terms) => {
|
|
444
|
+
const duplicate = {
|
|
445
|
+
status: 400,
|
|
446
|
+
body: { code: 3020, message: "Duplicate request: {{key}}" },
|
|
447
|
+
};
|
|
448
|
+
return {
|
|
449
|
+
path: "/pay",
|
|
450
|
+
key: { field: "request_id", maxLength: 40 },
|
|
451
|
+
fields: {
|
|
452
|
+
account: "account_id",
|
|
453
|
+
amount: "amount",
|
|
454
|
+
currency: "currency",
|
|
455
|
+
destination: "receiver.account_id",
|
|
456
|
+
},
|
|
457
|
+
fixed: terms,
|
|
458
|
+
amount: DECIMAL,
|
|
459
|
+
replay: "duplicate_reply",
|
|
460
|
+
replies: {
|
|
461
|
+
paid: {
|
|
462
|
+
status: 200,
|
|
463
|
+
body: { id: "{{effectId}}", state: "pending", created_at: CREATED_AT },
|
|
464
|
+
},
|
|
465
|
+
declined: undocumented(400),
|
|
466
|
+
conflict: duplicate,
|
|
467
|
+
duplicate,
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
/**
|
|
472
|
+
* `POST /api/payment_orders`. A key is at most 180 characters, and only
|
|
473
|
+
* successes are cached, with a concurrent repeat getting 409. The reply holds a
|
|
474
|
+
* few of the object's fields; its status, a changed body and the failures are
|
|
475
|
+
* unverified. `currency` is optional
|
|
476
|
+
* there, taken from the originating account, so pass it in `defaults`.
|
|
477
|
+
*/
|
|
478
|
+
export const modernTreasury = (terms, defaults = {}) => ({
|
|
479
|
+
path: "/api/payment_orders",
|
|
480
|
+
credential: { basic: true },
|
|
481
|
+
key: { header: "idempotency-key", maxLength: 180 },
|
|
482
|
+
fields: {
|
|
483
|
+
account: "originating_account_id",
|
|
484
|
+
amount: "amount",
|
|
485
|
+
currency: "currency",
|
|
486
|
+
destination: "receiving_account_id",
|
|
487
|
+
},
|
|
488
|
+
defaults,
|
|
489
|
+
fixed: terms,
|
|
490
|
+
replies: {
|
|
491
|
+
paid: {
|
|
492
|
+
status: 201,
|
|
493
|
+
body: {
|
|
494
|
+
id: "{{effectId}}",
|
|
495
|
+
object: "payment_order",
|
|
496
|
+
amount: "{{amount:number}}",
|
|
497
|
+
currency: "{{currency}}",
|
|
498
|
+
direction: "credit",
|
|
499
|
+
status: "approved",
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
declined: undocumented(422),
|
|
503
|
+
conflict: undocumented(422),
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
/**
|
|
507
|
+
* `POST /api/v1/transfers/create`. The amount is a string in the schema and a
|
|
508
|
+
* number in the example; either is read, as decimal, which is unverified, as
|
|
509
|
+
* is the `transfer_amount` name. Both failure codes are from Airwallex's
|
|
510
|
+
* transfer error list.
|
|
511
|
+
*/
|
|
512
|
+
export const airwallex = (terms) => {
|
|
513
|
+
const duplicate = {
|
|
514
|
+
status: 400,
|
|
515
|
+
body: { code: "duplicate_request_id", message: "duplicate_request_id" },
|
|
516
|
+
};
|
|
517
|
+
return {
|
|
518
|
+
path: "/api/v1/transfers/create",
|
|
519
|
+
key: { field: "request_id" },
|
|
520
|
+
fields: {
|
|
521
|
+
amount: "transfer_amount",
|
|
522
|
+
currency: "transfer_currency",
|
|
523
|
+
destination: "beneficiary_id",
|
|
524
|
+
},
|
|
525
|
+
fixed: terms,
|
|
526
|
+
amount: DECIMAL,
|
|
527
|
+
replay: "duplicate_reply",
|
|
528
|
+
replies: {
|
|
529
|
+
paid: {
|
|
530
|
+
status: 201,
|
|
531
|
+
body: {
|
|
532
|
+
id: "{{effectId}}",
|
|
533
|
+
request_id: "{{key}}",
|
|
534
|
+
status: "NEW",
|
|
535
|
+
created_at: CREATED_AT,
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
declined: {
|
|
539
|
+
status: 400,
|
|
540
|
+
body: { code: "balance_insufficient", message: "balance_insufficient" },
|
|
541
|
+
},
|
|
542
|
+
conflict: duplicate,
|
|
543
|
+
duplicate,
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
};
|
|
547
|
+
const DWOLLA_FUNDING = "/funding-sources/";
|
|
548
|
+
/**
|
|
549
|
+
* `POST /transfers`, answered by a `Location`, relative so a client following
|
|
550
|
+
* it stays in the lab. A repeated key gets 201 with the same `Location`; a 409
|
|
551
|
+
* there means the original is still processing, so it is not used for a changed
|
|
552
|
+
* body, whose status is unverified, as are failure bodies.
|
|
553
|
+
*/
|
|
554
|
+
export const dwolla = (terms) => {
|
|
555
|
+
const location = { location: "/transfers/{{effectId}}" };
|
|
556
|
+
return {
|
|
557
|
+
path: "/transfers",
|
|
558
|
+
jsonType: "application/vnd.dwolla.v1.hal+json",
|
|
559
|
+
key: { header: "idempotency-key" },
|
|
560
|
+
fields: {
|
|
561
|
+
account: "_links.source.href",
|
|
562
|
+
amount: "amount.value",
|
|
563
|
+
currency: "amount.currency",
|
|
564
|
+
destination: "_links.destination.href",
|
|
565
|
+
},
|
|
566
|
+
urlFields: { account: DWOLLA_FUNDING, destination: DWOLLA_FUNDING },
|
|
567
|
+
fixed: terms,
|
|
568
|
+
amount: DECIMAL,
|
|
569
|
+
replies: {
|
|
570
|
+
paid: { status: 201, body: null, headers: location },
|
|
571
|
+
declined: undocumented(400),
|
|
572
|
+
conflict: undocumented(400),
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
};
|
|
576
|
+
const WISE_ROUTES = {
|
|
577
|
+
quotes: "/v3/profiles/:profileId/quotes",
|
|
578
|
+
transfers: "/v1/transfers",
|
|
579
|
+
fund: "/v3/profiles/:profileId/transfers/:transferId/payments",
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* Quote, transfer, then fund, from Wise's API reference. Only the fund call
|
|
583
|
+
* moves money. The account is the quote's profile, the currency its source and
|
|
584
|
+
* the asset its target; `customerTransactionId` is the key, and a repeat makes
|
|
585
|
+
* no duplicate, though its reply body and quote reuse are unverified. Wise also
|
|
586
|
+
* serves dated bases such as `/2026Q4`, so a client may override the routes.
|
|
587
|
+
* Recipient creation is not imitated.
|
|
588
|
+
*/
|
|
589
|
+
export const wise = (terms, routes = WISE_ROUTES) => ({
|
|
590
|
+
flow: "wise",
|
|
591
|
+
fixed: terms,
|
|
592
|
+
scales: CURRENCY_DIGITS,
|
|
593
|
+
routes,
|
|
594
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider formats the stand-in can speak, so a customer's provider client can
|
|
3
|
+
* run against it within the subset `contracts/synthetic-provider.md` states per
|
|
4
|
+
* format. Each imitates one payout call's shape; none of them establishes how
|
|
5
|
+
* that provider behaves.
|
|
6
|
+
*/
|
|
7
|
+
export { bridge, circleMint, fireblocks } from "./crypto.js";
|
|
8
|
+
export { adyen, airwallex, checkout, dwolla, flutterwave, modernTreasury, molliePayouts, paypal, paystack, peach, razorpayx, revolut, stitch, stripePayouts, stripeTransfers, wise, } from "./fiat.js";
|
|
9
|
+
export type * from "./types.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider formats the stand-in can speak, so a customer's provider client can
|
|
3
|
+
* run against it within the subset `contracts/synthetic-provider.md` states per
|
|
4
|
+
* format. Each imitates one payout call's shape; none of them establishes how
|
|
5
|
+
* that provider behaves.
|
|
6
|
+
*/
|
|
7
|
+
export { bridge, circleMint, fireblocks } from "./crypto.js";
|
|
8
|
+
export { adyen, airwallex, checkout, dwolla, flutterwave, modernTreasury, molliePayouts, paypal, paystack, peach, razorpayx, revolut, stitch, stripePayouts, stripeTransfers, wise, } from "./fiat.js";
|