Broadleaf Microservices
  • v1.0.0-latest-prod

Klarna with Adyen

Integration overview

In your checkout flow, the Adyen Drop-in UI can be used to easily setup Klarna. The overall flow using Klarna reflects a typical hosted payment page interaction. (Note: this is the same pattern that’s described in the Adyen 3DS document.) When Klarna is selected via the Drop-in UI, trigger a checkout attempt & execute a payment transaction. We expect this response to prompt us with an additional action. From there, provide the action payload to the Adyen Drop-in UI, which will direct the user to Klarna’s UI. Once this interaction is successfully completed, a transaction is executed. From there, we use both webhooks & the CartOps callback endpoint (ExternalPaymentTransactionCallbackEndpoint) to gather transaction results, & attempt to finalize the cart.

One of the unique aspects of the Klarna integration is that it requires line item (i.e. cart item) data to be passed with the session creation & transaction payloads. This involves the frontend building line item data, which is included in the checkout submission request, so that it can be included in the initial Authorize or AuthorizeAndCapture transaction. From there, OrderOperationServices takes on the responsibility for communicating line items for Capture & Refund transactions.

Note
For the initial Authorize or AuthorizeAndCapture transaction, Adyen/Klarna validates that the sum of the item totals matches the overall transaction total. Therefore, it’s very important to account for every factor that can impact the item price including discounts, fulfillment costs, tax, etc. Later, when executing partial captures or partial refunds, you once again must pass line item data, but the validation around item totals is more lenient.

Adyen Configuration

For more information on how to Klarna with your Adyen merchant(s), see the following Adyen documents:

Broadleaf Configuration

As mentioned above, when capturing or refunding due to OrderOperationServices actions (e.g. fulfilling or returning an item), line item data must be provided with the request payload. To enable this for the Adyen-Klarna integration, the following property must be declared for OrderOperationServices:

broadleaf.orderoperation.service.payment.include-line-items-in-transaction-request.ADYEN.KLARNA=true

Frontend Integration

This guide generally builds on top of the Drop-in integration described here, & focuses on unique aspects required to support Klarna.

Payment Method Configuration

For Klarna, there’s no required specific payment method configuration is required for the Drop-in UI.

Creating Line Items for Session Creation & Checkout Submission Requests

export const useGetLineItems = () => {
  const handleMoneyAddition = useHandleMoneyAddition();
  const handleMoneySubtraction = useHandleMoneySubtraction();
  const formatAmountInMinorUnits = useFormatAmountInMinorUnits();

  return useEventCallback((cart: Cart) => {
    const cartItemById = groupBy(cart.cartItems, 'id');
    return cart.fulfillmentGroups[0].fulfillmentItems.map(fi => {
      const fulfillmentCosts = {
        amount: handleMoneyAddition(
          fi.fulfillmentTotal,
          get(fi, 'proratedFulfillmentCharge')
        ),
        currency: fi.merchandiseTotalAmount.currency,
      };
      const fulfillmentCostWithAdjustmentsApplied = {
        amount: handleMoneySubtraction(
          fulfillmentCosts,
          fi.proratedFulfillmentGroupAdjustments
        ),
        currency: fi.merchandiseTotalAmount.currency,
      };
      let merchandiseAndFulfillmentTax = {
        amount: 0,
        currency: fi.merchandiseTotalAmount.currency,
      };
      fi.fulfillmentItemTaxDetails.forEach(taxDetail => {
        merchandiseAndFulfillmentTax = {
          amount: handleMoneyAddition(
            taxDetail.taxCalculated,
            merchandiseAndFulfillmentTax
          ),
          currency: fi.merchandiseTotalAmount.currency,
        };
      });
      const withTax = {
        amount: handleMoneyAddition(
          fulfillmentCostWithAdjustmentsApplied,
          merchandiseAndFulfillmentTax
        ),
        currency: fi.merchandiseTotalAmount.currency,
      };
      let finalAmount = withTax;

      if (fi.merchandiseTaxableAmount) {
        finalAmount = {
          amount: handleMoneyAddition(withTax, fi.merchandiseTaxableAmount),
          currency: fi.merchandiseTotalAmount.currency,
        };
      }
      return {
        description: cartItemById[fi.cartItemId][0].name,
        sku: cartItemById[fi.cartItemId][0].sku,
        quantity: fi.quantity,
        amountIncludingTax: formatAmountInMinorUnits(finalAmount),
        imageUrl: cartItemById[fi.cartItemId][0].imageAsset?.contentUrl,
      };
    });
  }, []);
};
Note
This line item amount calculation incorporates merchandise costs, fulfillment costs, discounts (including order-level discounts, item-level discounts, & fulfillment discounts), & taxes (both merchandise & fulfillment tax).

Adding Line Items to the Session Creation Request

...
      const request = {
        amount,
        reference: cart.id,
        returnUrl,
        countryCode,
        //hardcode, backend will know what to do with that when customer logged in
        recurringProcessingModel: 'CardOnFile',
        storePaymentMethodMode: 'askForConsent',
        shopperEmail: authState.isAuthenticated
          ? authState.customerEmail
          : cart.emailAddress,
        lineItems: lineItems,
      } as CreatePaymentSessionRequest;
...

Handling the Checkout Submission Request

export const useHandleSubmitAdyenPayment = (
  props: HandleSubmitProps = {}
): HandleSubmitResponse => {
  const { setError = noop } = props;
  const cartState = useCartContext();
  const { cart } = cartState;
  const paymentClient = usePaymentClient();
  const authState = usePaymentAuthState();
  const { handleSubmitPaymentInfo } = useSubmitPaymentRequest({
    authState,
    payments: undefined,
    ownerId: cart?.id,
    owningUserEmailAddress: cart.emailAddress,
    paymentClient,
    multiplePaymentsAllowed: false,
    rejectOnError: true,
  });

  const getPaymentAmount = useGetPaymentAmount({
    gatewayType: AdyenPaymentForm.TYPE,
  });

  const getPaymentCallbackUrl = useGetPaymentCallbackUrl();

  const { error, onSubmit: submitCart } = useHandleSubmitCart();

  const [adyenComponent, setAdyenComponent] = useState<UIElement>();

  useEffect(() => {
    if (!adyenComponent) {
      return;
    }

    const errorType = get(error, 'failureType');
    if (
      errorType === 'PAYMENT_REQUIRES_3DS_VERIFICATION' ||
      errorType === 'PAYMENT_REQUIRES_EXTERNAL_INTERACTION' ||
      errorType === 'PAYMENT_REQUIRES_HOSTED_PAYMENT_PAGE_INTERACTION'
    ) {
      const errorDetails = find(
        get(error, 'paymentTransactionFailureDetails'),
        ({ failureType }) =>
          failureType === 'REQUIRES_3DS_VERIFICATION' ||
          failureType === 'REQUIRES_EXTERNAL_INTERACTION' ||
          failureType === 'REQUIRES_HOSTED_PAYMENT_PAGE_INTERACTION'
      );

      const action = get(errorDetails, 'nextAction.attributes');

      adyenComponent.handleAction(action);
    } else if (errorType && error) {
      setError(error);
    }
  }, [error, adyenComponent, setError]);

  const getLineItems = useGetLineItems();

  return useEventCallback(
    async ({
      state,
      paymentType,
      component,
      sessionId,
      paymentData,
    }: HandleSubmitParameters) => {
      setError(undefined);
      const data = get(state, 'data');

      setAdyenComponent(component);

      const billingAddress = getBillingAddress(state, paymentType, paymentData);

      // these are required to pass to the backend as part of the payment
      const returnUrl = getPaymentCallbackUrl({
        gatewayType: AdyenPaymentForm.TYPE,
      });

      const amount = getPaymentAmount(cart);
      const paymentRequest = {
        name: AdyenPaymentForm.TYPE,
        type: paymentType,
        gatewayType: AdyenPaymentForm.TYPE,
        amount,
        subtotal: cart.cartPricing.subtotal,
        adjustmentsTotal: cart.cartPricing.adjustmentsTotal,
        fulfillmentTotal: cart.cartPricing.fulfillmentTotal,
        taxTotal: cart.cartPricing.totalTax,
        isSingleUsePaymentMethod: true,
        shouldArchiveExistingPayments: true,
        paymentMethodProperties: {
          sessionId,
          returnUrl,
        },
        billingAddress,
      } as PaymentRequest;

      let paymentSummary;

      try {
        paymentSummary = await handleSubmitPaymentInfo(paymentRequest);

        if (paymentSummary) {
          pushGtmAddPayment(cart, paymentSummary);

          const countryCode = cart.fulfillmentGroups[0]?.address?.country;
          const lineItems = getLineItems(cart);

          const checkoutResponse = await submitCart({
            sensitivePaymentMethodData: [
              {
                paymentId: paymentSummary.paymentId,
                paymentMethodProperties: {
                  ADYEN_PAYMENT_DATA: {
                    ...data,
                    shopperEmail: authState.isAuthenticated
                      ? authState.customerEmail
                      : cart.emailAddress,
                    lineItems,
                    countryCode: countryCode,
                  },
                },
              },
            ],
          });

          return checkoutResponse?.paymentSummaries[0];
        }
      } catch (err) {
        console.error('There was an error adding payment information', err);
        setError(err);
      }
    },
    []
  );
};
Note
  • Notice the line items being included in the checkout submission request.

  • Notice the handling of the PAYMENT_REQUIRES_EXTERNAL_INTERACTION checkout failure type, leading to providing the action payload to the Drop-in component. This triggers the Klarna UI interaction.

Creating a PaymentTransactionServices Payment

When the Drop-in UI’s onSubmit action is triggered, we must first create a Payment in PaymentTransactionServices, before submitting checkout.

An example of the request to create a Klarna-based Payment:

{
  type: 'KLARNA',
  gatewayType: `ADYEN`,
  amount: {
      amount: 10,
      currency: 'USD',
  },
  isSingleUsePaymentMethod: true,
  paymentMethodProperties: {
    sessionId: '{Adyen session ID}',
    returnUrl: 'https://storefront.com/api/cart-operations/checkout/{cart_id}/payment-callback/ADYEN?tenantId={tenant_id}&applicationId={application_id}',
  },
  billingAddress: {...},
}
Note
  • This request assumes that you’ve already created an Adyen session.

  • Notice that KLARNA is declared via the payment type property.

  • See how to add a payment via the Commerce SDK.