UNPKG

trailpack-proxy-cart

Version:

eCommerce - Trailpack for Proxy Engine

1,404 lines (1,308 loc) 65.9 kB
/* eslint new-cap: [0] */ /* eslint no-console: [0] */ 'use strict' const Model = require('trails/model') const helpers = require('proxy-engine-helpers') const Errors = require('proxy-engine-errors') const _ = require('lodash') const CART_STATUS = require('../../lib').Enums.CART_STATUS const DISCOUNT_STATUS = require('../../lib').Enums.DISCOUNT_STATUS const PAYMENT_PROCESSING_METHOD = require('../../lib').Enums.PAYMENT_PROCESSING_METHOD const queryDefaults = require('../utils/queryDefaults') /** * @module Cart * @description Cart Model */ module.exports = class Cart extends Model { static config (app, Sequelize) { return { options: { enums: { CART_STATUS: CART_STATUS }, underscored: true, // defaultScope: { // where: { // live_mode: app.config.proxyEngine.live_mode // } // }, scopes: { live: { where: { live_mode: true } } }, indexes: [ // Creates a gin index on data with the jsonb_path_ops operator { fields: ['line_items'], using: 'gin', operator: 'jsonb_path_ops' }, { fields: ['client_details'], using: 'gin', operator: 'jsonb_path_ops' } ], hooks: { beforeCreate: (values, options) => { return app.services.CartService.beforeCreate(values, options) .catch(err => { return Promise.reject(err) }) }, beforeUpdate: (values, options) => { return app.services.CartService.beforeUpdate(values, options) .catch(err => { return Promise.reject(err) }) }, beforeSave: (values, options) => { return app.services.CartService.beforeSave(values, options) .catch(err => { return Promise.reject(err) }) } }, classMethods: { /** * Associate the Model * @param models */ associate: (models) => { models.Cart.belongsTo(models.Customer, { foreignKey: 'customer_id' // as: 'customer_id', }) models.Cart.belongsTo(models.Shop, { foreignKey: 'shop_id' // as: 'shop_id', }) models.Cart.belongsTo(models.Order, { foreignKey: 'order_id' // as: 'shop_id', }) models.Cart.belongsToMany(models.User, { as: 'owners', through: { model: models.UserItem, scope: { item: 'cart' } }, foreign_id: 'item_id', constraints: false }) models.Cart.belongsTo(models.Address, { as: 'shipping_address', foreignKey: 'shipping_address_id' }) models.Cart.belongsTo(models.Address, { as: 'billing_address', foreignKey: 'billing_address_id' }) models.Cart.belongsToMany(models.Address, { as: 'addresses', // otherKey: 'address_id', foreignKey: 'model_id', through: { model: models.ItemAddress, scope: { model: 'cart' }, constraints: false }, constraints: false }) models.Cart.belongsToMany(models.Discount, { as: 'discounts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'cart' } }, foreignKey: 'model_id', constraints: false }) // models.Cart.belongsTo(models.Customer, { // foreignKey: 'default_cart' // // as: 'customer_id' // }) // models.Cart.hasMany(models.Product, { // as: 'products' // // constraints: false // }) // models.Cart.hasMany(models.ProductVariant, { // as: 'variants' // // constraints: false // }) // models.Cart.hasMany(models.Coupon, { // as: 'coupons' // // constraints: false // }) // models.Cart.hasMany(models.GiftCard, { // as: 'gift_cards' // // constraints: false // }) }, /** * * @param criteria * @param options * @returns {*|Promise.<Instance>} */ findByIdDefault: function(criteria, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Cart.default(app), options || {} ) return this.findById(criteria, options) }, /** * * @param options * @returns {*|Promise.<Instance>} */ findOneDefault: function(options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Cart.default(app), options || {} ) return this.findOne(options) }, /** * * @param token * @param options * @returns {*|Promise.<Instance>} */ findByTokenDefault: function(token, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Cart.default(app), options || {}, { where: { token: token } } ) return this.findOne(options) }, /** * * @param cart * @param options * @returns {*} */ resolve: function(cart, options){ options = options || {} const Cart = this if (cart instanceof Cart){ return Promise.resolve(cart) } else if (cart && _.isObject(cart) && cart.id) { return Cart.findByIdDefault(cart.id, options) .then(resCart => { if (!resCart) { throw new Errors.FoundError(Error(`Cart ${cart.id} not found`)) } return resCart }) } else if (cart && _.isObject(cart) && cart.token) { return Cart.findByTokenDefault(cart.token, options) .then(resCart => { if (!resCart) { throw new Errors.FoundError(Error(`Cart ${cart.token} not found`)) } return resCart }) } else if (cart && _.isObject(cart)) { return this.create(cart, options) } else if (cart && (_.isNumber(cart))) { return Cart.findByIdDefault(cart, options) .then(resCart => { if (!resCart) { throw new Errors.FoundError(Error(`Cart ${cart} not found`)) } return resCart }) } else if (cart && (_.isString(cart))) { return Cart.findByTokenDefault(cart, options) .then(resCart => { if (!resCart) { throw new Errors.FoundError(Error(`Cart ${cart} not found`)) } return resCart }) } else { // TODO create proper error const err = new Error(`Unable to resolve Cart ${cart}`) return Promise.reject(err) } } }, instanceMethods: { /** * Resets the defaults so they can be recalculated * @returns {*} */ 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_subscription = false this.has_shipping = false this.has_taxes = false this.discounted_lines = [] this.coupon_lines = [] // this.shipping_lines = [] // this.tax_lines = [] // Filter any non manual tax lines this.tax_lines = this.tax_lines.filter(line => Object.keys(line).indexOf('line') === -1 ) // Filter any non manual shipping lines this.shipping_lines = this.shipping_lines.filter(line => Object.keys(line).indexOf('line') === -1 ) // Reset line items 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 }, /** * * @param lines */ setLineItems: function(lines) { this.line_items = lines || [] this.total_items = 0 this.subtotal_price = 0 this.total_line_items_price = 0 this.has_shipping = this.line_items.some(item => item.requires_shipping) this.has_taxes = this.line_items.some(item => item.requires_taxes) this.line_items.forEach(item => { // Check if at least one time requires shipping if (item.requires_shipping) { this.total_weight = this.total_weight + item.grams } // Check if at least one item requires subscription if (item.requires_subscription) { this.has_subscription = 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() }, /** * * @param item * @param discount * @param criteria * @returns {*} */ setItemDiscountedLines: function(item, discount, criteria) { if (!(discount instanceof app.orm['Discount'])) { throw new Error('setItemDiscountedLines expects discount parameter to be a Discount Instance') } item = discount.discountItem(item, criteria) return item }, /** * * @param discounts * @param criteria * @returns {*} */ setItemsDiscountedLines: function (discounts, criteria) { // Make this an array if null discounts = discounts || [] // Make this an array if null criteria = criteria || [] // Make this an array if null this.line_items = this.line_items || [] // 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 = [] // Holds list lines and their discounts let discountedLines = [] // For each item run the normal discounts 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 }) // Gather all discounts into a single array discountedLines.forEach(line => { discountsArr = [...discountsArr, ...line.discounts.map(d => d.id)] }) // Check rules discountedLines = discountedLines.map(line => { line.discounts = line.discounts.map(discount => { // 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 && this.total_line_items_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 }) return line }) // console.log('Lines results', discountedLines) // Apply rules to line item discounts 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 }) }) // Loop through items and apply discounts and factor cart discounted_lines this.line_items = this.line_items.map((item, index) => { item.discounted_lines.forEach(discountedLine => { if (discountedLine.applies === true) { // New Calculated Price const calculatedPrice = Math.max(0, item.calculated_price - discountedLine.price) // Total Deducted const totalDeducted = Math.min(item.calculated_price, (item.calculated_price - (item.calculated_price - discountedLine.price))) // Set item calculated price item.calculated_price = calculatedPrice // Set item total_discounts 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) }, /** * * @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() }, /** * * @param lines */ 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() }, /** * * @param lines */ 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() }, setItemsShippingLines: function (items) { let shippingLines = [] let totalShipping = 0 // Make this an array if null this.line_items = this.line_items || [] this.line_items = this.line_items.map((item, i) => { const shippedLine = items.find(i => i.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 }) // console.log('SHIPPED LINE', shippedLine) shippingLines = [...shippingLines, ...shippedLine.shipping_lines] item.shipping_lines = shippedLine.shipping_lines item.total_shipping = totalShipping } return item }) return this.setShippingLines(shippingLines) }, /** * * @param lines */ setShippingLines: function(lines) { lines = lines || [] this.total_shipping = 0 this.shipping_lines = [...this.shipping_lines, ...lines], this.shipping_lines.forEach(line => { this.total_shipping = this.total_shipping + line.price }) return this.setTotals() }, setItemsTaxLines: function (items) { let taxesLines = [] let totalTaxes = 0 // Make this an array if null 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 }) // console.log('TAXED LINE', taxedLine) taxesLines = [...taxesLines, ...taxedLine.tax_lines] item.tax_lines = taxedLine.tax_lines item.total_taxes = totalTaxes } return item }) return this.setTaxLines(taxesLines) }, /** * * @param lines */ setTaxLines: function(lines) { lines = lines || [] this.total_tax = 0 this.tax_lines = [...this.tax_lines, ...lines] this.tax_lines.forEach(line => { this.total_tax = this.total_tax + line.price }) return this.setTotals() }, /** * */ 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 }, setLineProperties: (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 }, /** * * @param data */ // TODO Select Vendor line: function(data){ // handle empty product 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 = { 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, property_pricing: 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, requires_subscription: data.requires_subscription, subscription_interval: data.subscription_interval, subscription_unit: data.subscription_unit, 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: app.services.ProxyCartService.resolveConversion(data.weight, data.weight_unit) * data.quantity, vendors: data.Product.vendors, vendor_id: data.vendor_id || null, average_shipping: data.Product.average_shipping, exclude_payment_types: data.Product.exclude_payment_types, fulfillment_extras: data.fufillment_extras, live_mode: data.live_mode } return line }, /** * * @param item * @param qty * @param properties * @param options * @returns {Promise.<TResult>} */ addLine: function(item, qty, properties, options) { options = options || {} // The quantity available of this variant let lineQtyAvailable = -1 let line // Check if Product is Available return item.checkAvailability(qty, {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`) } lineQtyAvailable = availability.quantity // Check if Product is Restricted 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}`) } // Rename line items so they are no longer immutable const lineItems = this.line_items // Make quantity an integer if (!qty || !_.isNumber(qty)) { qty = 1 } const itemIndex = _.findIndex(lineItems, {variant_id: item.id}) // If already in cart if (itemIndex > -1) { app.log.silly('Cart.addLine NEW QTY', lineItems[itemIndex]) const maxQuantity = lineItems[itemIndex].max_quantity || -1 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 } // If new item else { const maxQuantity = item.max_quantity || -1 let calculatedQty = qty if (maxQuantity > -1 && calculatedQty > maxQuantity) { calculatedQty = maxQuantity } if (lineQtyAvailable > -1 && calculatedQty > lineQtyAvailable) { calculatedQty = Math.max(0, lineQtyAvailable - calculatedQty) } // Item Quantity in cart item.quantity = calculatedQty // The max that will be fulfilled item.fulfillable_quantity = calculatedQty // The max allowed to be purchased item.max_quantity = maxQuantity // The properties of the item item.properties = properties // Set line line = this.line(item) line = this.setLineProperties(line) app.log.silly('Cart.addLine NEW LINE', line) // Add line to line items lineItems.push(line) // Assign line items this.line_items = lineItems } return this }) }, /** * * @param item * @param qty * @returns {Promise.<this>} */ removeLine: function(item, qty, options) { options = options || {} const lineItems = this.line_items if (!qty || !_.isNumber(qty)) { qty = 1 } const itemIndex = _.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) // Resolve Grams if ( lineItems[itemIndex].quantity < 1) { app.log.silly(`Cart.removeLine removing '${lineItems[itemIndex].variant_id}' line completely`) lineItems.splice(itemIndex, 1) } this.line_items = lineItems return Promise.resolve(this) } }, /** * * @param shipping * @param options * @returns {Promise.<T>} */ addShipping: function(shipping, options) { shipping = shipping || [] options = options || {} const shippingLines = this.shipping_lines if (_.isArray(shipping)) { shipping.forEach(ship => { const i = _.findIndex(shippingLines, (s) => { return s.name === ship.name }) // Make sure shipping price is a number ship.price = app.services.ProxyCartService.normalizeCurrency(parseInt(ship.price)) if (i > -1) { shippingLines[i] = ship } else { shippingLines.push(ship) } }) } else if (_.isObject(shipping)){ const i = _.findIndex(shippingLines, (s) => { return s.name === shipping.name }) // Make sure shipping price is a number shipping.price = app.services.ProxyCartService.normalizeCurrency(parseInt(shipping.price)) if (i > -1) { shippingLines[i] = shipping } else { shippingLines.push(shipping) } } this.shipping_lines = shippingLines // this.setShippingLines(shippingLines) return this.save({transaction: options.transaction || null}) }, /** * * @param shipping * @param options * @returns {Promise.<T>} */ removeShipping: function(shipping, options){ shipping = shipping || [] options = options || {} const shippingLines = this.shipping_lines if (_.isArray(shipping)) { shipping.forEach(ship => { const i = _.findIndex(shippingLines, (s) => { return s.name === ship.name }) if (i > -1) { shippingLines.splice(i, 1) } }) } else if (_.isObject(shipping)) { const i = _.findIndex(shippingLines, (s) => { return s.name === shipping.name }) if (i > -1) { shippingLines.splice(i, 1) } } this.shipping_lines = shippingLines // this.setShippingLines(shippingLines) return this.save({transaction: options.transaction || null}) }, /** * * @param taxes * @param options * @returns {Promise.<T>} */ addTaxes: function(taxes, options) { taxes = taxes || [] options = options || {} const taxLines = this.tax_lines if (_.isArray(taxes)) { taxes.forEach(tax => { const i = _.findIndex(taxLines, (s) => { return s.name === tax.name }) // Make sure taxes price is a number tax.price = app.services.ProxyCartService.normalizeCurrency(parseInt(tax.price)) if (i > -1) { taxLines[i] = tax } else { taxLines.push(tax) } }) } else if (_.isObject(taxes)) { const i = _.findIndex(taxLines, (s) => { return s.name === taxes.name }) // Make sure taxes price is a number taxes.price = app.services.ProxyCartService.normalizeCurrency(parseInt(taxes.price)) if (i > -1) { taxLines[i] = taxes } else { taxLines.push(taxes) } } this.tax_lines = taxLines return this.save({transaction: options.transaction || null}) }, /** * * @param taxes * @param options * @returns {Promise.<T>} */ removeTaxes: function(taxes, options){ taxes = taxes || [] options = options || {} const taxLines = this.tax_lines if (_.isArray(taxes)) { taxes.forEach(tax => { const i = _.findIndex(taxLines, (s) => { return s.name === tax.name }) if (i > -1) { taxLines.splice(i, 1) } }) } else if (_.isObject(taxes)) { const i = _.findIndex(taxLines, (s) => { return s.name === taxes.name }) if (i > -1) { taxLines.splice(i, 1) } } this.tax_lines = taxLines return this.save({transaction: options.transaction || null}) }, /** * * @param status * @param save */ close: function(status, save) { this.status = status || CART_STATUS.CLOSED if (save) { return this.save(save) } return this //Promise.resolve(this) }, /** * * @param status * @param save */ draft: function (status, save) { this.status = status || CART_STATUS.DRAFT if (save) { return this.save(save) } return this //Promise.resolve(this) }, clear: function () { this.line_items = [] return this }, /** * * @param order * @param save */ ordered: function(order, save) { this.order_id = order.id this.status = CART_STATUS.ORDERED if (save) { return this.save(save) } // console.log('WANTS PROMISE', this) return this //Promise.resolve(this) }, /** * * @param options */ buildOrder: function(options) { options = options || {} return { // Request info client_details: options.client_details || this.client_details || {}, ip: options.ip || null, payment_details: options.payment_details, payment_kind: options.payment_kind || app.config.proxyCart.orders.payment_kind, transaction_kind: options.transaction_kind || app.config.proxyCart.orders.transaction_kind, fulfillment_kind: options.fulfillment_kind || app.config.proxyCart.orders.fulfillment_kind, processing_method: options.processing_method || PAYMENT_PROCESSING_METHOD.CHECKOUT, shipping_address: options.shipping_address || this.shipping_address, billing_address: options.billing_address || this.billing_address, email: options.email || null, // Customer Info customer_id: options.customer_id || this.customer_id || null, // User ID user_id: options.user_id || this.user_id || null, // Cart Info cart_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_taxes: this.has_taxes, has_subscription: this.has_subscription, notes: this.notes, //Pricing Overrides pricing_override_id: this.pricing_override_id, pricing_overrides: this.pricing_overrides, total_overrides: this.total_overrides } }, /** * * @param options * @returns {*} */ calculatePricingOverrides: function(options) { options = options || {} this.line_items = this.line_items || [] this.pricing_overrides = this.pricing_overrides || [] let pricingOverrides = [] let deduction = 0 if (!this.customer_id) { return Promise.resolve(this) } return Promise.resolve() .then(() => { if (this.Customer) { return this.Customer } else { return app.orm['Customer'].findById(this.customer_id, { attributes: ['id', 'account_balance'], transaction: options.transaction || null }) } }) .then(_customer => { if (!_customer) { return } const exclusions = this.line_items.filter(item => { item.exclude_payment_types = item.exclude_payment_types || [] return item.exclude_payment_types.indexOf('Account Balance') !== -1 }) pricingOverrides = this.pricing_overrides.filter(override => override) const accountBalanceIndex = pricingOverrides.findIndex(p => p.name === 'Account Balance') if (_customer.account_balance > 0) { // Apply Customer Account balance const removeTotal = _.sumBy(exclusions, (e) => e.calculated_price) const deductibleTotal = Math.max(0, this.total_due - removeTotal) deduction = Math.min(deductibleTotal, (deductibleTotal - (deductibleTotal - _customer.account_balance))) if (deduction > 0) { // If account balance has not been applied if (accountBalanceIndex === -1) { pricingOverrides.push({ name: 'Account Balance', price: deduction }) } else { pricingOverrides[accountBalanceIndex].price = deduction } } } else { if (accountBalanceIndex > -1) { pricingOverrides.splice(accountBalanceIndex, 1) } } return this.setPricingOverrides(pricingOverrides) }) .catch(err => { app.log.error(err) return this }) }, /** * * @param options * @returns {Promise.<TResult>} */ calculateDiscounts(options) { options = 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 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 }) 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 && 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(i => i.id === discount.id) if (i > -1) { resDiscounts.splice(i, 1) } }) return this.setItemsDiscountedLines(resDiscounts, discountCriteria) }) .catch(err => { app.log.error(err) return this }) }, /** * * @param options * @returns {Promise.<TResult>} */ calculateShipping: function(options) { options = options || {} if (!this.has_shipping) { return Promise.resolve(this) } return app.services.ShippingService.calculate(this, this.line_items, this.shipping_address, app.orm['Cart'], options) .then(shippingResult => { // console.log('WORKING ON SHIPPING RESULT', shippingResult.line_items) this.setItemsShippingLines(shippingResult.line_items) return this }) .catch(err => { app.log.error(err) return this }) }, /** * * @param options * @returns {Promise.<TResult>} */ calculateTaxes: function(options) { options = options || {} if (!this.has_taxes) { return Promise.resolve(this) } return app.services.TaxService.calculate(this, this.line_items, this.shipping_address, app.orm['Cart'], options) .then(taxesResult => { // console.log('WORKING ON TAXES RESULT', taxesResult.line_items) this.setItemsTaxLines(taxesResult.line_items) return this }) .catch(err => { app.log.error(err) return this }) }, /** * * @param options * @returns {Promise.<TResult>} */ recalculate: function(options) { options = options || {} // Default Values // const collections = [] this.resetDefaults() this.setLineItems(this.line_items) return Promise.resolve() .then(() => { return this.calculateDiscounts({transaction: options.transaction || null}) }) // .then(discounts => { // // Calculate Coupons // return app.services.CouponService.calculate(this, collections, app.orm['Cart']) // }) .then(() => { return this.calculateShipping({transaction: options.transaction || null}) }) .then(() => { return this.calculateTaxes({transaction: options.transaction || null}) }) .then(() => { // Calculate Customer Balance return this.calculatePricingOverrides({transaction: options.transaction || null}) }) .then(() => { return this.setTotals() }) .catch(err => { app.log.error(err) return this }) }, /** * * @param options * @returns {*} */ resolveCustomer: function(options) { options = options || {} if ( this.Customer && this.Customer instanceof app.orm['Customer'] && options.reload !== true ) { return Promise.resolve(this) } // Some orders may not have a customer Id else if (!this.customer_id) { return Promise.resolve(this) } else { return this.getCustomer({transaction: options.transaction || null}) .then(_customer => { if (_customer) { _customer = _customer || null this.Customer = _customer this.setDataValue('Customer', _customer) this.set('Customer', _customer) }