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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
20 changes: 10 additions & 10 deletions site/assets/js/modules/paygate/purchases.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@
*/

/**
* Paygate product data returned by the product endpoint.
* Paygate order data returned by the purchase endpoint.
*
* @typedef {Object} PaygateProduct
* @property {string} id product identifier
* @property {string} name product display name
* @property {string} description product description shown on checkout
* @property {PaygateMoney} netAmount product price before VAT
* @typedef {Object} PaygateOrder
* @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
*/

/**
Expand Down Expand Up @@ -158,8 +158,8 @@
* Paygate purchase endpoint methods used by checkout.
*
* @typedef {Object} PaygatePurchaseClient
* @property {function(string): Promise<PaygateProduct>} getProduct
* loads checkout product data by product ID
* @property {function(string): Promise<PaygateOrder>} getOrder
* loads checkout order data by order ID
* @property {function(string): Promise<PlaceOrderResponse>} placeOrder
* creates a checkout order for the given product ID
* @property {function(CalculateChargesRequest): Promise<CalculateChargesResponse>} calculateCharges
Expand All @@ -176,8 +176,8 @@
*/
export function createPurchaseClient(serverUrl) {
return {
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});
Expand Down
6 changes: 3 additions & 3 deletions site/assets/js/pages/checkout/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
* @property {JQuery<HTMLElement>} $loadingSpinner summary spinner element
* @property {JQuery<HTMLElement>} $loadingText summary loading text element
* @property {JQuery<HTMLElement>} $loadingSupport summary support text element
* @property {JQuery<HTMLElement>} $productName product name element
* @property {JQuery<HTMLElement>} $productTitle product title element
* @property {JQuery<HTMLElement>} $productDescription product description
* element
* @property {JQuery<HTMLElement>} $subtotalValue subtotal value element
Expand All @@ -53,7 +53,7 @@
* @property {JQuery<HTMLElement>} $totalValue total amount element
* @property {JQuery<HTMLButtonElement>} $submitButton checkout submit button
* @property {JQuery<HTMLElement>} $errorModal generic checkout error modal
* @property {JQuery<HTMLElement>} $notFound product-not-found result panel
* @property {JQuery<HTMLElement>} $notFound order-not-found result panel
* @property {JQuery<HTMLElement>} $summaryError generic checkout summary-error panel
* @property {HTMLFormElement} form native checkout form element
*/
Expand All @@ -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'),
Expand Down
66 changes: 22 additions & 44 deletions site/assets/js/pages/checkout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,16 @@ $(
return;
}

const purchaseClient = createPurchaseClient(params.paygate.serverurl);
const productId = getProductId();
const purchaseClient = createPurchaseClient(params.payment.paygateurl);
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 => {
Expand All @@ -64,17 +62,16 @@ $(
logApiError
});

if (!productId) {
chargeController.invalidate();
view.showNotFoundView();
if (!orderId) {
redirectToGettingHelp();
return;
}

dom.$form.prop('hidden', true);
formController.updatePhoneCountryDisplay();
formController.bindPhoneEvents();
chargeController.updateSubmitState();
loadProduct();
loadOrder();
bindEvents();

/**
Expand Down Expand Up @@ -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<void>} resolves when the initial product load flow finishes
* @return {Promise<void>} 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();
Expand Down Expand Up @@ -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');
}

/**
Expand Down Expand Up @@ -238,31 +242,5 @@ $(

chargeController.requestIfReady();
}

/**
* Places the order once and reuses it for all later charge calculations.
*
* @return {Promise<string>} 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;
}
}
);
28 changes: 14 additions & 14 deletions site/assets/js/pages/checkout/view-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.$productTitle.text(order.productTitle || 'Untitled 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));
}

/**
Expand Down Expand Up @@ -206,7 +206,7 @@ export function createCheckoutView(dom) {

return {
closeErrorModal,
fillProductSummary,
fillOrderSummary,
isFormHidden,
setSubmitDisabled,
setSummaryLoading,
Expand Down
Loading
Loading