broadleaf:
cartoperation:
service:
checkout:
checkout-payment-method-options:
- payment-method-type: KNET
payment-method-gateway-type: MY_FATOORAH
KNET payments processed by MyFatoorah 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 MyFatoorah. 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 MyFatoorah, & 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 MyFatoorah, you’ll first need to set up your environment for the overall MyFatoorah integration as described in the Environment Setup Guide.
Add the following properties to declare the MY_FATOORAH
gateway as an available payment method.
broadleaf:
cartoperation:
service:
checkout:
checkout-payment-method-options:
- payment-method-type: KNET
payment-method-gateway-type: MY_FATOORAH
Note
|
KNET only supports the KWD currency, so make sure to only enable this payment method in a context that also uses KWD. |
MY_FATOORAH
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:
- ...
- MY_FATOORAH
- ...
Since MyFatoorah 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:
MY_FATOORAH.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:
MY_FATOORAH: true
The following permissions must be defined for MyFatoorah 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. |
To execute MyFatoorah KNET transactions via PaymentTransactionServices, we must first create a Payment in PaymentTransactionServices.
We expect the frontend to declare the KNET payment method id provided by the MyFatoorah Initiate Payment endpoint via the paymentMethodProperties
map. The request payload should include the following:
{ ... gatewayType: "MY_FATOORAH", type: "KNET", paymentMethodProperties: { "paymentMethodId": "KNET payment method id" }, isSingleUsePaymentMethod: true, ... }
Note
|
Also see how to add a payment via the Commerce SDK. |
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;
};