Alokai
ReferenceBigCommerceNormalizers

Cart normalizer

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

Parameters

NameTypeDefault valueDescription
contextNormalizerContextContext which contains e.g. currency
cartCartBigCommerce 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 parentId field.

export const unifiedApiExtension = createUnifiedExtension({
  normalizers: {
    addCustomFields: [
      {
        normalizeCart: (context, cart) => ({
          parentId: cart.parent_id,
        }),
      },
    ],
  },
  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.

import type { SfCart, SfCartLineItem } from "@alokai/connect";
import type { Cart, LineItems } from "@vsf-enterprise/bigcommerce-api";

import { maybe } from "@vsf-enterprise/unified-api-bigcommerce";
import type { NormalizerContext } from "@vsf-enterprise/unified-api-bigcommerce";

import { defineNormalizer } from "../defineNormalizer";

type CartTotals = Pick<
SfCart,
| "subtotalDiscountedPrice"
| "subtotalRegularPrice"
| "totalCouponDiscounts"
| "totalPrice"
| "totalShippingPrice"
| "totalTax"
>;

export const normalizeCart = defineNormalizer.normalizeCart((context, cart) => {
const ctx = { ...context, currency: cart.currency?.code as string };

const lineItems = normalizeCartLineItems(cart.line_items, ctx);

return {
  appliedCoupons:
    cart.coupons?.map((coupon) => ctx.normalizers.normalizeCartCoupon(coupon)) ?? [],
  billingAddress: null,
  customerEmail: maybe(cart.email || null),
  id: cart.id as string,
  lineItems,
  shippingAddress: null,
  shippingMethod: null,
  totalItems: lineItems.length,
  ...normalizeCartTotals(cart, lineItems, ctx),
};
});

function normalizeCartTotals(
cart: Cart,
lineItems: SfCartLineItem[],
ctx: NormalizerContext,
): CartTotals {
const lineItemsAmount = lineItems.reduce(
  (acc, item) => acc + (item.unitPrice?.regularPrice.amount || 0) * item.quantity,
  0,
);
const subtotalRegularPrice = ctx.normalizers.normalizeMoney(+lineItemsAmount.toFixed(2));

return {
  subtotalDiscountedPrice: getSubtotalDiscountedPrice(cart, ctx),
  subtotalRegularPrice: subtotalRegularPrice,
  totalCouponDiscounts: ctx.normalizers.normalizeMoney(
    cart.coupons?.reduce((acc, curr) => acc + (curr.discounted_amount ?? 0), 0) ?? 0,
  ),
  totalPrice: ctx.normalizers.normalizeMoney(cart.cart_amount ?? 0),
  totalShippingPrice: null, // @TODO shipping price is not available
  totalTax: ctx.normalizers.normalizeMoney(0), // @TODO tax is not available
};
}

function getSubtotalDiscountedPrice(cart: Cart, ctx: NormalizerContext) {
const { base_amount = 0, line_items } = cart;
const lineItems = [...(line_items?.physical_items ?? []), ...(line_items?.digital_items ?? [])];
const discounts = lineItems.reduce(
  (acc, curr) => acc + (curr.extended_list_price ?? 0) - (curr.extended_sale_price ?? 0),
  0,
);
return ctx.normalizers.normalizeMoney(base_amount - discounts);
}

function normalizeCartLineItems(
lineItems: LineItems | null | undefined,
ctx: NormalizerContext,
): SfCartLineItem[] {
if (!lineItems) {
  return [];
}

return [
  ...lineItems.physical_items.map((item) => ctx.normalizers.normalizeCartLineItem(item)),
  ...lineItems.digital_items.map((item) => ctx.normalizers.normalizeCartLineItem(item)),
];
}
import { defineNormalizer } from "../defineNormalizer";

export const normalizeCartCoupon = defineNormalizer.normalizeCartCoupon((_context, coupon) => {
return {
  code: coupon.code,
  id: coupon.code,
  name: coupon.name,
};
});
import type { SfShippingMethod } from "@alokai/connect";

import { defineNormalizer } from "../defineNormalizer";

export const normalizeShippingMethod = defineNormalizer.normalizeShippingMethod(
(context, input) => {
  const { name, price } = input;

  return {
    description: null,
    estimatedDelivery: null,
    id: name,
    name: name,
    price: context.normalizers.normalizeMoney(Number.parseFloat(price)),
  } satisfies SfShippingMethod;
},
);
import type { SfCartLineItem } from "@alokai/connect";
import { ValidationError } from "@alokai/connect/middleware";

import { maybe, slugify } from "@vsf-enterprise/unified-api-bigcommerce";
import { getProductQuantityLimit } from "@vsf-enterprise/unified-api-bigcommerce";
import type { NormalizeCartLineItemInput, NormalizerContext } from "@vsf-enterprise/unified-api-bigcommerce";

import { defineNormalizer } from "../defineNormalizer";

export const normalizeCartLineItem = defineNormalizer.normalizeCartLineItem((context, lineItem) => {
const { productsDetails } = context;

if (!productsDetails) {
  throw new ValidationError("Cart line item normalization failed", {
    issues: [
      {
        code: "invalid_type",
        message: "Products details are required to normalize a cart line item",
        path: ["productsDetails"],
      },
    ],
  });
}
const productDetails = productsDetails.find((product) => product.id === lineItem.product_id);

if (!productDetails) {
  throw new ValidationError("Cart line item normalization failed", {
    issues: [
      {
        code: "custom",
        message: `Product details for product ${lineItem.product_id} not found`,
        path: ["productDetails", lineItem.product_id.toString()],
      },
    ],
  });
}

const { totalPrice, unitPrice } = normalizeLineItemPrice(lineItem, context);
const productAvailableQuantity = getProductQuantityLimit(productDetails);

return {
  attributes:
    lineItem.options
      ?.map((option) => context.normalizers.normalizeAttribute(option))
      .filter(Boolean) ?? [],
  id: lineItem.id!,
  image: lineItem.image_url ? { alt: null, url: lineItem.image_url } : null,
  name: maybe(lineItem.name),
  productId: `${lineItem.product_id}`,
  quantity: lineItem.quantity,
  quantityLimit: maybe(productAvailableQuantity(productDetails.inventory_level)),
  sku: maybe(lineItem.sku),
  slug: slugify(lineItem.url ?? lineItem.sku ?? lineItem.name ?? `${lineItem.variant_id}`),
  totalPrice,
  unitPrice,
};
});

function normalizeLineItemPrice(
lineItem: NormalizeCartLineItemInput,
ctx: NormalizerContext,
): Pick<SfCartLineItem, "totalPrice" | "unitPrice"> {
if (!(lineItem.extended_list_price && lineItem.list_price)) {
  return {
    totalPrice: null,
    unitPrice: null,
  };
}

const totalPriceAmount = lineItem.extended_sale_price ?? lineItem.extended_list_price;
const unitPriceAmount = lineItem.sale_price ?? lineItem.list_price;
const unitRegularPriceAmount = lineItem.original_price ?? lineItem.list_price;

return {
  totalPrice: ctx.normalizers.normalizeMoney(totalPriceAmount),
  unitPrice: {
    isDiscounted: unitPriceAmount < unitRegularPriceAmount,
    regularPrice: ctx.normalizers.normalizeMoney(unitRegularPriceAmount),
    value: ctx.normalizers.normalizeMoney(unitPriceAmount),
  },
};
}

On this page