UNPKG

trailpack-proxy-cart

Version:

eCommerce - Trailpack for Proxy Engine

1,493 lines (1,444 loc) 52.3 kB
/* eslint new-cap: [0] */ /* eslint no-console: [0] */ 'use strict' const Model = require('trails/model') const Errors = require('proxy-engine-errors') const helpers = require('proxy-engine-helpers') const UNITS = require('../../lib').Enums.UNITS const PRODUCT_DEFAULTS = require('../../lib').Enums.PRODUCT_DEFAULTS const DISCOUNT_STATUS = require('../../lib').Enums.DISCOUNT_STATUS const queryDefaults = require('../utils/queryDefaults') const _ = require('lodash') // const Errors = require('proxy-engine-errors') /** * @module Product * @description Product Model */ module.exports = class Product extends Model { static config (app, Sequelize) { return { options: { underscored: true, // paranoid: !app.config.proxyCart.allow.destroy_product, // defaultScope: { // where: { // live_mode: app.config.proxyEngine.live_mode // } // // paranoid: false // }, scopes: { live: { where: { live_mode: true } }, published: { where: { published: true } } }, hooks: { beforeValidate(values, options) { if (!values.handle && values.title) { values.handle = values.title } if (!values.calculated_price && values.price) { values.calculated_price = values.price } if (!values.compare_at_price && values.price) { values.compare_at_price = values.price } }, beforeCreate(values, options) { return app.services.ProductService.beforeCreate(values, options) .catch(err => { return Promise.reject(err) }) }, beforeUpdate(values, options) { return app.services.ProductService.beforeUpdate(values, options) .catch(err => { return Promise.reject(err) }) } }, classMethods: { /** * Expose UNITS enums */ UNITS: UNITS, PRODUCT_DEFAULTS: PRODUCT_DEFAULTS, /** * Associate the Model * @param models */ associate: (models) => { // models.Product.belongsToMany(models.Shop, { // as: 'shops', // through: 'ShopProduct' // }) models.Product.hasMany(models.ProductImage, { as: 'images', foreignKey: 'product_id', through: null, onDelete: 'CASCADE' }) // models.Product.belongsToMany(models.Image, { // as: 'images', // through: { // model: models.ItemImage, // unique: false, // scope: { // model: 'product' // } // }, // foreignKey: 'model_id', // constraints: false // }) models.Product.hasMany(models.ProductVariant, { as: 'variants', foreignKey: 'product_id', // through: null, onDelete: 'CASCADE' }) models.Product.hasMany(models.ProductReview, { as: 'reviews', foreignKey: 'product_id', onDelete: 'CASCADE' }) // models.Product.belongsToMany(models.Cart, { // as: 'carts', // through: 'CartProduct' // }) models.Product.belongsToMany(models.Collection, { as: 'collections', through: { model: models.ItemCollection, unique: false, scope: { model: 'product' } }, foreignKey: 'model_id', constraints: false }) models.Product.hasOne(models.Metadata, { as: 'metadata', // scope: { // model: 'product' // }, foreignKey: 'product_id' // constraints: false }) models.Product.belongsToMany(models.Vendor, { as: 'vendors', through: { model: models.VendorProduct, unique: false, }, foreignKey: 'product_id', // constraints: false }) models.Product.belongsToMany(models.Shop, { as: 'shops', through: { model: models.ShopProduct, unique: false, }, foreignKey: 'product_id', // constraints: false }) models.Product.belongsToMany(models.Tag, { as: 'tags', through: { model: models.ItemTag, unique: false, scope: { model: 'product' } }, foreignKey: 'model_id', otherKey: 'tag_id', constraints: false }) // models.Product.belongsToMany(models.Collection, { // as: 'collections', // through: { // model: models.ItemCollection, // unique: false, // scope: { // model: 'product' // } // }, // foreignKey: 'model_id', // otherKey: 'collection_id', // constraints: false // }) models.Product.belongsToMany(models.Product, { as: 'associations', through: { model: models.ProductAssociation, unique: false }, foreignKey: 'product_id', otherKey: 'associated_product_id', // constraints: false }) models.Product.belongsToMany(models.Product, { as: 'relations', through: { model: models.ProductAssociation, unique: false }, foreignKey: 'associated_product_id', otherKey: 'product_id', // constraints: false }) models.Product.belongsToMany(models.Discount, { as: 'discounts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'product' } }, foreignKey: 'model_id', constraints: false }) models.Product.belongsToMany(models.Event, { as: 'event_items', through: { model: models.EventItem, unique: false, scope: { object: 'product' } }, foreignKey: 'object_id', constraints: false }) }, /** * * @param criteria * @param options * @returns {Promise.<TResult>} */ findByIdDefault: function(criteria, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Product.default(app), options || {} ) // console.log('Product.findByIdDefault', options) // console.log(criteria, options) let resProduct return this.findById(criteria, options) .then(product => { resProduct = product // if (resProduct) { // return resProduct.resolveReqCollections(options) // } // else { // return // } // }) // .then(() => { if (resProduct && options.req && options.req.customer) { return resProduct.getCustomerHistory(options.req.customer, { transaction: options.transaction || null }) } else { return } }) .then(() => { if (resProduct) { return resProduct.calculate({ req: options.req || null, transaction: options.transaction || null }) } else { return resProduct } }) }, /** * * @param handle * @param options * @returns {Promise.<TResult>} */ findByHandleDefault: function(handle, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Product.default(app), options || {}, { where: { handle: handle } } ) let resProduct return this.findOne(options) .then(product => { resProduct = product // if (resProduct) { // return resProduct.resolveReqCollections(options) // } // else { // return // } // }) // .then(() => { if (resProduct && options.req && options.req.customer) { return resProduct.getCustomerHistory(options.req.customer, { transaction: options.transaction || null }) } else { return } }) .then(() => { if (resProduct) { return resProduct.calculate({ req: options.req || null, transaction: options.transaction || null }) } else { return resProduct } }) }, /** * * @param criteria * @param options * @returns {Promise.<TResult>} */ findOneDefault: function(criteria, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Product.default(app), options || {} ) // console.log('Product.findOneDefault', options) let resProduct return this.findOne(criteria, options) .then(product => { if (!product) { // resProduct = app.orm['Product'].build() // throw new Errors.FoundError(Error(`${criteria} not found`)) } resProduct = product // if (resProduct) { // return resProduct.resolveReqCollections(options) // } // else { // return // } // }) // .then(() => { if (resProduct && options.req && options.req.customer) { return resProduct.getCustomerHistory(options.req.customer, { transaction: options.transaction || null }) } else { return } }) .then(() => { if (resProduct) { return resProduct.calculate({ req: options.req || null, transaction: options.transaction || null }) } else { return resProduct } }) }, /** * * @param options * @returns {*|Promise.<Array.<Instance>>} */ findAllDefault: function(options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Product.findAllDefault(app), options || {} ) return this.findAll(options) }, /** * * @param options * @returns {Promise.<Object>} */ findAndCountDefault: function(options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Product.findAllDefault(app), options || {} ) return this.findAndCount(options) }, /** * * @param product * @param options * @returns {*} */ resolve: function(product, options) { options = options || {} const Product = this if (product instanceof Product){ return Promise.resolve(product) } else if (product && _.isObject(product) && product.id) { return Product.findById(product.id, options) .then(resProduct => { if (!resProduct && options.reject !== false) { throw new Errors.FoundError(Error(`Product id ${product.id} not found`)) } return resProduct || product }) } else if (product && _.isObject(product) && product.handle) { return Product.findOne(app.services.ProxyEngineService.mergeOptionDefaults({ where: { handle: product.handle } }, options)) .then(resProduct => { if (!resProduct && options.reject !== false) { throw new Errors.FoundError(Error(`Product handle ${product.handle} not found`)) } return resProduct || product }) } else if (product && _.isNumber(product)) { return Product.findById(product, options) .then(resProduct => { if (!resProduct && options.reject !== false) { throw new Errors.FoundError(Error(`Product id ${product} not found`)) } return resProduct || product }) } else if (product && _.isString(product)) { return Product.findOne(app.services.ProxyEngineService.mergeOptionDefaults({ options, where: { handle: product } })) .then(resProduct => { if (!resProduct && options.reject !== false) { throw new Errors.FoundError(Error(`Product handle ${product} not found`)) } return resProduct || product }) } else { if (options.reject !== false) { // TODO create proper error const err = new Error(`Unable to resolve Product ${product}`) return Promise.reject(err) } else { return Promise.resovle(product) } } } }, instanceMethods: { /** * * @param discounts * @param criteria * @param options * @returns {*} */ setItemDiscountedLines: function (discounts, criteria, options) { options = options || {} // Make this an array if null discounts = discounts || [] // Make this an array if null criteria = criteria || [] // Set this to the default this.discounted_lines = [] // Holds the final factored results const factoredDiscountedLines = [] // Holds list of all discount objects being tried let discountsArr = [] // This handles one time and threshold discounts for items already in cart. if (options.req && options.req.cart) { options.req.cart.line_items = options.req.cart.line_items || [] options.req.cart.line_items.map(item => { // For each item run the normal discounts discounts.forEach(discount => { item = discount.discountItem(item, criteria) }) return item }) options.req.cart.line_items.forEach(item => { item.discounted_lines = item.discounted_lines || [] discountsArr = [...discountsArr, item.discounted_lines.map(line => line.id)] }) } // For each item run the normal discounts discounts.forEach(discount => { discount.discountItem(this, criteria) }) // Gather all discounts into a single array this.discounted_lines.forEach(line => { discountsArr = [...discountsArr, line.id] }) this.discounted_lines = this.discounted_lines.map(discount => { discount.rules = discount.rules || {} // Applies once Rule if (discount.rules.applies_once && discountsArr.filter(d => d === discount.id).length > 1) { const arrRemove = discountsArr.findIndex(d => d === discount.id) // Removes duplicated from discountArr discountsArr = discountsArr.splice(arrRemove, 1) // This means the next occurrence of the discount will receive the one time discount discount.applies = false } // Minimum Order Rule else if ( discount.rules.minimum_order_amount > 0 && options.req && options.req.cart && (options.req.cart.total_price + this.price) < discount.minimum_order_amount ) { discount.applies = false } // Compounding Discounts Rule else if ( discount.rules.applies_compound === false && discountsArr.length > 1 ) { discount.applies = false } else { discount.applies = true } return discount }) this.discounted_lines.forEach(discountedLine => { if (discountedLine.applies === true) { // New calculated price const calculatedPrice = Math.max(0, this.calculated_price - discountedLine.price) // New Total Deducted const totalDeducted = Math.min(this.calculated_price, (this.calculated_price - (this.calculated_price - discountedLine.price))) // Set calculated price this.calculated_price = calculatedPrice // Set total Discounts this.total_discounts = Math.min(this.price, this.total_discounts + totalDeducted) const fI = factoredDiscountedLines.findIndex(d => d.id === discountedLine.id) if (fI > -1) { factoredDiscountedLines[fI].price = factoredDiscountedLines[fI].price + totalDeducted } else { discountedLine.price = totalDeducted factoredDiscountedLines.push(discountedLine) } } }) // console.log('FACTORED PRODUCT DISCOUNTS',factoredDiscountedLines) return this.setDiscountedLines(factoredDiscountedLines) }, /** * * @param lines */ setDiscountedLines: function(lines) { this.total_discounts = 0 this.discounted_lines = lines || [] this.discounted_lines.forEach(line => { this.total_discounts = this.total_discounts + line.price }) return this.setTotals() }, setCalculatedPrice: function(calculatedPrice) { this.calculated_price = calculatedPrice return this }, /** * */ setTotals: function() { // Set Cart values this.total_price = Math.max(0, this.total_tax + this.total_shipping + this.subtotal_price ) this.total_due = Math.max(0, this.total_price - this.total_discounts - this.total_coupons - this.total_overrides ) return this }, getCustomerHistory: function(customer, options) { options = options || {} let hasPurchaseHistory = false, isSubscribed = false return this.hasPurchaseHistory(customer.id, options) .then(pHistory => { hasPurchaseHistory = pHistory return this.isSubscribed(customer.id, options) }) .then(pHistory => { isSubscribed = pHistory this.setDataValue('has_purchase_history', hasPurchaseHistory) this.setDataValue('is_subscribed', isSubscribed) return this }) .catch(err => { this.setDataValue('has_purchase_history', hasPurchaseHistory) this.setDataValue('is_subscribed', isSubscribed) return this }) }, /** * * @param customerId * @param options * @returns {Promise.<boolean>} */ hasPurchaseHistory: function(customerId, options) { options = options || {} return app.orm['OrderItem'].findOne({ where: { customer_id: customerId, product_id: this.id, fulfillment_status: { $not: ['cancelled','pending','none'] } }, attributes: ['id'], transaction: options.transaction || null }) .then(pHistory => { if (pHistory) { return true } else { return false } }) .catch(err => { return false }) }, isSubscribed: function(customerId, options) { options = options || {} return app.orm['Subscription'].findOne({ where: { customer_id: customerId, active: true, line_items: { $contains: [{ product_id: this.id }] } }, attributes: ['id'], transaction: options.transaction || null }) .then(pHistory => { if (pHistory) { return true } else { return false } }) .catch(err => { return false }) }, getCollectionPairs: function(options) { options = options || {} const collectionPairs = [] const criteria = [] return Promise.resolve() .then(() => { if (options.req && options.req.customer && options.req.customer.id) { criteria.push({ model: 'customer', model_id: options.req.customer.id }) } // if (options.req && options.req.cart && options.req.cart.id) { // criteria.push({ // model: 'cart', // model_id: options.req.cart.id // }) // } if (this.id) { criteria.push({ model: 'product', model_id: this.id }) } if (criteria.length > 0) { return app.orm['ItemCollection'].findAll({ where: { $or: criteria }, attributes: ['id','collection_id','model','model_id'], transaction: options.transaction || null }) } return [] }) .then(_collections => { _collections.forEach(collection => { const i = collectionPairs.findIndex(c => c.id === collection.collection_id) if (i > -1) { if (!collectionPairs[i][collection.model]) { collectionPairs[i][collection.model] = [] } collectionPairs[i][collection.model].push(collection.model_id) } else { collectionPairs.push({ collection: collection.collection_id, [collection.model]: [collection.model_id] }) } }) return collectionPairs }) .catch(err => { app.log.error(err) return [] }) }, /** * * @param options * @returns {Promise.<TResult>} */ calculateDiscounts(options) { options = options || {} const criteria = [] let collectionPairs = [], discountCriteria = [], checkHistory = [] // const discountedLines = this.discounted_lines || [] let resDiscounts return Promise.resolve() .then(() => { return this.getCollectionPairs({ req: options.req || null, transaction: options.transaction || null }) }) .then(_collections => { collectionPairs = _collections // console.log('BROKE COLLECTION IDS', collectionIds) if (options.req && options.req.cart && options.req.cart.id) { criteria.push({ model: 'cart', model_id: options.req.cart.id }) } if (options.req && options.req.customer && options.req.customer.id) { criteria.push({ model: 'customer', model_id: options.req.customer.id }) } if (this.id) { criteria.push({ model: 'product', model_id: this.id }) } if (collectionPairs.length > 0) { criteria.push({ model: 'collection', model_id: collectionPairs.map(c => c.collection) }) } if (criteria.length > 0) { return app.orm['ItemDiscount'].findAll({ where: { $or: criteria }, attributes: ['discount_id', 'model', 'model_id'], transaction: options.transaction || null }) } else { return [] } }) .then(discounts => { discounts.forEach(discount => { const i = discountCriteria.findIndex(d => d.discount === discount.discount_id) if (i > -1) { if (!discountCriteria[i][discount.model]) { discountCriteria[i][discount.model] = [] } discountCriteria[i][discount.model].push(discount.model_id) } else { discountCriteria.push({ discount: discount.discount_id, [discount.model]: [discount.model_id] }) } }) discountCriteria = discountCriteria.map(d => { if (d.collection) { d.collection.forEach(colId => { const i = collectionPairs.findIndex(c => c.collection = colId) if (i > -1) { d = _.merge(d, collectionPairs[i]) } }) } return d }) // console.log('Broke Criteria', discountCriteria) if (discounts.length > 0) { return app.orm['Discount'].findAll({ where: { id: discounts.map(item => item.discount_id), status: DISCOUNT_STATUS.ENABLED }, transaction: options.transaction || null }) } else { return [] } }) .then(_discounts => { _discounts = _discounts || [] resDiscounts = _discounts resDiscounts.forEach(discount => { if ( discount.applies_once_per_customer && options.req && options.req.customer && options.req.customer.id ) { checkHistory.push(discount) } }) if (checkHistory.length > 0) { return Promise.all(checkHistory.map(discount => { return discount.eligibleCustomer(options.req.customer.id, { transaction: options.transaction || null }) })) } else { return [] } }) .then(_eligible => { _eligible = _eligible || [] _eligible.forEach(discount => { const i = resDiscounts.findIndex(i => i.id === discount.id) if (i > -1) { resDiscounts.splice(i, 1) } }) return this.setItemDiscountedLines(resDiscounts, discountCriteria, options) }) .catch(err => { app.log.error(err) return this }) }, calculate: function (options) { options = options || {} if (!this) { return } // Set defaults this.calculated_price = this.price // Modify defaults // app.services.DiscountService.calculateCollections( // this, // this.collections, // app.orm['Product'], // {transaction: options.transaction || null} // ) //obj, collections, resolver, options return this.calculateDiscounts(options) .then(() => { return this }) }, /** * * @param colsB * @returns Instance */ mergeIntoCollections: function(colsB) { colsB = colsB || [] const collections = this.collections colsB.forEach(collection => { if (!this.collections.some(colA => colA.id === collection.id)) { collections.push(collection) } }) this.collections = collections this.setDataValue('collections', collections) this.set('collections', collections) return this }, /** * TODO, this should likely be done with a view * Format return data * Converts tags to array of strings * Converts any nested variant tags to array of strings * Returns only metadata data * Converts vendors to array of strings */ toJSON: function() { const position = this.position // Make JSON const resp = this instanceof app.orm['Product'] ? this.get({ plain: true }) : this // Set Defaults // resp.calculated_price = resp.price // Transform Tags to array on toJSON if (resp.tags) { // console.log(resp.tags) resp.tags = resp.tags.map(tag => { if (tag && _.isString(tag)) { return tag } else if (tag && tag.name && tag.name !== '') { return tag.name } }) } // Map Variants as Products are mapped if (resp.variants) { resp.variants.map((variant, idx) => { if (variant.tags) { resp.variants[idx].tags = variant.tags.map(tag => { if (tag && _.isString(tag)) { return tag } else if (tag && tag.name) { return tag.name } }) } if (variant.metadata) { if (typeof variant.metadata.data !== 'undefined') { resp.variants[idx].metadata = variant.metadata.data } } // Set Defaults resp.variants[idx].calculated_price = variant.price // TODO loop through collections and produce calculated price }) } // Transform Metadata to plain on toJSON if (resp.metadata) { if (typeof resp.metadata.data !== 'undefined') { resp.metadata = resp.metadata.data } } // Transform Vendors to strings if (resp.vendors) { // console.log(resp.vendors) resp.vendors = resp.vendors.map(vendor => { if (vendor && _.isString(vendor)) { return vendor } else { return vendor.name } }) } if (position) { resp.position = position } return resp }, /** * * @param options * @returns {*} */ resolveVariants: function(options) { options = options || {} if ( this.variants && this.variants.every(v => v instanceof app.orm['ProductVariant']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getVariants({ limit: 500, transaction: options.transaction || null, order: [['position', 'ASC']] }) .then(variants => { variants = variants || [] this.variants = variants this.setDataValue('variants', variants) this.set('variants', variants) return this }) } }, /** * * @param options * @returns {*} */ getDefaultVariant: function(options) { options = options || {} if ( this.variants && this.variants.every(v => v instanceof app.orm['ProductVariant']) && this.variants.some(v => v.position === 1) && options.reload !== true ) { return Promise.resolve(this.variants.find(v => v.position === 1)) } else { return this.getVariants({ where: { position: 1 }, limit: 1, transaction: options.transaction || null }) .then(variants => { const variant = variants[0] || null return variant }) } }, /** * * @param options * @returns {*} */ resolveAssociations: function(options) { options = options || {} if ( this.associations && this.associations.every(p => p instanceof app.orm['Product']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getAssociations({ limit: 100, transaction: options.transaction || null }) .then(associations => { associations = associations || [] this.associations = associations this.setDataValue('associations', associations) this.set('associations', associations) return this }) } }, /** * * @param options * @returns {*} */ resolveImages: function(options) { options = options || {} if ( this.images && this.images.every(i => i instanceof app.orm['ProductImage']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getImages({ limit: 50, transaction: options.transaction || null, order: [['position', 'ASC']] }) .then(images => { images = images || [] this.images = images this.setDataValue('images', images) this.set('images', images) return this }) } }, /** * * @param options * @returns {*} */ resolveVendors: function(options) { options = options || {} if ( this.vendors && this.vendors.every(v => v instanceof app.orm['Vendor']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getVendors({ limit: 50, transaction: options.transaction || null }) .then(vendors => { vendors = vendors || [] this.vendors = vendors this.setDataValue('vendors', vendors) this.set('vendors', vendors) return this }) } }, /** * * @param options * @returns {*} */ resolveMetadata: function(options) { options = options || {} if ( this.metadata && this.metadata instanceof app.orm['Metadata'] && options.reload !== true ) { return Promise.resolve(this) } else { return this.getMetadata({transaction: options.transaction || null}) .then(_metadata => { _metadata = _metadata || {product_id: this.id} this.metadata = _metadata this.setDataValue('metadata', _metadata) this.set('metadata', _metadata) return this }) } }, /** * * @param options * @returns {*} */ resolveShops: function(options) { options = options || {} if ( this.shops && this.shops.length > 0 && this.shops.every(d => d instanceof app.orm['Shop']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getShops({ limit: 50, transaction: options.transaction || null }) .then(shops => { shops = shops || [] this.shops = shops this.setDataValue('shops', shops) this.set('shops', shops) return this }) } }, /** * * @param options * @returns {*} */ resolveTags: function(options) { options = options || {} if ( this.tags && this.tags.length > 0 && this.tags.every(t => t instanceof app.orm['Tag']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getTags({ limit: 50, transaction: options.transaction || null }) .then(tags => { tags = tags || [] this.tags = tags this.setDataValue('tags', tags) this.set('tags', tags) return this }) } }, /** * * @param options * @returns {Promise.<TResult>} */ // resolveReqCollections: function(options) { // options = options || {} // // return Promise.resolve() // .then(() => { // return this.resolveCollections({transaction: options.transaction || null}) // }) // .then(() => { // if (options.req && options.req.customer) { // return options.req.customer.resolveCollections({transaction: options.transaction || null}) // } // else { // return {collections: []} // } // }) // .then(customer => { // return this.mergeIntoCollections(customer.collections) // }) // }, /** * * @param options * @returns {*} */ resolveCollections: function(options) { options = options || {} if ( this.collections && this.collections.length > 0 && this.collections.every(c => c instanceof app.orm['Collection']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getCollections({ limit: 50, transaction: options.transaction || null }) .then(collections => { collections = collections || [] this.collections = collections this.setDataValue('collections', collections) this.set('collections', collections) return this }) } }, /** * * @param options * @returns {*} */ resolveCoupons: function(options) { options = options || {} if ( this.coupons && this.coupons.length > 0 && this.coupons.every(c => c instanceof app.orm['Coupon']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getCoupons({transaction: options.transaction || null}) .then(coupons => { coupons = coupons || [] this.coupons = coupons this.setDataValue('coupons', coupons) this.set('coupons', coupons) return this }) } }, /** * * @param options * @returns {Promise.<T>} */ resolveDiscounts(options) { options = options || {} if ( this.discounts && this.discounts.length > 0 && this.discounts.every(d => d instanceof app.orm['Discount']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getDiscounts({ limit: 10, transaction: options.transaction || null }) .then(_discounts => { _discounts = _discounts || [] this.discounts = _discounts this.setDataValue('discounts', _discounts) this.set('discounts', _discounts) return this }) } } } } } } static schema (app, Sequelize) { return { // TODO Multi-Site Support. Change to domain? host: { type: Sequelize.STRING, defaultValue: PRODUCT_DEFAULTS.HOST }, // Unique Name for the product handle: { type: Sequelize.STRING, allowNull: false, unique: true, set: function(val) { this.setDataValue('handle', app.services.ProxyCartService.handle(val)) } }, // Product Title title: { type: Sequelize.STRING, set: function(val) { this.setDataValue('title', app.services.ProxyCartService.title(val)) } }, // The body of a product (in markdown or html) body: { type: Sequelize.TEXT }, // The html of a product (DO NOT EDIT DIRECTLY) html: { type: Sequelize.TEXT }, // SEO title seo_title: { type: Sequelize.STRING, set: function(val) { this.setDataValue('seo_title', app.services.ProxyCartService.title(val)) } }, // SEO description seo_description: { type: Sequelize.TEXT, set: function(val) { this.setDataValue('seo_description', app.services.ProxyCartService.description(val)) } }, // Type of the product e.g. 'Snow Board' type: { type: Sequelize.STRING, allowNull: false, set: function(val) { this.setDataValue('type', app.services.ProxyCartService.title(val)) } }, // The tax code of the product, defaults to physical good. tax_code: { type: Sequelize.STRING, defaultValue: PRODUCT_DEFAULTS.TAX_CODE // Physical Good }, // Pricing Average against competitors compare_at_price: { type: Sequelize.INTEGER, defaultValue: PRODUCT_DEFAULTS.PRICE }, // Default price of the product in cents price: { type: Sequelize.INTEGER, defaultValue: PRODUCT_DEFAULTS.PRICE }, // Pricing after calculated_price: { type: Sequelize.INTEGER, defaultValue: PRODUCT_DEFAULTS.CALCULATED_PRICE }, // Default currency of the product currency: { type: Sequelize.STRING, defaultValue: PRODUCT_DEFAULTS.CURRENCY }, // The total count of orders created with this product total_orders: { type: Sequelize.INTEGER, defaultValue: 0 }, // Discounts applied discounted_lines: helpers.JSONB('Product', app, Sequelize, 'discounted_lines', { defaultValue: PRODUCT_DEFAULTS.DISCOUNTED_LINES }), // Total value of discounts total_discounts: { type: Sequelize.INTEGER, defaultValue: PRODUCT_DEFAULTS.TOTAL_DISCOUNTS }, // The sales channels in which the product is visible. published_scope: { type: Sequelize.STRING, defaultValue: PRODUCT_DEFAULTS.PUBLISHED_SCOPE }, // Is product published published: { type: Sequelize.BOOLEAN, defaultValue: PRODUCT_DEFAULTS.PUBLISHED }, // Date/Time the Product was published published_at: { type: Sequelize.DATE }, // Date/Time the Product was unpublished unpublished_at: { type: Sequelize.DATE }, // If product is available and has not been discontinued available: { type: Sequelize.BOOLEAN, defaultValue: PRODUCT_DEFAULTS.AVAILABLE }, // Options for the product (size, color, etc.) options: helpers.JSONB('Product', app, Sequelize, 'options', { defaultValue: PRODUCT_DEFAULTS.OPTIONS }), // Property Based Pricing property_pricing: helpers.JSONB('Product', app, Sequelize, 'property_pricing', { defaultValue: {} }), // Weight of the product, defaults to grams weight: { type: Sequelize.INTEGER, defaultValue: PRODUCT_DEFAULTS.WEIGHT }, // Unit of Measurement for Weight of the product, defaults to grams weight_unit: { type: Sequelize.ENUM, values: _.values(UNITS), defaultValue: PRODUCT_DEFAULTS.WEIGHT_UNIT }, // The A