@fabrix/spool-cart
Version:
Spool - eCommerce Spool for Fabrix
1,081 lines (1,080 loc) • 78.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@fabrix/fabrix/dist/common");
const errors_1 = require("@fabrix/spool-sequelize/dist/errors");
const _ = require("lodash");
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");
class OrderService extends common_1.FabrixService {
publish(type, event, options = {}) {
if (this.app.services.EventsService) {
options.include = options.include || [{
model: this.app.models.EventItem.instance,
as: 'objects'
}];
return this.app.services.EventsService.publish(type, event, options);
}
this.app.log.debug('spool-events is not installed, please install it to use publish');
return Promise.resolve();
}
create(obj, options = {}) {
const Address = this.app.models['Address'];
const Customer = this.app.models['Customer'];
const Order = this.app.models['Order'];
const OrderItem = this.app.models['OrderItem'];
let totalDue = obj.total_due;
let totalPrice = obj.total_price;
let totalOverrides = 0;
let deduction = 0;
let resOrder = {};
let resCustomer = {};
let resBillingAddress = {};
let resShippingAddress = {};
if (!obj.cart_token && !obj.subscription_token) {
const err = new errors_1.ModelError('E_NOT_FOUND', 'Missing a Cart token or a Subscription token');
return Promise.reject(err);
}
if (!obj.payment_details) {
const err = new errors_1.ModelError('E_NOT_FOUND', 'Missing Payment Details');
return Promise.reject(err);
}
if (obj.shipping_address && !obj.billing_address) {
obj.billing_address = obj.shipping_address;
}
if (!obj.shipping_address && obj.billing_address) {
obj.shipping_address = obj.billing_address;
}
return Customer.resolve(obj.customer_id || obj.customer, {
include: [
{
model: Address.instance,
as: 'shipping_address'
},
{
model: Address.instance,
as: 'billing_address'
},
{
model: Address.instance,
as: 'default_address'
}
]
})
.then(customer => {
if (customer && customer.default_address && !customer.shipping_address) {
customer.shipping_address = customer.default_address;
}
if (customer && customer.default_address && !customer.billing_address) {
customer.billing_address = customer.default_address;
}
if (customer && !customer.shipping_address && !obj.shipping_address && obj.has_shipping) {
throw new errors_1.ModelError('E_NOT_FOUND', `Could not find customer shipping address for id '${obj.customer_id}'`);
}
if (customer && !customer.billing_address && !obj.billing_address && obj.has_shipping) {
throw new errors_1.ModelError('E_NOT_FOUND', `Could not find customer billing address for id '${obj.customer_id}'`);
}
if (!customer) {
resCustomer = {
id: null,
email: null,
account_balance: 0,
billing_address: null,
shipping_address: null
};
}
else {
resCustomer = customer;
}
resBillingAddress = this.resolveToAddress(resCustomer.billing_address, obj.billing_address);
resShippingAddress = this.resolveToAddress(resCustomer.shipping_address, obj.shipping_address);
if (!resShippingAddress && obj.has_shipping) {
throw new Error('Order does not have a valid shipping address');
}
if (!obj.payment_details) {
obj.payment_details = [];
}
if (!obj.pricing_overrides) {
obj.pricing_overrides = [];
}
const paymentGatewayNames = obj.payment_details.map(detail => { return detail.gateway; });
const accountBalanceIndex = _.findIndex(obj.pricing_overrides, { name: 'Account Balance' });
if (accountBalanceIndex > -1) {
const prevPrice = obj.pricing_overrides[accountBalanceIndex].price;
totalDue = totalDue + prevPrice;
totalPrice = totalPrice + prevPrice;
}
if (resCustomer.account_balance > 0) {
const exclusions = obj.line_items.filter(item => {
item.exclude_payment_types = item.exclude_payment_types || [];
return item.exclude_payment_types.indexOf('Account Balance') !== -1;
});
const removeTotal = _.sumBy(exclusions, (e) => e.calculated_price);
const deductibleTotal = Math.max(0, totalDue - removeTotal);
deduction = Math.min(deductibleTotal, (deductibleTotal - (deductibleTotal - resCustomer.account_balance)));
if (deduction > 0) {
if (accountBalanceIndex === -1) {
obj.pricing_overrides.push({
name: 'Account Balance',
price: deduction
});
totalDue = Math.max(0, totalDue - deduction);
totalPrice = Math.max(0, totalPrice - deduction);
}
else {
obj.pricing_overrides[accountBalanceIndex].price = deduction;
totalDue = Math.max(0, totalDue - deduction);
totalPrice = Math.max(0, totalPrice - deduction);
}
_.each(obj.pricing_overrides, override => {
totalOverrides = totalOverrides + override.price;
});
obj.total_overrides = totalOverrides;
}
}
else {
if (accountBalanceIndex > -1) {
const prevPrice = obj.pricing_overrides[accountBalanceIndex].price;
obj.pricing_overrides = obj.pricing_overrides.splice(accountBalanceIndex, 1);
totalDue = Math.max(0, totalDue + prevPrice);
totalPrice = Math.max(0, totalPrice + prevPrice);
}
}
const fulfillmentsGroups = _.groupBy(obj.line_items, 'fulfillment_service');
const fulfillments = _.map(fulfillmentsGroups, (items, service) => {
return {
service: service,
total_items: items.length,
total_pending_fulfillments: items.length
};
});
const lineItems = obj.line_items.map(item => {
item.customer_id = resCustomer.id || null;
return item;
});
return Order.create({
processing_method: obj.processing_method || enums_1.PAYMENT_PROCESSING_METHOD.DIRECT,
processed_at: new Date(),
cart_token: obj.cart_token,
subscription_token: obj.subscription_token,
currency: obj.currency,
order_items: lineItems,
tax_lines: obj.tax_lines,
shipping_lines: obj.shipping_lines,
discounted_lines: obj.discounted_lines,
coupon_lines: obj.coupon_lines,
subtotal_price: obj.subtotal_price,
taxes_included: obj.taxes_included,
total_discounts: obj.total_discounts,
total_coupons: obj.total_coupons,
total_line_items_price: obj.total_line_items_price,
total_price: totalPrice,
total_due: totalDue,
total_tax: obj.total_tax,
total_shipping: obj.total_shipping,
total_weight: obj.total_weight,
total_items: obj.total_items,
shop_id: obj.shop_id || null,
has_shipping: obj.has_shipping,
has_taxes: obj.has_taxes,
has_subscription: obj.has_subscription,
email: obj.email || resCustomer.email || null,
phone: obj.phone || resCustomer.phone || null,
fulfillment_kind: obj.fulfillment_kind || this.app.config.get('cart.orders.fulfillment_kind'),
payment_kind: obj.payment_kind || this.app.config.get('cart.orders.payment_kind'),
transaction_kind: obj.transaction_kind
|| this.app.config.get('cart.orders.transaction_kind')
|| enums_6.TRANSACTION_KIND.AUTHORIZE,
payment_gateway_names: paymentGatewayNames,
client_details: obj.client_details,
ip: obj.ip,
customer_id: resCustomer.id,
buyer_accepts_marketing: resCustomer.accepts_marketing || obj.buyer_accepts_marketing,
billing_address: resBillingAddress,
shipping_address: resShippingAddress,
user_id: obj.user_id || null,
pricing_override_id: obj.pricing_override_id || null,
pricing_overrides: obj.pricing_overrides || [],
total_overrides: obj.total_overrides || 0,
notes: obj.notes || null,
fulfillments: fulfillments,
total_pending_fulfillments: fulfillments.length,
shops: lineItems.map(item => item.shop_id).filter(n => n)
}, {
include: [
{
model: OrderItem.instance,
as: 'order_items'
},
{
model: this.app.models['Fulfillment'].instance,
as: 'fulfillments',
include: [
{
model: OrderItem.instance,
as: 'order_items'
}
]
},
{
model: this.app.models['Transaction'].instance,
as: 'transactions'
},
],
transaction: options.transaction || null
});
})
.then(_order => {
if (!_order) {
throw new Error('Unexpected Error while creating order');
}
resOrder = _order;
if (resCustomer instanceof Customer.instance && deduction > 0) {
return resCustomer.logAccountBalance('debit', deduction, resOrder.currency, null, resOrder.id, { transaction: options.transaction || null });
}
else {
return;
}
})
.then(() => {
if (resCustomer instanceof Customer.instance) {
return resCustomer
.setTotalSpent(totalPrice)
.setLastOrder(resOrder)
.setTotalOrders()
.setAvgSpent()
.save({ transaction: options.transaction || null });
}
else {
return;
}
})
.then(() => {
return resOrder.groupFulfillments({ transaction: options.transaction || null });
})
.then(() => {
return resOrder.groupTransactions(obj.payment_details, { transaction: options.transaction || null });
})
.then(() => {
return resOrder.reload({ transaction: options.transaction || null });
})
.then(() => {
return resOrder.saveStatus({ transaction: options.transaction || null });
})
.then(() => {
if (resOrder.discounted_lines.length > 0) {
return resOrder.logDiscountUsage({ transaction: options.transaction || null });
}
else {
return;
}
})
.then(() => {
if (resCustomer instanceof Customer.instance) {
return resCustomer.addOrder(resOrder.id, { transaction: options.transaction || null })
.then(() => {
const event = {
object_id: resCustomer.id,
object: 'customer',
objects: [{
customer: resCustomer.id
}, {
order: resOrder.id
}],
type: 'customer.order.created',
message: `Customer ${resCustomer.email || 'ID ' + resCustomer.id} Order ${resOrder.name} was created`,
data: resOrder
};
return this.publish(event.type, event, {
save: true,
transaction: options.transaction || null
});
});
}
else {
return;
}
})
.then(() => {
return Order.findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
update(order, options) {
options = options || {};
const Order = this.app.models.Order;
let resOrder;
return Order.resolve(order, options)
.then(_order => {
if (!_order) {
throw new Error('Order not found');
}
resOrder = _order;
if ([enums_2.FULFILLMENT_STATUS.PENDING, enums_2.FULFILLMENT_STATUS.NONE, enums_2.FULFILLMENT_STATUS.SENT]
.indexOf(resOrder.fulfillment_status) === -1 || resOrder.cancelled_at) {
throw new Error(`${order.name} can not be updated as it is already being fulfilled`);
}
if (order.billing_address) {
resOrder.billing_address = _.extend(resOrder.billing_address, order.billing_address);
resOrder.billing_address = this.app.services.ProxyCartService.validateAddress(resOrder.billing_address);
return this.app.services.GeolocationGenericService.locate(resOrder.billing_address)
.then(latLng => {
resOrder.billing_address = _.defaults(resOrder.billing_address, latLng);
return;
})
.catch(err => {
return;
});
}
return;
})
.then(() => {
if (order.shipping_address) {
resOrder.shipping_address = _.extend(resOrder.shipping_address, order.shipping_address);
resOrder.shipping_address = this.app.services.ProxyCartService.validateAddress(resOrder.shipping_address);
return this.app.services.GeolocationGenericService.locate(resOrder.shipping_address)
.then(latLng => {
resOrder.shipping_address = _.defaults(resOrder.shipping_address, latLng);
return;
})
.catch(err => {
return;
});
}
return;
})
.then(() => {
if (order.buyer_accepts_marketing) {
resOrder.buyer_accepts_marketing = order.buyer_accepts_marketing;
}
if (order.email) {
resOrder.email = order.email;
}
if (order.phone) {
resOrder.phone = order.phone;
}
if (order.note) {
resOrder.note = order.note;
}
return resOrder.recalculate({ transaction: options.transaction || null });
})
.then(() => {
return resOrder.sendUpdatedEmail({ transaction: options.transaction || null });
})
.then(() => {
return Order.findByIdDefault(resOrder.id);
});
}
pay(order, paymentDetails, options = {}) {
const Order = this.app.models['Order'];
const Sequelize = Order.sequelize;
options.includes = options.includes || [];
if (!options.includes.some(include => include.model === this.app.models['OrderItem'].instance)) {
options.includes.push({
model: this.app.models['OrderItem'].instance,
as: 'order_items'
});
}
if (!options.includes.some(include => include.model === this.app.models['Fulfillment'].instance)) {
options.includes.push({
model: this.app.models['Fulfillment'].instance,
as: 'fulfillments'
});
}
if (!options.includes.some(include => include.model === this.app.models['Transaction'].instance)) {
options.includes.push({
model: this.app.models['Transaction'].instance,
as: 'transactions'
});
}
if (!options.includes.some(include => include.model === this.app.models['Refund'].instance)) {
options.includes.push({
model: this.app.models['Refund'].instance,
as: 'refunds'
});
}
let resOrder;
return Order.resolve(order, options)
.then(_order => {
if (!_order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
if (_order.financial_status !== (enums_7.ORDER_FINANCIAL.AUTHORIZED || enums_7.ORDER_FINANCIAL.PARTIALLY_PAID)) {
throw new Error(`Order status is ${_order.financial_status} not '${enums_7.ORDER_FINANCIAL.AUTHORIZED} or ${enums_7.ORDER_FINANCIAL.PARTIALLY_PAID}'`);
}
resOrder = _order;
return resOrder.resolveTransactions({ transaction: options.transaction || null });
})
.then(() => {
const authorized = resOrder.transactions.filter(transaction => transaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE);
return Sequelize.Promise.mapSeries(authorized, transaction => {
return this.app.services.TransactionService.capture(transaction, { transaction: options.transaction || null });
});
})
.then(() => {
const event = {
object_id: resOrder.id,
object: 'order',
objects: [{
customer: resOrder.customer_id
}, {
order: resOrder.id
}],
type: `order.${resOrder.financial_status}`,
message: `Order ${resOrder.name} was ${resOrder.financial_status}`,
data: resOrder
};
return this.publish(event.type, event, {
save: true,
transaction: options.transaction || null
});
})
.then((event) => {
if (resOrder.financial_status === enums_7.ORDER_FINANCIAL.PAID && resOrder.customer_id) {
return resOrder.sendPaidEmail({ transaciton: options.transaction || null });
}
else {
return;
}
})
.then((notifications) => {
return this.app.models['Order'].findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
payOrders(orders, options = {}) {
const Sequelize = this.app.models['Order'].sequelize;
return Sequelize.Promise.mapSeries(orders, order => {
return this.pay(order, order.payment_details, { transaction: options.transaction || null });
});
}
refundOrderItem(orderItem, options = {}) {
const OrderItem = this.app.models['OrderItem'];
const Order = this.app.models['Order'];
const Refund = this.app.models['Refund'];
let resOrderItem, resOrder;
return OrderItem.resolve(orderItem, { transaction: options.transaction || null })
.then(_orderItem => {
if (!_orderItem) {
throw new errors_1.ModelError('E_NOT_FOUND', 'OrderItem not found');
}
resOrderItem = _orderItem;
return resOrderItem;
})
.then(() => {
return resOrderItem.getOrder({ transaction: options.transaction || null });
})
.then(order => {
if (!order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
const allowedStatuses = [
enums_7.ORDER_FINANCIAL.PAID,
enums_7.ORDER_FINANCIAL.PARTIALLY_PAID,
enums_7.ORDER_FINANCIAL.PARTIALLY_REFUNDED
];
if (allowedStatuses.indexOf(order.financial_status) === -1) {
throw new Error(`Order status is ${order.financial_status} not '${enums_7.ORDER_FINANCIAL.PAID}, ${enums_7.ORDER_FINANCIAL.PARTIALLY_PAID}' or '${enums_7.ORDER_FINANCIAL.PARTIALLY_REFUNDED}'`);
}
resOrder = order;
return resOrder.resolveTransactions({ transaction: options.transaction || null });
})
.then(() => {
const canRefund = resOrder.transactions.filter(transaction => {
return [enums_6.TRANSACTION_KIND.SALE, enums_6.TRANSACTION_KIND.CAPTURE].indexOf(transaction.kind) > -1;
});
const toRefund = canRefund.find(transaction => transaction.amount >= resOrderItem.calculated_price);
if (!toRefund) {
throw new Error('No transaction available to refund this item\'s calculated price');
}
return this.app.services.TransactionService.partiallyRefund(toRefund, resOrderItem.calculated_price, { transaction: options.transaction || null });
})
.then(transaction => {
if (transaction.kind === enums_6.TRANSACTION_KIND.REFUND && transaction.status === enums_5.TRANSACTION_STATUS.SUCCESS) {
return Refund.create({
order_id: resOrder.id,
transaction_id: transaction.id,
amount: transaction.amount,
restock: options.restock || null
}, {
transaction: options.transaction || null
});
}
else {
throw new Error('Was unable to refund this transaction');
}
})
.then(refund => {
return resOrderItem.setRefund(refund.id, { transaction: options.transaction || null });
})
.then(newRefund => {
return resOrder.resolveRefunds({ transaction: options.transaction || null });
})
.then(() => {
return resOrder.saveFinancialStatus({ transaction: options.transaction || null });
})
.then(order => {
return Order.findByIdDefault(resOrder.id);
});
}
refund(order, refunds = [], options = {}) {
const Order = this.app.models['Order'];
const Sequelize = Order.sequelize;
options.includes = options.includes || [];
if (!options.includes.some(include => include.model === this.app.models['OrderItem'].instance)) {
options.includes.push({
model: this.app.models['OrderItem'].instance,
as: 'order_items'
});
}
if (!options.includes.some(include => include.model === this.app.models['Fulfillment'].instance)) {
options.includes.push({
model: this.app.models['Fulfillment'].instance,
as: 'fulfillments'
});
}
if (!options.includes.some(include => include.model === this.app.models['Transaction'].instance)) {
options.includes.push({
model: this.app.models['Transaction'].instance,
as: 'transactions'
});
}
if (!options.includes.some(include => include.model === this.app.models['Refund'].instance)) {
options.includes.push({
model: this.app.models['Refund'].instance,
as: 'refunds'
});
}
let resOrder;
return Order.resolve(order, options)
.then(_order => {
if (!_order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
const allowedStatuses = [enums_7.ORDER_FINANCIAL.PAID, enums_7.ORDER_FINANCIAL.PARTIALLY_PAID, enums_7.ORDER_FINANCIAL.PARTIALLY_REFUNDED];
if (allowedStatuses.indexOf(_order.financial_status) === -1) {
throw new Error(`Order status is ${_order.financial_status} not '${enums_7.ORDER_FINANCIAL.PAID}, ${enums_7.ORDER_FINANCIAL.PARTIALLY_PAID}' or '${enums_7.ORDER_FINANCIAL.PARTIALLY_REFUNDED}'`);
}
return _order;
})
.then(_order => {
resOrder = _order;
return resOrder.resolveTransactions({ transaction: options.transaction || null });
})
.then(() => {
return resOrder.resolveRefunds({ transaction: options.transaction || null });
})
.then(() => {
if (refunds.length > 0) {
return Sequelize.Promise.mapSeries(refunds, refund => {
const refundTransaction = resOrder.transactions.find(transaction => transaction.id === refund.transaction);
if ([enums_6.TRANSACTION_KIND.SALE, enums_6.TRANSACTION_KIND.CAPTURE].indexOf(refundTransaction.kind) > -1
&& refundTransaction.status === enums_5.TRANSACTION_STATUS.SUCCESS) {
if (refund.amount === refundTransaction.amount) {
return this.app.services.TransactionService
.refund(refundTransaction, { transaction: options.transaction || null });
}
else {
return this.app.services.TransactionService
.partiallyRefund(refundTransaction, refund.amount, { transaction: options.transaction || null });
}
}
});
}
else {
const canRefund = resOrder.transactions.filter(transaction => {
if ([enums_6.TRANSACTION_KIND.SALE, enums_6.TRANSACTION_KIND.CAPTURE].indexOf(transaction.kind) > -1
&& transaction.status === enums_5.TRANSACTION_STATUS.SUCCESS) {
return transaction;
}
});
return Sequelize.Promise.mapSeries(canRefund, transaction => {
return this.app.services.TransactionService.refund(transaction, { transaction: options.transaction || null });
});
}
})
.then(refundedTransactions => {
const newRefunds = refundedTransactions.filter(transaction => transaction.kind === enums_6.TRANSACTION_KIND.REFUND
&& transaction.status === enums_5.TRANSACTION_STATUS.SUCCESS);
return Sequelize.Promise.mapSeries(newRefunds, transaction => {
return resOrder.createRefund({
order_id: resOrder.id,
transaction_id: transaction.id,
amount: transaction.amount
}, {
transaction: options.transaction || null
});
});
})
.then(newRefunds => {
return resOrder.reload({ transaction: options.transaction || null });
})
.then(() => {
let totalRefunds = 0;
resOrder.refunds.forEach(refund => {
totalRefunds = totalRefunds + refund.amount;
});
resOrder.total_refunds = totalRefunds;
return resOrder.saveFinancialStatus({ reload: true, transaction: options.transaction || null });
})
.then(() => {
const event = {
object_id: resOrder.id,
object: 'order',
objects: [{
customer: resOrder.customer_id
}, {
order: resOrder.id
}],
type: `order.${resOrder.financial_status}`,
message: `Order ${resOrder.name} was ${resOrder.financial_status}`,
data: resOrder
};
return this.publish(event.type, event, {
save: true,
transaction: options.transaction || null
});
})
.then(() => {
return resOrder.sendRefundedEmail({ transaction: options.transaction || null });
})
.then(email => {
return Order.findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
authorize(order, authorizations = [], options = {}) {
const Order = this.app.models['Order'];
let resOrder;
return Order.resolve(order, { transaction: options.transaction || null })
.then(_order => {
if (!_order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
resOrder = _order;
return resOrder.resolveTransactions({ transaction: options.transaction || null });
})
.then(() => {
if (authorizations.length > 0) {
const toAuthorize = authorizations.map(authorize => {
const authorizeTransaction = resOrder.transactions.find(transaction => transaction.id === authorize.transaction);
if (authorizeTransaction
&& authorizeTransaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE
&& authorizeTransaction.status === enums_5.TRANSACTION_STATUS.PENDING) {
return authorizeTransaction;
}
}).filter(n => n);
return Order.sequelize.Promise.mapSeries(toAuthorize, transaction => {
return this.app.services.TransactionService.authorize(transaction, { transaction: options.transaction || null });
});
}
else {
const canAuthorize = resOrder.transactions.filter(transaction => {
if (transaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE
&& transaction.status === enums_5.TRANSACTION_STATUS.PENDING) {
return transaction;
}
});
return Order.sequelize.Promise.mapSeries(canAuthorize, transaction => {
return this.app.services.TransactionService.authorize(transaction, { transaction: options.transaction || null });
});
}
})
.then(() => {
return resOrder.saveFinancialStatus({ transaction: options.transaction || null });
})
.then(() => {
return Order.findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
capture(order, captures = [], options = {}) {
const Order = this.app.models['Order'];
const Sequelize = Order.sequelize;
let resOrder;
return Order.resolve(order, options)
.then(_order => {
if (!_order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
resOrder = _order;
return resOrder.resolveTransactions({ transaction: options.transaction || null });
})
.then(() => {
if (captures.length > 0) {
const toCapture = captures.map(capture => {
const captureTransaction = resOrder.transactions.find(transaction => transaction.id === capture.transaction);
if (captureTransaction
&& captureTransaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE
&& captureTransaction.status === enums_5.TRANSACTION_STATUS.SUCCESS) {
return captureTransaction;
}
}).filter(n => n);
return Sequelize.Promise.mapSeries(toCapture, transaction => {
return this.app.services.TransactionService.capture(transaction, { transaction: options.transaction || null });
});
}
else {
const canCapture = resOrder.transactions.filter(transaction => {
if (transaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE
&& transaction.status === enums_5.TRANSACTION_STATUS.SUCCESS) {
return transaction;
}
});
return Sequelize.Promise.mapSeries(canCapture, transaction => {
return this.app.services.TransactionService.capture(transaction, { transaction: options.transaction || null });
});
}
})
.then(_captures => {
return resOrder.saveFinancialStatus({ transaction: options.transaction || null });
})
.then(_order => {
return Order.findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
void(order, voids = [], options = {}) {
const Order = this.app.models['Order'];
const Sequelize = Order.sequelize;
let resOrder;
return Order.resolve(order, options)
.then(_order => {
if (!_order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
resOrder = _order;
return resOrder.resolveTransactions({ transaction: options.transaction || null });
})
.then(() => {
if (voids.length > 0) {
const toVoid = voids.map(tVoid => {
const voidTransaction = resOrder.transactions.find(transaction => transaction.id === tVoid.transaction);
if (voidTransaction
&& voidTransaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE
&& voidTransaction.status === enums_5.TRANSACTION_STATUS.SUCCESS) {
return voidTransaction;
}
}).filter(n => n);
return Sequelize.Promise.mapSeries(toVoid, transaction => {
return this.app.services.TransactionService.void(transaction, { transaction: options.transaction || null });
});
}
else {
const canVoid = resOrder.transactions.filter(transaction => {
if (transaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE
&& transaction.status === enums_5.TRANSACTION_STATUS.SUCCESS) {
return transaction;
}
});
return Sequelize.Promise.mapSeries(canVoid, transaction => {
return this.app.services.TransactionService.void(transaction, { transaction: options.transaction || null });
});
}
})
.then(_voids => {
return resOrder.saveFinancialStatus({ transaction: options.transaction || null });
})
.then(_order => {
return Order.findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
retry(order, retries, options) {
retries = retries || [];
options = options || {};
const Order = this.app.models['Order'];
const Sequelize = Order.sequelize;
let resOrder;
return Order.resolve(order, options)
.then(_order => {
if (!_order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
resOrder = _order;
return resOrder.resolveTransactions({ transaction: options.transaction || null });
})
.then(() => {
if (retries.length > 0) {
const toRetry = retries.map(tRetry => {
const retryTransaction = resOrder.transactions.find(transaction => transaction.id === tRetry.transaction);
if (retryTransaction
&& [enums_5.TRANSACTION_STATUS.FAILURE, enums_5.TRANSACTION_STATUS.PENDING].indexOf(retryTransaction.status) !== -1) {
return retryTransaction;
}
}).filter(n => n);
return Sequelize.Promise.mapSeries(toRetry, transaction => {
return this.app.services.TransactionService.retry(transaction, { transaction: options.transaction || null });
});
}
else {
const canRetry = resOrder.transactions.filter(transaction => {
if ([enums_5.TRANSACTION_STATUS.FAILURE, enums_5.TRANSACTION_STATUS.PENDING].indexOf(transaction.status) !== -1) {
return transaction;
}
});
return Sequelize.Promise.mapSeries(canRetry, transaction => {
return this.app.services.TransactionService.retry(transaction, { transaction: options.transaction || null });
});
}
})
.then(_retries => {
return resOrder.saveFinancialStatus({ transaction: options.transaction || null });
})
.then(() => {
if (resOrder.financial_status === enums_7.ORDER_FINANCIAL.PAID) {
return resOrder.sendPaidEmail({ transaction: options.transaction || null });
}
else if (resOrder.financial_status === enums_7.ORDER_FINANCIAL.PARTIALLY_PAID) {
return resOrder.sendPartiallyPaidEmail({ transaction: options.transaction || null });
}
return;
})
.then(() => {
return Order.findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
cancel(order, options = {}) {
const Order = this.app.models['Order'];
const Sequelize = Order.sequelize;
const reason = order.cancel_reason || enums_8.ORDER_CANCEL.OTHER;
options.includes = options.includes || [];
if (!options.includes.some(include => include.model === this.app.models['OrderItem'].instance)) {
options.includes.push({
model: this.app.models['OrderItem'].instance,
as: 'order_items'
});
}
if (!options.includes.some(include => include.model === this.app.models['Fulfillment'].instance)) {
options.includes.push({
model: this.app.models['Fulfillment'].instance,
as: 'fulfillments'
});
}
if (!options.includes.some(include => include.model === this.app.models['Transaction'].instance)) {
options.includes.push({
model: this.app.models['Transaction'].instance,
as: 'transactions'
});
}
if (!options.includes.some(include => include.model === this.app.models['Refund'].instance)) {
options.includes.push({
model: this.app.models['Refund'].instance,
as: 'refunds'
});
}
let resOrder, canRefund = [], canVoid = [], canCancel = [], canCancelFulfillment = [];
return Order.resolve(order, options)
.then(_order => {
if (!_order) {
throw new Error('Order not found');
}
resOrder = _order;
if ([enums_4.ORDER_FULFILLMENT.NONE, enums_4.ORDER_FULFILLMENT.PENDING, enums_4.ORDER_FULFILLMENT.SENT]
.indexOf(resOrder.fulfillment_status) < 0) {
throw new Error(`Order can not be cancelled because it's fulfillment status is ${resOrder.fulfillment_status} not '${enums_4.ORDER_FULFILLMENT.NONE}', '${enums_4.ORDER_FULFILLMENT.PENDING}', '${enums_4.ORDER_FULFILLMENT.SENT}'`);
}
return resOrder.resolveTransactions({ transaction: options.transaction || null });
})
.then(() => {
canRefund = resOrder.transactions.filter(transaction => [enums_6.TRANSACTION_KIND.SALE, enums_6.TRANSACTION_KIND.CAPTURE].indexOf(transaction.kind) > -1
&& transaction.status === enums_5.TRANSACTION_STATUS.SUCCESS);
canVoid = resOrder.transactions.filter(transaction => transaction.kind === enums_6.TRANSACTION_KIND.AUTHORIZE
&& transaction.status === enums_5.TRANSACTION_STATUS.SUCCESS);
canCancel = resOrder.transactions.filter(transaction => transaction.status === enums_5.TRANSACTION_STATUS.PENDING);
return Sequelize.Promise.mapSeries(canRefund, transaction => {
return this.app.services.TransactionService.refund(transaction, {
transaction: options.transaction || null
});
});
})
.then(() => {
return Sequelize.Promise.mapSeries(canVoid, transaction => {
return this.app.services.TransactionService.void(transaction, {
transaction: options.transaction || null
});
});
})
.then(() => {
return Sequelize.Promise.mapSeries(canCancel, transaction => {
return this.app.services.TransactionService.cancel(transaction, {
transaction: options.transaction || null
});
});
})
.then(() => {
return resOrder.resolveFulfillments({ transaction: options.transaction || null });
})
.then(() => {
canCancelFulfillment = resOrder.fulfillments.filter(fulfillment => [enums_2.FULFILLMENT_STATUS.PENDING, enums_2.FULFILLMENT_STATUS.NONE, enums_2.FULFILLMENT_STATUS.SENT].indexOf(fulfillment.status) > -1);
return Sequelize.Promise.mapSeries(canCancelFulfillment, fulfillment => {
return this.app.services.FulfillmentService.cancelFulfillment(fulfillment, { transaction: options.transaction || null });
});
})
.then(() => {
return resOrder
.cancel({ cancel_reason: reason })
.save({ transaction: options.transaction || null });
})
.then(() => {
const event = {
object_id: resOrder.id,
object: 'order',
objects: [{
order: resOrder.id
}, {
customer: resOrder.customer_id
}],
type: 'order.cancelled',
message: `Order ${resOrder.name} was cancelled`,
data: resOrder
};
return this.publish(event.type, event, {
save: true,
transaction: options.transaction || null
});
})
.then(() => {
return resOrder.sendCancelledEmail({ transaction: options.transaction || null });
})
.then(() => {
return Order.findByIdDefault(resOrder.id);
});
}
resolveToAddress(customerAddress, address) {
const Address = this.app.models.Address;
if (address && !_.isEmpty(address)) {
address = this.app.services.ProxyCartService.validateAddress(address);
return address;
}
else {
if (customerAddress instanceof Address.instance) {
return customerAddress.get({ plain: true });
}
else {
return customerAddress;
}
}
}
addTag(order, tag, options = {}) {
const Order = this.app.models['Order'];
const Tag = this.app.models['Tag'];
let resOrder, resTag;
return Order.resolve(order, { transaction: options.transaction || null })
.then(_order => {
if (!_order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
resOrder = _order;
return Tag.resolve(tag, { transaction: options.transaction || null });
})
.then(_tag => {
if (!_tag) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Tag not found');
}
resTag = _tag;
return resOrder.hasTag(resTag.id, { transaction: options.transaction || null });
})
.then(hasTag => {
if (!hasTag) {
return resOrder.addTag(resTag.id, { transaction: options.transaction || null });
}
return resOrder;
})
.then(_tag => {
return Order.findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
removeTag(order, tag, options = {}) {
let resOrder, resTag;
const Order = this.app.models['Order'];
const Tag = this.app.models['Tag'];
return Order.resolve(order, { transaction: options.transaction || null })
.then(_order => {
if (!_order) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Order not found');
}
resOrder = _order;
return Tag.resolve(tag, { transaction: options.transaction || null });
})
.then(_tag => {
if (!_tag) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Tag not found');
}
resTag = _tag;
return resOrder.hasTag(resTag.id, { transaction: options.transaction || null });
})
.then(hasTag => {
if (hasTag) {
return resOrder.removeTag(resTag.id, { transaction: options.transaction || null });
}
return resOrder;
})
.then(_tag => {
return Order.findByIdDefault(resOrder.id, { transaction: options.transaction || null });
});
}
pricingOverrides(overrides, id, admin, options = {}) {
const Order = this.app.models['Order'];
if (_.isObject(overrides) && overrides.pricing_overrides) {
overrides = overrides.pricing_overrides;
}
overrides = overrides.map(override => {
override.admin_id = override.admin_id ? override.admin_id : admin.id;
override.price = this.app.services.ProxyCartService.normalizeCurrency(parseInt(override.price, 10));
return override;
});
let resOrder;
return Order.resolve(id, { transaction: options.transaction || null })
.then(_order => {
if (!_order) {
throw new Error('Order could not be resolved');
}
if ([enums_3.ORDER_STATUS.OPEN, enums_3.ORDER_STATUS.DRAFT].indexOf(_order.status) === -1) {
throw new Error(`Order is already ${_order.status}`);
}
resOrder = _order;
resOrder.pricing_overrides = overrides;
resOrder.pricing_override_id = admin.id;
return resOrder.save({ transaction: options.transaction || null });
})
.then(createdItem => {
return resOrder.recalculate({ transaction: options.transaction || null });
})
.then(() => {
const event = {
object_id: resOrder.id,
object: 'order',
objects: [{
order: resOrder.id
}],
type: 'order.pricingOverride',
message: `Order ${resOrder.name} pricing overrides updated`,
data: resOrder
};
return this.publish(event.type, event, {
save: true,
transaction: options.transaction || null
});
})
.then(event => {
return resOrder;
});
}
addItem(order, item, options = {}) {
if (!item) {
throw new errors_1.ModelError('E_NOT_FOUND', 'Item is not defined');
}
options.include = options.include || [];
if (!options.include.some(include => include.model === this.app.models['OrderItem'].instance)) {
options.include.push({
model: this.app.models['OrderItem'].instance,
as: 'order_items'
});
}
if (!options.include.some(include => include.model === this.app.models['Fulfillment'].instance)) {
options.include.push({
model: this.app.models['Fulfillment'].instance,
as: 'fulfillments'
});
}
if (!options.include.some(include => include.model === this.app.models['Transaction'].instance)) {