diff --git a/src/api/hosted-payments/hosted-payments.js b/src/api/hosted-payments/hosted-payments.js index 57c05f67..a43d2d9c 100644 --- a/src/api/hosted-payments/hosted-payments.js +++ b/src/api/hosted-payments/hosted-payments.js @@ -17,6 +17,12 @@ export default class HostedPayments { * * Notable optional fields (swagger HostedPaymentsRequest, 2026-06-08): * - body.authorization_type — e.g. `Estimated`, `Final`. + * - body.3ds.challenge_indicator — four values only (default + * `no_preference`): `no_preference`, `no_challenge_requested`, + * `challenge_requested`, `challenge_requested_mandate`. The exemption + * values (`low_value`, `trusted_listing`, `trusted_listing_prompt`, + * `transaction_risk_assessment`, `data_share`) are accepted only by + * `cko.sessions.request` and are rejected here. * - body.payment_plan — installment / recurring schedule * (`amount`, `name`, `start_date` added 2026-05-08). * diff --git a/src/api/payment-sessions/payment-sessions.js b/src/api/payment-sessions/payment-sessions.js index 63ba6453..94520ad1 100644 --- a/src/api/payment-sessions/payment-sessions.js +++ b/src/api/payment-sessions/payment-sessions.js @@ -18,6 +18,12 @@ export default class PaymentSessions { * * Notable optional fields (swagger CreatePaymentSessionsBaseRequest, 2026-06-08): * - body.authorization_type — e.g. `Estimated`, `Final`. + * - body.3ds.challenge_indicator — four values only (default + * `no_preference`): `no_preference`, `no_challenge_requested`, + * `challenge_requested`, `challenge_requested_mandate`. The exemption + * values (`low_value`, `trusted_listing`, `trusted_listing_prompt`, + * `transaction_risk_assessment`, `data_share`) are accepted only by + * `cko.sessions.request` and are rejected here. * - body.payment_plan — installment / recurring schedule. See swagger * `PaymentSessionPaymentPlanRecurring` for the recurring variant * (fields: amount, name, start_date — added 2026-05-08). @@ -46,6 +52,14 @@ export default class PaymentSessions { /** * Submit a payment attempt for a payment session. * + * Notable optional fields (swagger SubmitPaymentSessionsRequest, 2026-06-08): + * - body.3ds.challenge_indicator — four values only (default + * `no_preference`): `no_preference`, `no_challenge_requested`, + * `challenge_requested`, `challenge_requested_mandate`. The exemption + * values (`low_value`, `trusted_listing`, `trusted_listing_prompt`, + * `transaction_risk_assessment`, `data_share`) are accepted only by + * `cko.sessions.request` and are rejected here. + * * @memberof PaymentSessions * @param {string} id The payment session ID. * @param {object} body PaymentSessions Request body. @@ -70,6 +84,14 @@ export default class PaymentSessions { * Request a Payment Session with Payment. * Create a payment session and submit a payment attempt for it. * + * Notable optional fields (swagger CreateAndSubmitPaymentSessionsRequest, 2026-06-08): + * - body.3ds.challenge_indicator — four values only (default + * `no_preference`): `no_preference`, `no_challenge_requested`, + * `challenge_requested`, `challenge_requested_mandate`. The exemption + * values (`low_value`, `trusted_listing`, `trusted_listing_prompt`, + * `transaction_risk_assessment`, `data_share`) are accepted only by + * `cko.sessions.request` and are rejected here. + * * @memberof PaymentSessions * @param {object} body PaymentSessions Request body. * @return {Promise} A promise to payment response (201 processed or 202 action required). diff --git a/src/api/payments-links/payments-links.js b/src/api/payments-links/payments-links.js index 0fa62efd..74556d8f 100644 --- a/src/api/payments-links/payments-links.js +++ b/src/api/payments-links/payments-links.js @@ -18,6 +18,12 @@ export default class PaymentLinks { * * Notable optional fields (swagger PaymentLinksRequest, 2026-06-08): * - body.authorization_type — e.g. `Estimated`, `Final`. + * - body.3ds.challenge_indicator — four values only (default + * `no_preference`): `no_preference`, `no_challenge_requested`, + * `challenge_requested`, `challenge_requested_mandate`. The exemption + * values (`low_value`, `trusted_listing`, `trusted_listing_prompt`, + * `transaction_risk_assessment`, `data_share`) are accepted only by + * `cko.sessions.request` and are rejected here. * - body.payment_plan — installment / recurring schedule * (`amount`, `name`, `start_date` added 2026-05-08). * diff --git a/src/api/payments/payments.js b/src/api/payments/payments.js index c37d9d0e..abdcaf36 100644 --- a/src/api/payments/payments.js +++ b/src/api/payments/payments.js @@ -50,6 +50,12 @@ export default class Payments { * tracking, surfaced under `processing` (2026-05-07). * - body.payment_plan, body.authorization_type — present on * HostedPayments/PaymentLinks/PaymentSessions variants (2026-06-08). + * - body.3ds.challenge_indicator — four values only (default + * `no_preference`): `no_preference`, `no_challenge_requested`, + * `challenge_requested`, `challenge_requested_mandate`. The exemption + * values (`low_value`, `trusted_listing`, `trusted_listing_prompt`, + * `transaction_risk_assessment`, `data_share`) are accepted only by + * `cko.sessions.request` and are rejected here. * * Response fields newly available under `processing` (pass-through): * - scheme_transaction_link_id (Mastercard Transaction Link Identifier, 2026-06-08) diff --git a/src/api/sessions/sessions.js b/src/api/sessions/sessions.js index dcf17008..a01e9743 100644 --- a/src/api/sessions/sessions.js +++ b/src/api/sessions/sessions.js @@ -18,6 +18,16 @@ export default class Sessions { * Notable optional fields (swagger SessionRequest, 2026-05-07): * - body.device_information — device-fingerprint block. See swagger * `DeviceInformation` for shape (browser, ip, screen, timezone, etc.). + * - body.challenge_indicator — this endpoint is the only one that accepts the + * exemption values. Nine values (swagger `ChallengeIndicator`, default + * `no_preference`, max 50 characters): `no_preference`, + * `no_challenge_requested`, `challenge_requested`, + * `challenge_requested_mandate`, plus the exemption requests `low_value`, + * `trusted_listing`, `trusted_listing_prompt`, + * `transaction_risk_assessment`, and `data_share`. If an exemption cannot + * be applied, `no_challenge_requested` is used instead. The + * `3ds.challenge_indicator` field on payments, hosted payments, payment + * links and payment sessions accepts only the first four. * * @memberof Sessions * @param {Object} body Sessions request body. diff --git a/test/sessions/challenge-indicator.js b/test/sessions/challenge-indicator.js new file mode 100644 index 00000000..382d41f4 --- /dev/null +++ b/test/sessions/challenge-indicator.js @@ -0,0 +1,142 @@ +import { Checkout } from '../../src/index.js'; +import { expect } from 'chai'; +import nock from 'nock'; + +const ACCESS_HOST = 'https://123456789.access.sandbox.checkout.com'; +const API_HOST = 'https://123456789.api.sandbox.checkout.com'; + +const SECRET = + '2p7YQ37fHiRr8O6lQAikl8enICesB1dvAJrpmE2nZfEOpxzE-J_Gho7wDy0HY9951RfdUr0vSaQCzRKP0-o5Xg'; + +// The nine values accepted by POST /sessions, per the API Reference ChallengeIndicator schema. +const SESSION_VALUES = [ + 'no_preference', + 'no_challenge_requested', + 'challenge_requested', + 'challenge_requested_mandate', + 'low_value', + 'trusted_listing', + 'trusted_listing_prompt', + 'transaction_risk_assessment', + 'data_share', +]; + +// The four values accepted by the 3ds.challenge_indicator field on the payments family. +const PAYMENT_VALUES = [ + 'no_preference', + 'no_challenge_requested', + 'challenge_requested', + 'challenge_requested_mandate', +]; + +const client = () => + new Checkout(SECRET, { + client: 'ack_vvzhoai466su3j3vbxb47ts5oe', + scope: ['sessions:browser'], + environment: 'sandbox', + subdomain: '123456789', + }); + +const mockToken = () => + nock(ACCESS_HOST).post('/connect/token').reply(201, { + access_token: '1234', + expires_in: 3600, + token_type: 'Bearer', + scope: 'sessions:browser', + }); + +const sessionBody = (challengeIndicator) => ({ + source: { + type: 'card', + number: '4485040371536584', + expiry_month: 1, + expiry_year: 2030, + }, + amount: 100, + currency: 'USD', + authentication_type: 'regular', + authentication_category: 'payment', + challenge_indicator: challengeIndicator, + reference: 'ORD-5023-4E89', + completion: { + type: 'non_hosted', + callback_url: 'https://example.com/sessions/callback', + }, +}); + +/** + * The SDK is untyped and forwards the request body verbatim, so nothing constrains + * challenge_indicator at the client. These tests capture the body that actually reaches the wire, + * which is what makes them meaningful: they fail if a future change starts validating, rewriting or + * dropping the field — in particular the five exemption values, which only POST /sessions accepts. + */ +describe('Sessions challenge_indicator', () => { + afterEach(() => nock.cleanAll()); + + SESSION_VALUES.forEach((value) => { + it(`should send challenge_indicator "${value}" to /sessions unchanged`, async () => { + mockToken(); + + let sent; + nock(API_HOST) + .post('/sessions', (body) => { + sent = body; + return true; + }) + .reply(201, { id: 'sid_jlfm4ithpgpefdxgzzdnc3xrc4', status: 'pending' }); + + await client().sessions.request(sessionBody(value)); + + expect(sent.challenge_indicator).to.equal(value); + }); + }); + + it('should not strip or rewrite the five exemption values', async () => { + const exemptions = SESSION_VALUES.filter((value) => !PAYMENT_VALUES.includes(value)); + + expect(exemptions).to.have.lengthOf(5); + + for (const value of exemptions) { + mockToken(); + + let sent; + nock(API_HOST) + .post('/sessions', (body) => { + sent = body; + return true; + }) + .reply(201, { id: 'sid_jlfm4ithpgpefdxgzzdnc3xrc4', status: 'pending' }); + + await client().sessions.request(sessionBody(value)); + + expect(sent).to.have.property('challenge_indicator', value); + nock.cleanAll(); + } + }); + + PAYMENT_VALUES.forEach((value) => { + it(`should send 3ds.challenge_indicator "${value}" to /payment-sessions unchanged`, async () => { + mockToken(); + + let sent; + nock(API_HOST) + .post('/payment-sessions', (body) => { + sent = body; + return true; + }) + .reply(201, { id: 'ps_2adCU4m0Q1WMlM6Pqu3ilRvUUwr' }); + + await client().paymentSessions.request({ + amount: 100, + currency: 'USD', + reference: 'ORD-123A', + billing: { address: { country: 'GB' } }, + success_url: 'https://example.com/success', + failure_url: 'https://example.com/failure', + '3ds': { enabled: true, challenge_indicator: value }, + }); + + expect(sent['3ds'].challenge_indicator).to.equal(value); + }); + }); +});