@automattic/shopping-cart
Version:
A library to use the WordPress.com shopping cart.
327 lines (300 loc) • 8.4 kB
text/typescript
import debugFactory from 'debug';
import { getEmptyResponseCart } from './empty-carts';
import type {
TempResponseCart,
CartLocation,
RequestCart,
RequestCartProduct,
ResponseCart,
ResponseCartProduct,
GetCart,
CartKey,
} from './types';
const debug = debugFactory( 'shopping-cart:cart-functions' );
let lastUUID = 100;
const emptyResponseCart = getEmptyResponseCart();
function convertResponseCartProductToRequestCartProduct(
product: ResponseCartProduct | RequestCartProduct
): RequestCartProduct {
const { product_slug, meta, product_id, extra, volume, quantity } = product;
return {
product_slug,
meta,
volume,
quantity,
product_id,
extra,
};
}
export function convertResponseCartToRequestCart( {
products,
coupon,
tax,
blog_id,
}: TempResponseCart ): RequestCart {
let requestCartTax = null;
if (
tax.location.country_code ||
tax.location.postal_code ||
tax.location.subdivision_code ||
tax.location.vat_id ||
tax.location.organization ||
tax.location.address ||
tax.location.city
) {
requestCartTax = {
location: {
country_code: tax.location.country_code,
postal_code: tax.location.postal_code,
subdivision_code: tax.location.subdivision_code,
vat_id: tax.location.vat_id,
organization: tax.location.organization,
address: tax.location.address,
city: tax.location.city,
},
};
}
return {
blog_id,
products: products.map( convertResponseCartProductToRequestCartProduct ),
coupon,
temporary: false,
tax: requestCartTax,
};
}
export function convertTempResponseCartToResponseCart( cart: TempResponseCart ): ResponseCart {
return {
...cart,
products: cart.products.filter( isValidResponseCartProduct ),
};
}
function isValidResponseCartProduct(
product: RequestCartProduct | ResponseCartProduct
): product is ResponseCartProduct {
return 'uuid' in product;
}
export function removeItemFromResponseCart(
cart: TempResponseCart,
uuidToRemove: string
): TempResponseCart {
return {
...cart,
products: cart.products.filter( ( product ) => {
return isValidResponseCartProduct( product ) ? product.uuid !== uuidToRemove : true;
} ),
};
}
export function addCouponToResponseCart(
cart: TempResponseCart,
couponToAdd: string
): TempResponseCart {
return {
...cart,
coupon: couponToAdd,
is_coupon_applied: false,
};
}
export function removeCouponFromResponseCart( cart: TempResponseCart ): TempResponseCart {
return {
...cart,
coupon: '',
is_coupon_applied: false,
};
}
export function addLocationToResponseCart(
cart: TempResponseCart,
location: CartLocation
): TempResponseCart {
return {
...cart,
tax: {
...cart.tax,
location: {
country_code: location.countryCode || undefined,
postal_code: location.postalCode || undefined,
subdivision_code: location.subdivisionCode || undefined,
vat_id: location.vatId || undefined,
organization: location.organization || undefined,
address: location.address || undefined,
city: location.city || undefined,
},
},
};
}
export function doesCartLocationDifferFromResponseCartLocation(
cart: TempResponseCart,
location: CartLocation
): boolean {
const {
countryCode: newCountryCode = '',
postalCode: newPostalCode = '',
subdivisionCode: newSubdivisionCode = '',
vatId: newVatId = '',
organization: newOrganization = '',
address: newAddress = '',
city: newCity = '',
} = location;
const {
country_code: oldCountryCode = '',
postal_code: oldPostalCode = '',
subdivision_code: oldSubdivisionCode = '',
vat_id: oldVatId = '',
organization: oldOrganization = '',
address: oldAddress = '',
city: oldCity = '',
} = cart.tax?.location ?? {};
if ( location.countryCode !== undefined && newCountryCode !== oldCountryCode ) {
return true;
}
if ( location.postalCode !== undefined && newPostalCode !== oldPostalCode ) {
return true;
}
if ( location.subdivisionCode !== undefined && newSubdivisionCode !== oldSubdivisionCode ) {
return true;
}
if ( location.vatId !== undefined && newVatId !== oldVatId ) {
return true;
}
if ( location.organization !== undefined && newOrganization !== oldOrganization ) {
return true;
}
if ( location.address !== undefined && newAddress !== oldAddress ) {
return true;
}
if ( location.city !== undefined && newCity !== oldCity ) {
return true;
}
return false;
}
export function convertRawResponseCartToResponseCart(
rawResponseCart: Partial< ResponseCart >
): ResponseCart {
if ( typeof rawResponseCart !== 'object' || rawResponseCart === null ) {
return emptyResponseCart;
}
// If tax.location is an empty PHP associative array, it will be JSON serialized to [] but we need {}
let taxLocation = {};
if ( rawResponseCart.tax?.location ) {
if ( Array.isArray( rawResponseCart.tax.location ) ) {
taxLocation = {};
} else {
taxLocation = rawResponseCart.tax.location;
}
}
const rawProducts =
rawResponseCart.products?.length && Array.isArray( rawResponseCart.products )
? rawResponseCart.products
: [];
return {
...emptyResponseCart,
...rawResponseCart,
tax: {
location: taxLocation,
display_taxes: rawResponseCart.tax?.display_taxes ?? false,
},
// Add uuid to products returned by the server
products: rawProducts.filter( isRealProduct ).map( ( product ) => {
return {
...product,
uuid: product.product_slug + lastUUID++,
};
} ),
};
}
function isRealProduct( serverCartItem: ResponseCartProduct ): boolean {
// Credits are reported separately, so we do not need to include the pseudo-product in the line items.
if ( serverCartItem.product_slug === 'wordpress-com-credits' ) {
return false;
}
return true;
}
function shouldProductReplaceCart(
product: RequestCartProduct,
responseCart: TempResponseCart
): boolean {
const doesCartHaveRenewals = responseCart.products.some(
( cartProduct ) => cartProduct.extra?.purchaseType === 'renewal'
);
if (
! doesCartHaveRenewals &&
product.extra?.purchaseType === 'renewal' &&
product.product_slug !== 'domain_redemption'
) {
// adding a renewal replaces the cart unless it is a privacy protection (comment copied from original code from old checkout; is domain_redemption really privacy protection?)
return true;
}
if ( doesCartHaveRenewals && product.extra?.purchaseType !== 'renewal' ) {
// all items should replace the cart if the cart contains a renewal
return true;
}
return false;
}
function shouldProductsReplaceCart(
products: RequestCartProduct[],
responseCart: TempResponseCart
): boolean {
return products.some( ( product ) => shouldProductReplaceCart( product, responseCart ) );
}
export function addItemsToResponseCart(
responseCart: TempResponseCart,
products: RequestCartProduct[]
): TempResponseCart {
if ( shouldProductsReplaceCart( products, responseCart ) ) {
debug( 'items should replace response cart', products );
return replaceAllItemsInResponseCart( responseCart, products );
}
debug( 'items should not replace response cart', products );
return {
...responseCart,
products: [ ...responseCart.products, ...products ],
};
}
export function replaceAllItemsInResponseCart(
responseCart: TempResponseCart,
products: RequestCartProduct[]
): TempResponseCart {
return {
...responseCart,
products: [ ...products ],
};
}
export function replaceItemInResponseCart(
cart: TempResponseCart,
uuidToReplace: string,
productPropertiesToChange: Partial< RequestCartProduct >
): TempResponseCart {
return {
...cart,
products: cart.products.map( ( item ) => {
if ( isValidResponseCartProduct( item ) && item.uuid === uuidToReplace ) {
return { ...item, ...productPropertiesToChange };
}
return item;
} ),
};
}
export function doesResponseCartContainProductMatching(
responseCart: TempResponseCart,
productProperties: Partial< ResponseCartProduct >
): boolean {
return responseCart.products.some( ( product ) => {
return (
isValidResponseCartProduct( product ) &&
Object.keys( productProperties ).every( ( key ) => {
const typedKey = key as keyof ResponseCartProduct;
return product[ typedKey ] === productProperties[ typedKey ];
} )
);
} );
}
export async function findCartKeyFromSiteSlug(
slug: string,
getCart: GetCart
): Promise< CartKey > {
try {
const cart = await getCart( slug as CartKey );
return cart.cart_key;
} catch {
return 'no-site';
}
}