UNPKG

trailpack-proxy-cart

Version:

eCommerce - Trailpack for Proxy Engine

692 lines (656 loc) 23.3 kB
/* eslint new-cap: [0] */ /* eslint no-console: [0] */ 'use strict' const Model = require('trails/model') const Errors = require('proxy-engine-errors') const helpers = require('proxy-engine-helpers') const _ = require('lodash') const INTERVALS = require('../../lib').Enums.INTERVALS const FULFILLMENT_STATUS = require('../../lib').Enums.FULFILLMENT_STATUS const FULFILLMENT_SERVICE = require('../../lib').Enums.FULFILLMENT_SERVICE /** * @module OrderItem * @description Order Item Model */ module.exports = class OrderItem extends Model { static config (app, Sequelize) { return { options: { underscored: true, scopes: { live: { where: { live_mode: true } } }, hooks: { beforeCreate(values, options) { return app.services.OrderService.itemBeforeCreate(values, options) .catch(err => { return Promise.reject(err) }) }, beforeSave(values, options) { return app.services.OrderService.itemBeforeSave(values, options) .catch(err => { return Promise.reject(err) }) }, beforeUpdate(values, options) { return app.services.OrderService.itemBeforeUpdate(values, options) .catch(err => { return Promise.reject(err) }) }, afterCreate(values, options) { return app.services.OrderService.itemAfterCreate(values, options) .catch(err => { return Promise.reject(err) }) }, afterUpdate(values, options) { return app.services.OrderService.itemAfterUpdate(values, options) .catch(err => { return Promise.reject(err) }) }, afterDestroy(values, options) { return app.services.OrderService.itemAfterDestroy(values, options) .catch(err => { return Promise.reject(err) }) } }, classMethods: { INTERVALS: INTERVALS, FULFILLMENT_STATUS: FULFILLMENT_STATUS, FULFILLMENT_SERVICE: FULFILLMENT_SERVICE, /** * Associate the Model * @param models */ associate: (models) => { models.OrderItem.belongsTo(models.Order, { foreignKey: 'order_id' }) models.OrderItem.belongsTo(models.Customer, { foreignKey: 'customer_id' }) models.OrderItem.belongsTo(models.Fulfillment, { foreignKey: 'fulfillment_id' }) models.OrderItem.belongsTo(models.Product, { foreignKey: 'product_id' }) models.OrderItem.belongsTo(models.ProductVariant, { foreignKey: 'variant_id' }) models.OrderItem.belongsTo(models.Vendor, { foreignKey: 'vendor_id' }) models.OrderItem.belongsTo(models.Refund, { foreignKey: 'refund_id' }) models.OrderItem.belongsTo(models.GiftCard, { foreignKey: 'gift_card_id' }) models.OrderItem.belongsToMany(models.Discount, { as: 'discounts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'order_item' } }, foreignKey: 'model_id', constraints: false }) models.OrderItem.hasOne(models.Metadata, { as: 'metadata', foreignKey: 'order_item_id' }) }, resolve: function(item, options){ options = options || {} const OrderItem = this if (item instanceof OrderItem){ return Promise.resolve(item) } else if (item && _.isObject(item) && item.id) { return OrderItem.findById(item.id, options) .then(resOrderItem => { if (!resOrderItem) { throw new Errors.FoundError(Error(`Order ${item.id} not found`)) } return resOrderItem }) } else if (item && (_.isString(item) || _.isNumber(item))) { return OrderItem.findById(item, options) .then(resOrderItem => { if (!resOrderItem) { throw new Errors.FoundError(Error(`Order ${item} not found`)) } return resOrderItem }) } else { // TODO throw proper error const err = new Error('Unable to resolve Order Item') return Promise.reject(err) } } }, instanceMethods: { /** * Resets the defaults so they can be recalculated * @returns {*} */ resetDefaults: function() { this.calculated_price = 0 this.total_discounts = 0 this.total_shipping = 0 this.total_coupons = 0 this.total_taxes = 0 return this }, addShipping: function(shipping, options) { options = options || {} }, removeShipping: function(shipping, options){ options = options || {} }, setItemsShippingLines: function (shippingedLine) { // console.log('INCOMING ITEM', shippingedLine) // this.shipping_lines = [] let shippingesLines = [] let totalShippinges = 0 // Make this an array if null if (shippingedLine) { // console.log('FOUND SHIPPING LINE', shippingedLine.shipping_lines) shippingedLine.shipping_lines = shippingedLine.shipping_lines || [] shippingedLine.shipping_lines.map(line => { line.id = this.id return line }) totalShippinges = shippingedLine.shipping_lines.forEach(line => { totalShippinges = totalShippinges + line.price }) // console.log('SHIPPINGED LINE', shippingedLine) shippingesLines = [...shippingesLines, ...shippingedLine.shipping_lines] } this.shipping_lines = shippingesLines // console.log('FINAL SHIPPING LINES', this.shipping_lines) return this.setShippingLines(shippingesLines) }, /** * * @param lines */ 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 // return this.setTotals() }, setItemsTaxLines: function (taxedLine) { // console.log('INCOMING ITEM', taxedLine) // this.tax_lines = [] let taxesLines = [] let totalTaxes = 0 // Make this an array if null if (taxedLine) { // console.log('FOUND TAX LINE', taxedLine.tax_lines) taxedLine.tax_lines = taxedLine.tax_lines || [] taxedLine.tax_lines.map(line => { line.id = this.id return line }) totalTaxes = taxedLine.tax_lines.forEach(line => { totalTaxes = totalTaxes + line.price }) // console.log('TAXED LINE', taxedLine) taxesLines = [...taxesLines, ...taxedLine.tax_lines] } this.tax_lines = taxesLines // console.log('FINAL TAX LINES', this.tax_lines) return this.setTaxLines(taxesLines) }, /** * * @param lines */ 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 // return this.setTotals() }, setProperties: (prev) => { if (this.properties) { // Remove any old property pricing for (const l in prev.properties){ if (prev.properties.hasOwnProperty(l)) { this.price = this.price - prev.properties[l].price this.price_per_unit = this.price_per_unit - prev.properties[l].price } } // and then add the new properties in for (const l in this.properties){ if (this.properties.hasOwnProperty(l)) { this.price = this.price + this.properties[l].price this.price_per_unit = this.price_per_unit + this.properties[l].price } } } return this }, /** * */ setTotals: function() { // Set Cart values // this.total_price = Math.max(0, // this.total_tax // + this.total_shipping // + this.subtotal_price // ) return this }, /** * * @param options * @returns {Promise.<T>} */ recalculate: function(options) { options = options || {} if ( this.changed('price') || this.changed('quantity') || this.changed('properties') || this.changed('discounted_lines') || this.changed('coupon_lines') || this.changed('tax_lines') || this.changed('coupon_lines') ) { app.log.debug('ORDER ITEM CHANGED') let totalDiscounts = 0 // this.total_discounts let totalShipping = 0 let totalTaxes = 0 let totalCoupons = 0 if (this.changed('properties')) { this.setProperties(this.previous('properties')) } this.discounted_lines = this.discounted_lines || [] this.discounted_lines.map(line => { totalDiscounts = totalDiscounts + (line.price || 0) return line }) this.coupon_lines = this.coupon_lines || [] this.coupon_lines.map(line => { totalCoupons = totalCoupons + (line.price || 0) if (line.line) { line.line = this.id } return line }) this.shipping_lines = this.shipping_lines || [] this.shipping_lines.map(line => { totalShipping = totalShipping + (line.price || 0) if (line.line) { delete line.line line.id = this.id } return line }) this.tax_lines = this.tax_lines || [] this.tax_lines.map(line => { totalTaxes = totalTaxes + (line.price || 0) if (line.line) { delete line.line line.id = this.id } return line }) const calculatedPrice = Math.max(0, (this.price_per_unit * this.quantity) - totalDiscounts - totalCoupons) this.calculated_price = calculatedPrice this.total_discounts = totalDiscounts this.total_shipping = totalShipping this.total_coupons = totalCoupons this.total_taxes = totalTaxes return Promise.resolve(this) } else { return Promise.resolve(this) } }, /** * * @returns {Promise.<config>} */ reconcileFulfillment: function(options) { options = options || {} if (this.isNewRecord && !this.fulfillment_id) { // console.log('reconcileFulfillment: RECONCILE WILL CREATE OR ATTACH FULFILLMENT', this) return this.save({transaction: options.transaction || null}) .then(() => { return app.services.FulfillmentService.addOrCreateFulfillmentItem( this, { transaction: options.transaction || null } ) }) .then(() => { return this }) } else if (!this.isNewRecord && this.quantity === 0) { // console.log('reconcileFulfillment: RECONCILE WILL REMOVE', this) return this.save({transaction: options.transaction || null}) .then(() => { return app.services.FulfillmentService.removeFulfillmentItem( this, { transaction: options.transaction || null } ) }) .then(() => { return this }) } else if (!this.isNewRecord && this.changed('quantity') && (this.quantity > this.previous('quantity'))) { // console.log('reconcileFulfillment: RECONCILE WILL UPDATE UP QUANTITY', this) return this.save({transaction: options.transaction || null}) .then(() => { return app.services.FulfillmentService.updateFulfillmentItem( this, {transaction: options.transaction || null} ) }) .then(() => { return this }) } else if (!this.isNewRecord && this.changed('quantity') && (this.quantity < this.previous('quantity'))) { // console.log('reconcileFulfillment: RECONCILE WILL UPDATE DOWN QUANTITY', this) return this.save({transaction: options.transaction || null}) .then(() => { return app.services.FulfillmentService.removeFulfillmentItem( this, { transaction: options.transaction || null } ) }) .then(() => { return this }) } else { // console.log('reconcileFulfillment: UNHANDLED') // Unhandled Case return this.save({transaction: options.transaction || null}) } } } } } } static schema (app, Sequelize) { return { order_id: { type: Sequelize.INTEGER, // references: { // model: 'Order', // key: 'id' // }, allowNull: false }, customer_id: { type: Sequelize.INTEGER, // references: { // model: 'Customer', // key: 'id' // } // allowNull: false }, fulfillment_id: { type: Sequelize.INTEGER, // references: { // model: 'Fulfillment', // key: 'id' // } // allowNull: false }, product_id: { type: Sequelize.INTEGER, // references: { // model: 'Product', // key: 'id' // }, allowNull: false }, product_handle: { type: Sequelize.STRING, // references: { // model: 'Product', // key: 'handle' // }, allowNull: false }, variant_id: { type: Sequelize.INTEGER, // references: { // model: 'ProductVariant', // key: 'id' // }, allowNull: false }, subscription_id: { type: Sequelize.INTEGER, // references: { // model: 'Subscription', // key: 'id' // } }, refund_id: { type: Sequelize.INTEGER, // references: { // model: 'Refund', // key: 'id' // } }, gift_card_id: { type: Sequelize.INTEGER, // references: { // model: 'GiftCard', // key: 'id' // } }, // The option that this Variant is option: helpers.JSONB('OrderItem', app, Sequelize, 'option', { // name: string, value:string defaultValue: {} }), // The amount available to fulfill. This is the quantity - max(refunded_quantity, fulfilled_quantity) - pending_fulfilled_quantity - open_fulfilled_quantity. fulfillable_quantity: { type: Sequelize.INTEGER, defaultValue: 0 }, // The maximum allowed per order. max_quantity: { type: Sequelize.INTEGER, defaultValue: -1 }, // Service provider who is doing the fulfillment. Valid values are either "manual" or the name of the provider. eg: "amazon", "shipwire", etc. fulfillment_service: { type: Sequelize.STRING, defaultValue: FULFILLMENT_SERVICE.MANUAL // allowNull: false }, // How far along an order is in terms line items fulfilled. Valid values are: pending, none, sent, fulfilled, or partial. fulfillment_status: { type: Sequelize.ENUM, values: _.values(FULFILLMENT_STATUS), defaultValue: FULFILLMENT_STATUS.PENDING // allowNull: false }, // The weight of the item in grams. grams: { type: Sequelize.INTEGER, defaultValue: 0 }, // The MSRP compare_at_price: { type: Sequelize.INTEGER, defaultValue: 0 }, // The price of the item before discounts have been applied. price: { type: Sequelize.INTEGER, defaultValue: 0 }, // The price of the item after discounts have been applied. calculated_price: { type: Sequelize.INTEGER, defaultValue: 0 }, // The Unit Price price_per_unit: { type: Sequelize.INTEGER, defaultValue: 0 }, // The unique numeric identifier for the product in the fulfillment. Can be null if the original product associated with the order is deleted at a later date // The number of products that were purchased. quantity: { type: Sequelize.INTEGER }, // States whether or not the fulfillment requires shipping. Values are: true or false. requires_taxes: { type: Sequelize.BOOLEAN, defaultValue: true }, // States whether or not the fulfillment requires shipping. Values are: true or false. requires_shipping: { type: Sequelize.BOOLEAN, defaultValue: true }, // States whether or not the order item requires a subscription. Values are: true or false. requires_subscription: { type: Sequelize.BOOLEAN, defaultValue: true }, // If Product has subscription, the interval of the subscription, defaults to 0 months subscription_interval: { type: Sequelize.INTEGER, defaultValue: 0 }, // If product has subscription, the unit of the interval subscription_unit: { type: Sequelize.ENUM, values: _.values(INTERVALS), defaultValue: INTERVALS.NONE }, // A unique identifier of the item in the fulfillment. sku: { type: Sequelize.STRING }, // The type of Product type: { type: Sequelize.STRING }, // The title of the product. title: { type: Sequelize.STRING }, // The title of the product variant. variant_title: { type: Sequelize.STRING }, // The id of the supplier of the item. vendor_id: { type: Sequelize.INTEGER }, // The name of the product variant. name: { type: Sequelize.STRING }, // States whether or not the line_item is a gift card. If so, the item is not taxed or considered for shipping charges. gift_card: { type: Sequelize.BOOLEAN }, // An array of custom information for an item that has been added to the cart. Often used to provide product customization options. For more information, see the documentation on collecting customization information on the product page. properties: helpers.JSONB('OrderItem', app, Sequelize, 'properties', { defaultValue: {} }), property_pricing: helpers.JSONB('OrderItem', app, Sequelize, 'property_pricing', { defaultValue: {} }), // States whether or not the product was taxable. Values are: true or false. taxable: { type: Sequelize.BOOLEAN }, tax_code: { type: Sequelize.STRING, defaultValue: 'P000000' // Physical Good }, // The line_items that have discounts discounted_lines: helpers.JSONB('OrderItem', app, Sequelize, 'discounted_lines', { defaultValue: [] }), // The line_items that have discounts coupon_lines: helpers.JSONB('OrderItem', app, Sequelize, 'coupon_lines', { defaultValue: [] }), // The line_items that have shipping shipping_lines: helpers.JSONB('OrderItem', app, Sequelize, 'shipping_lines', { defaultValue: [] }), // A list of tax_line objects, each of which details the taxes applicable to this line_item. tax_lines: helpers.JSONB('OrderItem', app, Sequelize, 'tax_lines', { defaultValue: [] }), // The total discounts amount applied to this line item. This value is not subtracted in the line item price. total_discounts: { type: Sequelize.INTEGER, defaultValue: 0 }, // The total coupons amount applied to this line item. This value is not subtracted in the line item price. total_coupons: { type: Sequelize.INTEGER, defaultValue: 0 }, // The total shipping amount applied to this line item. This value is not added in the line item price. total_shipping: { type: Sequelize.INTEGER, defaultValue: 0 }, // The total taxes amount applied to this line item. This value is not added in the line item price. total_taxes: { type: Sequelize.INTEGER, defaultValue: 0 }, // The Average Shipping Cost average_shipping: { type: Sequelize.INTEGER, defaultValue: 0 }, // Payment types that can not be used to purchase this product exclude_payment_types: helpers.JSONB('OrderItem', app, Sequelize, 'exclude_payment_types', { defaultValue: [] }), // Product Images images: helpers.JSONB('OrderItem', app, Sequelize, 'images', { defaultValue: [] }), live_mode: { type: Sequelize.BOOLEAN, defaultValue: app.config.proxyEngine.live_mode } } } }