UNPKG

trailpack-proxy-cart

Version:

eCommerce - Trailpack for Proxy Engine

575 lines (567 loc) 19.3 kB
/* eslint no-console: [0] */ /* eslint new-cap: [0] */ 'use strict' const Model = require('trails/model') const Errors = require('proxy-engine-errors') const helpers = require('proxy-engine-helpers') const queryDefaults = require('../utils/queryDefaults') const UNITS = require('../../lib').Enums.UNITS const INTERVALS = require('../../lib').Enums.INTERVALS const INVENTORY_POLICY = require('../../lib').Enums.INVENTORY_POLICY const VARIANT_DEFAULTS = require('../../lib').Enums.VARIANT_DEFAULTS const _ = require('lodash') /** * @module ProductVariant * @description Product Variant Model */ module.exports = class ProductVariant extends Model { static config (app, Sequelize) { return { options: { underscored: true, enums: { /** * Expose UNITS enums */ UNITS: UNITS, /** * Expose INTERVALS enums */ INTERVALS: INTERVALS, /** * Expose INVENTORY_POLICY enums */ INVENTORY_POLICY: INVENTORY_POLICY, /** * Expose VARIANT_DEFAULTS */ VARIANT_DEFAULTS: VARIANT_DEFAULTS, }, // paranoid: !app.config.proxyCart.allow.destroy_variant, // defaultScope: { // where: { // live_mode: app.config.proxyEngine.live_mode // }, // // paranoid: false, // order: [['position','ASC']] // }, scopes: { live: { where: { live_mode: true } } }, hooks: { beforeValidate(values, options) { if (!values.calculated_price && values.price) { values.calculated_price = values.price } }, beforeCreate(values, options) { return app.services.ProductService.beforeVariantCreate(values, options) .catch(err => { return Promise.reject(err) }) }, beforeUpdate(values, options) { return app.services.ProductService.beforeVariantUpdate(values, options) .catch(err => { return Promise.reject(err) }) } }, classMethods: { /** * Associate the Model * @param models */ associate: (models) => { models.ProductVariant.belongsTo(models.Product, { foreignKey: 'product_id' // as: 'product_id', // foreign_key: 'id', // notNull: true // onDelete: 'CASCADE' }) models.ProductVariant.belongsToMany(models.ProductVariant, { as: 'associations', through: { model: models.ProductAssociation, unique: false // scope: { // model: 'product' // } }, foreignKey: 'variant_id', otherKey: 'associated_variant_id' // constraints: false }) models.ProductVariant.belongsToMany(models.ProductVariant, { as: 'relations', through: { model: models.ProductAssociation, unique: false // scope: { // model: 'product' // } }, foreignKey: 'associated_variant_id', otherKey: 'variant_id' // constraints: false }) // models.ProductVariant.belongsTo(models.Product, { // // foreignKey: 'variant_id', // // as: 'product_id', // onDelete: 'CASCADE' // // foreignKey: { // // allowNull: false // // } // }) // models.ProductVariant.belongsToMany(models.Image, { // as: 'images', // through: { // model: models.ItemImage, // unique: false, // scope: { // model: 'variant' // } // }, // foreignKey: 'model_id', // constraints: false // }) models.ProductVariant.hasMany(models.ProductImage, { as: 'images', foreignKey: 'product_variant_id', through: null, onDelete: 'CASCADE' // foreignKey: { // allowNull: false // } }) models.ProductVariant.hasOne(models.Metadata, { as: 'metadata', foreignKey: 'product_variant_id' }) models.ProductVariant.belongsToMany(models.Discount, { as: 'discounts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'productvariant' } }, foreignKey: 'model_id', constraints: false }) models.ProductVariant.hasMany(models.OrderItem, { as: 'order_items', foreignKey: 'variant_id' }) models.ProductVariant.belongsToMany(models.Event, { as: 'event_items', through: { model: models.EventItem, unique: false, scope: { object: 'productvariant' } }, foreignKey: 'object_id', constraints: false }) models.ProductVariant.belongsToMany(models.Vendor, { as: 'vendors', through: { model: models.VendorProduct, unique: false, }, foreignKey: 'variant_id', // constraints: false }) // models.ProductVariant.belongsToMany(models.Collection, { // as: 'collections', // through: { // model: models.ItemCollection, // unique: false, // scope: { // model: 'product_variant' // } // }, // foreignKey: 'model_id', // constraints: false // }) }, /** * * @param id * @param options * @returns {*|Promise.<Instance>} */ findByIdDefault: function(id, options) { options = options || {} options = _.defaultsDeep(options, queryDefaults.ProductVariant.default(app)) return this.findById(id, options) }, findAllDefault: function(options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.ProductVariant.default(app), options || {} ) return this.findAll(options) }, resolve: function(variant, options){ options = options || {} const Variant = this if (variant instanceof Variant){ return Promise.resolve(variant) } else if (variant && _.isObject(variant) && variant.id) { return Variant.findById(variant.id, options) .then(resVariant => { if (!resVariant && options.reject !== false) { throw new Errors.FoundError(Error(`Variant ${variant.id} not found`)) } return resVariant || variant }) } else if (variant && _.isObject(variant) && variant.sku) { return Variant.findOne(_.defaultsDeep({ where: { sku: variant.sku } }, options)) .then(resVariant => { if (!resVariant && options.reject !== false) { throw new Errors.FoundError(Error(`Variant ${variant.sku} not found`)) } return resVariant || variant }) } else if (variant && _.isNumber(variant)) { return Variant.findById(variant, options) .then(resVariant => { if (!resVariant && options.reject !== false) { throw new Errors.FoundError(Error(`Variant ${variant} not found`)) } return resVariant || variant }) } else if (variant && _.isString(variant)) { return Variant.findOne(_.defaultsDeep({ where: { sku: variant } }, options)) .then(resVariant => { if (!resVariant && options.reject !== false) { throw new Errors.FoundError(Error(`Variant ${variant} not found`)) } return resVariant || variant }) } else { if (options.reject !== false) { // TODO create proper error const err = new Error(`Unable to resolve Variant ${variant}`) return Promise.reject(err) } else { return Promise.resolve(variant) } } } }, instanceMethods: { // TODO Resolve customer address and see if product is allowed to be sent there checkRestrictions: function(customer, shippingAddress){ return Promise.resolve(false) }, // TODO check fulfillment policies checkAvailability: function(qty){ let allowed = true if (qty > this.inventory_quantity && this.inventory_policy == INVENTORY_POLICY.DENY) { allowed = false qty = Math.max(0, qty + ( this.inventory_quantity - qty)) } if (this.inventory_policy == INVENTORY_POLICY.RESTRICT) { qty = Math.max(0, qty + ( this.inventory_quantity - qty)) } const res = { title: this.title, allowed: allowed, quantity: qty } return Promise.resolve(res) }, resolveImages: function(options) { options = options || {} }, /** * * @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({transaction: options.transaction || null}) .then(_discounts => { _discounts = _discounts || [] this.discounts = _discounts this.setDataValue('discounts', _discounts) this.set('discounts', _discounts) 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_variant_id: this.id} this.metadata = _metadata this.setDataValue('metadata', _metadata) this.set('metadata', _metadata) return this }) } } } } } } static schema (app, Sequelize) { return { product_id: { type: Sequelize.INTEGER, unique: 'productvariant_sku', // references: { // model: 'Product', // key: 'id' // } }, // The SKU for this Variation sku: { type: Sequelize.STRING, unique: 'productvariant_sku', allowNull: false, set: function(val) { this.setDataValue('sku', app.services.ProxyCartService.sku(val)) } }, // Variant Title title: { type: Sequelize.STRING }, // Variant Title type: { type: Sequelize.STRING }, // The option that this Variant is option: helpers.JSONB('ProductVariant', app, Sequelize, 'option', { // name: string, value:string defaultValue: {} }), // Property Based Pricing property_pricing: helpers.JSONB('ProductVariant', app, Sequelize, 'property_pricing', { defaultValue: {} }), // The Barcode of the Variant barcode: { type: Sequelize.STRING }, // Default price of the product in cents price: { type: Sequelize.INTEGER, defaultValue: 0 }, // The calculated Price of the product calculated_price: { type: Sequelize.INTEGER, defaultValue: 0 }, // Competitor price of the variant in cents compare_at_price: { type: Sequelize.INTEGER, defaultValue: 0 }, // Default currency of the variant currency: { type: Sequelize.STRING, defaultValue: VARIANT_DEFAULTS.CURRENCY }, // The discounts applied to the product discounted_lines: helpers.JSONB('ProductVariant', app, Sequelize, 'discounted_lines', { defaultValue: [] }), // The total Discounts applied to the product total_discounts: { type: Sequelize.INTEGER, defaultValue: 0 }, // The total count of orders created with this product total_orders: { type: Sequelize.INTEGER, defaultValue: 0 }, // The fulfillment generic that handles this request fulfillment_service: { type: Sequelize.STRING, defaultValue: VARIANT_DEFAULTS.FULFILLMENT_SERVICE }, // The order of the product variant in the list of product variants. position: { type: Sequelize.INTEGER, defaultValue: 1 }, // Is product published published: { type: Sequelize.BOOLEAN, defaultValue: VARIANT_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: VARIANT_DEFAULTS.AVAILABLE }, // If Variant needs to be shipped requires_shipping: { type: Sequelize.BOOLEAN, defaultValue: VARIANT_DEFAULTS.REQUIRES_SHIPPING }, // If Product needs to be taxed requires_taxes: { type: Sequelize.BOOLEAN, defaultValue: VARIANT_DEFAULTS.REQUIRES_TAX }, // If Variant requires a subscription requires_subscription: { type: Sequelize.BOOLEAN, defaultValue: VARIANT_DEFAULTS.REQUIRES_SUBSCRIPTION }, // If Product has subscription, the interval of the subscription, defaults to 0 months subscription_interval: { type: Sequelize.INTEGER, defaultValue: VARIANT_DEFAULTS.SUBSCRIPTION_INTERVAL }, // If product has subscription, the unit of the interval subscription_unit: { type: Sequelize.ENUM, values: _.values(INTERVALS), defaultValue: VARIANT_DEFAULTS.SUBSCRIPTION_UNIT }, // Specifies whether or not Proxy Cart tracks the number of items in stock for this product variant. inventory_management: { type: Sequelize.BOOLEAN, defaultValue: VARIANT_DEFAULTS.INVENTORY_MANAGEMENT }, // Specifies whether or not customers are allowed to place an order for a product variant when it's out of stock. inventory_policy: { type: Sequelize.ENUM, values: _.values(INVENTORY_POLICY), defaultValue: VARIANT_DEFAULTS.INVENTORY_POLICY }, // Amount of variant in inventory inventory_quantity: { type: Sequelize.INTEGER, defaultValue: VARIANT_DEFAULTS.INVENTORY_QUANTITY }, // The average amount of days to come in stock if out of stock inventory_lead_time: { type: Sequelize.INTEGER, defaultValue: VARIANT_DEFAULTS.INVENTORY_LEAD_TIME }, max_quantity: { type: Sequelize.INTEGER, defaultValue: VARIANT_DEFAULTS.MAX_QUANTITY }, // The tax code of the product, defaults to physical good. tax_code: { type: Sequelize.STRING, defaultValue: VARIANT_DEFAULTS.TAX_CODE // Physical Good }, // Weight of the variant, defaults to grams weight: { type: Sequelize.INTEGER, defaultValue: 0 }, // Unit of Measurement for Weight of the variant, defaults to grams weight_unit: { type: Sequelize.ENUM, values: _.values(UNITS), defaultValue: VARIANT_DEFAULTS.WEIGHT_UNIT }, // Google Specific Listings google: helpers.JSONB('ProductVariant', app, Sequelize, 'google', { defaultValue: { // // 'Google Shopping / Google Product Category' // g_product_category: null, // // 'Google Shopping / Gender' // g_gender: null, // // 'Google Shopping / Age Group' // g_age_group: null, // // 'Google Shopping / MPN' // g_mpn: null, // // 'Google Shopping / Adwords Grouping' // g_adwords_grouping: null, // // 'Google Shopping / Adwords Labels' // g_adwords_label: null, // // 'Google Shopping / Condition' // g_condition: null, // // 'Google Shopping / Custom Product' // g_custom_product: null, // // 'Google Shopping / Custom Label 0' // g_custom_label_0: null, // // 'Google Shopping / Custom Label 1' // g_custom_label_1: null, // // 'Google Shopping / Custom Label 2' // g_custom_label_2: null, // // 'Google Shopping / Custom Label 3' // g_custom_label_3: null, // // 'Google Shopping / Custom Label 4' // g_custom_label_4: null } }), // Amazon Specific listings amazon: helpers.JSONB('ProductVariant', app, Sequelize, 'amazon', { defaultValue: {} }), // If this product was created in Live Mode live_mode: { type: Sequelize.BOOLEAN, defaultValue: app.config.proxyEngine.live_mode } } } }