UNPKG

@fabrix/spool-cart

Version:

Spool - eCommerce Spool for Fabrix

1,368 lines 74.4 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 shortId = require("shortid"); const queryDefaults_1 = require("../utils/queryDefaults"); const enums_1 = require("../../enums"); const enums_2 = require("../../enums"); const enums_3 = require("../../enums"); const enums_4 = require("../../enums"); const enums_5 = require("../../enums"); const enums_6 = require("../../enums"); const enums_7 = require("../../enums"); const enums_8 = require("../../enums"); const enums_9 = require("../../enums"); const enums_10 = require("../../enums"); class OrderResolver extends spool_sequelize_1.SequelizeResolver { findByIdDefault(id, options = {}) { options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Order.default(this.app), options); return this.findById(id, options); } findByTokenDefault(token, options = {}) { options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Order.default(this.app), options, { where: { token: token } }); return this.findOne(options); } findAndCountDefault(options = {}) { options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Order.default(this.app), options); return this.findAndCountAll(options); } resolveByInstance(order, options = {}) { return Promise.resolve(order); } resolveById(order, options = {}) { return this.findById(order.id, options) .then(resOrder => { if (!resOrder && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `order ${order.id} not found`); } return resOrder; }); } resolveByToken(order, options = {}) { return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, { where: { token: order.token } })) .then(resOrder => { if (!resOrder && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `order token ${order.token} not found`); } return resOrder; }); } resolveByNumber(order, options = {}) { return this.findById(order, options) .then(resOrder => { if (!resOrder && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `order ${order.token} not found`); } return resOrder; }); } resolveByString(order, options = {}) { return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, { where: { token: order } })) .then(resOrder => { if (!resOrder && options.reject !== false) { throw new errors_1.ModelError('E_NOT_FOUND', `order ${order} not found`); } return resOrder; }); } resolve(order, options = {}) { const resolvers = { 'instance': order instanceof this.instance, 'id': !!(order && lodash_1.isObject(order) && order.id), 'token': !!(order && lodash_1.isObject(order) && order.token), 'number': !!(order && lodash_1.isNumber(order)), 'string': !!(order && lodash_1.isString(order)) }; const type = Object.keys(resolvers).find((key) => resolvers[key]); switch (type) { case 'instance': { return this.resolveByInstance(order, options); } case 'id': { return this.resolveById(order, options); } case 'token': { return this.resolveByToken(order, options); } case 'number': { return this.resolveByNumber(order, options); } case 'string': { return this.resolveByString(order, options); } default: { const err = new Error(`Unable to resolve order ${order}`); return Promise.reject(err); } } } } exports.OrderResolver = OrderResolver; class Order extends common_1.FabrixModel { static get resolver() { return OrderResolver; } static config(app, Sequelize) { return { options: { autoSave: true, underscored: true, enums: { ORDER_STATUS: enums_1.ORDER_STATUS, ORDER_CANCEL: enums_2.ORDER_CANCEL, ORDER_FINANCIAL: enums_3.ORDER_FINANCIAL, ORDER_FULFILLMENT: enums_7.ORDER_FULFILLMENT, ORDER_FULFILLMENT_KIND: enums_8.ORDER_FULFILLMENT_KIND, PAYMENT_KIND: enums_4.PAYMENT_KIND, PAYMENT_PROCESSING_METHOD: enums_10.PAYMENT_PROCESSING_METHOD, TRANSACTION_STATUS: enums_5.TRANSACTION_STATUS, TRANSACTION_KIND: enums_6.TRANSACTION_KIND, FULFILLMENT_STATUS: enums_9.FULFILLMENT_STATUS, }, scopes: { live: { where: { live_mode: true } }, open: { where: { status: enums_1.ORDER_STATUS.OPEN } }, closed: { where: { status: enums_1.ORDER_STATUS.CLOSED } }, cancelled: { where: { status: enums_1.ORDER_STATUS.CANCELLED } } }, indexes: [ { fields: ['client_details'], using: 'gin', operator: 'jsonb_path_ops' } ], hooks: { beforeCreate: [ (order, options) => { if (order.ip) { order.create_ip = order.ip; } if (!order.token) { order.token = `order_${shortId.generate()}`; } } ], afterCreate: [ (order, options) => { return app.services.OrderService.afterCreate(order, options) .catch(err => { return Promise.reject(err); }); } ], beforeUpdate: [ (order, options) => { if (order.ip) { order.update_ip = order.ip; } if (order.changed('status') && order.status === enums_1.ORDER_STATUS.CLOSED) { order.close(); } } ], afterUpdate: [ (order, options) => { return app.services.OrderService.afterUpdate(order, options) .catch(err => { return Promise.reject(err); }); } ] } } }; } static schema(app, Sequelize) { return { token: { type: Sequelize.STRING, unique: true }, cart_token: { type: Sequelize.STRING, }, subscription_token: { type: Sequelize.STRING, }, customer_id: { type: Sequelize.INTEGER, allowNull: true }, shop_id: { type: Sequelize.INTEGER, }, user_id: { type: Sequelize.INTEGER, }, has_shipping: { type: Sequelize.BOOLEAN, defaultValue: false }, has_taxes: { type: Sequelize.BOOLEAN, defaultValue: false }, has_subscription: { type: Sequelize.BOOLEAN, defaultValue: false }, total_items: { type: Sequelize.INTEGER, defaultValue: 0 }, billing_address: { type: Sequelize.JSONB, defaultValue: {} }, shipping_address: { type: Sequelize.JSONB, defaultValue: {} }, buyer_accepts_marketing: { type: Sequelize.BOOLEAN, defaultValue: true }, cancel_reason: { type: Sequelize.ENUM, values: lodash_1.values(enums_2.ORDER_CANCEL) }, cancelled_at: { type: Sequelize.DATE }, client_details: { type: Sequelize.JSONB, defaultValue: { 'host': null, 'accept_language': null, 'browser_height': null, 'browser_ip': '0.0.0.0', 'browser_width': null, 'session_hash': null, 'user_agent': null, 'latitude': null, 'longitude': null } }, status: { type: Sequelize.ENUM, values: lodash_1.values(enums_1.ORDER_STATUS), defaultValue: enums_1.ORDER_STATUS.OPEN }, closed_at: { type: Sequelize.DATE }, currency: { type: Sequelize.STRING, defaultValue: app.config.get('cart.default_currency') || 'USD' }, email: { type: Sequelize.STRING, validate: { isEmail: true } }, phone: { type: Sequelize.STRING }, payment_kind: { type: Sequelize.ENUM, values: lodash_1.values(enums_4.PAYMENT_KIND), defaultValue: app.config.get('cart.orders.payment_kind') || enums_4.PAYMENT_KIND.IMMEDIATE }, financial_status: { type: Sequelize.ENUM, values: lodash_1.values(enums_3.ORDER_FINANCIAL), defaultValue: enums_3.ORDER_FINANCIAL.PENDING }, transaction_kind: { type: Sequelize.ENUM, values: lodash_1.values(enums_6.TRANSACTION_KIND), defaultValue: app.config.get('cart.orders.transaction_kind') || enums_6.TRANSACTION_KIND.AUTHORIZE }, fulfillment_status: { type: Sequelize.ENUM, values: lodash_1.values(enums_7.ORDER_FULFILLMENT), defaultValue: enums_7.ORDER_FULFILLMENT.PENDING }, fulfillment_kind: { type: Sequelize.ENUM, values: lodash_1.values(enums_8.ORDER_FULFILLMENT_KIND), defaultValue: app.config.get('cart.orders.fulfillment_kind') || enums_8.ORDER_FULFILLMENT_KIND.MANUAL }, landing_site: { type: Sequelize.STRING }, location_id: { type: Sequelize.STRING }, name: { type: Sequelize.STRING }, number: { type: Sequelize.STRING }, note: { type: Sequelize.STRING }, note_attributes: { type: Sequelize.JSONB, defaultValue: {} }, payment_gateway_names: { type: Sequelize.JSONB, defaultValue: [] }, processed_at: { type: Sequelize.DATE }, processing_method: { type: Sequelize.ENUM, values: lodash_1.values(enums_10.PAYMENT_PROCESSING_METHOD) }, referring_site: { type: Sequelize.STRING }, shipping_lines: { type: Sequelize.JSONB, defaultValue: [] }, discounted_lines: { type: Sequelize.JSONB, defaultValue: [] }, coupon_lines: { type: Sequelize.JSONB, defaultValue: [] }, pricing_overrides: { type: Sequelize.JSONB, defaultValue: [] }, pricing_override_id: { type: Sequelize.INTEGER }, total_overrides: { type: Sequelize.INTEGER, defaultValue: 0 }, source_name: { type: Sequelize.STRING, defaultValue: 'api' }, subtotal_price: { type: Sequelize.INTEGER, defaultValue: 0 }, tax_lines: { type: Sequelize.JSONB, defaultValue: [] }, refunded_lines: { type: Sequelize.JSONB, defaultValue: [] }, taxes_included: { type: Sequelize.BOOLEAN }, total_discounts: { type: Sequelize.INTEGER, defaultValue: 0 }, total_coupons: { type: Sequelize.INTEGER, defaultValue: 0 }, total_shipping: { type: Sequelize.INTEGER, defaultValue: 0 }, total_due: { type: Sequelize.INTEGER, defaultValue: 0 }, total_refunds: { type: Sequelize.INTEGER, defaultValue: 0 }, total_authorized: { type: Sequelize.INTEGER, defaultValue: 0 }, total_captured: { type: Sequelize.INTEGER, defaultValue: 0 }, total_voided: { type: Sequelize.INTEGER, defaultValue: 0 }, total_cancelled: { type: Sequelize.INTEGER, defaultValue: 0 }, total_pending: { type: Sequelize.INTEGER, defaultValue: 0 }, total_line_items_price: { type: Sequelize.INTEGER, defaultValue: 0 }, total_price: { type: Sequelize.INTEGER, defaultValue: 0 }, total_tax: { type: Sequelize.INTEGER, defaultValue: 0 }, total_weight: { type: Sequelize.INTEGER, defaultValue: 0 }, total_fulfilled_fulfillments: { type: Sequelize.INTEGER, defaultValue: 0 }, total_partial_fulfillments: { type: Sequelize.INTEGER, defaultValue: 0 }, total_sent_fulfillments: { type: Sequelize.INTEGER, defaultValue: 0 }, total_cancelled_fulfillments: { type: Sequelize.INTEGER, defaultValue: 0 }, total_pending_fulfillments: { type: Sequelize.INTEGER, defaultValue: 0 }, ip: { type: Sequelize.STRING }, create_ip: { type: Sequelize.STRING }, update_ip: { type: Sequelize.STRING }, live_mode: { type: Sequelize.BOOLEAN, defaultValue: app.config.get('cart.live_mode') } }; } static associate(models) { models.Order.hasMany(models.OrderItem, { as: 'order_items', foreignKey: 'order_id' }); models.Order.hasMany(models.Fulfillment, { as: 'fulfillments', foreignKey: 'order_id' }); models.Order.hasMany(models.Transaction, { as: 'transactions', foreignKey: 'order_id' }); models.Order.hasMany(models.Refund, { as: 'refunds', foreignKey: 'order_id' }); models.Order.belongsToMany(models.Discount, { as: 'discounts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'order' } }, foreignKey: 'model_id', constraints: false }); models.Order.belongsToMany(models.Tag, { as: 'tags', through: { model: models.ItemTag, unique: false, scope: { model: 'order' } }, foreignKey: 'model_id', constraints: false }); models.Order.belongsToMany(models.Source, { as: 'sources', through: { model: models.OrderSource, unique: false }, foreignKey: 'order_id', constraints: false }); 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.Customer, { foreignKey: 'customer_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' }); } } exports.Order = Order; Order.prototype.toJSON = function () { const resp = this instanceof this.app.models['Order'].instance ? this.get({ plain: true }) : this; if (resp.tags) { resp.tags = resp.tags.map(tag => { if (tag && lodash_1.isString(tag)) { return tag; } else if (tag && tag.name && tag.name !== '') { return tag.name; } }); } return resp; }; Order.prototype.cancel = function (data = {}) { this.cancelled_at = new Date(Date.now()); this.status = enums_1.ORDER_STATUS.CANCELLED; this.closed_at = this.cancelled_at; this.cancel_reason = data.cancel_reason || enums_2.ORDER_CANCEL.OTHER; return this; }; Order.prototype.close = function () { this.status = enums_1.ORDER_STATUS.CLOSED; this.closed_at = new Date(Date.now()); return this; }; Order.prototype.logDiscountUsage = function (options = {}) { return this.app.models['Order'].sequelize.Promise.mapSeries(this.discounted_lines, line => { return this.app.models['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 }); }); }); }; Order.prototype.notifyCustomer = function (preNotification, 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 this.app.models['Customer'].instance) { return this.Customer.notifyUsers(preNotification, { transaction: options.transaction || null }); } else { return; } }) .then(() => { return this; }); } else { return Promise.resolve(this); } }; Order.prototype.addShipping = function (shipping = [], options = {}) { return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { 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 }); }) .then(() => { return this.recalculate({ transaction: options.transaction || null }); }); }; Order.prototype.removeShipping = function (shipping = [], options = {}) { return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { 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 }); }) .then(() => { return this.recalculate({ transaction: options.transaction || null }); }); }; Order.prototype.addTaxes = function (taxes = [], options = {}) { return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { 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 }); }) .then(() => { return this.recalculate({ transaction: options.transaction || null }); }); }; Order.prototype.removeTaxes = function (taxes = [], options = {}) { return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { 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 }); }) .then(() => { return this.recalculate({ transaction: options.transaction || null }); }); }; Order.prototype.saveShippingAddress = function (address, options = {}) { this.shipping_address = lodash_1.extend(this.shipping_address, address); this.shipping_address = this.app.services.ProxyCartService.validateAddress(this.shipping_address); return this.app.services.GeolocationGenericService.locate(this.shipping_address) .then(latLng => { this.shipping_address = lodash_1.defaults(this.shipping_address, latLng); return this.recalculate({ transaction: options.transaction || null }); }) .catch(err => { return; }); }; Order.prototype.saveBillingAddress = function (address, options = {}) { this.billing_address = lodash_1.extend(this.billing_address, address); this.billing_address = this.app.services.ProxyCartService.validateAddress(this.billing_address); return this.app.services.GeolocationGenericService.locate(this.billing_address) .then(latLng => { this.billing_address = lodash_1.defaults(this.billing_address, latLng); return this.recalculate({ transaction: options.transaction || null }); }) .catch(err => { return; }); }; Order.prototype.groupFulfillments = function (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(() => { const groups = lodash_1.groupBy(this.order_items, 'fulfillment_service'); const tGroups = lodash_1.map(groups, (items, service) => { return { service: service, items: items }; }); return this.app.models['Order'].sequelize.Promise.mapSeries(tGroups, (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((fulfillments) => { fulfillments = fulfillments || []; this.fulfillments = fulfillments; this.setDataValue('fulfillments', fulfillments); this.set('fulfillments', fulfillments); return this; }); }; Order.prototype.groupTransactions = function (paymentDetails, options = {}) { return this.app.models['Order'].sequelize.Promise.mapSeries(paymentDetails, (detail, index) => { const transaction = this.app.models['Transaction'].build({ customer_id: this.customer_id, order_id: this.id, shop_id: this.shop_id, source_id: detail.source ? detail.source.id : null, account_id: detail.source ? detail.source.account_id : null, currency: this.currency, amount: detail.amount || this.total_due, payment_details: paymentDetails[index], gateway: detail.gateway, kind: this.transaction_kind, device_id: this.device_id || null, description: `Order ${this.name} original transaction ${this.transaction_kind}` }); if (this.payment_kind === enums_4.PAYMENT_KIND.MANUAL) { return this.app.services.PaymentService.manual(transaction, { transaction: options.transaction || null }); } else { return this.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; }); }; Order.prototype.groupSubscriptions = function (active, options = {}) { return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { const orderItems = lodash_1.filter(this.order_items, 'requires_subscription'); const groups = []; const units = lodash_1.groupBy(orderItems, 'subscription_unit'); lodash_1.forEach(units, function (value, unit) { const intervals = lodash_1.groupBy(units[unit], 'subscription_interval'); lodash_1.forEach(intervals, (items, interval) => { groups.push({ unit: unit, interval: interval, items: items }); }); }); return this.app.models['Order'].sequelize.Promise.mapSeries(groups, group => { return this.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; }); }; Order.prototype.fulfill = function (fulfillments = [], 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)); toFulfill = toFulfill.filter(f => f); return this.sequelize.Promise.mapSeries(toFulfill, resFulfillment => { if (!(resFulfillment instanceof this.app.models['Fulfillment'].instance)) { 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 }; return resFulfillment.fulfillUpdate(update, { transaction: options.transaction || null }); }); }) .then(() => { return this.resolveFulfillments({ reload: true, transaction: options.transaction || null }); }) .then(() => { return this.saveFulfillmentStatus({ transaction: options.transaction || null }); }); }; Order.prototype.resolveFinancialStatus = function (options = {}) { if (!this.id) { return Promise.resolve(this); } return this.resolveTransactions({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { this.setFinancialStatus(); return this; }); }; Order.prototype.resolveFulfillmentStatus = function (options = {}) { if (!this.id) { return Promise.resolve(this); } return this.resolveFulfillments({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { return this.resolveOrderItems({ transaction: options.transaction || null, reload: options.reload || null }); }) .then(() => { this.setFulfillmentStatus(); return this; }); }; Order.prototype.setStatus = function () { if (this.financial_status === enums_3.ORDER_FINANCIAL.PAID && this.fulfillment_status === enums_7.ORDER_FULFILLMENT.FULFILLED && this.status === enums_1.ORDER_STATUS.OPEN) { this.close(); } else if (this.financial_status === enums_3.ORDER_FINANCIAL.CANCELLED && this.fulfillment_status === enums_7.ORDER_FULFILLMENT.CANCELLED && this.status === enums_1.ORDER_STATUS.OPEN) { this.cancel(); } return this; }; Order.prototype.resolveStatus = function (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(); }); }; Order.prototype.saveStatus = function (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 }); }); }; Order.prototype.saveFinancialStatus = function (options = {}) { let currentStatus, previousStatus; 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 this.app.services.EventsService.publish(event.type, event, { save: true, transaction: options.transaction || null }); } else { return; } }) .then(() => { if (currentStatus === enums_3.ORDER_FINANCIAL.PAID && previousStatus !== enums_3.ORDER_FINANCIAL.PAID) { return this.attemptImmediate(options); } else { return this; } }) .then(() => { return this; }); }; Order.prototype.saveFulfillmentStatus = function (options = {}) { let currentStatus, previousStatus; 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 this.app.services.EventsService.publish(event.type, event, { save: true, transaction: options.transaction || null }); } else { return; } }) .then(() => { return this; }); }; Order.prototype.setFinancialStatus = function () { if (!this.transactions) { throw new Error('Order.setFinancialStatus requires transactions to be populated'); } const pending = this.transactions.filter(transaction => [ enums_5.TRANSACTION_STATUS.PENDING, enums_5.TRANSACTION_STATUS.FAILURE, enums_5.TRANSACTION_STATUS.ERROR ].indexOf(transaction.status) > -1); const cancelled = this.transactions.filter(transaction => [ enums_5.TRANSACTION_STATUS.CANCELLED ].indexOf(transaction.status) > -1); const successes = this.transactions.filter(transaction => [ enums_5.TRANSACTION_STATUS.SUCCESS ].indexOf(transaction.status) > -1); let financialStatus = enums_3.ORDER_FINANCIAL.PENDING; let totalAuthorized = 0; let totalVoided = 0; let totalSale = 0; let totalRefund = 0; let totalCancelled = 0; let totalPending = 0; lodash_1.each(successes, transaction => { if (transaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE) { totalAuthorized = totalAuthorized + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.VOID) { totalVoided = totalVoided + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.CAPTURE) { totalSale = totalSale + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.SALE) { totalSale = totalSale + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.REFUND) { totalRefund = totalRefund + transaction.amount; } }); lodash_1.each(pending, transaction => { if (transaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE) { totalPending = totalPending + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.CAPTURE) { totalPending = totalPending + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.SALE) { totalPending = totalPending + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.VOID) { totalPending = totalPending - transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.REFUND) { totalPending = totalPending - transaction.amount; } }); lodash_1.each(cancelled, transaction => { if (transaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE) { totalCancelled = totalCancelled + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.CAPTURE) { totalCancelled = totalCancelled + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.SALE) { totalCancelled = totalCancelled + transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.VOID) { totalCancelled = totalCancelled - transaction.amount; } else if (transaction.kind === enums_6.TRANSACTION_KIND.REFUND) { totalCancelled = totalCancelled - transaction.amount; } }); if (this.total_items === 0) { financialStatus = enums_3.ORDER_FINANCIAL.PENDING; } else if (this.total_price === 0 && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.PAID; } else if (totalAuthorized === this.total_price && totalSale === 0 && totalVoided === 0 && totalRefund === 0 && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.AUTHORIZED; } else if (totalAuthorized === totalVoided && totalVoided > 0 && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.VOIDED; } else if (this.total_price === totalVoided && totalVoided > 0 && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.VOIDED; } else if (totalSale === this.total_price && totalRefund === 0 && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.PAID; } else if (totalSale < this.total_price && totalSale > 0 && totalRefund === 0 && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.PARTIALLY_PAID; } else if (this.total_price === totalRefund && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.REFUNDED; } else if (totalRefund < this.total_price && totalRefund > 0 && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.PARTIALLY_REFUNDED; } else if (this.total_price === totalCancelled && this.total_items > 0) { financialStatus = enums_3.ORDER_FINANCIAL.CANCELLED; } this.app.log.debug(`ORDER ${this.id}: FINANCIAL Status: ${financialStatus}, Sales: ${totalSale}, Authorized: ${totalAuthorized}, Refunded: ${totalRefund}, Pending: ${totalPending}, Cancelled: ${totalCancelled}`); 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; }; Order.prototype.setFulfillmentStatus = function () { if (!this.fulfillments) { throw new Error('Order.setFulfillmentStatus requires fulfillments to be populated'); } if (!this.order_items) { throw new Error('Order.setFulfillmentStatus requires order_items to be populated'); } let fulfillmentStatus = enums_7.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 === enums_9.FULFILLMENT_STATUS.FULFILLED) { totalFulfillments++; } else if (fulfillment.status === enums_9.FULFILLMENT_STATUS.PARTIAL) { totalPartialFulfillments++; } else if (fulfillment.status === enums_9.FULFILLMENT_STATUS.SENT) { totalSentFulfillments++; } else if (fulfillment.status === enums_9.FULFILLMENT_STATUS.NONE) { totalNonFulfillments++; } else if (fulfillment.status === enums_9.FULFILLMENT_STATUS.PENDING) { totalPendingFulfillments++; } else if (fulfillment.status === enums_9.FULFILLMENT_STATUS.CANCELLED) { totalCancelledFulfillments++; } }); if (totalFulfillments === this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = enums_7.ORDER_FULFILLMENT.FULFILLED; } else if (totalSentFulfillments === this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = enums_7.ORDER_FULFILLMENT.SENT; } else if (totalPartialFulfillments > 0) { fulfillmentStatus = enums_7.ORDER_FULFILLMENT.PARTIAL; } else if (totalNonFulfillments >= this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = enums_7.ORDER_FULFILLMENT.NONE; } else if (totalCancelledFulfillments === this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = enums_7.ORDER_FULFILLMENT.CANCELLED; } else if (totalPendingFulfillments === this.fulfillments.length && this.fulfillments.length > 0) { fulfillmentStatus = enums_7.ORDER_FULFILLMENT.PENDING; } if (fulfillmentStatus === enums_7.ORDER_FULFILLMENT.FULFILLED || fulfillmentStatus === enums_7.ORDER_FULFILLMENT.CANCELLED) { this.status = enums_1.ORDER_STATUS.CLOSED; } this.total_fulfilled_fulfillments = totalFulfillments; this.total_partial_fulfillments = totalPartialFulfillments; this.total_sent_fulfillments = totalSentFulfillments; this.total_cancelled_fulfillments = totalCancelledFulfillments; this.total_pending_fulfillments = totalPendingFulfillments; this.fulfillment_status = fulfillmentStatus; return this; }; Order.prototype.sendToFulfillment = function (options = {}) { return this.resolveFulfillments({ transaction: options.transaction || null, reload: options.reload || null }) .then(() => { return this.app.models['Order'].sequelize.Promise.mapSeries(this.fulfillments, fulfillment => { return this.app.services.FulfillmentService.sendFulfillment(this, fulfillment, { transaction: options.tr