UNPKG

@fabrix/spool-cart

Version:

Spool - eCommerce Spool for Fabrix

1,368 lines 51.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const common_1 = require("@fabrix/fabrix/dist/common"); const spool_sequelize_1 = require("@fabrix/spool-sequelize"); const errors_1 = require("@fabrix/spool-sequelize/dist/errors"); const lodash_1 = require("lodash"); const enums_1 = require("../../enums"); const enums_2 = require("../../enums"); const enums_3 = require("../../enums"); const queryDefaults_1 = require("../utils/queryDefaults"); class CartResolver extends spool_sequelize_1.SequelizeResolver { findByIdDefault(id, options = {}) { if (typeof id !== 'number' && typeof id !== 'string') { throw new Error('Id was not a string or a number'); } options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Cart.default(this.app), options); return this.findById(id, options); } findOneDefault(options = {}) { options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Cart.default(this.app), options); return this.findOne(options); } findByTokenDefault(token, options = {}) { if (typeof token !== 'string') { throw new Error('Token is not a string'); } options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Cart.default(this.app), options, { where: { token: token } }); return this.findOne(options); } resolveByInstance(cart, options = {}) { return Promise.resolve(cart); } resolveById(cart, options = {}) { if (!(cart instanceof Object)) { return Promise.reject(new Error('resolveById requires an object with an id property')); } return this.findByIdDefault(cart.id, options) .then(resCart => { if (!resCart && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Cart ${cart.id} not found`); } return resCart; }); } resolveByToken(cart, options = {}) { if (!(cart instanceof Object)) { return Promise.reject(new Error('resolveByToken requires an object with a token property')); } if (!(cart.token)) { return Promise.reject(new Error('resolveByToken requires an object with a token property')); } return this.findByTokenDefault(cart.token, options) .then(resCart => { if (!resCart && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Cart token ${cart.token} not found`); } return resCart; }); } resolveByNumber(cart, options = {}) { return this.findByIdDefault(cart, options) .then(resCart => { if (!resCart && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Cart ${cart.token} not found`); } return resCart; }); } resolveByString(cart, options = {}) { return this.findByTokenDefault(cart, options) .then(resCart => { if (!resCart && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `Cart ${cart} not found`); } return resCart; }); } resolve(cart, options = {}) { const resolvers = { 'instance': cart instanceof this.instance, 'id': !!(cart && lodash_1.isObject(cart) && cart.id), 'token': !!(cart && lodash_1.isObject(cart) && cart.token), 'create': !!(cart && lodash_1.isObject(cart) && options.create !== false), 'number': !!(cart && lodash_1.isNumber(cart)), 'string': !!(cart && lodash_1.isString(cart)) }; const type = Object.keys(resolvers).find((key) => resolvers[key]); switch (type) { case 'instance': { return this.resolveByInstance(cart, options); } case 'id': { return this.resolveById(cart, options); } case 'token': { return this.resolveByToken(cart, options); } case 'create': { return this.create(cart, options); } case 'number': { return this.resolveByNumber(cart, options); } case 'string': { return this.resolveByString(cart, options); } default: { const err = new Error(`Unable to resolve Cart ${cart}`); return Promise.reject(err); } } } } exports.CartResolver = CartResolver; class Cart extends common_1.FabrixModel { static get resolver() { return CartResolver; } static config(app, Sequelize) { return { options: { enums: { CART_STATUS: enums_1.CART_STATUS }, underscored: true, scopes: { live: { where: { live_mode: true } } }, indexes: [ { fields: ['line_items'], using: 'gin', operator: 'jsonb_path_ops' }, { fields: ['client_details'], using: 'gin', operator: 'jsonb_path_ops' } ], hooks: { beforeCreate: [ (cart, options) => { return app.services.CartService.beforeCreate(cart, options) .catch(err => { return Promise.reject(err); }); } ], beforeUpdate: [ (cart, options) => { return app.services.CartService.beforeUpdate(cart, options) .catch(err => { return Promise.reject(err); }); } ], beforeSave: [ (cart, options) => { return app.services.CartService.beforeSave(cart, options) .catch(err => { return Promise.reject(err); }); } ] } } }; } static schema(app, Sequelize) { return { customer_id: { type: Sequelize.INTEGER, }, shop_id: { type: Sequelize.INTEGER, }, order_id: { type: Sequelize.INTEGER, }, billing_address_id: { type: Sequelize.INTEGER, }, shipping_address_id: { type: Sequelize.INTEGER, }, token: { type: Sequelize.STRING, unique: true }, status: { type: Sequelize.ENUM, values: lodash_1.values(enums_1.CART_STATUS), defaultValue: enums_1.CART_STATUS.OPEN }, 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_subscription: { type: Sequelize.BOOLEAN, defaultValue: false }, 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 }, pricing_overrides: { type: Sequelize.JSONB, defaultValue: [] }, pricing_override_id: { type: Sequelize.INTEGER }, total_overrides: { type: Sequelize.INTEGER, defaultValue: 0 }, total_line_items_price: { 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 }, ip: { type: Sequelize.STRING }, create_ip: { type: Sequelize.STRING }, update_ip: { type: Sequelize.STRING }, client_details: { type: Sequelize.JSONB, defaultValue: { 'accept_language': null, 'browser_height': null, 'browser_ip': '0.0.0.0', 'browser_width': null, 'session_hash': null, 'user_agent': null } }, reservation_time: { type: Sequelize.INTEGER, defaultValue: 0 }, reservation_time_left: { type: Sequelize.INTEGER, defaultValue: 0 }, notes: { type: Sequelize.TEXT }, live_mode: { type: Sequelize.BOOLEAN, defaultValue: app.config.get('cart.live_mode') } }; } static associate(models) { models.Cart.belongsTo(models.Customer, { foreignKey: 'customer_id' }); models.Cart.belongsTo(models.Shop, { foreignKey: 'shop_id' }); models.Cart.belongsTo(models.Order, { foreignKey: 'order_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', 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 }); } } exports.Cart = Cart; Cart.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_subscription = false; this.has_shipping = false; this.has_taxes = false; this.discounted_lines = []; this.coupon_lines = []; this.tax_lines = this.tax_lines.filter(line => Object.keys(line).indexOf('line') === -1); this.shipping_lines = this.shipping_lines.filter(line => Object.keys(line).indexOf('line') === -1); 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; }; Cart.prototype.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 => { if (item.requires_shipping) { this.total_weight = this.total_weight + item.grams; } 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(); }; Cart.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; }; Cart.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); }; Cart.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(); }; Cart.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(); }; Cart.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(); }; Cart.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); }; Cart.prototype.setShippingLines = function (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(); }; Cart.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(ii => ii.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); }; Cart.prototype.setTaxLines = function (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(); }; Cart.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; }; Cart.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; }; Cart.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 = { 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, 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: this.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; }; Cart.prototype.addLine = function (item, qty, properties, shop, options = {}) { let lineQtyAvailable = -1; let line; return item.checkAvailability(qty, { shop: options.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('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; } 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 = calculatedQty; item.fulfillable_quantity = calculatedQty; item.max_quantity = maxQuantity; item.properties = properties; line = this.line(item); line = this.setLineProperties(line); lineItems.push(line); this.line_items = lineItems; } return this; }); }; Cart.prototype.removeLine = function (item, qty, options = {}) { 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); } }; Cart.prototype.addShipping = function (shipping = [], options = {}) { const shippingLines = this.shipping_lines; if (lodash_1.isArray(shipping)) { shipping.forEach(ship => { const i = lodash_1.findIndex(shippingLines, (s) => { return s.name === ship.name; }); ship.price = this.app.services.ProxyCartService.normalizeCurrency(parseInt(ship.price, 10)); if (i > -1) { shippingLines[i] = ship; } else { shippingLines.push(ship); } }); } else if (lodash_1.isObject(shipping)) { const i = lodash_1.findIndex(shippingLines, (s) => { return s.name === shipping.name; }); shipping.price = this.app.services.ProxyCartService.normalizeCurrency(parseInt(shipping.price, 10)); if (i > -1) { shippingLines[i] = shipping; } else { shippingLines.push(shipping); } } this.shipping_lines = shippingLines; return this.save({ transaction: options.transaction || null }); }; Cart.prototype.removeShipping = function (shipping = [], options = {}) { const shippingLines = this.shipping_lines; if (lodash_1.isArray(shipping)) { shipping.forEach(ship => { const i = lodash_1.findIndex(shippingLines, (s) => { return s.name === ship.name; }); if (i > -1) { shippingLines.splice(i, 1); } }); } else if (lodash_1.isObject(shipping)) { const i = lodash_1.findIndex(shippingLines, (s) => { return s.name === shipping.name; }); if (i > -1) { shippingLines.splice(i, 1); } } this.shipping_lines = shippingLines; return this.save({ transaction: options.transaction || null }); }; Cart.prototype.addTaxes = function (taxes = [], options = {}) { const taxLines = this.tax_lines; if (lodash_1.isArray(taxes)) { taxes.forEach(tax => { const i = lodash_1.findIndex(taxLines, (s) => { return s.name === tax.name; }); tax.price = this.app.services.ProxyCartService.normalizeCurrency(parseInt(tax.price, 10)); if (i > -1) { taxLines[i] = tax; } else { taxLines.push(tax); } }); } else if (lodash_1.isObject(taxes)) { const i = lodash_1.findIndex(taxLines, (s) => { return s.name === taxes.name; }); taxes.price = this.app.services.ProxyCartService.normalizeCurrency(parseInt(taxes.price, 10)); if (i > -1) { taxLines[i] = taxes; } else { taxLines.push(taxes); } } this.tax_lines = taxLines; return this.save({ transaction: options.transaction || null }); }; Cart.prototype.removeTaxes = function (taxes = [], options = {}) { const taxLines = this.tax_lines; if (lodash_1.isArray(taxes)) { taxes.forEach(tax => { const i = lodash_1.findIndex(taxLines, (s) => { return s.name === tax.name; }); if (i > -1) { taxLines.splice(i, 1); } }); } else if (lodash_1.isObject(taxes)) { const i = lodash_1.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 }); }; Cart.prototype.close = function (status, save) { this.status = status || enums_1.CART_STATUS.CLOSED; if (save) { return this.save(save); } return this; }; Cart.prototype.draft = function (status, save) { this.status = status || enums_1.CART_STATUS.DRAFT; if (save) { return this.save(save); } return this; }; Cart.prototype.clear = function () { this.line_items = []; return this; }; Cart.prototype.ordered = function (order, save) { this.order_id = order.id; this.status = enums_1.CART_STATUS.ORDERED; if (save) { return this.save(save); } return this; }; Cart.prototype.buildOrder = function (options = {}) { return { client_details: options.client_details || this.client_details || {}, ip: options.ip || null, payment_details: options.payment_details, payment_kind: options.payment_kind || this.app.config.get('cart.orders.payment_kind'), transaction_kind: options.transaction_kind || this.app.config.get('cart.orders.transaction_kind'), fulfillment_kind: options.fulfillment_kind || this.app.config.get('cart.orders.fulfillment_kind'), processing_method: options.processing_method || enums_3.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_id: options.customer_id || this.customer_id || null, user_id: options.user_id || this.user_id || null, 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_override_id: this.pricing_override_id, pricing_overrides: this.pricing_overrides, total_overrides: this.total_overrides }; }; Cart.prototype.calculatePricingOverrides = function (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 this.app.models['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) { const removeTotal = lodash_1.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 (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 => { this.app.log.error(err); return this; }); }, Cart.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; }); if (discounts.length > 0) { return this.app.models['Discount'].findAll({ where: { id: discounts.map(item => item.discount_id), status: enums_2.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; }); }; Cart.prototype.calculateShipping = function (options = {}) { if (!this.has_shipping) { return Promise.resolve(this); } return this.app.services.ShippingService.calculate(this, this.line_items, this.shipping_address, this.app.models['Cart'], options) .then(shippingResult => { this.setItemsShippingLines(shippingResult.line_items); return this; }) .catch(err => { this.app.log.error(err); return this; }); }; Cart.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['Cart'], options) .then(taxesResult => { this.setItemsTaxLines(taxesResult.line_items); return this; }) .catch(err => { this.app.log.error(err); return this; }); }; Cart.prototype.recalculate = function (options = {}) { this.resetDefaults(); this.setLineItems(this.line_items); return Promise.resolve() .then(() => { return this.calculateDiscounts({ transaction: options.transaction || null }); }) .then(() => { return this.calculateShipping({ transaction: options.transaction || null }); }) .then(() => { return this.calculateTaxes({ transaction: options.transaction || null }); }) .then(() => { return this.calculatePricingOverrides({ transaction: options.transaction || null }); }) .then(() => { return this.setTotals(); }) .catch(err => { this.app.log.error(err); return this; }); }; Cart.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 => { if (_customer) { _customer = _customer || null; this.Customer = _customer; this.setDataValue('Customer', _customer); this.set('Customer', _customer); } return this; }); } }; Cart.prototype.resolveCustomerAndItemCollections = function (options = {}) { this.line_items.forEach(item => { }); }; Cart.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; }); } }; Cart.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 []; }); }; Cart.prototype.resolveShippingAddress = function (options = {}) { if (this.shipping_address && this.shipping_address instanceof this.app.models['Address'].instance && options.reload !== true) { return Promise.resolve(this); } else if (!this.shipping_address_id) { this.shipping_address = this.app.models['Address'].build({}); return Promise.resolve(this); } else { return this.getShipping_address({ transaction: options.transaction || null }) .then(address => { address = address || null; this.shipping_address = address; this.setDataValue('shipping_address', address); this.set('shipping_address', address); return this; }); } }; Cart.prototype.resolveBillingAddress = function (options = {}) { if (this.billing_address && this.billing_address instanceof this.app.models['Address'].instance && options.reload !== true) { return Promise.resolve(this); } else if (!this.billing_address_id) { this.billing_address = this.app.models['Address'].build({}); return Promise.resolve(this); } else { return this.getBilling_address({ transaction: options.transaction || null }) .then(address => { address = address || null; this.billing_address = address; this.setDataValue('billing_address', address); this.set('billing_address', address); return this; }); } }; Cart.prototype.updateShippingAddress = function (address, options = {}) { const Address = this.app.models['Address']; const shippingUpdate = Address.cleanAddress(address); return this.resolveShippingAddress({ transaction: options.transaction || null }) .then(() => { if (address.id || address.token) { return Address.resolve(address, { transaction: options.transaction || null }) .then(_address => { return _address.update(shippingUpdate, { transaction: options.transaction || null }); }); } else { return this.shipping_address .merge(shipping