Broadleaf Microservices
  • v1.0.0-latest-prod

Executing MyFatoorah KNET Payment

The Big Picture

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.

Before the Hosted Payment Page

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.

After 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.

Preparing Your Environment

Prerequisites

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.

CartOperationService Configuration

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

AuthenticationService Data Configuration

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.

Frontend Integration

Payment Stage of the Checkout Flow

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.

Preparing a PaymentTransactionServices Payment for MyFatoorah KNET Transactions

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.

Example Order Submission to Engage the Hosted Payment Page

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>
  );
};

Example Handling of KNET Hosted Payment Page Submission

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;
};