@fabrix/spool-cart
Version:
Spool - eCommerce Spool for Fabrix
1,368 lines • 74.4 kB
JavaScript
"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