@sails-pay/bachs 0.0.1 → 0.0.3
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/adapter.js +3 -0
- package/helpers/payloads.js +48 -19
- package/helpers/validate-checkout-items.js +177 -0
- package/machines/checkout.js +15 -5
- package/machines/customer/portal.js +57 -0
- package/machines/index.js +3 -0
- package/package.json +1 -1
- package/test/checkout.test.js +271 -6
- package/test/customer.test.js +183 -0
- package/test/payloads.test.js +171 -2
package/adapter.js
CHANGED
package/helpers/payloads.js
CHANGED
|
@@ -44,13 +44,22 @@ function buildProductCart(items) {
|
|
|
44
44
|
return undefined
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
return items.map((item) =>
|
|
48
|
-
|
|
47
|
+
return items.map((item) => {
|
|
48
|
+
const pricing =
|
|
49
|
+
item.amount !== undefined
|
|
50
|
+
? normalizePricing({
|
|
51
|
+
type: 'fixed',
|
|
52
|
+
amount: item.amount
|
|
53
|
+
})
|
|
54
|
+
: normalizePricing(item.pricing)
|
|
55
|
+
|
|
56
|
+
return withoutUndefined({
|
|
49
57
|
product_id: item.product || item.productId,
|
|
50
58
|
quantity: item.quantity,
|
|
51
|
-
|
|
59
|
+
pricing,
|
|
60
|
+
amount: item.chosenAmount
|
|
52
61
|
})
|
|
53
|
-
)
|
|
62
|
+
})
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
function buildCheckoutSessionPayload(inputs, adapterConfig = {}) {
|
|
@@ -76,25 +85,44 @@ function buildCheckoutSessionPayload(inputs, adapterConfig = {}) {
|
|
|
76
85
|
})
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
function
|
|
80
|
-
|
|
88
|
+
function normalizePricing(pricing, fallbackCurrencyOptions) {
|
|
89
|
+
if (!pricing || typeof pricing !== 'object' || Array.isArray(pricing)) {
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
const {
|
|
94
|
+
type,
|
|
95
|
+
presetAmount,
|
|
96
|
+
minimumAmount,
|
|
97
|
+
maximumAmount,
|
|
98
|
+
currencyOptions,
|
|
99
|
+
...pricingInput
|
|
100
|
+
} = pricing
|
|
101
|
+
const normalizedPricing = withoutUndefined({
|
|
102
|
+
...pricingInput,
|
|
103
|
+
price_type: type,
|
|
104
|
+
preset_amount: presetAmount,
|
|
105
|
+
minimum_amount: minimumAmount,
|
|
106
|
+
maximum_amount: maximumAmount,
|
|
107
|
+
currency_options:
|
|
108
|
+
currencyOptions === undefined ? fallbackCurrencyOptions : currencyOptions
|
|
109
|
+
})
|
|
84
110
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
currency_options: inputs.currencyOptions
|
|
94
|
-
})
|
|
111
|
+
return Object.keys(normalizedPricing).length > 0
|
|
112
|
+
? normalizedPricing
|
|
113
|
+
: undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildPricingPayload(inputs) {
|
|
117
|
+
if (inputs.pricing) {
|
|
118
|
+
return normalizePricing(inputs.pricing, inputs.currencyOptions)
|
|
95
119
|
}
|
|
96
120
|
|
|
97
|
-
return
|
|
121
|
+
return normalizePricing({
|
|
122
|
+
currency: inputs.currency,
|
|
123
|
+
amount: inputs.amount,
|
|
124
|
+
currencyOptions: inputs.currencyOptions
|
|
125
|
+
})
|
|
98
126
|
}
|
|
99
127
|
|
|
100
128
|
function buildPureCheckoutPayload(inputs, adapterConfig = {}) {
|
|
@@ -142,5 +170,6 @@ module.exports = {
|
|
|
142
170
|
buildRefundPayload,
|
|
143
171
|
buildProductCart,
|
|
144
172
|
buildCustomerPayload,
|
|
173
|
+
normalizePricing,
|
|
145
174
|
withoutUndefined
|
|
146
175
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const pricingTypes = new Set(['fixed', 'custom', 'free'])
|
|
2
|
+
const pricingMoneyFields = [
|
|
3
|
+
'amount',
|
|
4
|
+
'presetAmount',
|
|
5
|
+
'minimumAmount',
|
|
6
|
+
'maximumAmount'
|
|
7
|
+
]
|
|
8
|
+
const customPricingFields = ['presetAmount', 'minimumAmount', 'maximumAmount']
|
|
9
|
+
const decimalStringPattern = /^\d+(?:\.\d+)?$/
|
|
10
|
+
|
|
11
|
+
function fieldWasProvided(object, field) {
|
|
12
|
+
return object[field] !== undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function invalid(field, message) {
|
|
16
|
+
return {
|
|
17
|
+
field,
|
|
18
|
+
message: `${field} ${message}`
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validateMoney(value, field) {
|
|
23
|
+
if (typeof value !== 'string' || !decimalStringPattern.test(value)) {
|
|
24
|
+
return invalid(field, 'must be a non-negative decimal string.')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validatePricingMoney(pricing, itemPath) {
|
|
29
|
+
for (const field of pricingMoneyFields) {
|
|
30
|
+
if (fieldWasProvided(pricing, field)) {
|
|
31
|
+
const error = validateMoney(
|
|
32
|
+
pricing[field],
|
|
33
|
+
`${itemPath}.pricing.${field}`
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if (error) {
|
|
37
|
+
return error
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateItem(item, index) {
|
|
44
|
+
const itemPath = `items[${index}]`
|
|
45
|
+
|
|
46
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
47
|
+
return invalid(itemPath, 'must be an object.')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
fieldWasProvided(item, 'quantity') &&
|
|
52
|
+
(!Number.isInteger(item.quantity) || item.quantity < 1)
|
|
53
|
+
) {
|
|
54
|
+
return invalid(`${itemPath}.quantity`, 'must be an integer of at least 1.')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hasAmount = fieldWasProvided(item, 'amount')
|
|
58
|
+
const hasPricing = fieldWasProvided(item, 'pricing')
|
|
59
|
+
const hasChosenAmount = fieldWasProvided(item, 'chosenAmount')
|
|
60
|
+
|
|
61
|
+
if (hasAmount && hasPricing) {
|
|
62
|
+
return invalid(
|
|
63
|
+
itemPath,
|
|
64
|
+
'must not provide both amount and pricing; amount is the fixed-price shorthand.'
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (hasAmount) {
|
|
69
|
+
const error = validateMoney(item.amount, `${itemPath}.amount`)
|
|
70
|
+
|
|
71
|
+
if (error) {
|
|
72
|
+
return error
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (hasChosenAmount) {
|
|
76
|
+
return invalid(
|
|
77
|
+
`${itemPath}.chosenAmount`,
|
|
78
|
+
'can only be used with catalog custom pricing or ad-hoc custom pricing.'
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (hasChosenAmount) {
|
|
84
|
+
const error = validateMoney(item.chosenAmount, `${itemPath}.chosenAmount`)
|
|
85
|
+
|
|
86
|
+
if (error) {
|
|
87
|
+
return error
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!hasPricing) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
!item.pricing ||
|
|
97
|
+
typeof item.pricing !== 'object' ||
|
|
98
|
+
Array.isArray(item.pricing)
|
|
99
|
+
) {
|
|
100
|
+
return invalid(`${itemPath}.pricing`, 'must be an object.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pricing = item.pricing
|
|
104
|
+
|
|
105
|
+
if (!pricingTypes.has(pricing.type)) {
|
|
106
|
+
return invalid(
|
|
107
|
+
`${itemPath}.pricing.type`,
|
|
108
|
+
'must be fixed, custom, or free.'
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (pricing.type === 'fixed') {
|
|
113
|
+
if (!fieldWasProvided(pricing, 'amount')) {
|
|
114
|
+
return invalid(
|
|
115
|
+
`${itemPath}.pricing.amount`,
|
|
116
|
+
'is required for fixed pricing.'
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const customField = customPricingFields.find((field) =>
|
|
121
|
+
fieldWasProvided(pricing, field)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (customField) {
|
|
125
|
+
return invalid(
|
|
126
|
+
`${itemPath}.pricing.${customField}`,
|
|
127
|
+
'is not allowed with fixed pricing.'
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (hasChosenAmount) {
|
|
132
|
+
return invalid(
|
|
133
|
+
`${itemPath}.chosenAmount`,
|
|
134
|
+
'can only be used with catalog custom pricing or ad-hoc custom pricing.'
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (pricing.type === 'custom' && fieldWasProvided(pricing, 'amount')) {
|
|
140
|
+
return invalid(
|
|
141
|
+
`${itemPath}.pricing.amount`,
|
|
142
|
+
'is not allowed with custom pricing.'
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (pricing.type === 'free') {
|
|
147
|
+
const monetaryField = pricingMoneyFields.find((field) =>
|
|
148
|
+
fieldWasProvided(pricing, field)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if (monetaryField) {
|
|
152
|
+
return invalid(
|
|
153
|
+
`${itemPath}.pricing.${monetaryField}`,
|
|
154
|
+
'is not allowed with free pricing.'
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (hasChosenAmount) {
|
|
159
|
+
return invalid(
|
|
160
|
+
`${itemPath}.chosenAmount`,
|
|
161
|
+
'is not allowed with free pricing.'
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return validatePricingMoney(pricing, itemPath)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = function validateCheckoutItems(items) {
|
|
170
|
+
for (let index = 0; index < items.length; index++) {
|
|
171
|
+
const error = validateItem(items[index], index)
|
|
172
|
+
|
|
173
|
+
if (error) {
|
|
174
|
+
return error
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
package/machines/checkout.js
CHANGED
|
@@ -3,6 +3,7 @@ const {
|
|
|
3
3
|
buildCheckoutSessionPayload,
|
|
4
4
|
buildPureCheckoutPayload
|
|
5
5
|
} = require('../helpers/payloads')
|
|
6
|
+
const validateCheckoutItems = require('../helpers/validate-checkout-items')
|
|
6
7
|
const parameters = require('../helpers/parameters')
|
|
7
8
|
|
|
8
9
|
module.exports = require('machine').build({
|
|
@@ -16,7 +17,7 @@ module.exports = require('machine').build({
|
|
|
16
17
|
items: {
|
|
17
18
|
type: 'ref',
|
|
18
19
|
description:
|
|
19
|
-
'Product items for Bachs Checkout Sessions.
|
|
20
|
+
'Product items for Bachs Checkout Sessions. Supports catalog pricing, fixed amount shorthand, advanced pricing, and chosenAmount.'
|
|
20
21
|
},
|
|
21
22
|
productCollectionId: {
|
|
22
23
|
type: 'string',
|
|
@@ -141,10 +142,6 @@ module.exports = require('machine').build({
|
|
|
141
142
|
inputs.productCollectionId || inputs.productCollection
|
|
142
143
|
)
|
|
143
144
|
const shouldUseCheckoutSession = hasItems || hasProductCollection
|
|
144
|
-
const path = shouldUseCheckoutSession ? '/checkout-sessions' : '/checkouts'
|
|
145
|
-
const payload = shouldUseCheckoutSession
|
|
146
|
-
? buildCheckoutSessionPayload(inputs, adapterConfig)
|
|
147
|
-
: buildPureCheckoutPayload(inputs, adapterConfig)
|
|
148
145
|
|
|
149
146
|
if (shouldUseCheckoutSession && hasItems === hasProductCollection) {
|
|
150
147
|
return exits.invalidRequest({
|
|
@@ -153,6 +150,19 @@ module.exports = require('machine').build({
|
|
|
153
150
|
})
|
|
154
151
|
}
|
|
155
152
|
|
|
153
|
+
if (hasItems) {
|
|
154
|
+
const validationError = validateCheckoutItems(inputs.items)
|
|
155
|
+
|
|
156
|
+
if (validationError) {
|
|
157
|
+
return exits.invalidRequest(validationError)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const path = shouldUseCheckoutSession ? '/checkout-sessions' : '/checkouts'
|
|
162
|
+
const payload = shouldUseCheckoutSession
|
|
163
|
+
? buildCheckoutSessionPayload(inputs, adapterConfig)
|
|
164
|
+
: buildPureCheckoutPayload(inputs, adapterConfig)
|
|
165
|
+
|
|
156
166
|
if (!shouldUseCheckoutSession && !payload.pricing) {
|
|
157
167
|
return exits.invalidRequest({
|
|
158
168
|
message:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const fetch = require('../../helpers/fetch')
|
|
2
|
+
const parameters = require('../../helpers/parameters')
|
|
3
|
+
|
|
4
|
+
module.exports = require('machine').build({
|
|
5
|
+
friendlyName: 'Create customer portal session',
|
|
6
|
+
description:
|
|
7
|
+
'Creates a fresh Bachs customer portal session and returns its hosted URL.',
|
|
8
|
+
moreInfoUrl:
|
|
9
|
+
'https://docs.bachs.io/guides/customer-portal/create-portal-session',
|
|
10
|
+
inputs: {
|
|
11
|
+
apiKey: parameters.BACHS_API_KEY,
|
|
12
|
+
baseUrl: parameters.BACHS_BASE_URL,
|
|
13
|
+
customerId: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
required: true,
|
|
16
|
+
description: 'The Bachs customer ID.'
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
exits: {
|
|
20
|
+
success: {
|
|
21
|
+
description: 'The fresh customer portal URL.',
|
|
22
|
+
outputVariableName: 'portalUrl',
|
|
23
|
+
outputType: 'string'
|
|
24
|
+
},
|
|
25
|
+
couldNotCreatePortalUrl: {
|
|
26
|
+
description: 'Customer portal URL could not be created.',
|
|
27
|
+
outputVariableName: 'errors',
|
|
28
|
+
outputType: 'ref'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
fn: async function ({ apiKey, baseUrl, customerId }, exits) {
|
|
32
|
+
const adapterConfig = require('../../adapter').config
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const portalSession = await fetch(
|
|
36
|
+
`/customers/${encodeURIComponent(customerId)}/portal-sessions`,
|
|
37
|
+
{
|
|
38
|
+
method: 'POST',
|
|
39
|
+
apiKey: apiKey || adapterConfig.apiKey,
|
|
40
|
+
baseUrl: baseUrl || adapterConfig.baseUrl
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
!portalSession ||
|
|
46
|
+
typeof portalSession.url !== 'string' ||
|
|
47
|
+
portalSession.url.length === 0
|
|
48
|
+
) {
|
|
49
|
+
return exits.couldNotCreatePortalUrl(portalSession)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return exits.success(portalSession.url)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return exits.couldNotCreatePortalUrl(error.bachs || error)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
package/machines/index.js
CHANGED
package/package.json
CHANGED
package/test/checkout.test.js
CHANGED
|
@@ -4,6 +4,11 @@ const adapter = require('../adapter')
|
|
|
4
4
|
const checkout = require('../machines/checkout')
|
|
5
5
|
const fetch = require('../helpers/fetch')
|
|
6
6
|
|
|
7
|
+
test.afterEach(() => {
|
|
8
|
+
fetch.resetFetchImplementation()
|
|
9
|
+
adapter.config = {}
|
|
10
|
+
})
|
|
11
|
+
|
|
7
12
|
test('checkout creates a Bachs checkout session from camelCase inputs', async () => {
|
|
8
13
|
const calls = []
|
|
9
14
|
|
|
@@ -24,7 +29,7 @@ test('checkout creates a Bachs checkout session from camelCase inputs', async ()
|
|
|
24
29
|
|
|
25
30
|
const checkoutUrl = await checkout({
|
|
26
31
|
apiKey: 'sk_sandbox_123',
|
|
27
|
-
items: [{ product: 'prod_abc123'
|
|
32
|
+
items: [{ product: 'prod_abc123' }],
|
|
28
33
|
customer: {
|
|
29
34
|
email: 'customer@example.com',
|
|
30
35
|
name: 'Jane Doe'
|
|
@@ -47,18 +52,280 @@ test('checkout creates a Bachs checkout session from camelCase inputs', async ()
|
|
|
47
52
|
},
|
|
48
53
|
product_cart: [
|
|
49
54
|
{
|
|
50
|
-
product_id: 'prod_abc123'
|
|
51
|
-
quantity: 1
|
|
55
|
+
product_id: 'prod_abc123'
|
|
52
56
|
}
|
|
53
57
|
],
|
|
54
58
|
return_url: 'https://example.com/return',
|
|
55
59
|
cancel_url: 'https://example.com/cancel',
|
|
56
60
|
reference: 'order_123'
|
|
57
61
|
})
|
|
62
|
+
})
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
test('checkout sends custom pricing and the buyer-selected amount', async () => {
|
|
65
|
+
const calls = []
|
|
66
|
+
|
|
67
|
+
fetch.setFetchImplementation(async (url, options) => {
|
|
68
|
+
calls.push({ url, options })
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
status: 201,
|
|
73
|
+
statusText: 'Created',
|
|
74
|
+
text: async () =>
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
checkout_url: 'https://pay.bachs.io/c/custom'
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const checkoutUrl = await checkout({
|
|
82
|
+
apiKey: 'sk_sandbox_123',
|
|
83
|
+
items: [
|
|
84
|
+
{
|
|
85
|
+
product: 'prod_custom',
|
|
86
|
+
pricing: {
|
|
87
|
+
type: 'custom',
|
|
88
|
+
presetAmount: '10.00',
|
|
89
|
+
minimumAmount: '5.00',
|
|
90
|
+
maximumAmount: '100.00'
|
|
91
|
+
},
|
|
92
|
+
chosenAmount: '12.00'
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.equal(checkoutUrl, 'https://pay.bachs.io/c/custom')
|
|
98
|
+
assert.deepEqual(JSON.parse(calls[0].options.body), {
|
|
99
|
+
customer: {},
|
|
100
|
+
product_cart: [
|
|
101
|
+
{
|
|
102
|
+
product_id: 'prod_custom',
|
|
103
|
+
pricing: {
|
|
104
|
+
price_type: 'custom',
|
|
105
|
+
preset_amount: '10.00',
|
|
106
|
+
minimum_amount: '5.00',
|
|
107
|
+
maximum_amount: '100.00'
|
|
108
|
+
},
|
|
109
|
+
amount: '12.00'
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('checkout preserves pure checkout behavior', async () => {
|
|
116
|
+
const calls = []
|
|
117
|
+
|
|
118
|
+
fetch.setFetchImplementation(async (url, options) => {
|
|
119
|
+
calls.push({ url, options })
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
status: 201,
|
|
124
|
+
statusText: 'Created',
|
|
125
|
+
text: async () =>
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
checkout_url: 'https://pay.bachs.io/c/pure'
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const checkoutUrl = await checkout({
|
|
133
|
+
apiKey: 'sk_sandbox_123',
|
|
134
|
+
amount: '42.00',
|
|
135
|
+
currency: 'USD',
|
|
136
|
+
reference: 'pure_123'
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
assert.equal(checkoutUrl, 'https://pay.bachs.io/c/pure')
|
|
140
|
+
assert.equal(calls[0].url, 'https://sandbox-api.bachs.io/v1/checkouts')
|
|
141
|
+
assert.equal(calls[0].options.headers['Idempotency-Key'], 'pure_123')
|
|
142
|
+
assert.deepEqual(JSON.parse(calls[0].options.body), {
|
|
143
|
+
pricing: {
|
|
144
|
+
currency: 'USD',
|
|
145
|
+
amount: '42.00'
|
|
146
|
+
},
|
|
147
|
+
reference: 'pure_123'
|
|
148
|
+
})
|
|
60
149
|
})
|
|
61
150
|
|
|
151
|
+
const invalidItemCases = [
|
|
152
|
+
{
|
|
153
|
+
name: 'amount with explicit pricing',
|
|
154
|
+
item: {
|
|
155
|
+
product: 'prod_123',
|
|
156
|
+
amount: '19.00',
|
|
157
|
+
pricing: { type: 'fixed', amount: '19.00' }
|
|
158
|
+
},
|
|
159
|
+
field: 'items[0]',
|
|
160
|
+
message: /both amount and pricing/
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'an unsupported pricing type',
|
|
164
|
+
item: {
|
|
165
|
+
product: 'prod_123',
|
|
166
|
+
pricing: { type: 'metered' }
|
|
167
|
+
},
|
|
168
|
+
field: 'items[0].pricing.type',
|
|
169
|
+
message: /fixed, custom, or free/
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'fixed pricing without an amount',
|
|
173
|
+
item: {
|
|
174
|
+
product: 'prod_123',
|
|
175
|
+
pricing: { type: 'fixed' }
|
|
176
|
+
},
|
|
177
|
+
field: 'items[0].pricing.amount',
|
|
178
|
+
message: /required for fixed pricing/
|
|
179
|
+
},
|
|
180
|
+
...['presetAmount', 'minimumAmount', 'maximumAmount'].map((field) => ({
|
|
181
|
+
name: `fixed pricing with ${field}`,
|
|
182
|
+
item: {
|
|
183
|
+
product: 'prod_123',
|
|
184
|
+
pricing: {
|
|
185
|
+
type: 'fixed',
|
|
186
|
+
amount: '19.00',
|
|
187
|
+
[field]: '10.00'
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
field: `items[0].pricing.${field}`,
|
|
191
|
+
message: /not allowed with fixed pricing/
|
|
192
|
+
})),
|
|
193
|
+
{
|
|
194
|
+
name: 'custom pricing with a fixed amount',
|
|
195
|
+
item: {
|
|
196
|
+
product: 'prod_123',
|
|
197
|
+
pricing: {
|
|
198
|
+
type: 'custom',
|
|
199
|
+
amount: '19.00'
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
field: 'items[0].pricing.amount',
|
|
203
|
+
message: /not allowed with custom pricing/
|
|
204
|
+
},
|
|
205
|
+
...['amount', 'presetAmount', 'minimumAmount', 'maximumAmount'].map(
|
|
206
|
+
(field) => ({
|
|
207
|
+
name: `free pricing with ${field}`,
|
|
208
|
+
item: {
|
|
209
|
+
product: 'prod_123',
|
|
210
|
+
pricing: {
|
|
211
|
+
type: 'free',
|
|
212
|
+
[field]: '10.00'
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
field: `items[0].pricing.${field}`,
|
|
216
|
+
message: /not allowed with free pricing/
|
|
217
|
+
})
|
|
218
|
+
),
|
|
219
|
+
{
|
|
220
|
+
name: 'free pricing with chosenAmount',
|
|
221
|
+
item: {
|
|
222
|
+
product: 'prod_123',
|
|
223
|
+
pricing: { type: 'free' },
|
|
224
|
+
chosenAmount: '10.00'
|
|
225
|
+
},
|
|
226
|
+
field: 'items[0].chosenAmount',
|
|
227
|
+
message: /not allowed with free pricing/
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'fixed amount shorthand with chosenAmount',
|
|
231
|
+
item: {
|
|
232
|
+
product: 'prod_123',
|
|
233
|
+
amount: '19.00',
|
|
234
|
+
chosenAmount: '10.00'
|
|
235
|
+
},
|
|
236
|
+
field: 'items[0].chosenAmount',
|
|
237
|
+
message: /catalog custom pricing or ad-hoc custom pricing/
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'explicit fixed pricing with chosenAmount',
|
|
241
|
+
item: {
|
|
242
|
+
product: 'prod_123',
|
|
243
|
+
pricing: { type: 'fixed', amount: '19.00' },
|
|
244
|
+
chosenAmount: '10.00'
|
|
245
|
+
},
|
|
246
|
+
field: 'items[0].chosenAmount',
|
|
247
|
+
message: /catalog custom pricing or ad-hoc custom pricing/
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'a numeric amount shorthand',
|
|
251
|
+
item: {
|
|
252
|
+
product: 'prod_123',
|
|
253
|
+
amount: 1900
|
|
254
|
+
},
|
|
255
|
+
field: 'items[0].amount',
|
|
256
|
+
message: /decimal string/
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'a numeric chosenAmount',
|
|
260
|
+
item: {
|
|
261
|
+
product: 'prod_123',
|
|
262
|
+
chosenAmount: 1200
|
|
263
|
+
},
|
|
264
|
+
field: 'items[0].chosenAmount',
|
|
265
|
+
message: /decimal string/
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'a numeric fixed pricing amount',
|
|
269
|
+
item: {
|
|
270
|
+
product: 'prod_123',
|
|
271
|
+
pricing: { type: 'fixed', amount: 1900 }
|
|
272
|
+
},
|
|
273
|
+
field: 'items[0].pricing.amount',
|
|
274
|
+
message: /decimal string/
|
|
275
|
+
},
|
|
276
|
+
...['presetAmount', 'minimumAmount', 'maximumAmount'].map((field) => ({
|
|
277
|
+
name: `a numeric custom pricing ${field}`,
|
|
278
|
+
item: {
|
|
279
|
+
product: 'prod_123',
|
|
280
|
+
pricing: {
|
|
281
|
+
type: 'custom',
|
|
282
|
+
[field]: 1000
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
field: `items[0].pricing.${field}`,
|
|
286
|
+
message: /decimal string/
|
|
287
|
+
})),
|
|
288
|
+
...[
|
|
289
|
+
['zero', 0],
|
|
290
|
+
['a fraction', 1.5],
|
|
291
|
+
['a string', '2']
|
|
292
|
+
].map(([description, quantity]) => ({
|
|
293
|
+
name: `${description} quantity`,
|
|
294
|
+
item: {
|
|
295
|
+
product: 'prod_123',
|
|
296
|
+
quantity
|
|
297
|
+
},
|
|
298
|
+
field: 'items[0].quantity',
|
|
299
|
+
message: /integer of at least 1/
|
|
300
|
+
}))
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
for (const invalidCase of invalidItemCases) {
|
|
304
|
+
test(`checkout rejects ${invalidCase.name} before calling Bachs`, async () => {
|
|
305
|
+
let fetchCalls = 0
|
|
306
|
+
|
|
307
|
+
fetch.setFetchImplementation(async () => {
|
|
308
|
+
fetchCalls += 1
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
await assert.rejects(
|
|
312
|
+
() =>
|
|
313
|
+
checkout({
|
|
314
|
+
apiKey: 'sk_sandbox_123',
|
|
315
|
+
items: [invalidCase.item]
|
|
316
|
+
}),
|
|
317
|
+
(error) => {
|
|
318
|
+
assert.equal(error.exit, 'invalidRequest')
|
|
319
|
+
assert.equal(error.raw.field, invalidCase.field)
|
|
320
|
+
assert.match(error.raw.message, invalidCase.message)
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
assert.equal(fetchCalls, 0)
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
62
329
|
test('adapter exposes checkout.get as the uniform checkout lookup API', async () => {
|
|
63
330
|
const calls = []
|
|
64
331
|
|
|
@@ -95,6 +362,4 @@ test('adapter exposes checkout.get as the uniform checkout lookup API', async ()
|
|
|
95
362
|
'https://sandbox-api.bachs.io/v1/checkouts/chk_123'
|
|
96
363
|
)
|
|
97
364
|
assert.equal(calls[0].options.method, 'GET')
|
|
98
|
-
|
|
99
|
-
fetch.resetFetchImplementation()
|
|
100
365
|
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const { test, afterEach } = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const adapter = require('../adapter')
|
|
4
|
+
const fetch = require('../helpers/fetch')
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
adapter.config = {}
|
|
8
|
+
fetch.resetFetchImplementation()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('customer.portal creates a fresh sandbox portal session without a body', async () => {
|
|
12
|
+
const calls = []
|
|
13
|
+
|
|
14
|
+
fetch.setFetchImplementation(async (url, options) => {
|
|
15
|
+
calls.push({ url, options })
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
status: 201,
|
|
20
|
+
statusText: 'Created',
|
|
21
|
+
text: async () =>
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
id: `psn_${calls.length}`,
|
|
24
|
+
url: `https://portal.bachs.io/s/${calls.length}`
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const firstPortalUrl = await adapter.customer.portal({
|
|
30
|
+
apiKey: 'sk_sandbox_123',
|
|
31
|
+
customerId: 'cust_123'
|
|
32
|
+
})
|
|
33
|
+
const secondPortalUrl = await adapter.customer.portal({
|
|
34
|
+
apiKey: 'sk_sandbox_123',
|
|
35
|
+
customerId: 'cust_123'
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
assert.equal(typeof adapter.customer.portal, 'function')
|
|
39
|
+
assert.equal(firstPortalUrl, 'https://portal.bachs.io/s/1')
|
|
40
|
+
assert.equal(secondPortalUrl, 'https://portal.bachs.io/s/2')
|
|
41
|
+
assert.equal(calls.length, 2)
|
|
42
|
+
|
|
43
|
+
for (const call of calls) {
|
|
44
|
+
assert.equal(
|
|
45
|
+
call.url,
|
|
46
|
+
'https://sandbox-api.bachs.io/v1/customers/cust_123/portal-sessions'
|
|
47
|
+
)
|
|
48
|
+
assert.equal(call.options.method, 'POST')
|
|
49
|
+
assert.equal(call.options.headers.Authorization, 'Bearer sk_sandbox_123')
|
|
50
|
+
assert.equal(Object.hasOwn(call.options, 'body'), false)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('customer.portal falls back to adapter configuration', async () => {
|
|
55
|
+
const calls = []
|
|
56
|
+
adapter.config = {
|
|
57
|
+
apiKey: 'sk_live_configured',
|
|
58
|
+
baseUrl: 'https://configured.example'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fetch.setFetchImplementation(async (url, options) => {
|
|
62
|
+
calls.push({ url, options })
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
status: 201,
|
|
67
|
+
statusText: 'Created',
|
|
68
|
+
text: async () =>
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
id: 'psn_configured',
|
|
71
|
+
url: 'https://portal.bachs.io/s/configured'
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const portalUrl = await adapter.customer.portal({
|
|
77
|
+
customerId: 'cust_configured'
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
assert.equal(portalUrl, 'https://portal.bachs.io/s/configured')
|
|
81
|
+
assert.equal(
|
|
82
|
+
calls[0].url,
|
|
83
|
+
'https://configured.example/v1/customers/cust_configured/portal-sessions'
|
|
84
|
+
)
|
|
85
|
+
assert.equal(
|
|
86
|
+
calls[0].options.headers.Authorization,
|
|
87
|
+
'Bearer sk_live_configured'
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('customer.portal input credentials override adapter configuration', async () => {
|
|
92
|
+
const calls = []
|
|
93
|
+
adapter.config = {
|
|
94
|
+
apiKey: 'sk_live_configured',
|
|
95
|
+
baseUrl: 'https://configured.example'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fetch.setFetchImplementation(async (url, options) => {
|
|
99
|
+
calls.push({ url, options })
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
status: 201,
|
|
104
|
+
statusText: 'Created',
|
|
105
|
+
text: async () =>
|
|
106
|
+
JSON.stringify({
|
|
107
|
+
id: 'psn_override',
|
|
108
|
+
url: 'https://portal.bachs.io/s/override'
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await adapter.customer.portal({
|
|
114
|
+
apiKey: 'sk_sandbox_override',
|
|
115
|
+
baseUrl: 'https://override.example/api',
|
|
116
|
+
customerId: 'cust/override'
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
assert.equal(
|
|
120
|
+
calls[0].url,
|
|
121
|
+
'https://override.example/v1/customers/cust%2Foverride/portal-sessions'
|
|
122
|
+
)
|
|
123
|
+
assert.equal(
|
|
124
|
+
calls[0].options.headers.Authorization,
|
|
125
|
+
'Bearer sk_sandbox_override'
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('customer.portal rejects a malformed success response', async () => {
|
|
130
|
+
fetch.setFetchImplementation(async () => ({
|
|
131
|
+
ok: true,
|
|
132
|
+
status: 201,
|
|
133
|
+
statusText: 'Created',
|
|
134
|
+
text: async () => JSON.stringify({ id: 'psn_without_url' })
|
|
135
|
+
}))
|
|
136
|
+
|
|
137
|
+
await assert.rejects(
|
|
138
|
+
() =>
|
|
139
|
+
adapter.customer.portal({
|
|
140
|
+
apiKey: 'sk_sandbox_123',
|
|
141
|
+
customerId: 'cust_123'
|
|
142
|
+
}),
|
|
143
|
+
(error) => {
|
|
144
|
+
assert.equal(error.exit, 'couldNotCreatePortalUrl')
|
|
145
|
+
assert.deepEqual(error.raw, { id: 'psn_without_url' })
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
for (const statusCode of [401, 403, 404, 429, 500, 503]) {
|
|
152
|
+
test(`customer.portal normalizes Bachs ${statusCode} responses`, async () => {
|
|
153
|
+
fetch.setFetchImplementation(async () => ({
|
|
154
|
+
ok: false,
|
|
155
|
+
status: statusCode,
|
|
156
|
+
statusText: 'Request failed',
|
|
157
|
+
text: async () =>
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
detail: `Portal request failed with ${statusCode}`,
|
|
160
|
+
error_code: `STATUS_${statusCode}`
|
|
161
|
+
})
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
await assert.rejects(
|
|
165
|
+
() =>
|
|
166
|
+
adapter.customer.portal({
|
|
167
|
+
apiKey: 'sk_sandbox_123',
|
|
168
|
+
customerId: 'cust_123'
|
|
169
|
+
}),
|
|
170
|
+
(error) => {
|
|
171
|
+
assert.equal(error.exit, 'couldNotCreatePortalUrl')
|
|
172
|
+
assert.equal(error.raw.statusCode, statusCode)
|
|
173
|
+
assert.equal(error.raw.statusText, 'Request failed')
|
|
174
|
+
assert.equal(error.raw.code, `STATUS_${statusCode}`)
|
|
175
|
+
assert.equal(
|
|
176
|
+
error.raw.message,
|
|
177
|
+
`Portal request failed with ${statusCode}`
|
|
178
|
+
)
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
}
|
package/test/payloads.test.js
CHANGED
|
@@ -3,9 +3,147 @@ const assert = require('node:assert/strict')
|
|
|
3
3
|
const {
|
|
4
4
|
buildCheckoutSessionPayload,
|
|
5
5
|
buildPureCheckoutPayload,
|
|
6
|
-
buildRefundPayload
|
|
6
|
+
buildRefundPayload,
|
|
7
|
+
buildProductCart,
|
|
8
|
+
normalizePricing
|
|
7
9
|
} = require('../helpers/payloads')
|
|
8
10
|
|
|
11
|
+
test('buildProductCart preserves catalog pricing and optional quantity', () => {
|
|
12
|
+
assert.deepEqual(buildProductCart([{ product: 'prod_catalog' }]), [
|
|
13
|
+
{
|
|
14
|
+
product_id: 'prod_catalog'
|
|
15
|
+
}
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
assert.deepEqual(
|
|
19
|
+
buildProductCart([{ product: 'prod_quantity', quantity: 2 }]),
|
|
20
|
+
[
|
|
21
|
+
{
|
|
22
|
+
product_id: 'prod_quantity',
|
|
23
|
+
quantity: 2
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('buildProductCart normalizes fixed pricing shorthand and explicit pricing', () => {
|
|
30
|
+
const fixedPricing = {
|
|
31
|
+
product_id: 'prod_fixed',
|
|
32
|
+
pricing: {
|
|
33
|
+
price_type: 'fixed',
|
|
34
|
+
amount: '19.00'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
assert.deepEqual(
|
|
39
|
+
buildProductCart([{ product: 'prod_fixed', amount: '19.00' }]),
|
|
40
|
+
[fixedPricing]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert.deepEqual(
|
|
44
|
+
buildProductCart([
|
|
45
|
+
{
|
|
46
|
+
product: 'prod_fixed',
|
|
47
|
+
pricing: {
|
|
48
|
+
type: 'fixed',
|
|
49
|
+
amount: '19.00'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]),
|
|
53
|
+
[fixedPricing]
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('buildProductCart normalizes custom pricing and chosenAmount', () => {
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
buildProductCart([
|
|
60
|
+
{
|
|
61
|
+
product: 'prod_custom',
|
|
62
|
+
pricing: {
|
|
63
|
+
type: 'custom',
|
|
64
|
+
presetAmount: '10.00',
|
|
65
|
+
minimumAmount: '5.00',
|
|
66
|
+
maximumAmount: '100.00'
|
|
67
|
+
},
|
|
68
|
+
chosenAmount: '12.00'
|
|
69
|
+
}
|
|
70
|
+
]),
|
|
71
|
+
[
|
|
72
|
+
{
|
|
73
|
+
product_id: 'prod_custom',
|
|
74
|
+
pricing: {
|
|
75
|
+
price_type: 'custom',
|
|
76
|
+
preset_amount: '10.00',
|
|
77
|
+
minimum_amount: '5.00',
|
|
78
|
+
maximum_amount: '100.00'
|
|
79
|
+
},
|
|
80
|
+
amount: '12.00'
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('buildProductCart normalizes free pricing', () => {
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
buildProductCart([
|
|
89
|
+
{
|
|
90
|
+
product: 'prod_free',
|
|
91
|
+
pricing: {
|
|
92
|
+
type: 'free'
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
]),
|
|
96
|
+
[
|
|
97
|
+
{
|
|
98
|
+
product_id: 'prod_free',
|
|
99
|
+
pricing: {
|
|
100
|
+
price_type: 'free'
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('buildProductCart maps catalog chosenAmount to the flat Bachs amount', () => {
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
buildProductCart([
|
|
110
|
+
{
|
|
111
|
+
product: 'prod_catalog_custom',
|
|
112
|
+
chosenAmount: '12.00'
|
|
113
|
+
}
|
|
114
|
+
]),
|
|
115
|
+
[
|
|
116
|
+
{
|
|
117
|
+
product_id: 'prod_catalog_custom',
|
|
118
|
+
amount: '12.00'
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('normalizePricing maps reusable pricing fields to Bachs snake case', () => {
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
normalizePricing({
|
|
127
|
+
type: 'custom',
|
|
128
|
+
presetAmount: '10.00',
|
|
129
|
+
minimumAmount: '5.00',
|
|
130
|
+
maximumAmount: '100.00',
|
|
131
|
+
currencyOptions: {
|
|
132
|
+
NGN: '15000.00'
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
{
|
|
136
|
+
price_type: 'custom',
|
|
137
|
+
preset_amount: '10.00',
|
|
138
|
+
minimum_amount: '5.00',
|
|
139
|
+
maximum_amount: '100.00',
|
|
140
|
+
currency_options: {
|
|
141
|
+
NGN: '15000.00'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
9
147
|
test('buildCheckoutSessionPayload maps product checkout inputs to Bachs snake case', () => {
|
|
10
148
|
const payload = buildCheckoutSessionPayload(
|
|
11
149
|
{
|
|
@@ -37,7 +175,10 @@ test('buildCheckoutSessionPayload maps product checkout inputs to Bachs snake ca
|
|
|
37
175
|
{
|
|
38
176
|
product_id: 'prod_abc123',
|
|
39
177
|
quantity: 2,
|
|
40
|
-
|
|
178
|
+
pricing: {
|
|
179
|
+
price_type: 'fixed',
|
|
180
|
+
amount: '50.00'
|
|
181
|
+
}
|
|
41
182
|
}
|
|
42
183
|
],
|
|
43
184
|
billing_currency: 'NGN',
|
|
@@ -118,6 +259,34 @@ test('buildPureCheckoutPayload maps amount checkout inputs to Bachs snake case',
|
|
|
118
259
|
})
|
|
119
260
|
})
|
|
120
261
|
|
|
262
|
+
test('buildPureCheckoutPayload reuses advanced pricing normalization', () => {
|
|
263
|
+
const payload = buildPureCheckoutPayload({
|
|
264
|
+
pricing: {
|
|
265
|
+
type: 'custom',
|
|
266
|
+
currency: 'USD',
|
|
267
|
+
presetAmount: '10.00',
|
|
268
|
+
minimumAmount: '5.00',
|
|
269
|
+
maximumAmount: '100.00'
|
|
270
|
+
},
|
|
271
|
+
currencyOptions: {
|
|
272
|
+
NGN: '15000.00'
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
assert.deepEqual(payload, {
|
|
277
|
+
pricing: {
|
|
278
|
+
currency: 'USD',
|
|
279
|
+
price_type: 'custom',
|
|
280
|
+
preset_amount: '10.00',
|
|
281
|
+
minimum_amount: '5.00',
|
|
282
|
+
maximum_amount: '100.00',
|
|
283
|
+
currency_options: {
|
|
284
|
+
NGN: '15000.00'
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
121
290
|
test('buildRefundPayload maps refund inputs to Bachs snake case', () => {
|
|
122
291
|
const payload = buildRefundPayload({
|
|
123
292
|
chargeId: 'chr_123',
|