trailpack-proxy-cart
Version:
eCommerce - Trailpack for Proxy Engine
1,345 lines (1,296 loc) • 97.6 kB
JavaScript
/* 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