Vue Storefront is now Alokai! Learn More
Cart normalizer

Cart normalizer

The normalizeCart function is used to map a Commercetools Cart into the unified SfCart data model.

Parameters

NameTypeDefault valueDescription
contextNormalizerContextcontext needed for the normalizer
cartCartCommercetools Cart

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 addCustomFields API. The following example demonstrates how to extend SfCart with a version field.

export const unifiedApiExtension = createUnifiedExtension({
  normalizers: {
    addCustomFields: [
      {
        normalizeCart: (context, cart) => ({
          version: cart.version,
        }),
      },
    ],
  },
  config: {
    ...
  },
});

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
import type { NormalizerContext } from "@/normalizers/types";
import { maybe } from "@shared/utils";
import type { Cart } from "@vsf-enterprise/commercetools-types";
import type { SfCart } from "@vue-storefront/unified-data-model";
import { defineNormalizer } from "../defineNormalizer";

export const normalizeCart = defineNormalizer.normalizeCart((context, cart) => {
  const { lineItems } = normalizeLineItems(context, cart);
  const { appliedCoupons } = normalizeDiscounts(context, cart);
  const { shippingMethod, totalShippingPrice } = normalizeShipping(context, cart);
  const { billingAddress, shippingAddress } = normalizeAddresses(context, cart);
  const {
    totalItems,
    totalPrice,
    totalTax,
    subtotalRegularPrice,
    subtotalDiscountedPrice,
    totalCouponDiscounts,
  } = normalizeTotals(context, cart);

  return {
    id: cart.id,
    customerEmail: getCartCustomerEmail(cart),
    lineItems,
    totalPrice,
    subtotalRegularPrice,
    subtotalDiscountedPrice,
    totalItems,
    appliedCoupons,
    billingAddress,
    shippingAddress,
    shippingMethod,
    totalShippingPrice,
    totalCouponDiscounts,
    totalTax,
  };
});

function normalizeDiscounts(
  context: NormalizerContext,
  cart: Cart,
): Pick<SfCart, "appliedCoupons"> {
  const appliedCoupons = cart.discountCodes
    .map((discountCode) =>
      discountCode.discountCode
        ? context.normalizers.normalizeCartCoupon(discountCode.discountCode)
        : null,
    )
    .filter((item) => item !== null) as SfCart["appliedCoupons"];

  return {
    appliedCoupons,
  };
}

function normalizeShipping(
  context: NormalizerContext,
  cart: Cart,
): Pick<SfCart, "shippingMethod" | "totalShippingPrice"> {
  const { normalizeMoney, normalizeShippingMethod } = context.normalizers;
  const shippingMethod =
    cart.shippingInfo?.shippingMethod &&
    normalizeShippingMethod({
      ...cart.shippingInfo.shippingMethod,
      totalPrice: cart.totalPrice,
    });

  const totalShippingPrice = cart.shippingInfo && normalizeMoney(cart.shippingInfo.price);

  return {
    shippingMethod: maybe(shippingMethod),
    totalShippingPrice: maybe(totalShippingPrice),
  };
}

function normalizeAddresses(
  context: NormalizerContext,
  cart: Cart,
): Pick<SfCart, "billingAddress" | "shippingAddress"> {
  const { normalizeAddress } = context.normalizers;
  return {
    billingAddress: cart.billingAddress ? normalizeAddress(cart.billingAddress) : null,
    shippingAddress: cart.shippingAddress ? normalizeAddress(cart.shippingAddress) : null,
  };
}

type Totals = Pick<
  SfCart,
  | "totalPrice"
  | "subtotalRegularPrice"
  | "subtotalDiscountedPrice"
  | "totalItems"
  | "totalTax"
  | "totalCouponDiscounts"
>;

function normalizeTotals(context: NormalizerContext, cart: Cart): Totals {
  const { normalizeMoney } = context.normalizers;
  const totalPrice = normalizeMoney(cart.totalPrice);
  const totalItems = cart.lineItems.reduce((total, item) => total + item.quantity, 0);

  const { regular: subtotalRegularCentAmount, discounted: subtotalDiscountedCentAmount } =
    cart.lineItems.reduce(
      (total, item) => {
        const regular = item.price.value.centAmount * item.quantity;
        total.regular += regular;
        total.discounted += item.price.discounted?.value?.centAmount * item.quantity || regular;
        return total;
      },
      { regular: 0, discounted: 0 },
    );

  const subtotalRegularPrice = normalizeMoney({
    ...cart.totalPrice,
    centAmount: subtotalRegularCentAmount,
  });

  const subtotalDiscountedPrice = normalizeMoney({
    ...cart.totalPrice,
    centAmount: subtotalDiscountedCentAmount,
  });

  const totalTax =
    cart.taxedPrice?.totalGross && cart.taxedPrice?.totalNet
      ? normalizeMoney({
          ...cart.taxedPrice.totalGross,
          centAmount: cart.taxedPrice.totalGross.centAmount - cart.taxedPrice.totalNet.centAmount,
        })
      : normalizeMoney({
          ...cart.totalPrice,
          centAmount: 0,
        });

  const totalCouponDiscounts = normalizeTotalDiscounts(context, cart);

  return {
    totalPrice,
    totalItems,
    totalTax,
    subtotalRegularPrice,
    subtotalDiscountedPrice,
    totalCouponDiscounts,
  };
}

function normalizeLineItems(context: NormalizerContext, cart: Cart): Pick<SfCart, "lineItems"> {
  return {
    lineItems: cart.lineItems
      .map((lineItem) => context.normalizers.normalizeCartLineItem(lineItem))
      .filter(Boolean),
  };
}

function normalizeTotalDiscounts(context: NormalizerContext, cart: Cart) {
  const totalCouponsAmount = calculateDiscountsValue(cart);

  return context.normalizers.normalizeMoney({
    centAmount: totalCouponsAmount ?? 0,
    fractionDigits: cart.totalPrice.fractionDigits,
    currencyCode: cart.totalPrice.currencyCode,
    type: cart.totalPrice.type,
  });
}

function calculateDiscountsValue(cart: Cart) {
  const { lineItems } = cart;
  if (!lineItems || lineItems.length <= 0) {
    return null;
  }

  return lineItems.reduce((totalAmount, lineItem) => {
    const lineItemDiscountAmount = lineItem.discountedPricePerQuantity.reduce(
      (amount, discountedPricePerQuantity) => {
        const original =
          (lineItem.price.discounted?.value.centAmount || lineItem.price.value.centAmount) *
          discountedPricePerQuantity.quantity;

        const discounted =
          discountedPricePerQuantity.discountedPrice.value.centAmount *
          discountedPricePerQuantity.quantity;

        return amount + (original - discounted);
      },
      0,
    );
    return totalAmount + lineItemDiscountAmount;
  }, 0);
}

function getCartCustomerEmail(cart: Cart): string | null {
  return cart.customerEmail || cart.customer?.email || null;
}