From f084c64431a70647e3c12d8e60a11c69944b5d4f Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 28 Sep 2026 17:17:49 +0200 Subject: [PATCH 1/2] Airline and accommodation sub-tree model alignment --- src/api/hosted-payments/hosted-payments.js | 168 ++-- src/api/payment-contexts/payment-contexts.js | 35 + src/api/payment-sessions/payment-sessions.js | 24 + src/api/payment-setups/payment-setups.js | 9 +- src/api/payments-links/payments-links.js | 170 ++-- src/api/payments/payments.js | 822 ++++++++++-------- test/payment-setups/payment-setups-unit.js | 845 ++++++++++--------- test/payments/getPaymentAirlineData.js | 263 ++++++ 8 files changed, 1393 insertions(+), 943 deletions(-) create mode 100644 test/payments/getPaymentAirlineData.js diff --git a/src/api/hosted-payments/hosted-payments.js b/src/api/hosted-payments/hosted-payments.js index 148809f..ba19052 100644 --- a/src/api/hosted-payments/hosted-payments.js +++ b/src/api/hosted-payments/hosted-payments.js @@ -1,75 +1,93 @@ -import { determineError } from '../../services/errors.js'; -import { get, post } from '../../services/http.js'; - -// Path segments appended to the API base (config.host). -const HOSTED_PAYMENTS_PATH = 'hosted-payments'; - -/** - * Class dealing with the /hosted-payments endpoint - * - * @export - * @class HostedPayments - */ -export default class HostedPayments { - constructor(config) { - this.config = config; - } - - /** - * Create a Hosted Payments Page session. - * - * 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). - * - * @memberof HostedPayments - * @param {Object} body - Hosted Payments Page session request body - * @return {Promise} A promise to the Hosted Payment response. - */ - async create(body) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${HOSTED_PAYMENTS_PATH}`, - this.config, - this.config.sk, - body - ); - return await response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Get Hosted Payments Page details - * - * The response (swagger GetHostedPaymentsResponse) includes a `_links` - * object with `self` and `redirect` links, plus `payment` and - * `payment_actions` once a payment is in progress or completed. - * - * @memberof HostedPayments - * @param {string} id - Hosted payment id - * @return {Promise} A promise to the Hosted Payment response. - */ - async get(id) { - try { - const response = await get( - this.config.httpClient, - `${this.config.host}/${HOSTED_PAYMENTS_PATH}/${id}`, - this.config, - this.config.sk - ); - return await response.json; - } catch (err) { - throw await determineError(err); - } - } -} +import { determineError } from '../../services/errors.js'; +import { get, post } from '../../services/http.js'; + +// Path segments appended to the API base (config.host). +const HOSTED_PAYMENTS_PATH = 'hosted-payments'; + +/** + * Class dealing with the /hosted-payments endpoint + * + * @export + * @class HostedPayments + */ +export default class HostedPayments { + constructor(config) { + this.config = config; + } + + /** + * Create a Hosted Payments Page session. + * + * 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). + * - body.processing.airline_data: optional array of + * PaymentInterfacesProcessingAirlineData: `ticket`, `passenger` and + * `flight_leg_details`. See the `cko.payments.request` JSDoc for the full nested shape. + * - body.processing.accommodation_data: optional array of + * PaymentInterfacesProcessingAccommodationData: the same eleven fields as + * `cko.payments.request` **minus** `property_phone` and `customer_service_phone`. Those + * two are declared on `AccommodationData` only, so they are read by `POST /payments` and + * payment contexts and ignored here. + * + * **Send `passenger` as a single object here, never an array.** Verified against the + * sandbox on 2026-09-28: an array is rejected with 422 + * `processing_airline_data_0_passenger_invalid` on this endpoint, while a single + * `{ first_name, last_name, date_of_birth [date], address: { country } }` object is + * accepted. This inverts the specification, which declares the property array-only on + * `AirlineData` and `oneOf[array, object]` here. Only `POST /payments` and + * `POST /payment-sessions` accept the array form, so several passengers cannot be + * expressed on this endpoint at all. Omit the key entirely when there are no passengers: + * an empty array and an explicit `null` are both rejected too. + * + * @memberof HostedPayments + * @param {Object} body - Hosted Payments Page session request body + * @return {Promise} A promise to the Hosted Payment response. + */ + async create(body) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${HOSTED_PAYMENTS_PATH}`, + this.config, + this.config.sk, + body + ); + return await response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Get Hosted Payments Page details + * + * The response (swagger GetHostedPaymentsResponse) includes a `_links` + * object with `self` and `redirect` links, plus `payment` and + * `payment_actions` once a payment is in progress or completed. + * + * @memberof HostedPayments + * @param {string} id - Hosted payment id + * @return {Promise} A promise to the Hosted Payment response. + */ + async get(id) { + try { + const response = await get( + this.config.httpClient, + `${this.config.host}/${HOSTED_PAYMENTS_PATH}/${id}`, + this.config, + this.config.sk + ); + return await response.json; + } catch (err) { + throw await determineError(err); + } + } +} diff --git a/src/api/payment-contexts/payment-contexts.js b/src/api/payment-contexts/payment-contexts.js index b13c469..bae5dc1 100644 --- a/src/api/payment-contexts/payment-contexts.js +++ b/src/api/payment-contexts/payment-contexts.js @@ -19,6 +19,29 @@ export default class PaymentContexts { /** * Request a Payment Context. * + * Notable optional fields (swagger PaymentContext / PaymentContextProcessing): + * - body.processing.airline_data: optional array of AirlineData: `ticket`, `passenger` and + * `flight_leg_details`. See the `cko.payments.request` JSDoc for the full nested shape. + * - body.processing.accommodation_data: optional array of AccommodationData, the full + * thirteen-field shape **including** `property_phone` and `customer_service_phone`. + * Payment contexts resolves to the same `AccommodationData` schema as `POST /payments`, + * unlike hosted payments, payment links and payment sessions, which resolve to the + * narrower `PaymentInterfacesProcessingAccommodationData` without the two phone arrays. + * + * **Send `passenger` as a single object here, never an array.** Verified against the + * sandbox on 2026-09-28: an array is rejected with 422 `passenger_required`, a different + * error code from the `processing_airline_data_0_passenger_invalid` that hosted payments + * and payment links return, while a single + * `{ first_name, last_name, date_of_birth [date], address: { country } }` object is + * accepted. The specification declares this property array-only, so it is exactly + * inverted here. Several passengers cannot be expressed on this endpoint. + * - body.processing.airline_data[].flight_leg_details[].stop_over_code: **do not send + * this on payment contexts.** The endpoint rejects every value for it with 422 + * `flight_leg_detail_stop_over_code_invalid`, including the specification's own example + * `"x"`, while accepting the identical flight leg with the key omitted. Confirmed by + * sending the same request twice on 2026-09-28, once with the key and once without: the + * code appears only in the first. The other eight flight-leg fields are fine. + * * @memberof PaymentContexts * @param {object} body PaymentContexts Request body. * @param {string} [idempotencyKey] Idempotency Key. @@ -48,6 +71,18 @@ export default class PaymentContexts { * Response now carries an `id` field on `PaymentContextDetails` (swagger 2026-05-26) * — the payment-context identifier echoed back in the response. * + * Response fields available under `payment_request.processing` (pass-through, swagger + * `PaymentContextDetails`): + * - airline_data: array of AirlineData, each entry with `ticket`, `passenger` and + * `flight_leg_details`. See the `cko.payments.request` JSDoc for the full nested shape. + * - accommodation_data: array of AccommodationData, the full thirteen-field shape including + * `property_phone` and `customer_service_phone`. + * + * **Read `passenger` defensively: it may be a single object or an array.** The + * specification declares it array-only, but a single passenger is sent and echoed back as + * a bare object. This client returns `response.json` untouched, so whichever shape the API + * returns is the shape the caller receives. + * * @memberof PaymentContexts * @param {string} id /^(pay|sid)_(\w{26})$/ The payment or payment session identifier. * @return {Promise} A promise to the get payment context response. diff --git a/src/api/payment-sessions/payment-sessions.js b/src/api/payment-sessions/payment-sessions.js index a41e129..f6eedc0 100644 --- a/src/api/payment-sessions/payment-sessions.js +++ b/src/api/payment-sessions/payment-sessions.js @@ -32,6 +32,23 @@ export default class PaymentSessions { * - body.payment_plan — installment / recurring schedule. See swagger * `PaymentSessionPaymentPlanRecurring` for the recurring variant * (fields: amount, name, start_date — added 2026-05-08). + * - body.processing.airline_data: optional array of + * PaymentInterfacesProcessingAirlineData: `ticket`, `passenger` and + * `flight_leg_details`. See the `cko.payments.request` JSDoc for the full nested shape. + * - body.processing.accommodation_data: optional array of + * PaymentInterfacesProcessingAccommodationData: the same eleven fields as + * `cko.payments.request` **minus** `property_phone` and `customer_service_phone`. Those + * two are declared on `AccommodationData` only, so they are read by `POST /payments` and + * payment contexts and ignored here. + * + * **`passenger` accepts either a single object or an array on this endpoint.** Verified + * against the sandbox on 2026-09-28: both forms return 201. That makes payment sessions + * the exception among the payment-interfaces endpoints. Hosted payments and payment links + * resolve to the *same* `PaymentInterfacesProcessing` schema yet reject the array with + * 422 `processing_airline_data_0_passenger_invalid`, so the shared schema is not a + * reliable guide to which form a surface takes and each has to be tested. A single object + * is still the safer default, being the one form every surface accepts. Omit the key + * entirely when there are no passengers. * * @memberof PaymentSessions * @param {object} body PaymentSessions Request body. @@ -101,6 +118,13 @@ export default class PaymentSessions { * 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.processing.airline_data and body.processing.accommodation_data: accepted here too. + * The request schema `CreateAndSubmitPaymentSessionsRequest` composes the same + * `CreatePaymentSessionsBaseRequest` that `cko.paymentSessions.request` uses, so both + * fields and the `PaymentInterfacesProcessing` shape apply unchanged. See the `request` + * JSDoc above for the fields and the `passenger` cardinality, and + * `cko.payments.request` for the full nested shape. As on `request`, both a single + * `passenger` object and an array are accepted on this endpoint. * * @memberof PaymentSessions * @param {object} body PaymentSessions Request body. diff --git a/src/api/payment-setups/payment-setups.js b/src/api/payment-setups/payment-setups.js index 277af6b..60f4bde 100644 --- a/src/api/payment-setups/payment-setups.js +++ b/src/api/payment-setups/payment-setups.js @@ -45,7 +45,14 @@ export default class PaymentSetups { * PaymentSetupAirline (all fields optional): ticket (object: number, issue_date [date], * issuing_carrier_code, travel_package_indicator [free-form string], travel_agency_name, travel_agency_code), * passengers (array of { first_name, last_name, date_of_birth [date], address: { country [ISO 3166-1 alpha-2] } }), - * flight_leg_details (array of PaymentSetupFlightLegDetails), + * flight_leg_details (array of PaymentSetupFlightLegDetails: flight_number [string, + * e.g. "BA1483", not a number], carrier_code [IATA 2-letter accounting code], + * class_of_travelling [one-letter travel class, e.g. "W"], departure_airport [IATA + * 3-letter], departure_date [date format], departure_time [e.g. "18:30"], + * arrival_airport [IATA 3-letter], stop_over_code [one letter, e.g. "X"], + * fare_basis_code [e.g. "WUP14B"]. Note class_of_travelling with two l's and + * stop_over_code as three words: six SDKs previously shipped service_class, + * class_of_traveling or stopover_code here and the values never reached the API), * total_number_of_passengers (integer, added 2026-09-08), travel_type (string, added 2026-09-08; free-form, * not a typed enum), trip_type (string, added 2026-09-08; free-form, not a typed enum), * refundable (boolean, added 2026-09-08), delivery_recipient (string, added 2026-09-08; plain string, diff --git a/src/api/payments-links/payments-links.js b/src/api/payments-links/payments-links.js index f111f6d..cbc9ff1 100644 --- a/src/api/payments-links/payments-links.js +++ b/src/api/payments-links/payments-links.js @@ -1,76 +1,94 @@ -import { determineError } from '../../services/errors.js'; -import { get, post } from '../../services/http.js'; - -// Path segments appended to the API base (config.host). -const PAYMENT_LINKS_PATH = 'payment-links'; - -/** - * Class dealing with the /payment-links endpoint - * - * @export - * @class PaymentLinks - */ -export default class PaymentLinks { - constructor(config) { - this.config = config; - } - - /** - * Create a Payment Link and pass through all the payment information, - * like the amount, currency, country and reference. - * - * 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). - * - * @memberof PaymentLinks - * @param {Object} body Payment Link request body. - * @return {Promise} A promise to the Payment Link response. - */ - async create(body) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENT_LINKS_PATH}`, - this.config, - this.config.sk, - body - ); - return await response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Retrieve details about a specific Payment Link using its ID returned when the link was created. In the response, you will see the status of the Payment Link. - * - * The response (swagger GetPaymentLinkResponse) includes a `_links` - * object with `self` and `redirect` links, plus `payment` and - * `payment_actions` once a payment is in progress or completed. - * - * @memberof PaymentLinks - * @param {string} id - * @return {Promise} A promise to the Payment Link response. - */ - async get(id) { - try { - const response = await get( - this.config.httpClient, - `${this.config.host}/${PAYMENT_LINKS_PATH}/${id}`, - this.config, - this.config.sk - ); - return await response.json; - } catch (err) { - throw await determineError(err); - } - } -} +import { determineError } from '../../services/errors.js'; +import { get, post } from '../../services/http.js'; + +// Path segments appended to the API base (config.host). +const PAYMENT_LINKS_PATH = 'payment-links'; + +/** + * Class dealing with the /payment-links endpoint + * + * @export + * @class PaymentLinks + */ +export default class PaymentLinks { + constructor(config) { + this.config = config; + } + + /** + * Create a Payment Link and pass through all the payment information, + * like the amount, currency, country and reference. + * + * 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). + * - body.processing.airline_data: optional array of + * PaymentInterfacesProcessingAirlineData: `ticket`, `passenger` and + * `flight_leg_details`. See the `cko.payments.request` JSDoc for the full nested shape. + * - body.processing.accommodation_data: optional array of + * PaymentInterfacesProcessingAccommodationData: the same eleven fields as + * `cko.payments.request` **minus** `property_phone` and `customer_service_phone`. Those + * two are declared on `AccommodationData` only, so they are read by `POST /payments` and + * payment contexts and ignored here. + * + * **Send `passenger` as a single object here, never an array.** Verified against the + * sandbox on 2026-09-28: an array is rejected with 422 + * `processing_airline_data_0_passenger_invalid` on this endpoint, while a single + * `{ first_name, last_name, date_of_birth [date], address: { country } }` object is + * accepted. This inverts the specification, which declares the property array-only on + * `AirlineData` and `oneOf[array, object]` here. Only `POST /payments` and + * `POST /payment-sessions` accept the array form, so several passengers cannot be + * expressed on this endpoint at all. Omit the key entirely when there are no passengers: + * an empty array and an explicit `null` are both rejected too. + * + * @memberof PaymentLinks + * @param {Object} body Payment Link request body. + * @return {Promise} A promise to the Payment Link response. + */ + async create(body) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENT_LINKS_PATH}`, + this.config, + this.config.sk, + body + ); + return await response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Retrieve details about a specific Payment Link using its ID returned when the link was created. In the response, you will see the status of the Payment Link. + * + * The response (swagger GetPaymentLinkResponse) includes a `_links` + * object with `self` and `redirect` links, plus `payment` and + * `payment_actions` once a payment is in progress or completed. + * + * @memberof PaymentLinks + * @param {string} id + * @return {Promise} A promise to the Payment Link response. + */ + async get(id) { + try { + const response = await get( + this.config.httpClient, + `${this.config.host}/${PAYMENT_LINKS_PATH}/${id}`, + this.config, + this.config.sk + ); + return await response.json; + } catch (err) { + throw await determineError(err); + } + } +} diff --git a/src/api/payments/payments.js b/src/api/payments/payments.js index 06bc5d2..ceb19c8 100644 --- a/src/api/payments/payments.js +++ b/src/api/payments/payments.js @@ -1,375 +1,447 @@ - -import { determineError } from '../../services/errors.js'; -import { get, post } from '../../services/http.js'; -import { setSourceOrDestinationType, validatePayment } from '../../services/validation.js'; - -// Path segments appended to the API base (config.host). -const ACTIONS_PATH = 'actions'; -const AUTHORIZATIONS_PATH = 'authorizations'; -const CANCELLATIONS_PATH = 'cancellations'; -const CAPTURES_PATH = 'captures'; -const PAYMENTS_PATH = 'payments'; -const REFUNDS_PATH = 'refunds'; -const REVERSALS_PATH = 'reversals'; -const SEARCH_PATH = 'search'; -const VOIDS_PATH = 'voids'; - -const addUtilityParams = (json) => { - if (!json || typeof json !== 'object') return json; - let requiresRedirect = false; - - if (json.destination) { - requiresRedirect = false; - } else { - const isPending = json.status === 'Pending'; - const hasRedirectUrl = json._links && json._links.redirect !== undefined; - requiresRedirect = isPending && hasRedirectUrl; - } - - // If the redirection URL exists add it to the response body as 'redirectLink' - let redirectLink; - if (requiresRedirect && json._links && json._links.redirect) { - redirectLink = json._links.redirect.href; - } - return { - ...json, - requiresRedirect, - redirectLink, - }; -}; - -/** - * Class dealing with the /payments endpoint - * - * @export - * @class Payments - */ -export default class Payments { - constructor(config) { - this.config = config; - } - - /** - * Sends payment or a payout request. - * - * Notable optional fields (swagger PaymentRequest, 2026-04 → 2026-06): - * - body.source — supports BLIK via `{ type: 'blik', ... }` per - * `PaymentRequestBlikSource` (2026-05-08), and Bacs Direct Debit via - * `{ type: 'bacs', id: 'src_...' }` per `PaymentRequestBacsSource` - * (2026-07-30) — both fields required. Pass `type` explicitly: with `type` - * omitted, `setSourceOrDestinationType` infers `id` (not `bacs`) from a - * `src_`-prefixed id. The `bacs` source echoes back as - * `PaymentGetResponseBacsSource` (`type` + `id`) on retrieval. - * - body.source — SEPA Direct Debit via `{ type: 'sepa', ... }` per - * `PaymentRequestSEPAV4Source` (2026-07-30). Required: `type`, `country`, - * `account_number` (the IBAN), `currency` and `account_holder`, whose - * `billing_address` is itself required with all five of `address_line1`, - * `address_line2`, `city`, `zip` and `country`. Optional: `mandate_id`, - * `date_of_signature` (`yyyy-MM-dd`) and `mandate_type` — `Core` or `B2B`, - * capitalised. `account_holder` also takes optional `first_name`, `last_name`, - * `company_name` and `type`. **Send `account_holder.type` lowercase** - * (`individual` / `corporate`). The swagger declares it capitalised - * (`Individual` / `Corporate`) at this one site, but that looks like a spec - * defect: the other 23 account-holder-type sites in the specification are all - * lowercase - including the sibling `PaymentRequestAchSource` - and every other - * Checkout.com SDK sends lowercase here. Pending confirmation from the API - * owners, lowercase is the safe value. Note this source carries no `bank_code`. - * - body.fallback_source — alternate source attempted if the primary source - * fails (2026-04-23). - * - body.processing.affiliate_id / processing.affiliate_url — affiliate - * 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) - * - scheme (2026-06-02), failure_code, partner_code, partner_response_code (2026-05-08) - * - fallback_source_used (2026-04-23) - * - * @memberof Payments - * @param {Object} body Payment Request body. - * @param {string} [idempotencyKey] Idempotency Key. - * @return {Promise} A promise to payment response. - */ - async request(body, idempotencyKey) { - try { - setSourceOrDestinationType(body); - validatePayment(body); - - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}`, - this.config, - this.config.sk, - body, - idempotencyKey - ); - return addUtilityParams(await response.json); - } catch (err) { - throw await determineError(err); - } - } - - /** - * Returns a list of your business' payments that match the specified reference. - * - * @memberof Payments - * @param {Object} body /^(pay|sid)_(\w{26})$/ The payment or payment session identifier. - * @return {Promise} A promise to the get payment response. - */ - async getPaymentList(body) { - let url = `${this.config.host}/${PAYMENTS_PATH}`; - - if (body) { - const queryString = Object.keys(body) - .map((key) => `${key}=${body[key]}`) - .join('&'); - url += `?${queryString}`; - } - - try { - const response = await get(this.config.httpClient, url, this.config, this.config.sk); - return response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Returns the details of the payment with the specified identifier string. - * - * Response fields available under `processing` (pass-through, swagger ProcessingData): - * - scheme — the scheme on which the payment was authorized, which may differ from the - * card's scheme if the card is co-badged (2026-06-02) - * - partner_fraud_status — partner fraud status; if `Pending` and the merchant captures - * before it changes to `Accepted`, the transaction risk is solely on the merchant - * - partner_merchant_advice_code — Mastercard Merchant Advice Code (MAC), with retry - * guidance for declined transactions - * - scheme_transaction_link_id (Mastercard Transaction Link Identifier, 2026-06-08) - * - failure_code, partner_code, partner_response_code (2026-05-08) - * - fallback_source_used (2026-04-23) - * - * @memberof Payments - * @param {string} id /^(pay|sid)_(\w{26})$/ The payment or payment session identifier. - * @return {Promise} A promise to the get payment response. - */ - async get(id) { - try { - const response = await get( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${id}`, - this.config, - this.config.sk - ); - return response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Returns all the actions associated with a payment ordered by processing date in - * descending order (latest first). - * - * @memberof Payments - * @param {string} id /^(pay)_(\w{26})$/ The payment identifier. - * @return {Promise} A promise to the getActions response. - */ - async getActions(id) { - try { - const response = await get( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${id}/${ACTIONS_PATH}`, - this.config, - this.config.sk - ); - return response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Request an incremental authorization to increase the authorization amount or extend - * the authorization's validity period. - * - * @memberof Payments - * @param {string} id /^(pay)_(\w{26})$/ The payment identifier. - * @param {Object} body Payment Request body. - * @param {string} [idempotencyKey] Idempotency Key. - * @return {Promise} A promise to the getActions response. - */ - async increment(id, body, idempotencyKey) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${id}/${AUTHORIZATIONS_PATH}`, - this.config, - this.config.sk, - body, - idempotencyKey - ); - return await response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Cancels an upcoming retry, if there is one scheduled - * Cancellation requests are processed asynchronously. You can use workflows to be notified if the cancellation is successful. - * - * @memberof Payments - * @param {string} id /^(pay)_(\w{26})$/ The unique payment identifier. - * @param {Object} body Payment Request body. - * @param {string} [idempotencyKey] Idempotency Key. - * @return {Promise} A promise to the getActions response. - */ - async cancelScheduledRetry(id, body, idempotencyKey) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${id}/${CANCELLATIONS_PATH}`, - this.config, - this.config.sk, - body, - idempotencyKey - ); - return await response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Captures a payment if supported by the payment method. - * - * @memberof Payments - * @param {string} paymentId /^(pay)_(\w{26})$/ The payment or payment session identifier. - * @param {Object} [body] Capture request body. - * @param {string} [idempotencyKey] Idempotency Key. - * @return {Promise} A promise to the capture response. - */ - async capture(paymentId, body, idempotencyKey) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${paymentId}/${CAPTURES_PATH}`, - this.config, - this.config.sk, - body, - idempotencyKey - ); - return response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Refunds a payment if supported by the payment method. - * - * @memberof Payments - * @param {string} paymentId /^(pay)_(\w{26})$/ The payment or payment session identifier. - * @param {Object} [body] Refund request body. - * @param {string} [idempotencyKey] Idempotency Key. - * @return {Promise} A promise to the refund response. - */ - async refund(paymentId, body, idempotencyKey) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${paymentId}/${REFUNDS_PATH}`, - this.config, - this.config.sk, - body, - idempotencyKey - ); - return response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Reverse a payment if supported by the payment method. - * - * Response (PaymentReversalAcceptedResponse) carries `action_type` since - * swagger 2026-05-26 — exposed verbatim in the resolved JSON. - * - * @memberof Payments - * @param {string} paymentId /^(pay)_(\w{26})$/ The unique identifier for the payment. - * @param {Object} [body] Reverse request body. - * @param {string} [idempotencyKey] Idempotency Key. - * @return {Promise} A promise to the reverse response. - */ - async reverse(paymentId, body, idempotencyKey) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${paymentId}/${REVERSALS_PATH}`, - this.config, - this.config.sk, - body, - idempotencyKey - ); - return response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Voids a payment if supported by the payment method. - * - * @memberof Payments - * @param {string} paymentId /^(pay)_(\w{26})$/ The payment or payment session identifier. - * @param {Object} [body] Void request body. - * @param {number} [body.amount] The amount to void, in the minor currency unit (min 0, max 9999999999). If not specified, the full payment amount is voided. - * @param {string} [idempotencyKey] Idempotency Key. - * @return {Promise} A promise to the void response. - */ - async void(paymentId, body, idempotencyKey) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${paymentId}/${VOIDS_PATH}`, - this.config, - this.config.sk, - body, - idempotencyKey - ); - return response.json; - } catch (err) { - throw await determineError(err); - } - } - - /** - * Search payments if supported by the payment method. - * - * @memberof Payments - * @param {Object} [body] Search request body. - * @return {Promise} A promise to the void response. - */ - async search(body) { - try { - const response = await post( - this.config.httpClient, - `${this.config.host}/${PAYMENTS_PATH}/${SEARCH_PATH}`, - this.config, - this.config.sk, - body - ); - return response.json; - } catch (err) { - throw await determineError(err); - } - } -} + +import { determineError } from '../../services/errors.js'; +import { get, post } from '../../services/http.js'; +import { setSourceOrDestinationType, validatePayment } from '../../services/validation.js'; + +// Path segments appended to the API base (config.host). +const ACTIONS_PATH = 'actions'; +const AUTHORIZATIONS_PATH = 'authorizations'; +const CANCELLATIONS_PATH = 'cancellations'; +const CAPTURES_PATH = 'captures'; +const PAYMENTS_PATH = 'payments'; +const REFUNDS_PATH = 'refunds'; +const REVERSALS_PATH = 'reversals'; +const SEARCH_PATH = 'search'; +const VOIDS_PATH = 'voids'; + +const addUtilityParams = (json) => { + if (!json || typeof json !== 'object') return json; + let requiresRedirect = false; + + if (json.destination) { + requiresRedirect = false; + } else { + const isPending = json.status === 'Pending'; + const hasRedirectUrl = json._links && json._links.redirect !== undefined; + requiresRedirect = isPending && hasRedirectUrl; + } + + // If the redirection URL exists add it to the response body as 'redirectLink' + let redirectLink; + if (requiresRedirect && json._links && json._links.redirect) { + redirectLink = json._links.redirect.href; + } + return { + ...json, + requiresRedirect, + redirectLink, + }; +}; + +/** + * Class dealing with the /payments endpoint + * + * @export + * @class Payments + */ +export default class Payments { + constructor(config) { + this.config = config; + } + + /** + * Sends payment or a payout request. + * + * Notable optional fields (swagger PaymentRequest, 2026-04 → 2026-06): + * - body.source — supports BLIK via `{ type: 'blik', ... }` per + * `PaymentRequestBlikSource` (2026-05-08), and Bacs Direct Debit via + * `{ type: 'bacs', id: 'src_...' }` per `PaymentRequestBacsSource` + * (2026-07-30) — both fields required. Pass `type` explicitly: with `type` + * omitted, `setSourceOrDestinationType` infers `id` (not `bacs`) from a + * `src_`-prefixed id. The `bacs` source echoes back as + * `PaymentGetResponseBacsSource` (`type` + `id`) on retrieval. + * - body.source — SEPA Direct Debit via `{ type: 'sepa', ... }` per + * `PaymentRequestSEPAV4Source` (2026-07-30). Required: `type`, `country`, + * `account_number` (the IBAN), `currency` and `account_holder`, whose + * `billing_address` is itself required with all five of `address_line1`, + * `address_line2`, `city`, `zip` and `country`. Optional: `mandate_id`, + * `date_of_signature` (`yyyy-MM-dd`) and `mandate_type` — `Core` or `B2B`, + * capitalised. `account_holder` also takes optional `first_name`, `last_name`, + * `company_name` and `type`. **Send `account_holder.type` lowercase** + * (`individual` / `corporate`). The swagger declares it capitalised + * (`Individual` / `Corporate`) at this one site, but that looks like a spec + * defect: the other 23 account-holder-type sites in the specification are all + * lowercase - including the sibling `PaymentRequestAchSource` - and every other + * Checkout.com SDK sends lowercase here. Pending confirmation from the API + * owners, lowercase is the safe value. Note this source carries no `bank_code`. + * - body.fallback_source — alternate source attempted if the primary source + * fails (2026-04-23). + * - body.processing.affiliate_id / processing.affiliate_url — affiliate + * tracking, surfaced under `processing` (2026-05-07). + * - body.processing.airline_data: optional array of AirlineData, each entry with + * `ticket` (object: number, issue_date [date], issuing_carrier_code, + * travel_package_indicator [free-form string: C = car rental, A = airline flight, + * B = both included, N = unknown], travel_agency_name, travel_agency_code), + * `passenger` (see the cardinality note below) and `flight_leg_details` + * (array of { flight_number [**string**, e.g. "101", not a number], carrier_code, + * class_of_travelling, departure_airport, departure_date [date], departure_time, + * arrival_airport, stop_over_code, fare_basis_code }). + * + * **`passenger` cardinality does not follow the specification.** The spec declares it + * array-only on `AirlineData` and `oneOf[array, object]` on + * `PaymentInterfacesProcessingAirlineData`, but the live API disagrees, and it disagrees + * per endpoint rather than per schema. Every row below was sent to the sandbox on + * 2026-09-28: + * + * | Surface | passenger as object | passenger as array | + * |-------------------------|---------------------|---------------------------| + * | POST /payments | 201 | 201 | + * | POST /payment-sessions | 201 | 201 | + * | POST /hosted-payments | 201 | 422 ..._passenger_invalid | + * | POST /payment-links | 201 | 422 ..._passenger_invalid | + * | POST /payment-contexts | 201 | 422 passenger_required | + * + * Note that hosted payments, payment links and payment sessions all resolve to the *same* + * `PaymentInterfacesProcessing` schema, yet the first two reject the array and the third + * accepts it. The schema is not a reliable guide to which form a surface takes. + * + * So send a single `{ first_name, last_name, date_of_birth [date], + * address: { country } }` **object** for one passenger, which every surface accepts. Send + * an array only for two or more, and only to this endpoint or payment sessions. Omit the + * key entirely when there are no passengers: both an empty array and an explicit `null` + * are rejected with `processing_airline_data_0_passenger_invalid`. Note that payment + * setups is the mirror image, taking a plural `passengers` that is array-only. + * - body.processing.accommodation_data: optional array of AccommodationData, each entry + * with name, booking_reference, check_in_date [date], check_out_date [date], + * `address` (object: address_line1, zip: only those two, not the wide address shape), + * state, country, city, number_of_rooms [integer], `guests` + * (array of { first_name, last_name, date_of_birth [date] }), `room` + * (array of { rate [**string**, e.g. "70"], number_of_nights_at_room_rate + * [**string**, e.g. "3"] }; singular key name, but an array), and the two phone arrays + * `property_phone` and `customer_service_phone` + * (array of { country_code, number }). `state` and `country` are plain strings, not + * country codes: the specification's own examples are `"FL"` and the three-letter + * `"USA"`. The two phone arrays are declared on `AccommodationData` only, so they are + * read here and on payment contexts, and ignored by hosted payments, payment links and + * payment sessions. + * - 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) + * - scheme (2026-06-02), failure_code, partner_code, partner_response_code (2026-05-08) + * - fallback_source_used (2026-04-23) + * + * @memberof Payments + * @param {Object} body Payment Request body. + * @param {string} [idempotencyKey] Idempotency Key. + * @return {Promise} A promise to payment response. + */ + async request(body, idempotencyKey) { + try { + setSourceOrDestinationType(body); + validatePayment(body); + + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}`, + this.config, + this.config.sk, + body, + idempotencyKey + ); + return addUtilityParams(await response.json); + } catch (err) { + throw await determineError(err); + } + } + + /** + * Returns a list of your business' payments that match the specified reference. + * + * Each item in the returned `data` array carries the same `processing` object as + * `cko.payments.get`, including `airline_data` and `accommodation_data` (swagger + * `PaymentPaged`). See the `get` JSDoc below for both shapes, and read `passenger` + * defensively there: it may be a single object or an array. + * + * @memberof Payments + * @param {Object} body /^(pay|sid)_(\w{26})$/ The payment or payment session identifier. + * @return {Promise} A promise to the get payment response. + */ + async getPaymentList(body) { + let url = `${this.config.host}/${PAYMENTS_PATH}`; + + if (body) { + const queryString = Object.keys(body) + .map((key) => `${key}=${body[key]}`) + .join('&'); + url += `?${queryString}`; + } + + try { + const response = await get(this.config.httpClient, url, this.config, this.config.sk); + return response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Returns the details of the payment with the specified identifier string. + * + * Response fields available under `processing` (pass-through, swagger ProcessingData): + * - scheme — the scheme on which the payment was authorized, which may differ from the + * card's scheme if the card is co-badged (2026-06-02) + * - partner_fraud_status — partner fraud status; if `Pending` and the merchant captures + * before it changes to `Accepted`, the transaction risk is solely on the merchant + * - partner_merchant_advice_code — Mastercard Merchant Advice Code (MAC), with retry + * guidance for declined transactions + * - scheme_transaction_link_id (Mastercard Transaction Link Identifier, 2026-06-08) + * - failure_code, partner_code, partner_response_code (2026-05-08) + * - fallback_source_used (2026-04-23) + * - airline_data: array of AirlineData, each entry with `ticket` (object: number, + * issue_date [date], issuing_carrier_code, travel_package_indicator, + * travel_agency_name, travel_agency_code), `passenger` and `flight_leg_details` + * (array of { flight_number [string], carrier_code, class_of_travelling, + * departure_airport, departure_date [date], departure_time, arrival_airport, + * stop_over_code, fare_basis_code }). + * + * **Read `passenger` defensively: it may be a single object or an array.** The + * specification declares it array-only, but a single passenger is sent and echoed back + * as a bare object, which is what a typed SDK modelling it as one object or one array + * gets wrong. Node passes `response.json` straight through, so whichever shape the API + * returns is the shape the caller receives. Each passenger carries first_name, + * last_name, date_of_birth [date] and `address` ({ country }). + * - accommodation_data: array of AccommodationData, each entry with name, + * booking_reference, check_in_date [date], check_out_date [date], `address` + * (object: address_line1, zip), state, country, city, number_of_rooms [integer], + * `guests` (array of { first_name, last_name, date_of_birth [date] }), `room` + * (array of { rate [string], number_of_nights_at_room_rate [string] }), + * `property_phone` and `customer_service_phone` + * (arrays of { country_code, number }). `state` and `country` are plain strings, not + * country codes: the specification's examples are `"FL"` and the three-letter `"USA"`. + * + * @memberof Payments + * @param {string} id /^(pay|sid)_(\w{26})$/ The payment or payment session identifier. + * @return {Promise} A promise to the get payment response. + */ + async get(id) { + try { + const response = await get( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${id}`, + this.config, + this.config.sk + ); + return response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Returns all the actions associated with a payment ordered by processing date in + * descending order (latest first). + * + * @memberof Payments + * @param {string} id /^(pay)_(\w{26})$/ The payment identifier. + * @return {Promise} A promise to the getActions response. + */ + async getActions(id) { + try { + const response = await get( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${id}/${ACTIONS_PATH}`, + this.config, + this.config.sk + ); + return response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Request an incremental authorization to increase the authorization amount or extend + * the authorization's validity period. + * + * @memberof Payments + * @param {string} id /^(pay)_(\w{26})$/ The payment identifier. + * @param {Object} body Payment Request body. + * @param {string} [idempotencyKey] Idempotency Key. + * @return {Promise} A promise to the getActions response. + */ + async increment(id, body, idempotencyKey) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${id}/${AUTHORIZATIONS_PATH}`, + this.config, + this.config.sk, + body, + idempotencyKey + ); + return await response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Cancels an upcoming retry, if there is one scheduled + * Cancellation requests are processed asynchronously. You can use workflows to be notified if the cancellation is successful. + * + * @memberof Payments + * @param {string} id /^(pay)_(\w{26})$/ The unique payment identifier. + * @param {Object} body Payment Request body. + * @param {string} [idempotencyKey] Idempotency Key. + * @return {Promise} A promise to the getActions response. + */ + async cancelScheduledRetry(id, body, idempotencyKey) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${id}/${CANCELLATIONS_PATH}`, + this.config, + this.config.sk, + body, + idempotencyKey + ); + return await response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Captures a payment if supported by the payment method. + * + * @memberof Payments + * @param {string} paymentId /^(pay)_(\w{26})$/ The payment or payment session identifier. + * @param {Object} [body] Capture request body. + * @param {string} [idempotencyKey] Idempotency Key. + * @return {Promise} A promise to the capture response. + */ + async capture(paymentId, body, idempotencyKey) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${paymentId}/${CAPTURES_PATH}`, + this.config, + this.config.sk, + body, + idempotencyKey + ); + return response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Refunds a payment if supported by the payment method. + * + * @memberof Payments + * @param {string} paymentId /^(pay)_(\w{26})$/ The payment or payment session identifier. + * @param {Object} [body] Refund request body. + * @param {string} [idempotencyKey] Idempotency Key. + * @return {Promise} A promise to the refund response. + */ + async refund(paymentId, body, idempotencyKey) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${paymentId}/${REFUNDS_PATH}`, + this.config, + this.config.sk, + body, + idempotencyKey + ); + return response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Reverse a payment if supported by the payment method. + * + * Response (PaymentReversalAcceptedResponse) carries `action_type` since + * swagger 2026-05-26 — exposed verbatim in the resolved JSON. + * + * @memberof Payments + * @param {string} paymentId /^(pay)_(\w{26})$/ The unique identifier for the payment. + * @param {Object} [body] Reverse request body. + * @param {string} [idempotencyKey] Idempotency Key. + * @return {Promise} A promise to the reverse response. + */ + async reverse(paymentId, body, idempotencyKey) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${paymentId}/${REVERSALS_PATH}`, + this.config, + this.config.sk, + body, + idempotencyKey + ); + return response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Voids a payment if supported by the payment method. + * + * @memberof Payments + * @param {string} paymentId /^(pay)_(\w{26})$/ The payment or payment session identifier. + * @param {Object} [body] Void request body. + * @param {number} [body.amount] The amount to void, in the minor currency unit (min 0, max 9999999999). If not specified, the full payment amount is voided. + * @param {string} [idempotencyKey] Idempotency Key. + * @return {Promise} A promise to the void response. + */ + async void(paymentId, body, idempotencyKey) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${paymentId}/${VOIDS_PATH}`, + this.config, + this.config.sk, + body, + idempotencyKey + ); + return response.json; + } catch (err) { + throw await determineError(err); + } + } + + /** + * Search payments if supported by the payment method. + * + * @memberof Payments + * @param {Object} [body] Search request body. + * @return {Promise} A promise to the void response. + */ + async search(body) { + try { + const response = await post( + this.config.httpClient, + `${this.config.host}/${PAYMENTS_PATH}/${SEARCH_PATH}`, + this.config, + this.config.sk, + body + ); + return response.json; + } catch (err) { + throw await determineError(err); + } + } +} diff --git a/test/payment-setups/payment-setups-unit.js b/test/payment-setups/payment-setups-unit.js index 66514c1..3ea3df1 100644 --- a/test/payment-setups/payment-setups-unit.js +++ b/test/payment-setups/payment-setups-unit.js @@ -464,7 +464,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -481,51 +481,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -652,7 +653,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -669,51 +670,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -856,7 +858,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -873,51 +875,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -1064,7 +1067,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -1081,51 +1084,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -1272,7 +1276,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -1289,51 +1293,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -1486,7 +1491,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -1503,51 +1508,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -1719,7 +1725,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -1736,51 +1742,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -2013,7 +2020,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -2030,51 +2037,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -2202,7 +2210,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -2219,51 +2227,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -2407,7 +2416,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -2424,51 +2433,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -2616,7 +2626,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -2633,51 +2643,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -2825,7 +2836,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -2842,51 +2853,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, @@ -3040,7 +3052,7 @@ describe('Unit::Payment-Setups', () => { ], shipping: { address: { - address_line_1: '10 Canterbury Road', + address_line1: '10 Canterbury Road', city: 'London', zip: 'SW1 1AA' }, @@ -3057,51 +3069,52 @@ describe('Unit::Payment-Setups', () => { discount_amount: 10 }, industry: { - airline_data: { + airline: [ + { ticket: { - number: '0742464639523', - issue_date: '2025-05-01', - issuing_carrier_code: '042', - travel_package_indicator: 'A', - travel_agency_name: 'Checkout Travel Agents', - travel_agency_code: '91114362' + number: '0742464639523', + issue_date: '2025-05-01', + issuing_carrier_code: '042', + travel_package_indicator: 'A', + travel_agency_name: 'Checkout Travel Agents', + travel_agency_code: '91114362' }, passengers: [ - { - first_name: 'John', - last_name: 'Smith', - date_of_birth: '1990-10-31', - address: { - country: 'GB' - } - } + { + first_name: 'John', + last_name: 'Smith', + date_of_birth: '1990-10-31', + address: { + country: 'GB' + } + } ], flight_leg_details: [ - { - flight_number: 'BA1483', - carrier_code: 'BA', - class_of_travelling: 'W', - departure_airport: 'LHW', - departure_date: '2025-10-13', - departure_time: '18:30', - arrival_airport: 'JFK', - stop_over_code: 'X', - fare_basis_code: 'WUP14B' - } + { + flight_number: 'BA1483', + carrier_code: 'BA', + class_of_travelling: 'W', + departure_airport: 'LHW', + departure_date: '2025-10-13', + departure_time: '18:30', + arrival_airport: 'JFK', + stop_over_code: 'X', + fare_basis_code: 'WUP14B' + } ] - }, - accommodation_data: [ + } + ], + accommodation: [ { name: 'Checkout Lodge', booking_reference: 'REF9083748', check_in_date: '2025-04-11', check_out_date: '2025-04-18', address: { - address_line_1: '123 High Street', - address_line_2: 'Flat 456', + address_line1: '123 High Street', city: 'London', state: 'Greater London', - country: 'United Kingdom', + country: 'GB', zip: 'SW1 1AA' }, number_of_rooms: 2, diff --git a/test/payments/getPaymentAirlineData.js b/test/payments/getPaymentAirlineData.js new file mode 100644 index 0000000..099d9ae --- /dev/null +++ b/test/payments/getPaymentAirlineData.js @@ -0,0 +1,263 @@ +import { Checkout } from "../../src/index.js"; +import { expect } from "chai"; +import nock from "nock"; + +const SK = "sk_test_0b9b5db6-f223-49d0-b68f-f6643dd4f808"; + +// Node returns `response.json` untouched, so the pass-through IS the feature: whatever shape the +// API sends under `processing.airline_data` is what the caller receives. These tests pin that, +// including the two things a typed SDK gets wrong on this sub-tree: `passenger` arriving as a bare +// object rather than an array, and the `class_of_travelling` / `stop_over_code` / string +// `flight_number` keys that six SDKs previously misspelled or mistyped. +describe("Get payment details with airline and accommodation data", () => { + const airlineData = [ + { + ticket: { + number: "045-21351455613", + issue_date: "2023-05-20", + issuing_carrier_code: "AI", + travel_package_indicator: "B", + travel_agency_name: "World Tours", + travel_agency_code: "01" + }, + passenger: { + first_name: "John", + last_name: "White", + date_of_birth: "1990-05-26", + address: { country: "US" } + }, + flight_leg_details: [ + { + flight_number: "101", + carrier_code: "BA", + class_of_travelling: "J", + departure_airport: "LHR", + departure_date: "2023-06-19", + departure_time: "15:30", + arrival_airport: "LAX", + stop_over_code: "x", + fare_basis_code: "SPRSVR" + } + ] + } + ]; + + const accommodationData = [ + { + name: "The Sea View Hotel", + booking_reference: "HOTEL123", + check_in_date: "2023-06-20", + check_out_date: "2023-06-23", + address: { address_line1: "123 Beach Road", zip: "10001" }, + state: "FL", + country: "USA", + city: "Los Angeles", + number_of_rooms: 2, + guests: [ + { first_name: "Jane", last_name: "Doe", date_of_birth: "1985-07-14" } + ], + room: [{ rate: "70", number_of_nights_at_room_rate: "3" }], + property_phone: [{ country_code: "44", number: "7123456789" }], + customer_service_phone: [{ country_code: "44", number: "7123456789" }] + } + ]; + + const reply = (processing) => ({ + id: "pay_je5hbbb4u3oe7k4u3lbwlu3zkq", + amount: 10, + currency: "USD", + status: "Authorized", + approved: true, + processing + }); + + it("passes a single-object passenger through unchanged", async () => { + nock("https://test.api.sandbox.checkout.com") + .get("/payments/pay_je5hbbb4u3oe7k4u3lbwlu3zkq") + .reply(200, reply({ airline_data: airlineData })); + + const cko = new Checkout(SK, { subdomain: "test" }); + const payment = await cko.payments.get("pay_je5hbbb4u3oe7k4u3lbwlu3zkq"); + + // Not normalised into an array: a bare object stays a bare object. + expect(payment.processing.airline_data[0].passenger).to.be.an("object"); + expect(payment.processing.airline_data[0].passenger).to.not.be.an("array"); + expect(payment.processing.airline_data[0].passenger.first_name).to.equal("John"); + expect(payment.processing.airline_data[0].passenger.address.country).to.equal("US"); + expect(payment.processing.airline_data).to.deep.equal(airlineData); + }); + + it("passes an array of passengers through unchanged", async () => { + const twoPassengers = [ + { + ...airlineData[0], + passenger: [ + airlineData[0].passenger, + { first_name: "Jane", last_name: "Doe" } + ] + } + ]; + + nock("https://test.api.sandbox.checkout.com") + .get("/payments/pay_je5hbbb4u3oe7k4u3lbwlu3zkq") + .reply(200, reply({ airline_data: twoPassengers })); + + const cko = new Checkout(SK, { subdomain: "test" }); + const payment = await cko.payments.get("pay_je5hbbb4u3oe7k4u3lbwlu3zkq"); + + expect(payment.processing.airline_data[0].passenger).to.be.an("array"); + expect(payment.processing.airline_data[0].passenger).to.have.lengthOf(2); + expect(payment.processing.airline_data).to.deep.equal(twoPassengers); + }); + + it("preserves the flight leg keys and the string flight_number", async () => { + nock("https://test.api.sandbox.checkout.com") + .get("/payments/pay_je5hbbb4u3oe7k4u3lbwlu3zkq") + .reply(200, reply({ airline_data: airlineData })); + + const cko = new Checkout(SK, { subdomain: "test" }); + const payment = await cko.payments.get("pay_je5hbbb4u3oe7k4u3lbwlu3zkq"); + const leg = payment.processing.airline_data[0].flight_leg_details[0]; + + expect(leg.class_of_travelling).to.equal("J"); + expect(leg.stop_over_code).to.equal("x"); + expect(leg.flight_number).to.be.a("string"); + expect(leg.flight_number).to.equal("101"); + // The misspellings that six SDKs shipped must not appear. + expect(leg).to.not.have.property("service_class"); + expect(leg).to.not.have.property("stopover_code"); + expect(leg).to.not.have.property("class_of_traveling"); + }); + + it("passes the accommodation block through including both phone arrays", async () => { + nock("https://test.api.sandbox.checkout.com") + .get("/payments/pay_je5hbbb4u3oe7k4u3lbwlu3zkq") + .reply(200, reply({ accommodation_data: accommodationData })); + + const cko = new Checkout(SK, { subdomain: "test" }); + const payment = await cko.payments.get("pay_je5hbbb4u3oe7k4u3lbwlu3zkq"); + const accommodation = payment.processing.accommodation_data[0]; + + expect(payment.processing.accommodation_data).to.deep.equal(accommodationData); + expect(accommodation.property_phone[0].number).to.equal("7123456789"); + expect(accommodation.customer_service_phone[0].country_code).to.equal("44"); + // Plain strings, so the three-letter "USA" survives where a country enum would not. + expect(accommodation.country).to.equal("USA"); + expect(accommodation.state).to.equal("FL"); + // Strings, not numbers, on both room fields. + expect(accommodation.room[0].rate).to.be.a("string"); + expect(accommodation.room[0].number_of_nights_at_room_rate).to.be.a("string"); + }); +}); + +// Request-side pass-through. Node sends `body` untouched, so what the caller builds is what goes +// on the wire. These assert the outgoing JSON with nock's request-body matcher, the same form +// requestPaymentBacs.js uses, because the cardinality problem bites on the way out: an array sent +// to hosted payments, payment links or payment contexts returns 422. +describe("Request airline data, outgoing body", () => { + const passenger = { + first_name: "John", + last_name: "White", + date_of_birth: "1990-05-26", + address: { country: "US" } + }; + const leg = { + flight_number: "101", + carrier_code: "BA", + class_of_travelling: "J", + departure_airport: "LHR", + departure_date: "2023-06-19", + departure_time: "15:30", + arrival_airport: "LAX", + stop_over_code: "x", + fare_basis_code: "SPRSVR" + }; + const cardSource = { + type: "card", + number: "4242424242424242", + expiry_month: 6, + expiry_year: 2029, + cvv: "100" + }; + + const paymentBody = (airline) => ({ + source: cardSource, + amount: 10, + currency: "USD", + processing: { airline_data: [airline] } + }); + + it("sends a single passenger as an object, not wrapped in an array", async () => { + let sent; + nock("https://test.api.sandbox.checkout.com") + .post("/payments", (body) => { + sent = body; + return true; + }) + .reply(201, { id: "pay_x", approved: true }); + + const cko = new Checkout(SK, { subdomain: "test" }); + await cko.payments.request(paymentBody({ passenger, flight_leg_details: [leg] })); + + // The object must survive as an object: the SDK does not normalise it into a list. + expect(sent.processing.airline_data[0].passenger).to.be.an("object"); + expect(sent.processing.airline_data[0].passenger).to.not.be.an("array"); + expect(sent.processing.airline_data[0].passenger).to.deep.equal(passenger); + }); + + it("sends several passengers as an array", async () => { + let sent; + nock("https://test.api.sandbox.checkout.com") + .post("/payments", (body) => { + sent = body; + return true; + }) + .reply(201, { id: "pay_x", approved: true }); + + const cko = new Checkout(SK, { subdomain: "test" }); + await cko.payments.request( + paymentBody({ passenger: [passenger, { first_name: "Jane" }], flight_leg_details: [leg] }) + ); + + expect(sent.processing.airline_data[0].passenger).to.be.an("array"); + expect(sent.processing.airline_data[0].passenger).to.have.lengthOf(2); + }); + + it("omits passenger entirely when it was never set", async () => { + let sent; + nock("https://test.api.sandbox.checkout.com") + .post("/payments", (body) => { + sent = body; + return true; + }) + .reply(201, { id: "pay_x", approved: true }); + + const cko = new Checkout(SK, { subdomain: "test" }); + await cko.payments.request(paymentBody({ flight_leg_details: [leg] })); + + // An empty array and an explicit null are both rejected with + // processing_airline_data_0_passenger_invalid, so absence is the only safe zero-passenger form. + expect(sent.processing.airline_data[0]).to.not.have.property("passenger"); + }); + + it("sends the renamed flight leg keys and the string flight_number on the wire", async () => { + let sent; + nock("https://test.api.sandbox.checkout.com") + .post("/payments", (body) => { + sent = body; + return true; + }) + .reply(201, { id: "pay_x", approved: true }); + + const cko = new Checkout(SK, { subdomain: "test" }); + await cko.payments.request(paymentBody({ passenger, flight_leg_details: [leg] })); + + const outgoing = sent.processing.airline_data[0].flight_leg_details[0]; + expect(outgoing.class_of_travelling).to.equal("J"); + expect(outgoing.stop_over_code).to.equal("x"); + expect(outgoing.flight_number).to.equal("101"); + expect(outgoing.flight_number).to.be.a("string"); + expect(outgoing).to.not.have.property("service_class"); + expect(outgoing).to.not.have.property("stopover_code"); + }); +}); From 37f79367894256cd1bb05102f68c07031da7d32d Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 29 Sep 2026 11:17:43 +0200 Subject: [PATCH 2/2] New it test --- test/payments/payments-airline-it.js | 129 +++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 test/payments/payments-airline-it.js diff --git a/test/payments/payments-airline-it.js b/test/payments/payments-airline-it.js new file mode 100644 index 0000000..a295fd3 --- /dev/null +++ b/test/payments/payments-airline-it.js @@ -0,0 +1,129 @@ +import { expect } from "chai"; +import nock from "nock"; +import Checkout from "../../src/Checkout.js"; + +afterEach(() => { + nock.cleanAll(); + nock.enableNetConnect(); +}); + +const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { + useLegacyDomain: true, +}); + +// Live coverage for the airline and accommodation processing sub-tree. +// +// The whole reason this file exists: the specification declares `passenger` array-only, and the +// live API rejects the array on three of five request surfaces. No spec-derived test can catch +// that, only a real request. Node passes `body` through untouched, so the JSDoc is the only thing +// standing between a merchant and a 422, which makes a live check the only real verification. +const airlineData = (passenger) => [ + { + ticket: { + number: "045-21351455613", + issue_date: "2023-05-20", + issuing_carrier_code: "AI", + travel_package_indicator: "B", + travel_agency_name: "World Tours", + travel_agency_code: "01", + }, + passenger, + flight_leg_details: [ + { + flight_number: "101", + carrier_code: "BA", + class_of_travelling: "J", + departure_airport: "LHR", + departure_date: "2023-06-19", + departure_time: "15:30", + arrival_airport: "LAX", + stop_over_code: "x", + fare_basis_code: "SPRSVR", + }, + ], + }, +]; + +const onePassenger = { + first_name: "John", + last_name: "White", + date_of_birth: "1990-05-26", + address: { country: "US" }, +}; + +const accommodationData = [ + { + name: "The Sea View Hotel", + booking_reference: "HOTEL123", + check_in_date: "2023-06-20", + check_out_date: "2023-06-23", + address: { address_line1: "123 Beach Road", zip: "10001" }, + state: "FL", + country: "USA", + city: "Los Angeles", + number_of_rooms: 2, + guests: [{ first_name: "Jane", last_name: "Doe", date_of_birth: "1985-07-14" }], + room: [{ rate: "70", number_of_nights_at_room_rate: "3" }], + property_phone: [{ country_code: "44", number: "7123456789" }], + customer_service_phone: [{ country_code: "44", number: "7123456789" }], + }, +]; + +const paymentWith = (processing) => ({ + source: { + type: "card", + number: "4242424242424242", + expiry_month: 6, + expiry_year: 2029, + cvv: "100", + }, + amount: 10, + currency: "USD", + processing, +}); + +describe("Integration::Payments-Airline", () => { + it("should request a payment with a single passenger as an object", async () => { + const response = await cko.payments.request( + paymentWith({ airline_data: airlineData(onePassenger) }) + ); + + expect(response.id).to.not.be.undefined; + expect(response.approved).to.equal(true); + }); + + it("should request a payment with several passengers as an array", async () => { + // POST /payments is one of only two surfaces that accept the array form. Hosted payments, + // payment links and payment contexts all return 422 for it. + const response = await cko.payments.request( + paymentWith({ + airline_data: airlineData([onePassenger, { first_name: "Jane", last_name: "Doe" }]), + }) + ); + + expect(response.id).to.not.be.undefined; + expect(response.approved).to.equal(true); + }); + + it("should request a payment with accommodation data including both phone arrays", async () => { + const response = await cko.payments.request( + paymentWith({ + airline_data: airlineData(onePassenger), + accommodation_data: accommodationData, + }) + ); + + expect(response.id).to.not.be.undefined; + expect(response.approved).to.equal(true); + }); + + it("should accept a fractional tax_amount and echo it back", async () => { + // The swagger types the six processing amount fields as `number`, not `integer`. Node passes + // the value through untouched, so this is the surface where that is observable end to end; + // Java threw and Go failed the whole response on this payload before being retyped. + const created = await cko.payments.request(paymentWith({ tax_amount: 10.5 })); + const fetched = await cko.payments.get(created.id); + + expect(fetched.processing.tax_amount).to.equal(10.5); + }); +});