broadleaf:
cartoperation:
service:
checkout:
checkout-payment-method-options:
- payment-method-type: KNET
payment-method-gateway-type: CHECKOUT_COM
KNET payments processed by Checkout.com will always navigate the customer to KNET hosted page where they will enter their debit card details securely. When this page is submitted, an AuthorizeAndCapture transaction is executed immediately. From there, the Broadleaf system must be made aware of the transaction results in order to finalize the checkout attempt.
To validate the cart’s checkout-readiness & gather the url for the hosted payment page, this interaction should start with an attempt to submit checkout. This will cause the cart to be evaluated via the checkout workflow’s validation activities, followed by a payment failure including a CheckoutResponse#failureType
of PAYMENT_REQUIRES_EXTERNAL_VERIFICATION
, a related payment transaction failure detail (CheckoutResponse#paymentTransactionFailureDetails
) will have a failureType
of REQUIRES_HOSTED_PAYMENT_PAGE_INTERACTION
& a populated NextAction#redirectUrl
containing the redirect URL provided by Checkout.com. The user’s browser should be redirected to this location to render the hosted payment page.
Once the hosted payment page has been submitted, an AuthorizeAndCapture transaction is executed. At this point, a second checkout request should be submitted, where we’ll attempt to lookup the external transaction results from Checkout.com, & continue the checkout process if the transaction was successful. Simultaneously, we also leverage webhook events as a secondary means of gathering & recording transaction results asynchronously.
Before configuring KNET with Checkout.com, you’ll first need to set up your environment for the overall Checkout.com integration as described in the Environment Setup Guide.
Add the following properties to declare the CHECKOUT_COM
gateway as an available payment method.
broadleaf:
cartoperation:
service:
checkout:
checkout-payment-method-options:
- payment-method-type: KNET
payment-method-gateway-type: CHECKOUT_COM
Note
|
KNET only supports the KWD currency, so make sure to only enable this payment method in a context that also uses KWD. |
CHECKOUT_COM
should be added to the following property to declare when transactions for this gateway should be executed, relative to other gateways, during checkout processing.
broadleaf:
cartoperation:
service:
checkout:
payment-gateway-priorities:
- ...
- CHECKOUT_COM
- ...
Since Checkout.com KNET interactions always result in an AuthorizeAndCapture transaction, the checkout transaction type must be declared in the following way:
broadleaf:
cartoperation:
service:
checkout:
checkout-transaction-types:
CHECKOUT_COM.KNET: AUTHORIZE_AND_CAPTURE
To enable this transaction lookup in the checkout workflow, add the following required property:
broadleaf:
cartoperation:
service:
checkout:
checkout-lookup-external-transactions-enabled:
CHECKOUT_COM: true
The following permissions must be defined for Checkout.com KNET:
-- EXTERNAL_TRANSACTION_RESULT
INSERT INTO auth.blc_security_scope (id, "name", "open") VALUES('-1800', 'EXTERNAL_TRANSACTION_RESULT', 'N');
INSERT INTO auth.blc_permission_scope(id, permission, is_permission_root, scope_id) VALUES('-1800', 'EXTERNAL_TRANSACTION_RESULT', 'Y', '-1800');
INSERT INTO auth.blc_permission_scope(id, permission, is_permission_root, scope_id) VALUES('-920', 'EXTERNAL_TRANSACTION_RESULT', 'Y', '-100');
INSERT INTO auth.blc_user_permission(id, archived, last_updated, "name") VALUES('-1100', 'N', '1970-01-01 00:00:00.000', 'ALL_EXTERNAL_TRANSACTION_RESULT');
INSERT INTO auth.blc_client_scopes(id, "scope") VALUES('anonymous', 'EXTERNAL_TRANSACTION_RESULT');
INSERT INTO auth.blc_client_scopes(id, "scope") VALUES('cartopsclient', 'EXTERNAL_TRANSACTION_RESULT');
INSERT INTO auth.blc_client_permissions(id, permission) VALUES('anonymous', 'ALL_EXTERNAL_TRANSACTION_RESULT');
INSERT INTO auth.blc_client_permissions(id, permission) VALUES('cartopsclient', 'ALL_EXTERNAL_TRANSACTION_RESULT');
INSERT INTO auth.blc_role_permission_xref (role_id, permission_id) VALUES ('-100', '-1100');
If you’re consuming the openapi
client for use with OpenAPI, the following permission updates are necessary:
INSERT INTO auth.blc_client_scopes (id, scope) VALUES ('openapi', 'EXTERNAL_TRANSACTION_RESULT');
INSERT INTO auth.blc_client_permissions (id, permission) VALUES ('openapi', 'ALL_EXTERNAL_TRANSACTION_RESULT');
Note
|
Based on the Auth data configured in your environment, you may need modify the ids defined in the scripts above. |
When a KNET payment method is used, the payment section of the checkout flow doesn’t have any information to gather from the customer, but a PaymentTransactionServices Payment should still be created.
Note
|
Data about the payment method (i.e. the debit card details) will be collected by the hosted payment page after checkout submission. |
import { FC, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { ApiError } from '@broadleaf/commerce-core';
import { useSubmitPaymentRequest } from '@broadleaf/payment-react';
import { useCartContext, usePaymentsContext } from '@app/cart/contexts';
import {
useGetPaymentAmount,
useRemainingTotalToPay,
} from '@app/checkout/hooks';
import { usePaymentClient } from '@app/common/contexts';
import { usePaymentAuthState } from '@app/common/hooks';
type CheckoutComKnetPaymentFormType = FC & {
TYPE: 'CHECKOUT_COM';
};
export const CheckoutComKnetPaymentForm: CheckoutComKnetPaymentFormType =
() => {
const router = useRouter();
const { isSubmitting, onSubmit, error } = useHandleSubmit();
const {
payments: cartPayments,
refetchPayments,
isStale,
setIsStale,
} = usePaymentsContext();
const remainingTotalToPay = useRemainingTotalToPay();
useEffect(() => {
refetchPayments().then(() => {
setIsStale(false);
});
}, [refetchPayments, setIsStale]);
return (
<>
<div className="my-2">
The payment data will be collected when the order is submitted.
</div>
{error && (
<strong className="block my-4 text-red-600 text-lg font-normal">
Error
</strong>
)}
<button type="submit">Submit</button>
</>
);
};
type UseHandleSubmitResponse = {
isSubmitting: boolean;
onSubmit: () => Promise<void>;
error?: ApiError;
};
const useHandleSubmit = (): UseHandleSubmitResponse => {
const cartState = useCartContext();
const { cart } = cartState;
const { payments } = usePaymentsContext();
const paymentClient = usePaymentClient();
const authState = usePaymentAuthState();
const { handleSubmitPaymentInfo } = useSubmitPaymentRequest({
authState,
payments,
ownerId: cart?.id,
paymentClient,
multiplePaymentsAllowed: true,
rejectOnError: true,
});
const getPaymentAmount = useGetPaymentAmount({
gatewayType: 'CHECKOUT_COM',
});
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [error, setError] = useState<ApiError>();
const onSubmit = async (): Promise<void> => {
setIsSubmitting(true);
const protocol = window.location.protocol.slice(0, -1);
const host = window.location.host;
// these are required to pass to the backend as part of the payment
const paymentMethodProperties = {
success_url: `${protocol}://${host}/checkout/external-payment-verification?gatewayType=CHECKOUT_COM`,
failure_url: `${protocol}://${host}/checkout/external-payment-verification?external-payment-verification-success=false`,
};
const amount = getPaymentAmount(cart);
const paymentRequest = {
name: 'KNET',
type: 'KNET',
gatewayType: 'CHECKOUT_COM',
amount,
isSingleUsePaymentMethod: true,
paymentMethodProperties,
displayAttributes: {},
} as unknown as PaymentRequest;
try {
const paymentSummary = await handleSubmitPaymentInfo(paymentRequest);
...
} catch (err) {
console.error('There was an error adding payment information', err);
setError(err);
} finally {
...
}
};
return { isSubmitting, error, onSubmit };
};
To execute Checkout.com KNET transactions via PaymentTransactionServices, we must first create a Payment in PaymentTransactionServices.
We expect the frontend to provide a success_url & failure_url in paymentMethodProperties, and a payment type of KNET. The request payload should include the following:
{ ... gatewayType: "CHECKOUT_COM", type: "KNET", paymentMethodProperties: { "success_url": "https://${host}/checkout/external-payment-verification?gatewayType=CHECKOUT_COM", "failure_url": "https://${host}/checkout/external-payment-verification?external-payment-verification-success=false" }, isSingleUsePaymentMethod: true, ... }
This code snippet is meant to serve as an example of how to submit checkout to verify the checkout-readiness of the cart, followed by how to gather the KNET hosted payment page url & navigate the user’s browser.
import { FC, useEffect } from 'react';
import { useRouter } from 'next/router';
import { isEmpty, get, find } from 'lodash';
import { useCartContext, usePaymentsContext } from '@app/cart/contexts';
import {
useHandleSubmitCart,
} from '@app/checkout/hooks';
const CheckoutReview: FC = () => {
const router = useRouter();
const { query } = router;
const { cart, resolving } = useCartContext();
const { payments } = usePaymentsContext();
const { error, submitting, onSubmit } = useHandleSubmitCart();
const externalVerificationResult = query['external-payment-verification-success'];
useEffect(() => {
if (externalVerificationResult === 'true') {
if (!resolving && cart && !submitting) {
// submit the order if the payment result is successful
onSubmit();
}
if (error) {
delete router.query['external-payment-verification-success'];
router.replace(router, undefined, { shallow: true });
}
}
}, [
externalVerificationResult,
router,
onSubmit,
cart,
error,
resolving,
submitting,
]);
useEffect(() => {
if (
!isEmpty(payments?.content) &&
get(error, 'failureType') === 'PAYMENT_REQUIRES_EXTERNAL_INTERACTION'
) {
const externalVerification = find(
get(error, 'paymentTransactionFailureDetails'),
({ failureType }) =>
failureType === 'REQUIRES_HOSTED_PAYMENT_PAGE_INTERACTION'
);
const paymentId = get(externalVerification, 'paymentId');
const payment = find(payments.content, ['paymentId', paymentId]);
const redirectUrl = get(externalVerification, 'nextAction.redirectUrl');
if (payment.type === 'KNET' && redirectUrl) {
// redirect to the KNET verification page beacause KNET doesn't allow using the iframe
router.push(redirectUrl);
}
}
}, [error, payments, router]);
const externalInteractionVerificationFailedError =
router.query['external-payment-verification-success'] === 'false'
? 'The payment verification failed. Please try a different form of payment.'
: undefined;
const externalInteractionTransactionFailedError =
router.query['external-payment-success'] === 'false'
? 'General error'
: undefined;
return (
<div>
{error && <div>{...}</div>}
{externalInteractionVerificationFailedError && (
<strong className="block my-4 text-red-600 text-lg font-normal">
{externalInteractionVerificationFailedError}
</strong>
)}
{externalInteractionTransactionFailedError && (
<strong className="block my-4 text-red-600 text-lg font-normal">
{externalInteractionTransactionFailedError}
</strong>
)}
<button type="submit">Submit</button>
</div>
);
};
This snippet is meant to serve as an example of how to handle the return from the KNET hosted payment page, & how to attempt finalizing the checkout attempt following the payment interaction.
import React, { FC, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { isEmpty, set } from 'lodash';
import { useHandleExternalTransactionResult } from '@broadleaf/payment-react';
import { usePaymentClient } from '@app/common/contexts';
import { usePaymentAuthState } from '@app/common/hooks';
const ExternalPaymentVerification: FC = () => {
const router = useRouter();
const { query } = router;
const paymentClient = usePaymentClient();
const authState = usePaymentAuthState();
const [transactionResult, setTransactionResult] =
useState<Record<string, unknown>>();
const { handleExternalTransactionResult, error } =
useHandleExternalTransactionResult({
paymentClient,
authState,
});
const protocol = window.location.protocol.slice(0, -1);
const host = window.location.host;
const search = window.location.search;
const externalSuccessRedirectUrl = `${protocol}://${host}/checkout/review?external-payment-verification-success=true`;
const externalFailureRedirectUrl = `${protocol}://${host}/checkout/review?external-payment-verification-success=false`;
const transactionFailureRedirectUrl = `${protocol}://${host}/checkout/review?external-payment-success=false`;
const externalVerificationResult = query['external-payment-verification-success'];
useEffect(() => {
if (externalVerificationResult !== 'false') {
const { gatewayType, ...request } = parseParams(search);
handleExternalTransactionResult(gatewayType, request).then(response => {
setTransactionResult(response);
});
} else {
redirectTo(externalFailureRedirectUrl);
}
}, [
externalFailureRedirectUrl,
handleExternalTransactionResult,
router,
search,
externalVerificationResult,
]);
const transactionStatus =
transactionResult?.transactionExecutionDetails[0]?.transactionStatus;
if (
transactionResult &&
transactionStatus !== 'SUCCESS' &&
transactionStatus !== 'AWAITING_ASYNC_RESULTS'
) {
redirectTo(transactionFailureRedirectUrl);
}
if (error) {
/*
if there's an error, we'll still submit the order after the timeout to leverage other
verification fall back options, The timeout is to ensure
webhooks would be processed by the time we re-submit the order
*/
console.error(
'Error encountered while verifying transaction result: ',
error
);
setTimeout(() => redirectAndSubmitOrder(externalSuccessRedirectUrl), 3000);
}
if (
transactionStatus === 'SUCCESS' ||
transactionStatus === 'AWAITING_ASYNC_RESULTS'
) {
redirectAndSubmitOrder(externalSuccessRedirectUrl);
}
return <div>Loading...</div>;
};
const parseParams = (search?: string): Record<string, never> => {
if (isEmpty(search)) {
return {};
}
search = search?.startsWith('?') ? search.slice(1) : search;
return search
?.split('&')
.map(param => {
// eslint-disable-next-line prefer-const
let [key, value] = param.split('=');
key = decodeURIComponent(key);
return { key, value };
})
.reduce((accumulator, { key, value }) => {
set(accumulator, key, value);
return accumulator;
}, {});
};
const redirectAndSubmitOrder = verificationSuccessRedirectUrl => {
window.location.href = verificationSuccessRedirectUrl;
};
const redirectTo = redirectUrl => {
window.location.href = redirectUrl;
};