Cart normalizer
The normalizeCart
function is used to map a SFCC Basket into the unified SfCart
data model.
Parameters
Name | Type | Default value | Description |
---|---|---|---|
context | NormalizerContext | Context needed for the normalizer. Context contain a currency field that contains a currency code | |
basket | Basket | SFCC 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 an lastModification
field.
export const unifiedApiExtension = createUnifiedExtension({
normalizers: {
addCustomFields: [
{
normalizeCart: (context, basket) => ({
lastModification: basket.lastModified,
}),
},
],
},
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
/* eslint-disable complexity */
import type { NormalizerContext } from "@/normalizers/types";
import type { ProductItem } from "@internal";
import { maybe } from "@/helpers";
import type { 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((context, basket) => {
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.
context.currency = basket.currency;
const { normalizeCartCoupon, normalizeCartLineItem, normalizeMoney } = context.normalizers;
const totalTax = getTotalTax(context, basket);
const totalShippingPrice = basket.shippingTotal ? normalizeMoney(basket.shippingTotal) : null;
const totalPrice = getTotalPrice(context, basket);
const totalItems = getTotalItems(basket);
const shippingAddress = getAddress(context, basket.shipments?.[0]?.shippingAddress);
const billingAddress = getAddress(context, basket.billingAddress);
const appliedCoupons = basket.couponItems?.map((coupon) => normalizeCartCoupon(coupon)) ?? [];
const lineItems = basket.productItems?.map((item) => normalizeCartLineItem(item)) ?? [];
const shippingMethod = getShippingMethod(context, basket);
const totalCouponDiscounts = basket.productItems
? getTotalDiscounts(context, basket.productItems)
: normalizeMoney(0);
const subtotalRegularPrice = getSubtotalRegularPrice(context, lineItems);
const subtotalDiscountedPrice = getSubtotalDiscountedPrice(context, basket.productItems);
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(context: NormalizerContext, basket: Basket): SfCart["totalTax"] {
const amount = basket.taxTotal ?? basket.adjustedMerchandizeTotalTax ?? 0;
return context.normalizers.normalizeMoney(amount);
}
/**
* orderTotal is undefined until the shipping method is selected, therefore we use productTotal as a fallback.
*/
function getTotalPrice(context: NormalizerContext, basket: Basket): SfCart["totalPrice"] {
const shippingPrice = basket.shippingTotal ?? 0;
const amount = basket.orderTotal || (basket.productTotal ?? 0) + shippingPrice;
return context.normalizers.normalizeMoney(amount);
}
function getAddress(
context: NormalizerContext,
address: BasketAddress | undefined,
): Maybe<SfAddress> {
return address ? context.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(context: NormalizerContext, basket: Basket): SfCart["shippingMethod"] {
const unnormalizedShippingMethod = basket.shipments?.[0]?.shippingMethod;
return unnormalizedShippingMethod
? context.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(
context: NormalizerContext,
items: ProductItem[],
): 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 context.normalizers.normalizeMoney(amount);
}
function getSubtotalRegularPrice(
context: NormalizerContext,
lineItems: SfCartLineItem[],
): SfCart["subtotalRegularPrice"] {
const lineItemsAmount = lineItems.reduce(
(acc, item) => acc + (item.unitPrice?.regularPrice.amount || 0) * item.quantity,
0,
);
return context.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(
context: NormalizerContext,
productItems: Basket["productItems"] = [],
): SfCart["subtotalDiscountedPrice"] {
const amount = productItems.reduce((acc, item) => acc + (item.price || 0), 0);
return context.normalizers.normalizeMoney(amount);
}
function getTotalItems(basket: Basket): SfCart["totalItems"] {
return basket.productItems?.reduce((acc, curr) => acc + (curr.quantity || 0), 0) ?? 0;
}