From 04105b323be7b42ab2a16b24dfcb944789f53d58 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 5 May 2026 16:47:01 +0200 Subject: [PATCH 1/9] Use `get-order` instead of `get-product`. --- site/assets/js/modules/paygate/purchases.js | 15 +++++ site/assets/js/pages/checkout/dom.js | 2 +- site/assets/js/pages/checkout/index.js | 64 ++++++------------- .../js/pages/checkout/view-controller.js | 28 ++++---- site/layouts/checkout/single.html | 6 +- 5 files changed, 54 insertions(+), 61 deletions(-) diff --git a/site/assets/js/modules/paygate/purchases.js b/site/assets/js/modules/paygate/purchases.js index 2a91f14b..b70a1894 100644 --- a/site/assets/js/modules/paygate/purchases.js +++ b/site/assets/js/modules/paygate/purchases.js @@ -52,6 +52,16 @@ * @property {PaygateMoney} netAmount product price before VAT */ +/** + * Paygate order data returned by the purchase endpoint. + * + * @typedef {Object} PaygateOrder + * @property {string} orderId paygate order ID + * @property {string} productName product display name + * @property {string} productDescription product description shown on checkout + * @property {boolean} paymentCompleted whether the order was already paid + */ + /** * Paygate response for the `place-order` endpoint. * @@ -160,6 +170,8 @@ * @typedef {Object} PaygatePurchaseClient * @property {function(string): Promise} getProduct * loads checkout product data by product ID + * @property {function(string): Promise} getOrder + * loads checkout order data by order ID * @property {function(string): Promise} placeOrder * creates a checkout order for the given product ID * @property {function(CalculateChargesRequest): Promise} calculateCharges @@ -179,6 +191,9 @@ export function createPurchaseClient(serverUrl) { getProduct(productId) { return getJson(`${serverUrl}/products/${encodeURIComponent(productId)}`); }, + getOrder(orderId) { + return getJson(`${serverUrl}/purchases/${encodeURIComponent(orderId)}`); + }, placeOrder(productId) { return postJson(`${serverUrl}/purchases/place-order`, {productId}); }, diff --git a/site/assets/js/pages/checkout/dom.js b/site/assets/js/pages/checkout/dom.js index 2fff1c10..c4620755 100644 --- a/site/assets/js/pages/checkout/dom.js +++ b/site/assets/js/pages/checkout/dom.js @@ -53,7 +53,7 @@ * @property {JQuery} $totalValue total amount element * @property {JQuery} $submitButton checkout submit button * @property {JQuery} $errorModal generic checkout error modal - * @property {JQuery} $notFound product-not-found result panel + * @property {JQuery} $notFound order-not-found result panel * @property {JQuery} $summaryError generic checkout summary-error panel * @property {HTMLFormElement} form native checkout form element */ diff --git a/site/assets/js/pages/checkout/index.js b/site/assets/js/pages/checkout/index.js index 053c96ed..43a099d7 100644 --- a/site/assets/js/pages/checkout/index.js +++ b/site/assets/js/pages/checkout/index.js @@ -44,17 +44,15 @@ $( } const purchaseClient = createPurchaseClient(params.paygate.serverurl); - const productId = getProductId(); + const orderId = getOrderId(); const view = createCheckoutView(dom); const formController = createCheckoutFormController({dom}); - let orderId = null; - let orderPromise = null; let countryManuallySelected = false; let phoneCountryManuallySelected = false; const chargeController = createChargeController({ purchaseClient, view, - ensureOrderId, + ensureOrderId: () => Promise.resolve(orderId), getBuyerCountryCode: () => dom.$country.val(), getVatId: () => (dom.$vatId.val() || '').trim(), onFieldValidationStateChange: state => { @@ -64,9 +62,8 @@ $( logApiError }); - if (!productId) { - chargeController.invalidate(); - view.showNotFoundView(); + if (!orderId) { + redirectToGettingHelp(); return; } @@ -74,7 +71,7 @@ $( formController.updatePhoneCountryDisplay(); formController.bindPhoneEvents(); chargeController.updateSubmitState(); - loadProduct(); + loadOrder(); bindEvents(); /** @@ -136,16 +133,16 @@ $( } /** - * Loads product details for the checkout page from the current checkout URL. + * Loads order details for the checkout page from the current checkout URL. * - * @return {Promise} resolves when the initial product load flow finishes + * @return {Promise} resolves when the initial order load flow finishes */ - async function loadProduct() { + async function loadOrder() { view.setSummaryLoading(true); try { - const product = await purchaseClient.getProduct(productId); - view.fillProductSummary(product); + const order = await purchaseClient.getOrder(orderId); + view.fillOrderSummary(order); view.setSummaryLoading(false); dom.$form.prop('hidden', false); chargeController.updateSubmitState(); @@ -197,12 +194,19 @@ $( } /** - * Reads the product ID from the `product` query parameter. + * Reads the order ID from the `orderId` query parameter. * - * @return {string} checkout product ID, or empty string when unavailable + * @return {string} checkout order ID, or empty string when unavailable */ - function getProductId() { - return (new URLSearchParams(window.location.search).get('product') || '').trim(); + function getOrderId() { + return (new URLSearchParams(window.location.search).get('orderId') || '').trim(); + } + + /** + * Redirects visitors with incomplete checkout links to the help page. + */ + function redirectToGettingHelp() { + window.location.replace('/getting-help'); } /** @@ -238,31 +242,5 @@ $( chargeController.requestIfReady(); } - - /** - * Places the order once and reuses it for all later charge calculations. - * - * @return {Promise} paygate order ID - */ - async function ensureOrderId() { - if (orderId) { - return orderId; - } - - if (!orderPromise) { - orderPromise = purchaseClient - .placeOrder(productId) - .then(response => { - orderId = response.orderId; - return orderId; - }) - .catch(error => { - orderPromise = null; - throw error; - }); - } - - return orderPromise; - } } ); diff --git a/site/assets/js/pages/checkout/view-controller.js b/site/assets/js/pages/checkout/view-controller.js index f6dfec6e..1a1be999 100644 --- a/site/assets/js/pages/checkout/view-controller.js +++ b/site/assets/js/pages/checkout/view-controller.js @@ -36,8 +36,8 @@ * @typedef {Object} CheckoutViewController * @property {function(): void} closeErrorModal * closes the generic checkout error modal - * @property {function(Object): void} fillProductSummary - * fills summary fields with product data + * @property {function(Object): void} fillOrderSummary + * fills summary fields with order data * @property {function(): boolean} isFormHidden * checks whether the checkout form is currently hidden * @property {function(boolean): void} setSubmitDisabled @@ -47,7 +47,7 @@ * @property {function(): void} showErrorModal * opens the generic checkout error modal * @property {function(): void} showNotFoundView - * shows the checkout product-not-found panel + * shows the checkout order-not-found panel * @property {function(): void} showSummaryError * shows the generic checkout summary-error panel * @property {function(Object): void} updateCharges @@ -81,27 +81,27 @@ export function createCheckoutView(dom) { } /** - * Fills the order summary with product details returned by Paygate. + * Fills the order summary with order details returned by Paygate. * - * @param {Object} product paygate product data for the current order + * @param {Object} order paygate order data for the current order */ - function fillProductSummary(product) { - if (!product) { + function fillOrderSummary(order) { + if (!order) { return; } - dom.$productName.text(product.name || 'Unnamed product').prop('hidden', false); + dom.$productName.text(order.productName || 'Unnamed product').prop('hidden', false); - if (product.description) { - dom.$productDescription.text(product.description).prop('hidden', false); + if (order.productDescription) { + dom.$productDescription.text(order.productDescription).prop('hidden', false); } else { dom.$productDescription.text('').prop('hidden', true); } - dom.$subtotalValue.text(formatMoney(product.netAmount)); + dom.$subtotalValue.text(formatMoney(order.netAmount)); dom.$vatLabel.text('VAT'); - dom.$vatValue.text(formatMoney(zeroMoney(product.netAmount.currency))); - dom.$totalValue.text(formatMoney(product.netAmount)); + dom.$vatValue.text(formatMoney(zeroMoney(order.netAmount.currency))); + dom.$totalValue.text(formatMoney(order.netAmount)); } /** @@ -206,7 +206,7 @@ export function createCheckoutView(dom) { return { closeErrorModal, - fillProductSummary, + fillOrderSummary, isFormHidden, setSubmitDisabled, setSummaryLoading, diff --git a/site/layouts/checkout/single.html b/site/layouts/checkout/single.html index e8c53796..6d68ce1c 100644 --- a/site/layouts/checkout/single.html +++ b/site/layouts/checkout/single.html @@ -241,15 +241,15 @@

Something went w - -
- {{ partial "getting-help/mailto-button.html" . }}
diff --git a/site/layouts/_partials/getting-help/services.html b/site/layouts/_partials/getting-help/services.html index 501a954c..35751883 100644 --- a/site/layouts/_partials/getting-help/services.html +++ b/site/layouts/_partials/getting-help/services.html @@ -31,8 +31,6 @@ The Google reCaptcha is used for the payment option. --> - - {{ partial "scripts/body/recaptcha.html" . }} {{ with .Resources.GetMatch "services.json" }} @@ -43,7 +41,8 @@

{{ $data.title | markdownify }}

{{ $comparableServices := $data.comparable_services }} {{ range $comparableServices.header_cols }} -
+

{{ .title | markdownify }}

From 0de01adbbe1dabb4a6a09ecc935968693d996e71 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 5 May 2026 17:51:12 +0200 Subject: [PATCH 4/9] Move params. --- site/assets/js/pages/checkout/index.js | 2 +- site/assets/js/pages/pricing.js | 2 +- site/config/_default/hugo.toml | 7 ++----- site/config/development/hugo.toml | 6 ++---- site/layouts/_partials/scripts/body-scripts.html | 2 -- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/site/assets/js/pages/checkout/index.js b/site/assets/js/pages/checkout/index.js index 43a099d7..e13867e4 100644 --- a/site/assets/js/pages/checkout/index.js +++ b/site/assets/js/pages/checkout/index.js @@ -43,7 +43,7 @@ $( return; } - const purchaseClient = createPurchaseClient(params.paygate.serverurl); + const purchaseClient = createPurchaseClient(params.payment.paygateurl); const orderId = getOrderId(); const view = createCheckoutView(dom); const formController = createCheckoutFormController({dom}); diff --git a/site/assets/js/pages/pricing.js b/site/assets/js/pages/pricing.js index 250f9259..5625fb45 100644 --- a/site/assets/js/pages/pricing.js +++ b/site/assets/js/pages/pricing.js @@ -73,7 +73,7 @@ $( const $linkBack = $redirectScreen.find('#linkBack'); const $consentCheckboxes = $("input[type='checkbox'].consent"); - const purchaseClient = createPurchaseClient(params.paygate.serverurl); + const purchaseClient = createPurchaseClient(params.payment.paygateurl); const reCaptchaSiteKey = params.payment.recaptchakey; const consentUrl = params.payment.consenturl; const productId = ($('[data-paygate-product-id]').attr('data-paygate-product-id') || '') diff --git a/site/config/_default/hugo.toml b/site/config/_default/hugo.toml index fbfaeb14..d671b863 100644 --- a/site/config/_default/hugo.toml +++ b/site/config/_default/hugo.toml @@ -15,13 +15,10 @@ disableKinds = ['taxonomy', 'term'] description = 'Open-source CQRS/ES framework for modern cloud applications' [params.payment] - apiURL = 'https://us-central1-spine-site-server.cloudfunctions.net/paymentTransaction' - orderURL = 'https://secure.2checkout.com/checkout/buy?merchant=999999999589¤cy=EUR&tpl=default&prod=4HJME1JJDF&qty=1' + consentURL = 'https://us-central1-spine-site-server.cloudfunctions.net/api/consent' + paygateURL = 'https://stag.paygate.teamdev.com' reCaptchaKey = '6Lef7b0ZAAAAAHPIbf6XQIzzeyCoSzS56GTej1c0' -[params.paygate] - serverURL = 'https://stag.paygate.teamdev.com' - [params.libs.docsearch] appId = 'DUYV0WFHKV' apiKey = '50ce4ead490484f1436ae042c0a1a4dd' diff --git a/site/config/development/hugo.toml b/site/config/development/hugo.toml index ab1f3487..c5c23c67 100644 --- a/site/config/development/hugo.toml +++ b/site/config/development/hugo.toml @@ -1,5 +1,3 @@ [params.payment] - apiURL = 'http://localhost:5002/spine-site-server/us-central1/paymentTransaction' - -[params.paygate] - serverURL = 'https://stag.paygate.teamdev.com' + consentURL = 'http://localhost:5002/spine-site-server/us-central1/api/consent' + paygateURL = 'https://stag.paygate.teamdev.com' diff --git a/site/layouts/_partials/scripts/body-scripts.html b/site/layouts/_partials/scripts/body-scripts.html index 85783cf8..dd2c48e4 100644 --- a/site/layouts/_partials/scripts/body-scripts.html +++ b/site/layouts/_partials/scripts/body-scripts.html @@ -26,12 +26,10 @@ {{ $environment := hugo.Environment }} {{ $payment := site.Params.payment }} -{{ $paygate := site.Params.paygate }} {{ $params := (dict "environment" $environment "payment" $payment - "paygate" $paygate )}} {{ partial "theme/scripts/body/baseurl.html" . }} From 975556bd934965a075f9b26eea2f78a5fe84cb40 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 6 May 2026 09:48:11 +0200 Subject: [PATCH 5/9] Remove redundant. --- site/assets/js/pages/pricing.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/site/assets/js/pages/pricing.js b/site/assets/js/pages/pricing.js index 5625fb45..1d580432 100644 --- a/site/assets/js/pages/pricing.js +++ b/site/assets/js/pages/pricing.js @@ -148,10 +148,6 @@ $( } if (shouldSubmitConsent) { - if (!consentUrl) { - throw new Error('Consent URL is not configured.'); - } - const token = await getRecaptchaToken(); await submitConsent(buildConsentRequest(orderId, token)); } @@ -177,7 +173,6 @@ $( consent: { privacyConsent: $privacyConsent.prop('checked'), supportAgreementConsent: $supportAgreementConsent.prop('checked'), - newsletterConsent: false }, recaptcha: { token From 468730e857c18e5a594dae2962fbdbfa22ec812a Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 6 May 2026 11:47:22 +0200 Subject: [PATCH 6/9] Improve docs. --- site/assets/js/pages/pricing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/assets/js/pages/pricing.js b/site/assets/js/pages/pricing.js index 1d580432..3c934aaa 100644 --- a/site/assets/js/pages/pricing.js +++ b/site/assets/js/pages/pricing.js @@ -278,7 +278,7 @@ $( } /** - * Redirects to checkout for the created order. + * Redirects to the checkout page for the created order. * * @param {string} orderId paygate order ID */ From f072290fc39b75ace4fae0e863c44b754e9ba38f Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 6 May 2026 16:30:21 +0200 Subject: [PATCH 7/9] Provide prod Paygate address. --- site/config/_default/hugo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/config/_default/hugo.toml b/site/config/_default/hugo.toml index d671b863..0d71eaf5 100644 --- a/site/config/_default/hugo.toml +++ b/site/config/_default/hugo.toml @@ -16,7 +16,7 @@ disableKinds = ['taxonomy', 'term'] [params.payment] consentURL = 'https://us-central1-spine-site-server.cloudfunctions.net/api/consent' - paygateURL = 'https://stag.paygate.teamdev.com' + paygateURL = 'https://paygate.teamdev.com' reCaptchaKey = '6Lef7b0ZAAAAAHPIbf6XQIzzeyCoSzS56GTej1c0' [params.libs.docsearch] From e47de34b01b199e240970f175cf37b9669166b48 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 7 May 2026 11:54:24 +0200 Subject: [PATCH 8/9] Rename product `name` to `title`. --- site/assets/js/modules/paygate/purchases.js | 2 +- site/assets/js/pages/checkout/dom.js | 4 ++-- site/assets/js/pages/checkout/view-controller.js | 2 +- site/layouts/checkout/single.html | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/site/assets/js/modules/paygate/purchases.js b/site/assets/js/modules/paygate/purchases.js index f0fa6bd2..6c10dbb1 100644 --- a/site/assets/js/modules/paygate/purchases.js +++ b/site/assets/js/modules/paygate/purchases.js @@ -47,7 +47,7 @@ * * @typedef {Object} PaygateOrder * @property {string} orderId paygate order ID - * @property {string} productName product display name + * @property {string} productTitle product display title * @property {string} productDescription product description shown on checkout * @property {boolean} paymentCompleted whether the order was already paid */ diff --git a/site/assets/js/pages/checkout/dom.js b/site/assets/js/pages/checkout/dom.js index c4620755..d31bdcaf 100644 --- a/site/assets/js/pages/checkout/dom.js +++ b/site/assets/js/pages/checkout/dom.js @@ -44,7 +44,7 @@ * @property {JQuery} $loadingSpinner summary spinner element * @property {JQuery} $loadingText summary loading text element * @property {JQuery} $loadingSupport summary support text element - * @property {JQuery} $productName product name element + * @property {JQuery} $productTitle product title element * @property {JQuery} $productDescription product description * element * @property {JQuery} $subtotalValue subtotal value element @@ -78,7 +78,7 @@ export function getCheckoutDom() { $loadingSpinner: $('#checkout-summary-loading-spinner'), $loadingText: $('#checkout-summary-loading-text'), $loadingSupport: $('#checkout-summary-support'), - $productName: $('#checkout-product-name'), + $productTitle: $('#checkout-product-title'), $productDescription: $('#checkout-product-description'), $subtotalValue: $('#checkout-subtotal-value'), $vatLabel: $('#checkout-vat-label'), diff --git a/site/assets/js/pages/checkout/view-controller.js b/site/assets/js/pages/checkout/view-controller.js index 1a1be999..e49a6009 100644 --- a/site/assets/js/pages/checkout/view-controller.js +++ b/site/assets/js/pages/checkout/view-controller.js @@ -90,7 +90,7 @@ export function createCheckoutView(dom) { return; } - dom.$productName.text(order.productName || 'Unnamed product').prop('hidden', false); + dom.$productTitle.text(order.productTitle || 'Untitled product').prop('hidden', false); if (order.productDescription) { dom.$productDescription.text(order.productDescription).prop('hidden', false); diff --git a/site/layouts/checkout/single.html b/site/layouts/checkout/single.html index 6d68ce1c..a4fadf00 100644 --- a/site/layouts/checkout/single.html +++ b/site/layouts/checkout/single.html @@ -23,7 +23,7 @@ Something went wrong, please contact {{ $emailLink }}.

- +
Subtotal From 90491eb92379396671eebbac451c73952c24440f Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 7 May 2026 12:28:40 +0200 Subject: [PATCH 9/9] Improve readme. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 13667f71..87c5d6cc 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,18 @@ hugo mod clean --all Then run the `hugo serve` again. +### Testing locally + +#### Payments +The `getting-help` page contains products for sale. + +It is possible to test the product checkout flow. +The development environment provides a connection to the staging Paygate server, +which allows testing of the full checkout process, including payment. + +To pay for a product, it is necessary to use +[LHV sandbox cards](https://merchant.lhv.ee/help/en/articles/12807566-test-cards). + ## Documentation The documentation is located in a [separate repository][documentation-repo].