@megaads/wm
Version:
To install the library, use npm:
595 lines (533 loc) • 23.9 kB
text/typescript
/**
* Variants
* @module Variants
* @exports Variants
* @type {{init: (function(*): Variants)}}
*/
import {
Variant,
ProductVariant,
Galleries,
VariantsOptions,
VariantStatistic,
ProductVariants,
ProductSkuDetail,
BulkPriceData
} from './types';
class Variants {
variants: Variant[];
productVariants: ProductVariant[];
galleries: Galleries;
bulkPriceData: BulkPriceData;
variantStatistic: VariantStatistic;
products: ProductVariants;
variantByOption: { [key: number]: number };
groupVariants: Variant[];
currentProductVariant: ProductVariant | null;
constructor(options: VariantsOptions) {
this.variants = options.variants;
this.productVariants = options.productVariants;
this.galleries = options.galleries;
this.bulkPriceData = options.bulkPriceData;
this.variantStatistic = this.buildVariantStatistic(options.variants, options.productVariants);
this.products = this.buildProductVariants(options.productVariants);
this.variantByOption = this.buildVariantByOption(options.variants);
this.groupVariants = this.buildGroupVariants(options.variants);
this.currentProductVariant = null;
}
private rebuildVariants(
variants: Variant[],
currentProductVariant: ProductVariant,
variantsStatistic: VariantStatistic,
variantByOption: { [key: number]: number },
products: ProductVariants,
groupVariants: Variant[]
) {
const groupVariantsCopy = groupVariants.map(variant => ({
...variant,
values: [...variant.values]
}));
for (let index = 0; index < variants.length; index++) {
const variant = variants[index];
let variantKeyMap = new Map();
currentProductVariant.variants.forEach(id => {
variantKeyMap.set(variantByOption[id], id);
})
let otherVariants = currentProductVariant.variants.filter(id => variantByOption[id] != variant.id);
otherVariants.forEach(id => {
variantKeyMap.set(variantByOption[id], id);
});
let optionGroup: { id: number; variant_id?: number; variant_slug?: string; }[] = [];
variant.values.forEach(item => {
let tmpKeyMap = new Map(variantKeyMap);
tmpKeyMap.set(variant.id, item.id);
let itemVariantKey = [...tmpKeyMap.values()].join('-');
let prefixVariant = '';
let variantIsOk = true;
if (variant.slug == 'style' && index >= 0) {
if (otherVariants && otherVariants.length >= 2) {
prefixVariant = otherVariants[0] + '-' + item.id;
variantIsOk = false;
if (prefixVariant && variantsStatistic) {
if (variantsStatistic[prefixVariant]) {
variantIsOk = true;
} else if (variantsStatistic[item.id + '-' + otherVariants[0]]) {
variantIsOk = true;
}
}
if (index == 0 && !variantIsOk && variantsStatistic[item.id]) {
variantIsOk = true;
}
}
}
if (variantIsOk && ((products.productByUniqId[itemVariantKey] && products.productByUniqId[itemVariantKey].id) || variant.show_invalid)) {
optionGroup.push(item);
} else if (variantIsOk && variant.show_invalid_above) {
let aboveKey = currentProductVariant.variants.filter((id, idx) => idx < index).join('-');
if (products.productByUniqIdAbove[aboveKey]) {
optionGroup.push(item);
}
}
});
for (const groupVariant of groupVariantsCopy) {
if (groupVariant.id == variant.id) {
groupVariant.values = optionGroup;
}
}
}
this.groupVariants = groupVariantsCopy;
}
private buildVariantStatistic (variants: Variant[], productVariants: ProductVariant[]): VariantStatistic {
let variantsStatistic: { [key: string]: {id: number, sku_key: string, count: number, price: number, high_price: number} } = {};
if (variants.length < 2) {
return variantsStatistic;
}
let firstOptionIndex = 0;
for (let key in variants) {
if (variants[key].type == 'OPTION') {
firstOptionIndex = parseInt(key);
break;
}
}
for (let item of productVariants) {
let vKeyArr = item.variants.filter((value, index) => index <= firstOptionIndex).map(item => item);
for (let i = firstOptionIndex + 1; i < item.variants.length; i++) {
let vKey = vKeyArr.join('-') + '-' + item.variants[i];
if (!variantsStatistic[vKey]) {
variantsStatistic[vKey] = {
id: item.id,
sku_key: item.variants.join('-'),
count: 1,
price: item.price,
high_price: item.high_price
}
} else {
variantsStatistic[vKey].count++;
if (item.price < variantsStatistic[vKey].price) {
variantsStatistic[vKey].id = item.id;
variantsStatistic[vKey].sku_key = item.variants.join('-');
variantsStatistic[vKey].price = item.price;
variantsStatistic[vKey].high_price = item.high_price;
}
}
}
let vKey = vKeyArr.join('-');
if (!variantsStatistic[vKey]) {
variantsStatistic[vKey] = {
id: item.id,
sku_key: item.variants.join('-'),
count: 1,
price: item.price,
high_price: item.high_price
}
} else {
variantsStatistic[vKey].count++;
if (item.price < variantsStatistic[vKey].price) {
variantsStatistic[vKey].id = item.id;
variantsStatistic[vKey].sku_key = item.variants.join('-');
variantsStatistic[vKey].price = item.price;
variantsStatistic[vKey].high_price = item.high_price;
}
}
}
return variantsStatistic;
}
private buildProductVariants(productVariants: ProductVariant[]): ProductVariants {
const productById: { [key: number]: {id: number, sku: string, price: number, high_price: number, variants: number[]} } = {};
const productByUniqId: { [key: string]: any } = {};
const productByUniqIdAbove: { [key: string]: any } = {};
if (productVariants) {
productVariants.forEach(function(item) {
productById[item.id] = {
id: item.id,
sku: item.sku,
price: item.price,
high_price: item.high_price,
variants: item.variants
};
const variantOptionIds = item.variants;
if (variantOptionIds.length > 0) {
const key = variantOptionIds.join("-");
productByUniqId[key] = item;
if (item.variants.length > 2) {
for (let j = 1; j < variantOptionIds.length - 2; j++) {
let aboveKey = variantOptionIds.slice(0, j).join('-');
productByUniqIdAbove[aboveKey] = item;
}
}
}
});
}
return {
productById: productById,
productByUniqId: productByUniqId,
productByUniqIdAbove: productByUniqIdAbove
};
}
private buildVariantByOption(variants: Variant[]): { [key: number]: number } {
let variantByOption: { [key: number]: number } = {};
let optionById: { [key: number]: any } = {};
variants.forEach(variant => {
variant.values.forEach(item => {
variantByOption[item.id] = variant.id
item.variant_id = variant.id;
item.variant_slug = variant.slug;
optionById[item.id] = item;
})
});
return variantByOption;
}
private buildGroupVariants(variants: Variant[]): Variant[] {
const hasOnlyOneOption = variants.filter((item, index) => item.type == "OPTION" && index < variants.length - 1).length == 1;
variants.forEach((element, index) => {
element.show_invalid = index == 0;
if (variants[0].type != 'OPTION') {
if (index <= variants.length - 3) {
element.show_invalid = true;
}
if (index <= variants.length - 2 && element.type != "IMAGE") {
element.show_invalid_above = true;
}
} else {
if (index && index == variants.length - 2 && element.type == "OPTION") {
element.show_invalid_above = true;
}
if (index == 1 && variants.length > 2 && element.type == "OPTION") {
element.show_invalid = true;
}
}
if (hasOnlyOneOption && element.type == "OPTION" && index < variants.length - 1) {
element.show_invalid = true;
}
});
return variants;
}
private isSelectedVariantValue(variantValueId: number): boolean {
if (!this.currentProductVariant || !this.currentProductVariant.variants) {
return false;
}
let isExists = this.currentProductVariant.variants.find(id => id == variantValueId);
return !!isExists;
}
private getPriceVariant(option: {
id: number;
variant_id?: number;
variant_slug?: string;
is_selected?: boolean,
name?: string,
slug?: string,
price?: number,
high_price?: number,
}, groupId: number, quantity: number): number {
let result = 0;
if (this.variants.length > 0) {
if (!this.currentProductVariant) {
return result;
}
let isStyleFirstVariant = this.variants[0].slug == 'style';
let listIndex: (number | string)[] = [];
let variantKeyMap = new Map();
this.currentProductVariant.variants.forEach((optionId: any) => {
if (this.variantByOption[optionId] != groupId) {
variantKeyMap.set(this.variantByOption[optionId], optionId);
listIndex.push(optionId);
} else {
variantKeyMap.set(this.variantByOption[option.id], option.id);
}
});
let key = [...variantKeyMap.values()].join('-');
let currentProductBySpid = this.products.productById[this.currentProductVariant.id];
if (key in this.products.productByUniqId) {
result = this.products.productByUniqId[key].price;
} else {
let prefixVariant = '';
let variantIsOk = true;
if (listIndex && listIndex.length >= 2) {
prefixVariant = listIndex[0] + '-' + option.id;
variantIsOk = false;
if (this.currentProductVariant && this.currentProductVariant.id) {
let keyWithColor = currentProductBySpid.variants
.filter(id => this.variantByOption[id] != 2)
.map(id => this.variantByOption[id] == this.variantByOption[option.id] ? option.id : id).join('-');
if (this.variantStatistic[keyWithColor]) {
variantIsOk = true;
prefixVariant = keyWithColor;
}
}
if (prefixVariant && this.variantStatistic) {
if (this.variantStatistic[prefixVariant]) {
variantIsOk = true;
} else if (this.variantStatistic[option.id + '-' + listIndex[0]]) {
variantIsOk = true;
prefixVariant = option.id + '-' + listIndex[0];
}
if (!variantIsOk && isStyleFirstVariant && this.variantStatistic[option.id]) {
prefixVariant = option.id.toString();
variantIsOk = true;
}
}
}
if (variantIsOk && key) {
if (prefixVariant && this.variantStatistic && this.variantStatistic[prefixVariant]) {
result = this.variantStatistic[prefixVariant].price;
}
}
if (!result) {
// find index of option in variants
const index = this.variants.findIndex(v => v.id === groupId);
// append option id to the index of listIndex
listIndex.splice(index, 0, option.id);
prefixVariant = listIndex.join('-');
if (this.variantStatistic && this.variantStatistic[prefixVariant]) {
result = this.variantStatistic[prefixVariant].price;
}
}
// apply bulk price
if (this.variantStatistic && this.variantStatistic[prefixVariant]) {
const productSkuId = this.variantStatistic[prefixVariant].id;
if (this.bulkPriceData[productSkuId]) {
const bulkPriceDataBySkuId = this.bulkPriceData[productSkuId];
const validBulkPriceItems = bulkPriceDataBySkuId.filter((item) => {
return item.min_quantity <= quantity;
});
if (validBulkPriceItems.length > 0) {
const validBulkPriceItem = validBulkPriceItems.pop();
if (validBulkPriceItem) {
result = validBulkPriceItem.price;
}
}
}
}
}
}
return result;
}
getPriceVariantV2(option: {
id: number;
variant_id?: number;
variant_slug?: string;
is_selected?: boolean,
name?: string,
slug?: string,
price?: number,
high_price?: number,
}, groupId: number, quantity: number, isBulkOrderItem = true) : {price: number, original_price: number} {
let result = 0;
const currentValueId = option.id;
const currentProductVariant = this.currentProductVariant;
if (!currentProductVariant || this.variants.length === 0) {
return { price: result, original_price: result };
}
// build list variant value ids
let valueIds: number[] = [];
this.groupVariants.forEach((variant) => {
variant.values.forEach((value) => {
if (value.is_selected) {
valueIds.push(value.id);
}
});
});
const productSkuId = this.getSkuIdByValueIds(valueIds, currentValueId);
const productVariant = this.productVariants.find(v => v.id === productSkuId);
if (!productVariant) {
return { price: result, original_price: result };
}
result = productVariant.price;
let originalPrice = result;
if (this.bulkPriceData[productSkuId] && isBulkOrderItem) {
const bulkPriceDataBySkuId = this.bulkPriceData[productSkuId];
const validBulkPriceItems = bulkPriceDataBySkuId.filter((item) => {
return item.min_quantity <= quantity;
});
if (validBulkPriceItems.length > 0) {
const validBulkPriceItem = validBulkPriceItems.pop();
if (validBulkPriceItem) {
result = validBulkPriceItem.price;
}
}
}
return { price: result, original_price: originalPrice };
}
getProductSkuDetail(productSkuId: number, quantity: number, isBulkOrderItem = true): ProductSkuDetail {
if (!this.variants) {
throw new Error('Variants Data not set');
}
if (!this.productVariants) {
throw new Error('Product Variants Data not set');
}
if (!this.galleries) {
throw new Error('Galleries not set');
}
this.currentProductVariant = this.productVariants.find(v => v.id === productSkuId) || null;
if (!this.currentProductVariant) {
throw new Error(`Product SKU ${productSkuId} not found`);
}
this.rebuildVariants(
this.variants,
this.currentProductVariant,
this.variantStatistic,
this.variantByOption,
this.products,
this.groupVariants
);
let galleries = this.galleries[this.currentProductVariant.id] ?? [];
let price = this.currentProductVariant.price;
let originalPrice = this.currentProductVariant.price;
let highPrice = this.currentProductVariant.high_price;
// apply bulk price
if (this.bulkPriceData[productSkuId] && isBulkOrderItem) {
const bulkPriceDataBySkuId = this.bulkPriceData[productSkuId];
const validBulkPriceItems = bulkPriceDataBySkuId.filter((item) => {
return item.min_quantity <= quantity;
});
if (validBulkPriceItems.length > 0) {
const validBulkPriceItem = validBulkPriceItems.pop();
if (validBulkPriceItem) {
price = validBulkPriceItem.price;
}
}
}
for (const variant of this.groupVariants) {
for (const value of variant.values) {
value.is_selected = this.isSelectedVariantValue(value.id);
if (value.is_selected) {
variant.current_value_id = value.id;
variant.current_value_name = value.name;
}
}
}
for (const variant of this.groupVariants) {
if (variant.type === 'OPTION') {
for (const value of variant.values) {
const { price, original_price } = this.getPriceVariantV2(value, variant.id, quantity, isBulkOrderItem);
value.price = price;
value.original_price = original_price;
}
}
}
return {
product: this.currentProductVariant,
variants: this.groupVariants,
galleries: galleries,
price: price,
original_price: originalPrice,
high_price: highPrice
}
}
getSkuIdByValueIds(valueIds: number[], selectedValueId: number): number {
const getMatchCount = (source: any, target: any) => {
let matchCount = 0;
for (let i = 0; i < source.length; i++) {
if (source[i] === target[i]) {
matchCount++;
}
}
return matchCount;
}
let productVariantMatch = null
let highestMatch = -1;
for (const productVariant of this.productVariants) {
if (!productVariant.variants.includes(selectedValueId)) {
continue;
}
const currentMatches = getMatchCount(valueIds, productVariant.variants);
if (currentMatches > highestMatch) {
highestMatch = currentMatches;
productVariantMatch = productVariant;
}
}
if (!productVariantMatch) {
productVariantMatch = this.productVariants[0];
}
return productVariantMatch ? productVariantMatch.id : 0;
}
applyBulkPrice(productSkuDetail: ProductSkuDetail, quantity: number): void {
const productSkuId = productSkuDetail.product.id;
if (this.bulkPriceData[productSkuId]) {
const bulkPriceDataBySkuId = this.bulkPriceData[productSkuId];
const validBulkPriceItems = bulkPriceDataBySkuId.filter((item) => {
return item.min_quantity <= quantity;
});
if (validBulkPriceItems.length > 0) {
const validBulkPriceItem = validBulkPriceItems.pop();
if (validBulkPriceItem) {
productSkuDetail.price = validBulkPriceItem.price;
}
} else if (this.currentProductVariant) {
productSkuDetail.price = this.currentProductVariant.price;
}
}
for (const variant of productSkuDetail.variants) {
if (variant.type === 'OPTION') {
for (const value of variant.values) {
const { price, original_price } = this.getPriceVariantV2(value, variant.id, quantity);
value.price = price;
value.original_price = original_price;
}
}
}
}
getBulkPriceText(productSkuDetail: ProductSkuDetail, quantity: number): { text: string, price?: string, quantity?: number } {
let buyMoreByQuantityText = '#price each for #quantity items';
let spid = productSkuDetail.product.id;
if (this.bulkPriceData[spid]) {
let price = 0;
let minQuantity = 0;
const bulkPriceDataBySkuId = this.bulkPriceData[spid];
const validBulkPriceItems = bulkPriceDataBySkuId.filter((item) => {
return item.min_quantity >= quantity;
});
if (validBulkPriceItems.length > 0) {
let validBulkPriceItem = validBulkPriceItems.reduce((closest, current) => {
return current.min_quantity < closest.min_quantity ? current : closest;
}, validBulkPriceItems[0]);
if (validBulkPriceItem) {
price = validBulkPriceItem.price;
minQuantity = validBulkPriceItem.min_quantity;
return {
text: buyMoreByQuantityText,
price: price.toFixed(2),
quantity: minQuantity
};
}
} else {
let validBulkPriceItem = this.bulkPriceData[spid].reduce((closest, current) => {
return current.min_quantity > closest.min_quantity ? current : closest;
}, this.bulkPriceData[spid][0]);
if (validBulkPriceItem) {
price = validBulkPriceItem.price;
minQuantity = validBulkPriceItem.min_quantity;
return {
text: buyMoreByQuantityText,
price: price.toFixed(2),
quantity: minQuantity
};
}
}
}
return {
text: 'Buying In Bulk?'
}
}
}
export default Variants;