Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/checkout-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Checkout tests

on:
pull_request:
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '26'
cache: 'npm'
cache-dependency-path: site/package-lock.json

- name: Install dependencies
working-directory: site
run: npm ci

- name: Run checkout tests
working-directory: site
run: npm test
46 changes: 32 additions & 14 deletions site/assets/js/modules/forms/phone-number.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
Expand All @@ -26,18 +26,6 @@

'use strict';

/**
* Removes characters that are not accepted by the phone-number field.
*
* <p>Allowed: digits, parentheses, hyphens, and spaces.
*
* @param {string} value phone-number value to sanitize
* @return {string} sanitized phone-number value
*/
export function sanitizePhoneNumberInput(value) {
return String(value || '').replace(/[^0-9\s()-]/g, '');
}

/**
* Builds the phone-number payload with country code and number with digits only.
*
Expand All @@ -46,7 +34,7 @@ export function sanitizePhoneNumberInput(value) {
* @return {{countryCode: number, number: string}|null}
* normalized phone-number payload, or null when incomplete
*/
export function normalizePhoneNumber(rawCountryCode, rawNumber) {
function normalizePhoneNumber(rawCountryCode, rawNumber) {
const countryCode = String(rawCountryCode || '').replace(/\D/g, '');
const number = String(rawNumber || '').replace(/\D/g, '');

Expand All @@ -64,3 +52,33 @@ export function normalizePhoneNumber(rawCountryCode, rawNumber) {
number
};
}

/**
* Builds a Paygate phone payload from an `intl-tel-input` field.
*
* @param {string} rawNumber displayed national phone number
* @param {string} rawCountryCode selected international dial code
* @param {string} rawFullNumber full number returned by the plugin
* @return {{countryCode: number, number: string}|null}
* normalized phone-number payload, or null when incomplete
*/
export function normalizeIntlPhoneNumber(rawNumber, rawCountryCode, rawFullNumber) {
const countryCode = digits(rawCountryCode);
const fullNumber = digits(rawFullNumber);
const fallbackNumber = digits(rawNumber);

if (!countryCode || !fallbackNumber) {
return null;
}

const number = fullNumber.indexOf(countryCode) === 0
? fullNumber.slice(countryCode.length)
: fallbackNumber;

return normalizePhoneNumber(countryCode, number);
}

/** Returns decimal digits from a phone-number fragment. */
function digits(value) {
return String(value || '').replace(/\D/g, '');
}
5 changes: 3 additions & 2 deletions site/assets/js/modules/paygate/purchases.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
* @property {string} orderId paygate order ID
* @property {string} productTitle product display title
* @property {string} productDescription product description shown on checkout
* @property {boolean} paymentCompleted whether the order was already paid
* @property {string} [paymentStatus] current known payment status
* @property {boolean} completed whether the order was already paid
*/

/**
Expand Down Expand Up @@ -202,7 +203,7 @@ export function createPurchaseClient(serverUrl) {
* @throws {PurchaseApiError} if response status is not OK
*/
async function getJson(url) {
const response = await fetch(url);
const response = await fetch(url, {cache: 'no-store'});
const body = await readResponseBody(response);

if (!response.ok) {
Expand Down
32 changes: 12 additions & 20 deletions site/assets/js/pages/checkout/charge-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
Expand Down Expand Up @@ -33,6 +33,7 @@
*/

import {createDelayedRequestController} from 'js/pages/checkout/delayed-request-controller';
import {buildChargeRequest} from 'js/pages/checkout/charge-request';
import {fieldValidationState} from 'js/pages/checkout/form-controller';

/**
Expand Down Expand Up @@ -128,9 +129,9 @@ export function createChargeController(
* @param {boolean} state.isRequesting whether a charge request is currently in flight
*/
function updateVatIdState({hasCurrentResult, isRequesting}) {
const hasRequestKey = Boolean(getRequestKey());
const hasVatId = Boolean(getVatId());

if (!hasRequestKey) {
if (!hasVatId) {
onFieldValidationStateChange(fieldValidationState.idle);
return;
}
Expand All @@ -155,15 +156,12 @@ export function createChargeController(
const buyerCountryCode = getBuyerCountryCode();
const vatId = getVatId();

if (!orderId || !buyerCountryCode || !vatId) {
const payload = buildChargeRequest(orderId, buyerCountryCode, vatId);
if (!payload) {
return null;
}

return purchaseClient.calculateCharges({
orderId,
buyerCountryCode,
vatId
});
return purchaseClient.calculateCharges(payload);
}

/**
Expand All @@ -175,9 +173,7 @@ export function createChargeController(
const buyerCountryCode = getBuyerCountryCode();
const vatId = getVatId();

return buyerCountryCode && vatId
? [buyerCountryCode, vatId].join(':')
: '';
return buyerCountryCode ? [buyerCountryCode, vatId].join(':') : '';
}

/**
Expand All @@ -187,19 +183,15 @@ export function createChargeController(
* @param {boolean} isCurrentRequest whether the failed request is still current
*/
function handleRequestError(error, isCurrentRequest) {
const isVatError = isVatErrorResponse(error);

if (!isVatError) {
view.showErrorModal();
}

if (!isCurrentRequest) {
logApiError(error);
return;
}

if (isVatError) {
if (isVatErrorResponse(error)) {
onVatIdError(getVatErrorReason(error));
} else {
view.showErrorModal();
}

logApiError(error);
Expand All @@ -212,7 +204,7 @@ export function createChargeController(
* @return {boolean} true when the error is a Paygate VAT validation response
*/
function isVatErrorResponse(error) {
return error.status === 422 && Boolean(error.body.vatIdInvalid);
return error.status === 422 && Boolean(error.body && error.body.vatIdInvalid);
}

/**
Expand Down
45 changes: 45 additions & 0 deletions site/assets/js/pages/checkout/charge-request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

'use strict';

/**
* Builds a Paygate charge-calculation request for the current buyer inputs.
*
* @param {string} orderId Paygate order ID
* @param {string} buyerCountryCode billing country ISO code
* @param {string} vatId optional VAT ID
* @return {Object|null} request body, or null while required inputs are absent
*/
export function buildChargeRequest(orderId, buyerCountryCode, vatId) {
if (!orderId || !buyerCountryCode) {
return null;
}

return vatId
? {orderId, buyerCountryCode, vatId}
: {orderId, buyerCountryCode};
}
74 changes: 74 additions & 0 deletions site/assets/js/pages/checkout/completed-page-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

'use strict';

/**
* Builds Spine's payment-result URL from its checkout URL.
*
* @param {string} currentUrl absolute checkout URL
* @param {string} orderId Paygate order ID
* @return {string} absolute payment-result URL
*/
export function getCompletedPageUrl(currentUrl, orderId) {
const completedUrl = new URL(currentUrl);
const checkoutPath = /\/checkout\/?$/;

if (checkoutPath.test(completedUrl.pathname)) {
completedUrl.pathname = completedUrl.pathname.replace(
checkoutPath,
'/checkout-completed/'
);
} else {
return '';
}
completedUrl.search = '';
completedUrl.searchParams.set('orderId', orderId);
completedUrl.hash = '';
return completedUrl.href;
}

/**
* Builds Spine's checkout URL from its payment-result URL.
*
* @param {string} currentUrl absolute payment-result URL
* @param {string} orderId Paygate order ID
* @return {string} absolute checkout URL, or an empty string for an unsupported path
*/
export function getCheckoutPageUrl(currentUrl, orderId) {
const checkoutUrl = new URL(currentUrl);
const completedPath = /\/checkout-completed\/?$/;

if (completedPath.test(checkoutUrl.pathname)) {
checkoutUrl.pathname = checkoutUrl.pathname.replace(completedPath, '/checkout/');
} else {
return '';
}
checkoutUrl.search = '';
checkoutUrl.searchParams.set('orderId', orderId);
checkoutUrl.hash = '';
return checkoutUrl.href;
}
Loading
Loading