@sails-pay/bachs 0.0.4 → 0.0.5
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/helpers/payloads.js +5 -1
- package/helpers/validate-checkout-connect.js +54 -0
- package/machines/checkout.js +16 -0
- package/package.json +1 -1
- package/test/checkout.test.js +156 -0
package/helpers/payloads.js
CHANGED
|
@@ -81,7 +81,11 @@ function buildCheckoutSessionPayload(inputs, adapterConfig = {}) {
|
|
|
81
81
|
return_url: returnUrl,
|
|
82
82
|
cancel_url: inputs.cancelUrl || adapterConfig.cancelUrl,
|
|
83
83
|
reference: inputs.reference,
|
|
84
|
-
metadata: inputs.metadata || checkoutData.custom
|
|
84
|
+
metadata: inputs.metadata || checkoutData.custom,
|
|
85
|
+
transfer_data: inputs.connect && {
|
|
86
|
+
destination: inputs.connect.destination
|
|
87
|
+
},
|
|
88
|
+
platform_fee: inputs.connect && inputs.connect.platformFee
|
|
85
89
|
})
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const allowedFields = new Set(['destination', 'platformFee'])
|
|
2
|
+
const decimalStringPattern = /^\d+(?:\.\d+)?$/
|
|
3
|
+
|
|
4
|
+
function invalid(field, message) {
|
|
5
|
+
return {
|
|
6
|
+
field,
|
|
7
|
+
message: `${field} ${message}`
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates the `connect` checkout input that splits a checkout between a
|
|
13
|
+
* connected account and the platform. Returns an error object, or null.
|
|
14
|
+
*/
|
|
15
|
+
function validateCheckoutConnect(connect, { isCheckoutSession }) {
|
|
16
|
+
if (!connect || typeof connect !== 'object' || Array.isArray(connect)) {
|
|
17
|
+
return invalid('connect', 'must be an object.')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!isCheckoutSession) {
|
|
21
|
+
return invalid(
|
|
22
|
+
'connect',
|
|
23
|
+
'is only supported on product checkout sessions (items or productCollectionId).'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const field of Object.keys(connect)) {
|
|
28
|
+
if (!allowedFields.has(field)) {
|
|
29
|
+
return invalid(
|
|
30
|
+
`connect.${field}`,
|
|
31
|
+
'is not supported. Use connect.destination and connect.platformFee.'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof connect.destination !== 'string' || !connect.destination) {
|
|
37
|
+
return invalid('connect.destination', 'must be a connected account ID.')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
typeof connect.platformFee !== 'string' ||
|
|
42
|
+
!decimalStringPattern.test(connect.platformFee) ||
|
|
43
|
+
Number(connect.platformFee) <= 0
|
|
44
|
+
) {
|
|
45
|
+
return invalid(
|
|
46
|
+
'connect.platformFee',
|
|
47
|
+
'must be a positive decimal string, e.g. "500.00".'
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = validateCheckoutConnect
|
package/machines/checkout.js
CHANGED
|
@@ -4,6 +4,7 @@ const {
|
|
|
4
4
|
buildPureCheckoutPayload
|
|
5
5
|
} = require('../helpers/payloads')
|
|
6
6
|
const validateCheckoutItems = require('../helpers/validate-checkout-items')
|
|
7
|
+
const validateCheckoutConnect = require('../helpers/validate-checkout-connect')
|
|
7
8
|
const parameters = require('../helpers/parameters')
|
|
8
9
|
|
|
9
10
|
module.exports = require('machine').build({
|
|
@@ -116,6 +117,11 @@ module.exports = require('machine').build({
|
|
|
116
117
|
simulatedOutcome: {
|
|
117
118
|
type: 'string',
|
|
118
119
|
description: 'Sandbox-only forced outcome.'
|
|
120
|
+
},
|
|
121
|
+
connect: {
|
|
122
|
+
type: 'ref',
|
|
123
|
+
description:
|
|
124
|
+
'Splits a checkout session with a connected account: { destination, platformFee }. The account receives the rest of the sale when it settles.'
|
|
119
125
|
}
|
|
120
126
|
},
|
|
121
127
|
exits: {
|
|
@@ -158,6 +164,16 @@ module.exports = require('machine').build({
|
|
|
158
164
|
}
|
|
159
165
|
}
|
|
160
166
|
|
|
167
|
+
if (inputs.connect !== undefined) {
|
|
168
|
+
const connectError = validateCheckoutConnect(inputs.connect, {
|
|
169
|
+
isCheckoutSession: shouldUseCheckoutSession
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (connectError) {
|
|
173
|
+
return exits.invalidRequest(connectError)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
161
177
|
const path = shouldUseCheckoutSession ? '/checkout-sessions' : '/checkouts'
|
|
162
178
|
const payload = shouldUseCheckoutSession
|
|
163
179
|
? buildCheckoutSessionPayload(inputs, adapterConfig)
|
package/package.json
CHANGED
package/test/checkout.test.js
CHANGED
|
@@ -363,3 +363,159 @@ test('adapter exposes checkout.get as the uniform checkout lookup API', async ()
|
|
|
363
363
|
)
|
|
364
364
|
assert.equal(calls[0].options.method, 'GET')
|
|
365
365
|
})
|
|
366
|
+
|
|
367
|
+
test('checkout splits a checkout session with a connected account', async () => {
|
|
368
|
+
const calls = []
|
|
369
|
+
|
|
370
|
+
fetch.setFetchImplementation(async (url, options) => {
|
|
371
|
+
calls.push({ url, options })
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
status: 201,
|
|
376
|
+
statusText: 'Created',
|
|
377
|
+
text: async () =>
|
|
378
|
+
JSON.stringify({
|
|
379
|
+
checkout_id: 'chk_split',
|
|
380
|
+
checkout_url: 'https://pay.bachs.io/c/split'
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const checkoutUrl = await checkout({
|
|
386
|
+
apiKey: 'sk_sandbox_123',
|
|
387
|
+
items: [{ product: 'prod_abc123', amount: '10500.00' }],
|
|
388
|
+
reference: 'sponsorship_1',
|
|
389
|
+
connect: {
|
|
390
|
+
destination: 'acct_123',
|
|
391
|
+
platformFee: '500.00'
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
assert.equal(checkoutUrl, 'https://pay.bachs.io/c/split')
|
|
396
|
+
assert.equal(
|
|
397
|
+
calls[0].url,
|
|
398
|
+
'https://sandbox-api.bachs.io/v1/checkout-sessions'
|
|
399
|
+
)
|
|
400
|
+
assert.equal(calls[0].options.headers['X-Account-Id'], undefined)
|
|
401
|
+
|
|
402
|
+
const body = JSON.parse(calls[0].options.body)
|
|
403
|
+
assert.deepEqual(body.transfer_data, { destination: 'acct_123' })
|
|
404
|
+
assert.equal(body.platform_fee, '500.00')
|
|
405
|
+
assert.equal(body.connect, undefined)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('checkout without connect sends no split fields', async () => {
|
|
409
|
+
const calls = []
|
|
410
|
+
|
|
411
|
+
fetch.setFetchImplementation(async (url, options) => {
|
|
412
|
+
calls.push({ url, options })
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
status: 201,
|
|
417
|
+
statusText: 'Created',
|
|
418
|
+
text: async () =>
|
|
419
|
+
JSON.stringify({ checkout_url: 'https://pay.bachs.io/c/plain' })
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
await checkout({
|
|
424
|
+
apiKey: 'sk_sandbox_123',
|
|
425
|
+
items: [{ product: 'prod_abc123' }]
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
const body = JSON.parse(calls[0].options.body)
|
|
429
|
+
assert.equal('transfer_data' in body, false)
|
|
430
|
+
assert.equal('platform_fee' in body, false)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
const invalidConnectCases = [
|
|
434
|
+
{
|
|
435
|
+
name: 'connect that is not an object',
|
|
436
|
+
inputs: { items: [{ product: 'prod_abc123' }], connect: 'acct_123' },
|
|
437
|
+
field: 'connect',
|
|
438
|
+
message: /must be an object/
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: 'connect on a pure checkout',
|
|
442
|
+
inputs: {
|
|
443
|
+
amount: '10500.00',
|
|
444
|
+
currency: 'NGN',
|
|
445
|
+
connect: { destination: 'acct_123', platformFee: '500.00' }
|
|
446
|
+
},
|
|
447
|
+
field: 'connect',
|
|
448
|
+
message: /only supported on product checkout sessions/
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: 'connect without a destination',
|
|
452
|
+
inputs: {
|
|
453
|
+
items: [{ product: 'prod_abc123' }],
|
|
454
|
+
connect: { platformFee: '500.00' }
|
|
455
|
+
},
|
|
456
|
+
field: 'connect.destination',
|
|
457
|
+
message: /connected account ID/
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: 'connect without a platform fee',
|
|
461
|
+
inputs: {
|
|
462
|
+
items: [{ product: 'prod_abc123' }],
|
|
463
|
+
connect: { destination: 'acct_123' }
|
|
464
|
+
},
|
|
465
|
+
field: 'connect.platformFee',
|
|
466
|
+
message: /positive decimal string/
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'a numeric platform fee',
|
|
470
|
+
inputs: {
|
|
471
|
+
items: [{ product: 'prod_abc123' }],
|
|
472
|
+
connect: { destination: 'acct_123', platformFee: 500 }
|
|
473
|
+
},
|
|
474
|
+
field: 'connect.platformFee',
|
|
475
|
+
message: /positive decimal string/
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: 'a zero platform fee',
|
|
479
|
+
inputs: {
|
|
480
|
+
items: [{ product: 'prod_abc123' }],
|
|
481
|
+
connect: { destination: 'acct_123', platformFee: '0.00' }
|
|
482
|
+
},
|
|
483
|
+
field: 'connect.platformFee',
|
|
484
|
+
message: /positive decimal string/
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'an unknown connect field',
|
|
488
|
+
inputs: {
|
|
489
|
+
items: [{ product: 'prod_abc123' }],
|
|
490
|
+
connect: {
|
|
491
|
+
destination: 'acct_123',
|
|
492
|
+
platformFee: '500.00',
|
|
493
|
+
amount: '10000.00'
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
field: 'connect.amount',
|
|
497
|
+
message: /is not supported/
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
for (const invalidCase of invalidConnectCases) {
|
|
502
|
+
test(`checkout rejects ${invalidCase.name} before calling Bachs`, async () => {
|
|
503
|
+
let fetchCalls = 0
|
|
504
|
+
|
|
505
|
+
fetch.setFetchImplementation(async () => {
|
|
506
|
+
fetchCalls += 1
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
await assert.rejects(
|
|
510
|
+
() => checkout({ apiKey: 'sk_sandbox_123', ...invalidCase.inputs }),
|
|
511
|
+
(error) => {
|
|
512
|
+
assert.equal(error.exit, 'invalidRequest')
|
|
513
|
+
assert.equal(error.raw.field, invalidCase.field)
|
|
514
|
+
assert.match(error.raw.message, invalidCase.message)
|
|
515
|
+
return true
|
|
516
|
+
}
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
assert.equal(fetchCalls, 0)
|
|
520
|
+
})
|
|
521
|
+
}
|