@open-tender/utils
Version:
A library of utils for use with Open Tender applications that utilize our cloud-based Order API.
911 lines (910 loc) • 37.7 kB
JavaScript
import { isCreditTender, isHouseAccountTender, isOrderCustomer } from '@open-tender/types';
import { convertStringToArray } from './helpers';
import { makeMenuItemLookup } from './favorites';
import { isoToDate } from './datetimes';
import { fromUnixTime } from 'date-fns';
// TODO: refactor for nested mods (if needed)
export const getItemOptions = (item) => {
if (!item.groups.length)
return [];
const options = item.groups
.map(group => group.options.filter(option => option.quantity > 0))
.reduce((arr, i) => [...arr, ...i], []);
return options;
};
export const makeModifierNames = (item) => {
return item.groups
.map(group => group.options.filter(option => option.quantity > 0))
.reduce((arr, i) => [...arr, ...i], [])
.map(i => {
const optionNames = makeModifierNames(i);
const name = optionNames ? `${i.name} (${optionNames})` : i.name;
return Object.assign(Object.assign({}, i), { name });
})
.map(option => option.name)
.join(', ');
};
export const makeModifierNamesForGroups = (groups) => {
return groups
.map(group => group.options.filter(option => option.quantity > 0))
.reduce((arr, i) => [...arr, ...i], [])
.map(i => {
const optionNames = makeModifierNamesForGroups(i.groups);
const name = optionNames ? `${i.name} (${optionNames})` : i.name;
return Object.assign(Object.assign({}, i), { name });
})
.map(option => option.name)
.join(', ');
};
// export const makeModifierNames = <T extends BaseItem>(item: T): string => {
// return makeModifierNamesForGroups(item.groups)
// }
export const hasGroupsBelowMin = (option, groups) => {
if (!groups || !groups.length)
return false;
if (option && option.quantity === 0)
return false;
const childGroupsBelow = groups.filter(g => {
const optionsBelowMin = g.options.filter(o => hasGroupsBelowMin(o, o.groups));
return optionsBelowMin.length > 0;
});
if (childGroupsBelow.length > 0)
return true;
return groups.filter(g => ((g === null || g === void 0 ? void 0 : g.quantity) || 0) < g.min).length > 0;
};
export const makeModifierGroups = (groups) => {
if (!groups || !groups.length)
return [[], []];
const otherGroups = groups.filter(g => !g.isSize);
if (otherGroups.length)
return [groups, []];
const sizeGroup = groups.find(g => g.isSize);
if (!sizeGroup)
return [[], []];
const sizeGroupNested = sizeGroup.options.filter(o => o.groups && o.groups.length);
if (!sizeGroupNested || !sizeGroupNested.length)
return [[], []];
const selectedOption = sizeGroup.options.find(o => o.quantity > 0);
return selectedOption
? [selectedOption.groups, [[sizeGroup.id, selectedOption.id]]]
: [[], []];
};
export const makeUpsellItems = (itemIds, itemLookup) => {
return itemIds
.map(id => itemLookup[id])
.filter(i => i)
.map(i => makeOrderItem(i))
.filter(i => !i.isSoldOut)
.filter(i => {
const belowMin = i.groups.filter(g => !g.isSize && (g.quantity || 0) < g.min);
return belowMin.length === 0;
});
};
export const makeUpsellItemIds = (item, cartIds, soldOut) => {
const upsellItems = item ? item.upsellItems : null;
return upsellItems
? upsellItems.filter(id => !cartIds.includes(id) && !soldOut.includes(id))
: [];
};
export const makeUpsellItemsForCart = (categories, cartIds, soldOut) => {
const itemLookup = makeMenuItemLookup(categories);
const menuItems = cartIds.map(id => itemLookup[id]).filter(Boolean);
const upsellItemIds = menuItems.reduce((arr, i) => {
const upsellsItems = i.upsell_items.slice(0, cartIds.length > 2 ? 2 : undefined);
return [...arr, ...upsellsItems];
}, []);
const uniqueIds = Array.from(new Set(upsellItemIds));
const itemIds = uniqueIds.filter(id => !cartIds.includes(id) && !soldOut.includes(id));
return makeUpsellItems(itemIds, itemLookup);
};
const nutritionKeys = [
'calories',
'cholesterol',
'dietary_fiber',
'protein',
'saturated_fat',
'sodium',
'sugars',
'total_carbs',
'total_fat',
'trans_fat'
];
export const calcNutrition = (item) => {
const { nutritionalInfo, groups } = item;
let nutrition = (nutritionalInfo ? Object.assign({}, nutritionalInfo) : {});
const options = groups.reduce((arr, g) => {
return [...arr, ...g.options.filter(o => o.quantity > 0)];
}, []);
options.forEach(option => {
const optionNutrition = calcNutrition(option);
nutritionKeys.forEach(key => {
const optionPer = optionNutrition ? optionNutrition[key] || 0 : 0;
const optionVal = optionPer * option.quantity;
const itemVal = nutrition[key] || 0;
nutrition = Object.assign(Object.assign({}, nutrition), { [key]: itemVal + optionVal });
});
});
return nutrition;
};
export const calcNutritionalInfo = (item) => {
const nutrition = calcNutrition(item);
const { serving_size = '0.00' } = item.nutritionalInfo || {};
return Object.assign(Object.assign({}, nutrition), { serving_size });
};
export const calcPrices = (item) => {
const groups = item.groups.map(g => {
let groupQuantity = 0;
const optionsWithPrices = g.options.map((o, index) => {
return Object.assign(Object.assign({}, calcPrices(o)), { index });
});
const sortedOptions = optionsWithPrices.sort((a, b) => {
const pricePerA = a.totalPrice && a.quantity ? a.totalPrice / a.quantity : 0;
const pricePerB = b.totalPrice && b.quantity ? b.totalPrice / b.quantity : 0;
return pricePerA - pricePerB;
});
const pricedOptions = sortedOptions.map(o => {
const pricePer = o.totalPrice && o.quantity ? o.totalPrice / o.quantity : 0;
const pointsPer = o.totalPoints && o.quantity ? o.totalPoints / o.quantity : 0;
const includedRemaining = Math.max(g.included - groupQuantity, 0);
const priceQuantity = Math.max(o.quantity - includedRemaining, 0);
const option = Object.assign(Object.assign({}, o), { totalPrice: priceQuantity * pricePer, totalPoints: priceQuantity * pointsPer, totalCals: o.totalCals || 0 });
groupQuantity += o.quantity || 0;
return option;
});
const options = pricedOptions.sort((a, b) => a.index - b.index);
return Object.assign(Object.assign({}, g), { quantity: groupQuantity, options });
});
const optionsPrice = groups.reduce((t, g) => {
return t + g.options.reduce((ot, o) => ot + o.totalPrice, 0.0);
}, 0.0);
const totalPrice = item.quantity * (item.price + optionsPrice);
const optionsPoints = groups.reduce((t, g) => {
return t + g.options.reduce((ot, o) => ot + o.totalPoints, 0);
}, 0.0);
const totalPoints = item.points
? item.quantity * (item.points + optionsPoints)
: null;
const optionsCals = groups.reduce((t, g) => {
return t + g.options.reduce((ot, o) => ot + o.totalCals, 0);
}, 0.0);
const totalCals = item.cals || item.cals === 0
? item.quantity * (item.cals + optionsCals)
: null;
const nonSizeGroups = groups ? groups.filter(i => !i.isSize) : null;
const currentOptions = nonSizeGroups
? getItemOptions(Object.assign(Object.assign({}, item), { groups: nonSizeGroups }))
: [];
const modifiersAllergens = currentOptions.reduce((t, o) => {
return [...t, ...o.allergens];
}, []);
const totalAllergens = [...new Set(item.allergens.concat(modifiersAllergens))];
return Object.assign(Object.assign({}, item), { groups, totalPrice, totalPoints, totalCals, totalAllergens });
};
export const checkSoldOut = (id, suspendUntil, soldOut = [], requestedAt) => {
if (soldOut.includes(id))
return true;
if (!suspendUntil || !requestedAt)
return false;
const now = new Date();
const suspendDate = fromUnixTime(suspendUntil);
const requestedDate = requestedAt === null || requestedAt === 'asap'
? now
: isoToDate(requestedAt);
return requestedDate < suspendDate;
};
const makeOrderItemGroups = (optionGroups, isEdit, soldOut = [], requestedAt = null) => {
if (!optionGroups)
return [];
const groups = optionGroups.map(g => {
var _a, _b;
const options = g.option_items.map(o => {
var _a, _b, _c;
const isSoldOut = checkSoldOut(o.id, o.suspend_until, soldOut, requestedAt);
const quantity = isSoldOut
? 0
: o.opt_is_default && !isEdit
? o.min_quantity || 1
: 0;
const cals = ((_a = o.nutritional_info) === null || _a === void 0 ? void 0 : _a.calories)
? parseInt(o.nutritional_info.calories.toString())
: null;
const groups = makeOrderItemGroups(o.option_groups, isEdit, soldOut, requestedAt);
const option = {
allergens: convertStringToArray(o.allergens),
allergens_list: o.allergens_list || [],
cals: cals,
description: o.description,
groups: groups,
id: o.id,
imageUrl: o.small_image_url,
appImageUrl: o.app_image_url,
increment: o.increment,
ingredients: o.ingredients,
isDefault: o.opt_is_default,
isSoldOut: isSoldOut,
suspend_until: o.suspend_until,
max: isSoldOut ? 0 : o.max_quantity,
min: o.min_quantity,
name: o.name,
nutritionalInfo: o.nutritional_info,
points: o.points || 0,
// @ts-ignore
pos_ext_id: o.pos_ext_id,
price: parseFloat(o.price),
quantity: quantity,
shorthand: o.shorthand,
shortDescription: o.short_description,
shortName: o.short_name,
slug: o.slug,
tags: convertStringToArray(o.tags),
tags_list: o.tags_list || [],
totalAllergens: [],
totalCals: null,
totalPoints: null,
totalPrice: null,
itemShape: (_b = o.item_shape) !== null && _b !== void 0 ? _b : null,
excludeFromBuilder: (_c = o.exclude_from_builder) !== null && _c !== void 0 ? _c : false
};
return option;
});
const group = {
description: g.description,
id: g.id,
imageUrl: g.small_image_url,
included: g.included_options,
isPizza: !!g.is_pizza,
isSize: !!g.is_size,
max: g.max_options,
min: g.min_options,
name: g.name,
options: options,
quantity: options.reduce((t, o) => t + o.quantity, 0),
itemShape: (_a = g.item_shape) !== null && _a !== void 0 ? _a : null,
excludeFromBuilder: (_b = g.exclude_from_builder) !== null && _b !== void 0 ? _b : false
};
return group;
});
return groups;
};
export const makeOrderItem = (item, isEdit, soldOut = [], simpleItem, hasPoints = false, requestedAt = null) => {
var _a, _b, _c;
const groups = makeOrderItemGroups(item.option_groups, isEdit, soldOut, requestedAt);
const cals = ((_a = item.nutritional_info) === null || _a === void 0 ? void 0 : _a.calories)
? parseInt(item.nutritional_info.calories.toString())
: null;
const isSoldOut = checkSoldOut(item.id, item.suspend_until, soldOut, requestedAt);
const orderItem = {
allergens: convertStringToArray(item.allergens),
allergens_list: item.allergens_list || [],
cals: cals,
cartGuestId: simpleItem ? simpleItem.cart_guest_id || null : null,
customerId: simpleItem ? simpleItem.customer_id || null : null,
description: item.description,
groups: groups,
id: item.id,
imageUrl: item.large_image_url,
increment: item.increment,
//index: null,
ingredients: item.ingredients,
isSoldOut: isSoldOut,
suspend_until: item.suspend_until,
madeFor: simpleItem ? simpleItem.made_for || null : null,
max: item.max_quantity,
min: item.min_quantity,
name: item.name,
notes: simpleItem ? simpleItem.notes || null : null,
nutritionalInfo: item.nutritional_info,
points: hasPoints ? item.points || null : null,
// @ts-ignore
pos_ext_id: item.pos_ext_id,
price: parseFloat(item.price),
quantity: item.min_quantity || 1 * item.increment,
shorthand: item.shorthand,
shortDescription: item.short_description,
shortName: item.short_name,
similarItems: item.similar_items,
slug: item.slug,
tags: convertStringToArray(item.tags),
tags_list: item.tags_list || [],
totalAllergens: [],
totalCals: null,
totalPoints: null,
totalPrice: null,
upsellItems: item.upsell_items,
itemShape: (_b = item.item_shape) !== null && _b !== void 0 ? _b : null,
excludeFromBuilder: (_c = item.exclude_from_builder) !== null && _c !== void 0 ? _c : false,
list_id: item.list_id,
list_name: item.list_name,
menu_position: item.menu_position
};
const pricedItem = calcPrices(orderItem);
return pricedItem;
};
export const calcCartCounts = (cart) => {
return cart.reduce((obj, item) => {
const newCount = (obj[item.id.toString()] || 0) + item.quantity;
return Object.assign(Object.assign({}, obj), { [item.id]: newCount });
}, {});
};
const makeSimpleGroupsLookup = (groups) => {
if (!groups || !groups.length)
return null;
return groups.reduce((grpObj, g) => {
const options = g.options.reduce((optObj, o) => {
optObj[o.id] = {
quantity: o.quantity,
groupsLookup: makeSimpleGroupsLookup(o.groups)
};
return optObj;
}, {});
grpObj[g.id] = { options };
return grpObj;
}, {});
};
const rehydrateGroups = (groups, groupsLookup) => {
if (!groupsLookup)
return groups;
groups.forEach(group => {
const simpleGroup = groupsLookup[group.id];
if (simpleGroup) {
group.options.forEach(option => {
const simpleOption = simpleGroup.options[option.id];
if (simpleOption) {
option.quantity = simpleOption.quantity;
option.groups = rehydrateGroups(option.groups, simpleOption.groupsLookup);
}
});
}
});
return groups;
};
export const rehydrateOrderItem = (menuItem, simpleCartItem, soldOut = [], requestedAt = null) => {
const orderItem = makeOrderItem(menuItem, true, soldOut, simpleCartItem, undefined, requestedAt);
orderItem.quantity = simpleCartItem.quantity || 1;
const groupsLookup = makeSimpleGroupsLookup(simpleCartItem.groups);
if (groupsLookup) {
orderItem.groups = rehydrateGroups(orderItem.groups, groupsLookup);
}
const pricedItem = calcPrices(orderItem);
return pricedItem;
};
export const rehydrateCart = (menuItems, simpleCartItems, soldOut = []) => {
const orderItems = [];
simpleCartItems.forEach(item => {
const menuItem = menuItems.find(i => i.id === item.id);
if (menuItem) {
const orderItem = rehydrateOrderItem(menuItem, item, soldOut);
orderItems.push(orderItem);
}
});
const cart = orderItems.map((i, index) => (Object.assign(Object.assign({}, i), { index })));
const cartCounts = calcCartCounts(cart);
return { cart, cartCounts };
};
// TODO: maybe refactor
const rehydrateDetails = (details) => {
return {
cart_id: details === null || details === void 0 ? void 0 : details.cart_id,
eating_utensils: details === null || details === void 0 ? void 0 : details.eating_utensils,
notes: details === null || details === void 0 ? void 0 : details.notes,
person_count: details === null || details === void 0 ? void 0 : details.person_count,
serving_utensils: details === null || details === void 0 ? void 0 : details.serving_utensils,
tax_exempt_id: details === null || details === void 0 ? void 0 : details.tax_exempt_id
};
};
// TODO: maybe refactor
const rehydrateCustomer = (customer) => {
return {
customer_id: customer.customer_id,
first_name: customer.first_name,
last_name: customer.last_name,
email: customer.email,
phone: customer.phone,
company: customer.company || ''
};
};
// TODO: maybe refactor
const rehydrateAddress = (address) => {
if (!address)
return {};
return {
unit: address.unit,
company: address.company,
contact: address.contact,
phone: address.phone
};
};
const rehydrateSurcharges = (surcharges) => {
return (surcharges === null || surcharges === void 0 ? void 0 : surcharges.filter(i => i.is_optional).map(i => ({ id: i.id }))) || [];
};
const rehydrateDiscounts = (discounts) => {
return ((discounts === null || discounts === void 0 ? void 0 : discounts.filter(i => !i.is_promo_code).map(i => ({ id: i.id, ext_id: i.ext_id }))) || []);
};
const rehydratePromoCodes = (discounts) => {
return (discounts === null || discounts === void 0 ? void 0 : discounts.filter(i => i.is_promo_code).map(i => i.name)) || [];
};
const rehydrateTenders = (tenders) => {
const other = tenders === null || tenders === void 0 ? void 0 : tenders.map(i => {
if (isCreditTender(i)) {
return Object.assign(Object.assign({}, i), i.credit_card);
}
else if (isHouseAccountTender(i)) {
return Object.assign(Object.assign({}, i), i.house_account);
}
else {
return i;
}
});
return other;
};
export const rehydrateCheckoutForm = (order) => {
const form = {
details: rehydrateDetails(order.details),
customer: order.customer && isOrderCustomer(order.customer)
? rehydrateCustomer(order.customer)
: {},
address: rehydrateAddress(order.address),
surcharges: rehydrateSurcharges(order.surcharges),
discounts: rehydrateDiscounts(order.discounts),
promoCodes: rehydratePromoCodes(order.discounts),
tenders: rehydrateTenders(order.tenders),
tip: order.totals.tip
};
return form;
};
// group orders
export const makeGuestLookup = (cartGuests) => {
return cartGuests.reduce((obj, i) => i.cart_guest_id ? Object.assign(Object.assign({}, obj), { [i.cart_guest_id]: i }) : Object.assign({}, obj), {});
};
export const compareCarts = (cart) => {
if (!cart)
return null;
const customerCart = cart.filter(i => !i.cartGuestId);
return customerCart.length < cart.length ? customerCart : null;
};
export const combineCarts = (cart, guestCart, cartOwner, cartGuests) => {
const guestLookup = makeGuestLookup(cartGuests);
const withGuestNames = guestCart
.filter(i => i.cartGuestId && guestLookup[i.cartGuestId])
.map(i => {
const guest = i.cartGuestId ? guestLookup[i.cartGuestId] : null;
const madeFor = guest ? `${guest === null || guest === void 0 ? void 0 : guest.first_name} ${guest === null || guest === void 0 ? void 0 : guest.last_name}` : null;
return Object.assign(Object.assign({}, i), { madeFor });
});
const withOwnerName = cart.map(i => {
return Object.assign(Object.assign({}, i), { customerId: cartOwner.customer_id, madeFor: `${cartOwner.first_name} ${cartOwner.last_name}` });
});
return [...withOwnerName, ...withGuestNames];
};
// display items from past orders
export const makeItemImageUrl = (images) => {
const imageMap = images
.filter(i => i.url)
.reduce((obj, i) => (Object.assign(Object.assign({}, obj), { [i.type]: i.url })), {});
return (imageMap.SMALL_IMAGE || imageMap.LARGE_IMAGE || imageMap.APP_IMAGE || null);
};
export const makeDisplayItemGroups = (optionGroups) => {
if (!optionGroups || !optionGroups.length)
return [];
return optionGroups.map(g => {
var _a;
const options = (_a = g.options) === null || _a === void 0 ? void 0 : _a.map(o => makeDisplayItem(o));
return Object.assign(Object.assign({}, g), { options });
});
};
export const makeDisplayItem = (item, isOption = false) => {
var _a;
const cals = item.nutritional_info ? (_a = item.nutritional_info) === null || _a === void 0 ? void 0 : _a.calories : null;
let displayItem = {
allergens: item.allergens,
cals: cals,
description: item.description,
groups: makeDisplayItemGroups(item.groups),
imageUrl: makeItemImageUrl(item.images),
id: item.id,
name: item.name,
nutritionalInfo: item.nutritional_info,
quantity: item.quantity,
points: null,
price: item.price,
shortDescription: item.short_description,
tags: item.tags,
totalPrice: item.price_total
};
if (!isOption) {
const itemDetails = {
madeFor: item.made_for,
notes: item.notes,
signature: makeItemSignature(displayItem)
};
displayItem = Object.assign(Object.assign({}, displayItem), itemDetails);
}
return displayItem;
};
export const makeDisplayItems = (orders) => {
const items = orders.reduce((items, order) => [...items, ...order.cart], []);
return items.map(i => makeDisplayItem(i));
};
export const makeUniqueDisplayItems = (orders) => {
const items = makeDisplayItems(orders);
const unique = [];
const signatures = [];
items.forEach(item => {
var _a;
if (signatures.includes((_a = item.signature) !== null && _a !== void 0 ? _a : ''))
return;
item.signature && signatures.push(item.signature);
unique.push(item);
});
return unique;
};
export const makeFavoritesLookup = (favorites) => {
if (!favorites)
return {};
return favorites
.filter(i => i.cart)
.reduce((obj, i) => (Object.assign(Object.assign({}, obj), { [makeItemSignature(i.cart)]: i.favorite_id })), {});
};
const makeCartItemOptionIds = (groups) => {
if (!groups || !groups.length)
return [];
return groups
.reduce((arr, group) => {
const ids = group.options
.filter(o => o.quantity > 0)
.reduce((optionsArr, o) => {
const optionArr = new Array(o.quantity);
optionArr.fill(o.id);
const nestedIds = makeCartItemOptionIds(o.groups);
return [...optionsArr, ...optionArr, ...nestedIds];
}, []);
return [...arr, ...ids];
}, [])
.sort((a, b) => a - b);
};
export const makeItemSignature = (item) => {
const optionIds = makeCartItemOptionIds(item.groups);
return [item.id, ...optionIds].join('.');
};
export const makeCartItemSignature = (item) => {
const optionIds = makeCartItemOptionIds(item.groups);
const itemIds = [item.id, ...optionIds].join('.');
const signature = `${itemIds}.${item.madeFor}.${item.notes}`;
return signature;
};
export const addItem = (cart, item) => {
if (typeof item.index === 'undefined') {
const itemSignature = makeCartItemSignature(item);
const itemSignatures = cart.map(i => (Object.assign({ signature: makeCartItemSignature(i) }, i)));
const match = itemSignatures.find(i => i.signature === itemSignature);
if (match && match.index !== undefined) {
const quantity = item.quantity + match.quantity;
const newItem = Object.assign(Object.assign({}, item), { quantity });
const updated = calcPrices(newItem);
cart[match.index] = Object.assign(Object.assign({}, updated), { index: match.index });
}
else {
cart.push(Object.assign(Object.assign({}, item), { index: cart.length }));
}
}
else {
cart[item.index] = item;
}
const cartCounts = calcCartCounts(cart);
return { cart, cartCounts };
};
const getItemRealIndex = (cart, index) => cart.findIndex(item => item.index === index);
export const removeItem = (cart, index) => {
const realIndex = getItemRealIndex(cart, index);
if (realIndex > -1) {
cart.splice(realIndex, 1);
cart = cart.map((i, _index) => (Object.assign(Object.assign({}, i), { index: _index })));
}
const cartCounts = calcCartCounts(cart);
return { cart, cartCounts };
};
export const incrementItem = (cart, index) => {
const realIndex = getItemRealIndex(cart, index);
if (realIndex > -1) {
const item = cart[realIndex];
if (item.max === 0 || item.quantity < item.max) {
let newQuantity = item.quantity + item.increment;
newQuantity =
item.max === 0 ? newQuantity : Math.min(item.max, newQuantity);
const newItem = calcPrices(Object.assign(Object.assign({}, item), { quantity: newQuantity }));
cart[realIndex] = newItem;
}
}
const cartCounts = calcCartCounts(cart);
return { cart, cartCounts };
};
export const decrementItem = (cart, index) => {
const realIndex = getItemRealIndex(cart, index);
if (realIndex > -1) {
const item = cart[realIndex];
const newQuantity = Math.max(item.quantity - item.increment, 0);
if (newQuantity === 0 || newQuantity < item.min) {
cart.splice(realIndex, 1);
cart = cart.map((i, _index) => (Object.assign(Object.assign({}, i), { index: _index })));
}
else {
const newItem = calcPrices(Object.assign(Object.assign({}, item), { quantity: newQuantity }));
cart[realIndex] = newItem;
}
}
const cartCounts = calcCartCounts(cart);
return { cart, cartCounts };
};
const makeSimpleGroups = (groups) => {
return groups.map(g => {
const options = g.options
.filter(o => o.quantity !== 0)
.map(o => ({
id: o.id,
quantity: o.quantity,
groups: makeSimpleGroups(o.groups || [])
}));
return { id: g.id, options: options };
});
};
export const makeSimpleCart = (cart) => {
const simpleCart = cart.map(i => {
return {
id: i.id,
quantity: i.quantity,
groups: makeSimpleGroups(i.groups),
made_for: i.madeFor || i.made_for || '',
notes: i.notes || ''
};
});
return simpleCart;
};
/* cart validation */
// TODO: refactor for nested modifiers
// convert menu items to order items for validation but only include
// items that are already in the cart to save wasted effort
const makeItemLookup = (categories, itemIds) => {
const itemLookup = {};
categories.forEach(category => {
category.items.forEach(item => {
if (itemIds.includes(item.id)) {
itemLookup[item.id] = makeOrderItem(item, true);
}
});
category.children.forEach(child => {
child.items.forEach(item => {
if (itemIds.includes(item.id)) {
itemLookup[item.id] = makeOrderItem(item, true);
}
});
});
});
return itemLookup;
};
// TODO: refactor for nested modifiers
const makeGroupsLookup = (item) => {
return item.groups.reduce((grpObj, i) => {
const options = i.options.reduce((optObj, o) => {
if (o.id) {
optObj[o.id] = o;
}
return optObj;
}, {});
grpObj[i.id] = Object.assign(Object.assign({}, i), { options });
return grpObj;
}, {});
};
// TODO: refactor for nested modifiers
export const validateCart = (cart, categories, soldOut, requestedAt) => {
const itemIds = cart.map(item => item.id);
const itemLookup = makeItemLookup(categories, itemIds);
let errors = null;
const missingItems = [];
const invalidItems = [];
let newCart = cart.map(oldItem => {
var _a, _b, _c, _d;
const newItem = itemLookup[oldItem.id];
if (!newItem ||
checkSoldOut(oldItem.id, newItem.suspend_until, soldOut, requestedAt)) {
missingItems.push(oldItem);
return null;
}
let missingGroups = [];
const invalidGroups = [];
const missingOptions = [];
const invalidOptions = [];
const newGroups = makeGroupsLookup(newItem);
// check if any required groups for the new item are missing from the old item
const newRequiredGroups = Object.values(newGroups).filter(i => i.min > 0);
const oldGroupIds = (_a = oldItem.groups) === null || _a === void 0 ? void 0 : _a.map(i => i.id);
missingGroups = newRequiredGroups.filter(i => !(oldGroupIds === null || oldGroupIds === void 0 ? void 0 : oldGroupIds.includes(i.id)));
const newEmptyGroups = Object.values(newGroups)
.filter((i) => {
return i.min === 0 && !(oldGroupIds === null || oldGroupIds === void 0 ? void 0 : oldGroupIds.includes(i.id));
})
.map((i) => (Object.assign(Object.assign({}, i), { options: Object.values(i.options) })));
const updatedGroups = (_b = oldItem.groups) === null || _b === void 0 ? void 0 : _b.map(group => {
const optionCount = group.options.reduce((t, i) => (t += i.quantity), 0);
const newGroup = newGroups[group.id];
// check if option group is still part of the item
if (!newGroup) {
if (optionCount > 0) {
invalidGroups.push(group);
return group;
}
else {
return null;
}
}
else {
const updatedOptions = group.options.map(option => {
const newOption = newGroup.options[option.id];
// check to see if any OLD options aren't avaiilable on the current menu
// or if any of the options are currently sold out
if (!newOption ||
(option.quantity > 0 &&
checkSoldOut(option.id, option.suspend_until, soldOut, requestedAt))) {
missingOptions.push(option);
return option;
}
else {
// check to see if option quantity is valid relative to option min & max
// ignore the option increment setting for the time being
if ((newOption.max && option.quantity > newOption.max) ||
(newOption.min && option.quantity < newOption.min)) {
invalidOptions.push(option);
return option;
}
else {
// update the old option price to the new option price
const updatedOption = Object.assign(Object.assign({}, option), { price: newOption.price });
return updatedOption;
}
}
});
// check to see if the option count is greater than the max (if one exists)
// or less than the min (if one exists)
if ((newGroup.max && optionCount > newGroup.max) ||
(newGroup.min && optionCount < newGroup.min)) {
invalidGroups.push(group);
}
const updatedGroup = Object.assign(Object.assign({}, group), { options: updatedOptions });
return updatedGroup;
}
});
if (missingGroups.length ||
invalidGroups.length ||
missingOptions.length ||
invalidOptions.length) {
const invalidItem = Object.assign(Object.assign({}, oldItem), { missingGroups,
invalidGroups,
missingOptions,
invalidOptions });
invalidItems.push(invalidItem);
return null;
}
else {
// check to see if this item's quantity is below the min or
// above the max from the current menu
if ((newItem.max && ((_c = oldItem.quantity) !== null && _c !== void 0 ? _c : 0) > newItem.max) ||
(newItem.min && ((_d = oldItem.quantity) !== null && _d !== void 0 ? _d : 0) < newItem.min)) {
const invalidItem = Object.assign(Object.assign({}, oldItem), { missingGroups,
invalidGroups,
missingOptions,
invalidOptions });
invalidItems.push(invalidItem);
return null;
}
const nonNullGroups = updatedGroups === null || updatedGroups === void 0 ? void 0 : updatedGroups.filter(i => i !== null);
const allNewGroups = [...(nonNullGroups !== null && nonNullGroups !== void 0 ? nonNullGroups : []), ...newEmptyGroups];
const updatedItem = Object.assign(Object.assign({}, oldItem), { groups: allNewGroups });
// update the old item price to the new item price
updatedItem.price = newItem.price;
const pricedItem = calcPrices(updatedItem);
return pricedItem;
}
});
newCart = newCart.filter(i => i !== null);
if (missingItems.length || invalidItems.length) {
errors = { missingItems, invalidItems };
}
return { newCart: newCart, errors };
};
/* order submission */
export const getDefaultTip = (config) => {
if (!config || !config.gratuity || !config.gratuity.has_tip)
return null;
return config.gratuity.default ? config.gratuity.default.amount : null;
};
export const prepareOrder = (data) => {
const order = {
revenue_center_id: data.revenueCenterId,
service_type: data.serviceType,
requested_at: data.requestedAt,
cart: makeSimpleCart(data.cart)
};
if (data.customer)
order.customer = data.customer;
if (data.details) {
const details = Object.assign({}, data.details);
// person_count must be submitted as integer
if (details.person_count)
details.person_count = parseInt(`${details.person_count}`) || null;
if (data.deviceType)
details.device_type = data.deviceType;
if (data.table || data.prepType) {
const prepType = data.prepType
? data.prepType
: data.table
? 'EAT_HERE'
: 'TAKE_OUT';
details.notes_internal =
prepType === 'EAT_HERE'
? data.table
? `DINE IN -- TABLE ${data.table}`
: 'DINE IN'
: 'TAKE OUT';
}
order.details = Object.assign({}, details);
}
if (data.surcharges)
order.surcharges = data.surcharges;
if (data.discounts)
order.discounts = data.discounts;
if (data.discountsInternal)
order.discounts_internal = data.discountsInternal;
if (data.promoCodes)
order.promo_codes = data.promoCodes;
if (data.points)
order.points = data.points;
if (data.tip)
order.tip = data.tip;
if (data.deposit)
order.deposit = data.deposit;
if (data.tenders)
order.tenders = data.tenders;
if (data.address && data.serviceType === 'DELIVERY') {
order.address = data.address;
}
else {
order.address = null;
}
if (data.orderId)
order.order_id = data.orderId;
if (data.cartId)
order.cart_id = data.cartId;
if (data.save)
order.save = data.save;
if (data.token)
order.token = data.token;
if (data.kount_device_session_id)
order.kount_device_session_id = data.kount_device_session_id;
return order;
};
export const checkAmountRemaining = (total, tenders) => {
if (!tenders)
return total ? parseFloat(total) : 0.0;
let remaining = parseFloat(total) -
tenders.reduce((t, i) => { var _a; return (t += parseFloat((_a = i.amount) !== null && _a !== void 0 ? _a : '0')); }, 0.0);
remaining = Math.round((remaining + Number.EPSILON) * 100) / 100;
return Object.is(remaining, -0) ? 0.0 : remaining;
};
export const makeDeviceType = (deviceType, deviceId) => {
// when app is only built in phone mode (not Universal) react-native-device-info shows device type as "Handset"
// so override it to "Tablet" if the deviceId has "iPad" in the name
deviceType =
deviceType === 'Handset' && !!deviceId && /iPad/.test(deviceId)
? 'Tablet'
: deviceType;
switch (deviceType === null || deviceType === void 0 ? void 0 : deviceType.toLowerCase()) {
case 'tablet':
return 'TABLET';
case 'mobile':
case 'handset':
return 'MOBILE';
case 'browser':
case 'desktop':
default:
return 'DESKTOP';
}
};