UNPKG

@fabrix/spool-cart

Version:

Spool - eCommerce Spool for Fabrix

1,081 lines (1,080 loc) 78.9 kB
"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)) {