UNPKG

@fabrix/spool-cart

Version:

Spool - eCommerce Spool for Fabrix

1,358 lines (1,357 loc) 49.3 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 moment = require("moment"); const queryDefaults_1 = require("../utils/queryDefaults"); const enums_1 = require("../../enums"); const enums_2 = require("../../enums"); const enums_3 = require("../../enums"); const enums_4 = require("../../enums"); class SubscriptionResolver extends spool_sequelize_1.SequelizeResolver { findByIdDefault(criteria, options = {}) { options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Subscription.default(this.app), options); return this.findById(criteria, options); } findByTokenDefault(token, options = {}) { options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Subscription.default(this.app), options, { where: { token: token } }); return this.findOne(options); } 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(subscription, options = {}) { return Promise.resolve(subscription); } resolveById(subscription, options = {}) { return this.findById(subscription.id, options) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Subscription ${subscription.id} not found`); } return resUser; }); } resolveByToken(subscription, options = {}) { return this.findOne(lodash_1.defaultsDeep({ where: { token: subscription.token } }, options)) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Subscription token ${subscription.token} not found`); } return resUser; }); } resolveByNumber(subscription, options = {}) { return this.findById(subscription, options) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Subscription ${subscription.token} not found`); } return resUser; }); } resolveByString(subscription, options = {}) { return this.findOne(lodash_1.defaultsDeep({ where: { token: subscription } }, options)) .then(resUser => { if (!resUser && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Subscription ${subscription} not found`); } return resUser; }); } resolve(subscription, options = {}) { const resolvers = { 'instance': subscription instanceof this.instance, 'id': !!(subscription && lodash_1.isObject(subscription) && subscription.id), 'token': !!(subscription && lodash_1.isObject(subscription) && subscription.token), 'number': !!(subscription && lodash_1.isNumber(subscription)), 'string': !!(subscription && lodash_1.isString(subscription)) }; const type = Object.keys(resolvers).find((key) => resolvers[key]); switch (type) { case 'instance': { return this.resolveByInstance(subscription, options); } case 'id': { return this.resolveById(subscription, options); } case 'token': { return this.resolveByToken(subscription, options); } case 'number': { return this.resolveByNumber(subscription, options); } case 'string': { return this.resolveByString(subscription, options); } default: { const err = new Error(`Unable to resolve Subscription ${subscription}`); return Promise.reject(err); } } } } exports.SubscriptionResolver = SubscriptionResolver; class Subscription extends common_1.FabrixModel { static get resolver() { return SubscriptionResolver; } static config(app, Sequelize) { return { options: { underscored: true, enums: { INTERVALS: enums_1.INTERVALS, SUBSCRIPTION_CANCEL: enums_2.SUBSCRIPTION_CANCEL }, scopes: { live: { where: { live_mode: true } }, active: { where: { active: true } }, deactivated: { where: { active: false, cancelled: false } }, cancelled: { where: { cancelled: true } } }, indexes: [ { fields: ['line_items'], using: 'gin', operator: 'jsonb_path_ops' } ], hooks: { beforeCreate: [ (subscription, options) => { return app.services.SubscriptionService.beforeCreate(subscription) .catch(err => { return Promise.reject(err); }); } ], beforeUpdate: [ (subscription, options) => { return app.services.SubscriptionService.beforeUpdate(subscription) .catch(err => { return Promise.reject(err); }); } ], afterCreate: [ (subscription, options) => { return app.services.SubscriptionService.afterCreate(subscription, options) .then(subscription => { return subscription.save({ transaction: options.transaction || null }); }) .catch(err => { return Promise.reject(err); }); } ], afterUpdate: [ (subscription, options) => { return app.services.SubscriptionService.afterCreate(subscription, options) .catch(err => { return Promise.reject(err); }); } ] } } }; } static schema(app, Sequelize) { return { token: { type: Sequelize.STRING, unique: true }, shop_id: { type: Sequelize.INTEGER, }, original_order_id: { type: Sequelize.INTEGER, }, last_order_id: { type: Sequelize.INTEGER, }, customer_id: { type: Sequelize.INTEGER, allowNull: false }, email: { type: Sequelize.STRING }, interval: { type: Sequelize.INTEGER, defaultValue: 0 }, unit: { type: Sequelize.ENUM, values: lodash_1.values(enums_1.INTERVALS), defaultValue: enums_1.INTERVALS.MONTH }, active: { type: Sequelize.BOOLEAN, defaultValue: true }, renewed_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, renews_on: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, total_renewals: { type: Sequelize.INTEGER, defaultValue: 0 }, renew_retry_at: { type: Sequelize.DATE }, total_renewal_attempts: { type: Sequelize.INTEGER, defaultValue: 0 }, cancelled: { type: Sequelize.BOOLEAN, defaultValue: false }, cancel_reason: { type: Sequelize.ENUM, values: lodash_1.values(enums_2.SUBSCRIPTION_CANCEL) }, cancelled_at: { type: Sequelize.DATE }, currency: { type: Sequelize.STRING, defaultValue: 'USD' }, line_items: { type: Sequelize.JSONB, defaultValue: [] }, subtotal_price: { type: Sequelize.INTEGER, defaultValue: 0 }, discounted_lines: { type: Sequelize.JSONB, defaultValue: [] }, coupon_lines: { type: Sequelize.JSONB, defaultValue: [] }, shipping_lines: { type: Sequelize.JSONB, defaultValue: [] }, shipping_included: { type: Sequelize.BOOLEAN, defaultValue: false }, shipping_rate: { type: Sequelize.JSONB, defaultValue: [] }, shipping_rates: { type: Sequelize.JSONB, defaultValue: [] }, has_shipping: { type: Sequelize.BOOLEAN, defaultValue: false }, has_taxes: { type: Sequelize.BOOLEAN, defaultValue: false }, total_items: { type: Sequelize.INTEGER, defaultValue: 0 }, tax_shipping: { type: Sequelize.BOOLEAN, defaultValue: false }, tax_lines: { type: Sequelize.JSONB, defaultValue: [] }, tax_rate: { type: Sequelize.FLOAT, defaultValue: 0.0 }, tax_percentage: { type: Sequelize.FLOAT, defaultValue: 0.0 }, taxes_included: { type: Sequelize.BOOLEAN, defaultValue: false }, total_discounts: { type: Sequelize.INTEGER, defaultValue: 0 }, total_coupons: { type: Sequelize.INTEGER, defaultValue: 0 }, total_line_items_price: { type: Sequelize.INTEGER, defaultValue: 0 }, pricing_overrides: { type: Sequelize.JSONB, defaultValue: [] }, pricing_override_id: { type: Sequelize.INTEGER }, total_overrides: { type: Sequelize.INTEGER, defaultValue: 0 }, total_price: { type: Sequelize.INTEGER, defaultValue: 0 }, total_due: { type: Sequelize.INTEGER, defaultValue: 0 }, total_shipping: { type: Sequelize.INTEGER, defaultValue: 0 }, total_tax: { type: Sequelize.INTEGER, defaultValue: 0 }, total_weight: { type: Sequelize.INTEGER, defaultValue: 0 }, notice_sent: { type: Sequelize.BOOLEAN, defaultValue: false, }, notice_sent_at: { type: Sequelize.DATE }, live_mode: { type: Sequelize.BOOLEAN, defaultValue: app.config.get('cart.live_mode') } }; } static associate(models) { models.Subscription.belongsTo(models.Customer, {}); models.Subscription.belongsTo(models.Shop, {}); models.Subscription.belongsTo(models.Order, { as: 'original_order' }); models.Subscription.belongsTo(models.Order, { as: 'last_order' }); models.Subscription.belongsToMany(models.Collection, { as: 'collections', through: { model: models.ItemCollection, unique: false, scope: { model: 'subscription' } }, foreignKey: 'model_id', constraints: false }); models.Subscription.hasMany(models.Event, { as: 'events', foreignKey: 'object_id', scope: { object: 'subscription' }, constraints: false }); models.Subscription.hasMany(models.OrderItem, { as: 'order_items', foreignKey: 'subscription_id' }); models.Subscription.belongsToMany(models.Discount, { as: 'discounts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'subscription' } }, foreignKey: 'model_id', constraints: false }); } } exports.Subscription = Subscription; Subscription.prototype.activate = function () { this.cancel_reason = null; this.cancelled_at = null; this.cancelled = false; this.active = true; const d = moment().startOf('hour'); const r = d.clone(); if (this.unit === enums_1.INTERVALS.DAY) { d.add(this.interval, 'day'); } else if (this.unit === enums_1.INTERVALS.WEEK) { d.add(this.interval, 'week'); } else if (this.unit === enums_1.INTERVALS.MONTH) { d.add(this.interval, 'month'); } else if (this.unit === enums_1.INTERVALS.BIMONTH) { d.add(this.interval * 2, 'month'); } else if (this.unit === enums_1.INTERVALS.YEAR) { d.add(this.interval, 'year'); } else if (this.unit === enums_1.INTERVALS.BIYEAR) { d.add(this.interval * 2, 'year'); } if (moment() > moment(this.renews_on)) { this.renewed_at = r.format('YYYY-MM-DD HH:mm:ss'); this.renews_on = d.format('YYYY-MM-DD HH:mm:ss'); } return this; }; Subscription.prototype.resolveCustomer = function (options = {}) { if (this.Customer && this.Customer instanceof this.app.models['Customer'].instance && options.reload !== true) { return Promise.resolve(this); } else if (!this.customer_id) { return Promise.resolve(this); } else { return this.getCustomer({ transaction: options.transaction || null }) .then(_customer => { _customer = _customer || null; this.Customer = _customer; this.setDataValue('Customer', _customer); this.set('Customer', _customer); return this; }); } }, Subscription.prototype.resolveDiscounts = function (options = {}) { if (this.discounts && this.discounts.length > 0 && this.discounts.every(d => d instanceof this.app.models['Discount'].instance) && 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; }); } }; Subscription.prototype.resolveLastOrder = function (options = {}) { if (this.last_order && this.last_order instanceof this.app.models['Order'].instance && options.reload !== true) { return Promise.resolve(this); } else if (!this.last_order_id) { return Promise.resolve(this); } else { return this.getLast_order({ transaction: options.transaction || null }) .then(_order => { _order = _order || null; this.last_order = _order; this.setDataValue('last_order', _order); this.set('last_order', _order); return this; }); } }; Subscription.prototype.resolveOriginalOrder = function (options = {}) { if (this.original_order && this.original_order instanceof this.app.models['Order'].instance && options.reload !== true) { return Promise.resolve(this); } else if (!this.original_order_id) { return Promise.resolve(this); } else { return this.getOriginal_order({ transaction: options.transaction || null }) .then(_order => { _order = _order || null; this.original_order = _order; this.setDataValue('original_order', _order); this.set('original_order', _order); return this; }); } }; Subscription.prototype.notifyCustomer = function (preNotification, options = {}) { if (this.customer_id) { return this.resolveCustomer({ attributes: ['id', 'email', 'company', 'first_name', 'last_name', 'full_name'], transaction: options.transaction || null, reload: options.reload || null }) .then(() => { if (this.Customer && this.Customer instanceof this.app.models['Customer'].instance) { return this.Customer.notifyUsers(preNotification, { transaction: options.transaction || null }); } else { return; } }) .then(() => { return this; }); } else { return Promise.resolve(this); } }; Subscription.prototype.resetDefaults = function () { this.total_items = 0; this.total_shipping = 0; this.subtotal_price = 0; this.total_discounts = 0; this.total_coupons = 0; this.total_tax = 0; this.total_weight = 0; this.total_line_items_price = 0; this.total_overrides = 0; this.total_price = 0; this.total_due = 0; this.has_shipping = false; this.has_taxes = false; this.discounted_lines = []; this.coupon_lines = []; this.shipping_lines = []; this.tax_lines = []; this.line_items.map(item => { item.shipping_lines = []; item.discounted_lines = []; item.coupon_lines = []; item.tax_lines = []; item.total_discounts = 0; item.calculated_price = item.price; return item; }); return this; }; Subscription.prototype.setLineProperties = function (line) { if (line.properties) { for (const l in line.properties) { if (line.properties.hasOwnProperty(l)) { line.price = line.price + line.properties[l].price; line.price_per_unit = line.price_per_unit + line.properties[l].price; } } } return line; }; Subscription.prototype.setLineItems = function (lines) { this.line_items = lines || []; this.total_items = 0; this.subtotal_price = 0; this.total_line_items_price = 0; this.line_items.forEach(item => { if (item.requires_shipping) { this.total_weight = this.total_weight + item.grams; this.has_shipping = true; } if (item.requires_taxes) { this.has_taxes = true; } this.total_items = this.total_items + item.quantity; this.subtotal_price = this.subtotal_price + item.price; this.total_line_items_price = this.total_line_items_price + item.price; }); return this.setTotals(); }; Subscription.prototype.setItemDiscountedLines = function (item, discount, criteria) { if (!(discount instanceof this.app.models['Discount'].instance)) { throw new Error('setItemDiscountedLines expects discount parameter to be a Discount Instance'); } item = discount.discountItem(item, criteria); return item; }; Subscription.prototype.setItemsDiscountedLines = function (discounts, criteria) { discounts = discounts || []; criteria = criteria || []; this.line_items = this.line_items || []; this.discounted_lines = []; const factoredDiscountedLines = []; let discountsArr = []; let discountedLines = []; this.line_items = this.line_items.map((item, index) => { discounts.forEach(discount => { item = this.setItemDiscountedLines(item, discount, criteria); }); if (item.discounted_lines.length > 0) { const i = discountedLines.findIndex(line => line.line === index); if (i > -1) { discountedLines[i].discounts = [...discountedLines[i].discounts, ...item.discounted_lines]; } else { discountedLines.push({ line: index, discounts: item.discounted_lines }); } } return item; }); discountedLines.forEach(line => { discountsArr = [...discountsArr, ...line.discounts.map(d => d.id)]; }); discountedLines = discountedLines.map(line => { line.discounts = line.discounts.map(discount => { if (discount.rules.applies_once && discountsArr.filter(d => d === discount.id).length > 1) { const arrRemove = discountsArr.findIndex(d => d === discount.id); discountsArr = discountsArr.splice(arrRemove, 1); discount.applies = false; } else if (discount.rules.minimum_order_amount > 0 && this.total_line_items_price < discount.minimum_order_amount) { discount.applies = false; } else if (discount.rules.applies_compound === false && discountsArr.length > 1) { discount.applies = false; } else { discount.applies = true; } return discount; }); return line; }); discountedLines.forEach(line => { line.discounts.forEach(discount => { const index = this.line_items[line.line].discounted_lines.findIndex(d => d.id === discount.id); this.line_items[line.line].discounted_lines[index].applies = discount.applies; }); }); this.line_items = this.line_items.map((item, index) => { item.discounted_lines.forEach(discountedLine => { if (discountedLine.applies === true) { const calculatedPrice = Math.max(0, item.calculated_price - discountedLine.price); const totalDeducted = Math.min(item.calculated_price, (item.calculated_price - (item.calculated_price - discountedLine.price))); item.calculated_price = calculatedPrice; item.total_discounts = Math.min(item.price, item.total_discounts + totalDeducted); const fI = factoredDiscountedLines.findIndex(d => d.id === discountedLine.id); if (fI > -1) { factoredDiscountedLines[fI].lines = [...factoredDiscountedLines[fI].lines, index]; factoredDiscountedLines[fI].price = factoredDiscountedLines[fI].price + totalDeducted; } else { discountedLine.lines = [index]; discountedLine.price = totalDeducted; factoredDiscountedLines.push(discountedLine); } } }); return item; }); return this.setDiscountedLines(factoredDiscountedLines); }; Subscription.prototype.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(); }; Subscription.prototype.setPricingOverrides = function (lines) { this.total_overrides = 0; this.pricing_overrides = lines || []; this.pricing_overrides.forEach(line => { this.total_overrides = this.total_overrides + line.price; }); return this.setTotals(); }, Subscription.prototype.setCouponLines = function (lines) { this.total_coupons = 0; this.coupon_lines = lines || []; this.coupon_lines.forEach(line => { this.total_coupons = this.total_coupons + line.price; }); return this.setTotals(); }; Subscription.prototype.setItemsShippingLines = function (items) { let shippingLines = []; let totalShipping = 0; this.line_items = this.line_items || []; this.line_items = this.line_items.map((item, i) => { const shippedLine = items.find(ii => ii.sku === item.sku); if (shippedLine) { shippedLine.shipping_lines = shippedLine.shipping_lines || []; shippedLine.shipping_lines.map(line => { line.line = i; return line; }); totalShipping = shippedLine.shipping_lines.forEach(line => { totalShipping = totalShipping + line.price; }); shippingLines = [...shippingLines, ...shippedLine.shipping_lines]; item.shipping_lines = shippedLine.shipping_lines; item.total_shipping = totalShipping; } return item; }); return this.setShippingLines(shippingLines); }; Subscription.prototype.setShippingLines = function (lines) { this.total_shipping = 0; this.shipping_lines = lines || []; this.shipping_lines.forEach(line => { this.total_shipping = this.total_shipping + line.price; }); return this.setTotals(); }; Subscription.prototype.setItemsTaxLines = function (items) { let taxesLines = []; let totalTaxes = 0; this.line_items = this.line_items || []; this.line_items = this.line_items.map((item, i) => { const taxedLine = items.find(i => i.sku === item.sku); if (taxedLine) { taxedLine.tax_lines = taxedLine.tax_lines || []; taxedLine.tax_lines.map(line => { line.line = i; return line; }); totalTaxes = taxedLine.tax_lines.forEach(line => { totalTaxes = totalTaxes + line.price; }); taxesLines = [...taxesLines, ...taxedLine.tax_lines]; item.tax_lines = taxedLine.tax_lines; item.total_taxes = totalTaxes; } return item; }); return this.setTaxLines(taxesLines); }; Subscription.prototype.setTaxLines = function (lines) { this.total_tax = 0; this.tax_lines = lines || []; this.tax_lines.forEach(line => { this.total_tax = this.total_tax + line.price; }); return this.setTotals(); }; Subscription.prototype.setTotals = function () { 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; }; Subscription.prototype.line = function (data) { data.Product = data.Product || {}; data.property_pricing = data.property_pricing || data.Product.property_pricing; data.properties = data.properties || []; const properties = {}; if (data.properties.length > 0 && data.property_pricing) { data.properties.forEach(prop => { if (!prop.name) { return; } if (data.property_pricing[prop.name]) { properties[prop.name] = data.property_pricing[prop.name]; if (prop.value) { properties[prop.name]['value'] = prop.value; } } }); } const line = { subscription_id: this.id, shop_id: data.shop_id, product_id: data.product_id, product_handle: data.Product.handle, variant_id: data.id || data.variant_id, type: data.type, sku: data.sku, title: data.Product.title, variant_title: data.title, name: data.title === data.Product.title ? data.title : `${data.Product.title} - ${data.title}`, properties: properties, pricing_properties: data.property_pricing, option: data.option, barcode: data.barcode, price: data.price * data.quantity, calculated_price: data.price * data.quantity, compare_at_price: data.compare_at_price, price_per_unit: data.price, currency: data.currency, fulfillment_service: data.fulfillment_service, gift_card: data.gift_card, requires_shipping: data.requires_shipping, requires_taxes: data.requires_taxes, tax_code: data.tax_code, tax_lines: [], total_taxes: 0, shipping_lines: [], total_shipping: 0, discounted_lines: [], total_discounts: 0, weight: data.weight * data.quantity, weight_unit: data.weight_unit, images: data.images.length > 0 ? data.images : data.Product.images, quantity: data.quantity, fulfillable_quantity: data.fulfillable_quantity, max_quantity: data.max_quantity, grams: this.app.services.ProxyCartService.resolveConversion(data.weight, data.weight_unit) * data.quantity, average_shipping: data.Product.average_shipping, exclude_payment_types: data.Product.exclude_payment_types, vendor: data.Product.vendor ? data.Product.vendor.name || data.Product.vendor : data.Product.vendor, live_mode: data.live_mode }; return line; }; Subscription.prototype.addLine = function (item, qty, properties, shop, options = {}) { let lineQtyAvailable = -1; return item.checkAvailability(qty, { shop: shop || null, transaction: options.transaction || null }) .then(availability => { if (!availability.allowed) { throw new Error(`${availability.title} is not available in this quantity, please try a lower quantity`); } if (availability.shop) { item.shop_id = availability.shop.id; } lineQtyAvailable = availability.quantity; return item.checkRestrictions(this.Customer || this.customer_id, { transaction: options.transaction || null }); }) .then(restricted => { if (restricted) { throw new Error(`${restricted.title} can not be delivered to ${restricted.city} ${restricted.province} ${restricted.country}`); } const lineItems = this.line_items; if (!qty || !lodash_1.isNumber(qty)) { qty = 1; } const itemIndex = lodash_1.findIndex(lineItems, { variant_id: item.id }); if (itemIndex > -1) { this.app.log.silly('Subscription.addLine NEW QTY', lineItems[itemIndex]); const maxQuantity = lineItems[itemIndex].max_quantity; let calculatedQty = lineItems[itemIndex].quantity + qty; if (maxQuantity > -1 && calculatedQty > maxQuantity) { calculatedQty = maxQuantity; } if (lineQtyAvailable > -1 && calculatedQty > lineQtyAvailable) { calculatedQty = Math.max(0, lineQtyAvailable - calculatedQty); } lineItems[itemIndex].quantity = calculatedQty; lineItems[itemIndex].fulfillable_quantity = calculatedQty; lineItems[itemIndex] = this.setLineProperties(lineItems[itemIndex]); this.line_items = lineItems; } else { const maxQuantity = item.max_quantity; let calculatedQty = qty; if (maxQuantity > -1 && calculatedQty > maxQuantity) { calculatedQty = maxQuantity; } if (lineQtyAvailable > -1 && calculatedQty > lineQtyAvailable) { calculatedQty = Math.max(0, lineQtyAvailable - calculatedQty); } item.quantity = calculatedQty; item.fulfillable_quantity = calculatedQty; item.max_quantity = maxQuantity; item.properties = properties; let line = this.line(item); line = this.setLineProperties(line); this.app.log.silly('Subscription.addLine NEW LINE', line); lineItems.push(line); this.line_items = lineItems; } return this; }); }; Subscription.prototype.removeLine = function (item, qty) { const lineItems = this.line_items; if (!qty || !lodash_1.isNumber(qty)) { qty = 1; } const itemIndex = lodash_1.findIndex(lineItems, { variant_id: item.id }); if (itemIndex > -1) { lineItems[itemIndex].quantity = lineItems[itemIndex].quantity - qty; lineItems[itemIndex].fulfillable_quantity = Math.max(0, lineItems[itemIndex].fulfillable_quantity - qty); if (lineItems[itemIndex].quantity < 1) { this.app.log.silly(`Cart.removeLine removing '${lineItems[itemIndex].variant_id}' line completely`); lineItems.splice(itemIndex, 1); } this.line_items = lineItems; return Promise.resolve(this); } }; Subscription.prototype.renew = function () { this.renewed_at = new Date(Date.now()); this.renew_retry_at = null; this.total_renewal_attempts = 0; this.total_renewals++; this.active = true; this.cancelled = false; this.cancel_reason = null; this.cancelled_at = null; this.notice_sent = false; this.notice_sent_at = null; return this; }; Subscription.prototype.willRenew = function () { this.notice_sent = true; this.notice_sent_at = new Date(Date.now()); return this; }; Subscription.prototype.retry = function () { this.renew_retry_at = new Date(Date.now()); this.total_renewal_attempts++; return this; }; Subscription.prototype.sendActivateEmail = function (options = {}) { return this.app.emails.Subscription.activated(this, { send_email: this.app.config.get('cart.emails.subscriptionRenewed') }, { transaction: options.transaction || null }) .then(email => { return this.notifyCustomer(email, { transaction: options.transaction || null }); }) .catch(err => { this.app.log.error(err); return; }); }; Subscription.prototype.sendCancelledEmail = function (options = {}) { return this.app.emails.Subscription.cancelled(this, { send_email: this.app.config.get('cart.emails.subscriptionCancelled') }, { transaction: options.transaction || null }) .then(email => { return this.notifyCustomer(email, { transaction: options.transaction || null }); }) .catch(err => { this.app.log.error(err); return; }); }; Subscription.prototype.sendDeactivateEmail = function (options = {}) { return this.app.emails.Subscription.deactivated(this, { send_email: this.app.config.get('cart.emails.subscriptionDeactivated') }, { transaction: options.transaction || null }) .then(email => { return this.notifyCustomer(email, { transaction: options.transaction || null }); }) .catch(err => { this.app.log.error(err); return; }); }; Subscription.prototype.sendFailedEmail = function (options = {}) { return this.app.emails.Subscription.failed(this, { send_email: this.app.config.get('cart.emails.subscriptionFailed') }, { transaction: options.transaction || null }) .then(email => { return this.notifyCustomer(email, { transaction: options.transaction || null }); }) .catch(err => { this.app.log.error(err); return; }); }; Subscription.prototype.sendRenewedEmail = function (options = {}) { return this.app.emails.Subscription.renewed(this, { send_email: this.app.config.get('cart.emails.subscriptionRenewed') }, { transaction: options.transaction || null }) .then(email => { return this.notifyCustomer(email, { transaction: options.transaction || null }); }) .catch(err => { this.app.log.error(err); return; }); }; Subscription.prototype.sendUpdatedEmail = function (options = {}) { return this.app.emails.Subscription.updated(this, { send_email: this.app.config.get('cart.emails.subscriptionUpdated') }, { transaction: options.transaction || null }) .then(email => { return this.notifyCustomer(email, { transaction: options.transaction || null }); }) .catch(err => { this.app.log.error(err); return; }); }; Subscription.prototype.sendWillRenewEmail = function (options = {}) { return this.app.emails.Subscription.willRenew(this, { send_email: this.app.config.get('cart.emails.subscriptionWillRenew') }, { transaction: options.transaction || null }) .then(email => { return this.notifyCustomer(email, { transaction: options.transaction || null }); }) .catch(err => { this.app.log.error(err); return; }); }; Subscription.prototype.buildOrder = function (data = {}) { return { client_details: data.client_details || this.client_details, ip: data.ip || null, payment_details: data.payment_details, payment_kind: data.payment_kind || this.app.config.get('cart.orders.payment_kind'), transaction_kind: data.transaction_kind || this.app.config.get('cart.orders.transaction_kind'), fulfillment_kind: data.fulfillment_kind || this.app.config.get('cart.orders.fulfillment_kind'), processing_method: data.processing_method || enums_4.PAYMENT_PROCESSING_METHOD.SUBSCRIPTION, shipping_address: data.shipping_address || this.shipping_address, billing_address: data.billing_address || this.billing_address, customer_id: data.customer_id || this.customer_id || null, email: data.email || null, user_id: data.user_id || this.user_id || null, subscription_token: this.token, currency: this.currency, line_items: this.line_items, tax_lines: this.tax_lines, shipping_lines: this.shipping_lines, discounted_lines: this.discounted_lines, coupon_lines: this.coupon_lines, subtotal_price: this.subtotal_price, taxes_included: this.taxes_included, total_discounts: this.total_discounts, total_coupons: this.total_coupons, total_line_items_price: this.total_line_items_price, total_price: this.total_due, total_due: this.total_due, total_tax: this.total_tax, total_weight: this.total_weight, total_items: this.total_items, shop_id: this.shop_id, has_shipping: this.has_shipping, has_subscription: this.has_subscription, has_taxes: this.has_taxes, pricing_override_id: this.pricing_override_id, pricing_overrides: this.pricing_overrides || [], total_overrides: this.total_overrides }; }; Subscription.prototype.calculateDiscounts = function (options = {}) { const criteria = []; const productIds = this.line_items.map(item => item.product_id); let collectionPairs = [], discountCriteria = [], checkHistory = []; let resDiscounts; return Promise.resolve() .then(() => { return this.getCollectionPairs({ transaction: options.transaction || null }); }) .then(_collections => { collectionPairs = _collections || []; if (this.id) { criteria.push({ model: 'cart', model_id: this.id }); } if (this.customer_id) { criteria.push({ model: 'customer', model_id: this.customer_id }); } if (productIds.length > 0) { criteria.push({ model: 'product', model_id: productIds }); } if (collectionPairs.length > 0) { criteria.push({ model: 'collection', model_id: collectionPairs.map(c => c.collection) }); } if (criteria.length > 0) { return this.app.models['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 = lodash_1.merge(d, collectionPairs[i]); } }); } return d; }); this.app.log.debug('Subscription.calculateDiscount criteria', discountCriteria); if (discounts.length > 0) { return this.app.models['Discount'].findAll({ where: { id: discounts.map(item => item.discount_id), status: enums_3.DISCOUNT_STATUS.ENABLED }, transaction: options.transaction || null }); } else { return []; } }) .then(_discounts => { _discounts = _discounts || []; resDiscounts = _discounts; resDiscounts.forEach(discount => { if (discount.applies_once_per_customer && this.customer_id) { checkHistory.push(discount); } }); if (checkHistory.length > 0) { return Promise.all(checkHistory.map(discount => { return discount.eligibleCustomer(this.customer_id, { transaction: options.transaction || null }); })); } else { return []; } }) .then(_eligible => { _eligible = _eligible || []; _eligible.forEach(discount => { const i = resDiscounts.findIndex(ii => ii.id === discount.id); if (i > -1) { resDiscounts.splice(i, 1); } }); return this.setItemsDiscountedLines(resDiscounts, discountCriteria); }) .catch(err => { this.app.log.error(err); return this; }); }; Subscription.prototype.getCollectionPairs = function (options = {}) { const collectionPairs = []; const criteria = []; let productIds = this.line_items.map(item => item.product_id); productIds = productIds.filter(i => i); let variantIds = this.line_items.map(item => item.variant_id); variantIds = variantIds.filter(i => i); return Promise.resolve() .then(() => { if (this.customer_id) { criteria.push({ model: 'customer', model_id: this.customer_id }); } if (productIds.length > 0) { criteria.push({ model: 'product', model_id: productIds }); } if (variantIds.length > 0) { criteria.push({ model: 'productvariant', model_id: variantIds }); } if (criteria.length > 0) { return this.app.models['ItemCollection'].findAll({ where: { $or: criteria }, attributes: ['id', 'collection_id', 'model', 'model_id'], transaction: options.transaction || null }); } return []; }) .then(_collections => { _collections = _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 => { this.app.log.error(err); return []; }); }; Subscription.prototype.calculateTaxes = function (options = {}) { if (!this.has_taxes) { return Promise.resolve(this); } return this.app.services.TaxService.calculate(this, this.line_items, this.shipping_address, this.app.models['Subscription'], options) .then(taxesResult => { this.setItemsTaxLines(taxesResult.line_items); return this; }) .catch(err => { this.app.log.error(err); return this; }); }, Subscription.prototype.recalculate = function (options = {}) { const d = moment(this.renewed_at); if (this.unit === enums_1.INTERVALS.DAY) { d.add(this.interval, 'day'); } else if (this.unit === enums_1.INTERVALS.WEEK) { d.add(this.interval, 'week'); } else if (this.unit === enums_1.INTERVALS.MONTH) { d.add(this.interval, 'month'); } else if (this.unit === enums_1.INTERVALS.BIMONTH) { d.add(this.interval * 2, 'month'); } else if (this.unit === enums_1.INTERVALS.YEAR) { d.add(this.interval, 'year'); } else if (this.unit === enums_1.INTERVALS.BIYEAR) { d.add(this.interval * 2, 'year'); } this.renews_on = d.format('YYYY-MM-DD HH:mm:ss'); this.resetDefaults(); this.setLineItems(this.line_items); return Promise.resolve() .then(() => { return this.calculateDiscounts({ transaction: options.transaction || null }); }) .then(() => { return this.calculateTaxes({ transaction: options.transaction || null }); }) .then(() => { return this.setTotals(); }) .catch(err => { this.app.log.error(err); return this; }); };