UNPKG

trailpack-proxy-cart

Version:

eCommerce - Trailpack for Proxy Engine

1,345 lines (1,296 loc) 97.6 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 shortId = require('shortid') const queryDefaults = require('../utils/queryDefaults') const ORDER_STATUS = require('../../lib').Enums.ORDER_STATUS const ORDER_CANCEL = require('../../lib').Enums.ORDER_CANCEL const ORDER_FINANCIAL = require('../../lib').Enums.ORDER_FINANCIAL const PAYMENT_KIND = require('../../lib').Enums.PAYMENT_KIND const TRANSACTION_STATUS = require('../../lib').Enums.TRANSACTION_STATUS const TRANSACTION_KIND = require('../../lib').Enums.TRANSACTION_KIND const ORDER_FULFILLMENT = require('../../lib').Enums.ORDER_FULFILLMENT const ORDER_FULFILLMENT_KIND = require('../../lib').Enums.ORDER_FULFILLMENT_KIND const FULFILLMENT_STATUS = require('../../lib').Enums.FULFILLMENT_STATUS const PAYMENT_PROCESSING_METHOD = require('../../lib').Enums.PAYMENT_PROCESSING_METHOD /** * @module Order * @description Order Model */ module.exports = class Order extends Model { static config (app, Sequelize) { return { options: { autoSave: true, underscored: true, enums: { ORDER_STATUS: ORDER_STATUS, ORDER_CANCEL: ORDER_CANCEL, ORDER_FINANCIAL: ORDER_FINANCIAL, ORDER_FULFILLMENT: ORDER_FULFILLMENT, ORDER_FULFILLMENT_KIND: ORDER_FULFILLMENT_KIND, PAYMENT_KIND: PAYMENT_KIND, PAYMENT_PROCESSING_METHOD: PAYMENT_PROCESSING_METHOD, TRANSACTION_STATUS: TRANSACTION_STATUS, TRANSACTION_KIND: TRANSACTION_KIND, FULFILLMENT_STATUS: FULFILLMENT_STATUS, }, // defaultScope: { // where: { // live_mode: app.config.proxyEngine.live_mode // } // }, scopes: { live: { where: { live_mode: true } }, open: { where: { status: ORDER_STATUS.OPEN } }, closed: { where: { status: ORDER_STATUS.CLOSED } }, cancelled: { where: { status: ORDER_STATUS.CANCELLED } } }, indexes: [ // Creates a gin index on data with the jsonb_path_ops operator { fields: ['client_details'], using: 'gin', operator: 'jsonb_path_ops' } ], hooks: { /** * * @param values * @param options */ beforeCreate: (values, options) => { if (values.ip) { values.create_ip = values.ip } if (!values.token) { values.token = `order_${shortId.generate()}` } }, /** * * @param values * @param options */ afterCreate: (values, options) => { return app.services.OrderService.afterCreate(values, options) .catch(err => { return Promise.reject(err) }) }, /** * * @param values * @param options */ beforeUpdate: (values, options) => { if (values.ip) { values.update_ip = values.ip } // values.setStatus() if (values.changed('status') && values.status == ORDER_STATUS.CLOSED) { values.close() } }, /** * * @param values * @param options */ afterUpdate: (values, options) => { return app.services.OrderService.afterUpdate(values, options) .catch(err => { return Promise.reject(err) }) } }, classMethods: { /** * Associate the Model * @param models */ associate: (models) => { // The individual items of this order models.Order.hasMany(models.OrderItem, { as: 'order_items', foreignKey: 'order_id' }) // The fulfillments for this order models.Order.hasMany(models.Fulfillment, { as: 'fulfillments', foreignKey: 'order_id' }) // The transactions for this order models.Order.hasMany(models.Transaction, { as: 'transactions', foreignKey: 'order_id' }) // The list of refunds applied to the order. models.Order.hasMany(models.Refund, { as: 'refunds', foreignKey: 'order_id' }) // Applicable discount codes that can be applied to the order. If no codes exist the value will default to blank. models.Order.belongsToMany(models.Discount, { as: 'discounts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'order' } }, foreignKey: 'model_id', constraints: false }) // The tags added to this order models.Order.belongsToMany(models.Tag, { as: 'tags', through: { model: models.ItemTag, unique: false, scope: { model: 'order' } }, foreignKey: 'model_id', constraints: false }) // The payment source used to pay this order models.Order.belongsToMany(models.Source, { as: 'sources', through: { model: models.OrderSource, unique: false }, foreignKey: 'order_id', constraints: false }) // The events tied to this order models.Order.hasMany(models.Event, { as: 'events', foreignKey: 'object_id', scope: { object: 'order' }, constraints: false }) models.Order.hasOne(models.Cart, { foreignKey: 'order_id' }) // models.Order.belongsTo(models.Cart, { // targetKey: 'token', // foreignKey: 'cart_token' // }) models.Order.belongsTo(models.Customer, { foreignKey: 'customer_id' }) // models.Order.hasOne(models.Customer, { // as: 'last_order', // foreignKey: 'last_order_id' // }) models.Order.belongsToMany(models.Event, { as: 'event_items', through: { model: models.EventItem, unique: false, scope: { object: 'order' } }, foreignKey: 'object_id', constraints: false }) models.Order.hasMany(models.DiscountEvent, { as: 'discount_events', foreignKey: 'order_id' }) models.Order.hasMany(models.AccountEvent, { as: 'account_events', foreignKey: 'order_id' }) models.Order.hasOne(models.Metadata, { as: 'metadata', foreignKey: 'order_id' }) }, /** * * @param id * @param options */ findByIdDefault: function(id, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Order.default(app), options || {} ) return this.findById(id, options) }, /** * * @param token * @param options * @returns {*|Promise.<Model>} */ findByTokenDefault: function(token, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Order.default(app), options || {}, { where: { token: token } } ) return this.findOne(options) }, /** * * @param options * @returns {Promise.<{count: Integer, rows: Model[]}>} */ findAndCountDefault: function(options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Order.default(app), options || {} ) return this.findAndCount(options) }, /** * * @param order * @param options * @returns {*} */ resolve: function(order, options){ options = options || {} const Order = this if (order instanceof Order){ return Promise.resolve(order) } else if (order && _.isObject(order) && order.id) { return Order.findByIdDefault(order.id, options) .then(resOrder => { if (!resOrder) { throw new Errors.FoundError(Error(`Order ${order.id} not found`)) } return resOrder }) } else if (order && _.isNumber(order)) { return Order.findByIdDefault(order, options) .then(resOrder => { if (!resOrder) { throw new Errors.FoundError(Error(`Order ${order} not found`)) } return resOrder }) } else if (order && _.isString(order)) { return Order.findByTokenDefault(order, options) .then(resOrder => { if (!resOrder) { throw new Errors.FoundError(Error(`Order ${order} not found`)) } return resOrder }) } else { // TODO create proper error const err = new Error('Unable to resolve Order') return Promise.reject(err) } } }, instanceMethods: { toJSON: function() { // Make JSON const resp = this instanceof app.orm['Order'] ? this.get({ plain: true }) : this // Transform Tags to array on toJSON if (resp.tags) { // console.log(resp.tags) resp.tags = resp.tags.map(tag => { if (tag && _.isString(tag)) { return tag } else if (tag && tag.name && tag.name !== '') { return tag.name } }) } return resp }, /** * * @param data */ cancel: function(data) { data = data || {} this.cancelled_at = new Date(Date.now()) this.status = ORDER_STATUS.CANCELLED this.closed_at = this.cancelled_at this.cancel_reason = data.cancel_reason || ORDER_CANCEL.OTHER return this }, /** * closes the order */ close: function() { this.status = ORDER_STATUS.CLOSED this.closed_at = new Date(Date.now()) return this }, /** * * @param options * @returns {*} */ logDiscountUsage: function(options) { return app.orm['Order'].sequelize.Promise.mapSeries(this.discounted_lines, line => { return app.orm['Discount'].findById(line.id, { attributes: ['id','times_used','usage_limit'], transaction: options.transaction || null }) .then(_discount => { return _discount.logUsage( this.id, this.customer_id, line.price, {transaction: options.transaction || null} ) }) }) }, /** * * @param preNotification * @param options */ notifyCustomer: function(preNotification, options) { options = 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 app.orm['Customer']) { return this.Customer.notifyUsers(preNotification, {transaction: options.transaction || null}) } else { return } }) .then(() => { return this }) } else { return Promise.resolve(this) } }, /** * * @param shipping * @param options * @returns {Promise.<T>} */ addShipping: function(shipping, options) { shipping = shipping || [] options = options || {} return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { 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 return this.save({transaction: options.transaction || null}) }) .then(() => { return this.recalculate({transaction: options.transaction || null}) }) }, /** * * @param shipping * @param options * @returns {Promise.<T>} */ removeShipping: function(shipping, options){ shipping = shipping || [] options = options || {} return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { 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 return this.save({transaction: options.transaction || null}) }) .then(() => { return this.recalculate({transaction: options.transaction || null}) }) }, /** * * @param taxes * @param options * @returns {Promise.<T>} */ addTaxes: function(taxes, options) { taxes = taxes || [] options = options || {} return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { 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}) }) .then(() => { return this.recalculate({transaction: options.transaction || null}) }) }, /** * * @param taxes * @param options * @returns {Promise.<T>} */ removeTaxes: function(taxes, options){ taxes = taxes || [] options = options || {} return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { 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}) }) .then(() => { return this.recalculate({transaction: options.transaction || null}) }) }, saveShippingAddress: function(address, options) { options = options || {} this.shipping_address = _.extend(this.shipping_address, address) this.shipping_address = app.services.ProxyCartService.validateAddress(this.shipping_address) return app.services.GeolocationGenericService.locate(this.shipping_address) .then(latLng => { this.shipping_address = _.defaults(this.shipping_address, latLng) return this.recalculate({transaction: options.transaction || null}) }) .catch(err => { return }) }, saveBillingAddress: function(address, options) { options = options || {} this.billing_address = _.extend(this.billing_address, address) this.billing_address = app.services.ProxyCartService.validateAddress(this.billing_address) return app.services.GeolocationGenericService.locate(this.billing_address) .then(latLng => { this.billing_address = _.defaults(this.billing_address, latLng) return this.recalculate({transaction: options.transaction || null}) }) .catch(err => { return }) }, /** * * @param options * @returns {Promise.<T>} */ groupFulfillments: function(options) { options = options || {} return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { return this.resolveFulfillments({ transaction: options.transaction || null, reload: options.reload || null }) }) .then(() => { // Group by Service let groups = _.groupBy(this.order_items, 'fulfillment_service') // Map into array groups = _.map(groups, (items, service) => { return { service: service, items: items } }) // Create the non sent fulfillments return app.orm['Order'].sequelize.Promise.mapSeries(groups, (group) => { const resFulfillment = this.fulfillments.find(fulfillment => fulfillment.service == group.service) return resFulfillment.addOrder_items(group.items, { hooks: false, individualHooks: false, returning: false, transaction: options.transaction || null }) .then(() => { return resFulfillment.reload({ transaction: options.transaction || null }) }) // .then(() => { // return resFulfillment.saveFulfillmentStatus() // }) }) }) .then((fulfillments) => { fulfillments = fulfillments || [] this.fulfillments = fulfillments this.setDataValue('fulfillments', fulfillments) this.set('fulfillments', fulfillments) return this }) }, /** * * @param paymentDetails * @param options * @returns {*|Promise.<T>} */ groupTransactions: function(paymentDetails, options) { options = options || {} return app.orm['Order'].sequelize.Promise.mapSeries(paymentDetails, (detail, index) => { const transaction = app.orm['Transaction'].build({ // Set the customer id (in case we can save this source) customer_id: this.customer_id, // Set the order id order_id: this.id, // Set the source if it is given source_id: detail.source ? detail.source.id : null, // Set the order currency currency: this.currency, // Set the amount for this transaction and handle if it is a split transaction amount: detail.amount || this.total_due, // Copy the entire payment details to this transaction payment_details: paymentDetails[index], // Specify the gateway to use gateway: detail.gateway, // Set the specific type of transactions this is kind: this.transaction_kind, // Set the device (that input the credit card) or null device_id: this.device_id || null, // Set the Description description: `Order ${this.name} original transaction ${this.transaction_kind}` }) // Return the Payment Service if (this.payment_kind === PAYMENT_KIND.MANUAL) { return app.services.PaymentService.manual(transaction, { transaction: options.transaction || null }) } else { return app.services.PaymentService[this.transaction_kind](transaction, { transaction: options.transaction || null }) } }) .then(transactions => { transactions = transactions || [] this.transactions = transactions this.setDataValue('transactions', transactions) this.set('transactions', transactions) return this }) }, groupSubscriptions: function(active, options) { options = options || {} return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { const orderItems = _.filter(this.order_items, 'requires_subscription') const groups = [] const units = _.groupBy(orderItems, 'subscription_unit') _.forEach(units, function(value, unit) { const intervals = _.groupBy(units[unit], 'subscription_interval') _.forEach(intervals, (items, interval) => { groups.push({ unit: unit, interval: interval, items: items }) }) }) return app.orm['Order'].sequelize.Promise.mapSeries(groups, group => { return app.services.SubscriptionService.create( this, group.items, group.unit, group.interval, active, { transaction: options.transaction || null} ) }) }) .then(subscriptions => { subscriptions = subscriptions || [] this.subscriptions = subscriptions this.set('subscriptions', subscriptions) this.setDataValue('subscriptions', subscriptions) return this }) }, /** * * @param fulfillments * @param options * @returns {Promise.<T>} */ fulfill: function(fulfillments, options){ fulfillments = fulfillments || [] options = options || {} let toFulfill = [] return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { return this.resolveFulfillments({ transaction: options.transaction || null, reload: options.reload || null }) }) .then(() => { toFulfill = fulfillments.map(fulfillment => this.fulfillments.find(f => f.id === fulfillment.id)) // Remove empties toFulfill = toFulfill.filter(f => f) // console.log('BROKE FULFILL', toFulfill) return this.sequelize.Promise.mapSeries(toFulfill, resFulfillment => { if (!(resFulfillment instanceof app.orm['Fulfillment'])) { throw new Error('resFulfillment is not an instance of Fulfillment') } const fulfillment = fulfillments.find(f => f.id === resFulfillment.id) const update = { status: fulfillment.status || resFulfillment.status, status_url: fulfillment.status_url || resFulfillment.status_url, tracking_company: fulfillment.tracking_company || resFulfillment.tracking_company, tracking_number: fulfillment.tracking_number || resFulfillment.tracking_number, receipt: fulfillment.receipt || resFulfillment.receipt } // console.log('UPDATE', update) return resFulfillment.fulfillUpdate(update, { transaction: options.transaction || null }) }) }) .then(() => { return this.saveFulfillmentStatus({transaction: options.transaction || null}) }) }, /** * * @param options */ resolveFinancialStatus: function(options){ options = options || {} if (!this.id) { return Promise.resolve(this) } return this.resolveTransactions({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { // Set the new financial status this.setFinancialStatus() return this }) }, /** * * @param options */ resolveFulfillmentStatus: function (options) { options = options || {} if (!this.id) { return Promise.resolve(this) } // Set fulfillment status requires fulfillments be resolved. return this.resolveFulfillments({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { // Set fulfillment status requires that order items also be resolved return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) }) .then(() => { // Set the new fulfillment status this.setFulfillmentStatus() return this }) }, /** * */ setStatus: function () { if ( this.financial_status === ORDER_FINANCIAL.PAID && this.fulfillment_status === ORDER_FULFILLMENT.FULFILLED && this.status === ORDER_STATUS.OPEN ) { this.close() } else if ( this.financial_status === ORDER_FINANCIAL.CANCELLED && this.fulfillment_status === ORDER_FULFILLMENT.CANCELLED && this.status === ORDER_STATUS.OPEN ) { this.cancel() } return this }, /** * * @param options * @returns {Promise.<TResult>} */ resolveStatus: function(options) { options = options || {} return this.resolveFinancialStatus({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { return this.resolveFulfillmentStatus({ transaction: options.transaction || null, reload: options.reload || null }) }) .then(() => { return this.setStatus() }) }, /** * * @param options * @returns {*} */ saveStatus: function (options) { options = options || {} if (!this.id) { return Promise.resolve(this) } return this.resolveStatus({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { return this.save({ fields: [ 'status', 'closed_at', 'cancelled_at', 'total_fulfilled_fulfillments', 'total_sent_fulfillments', 'total_cancelled_fulfillments', 'total_partial_fulillments', 'total_pending_fulfillments', 'fulfillment_status', 'financial_status', 'total_authorized', 'total_captured', 'total_refunds', 'total_voided', 'total_cancelled', 'total_pending', 'total_due' ], transaction: options.transaction || null }) }) }, /** * * @param options * @returns {*} */ saveFinancialStatus: function(options) { options = options || {} let currentStatus, previousStatus // If not a persisted instance if (!this.id) { return Promise.resolve(this) } return this.resolveFinancialStatus({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { if (this.changed('financial_status')) { currentStatus = this.financial_status previousStatus = this.previous('financial_status') } return this.save({ fields: [ 'financial_status', 'total_authorized', 'total_captured', 'total_refunds', 'total_voided', 'total_cancelled', 'total_pending', 'total_due' ], transaction: options.transaction || null }) }) .then(() => { if (currentStatus && previousStatus) { const event = { object_id: this.id, object: 'order', objects: [{ customer: this.customer_id },{ order: this.id }], type: `order.financial_status.${currentStatus}`, message: `Order ${ this.name || 'ID ' + this.id } financial status changed from "${previousStatus}" to "${currentStatus}"`, data: this } return app.services.ProxyEngineService.publish(event.type, event, { save: true, transaction: options.transaction || null }) } else { return } }) .then(() => { if (currentStatus === ORDER_FINANCIAL.PAID && previousStatus !== ORDER_FINANCIAL.PAID) { return this.attemptImmediate(options) } else { return this } }) .then(() => { return this }) }, saveFulfillmentStatus: function(options) { options = options || {} let currentStatus, previousStatus // If not a persisted instance return right away if (!this.id) { return Promise.resolve(this) } return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { return this.resolveFulfillments({ transaction: options.transaction || null, reload: options.reload || null }) }) .then(() => { this.setFulfillmentStatus() if (this.changed('fulfillment_status')) { currentStatus = this.fulfillment_status previousStatus = this.previous('fulfillment_status') } return this.save({ fields: [ 'total_fulfilled_fulfillments', 'total_sent_fulfillments', 'total_cancelled_fulfillments', 'total_partial_fulillments', 'total_pending_fulfillments', 'fulfillment_status' ], transaction: options.transaction || null }) }) .then(() => { if (currentStatus && previousStatus) { const event = { object_id: this.id, object: 'order', objects: [{ customer: this.customer_id },{ order: this.id }], type: `order.fulfillment_status.${currentStatus}`, message: `Order ${ this.name || 'ID ' + this.id } fulfillment status changed from "${previousStatus}" to "${currentStatus}"`, data: this } return app.services.ProxyEngineService.publish(event.type, event, { save: true, transaction: options.transaction || null }) } else { return } }) .then(() => { return this }) }, setFinancialStatus: function(){ if (!this.transactions) { throw new Error('Order.setFinancialStatus requires transactions to be populated') // return Promise.reject(err) } const pending = this.transactions.filter(transaction => [ TRANSACTION_STATUS.PENDING, TRANSACTION_STATUS.FAILURE, TRANSACTION_STATUS.ERROR ].indexOf(transaction.status ) > -1) const cancelled = this.transactions.filter(transaction => [ TRANSACTION_STATUS.CANCELLED ].indexOf(transaction.status ) > -1) const successes = this.transactions.filter(transaction => [ TRANSACTION_STATUS.SUCCESS ].indexOf(transaction.status ) > -1) let financialStatus = ORDER_FINANCIAL.PENDING let totalAuthorized = 0 let totalVoided = 0 let totalSale = 0 let totalRefund = 0 let totalCancelled = 0 let totalPending = 0 // Calculate the totals of the successful transactions _.each(successes, transaction => { if (transaction.kind === TRANSACTION_KIND.AUTHORIZE) { totalAuthorized = totalAuthorized + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.VOID) { totalVoided = totalVoided + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.CAPTURE) { totalSale = totalSale + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.SALE) { totalSale = totalSale + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.REFUND) { totalRefund = totalRefund + transaction.amount } }) // Calculate the totals of pending transactions _.each(pending, transaction => { if (transaction.kind === TRANSACTION_KIND.AUTHORIZE) { totalPending = totalPending + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.CAPTURE) { totalPending = totalPending + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.SALE) { totalPending = totalPending + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.VOID) { totalPending = totalPending - transaction.amount } else if (transaction.kind === TRANSACTION_KIND.REFUND) { totalPending = totalPending - transaction.amount } }) // Calculate the totals of cancelled pending transactions _.each(cancelled, transaction => { if (transaction.kind === TRANSACTION_KIND.AUTHORIZE) { totalCancelled = totalCancelled + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.CAPTURE) { totalCancelled = totalCancelled + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.SALE) { totalCancelled = totalCancelled + transaction.amount } else if (transaction.kind === TRANSACTION_KIND.VOID) { totalCancelled = totalCancelled - transaction.amount } else if (transaction.kind === TRANSACTION_KIND.REFUND) { totalCancelled = totalCancelled - transaction.amount } }) // If this a draft style order with 0 items in it if (this.total_items === 0) { financialStatus = ORDER_FINANCIAL.PENDING } // If this item is completely free else if (this.total_price === 0 && this.total_items > 0) { financialStatus = ORDER_FINANCIAL.PAID } // Total Authorized is the Price of the Order and there are no Capture/Sale transactions and 0 voided else if ( totalAuthorized === this.total_price && totalSale === 0 && totalVoided === 0 && totalRefund === 0 && this.total_items > 0 ) { financialStatus = ORDER_FINANCIAL.AUTHORIZED } // Total Authorized is the Price of the Order and there are no Capture/Sale transactions else if ( totalAuthorized === totalVoided && totalVoided > 0 && this.total_items > 0 ) { financialStatus = ORDER_FINANCIAL.VOIDED } else if ( this.total_price === totalVoided && totalVoided > 0 && this.total_items > 0 ) { financialStatus = ORDER_FINANCIAL.VOIDED } // Total Sale is the Price of the order and there are no refunds else if ( totalSale === this.total_price && totalRefund === 0 && this.total_items > 0 ) { financialStatus = ORDER_FINANCIAL.PAID } // Total Sale is not yet the Price of the order and there are no refunds else if ( totalSale < this.total_price && totalSale > 0 && totalRefund === 0 && this.total_items > 0 ) { financialStatus = ORDER_FINANCIAL.PARTIALLY_PAID } // Total Sale is the Total Price and Total Refund is Total Price else if ( this.total_price === totalRefund && this.total_items > 0 ) { financialStatus = ORDER_FINANCIAL.REFUNDED } // Total Sale is the Total Price but Total Refund is less than the Total Price else if ( totalRefund < this.total_price && totalRefund > 0 && this.total_items > 0 ) { financialStatus = ORDER_FINANCIAL.PARTIALLY_REFUNDED } else if ( this.total_price === totalCancelled && this.total_items > 0 ) { financialStatus = ORDER_FINANCIAL.CANCELLED } app.log.debug(`ORDER ${this.id}: FINANCIAL Status: ${financialStatus}, Sales: ${totalSale}, Authorized: ${totalAuthorized}, Refunded: ${totalRefund}, Pending: ${totalPending}, Cancelled: ${totalCancelled}`) // pending: The finances are pending. (This is the default value.) // cancelled: The finances pending have been cancelled. // authorized: The finances have been authorized. // partially_paid: The finances have been partially paid. // paid: The finances have been paid. // partially_refunded: The finances have been partially refunded. // refunded: The finances have been refunded. // voided: The finances have been voided. this.financial_status = financialStatus this.total_authorized = totalAuthorized this.total_captured = totalSale this.total_refunds = totalRefund this.total_voided = totalVoided this.total_cancelled = totalCancelled this.total_pending = totalPending this.total_due = this.total_price - totalSale return this }, /** * * @returns {config} */ setFulfillmentStatus: function(){ if (!this.fulfillments) { throw new Error('Order.setFulfillmentStatus requires fulfillments to be populated') // return Promise.reject(err) } if (!this.order_items) { throw new Error('Order.setFulfillmentStatus requires order_items to be populated') // return Promise.reject(err) } let fulfillmentStatus = ORDER_FULFILLMENT.PENDING let totalFulfillments = 0 let totalPartialFulfillments = 0 let totalSentFulfillments = 0 let totalNonFulfillments = 0 let totalPendingFulfillments = 0 let totalCancelledFulfillments = 0 this.fulfillments.forEach(fulfillment => { if (fulfillment.status === FULFILLMENT_STATUS.FULFILLED) { totalFulfillments++ } else if (fulfillment.status === FULFILLMENT_STATUS.PARTIAL) { totalPartialFulfillments++ } else if (fulfillment.status === FULFILLMENT_STATUS.SENT) { totalSentFulfillments++ } else if (fulfillment.status === FULFILLMENT_STATUS.NONE) { totalNonFulfillments++ } else if (fulfillment.status === FULFILLMENT_STATUS.PENDING) { totalPendingFulfillments++ } else if (fulfillment.status === FULFILLMENT_STATUS.CANCELLED) { totalCancelledFulfillments++ } }) if (totalFulfillments === this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = ORDER_FULFILLMENT.FULFILLED } else if (totalSentFulfillments === this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = ORDER_FULFILLMENT.SENT } else if (totalPartialFulfillments > 0) { fulfillmentStatus = ORDER_FULFILLMENT.PARTIAL } else if (totalNonFulfillments >= this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = ORDER_FULFILLMENT.NONE // back to default } else if (totalCancelledFulfillments === this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = ORDER_FULFILLMENT.CANCELLED // back to default } else if (totalPendingFulfillments === this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = ORDER_FULFILLMENT.PENDING // back to default } // IF done or cancelled if (fulfillmentStatus === ORDER_FULFILLMENT.FULFILLED || fulfillmentStatus === ORDER_FULFILLMENT.CANCELLED) { this.status = ORDER_STATUS.CLOSED } this.total_fulfilled_fulfillments = totalFulfillments this.total_partial_fulfillments = totalPartialFulfillments this.total_sent_fulfillments = totalSentFulfillments t