Vue Storefront is now Alokai! Learn More
Cart normalizer

Cart normalizer

The normalizeCart function is used to map a SFCC Basket into the unified SfCart data model.

Parameters

NameTypeDefault valueDescription
basketBasketSFCC Cart
ctxNormalizerContextContext needed for the normalizer. Context contain a currency field that contains a currency code

Extending

The SfCart structure is returned from all Unified Cart Methods such as GetCart, AddCartLineItem, and UpdateCartLineItem. If the SfCart structure doesn't contain the information you need for your Storefront, you can extend its logic using the defineNormalizers function. The following example demonstrates how to extend SfCart with an lastModification field.

import { normalizers as normalizersSFCC, defineNormalizers } from "@vsf-enterprise/unified-api-sfcc";

const normalizers = defineNormalizers<typeof normalizersSFCC>()({
  ...normalizersSFCC,
  normalizeCart: (basket, context) => ({
    ...normalizersSFCC.normalizeCart(basket, context),
    lastModification: basket.lastModified,
  }),
});

You can override the normalizeCart, but it's also available to override the smaller normalizers such as normalizeCartCoupon, normalizeShippingMethod, normalizeCartLineItem.

Source

The normalizeCart function consists of several smaller normalizers such as normalizeCartCoupon, normalizeShippingMethod, and more.

cart.ts
/* eslint-disable complexity */
import type { NormalizerContext } from "@/normalizers/types";
import { ProductItem } from "@internal";
import { maybe } from "@shared/utils";
import { Basket, BasketAddress } from "@vsf-enterprise/sfcc-types";
import type { Maybe, SfAddress, SfCart, SfCartLineItem } from "@vue-storefront/unified-data-model";
import { defineNormalizer } from "../defineNormalizer";

/**
 * @link Reference: https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=type:Basket
 */
export const normalizeCart = defineNormalizer.normalizeCart((basket, ctx) => {
  if (!basket.basketId || !basket.currency) {
    throw new Error("Basket must have an id, currency");
  }
  // In SAPCC basket currency is static and can't be changed.
  ctx.currency = basket.currency;
  const { normalizeCartCoupon, normalizeCartLineItem, normalizeMoney } = ctx.normalizers;
  const totalTax = getTotalTax(basket, ctx);
  const totalShippingPrice = basket.shippingTotal ? normalizeMoney(basket.shippingTotal) : null;
  const totalPrice = getTotalPrice(basket, ctx);
  const totalItems = getTotalItems(basket);
  const shippingAddress = getAddress(basket.shipments?.[0]?.shippingAddress, ctx);
  const billingAddress = getAddress(basket.billingAddress, ctx);
  const appliedCoupons = basket.couponItems?.map((coupon) => normalizeCartCoupon(coupon)) ?? [];
  const lineItems = basket.productItems?.map((item) => normalizeCartLineItem(item)) ?? [];
  const shippingMethod = getShippingMethod(basket, ctx);
  const totalCouponDiscounts = basket.productItems
    ? getTotalDiscounts(basket.productItems, ctx)
    : normalizeMoney(0);
  const subtotalRegularPrice = getSubtotalRegularPrice(lineItems, ctx);
  const subtotalDiscountedPrice = getSubtotalDiscountedPrice(basket.productItems, ctx);

  return {
    appliedCoupons,
    billingAddress,
    customerEmail: maybe(basket.customerInfo?.email || undefined),
    id: basket.basketId,
    lineItems,
    shippingAddress,
    shippingMethod,
    subtotalDiscountedPrice,
    subtotalRegularPrice,
    totalCouponDiscounts,
    totalItems,
    totalPrice,
    totalShippingPrice,
    totalTax,
  };
});

/**
 * taxTotal is undefined until the shipping method is selected, therefore we use adjustedMerchandizeTotalTax as a fallback.
 */
function getTotalTax(basket: Basket, ctx: NormalizerContext): SfCart["totalTax"] {
  const amount = basket.taxTotal ?? basket.adjustedMerchandizeTotalTax ?? 0;
  return ctx.normalizers.normalizeMoney(amount);
}

/**
 * orderTotal is undefined until the shipping method is selected, therefore we use productTotal as a fallback.
 */
function getTotalPrice(basket: Basket, ctx: NormalizerContext): SfCart["totalPrice"] {
  const shippingPrice = basket.shippingTotal ?? 0;
  const amount = basket.orderTotal || (basket.productTotal ?? 0) + shippingPrice;
  return ctx.normalizers.normalizeMoney(amount);
}

function getAddress(address: BasketAddress | undefined, ctx: NormalizerContext): Maybe<SfAddress> {
  return address ? ctx.normalizers.normalizeAddress(address) : null;
}

/**
 * SFCC always create one shipment which is available by default. Its id is "me". Although SFCC supports splitting product items into multiple shipments, we don't support it yet.
 * @link Reference: https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=type:Shipment
 */
function getShippingMethod(basket: Basket, ctx: NormalizerContext): SfCart["shippingMethod"] {
  const unnormalizedShippingMethod = basket.shipments?.[0]?.shippingMethod;
  return unnormalizedShippingMethod
    ? ctx.normalizers.normalizeShippingMethod(unnormalizedShippingMethod)
    : null;
}

/**
 * ProductItem may include different types of discounts - coupons, promotions, manual discounts etc. All of them are sum up in priceAfterOrderDiscount.
 * To get discount components, you can iterate through priceAdjustments array.
 * @link Reference: https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=type%3AProductItem
 */
function getTotalDiscounts(
  items: ProductItem[],
  ctx: NormalizerContext,
): SfCart["totalCouponDiscounts"] {
  const amount = items.reduce((acc, product) => {
    if (!product.price || !product.priceAfterOrderDiscount) return acc;

    const discount = product.price - product.priceAfterOrderDiscount;
    return acc + discount;
  }, 0);

  return ctx.normalizers.normalizeMoney(amount);
}

function getSubtotalRegularPrice(
  lineItems: SfCartLineItem[],
  ctx: NormalizerContext,
): SfCart["subtotalRegularPrice"] {
  const lineItemsAmount = lineItems.reduce(
    (acc, item) => acc + (item.unitPrice?.regularPrice.amount || 0) * item.quantity,
    0,
  );

  return ctx.normalizers.normalizeMoney(+lineItemsAmount.toFixed(2));
}

/**
 * Returns cumulated `productItem[].price` value, which is the price of the line item before applying any adjustments (e.g. coupons) or shipping charges.
 * Equals to purchasable price of the product - Sale pricebook value (if exists) or List pricebook value.
 * @link Reference: https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getBasket
 */
function getSubtotalDiscountedPrice(
  productItems: Basket["productItems"] = [],
  ctx: NormalizerContext,
): SfCart["subtotalDiscountedPrice"] {
  const amount = productItems.reduce((acc, item) => acc + (item.price || 0), 0);

  return ctx.normalizers.normalizeMoney(amount);
}

function getTotalItems(basket: Basket): SfCart["totalItems"] {
  return basket.productItems?.reduce((acc, curr) => acc + (curr.quantity || 0), 0) ?? 0;
}