@sails-pay/bachs 0.0.5 → 0.0.7

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.
@@ -37,7 +37,8 @@ module.exports = {
37
37
  BACHS_SUCCESS_URL: {
38
38
  type: 'string',
39
39
  friendlyName: 'Success URL',
40
- description: 'Default URL Bachs redirects to after pure checkout payment.'
40
+ description:
41
+ 'Default URL Bachs redirects to after payment. Alias for returnUrl.'
41
42
  },
42
43
  BACHS_CANCEL_URL: {
43
44
  type: 'string',
@@ -65,6 +65,7 @@ function buildProductCart(items) {
65
65
  function buildCheckoutSessionPayload(inputs, adapterConfig = {}) {
66
66
  const productCollectionId =
67
67
  inputs.productCollectionId || inputs.productCollection
68
+ const productCart = buildProductCart(inputs.items)
68
69
  const checkoutData = inputs.checkoutData || {}
69
70
  const returnUrl =
70
71
  inputs.returnUrl ||
@@ -74,14 +75,20 @@ function buildCheckoutSessionPayload(inputs, adapterConfig = {}) {
74
75
 
75
76
  return withoutUndefined({
76
77
  customer: buildCustomerPayload(inputs),
77
- product_cart: buildProductCart(inputs.items),
78
+ product_cart: productCart,
78
79
  product_collection_id: productCollectionId,
80
+ // A session prices itself with products or with a raw amount, never both.
81
+ pricing:
82
+ productCart || productCollectionId
83
+ ? undefined
84
+ : buildPricingPayload(inputs),
79
85
  billing_currency: inputs.billingCurrency,
80
86
  allowed_payment_method_types: inputs.allowedPaymentMethodTypes,
81
87
  return_url: returnUrl,
82
88
  cancel_url: inputs.cancelUrl || adapterConfig.cancelUrl,
83
89
  reference: inputs.reference,
84
90
  metadata: inputs.metadata || checkoutData.custom,
91
+ expires_in_minutes: inputs.expiresInMinutes,
85
92
  transfer_data: inputs.connect && {
86
93
  destination: inputs.connect.destination
87
94
  },
@@ -129,32 +136,6 @@ function buildPricingPayload(inputs) {
129
136
  })
130
137
  }
131
138
 
132
- function buildPureCheckoutPayload(inputs, adapterConfig = {}) {
133
- const checkoutData = inputs.checkoutData || {}
134
- const customer = inputs.customer || {}
135
-
136
- return withoutUndefined({
137
- pricing: buildPricingPayload(inputs),
138
- customer_email:
139
- inputs.customerEmail ||
140
- inputs.email ||
141
- customer.email ||
142
- checkoutData.email,
143
- customer_name:
144
- inputs.customerName || inputs.name || customer.name || checkoutData.name,
145
- success_url:
146
- inputs.successUrl ||
147
- inputs.returnUrl ||
148
- adapterConfig.successUrl ||
149
- adapterConfig.returnUrl,
150
- cancel_url: inputs.cancelUrl || adapterConfig.cancelUrl,
151
- reference: inputs.reference,
152
- metadata: inputs.metadata || checkoutData.custom,
153
- expires_in_minutes: inputs.expiresInMinutes,
154
- simulated_outcome: inputs.simulatedOutcome
155
- })
156
- }
157
-
158
139
  function buildRefundPayload(inputs) {
159
140
  return withoutUndefined({
160
141
  charge_id: inputs.chargeId,
@@ -170,7 +151,6 @@ function buildRefundPayload(inputs) {
170
151
 
171
152
  module.exports = {
172
153
  buildCheckoutSessionPayload,
173
- buildPureCheckoutPayload,
174
154
  buildRefundPayload,
175
155
  buildProductCart,
176
156
  buildCustomerPayload,
@@ -20,7 +20,7 @@ function validateCheckoutConnect(connect, { isCheckoutSession }) {
20
20
  if (!isCheckoutSession) {
21
21
  return invalid(
22
22
  'connect',
23
- 'is only supported on product checkout sessions (items or productCollectionId).'
23
+ 'is only supported on checkout sessions (items, productCollectionId, or pricing).'
24
24
  )
25
25
  }
26
26
 
@@ -1,8 +1,5 @@
1
1
  const fetch = require('../helpers/fetch')
2
- const {
3
- buildCheckoutSessionPayload,
4
- buildPureCheckoutPayload
5
- } = require('../helpers/payloads')
2
+ const { buildCheckoutSessionPayload } = require('../helpers/payloads')
6
3
  const validateCheckoutItems = require('../helpers/validate-checkout-items')
7
4
  const validateCheckoutConnect = require('../helpers/validate-checkout-connect')
8
5
  const parameters = require('../helpers/parameters')
@@ -46,8 +43,7 @@ module.exports = require('machine').build({
46
43
  },
47
44
  successUrl: {
48
45
  type: 'string',
49
- description:
50
- 'URL Bachs redirects to after a pure checkout payment. Maps to success_url.'
46
+ description: 'Alias for returnUrl.'
51
47
  },
52
48
  cancelUrl: parameters.BACHS_CANCEL_URL,
53
49
  customer: {
@@ -96,23 +92,27 @@ module.exports = require('machine').build({
96
92
  },
97
93
  pricing: {
98
94
  type: 'ref',
99
- description: 'Pure Checkout pricing object.'
95
+ description:
96
+ 'Pricing for a checkout that sets its own price, instead of products.'
100
97
  },
101
98
  amount: {
102
99
  type: 'string',
103
- description: 'Pure Checkout amount, e.g. "50.00".'
100
+ description:
101
+ 'Amount for a checkout that sets its own price, e.g. "50.00".'
104
102
  },
105
103
  currency: {
106
104
  type: 'string',
107
- description: 'Pure Checkout currency, e.g. "USD" or "NGN".'
105
+ description:
106
+ 'Currency for a checkout that sets its own price, e.g. "USD" or "NGN".'
108
107
  },
109
108
  currencyOptions: {
110
109
  type: 'ref',
111
- description: 'Pure Checkout per-currency amount overrides.'
110
+ description:
111
+ 'Per-currency amount overrides for a checkout that sets its own price.'
112
112
  },
113
113
  expiresInMinutes: {
114
114
  type: 'number',
115
- description: 'Pure Checkout expiry in minutes.'
115
+ description: 'Checkout expiry in minutes.'
116
116
  },
117
117
  simulatedOutcome: {
118
118
  type: 'string',
@@ -147,9 +147,17 @@ module.exports = require('machine').build({
147
147
  const hasProductCollection = Boolean(
148
148
  inputs.productCollectionId || inputs.productCollection
149
149
  )
150
- const shouldUseCheckoutSession = hasItems || hasProductCollection
150
+ const hasPricing = Boolean(
151
+ inputs.pricing || inputs.amount !== undefined || inputs.currency
152
+ )
153
+ // Bachs checks out through sessions. A session takes products or a raw
154
+ // price, and it permits a split rather than requiring one, so a checkout
155
+ // that states its own amount and currency belongs here whether or not it
156
+ // pays a connected account.
157
+ const shouldUseCheckoutSession =
158
+ hasItems || hasProductCollection || hasPricing
151
159
 
152
- if (shouldUseCheckoutSession && hasItems === hasProductCollection) {
160
+ if (hasItems && hasProductCollection) {
153
161
  return exits.invalidRequest({
154
162
  message:
155
163
  'Provide exactly one of items or productCollectionId for a Bachs checkout session.'
@@ -174,20 +182,21 @@ module.exports = require('machine').build({
174
182
  }
175
183
  }
176
184
 
177
- const path = shouldUseCheckoutSession ? '/checkout-sessions' : '/checkouts'
178
- const payload = shouldUseCheckoutSession
179
- ? buildCheckoutSessionPayload(inputs, adapterConfig)
180
- : buildPureCheckoutPayload(inputs, adapterConfig)
185
+ const payload = buildCheckoutSessionPayload(inputs, adapterConfig)
181
186
 
182
- if (!shouldUseCheckoutSession && !payload.pricing) {
187
+ if (
188
+ !payload.product_cart &&
189
+ !payload.product_collection_id &&
190
+ !payload.pricing
191
+ ) {
183
192
  return exits.invalidRequest({
184
193
  message:
185
- 'Provide product items/productCollectionId or pure checkout pricing.'
194
+ 'Provide items, productCollectionId, or pricing for a Bachs checkout.'
186
195
  })
187
196
  }
188
197
 
189
198
  try {
190
- const checkout = await fetch(path, {
199
+ const checkout = await fetch('/checkout-sessions', {
191
200
  method: 'POST',
192
201
  apiKey: inputs.apiKey || adapterConfig.apiKey,
193
202
  baseUrl: inputs.baseUrl || adapterConfig.baseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sails-pay/bachs",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Bachs adapter for Sails Pay",
5
5
  "main": "adapter.js",
6
6
  "scripts": {
@@ -112,7 +112,7 @@ test('checkout sends custom pricing and the buyer-selected amount', async () =>
112
112
  })
113
113
  })
114
114
 
115
- test('checkout preserves pure checkout behavior', async () => {
115
+ test('checkout prices itself with amount and currency', async () => {
116
116
  const calls = []
117
117
 
118
118
  fetch.setFetchImplementation(async (url, options) => {
@@ -137,9 +137,13 @@ test('checkout preserves pure checkout behavior', async () => {
137
137
  })
138
138
 
139
139
  assert.equal(checkoutUrl, 'https://pay.bachs.io/c/pure')
140
- assert.equal(calls[0].url, 'https://sandbox-api.bachs.io/v1/checkouts')
140
+ assert.equal(
141
+ calls[0].url,
142
+ 'https://sandbox-api.bachs.io/v1/checkout-sessions'
143
+ )
141
144
  assert.equal(calls[0].options.headers['Idempotency-Key'], 'pure_123')
142
145
  assert.deepEqual(JSON.parse(calls[0].options.body), {
146
+ customer: {},
143
147
  pricing: {
144
148
  currency: 'USD',
145
149
  amount: '42.00'
@@ -405,6 +409,73 @@ test('checkout splits a checkout session with a connected account', async () =>
405
409
  assert.equal(body.connect, undefined)
406
410
  })
407
411
 
412
+ test('checkout splits a product-less checkout session', async () => {
413
+ const calls = []
414
+
415
+ fetch.setFetchImplementation(async (url, options) => {
416
+ calls.push({ url, options })
417
+
418
+ return {
419
+ ok: true,
420
+ status: 201,
421
+ statusText: 'Created',
422
+ text: async () =>
423
+ JSON.stringify({ checkout_url: 'https://pay.bachs.io/c/raw-split' })
424
+ }
425
+ })
426
+
427
+ const checkoutUrl = await checkout({
428
+ apiKey: 'sk_sandbox_123',
429
+ amount: '10500.00',
430
+ currency: 'NGN',
431
+ billingCurrency: 'NGN',
432
+ customer: { email: 'sponsor@example.com' },
433
+ reference: 'sponsorship_2',
434
+ connect: { destination: 'acct_123', platformFee: '500.00' }
435
+ })
436
+
437
+ assert.equal(checkoutUrl, 'https://pay.bachs.io/c/raw-split')
438
+ assert.equal(
439
+ calls[0].url,
440
+ 'https://sandbox-api.bachs.io/v1/checkout-sessions'
441
+ )
442
+
443
+ const body = JSON.parse(calls[0].options.body)
444
+ assert.deepEqual(body.pricing, { currency: 'NGN', amount: '10500.00' })
445
+ assert.deepEqual(body.transfer_data, { destination: 'acct_123' })
446
+ assert.equal(body.platform_fee, '500.00')
447
+ assert.equal(body.billing_currency, 'NGN')
448
+ assert.equal('product_cart' in body, false)
449
+ })
450
+
451
+ test('a self-priced checkout without connect still uses a checkout session', async () => {
452
+ const calls = []
453
+
454
+ fetch.setFetchImplementation(async (url, options) => {
455
+ calls.push({ url, options })
456
+
457
+ return {
458
+ ok: true,
459
+ status: 201,
460
+ statusText: 'Created',
461
+ text: async () =>
462
+ JSON.stringify({ checkout_url: 'https://pay.bachs.io/c/raw' })
463
+ }
464
+ })
465
+
466
+ await checkout({
467
+ apiKey: 'sk_sandbox_123',
468
+ amount: '10500.00',
469
+ currency: 'NGN',
470
+ customer: { email: 'sponsor@example.com' }
471
+ })
472
+
473
+ assert.equal(
474
+ calls[0].url,
475
+ 'https://sandbox-api.bachs.io/v1/checkout-sessions'
476
+ )
477
+ })
478
+
408
479
  test('checkout without connect sends no split fields', async () => {
409
480
  const calls = []
410
481
 
@@ -437,16 +508,6 @@ const invalidConnectCases = [
437
508
  field: 'connect',
438
509
  message: /must be an object/
439
510
  },
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
511
  {
451
512
  name: 'connect without a destination',
452
513
  inputs: {
@@ -2,7 +2,6 @@ const test = require('node:test')
2
2
  const assert = require('node:assert/strict')
3
3
  const {
4
4
  buildCheckoutSessionPayload,
5
- buildPureCheckoutPayload,
6
5
  buildRefundPayload,
7
6
  buildProductCart,
8
7
  normalizePricing
@@ -216,77 +215,6 @@ test('buildCheckoutSessionPayload maps productCollectionId and configured return
216
215
  })
217
216
  })
218
217
 
219
- test('buildPureCheckoutPayload maps amount checkout inputs to Bachs snake case', () => {
220
- const payload = buildPureCheckoutPayload(
221
- {
222
- amount: '50.00',
223
- currency: 'USD',
224
- currencyOptions: {
225
- NGN: '75000.00'
226
- },
227
- email: 'customer@example.com',
228
- name: 'Jane Doe',
229
- successUrl: 'https://example.com/success',
230
- cancelUrl: 'https://example.com/cancel',
231
- reference: 'order_9876',
232
- metadata: {
233
- orderId: '9876'
234
- },
235
- expiresInMinutes: 30,
236
- simulatedOutcome: 'success'
237
- },
238
- {}
239
- )
240
-
241
- assert.deepEqual(payload, {
242
- pricing: {
243
- currency: 'USD',
244
- amount: '50.00',
245
- currency_options: {
246
- NGN: '75000.00'
247
- }
248
- },
249
- customer_email: 'customer@example.com',
250
- customer_name: 'Jane Doe',
251
- success_url: 'https://example.com/success',
252
- cancel_url: 'https://example.com/cancel',
253
- reference: 'order_9876',
254
- metadata: {
255
- orderId: '9876'
256
- },
257
- expires_in_minutes: 30,
258
- simulated_outcome: 'success'
259
- })
260
- })
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
-
290
218
  test('buildRefundPayload maps refund inputs to Bachs snake case', () => {
291
219
  const payload = buildRefundPayload({
292
220
  chargeId: 'chr_123',