UNPKG

@fabrix/spool-cart

Version:

Spool - eCommerce Spool for Fabrix

570 lines (569 loc) 20.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const common_1 = require("@fabrix/fabrix/dist/common"); const errors_1 = require("@fabrix/spool-sequelize/dist/errors"); const spool_sequelize_1 = require("@fabrix/spool-sequelize"); const lodash_1 = require("lodash"); const enums_1 = require("../../enums"); const enums_2 = require("../../enums"); const enums_3 = require("../../enums"); class DiscountUploadResolver extends spool_sequelize_1.SequelizeResolver { batch(options, batch) { const self = this; options.limit = options.limit || 100; options.offset = options.offset || 0; options.regressive = options.regressive || false; const recursiveQuery = function (options) { let count = 0; return self.findAndCountAll(options) .then(results => { count = results.count; return batch(results.rows); }) .then(batched => { if (count >= (options.regressive ? options.limit : options.offset + options.limit)) { options.offset = options.regressive ? 0 : options.offset + options.limit; return recursiveQuery(options); } else { return Promise.resolve(); } }); }; return recursiveQuery(options); } resolveByInstance(discount, options = {}) { return Promise.resolve(discount); } resolveById(discount, options = {}) { return this.findById(discount.id, options) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Discount ${discount.id} not found`); } return resUser; }); } resolveByToken(discount, options = {}) { return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, { where: { token: discount.token } })) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Discount token ${discount.token} not found`); } return resUser; }); } resolveByCode(discount, options = {}) { return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, { where: { code: discount.code } })) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Discount code ${discount.code} not found`); } return resUser; }); } resolveByNumber(discount, options = {}) { return this.findById(discount, options) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Discount ${discount.token} not found`); } return resUser; }); } resolveByString(discount, options = {}) { return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, { where: { code: discount } })) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Discount ${discount} not found`); } return resUser; }); } resolve(discount, options = {}) { const resolvers = { 'instance': discount instanceof this.instance, 'id': !!(discount && lodash_1.isObject(discount) && discount.id), 'token': !!(discount && lodash_1.isObject(discount) && discount.token), 'code': !!(discount && lodash_1.isObject(discount) && discount.code), 'number': !!(discount && lodash_1.isNumber(discount)), 'string': !!(discount && lodash_1.isString(discount)) }; const type = Object.keys(resolvers).find((key) => resolvers[key]); switch (type) { case 'instance': { return this.resolveByInstance(discount, options); } case 'id': { return this.resolveById(discount, options); } case 'token': { return this.resolveByToken(discount, options); } case 'code': { return this.resolveByCode(discount, options); } case 'number': { return this.resolveByNumber(discount, options); } case 'string': { return this.resolveByString(discount, options); } default: { const err = new Error(`Unable to resolve Discount ${discount}`); return Promise.reject(err); } } } transformDiscounts(discounts = [], options = {}) { const DiscountModel = this.app.models['Discount']; const Sequelize = DiscountModel.sequelize; discounts = discounts.map(discount => { if (discount && lodash_1.isNumber(discount)) { return { id: discount }; } else if (discount && lodash_1.isString(discount)) { return { handle: this.app.services.ProxyCartService.handle(discount), name: discount }; } else if (discount && lodash_1.isObject(discount) && (discount.name || discount.handle)) { discount.handle = this.app.services.ProxyCartService.handle(discount.handle) || this.app.services.ProxyCartService.handle(discount.name); return discount; } }); discounts = discounts.filter(discount => discount); return Sequelize.Promise.mapSeries(discounts, discount => { return DiscountModel.findOne({ where: lodash_1.pick(discount, ['id', 'handle']), attributes: ['id', 'handle', 'name'], transaction: options.transaction || null }) .then(_discount => { if (_discount) { return lodash_1.extend(_discount, discount); } else { return this.app.services.DiscountService.create(discount, { transaction: options.transaction || null }); } }); }); } } exports.DiscountUploadResolver = DiscountUploadResolver; class Discount extends common_1.FabrixModel { static get resolver() { return DiscountUploadResolver; } static config(app, Sequelize) { return { options: { underscored: true, enums: { DISCOUNT_TYPES: enums_1.DISCOUNT_TYPES, DISCOUNT_STATUS: enums_2.DISCOUNT_STATUS, DISCOUNT_SCOPE: enums_3.DISCOUNT_SCOPE }, scopes: { live: { where: { live_mode: true } }, expired: () => { return { where: { ends_at: { $gte: new Date() } } }; }, active: () => { return { where: { status: enums_2.DISCOUNT_STATUS.ENABLED, starts_at: { $gte: new Date() }, ends_at: { $lte: new Date() } } }; } }, hooks: { beforeValidate: [ (discount, options) => { if (!discount.handle && discount.name) { discount.handle = discount.name; } } ], beforeCreate: [ (discount, options) => { if (discount.body) { const bodyDoc = app.services.RenderGenericService.renderSync(discount.body); discount.body_html = bodyDoc.document; } } ], beforeUpdate: [ (discount, options) => { if (discount.body) { const bodyDoc = app.services.RenderGenericService.renderSync(discount.body); discount.body_html = bodyDoc.document; } } ] } } }; } static schema(app, Sequelize) { return { handle: { type: Sequelize.STRING, allowNull: false, unique: true, set: function (val) { this.setDataValue('handle', app.services.ProxyCartService.splitHandle(val) || null); } }, name: { type: Sequelize.STRING }, description: { type: Sequelize.TEXT }, body: { type: Sequelize.TEXT }, body_html: { type: Sequelize.TEXT }, code: { type: Sequelize.STRING, notNull: true }, discount_scope: { type: Sequelize.ENUM, values: lodash_1.values(enums_3.DISCOUNT_SCOPE), defaultValue: enums_3.DISCOUNT_SCOPE.INDIVIDUAL }, discount_type: { type: Sequelize.ENUM, values: lodash_1.values(enums_1.DISCOUNT_TYPES), defaultValue: enums_1.DISCOUNT_TYPES.RATE }, discount_rate: { type: Sequelize.INTEGER }, discount_threshold: { type: Sequelize.INTEGER }, discount_percentage: { type: Sequelize.FLOAT, defaultValue: 0.0 }, discount_shipping: { type: Sequelize.FLOAT, defaultValue: 0.0 }, discount_product_include: { type: Sequelize.JSONB, defaultValue: [] }, discount_product_exclude: { type: Sequelize.JSONB, defaultValue: [] }, discount_customer_include: { type: Sequelize.JSONB, defaultValue: [] }, discount_customer_exclude: { type: Sequelize.JSONB, defaultValue: [] }, shipping_product_exclude: { type: Sequelize.JSONB, defaultValue: [] }, tax_product_exclude: { type: Sequelize.JSONB, defaultValue: [] }, ends_at: { type: Sequelize.DATE }, starts_at: { type: Sequelize.DATE }, status: { type: Sequelize.ENUM, values: lodash_1.values(enums_2.DISCOUNT_STATUS), defaultValue: enums_2.DISCOUNT_STATUS.ENABLED }, minimum_order_amount: { type: Sequelize.INTEGER, defaultValue: 0 }, usage_limit: { type: Sequelize.INTEGER, defaultValue: 0 }, times_used: { type: Sequelize.INTEGER, defaultValue: 0 }, applies_once: { type: Sequelize.BOOLEAN, defaultValue: false, }, applies_once_per_customer: { type: Sequelize.BOOLEAN, defaultValue: false }, applies_compound: { type: Sequelize.BOOLEAN, defaultValue: true }, live_mode: { type: Sequelize.BOOLEAN, defaultValue: app.config.get('cart.live_mode') } }; } static associate(models) { models.Discount.belongsToMany(models.Order, { as: 'orders', through: { model: models.ItemDiscount, unique: false, scope: { model: 'order' } }, foreignKey: 'discount_id', constraints: false }); models.Discount.belongsToMany(models.Cart, { as: 'carts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'cart' } }, foreignKey: 'discount_id', constraints: false }); models.Discount.belongsToMany(models.Product, { as: 'products', through: { model: models.ItemDiscount, unique: false, scope: { model: 'product' } }, foreignKey: 'discount_id', constraints: false }); models.Discount.belongsToMany(models.ProductVariant, { as: 'variants', through: { model: models.ItemDiscount, unique: false, scope: { model: 'productvariant' } }, foreignKey: 'discount_id', constraints: false }); models.Discount.belongsToMany(models.Customer, { as: 'customers', through: { model: models.ItemDiscount, unique: false, scope: { model: 'customer' } }, foreignKey: 'discount_id', constraints: false }); models.Discount.belongsToMany(models.Collection, { as: 'collections', through: { model: models.ItemDiscount, unique: false, scope: { model: 'collection' } }, foreignKey: 'discount_id', constraints: false }); models.Discount.hasMany(models.DiscountEvent, { as: 'discount_events', foreignKey: 'discount_id' }); } } exports.Discount = Discount; Discount.prototype.start = function () { this.status = enums_2.DISCOUNT_STATUS.ENABLED; return this; }; Discount.prototype.stop = function () { this.status = enums_2.DISCOUNT_STATUS.DISABLED; return this; }; Discount.prototype.depleted = function () { this.status = enums_2.DISCOUNT_STATUS.DEPLETED; return this; }; Discount.prototype.logUsage = function (orderId, customerId, price, options) { this.times_used++; if (this.usage_limit > 0 && this.times_used >= this.usage_limit) { this.depleted(); } return this.createDiscount_event({ customer_id: customerId, order_id: orderId, price: price }, { transaction: options.transaction || null }) .then(() => { return this.save({ transaction: options.transaction || null }); }); }; Discount.prototype.eligibleCustomer = function (customerId, options = {}) { return this.getDiscount_events({ where: { customer_id: customerId }, limit: 1, attributes: ['id', 'discount_id'], transaction: options.transaction || null }) .then(_previousUsages => { _previousUsages = _previousUsages || []; if (this.applies_once_per_customer && _previousUsages.length > 0) { return this; } else { return true; } }) .catch(err => { this.app.log.error(err); return; }); }; Discount.prototype.discountItem = function (item, criteria = []) { item.discounted_lines = item.discounted_lines || []; item.shipping_lines = item.shipping_lines || []; item.calculated_price = item.calculated_price || item.price; item.total_discounts = item.total_discounts || 0; const discountedLine = { id: this.id, model: 'discount', type: null, name: this.name, scope: this.discount_scope, price: 0, applies: false, rules: { start: this.starts_at, end: this.ends_at, applies_once: this.applies_once, applies_once_per_customer: this.applies_once_per_customer, applies_compound: this.applies_compound, minimum_order_amount: this.minimum_order_amount } }; let totalDeducted = 0; if (this.status !== enums_2.DISCOUNT_STATUS.ENABLED) { return item; } if (this.usage_limit > 0 && this.times_used > this.usage_limit) { return item; } if (this.discount_product_exclude.length > 0 && this.discount_product_exclude.indexOf(item.type) > -1) { return item; } if (this.discount_product_include.length > 0 && this.discount_product_include.indexOf(item.type) === -1) { return item; } if (item.discounted_lines && item.discounted_lines.some(discount => discount.id === this.id)) { return item; } if (this.discount_scope === enums_3.DISCOUNT_SCOPE.INDIVIDUAL) { const criteriaPair = criteria.find(d => d.discount === this.id); if (!criteriaPair) { return item; } else if (item.product_id && criteriaPair['product'] && criteriaPair['product'].indexOf(item.product_id) === -1) { return item; } else if (item.variant_id && criteriaPair['productvariant'] && criteriaPair['productvariant'].indexOf(item.variant_id) === -1) { return item; } } if (this.discount_type === enums_1.DISCOUNT_TYPES.RATE) { discountedLine.rate = this.discount_rate; discountedLine.type = enums_1.DISCOUNT_TYPES.RATE; discountedLine.price = discountedLine.rate; } else if (this.discount_type === enums_1.DISCOUNT_TYPES.THRESHOLD) { discountedLine.threshold = this.discount_threshold; discountedLine.type = enums_1.DISCOUNT_TYPES.THRESHOLD; discountedLine.price = discountedLine.threshold; } else if (this.discount_type === enums_1.DISCOUNT_TYPES.PERCENTAGE) { discountedLine.percentage = this.discount_percentage; discountedLine.type = enums_1.DISCOUNT_TYPES.PERCENTAGE; discountedLine.price = Math.round((item.price * (discountedLine.percentage / 100))); } else if (this.discount_type === enums_1.DISCOUNT_TYPES.SHIPPING) { return item; } totalDeducted = Math.min(item.price, (item.price - (item.price - discountedLine.price))); if (totalDeducted > 0) { if (discountedLine.type === enums_1.DISCOUNT_TYPES.THRESHOLD) { this.discount_threshold = Math.max(0, this.discount_threshold - totalDeducted); } discountedLine.price = totalDeducted; item.discounted_lines.push(discountedLine); } return item; };