UNPKG

@fabrix/spool-cart

Version:

Spool - eCommerce Spool for Fabrix

842 lines (841 loc) 37.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const common_1 = require("@fabrix/fabrix/dist/common"); const csvParser = require("papaparse"); const _ = require("lodash"); const shortid = require("shortid"); const fs = require("fs"); const enums_1 = require("../../enums"); const enums_2 = require("../../enums"); class ProductCsvService extends common_1.FabrixService { productCsv(file) { console.time('csv'); const uploadID = shortid.generate(); const EventsService = this.app.services.EventsService; const errors = []; let errorsCount = 0, lineNumber = 1; return new Promise((resolve, reject) => { const options = { header: true, dynamicTyping: true, encoding: 'utf-8', step: (results, parser) => { parser.pause(); lineNumber++; return this.csvProductRow(results.data[0], uploadID) .then(row => { parser.resume(); }) .catch(err => { errorsCount++; this.app.log.error('ROW ERROR', err); errors.push(`Line ${lineNumber}: ${err.message}`); parser.resume(); }); }, complete: (results, _file) => { console.timeEnd('csv'); results.upload_id = uploadID; EventsService.count('ProductUpload', { where: { upload_id: uploadID } }) .then(count => { results.products = count; results.errors_count = errorsCount; results.errors = errors; EventsService.publish('product_upload.complete', results); return resolve(results); }) .catch(err => { errorsCount++; errors.push(err.message); results.errors_count = errorsCount; results.errors = errors; return resolve(results); }); }, error: (err, _file) => { return reject(err); } }; const fileString = fs.readFileSync(file, 'utf8'); csvParser.parse(fileString, options); }); } csvProductRow(row, uploadID) { return Promise.resolve() .then(() => { const ProductUpload = this.app.models.ProductUpload; const values = _.values(enums_1.PRODUCT_UPLOAD); const keys = _.keys(enums_1.PRODUCT_UPLOAD); const google = {}; const amazon = {}; const upload = { upload_id: uploadID, options: {}, properties: {}, images: [], variant_images: [], vendors: [], collections: [], associations: [], tags: [] }; _.each(row, (data, key) => { if (typeof (data) === 'undefined' || data === '') { row[key] = null; } }); row = _.omitBy(row, _.isNil); if (_.isEmpty(row)) { return Promise.resolve({}); } _.each(row, (data, key) => { if (typeof (data) !== 'undefined' && data !== null && data !== '') { const i = values.indexOf(key.replace(/^\s+|\s+$/g, '')); const k = keys[i]; if (i > -1 && k) { if (k === 'handle') { upload[k] = this.app.services.ProxyCartService.handle(data); } else if (k === 'title') { upload[k] = this.app.services.ProxyCartService.title(data); } else if (k === 'seo_title') { upload[k] = this.app.services.ProxyCartService.title(data); } else if (k === 'seo_description') { upload[k] = this.app.services.ProxyCartService.description(data); } else if (k === 'price') { upload[k] = parseInt(data, 10); } else if (k === 'compare_at_price') { upload[k] = parseInt(data, 10); } else if (k === 'tags') { upload[k] = _.uniq(data.toString().split(',').map(tag => { return tag.toLowerCase().trim(); })); } else if (k === 'images') { upload[k] = data.toString().split(',').map(image => { return image.trim(); }); } else if (k === 'images_alt') { upload[k] = data.toString().split('|').map(alt => { return alt.trim(); }); } else if (k === 'variant_images') { upload[k] = data.toString().split(',').map(image => { return image.trim(); }); } else if (k === 'variant_images_alt') { upload[k] = data.toString().split('|').map(alt => { return alt.trim(); }); } else if (k === 'collections') { upload[k] = _.uniq(data.toString().split(',').map(collection => { return collection.trim(); })); } else if (k === 'associations') { upload[k] = data.toString().split(',').map(association => { return association.trim(); }); } else if (k === 'exclude_payment_types') { upload[k] = data.toString().split(',').map(type => { return type.trim(); }); } else if (k === 'vendors') { upload[k] = data.toString().split(',').map(vendor => { return vendor.trim(); }); } else if (k === 'shops') { upload[k] = data.toString().split(',').map(shop => { return shop.trim(); }); } else if (k === 'shops_quantity') { upload[k] = data.toString().split(',').map(shopQty => { return parseInt(shopQty.trim(), 10); }); } else if (k === 'weight_unit') { upload[k] = data.toString().toLowerCase().trim(); } else if (k === 'inventory_policy') { upload[k] = data.toString().toLowerCase().trim(); } else if (k === 'published') { upload[k] = data.toString().toLowerCase().trim() === 'false' ? false : true; } else if (k === 'available') { upload[k] = data.toString().toLowerCase().trim() === 'false' ? false : true; } else if (k === 'requires_shipping') { upload[k] = data.toString().toLowerCase().trim() === 'false' ? false : true; } else if (k === 'requires_taxes') { upload[k] = data.toString().toLowerCase().trim() === 'false' ? false : true; } else if (k === 'requires_subscription') { upload[k] = data.toString().toLowerCase().trim() === 'false' ? false : true; } else if (k === 'g_product_category') { google['g_product_category'] = data.toString().trim(); } else if (k === 'g_gender') { google['g_gender'] = data.toString().trim(); } else if (k === 'g_age_group') { google['g_age_group'] = data.toString().trim(); } else if (k === 'g_mpn') { google['g_mpn'] = data.toString().trim(); } else if (k === 'g_adwords_grouping') { google['g_adwords_grouping'] = data.toString().trim(); } else if (k === 'g_adwords_label') { google['g_adwords_label'] = data.toString().trim(); } else if (k === 'g_condition') { google['g_condition'] = data.toString().trim(); } else if (k === 'g_custom_product') { google['g_custom_product'] = data.toString().trim(); } else if (k === 'g_custom_label_0') { google['g_custom_label_0'] = data.toString().trim(); } else if (k === 'g_custom_label_1') { google['g_custom_label_1'] = data.toString().trim(); } else if (k === 'g_custom_label_2') { google['g_custom_label_2'] = data.toString().trim(); } else if (k === 'g_custom_label_3') { google['g_custom_label_3'] = data.toString().trim(); } else if (k === 'g_custom_label_4') { google['g_custom_label_4'] = data.toString().trim(); } else if (k === 'metadata') { let formatted = data.toString().trim(); if (this.app.services.ProxyCartService.isJson(formatted)) { formatted = JSON.parse(formatted); upload[k] = formatted; } } else if (k === 'discounts') { let formatted = data.toString().trim(); if (this.app.services.ProxyCartService.isJson(formatted)) { formatted = JSON.parse(formatted); upload[k] = formatted; } } else { upload[k] = data; } } else { const optionsReg = new RegExp('^((Option \/).([0-9]+).(Name|Value))', 'g'); const propertyReg = new RegExp('^((Property Pricing \/).([0-9]+).(Name|Group|Value|Image|Multi Select))', 'g'); const matchOptions = optionsReg.exec(key); if (matchOptions && matchOptions[2] === 'Option /' && typeof matchOptions[3] !== 'undefined' && typeof matchOptions[4] !== 'undefined') { const part = matchOptions[4].toLowerCase(); const index = Number(matchOptions[3]) - 1; if (typeof upload.options[index] === 'undefined') { upload.options[index] = { name: '', value: '' }; } upload.options[index][part] = typeof data === 'string' ? data.trim() : data; } const matchProperties = propertyReg.exec(key); if (matchProperties && matchProperties[2] === 'Property Pricing /' && typeof matchProperties[3] !== 'undefined' && typeof matchProperties[4] !== 'undefined') { const part = matchProperties[4].toLowerCase().replace(' ', '_'); const index = Number(matchProperties[3]) - 1; if (typeof upload.properties[index] === 'undefined') { upload.properties[index] = { name: '', group: null, value: '', image: null }; } upload.properties[index][part] = typeof data === 'string' ? data.trim() : data; } } } }); upload.option = {}; _.map(upload.options, option => { upload.option[option.name] = option.value; }); delete upload.options; upload.property_pricing = {}; _.map(upload.properties, property => { upload.property_pricing[property.name] = {}; upload.property_pricing[property.name]['price'] = property.value; if (property.group) { upload.property_pricing[property.name]['group'] = property.group; } else { upload.property_pricing[property.name]['group'] = null; } if (property.image) { upload.property_pricing[property.name]['image'] = property.image; } else { upload.property_pricing[property.name]['image'] = null; } if (property.multi_select !== 'undefined') { upload.property_pricing[property.name]['multi_select'] = property.multi_select; } else { upload.property_pricing[property.name]['multi_select'] = true; } }); delete upload.properties; upload.images = upload.images.map((image, index) => { return { src: image, alt: upload.images_alt && upload.images_alt[index] ? this.app.services.ProxyCartService.description(upload.images_alt[index]) : '' }; }); delete upload.images_alt; upload.images = upload.images.filter(image => image); upload.variant_images = upload.variant_images.map((image, index) => { return { src: image, alt: upload.variant_images_alt && upload.variant_images_alt[index] ? this.app.services.ProxyCartService.description(upload.variant_images_alt[index]) : null }; }); delete upload.variant_images_alt; upload.variant_images = upload.variant_images.filter(image => image); upload.vendors = upload.vendors.map(vendor => { return { handle: this.app.services.ProxyCartService.handle(vendor), name: this.app.services.ProxyCartService.title(vendor) }; }); upload.vendors = upload.vendors.filter(vendor => vendor); upload.vendors = _.uniqBy(upload.vendors, 'handle'); upload.collections = upload.collections.map(collection => { if (collection !== '') { let position; if (/:/.test(collection)) { position = Number(collection.split(':')[1]); collection = collection.split(':')[0]; } return { handle: this.app.services.ProxyCartService.handle(collection), title: collection, product_position: position }; } }); upload.collections = upload.collections.filter(collection => collection); upload.collections = _.uniqBy(upload.collections, 'handle'); upload.tags = upload.tags.map(tag => { if (tag !== '') { return { name: this.app.services.ProxyCartService.name(tag) }; } }); upload.tags = upload.tags.filter(tag => tag); upload.tags = _.uniqBy(upload.tags, 'name'); upload.associations = upload.associations.map(association => { const product = association.split(/:(.+)/); const handle = this.app.services.ProxyCartService.handle(product[0]); const sku = this.app.services.ProxyCartService.title(product[1]); const res = {}; if (handle && handle !== '') { res.handle = handle; if (sku && sku !== '') { res.sku = sku; } return res; } return; }); upload.associations = upload.associations.filter(association => association); upload.google = google; upload.amazon = amazon; return ProductUpload.build(upload).save(); }); } processProductUpload(uploadId) { const ProductUpload = this.app.models['ProductUpload']; let errors = [], productsTotal = 0, variantsTotal = 0, associationsTotal = 0, errorsCount = 0; return ProductUpload.batch({ where: { upload_id: uploadId }, offset: 0, limit: 10, attributes: ['handle'], group: ['handle'], regressive: false, }, (products) => { const Sequelize = this.app.models['Product'].sequelize; return Sequelize.Promise.mapSeries(products, product => { return this.processProductGroup(product.handle, uploadId, {}) .then((results) => { productsTotal = productsTotal + (results.products || 0); variantsTotal = variantsTotal + (results.variants || 0); associationsTotal = associationsTotal + (results.associations || 0); errorsCount = errorsCount + (results.errors_count || 0); errors = errors.concat(results.errors); return results; }) .catch(err => { errors.push(err.message); return; }); }); }) .then(results => { return ProductUpload.destroy({ where: { upload_id: uploadId } }) .catch(err => { errors.push(err.message); return err; }); }) .then(destroyed => { return this.processProductAssociationUpload(uploadId, {}) .then(results => { associationsTotal = associationsTotal + (results.associations || 0); return results; }) .catch(err => { errors.push(err.message); return; }); }) .then(() => { const results = { upload_id: uploadId, products: productsTotal, variants: variantsTotal, associations: associationsTotal, errors_count: errorsCount, errors: errors }; this.app.services.EventsService.publish('product_process.complete', results); return results; }); } processProductGroup(handle, uploadId, options) { options = options || {}; this.app.log.debug('ProductCsvService.processProductGroup: processing', handle, uploadId); const Product = this.app.models['Product']; const ProductUpload = this.app.models['ProductUpload']; const AssociationUpload = this.app.models['ProductAssociationUpload']; const associations = []; const errors = []; let errorsCount = 0, productsCount = 0, variantsCount = 0, associationsCount = 0; return Product.datastore.transaction(t => { options.transaction = t; return ProductUpload.findAll({ where: { handle: handle, upload_id: uploadId }, order: [['id', 'ASC']], }) .then(products => { products = products.map(product => { return _.omit(product.get({ plain: true }), ['id', 'upload_id', 'created_at', 'updated_at']); }); products = products.map(product => { if (product.associations) { product.associations.forEach((a, index) => { const association = { upload_id: uploadId, product_handle: product.handle || null, product_sku: product.sku || null, associated_product_handle: a.handle || null, associated_product_sku: a.sku || null, position: index + 1 }; associations.push(association); }); } delete product.associations; return product; }); if (!products[0].title || products[0].title === '') { products = products.sort((a, b) => { if (a.title > b.title) { return 1; } else if (a.title < b.title) { return 0; } else { return -1; } }); } const defaultProduct = products.shift(); if (!defaultProduct) { throw new Error(`${handle}: Default Product could not be established`); } if (!defaultProduct.handle) { throw new Error(`${handle}: Default Product could not be established, missing handle`); } if (!defaultProduct.title) { throw new Error(`${handle}: Default Product could not be established, missing title`); } defaultProduct.variants = products.map(variant => { if (!variant) { throw new Error(`${handle}: Missing`); } else if (!variant.handle) { throw new Error(`${handle}: Missing Handle`); } else if (!variant.sku) { throw new Error(`${handle}: Missing Variant SKU`); } else { variant.images = variant.variant_images; return _.omit(variant, ['variant_images']); } }); defaultProduct.variants = defaultProduct.variants.filter(variant => variant); return this.app.services.ProductService.addProduct(defaultProduct, { transaction: options.transaction || null }) .then(createdProduct => { if (!createdProduct) { throw new Error(`${handle} was not created`); } productsCount = 1; if (createdProduct.variants) { variantsCount = createdProduct.variants.length; } return; }) .catch(err => { this.app.log.error(err); errorsCount++; errors.push(`${handle}: ${err.message}`); return; }); }) .then(() => { return AssociationUpload.bulkCreate(associations) .then(uploadedAssociations => { associationsCount = uploadedAssociations ? uploadedAssociations.length : 0; return; }) .catch(err => { errorsCount++; errors.push(err.message); return; }); }) .then(uploadedAssociations => { return { products: productsCount, variants: variantsCount, associations: associationsCount, errors_count: errorsCount, errors: errors }; }) .catch(err => { errorsCount++; errors.push(err.message); return { products: productsCount, variants: variantsCount, associations: associationsCount, errors_count: errorsCount, errors: errors }; }); }); } productMetaCsv(file) { console.time('csv'); const uploadID = shortid.generate(); const EventsService = this.app.services.EventsService; const errors = []; let errorsCount = 0, lineNumber = 1; return new Promise((resolve, reject) => { const options = { header: true, dynamicTyping: true, encoding: 'utf-8', step: (results, parser) => { parser.pause(); lineNumber++; return this.csvProductMetaRow(results.data[0], uploadID) .then(row => { parser.resume(); }) .catch(err => { errorsCount++; errors.push(`Line ${lineNumber}: ${err.message}`); this.app.log.error('ROW ERROR', err); parser.resume(); }); }, complete: (results, _file) => { console.timeEnd('csv'); results.upload_id = uploadID; EventsService.count('ProductMetaUpload', { where: { upload_id: uploadID } }) .then(count => { results.products = count; results.errors = errors; results.errors_count = errorsCount; EventsService.publish('product_meta_upload.complete', results); return resolve(results); }) .catch(err => { errorsCount++; errors.push(err.message); results.errors = errors; results.errors_count = errorsCount; return resolve(results); }); }, error: (err, _file) => { reject(err); } }; const fileString = fs.readFileSync(file, 'utf8'); csvParser.parse(fileString, options); }); } csvProductMetaRow(row, uploadID) { return Promise.resolve() .then(() => { const ProductMetaUpload = this.app.models.ProductMetaUpload; const values = _.values(enums_2.PRODUCT_META_UPLOAD); const keys = _.keys(enums_2.PRODUCT_META_UPLOAD); const upload = { upload_id: uploadID, data: {} }; _.each(row, (data, key) => { if (typeof (data) === 'undefined' || data === '') { row[key] = null; } }); row = _.omitBy(row, _.isNil); if (_.isEmpty(row)) { return Promise.resolve({}); } _.each(row, (data, key) => { if (typeof (data) !== 'undefined' && data !== null && data !== '') { const i = values.indexOf(key.replace(/^\s+|\s+$/g, '')); const k = keys[i]; if (i > -1 && k) { if (k === 'handle') { upload[k] = this.app.services.ProxyCartService.splitHandle(data); } else { upload[k] = data; } } else { let formatted = data; if (this.app.services.ProxyCartService.isJson(formatted)) { formatted = JSON.parse(formatted); } upload.data[key] = formatted; } } }); return ProductMetaUpload.build(upload).save(); }); } processProductMetaUpload(uploadId, options = {}) { const ProductMetaUpload = this.app.models.ProductMetaUpload; const Metadata = this.app.models.Metadata; const Product = this.app.models.Product; const ProductVariant = this.app.models.ProductVariant; const errors = []; let productsTotal = 0, errorsCount = 0; return ProductMetaUpload.batch({ where: { upload_id: uploadId } }, metadatums => { const Sequelize = this.app.models.Product.sequelize; return Sequelize.Promise.mapSeries(metadatums, metadata => { const Type = metadata.handle.indexOf(':') === -1 ? Product : ProductVariant; let where = {}; const includes = [ { model: Metadata.instance, as: 'metadata', attributes: ['data', 'id'] } ]; if (Type === Product) { where = { 'handle': metadata.handle }; } else if (Type === ProductVariant) { const product = metadata.handle.split(/:(.+)/); where = { 'sku': this.app.services.ProxyCartService.sku(product[1]), '$Product.handle$': this.app.services.ProxyCartService.handle(product[0]) }; includes.push({ model: Product.instance, attributes: ['handle'] }); } else { const err = new Error(`Target ${metadata.handle} not a Product or a Variant`); errorsCount++; errors.push(err.message); return; } return Type.findOne({ where: where, attributes: ['id'], include: includes, transaction: options.transaction || null }) .then(target => { if (!target) { const err = new Error(`Target ${Type.getTableName()} ${metadata.handle} not found`); errorsCount++; errors.push(err.message); return; } if (target.metadata) { target.metadata.data = metadata.data; return target.metadata.save({ transaction: options.transaction || null }) .then(() => { productsTotal++; return; }) .catch(err => { errorsCount++; errors.push(`${metadata.handle}: ${err.message}`); return; }); } else { return target.createMetadata({ data: metadata.data }, { transaction: options.transaction || null }) .then(() => { productsTotal++; return; }) .catch(err => { errorsCount++; errors.push(`${metadata.handle}: ${err.message}`); return; }); } }) .catch(err => { errorsCount++; errors.push(err.message); return; }); }); }) .then(results => { return ProductMetaUpload.destroy({ where: { upload_id: uploadId } }) .catch(err => { errorsCount++; errors.push(err.message); return; }); }) .then(destroyed => { const results = { upload_id: uploadId, products: productsTotal, errors_count: errorsCount, errors: errors }; this.app.services.EventsService.publish('product_metadata_process.complete', results); return results; }); } processProductAssociationUpload(uploadId, options = {}) { const AssociationUpload = this.app.models['ProductAssociationUpload']; const ProductService = this.app.services.ProductService; const errors = []; let associationsTotal = 0, errorsCount = 0; return AssociationUpload.batch({ where: { upload_id: uploadId }, transaction: options.transaction || null }, associations => { return AssociationUpload.sequelize.Promise.mapSeries(associations, association => { return ProductService.addAssociation({ handle: association.product_handle, sku: association.product_sku }, { handle: association.associated_product_handle, sku: association.associated_product_sku, position: association.position }, { transaction: options.transaction || null }) .then(() => { associationsTotal++; return; }) .catch(err => { errorsCount++; errors.push(`${association.product_handle} -> ${association.associated_product_handle}: ${err.message}`); return; }); }); }) .then(results => { return AssociationUpload.destroy({ where: { upload_id: uploadId }, transaction: options.transaction || null }) .catch(err => { errorsCount++; errors.push(err); return; }); }) .then(destroyed => { const results = { upload_id: uploadId, associations: associationsTotal, errors_count: errorsCount, errors: errors }; this.app.services.EventsService.publish('product_associations_process.complete', results); return results; }); } } exports.ProductCsvService = ProductCsvService;