UNPKG

@megaads/wm

Version:

To install the library, use npm:

582 lines (522 loc) 23.6 kB
/** * 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[] { 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; } } }); 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) : number { let result = 0; const currentValueId = option.id; const currentProductVariant = this.currentProductVariant; if (!currentProductVariant || this.variants.length === 0) { return 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 result; } result = productVariant.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) { result = validBulkPriceItem.price; } } } return result; } 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 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) { value.price = this.getPriceVariantV2(value, variant.id, quantity, isBulkOrderItem); } } } return { product: this.currentProductVariant, variants: this.groupVariants, galleries: galleries, price: price, 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) { value.price = this.getPriceVariantV2(value, variant.id, quantity); } } } } 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;