UNPKG

trailpack-proxy-cart

Version:

eCommerce - Trailpack for Proxy Engine

1,400 lines (1,354 loc) 74.9 kB
/* eslint no-console: [0] */ 'use strict' const Service = require('trails/service') const _ = require('lodash') const Errors = require('proxy-engine-errors') const PAYMENT_PROCESSING_METHOD = require('../../lib').Enums.PAYMENT_PROCESSING_METHOD const FULFILLMENT_STATUS = require('../../lib').Enums.FULFILLMENT_STATUS const ORDER_STATUS = require('../../lib').Enums.ORDER_STATUS const ORDER_FULFILLMENT = require('../../lib').Enums.ORDER_FULFILLMENT // const PAYMENT_KIND = require('../../lib').Enums.PAYMENT_KIND // const orders.fulfillment_kind = require('../../lib').Enums.orders.fulfillment_kind const TRANSACTION_STATUS = require('../../lib').Enums.TRANSACTION_STATUS const TRANSACTION_KIND = require('../../lib').Enums.TRANSACTION_KIND const ORDER_FINANCIAL = require('../../lib').Enums.ORDER_FINANCIAL const ORDER_CANCEL = require('../../lib').Enums.ORDER_CANCEL /** * @module OrderService * @description Order Service */ module.exports = class OrderService extends Service { /** * Creates an Order * @param obj * @returns {Promise} */ // TODO Select Vendor if not selected per order_item // TODO handle inventory policy and coupon policy on fulfillments create(obj, options) { options = options || {} const Address = this.app.orm['Address'] const Customer = this.app.orm['Customer'] const Order = this.app.orm['Order'] const OrderItem = this.app.orm['OrderItem'] // const Transaction = this.app.orm['Transaction'] // const Fulfillment = this.app.orm['Fulfillment'] // const PaymentService = this.app.services.PaymentService // Set the initial total amount due for this order let totalDue = obj.total_due let totalPrice = obj.total_price let totalOverrides = 0 let deduction = 0 let resOrder = {} let resCustomer = {} let resBillingAddress = {} let resShippingAddress = {} // Validate obj cart if (!obj.cart_token && !obj.subscription_token) { const err = new Errors.FoundError(Error('Missing a Cart token or a Subscription token')) return Promise.reject(err) } // Validate payment details if (!obj.payment_details) { const err = new Errors.FoundError(Error('Missing Payment Details')) return Promise.reject(err) } // Reconcile some shipping if one of the values is missing 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 Order.sequelize.transaction(t => { return Customer.resolve(obj.customer_id || obj.customer, { include: [ { model: Address, as: 'shipping_address' }, { model: Address, as: 'billing_address' }, { model: Address, as: 'default_address' } ] }) .then(customer => { // The customer exists, has a default address, but no shipping address if (customer && customer.default_address && !customer.shipping_address) { customer.shipping_address = customer.default_address } // The customer exists, has a default address, but no billing address if (customer && customer.default_address && !customer.billing_address) { customer.billing_address = customer.default_address } // The customer exist, the order requires shipping, but no shipping information if (customer && !customer.shipping_address && !obj.shipping_address && obj.has_shipping) { throw new Errors.FoundError(Error(`Could not find customer shipping address for id '${obj.customer_id}'`)) } // The customer exist, the order requires shipping, but no billing information if (customer && !customer.billing_address && !obj.billing_address && obj.has_shipping) { throw new Errors.FoundError(Error(`Could not find customer billing address for id '${obj.customer_id}'`)) } // Set a blank customer object if there isn't one for this order if (!customer) { resCustomer = { id: null, email: null, account_balance: 0, billing_address: null, shipping_address: null } } // Return this resolved customer else { resCustomer = customer } // Resolve the Billing Address resBillingAddress = this.resolveToAddress(resCustomer.billing_address, obj.billing_address) // Resolve the Shipping 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 not payment_details, make blank array if (!obj.payment_details){ obj.payment_details = [] } // If not pricing_overrides, make blank array if (!obj.pricing_overrides) { obj.pricing_overrides = [] } // Map the gateway names being used const paymentGatewayNames = obj.payment_details.map(detail => { return detail.gateway }) const accountBalanceIndex = _.findIndex(obj.pricing_overrides, {name: 'Account Balance'}) // Account balance has been applied, check to update it. if (accountBalanceIndex > -1) { const prevPrice = obj.pricing_overrides[accountBalanceIndex].price // If account balance is present, revert it so it can be added back in with current. totalDue = totalDue + prevPrice totalPrice = totalPrice + prevPrice } // Add the account balance to the overrides 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) // Apply Customer Account balance deduction = Math.min(deductibleTotal, (deductibleTotal - (deductibleTotal - resCustomer.account_balance))) if (deduction > 0) { // If account balance has not been applied if (accountBalanceIndex === -1) { obj.pricing_overrides.push({ name: 'Account Balance', price: deduction }) totalDue = Math.max(0, totalDue - deduction) totalPrice = Math.max(0, totalPrice - deduction) } // Otherwise update the account balance else { // const prevPrice = obj.pricing_overrides[accountBalanceIndex].price obj.pricing_overrides[accountBalanceIndex].price = deduction totalDue = Math.max(0, totalDue - deduction) totalPrice = Math.max(0, totalPrice - deduction) } // Recalculate Overrides _.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) } } // obj.line_items = obj.line_items.map(item => { // return OrderItem.build(item) // }) // Create the blank fulfillments let fulfillments = _.groupBy(obj.line_items, 'fulfillment_service') // Map into array fulfillments = _.map(fulfillments, (items, service) => { // return Fulfillment.build({ return { service: service, total_items: items.length, total_pending_fulfillments: items.length } // }) }) // Make sure all order items are given the customer id const lineItems = obj.line_items.map(item => { item.customer_id = resCustomer.id || null return item }) const order = Order.build({ // Order Info processing_method: obj.processing_method || PAYMENT_PROCESSING_METHOD.DIRECT, processed_at: new Date(), // Cart/Subscription Info 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, // Types fulfillment_kind: obj.fulfillment_kind || this.app.config.get('proxyCart.orders.fulfillment_kind'), payment_kind: obj.payment_kind || this.app.config.get('proxyCart.orders.payment_kind'), transaction_kind: obj.transaction_kind || this.app.config.get('proxyCart.orders.transaction_kind') || TRANSACTION_KIND.AUTHORIZE, // Gateway payment_gateway_names: paymentGatewayNames, // Client Info client_details: obj.client_details, ip: obj.ip, // Customer Info customer_id: resCustomer.id, // (May Be Null) buyer_accepts_marketing: resCustomer.accepts_marketing || obj.buyer_accepts_marketing, billing_address: resBillingAddress, shipping_address: resShippingAddress, // User Info user_id: obj.user_id || null, // Overrides pricing_override_id: obj.pricing_override_id || null, pricing_overrides: obj.pricing_overrides || [], total_overrides: obj.total_overrides || 0, // Notes notes: obj.notes || null, // Fulfillments fulfillments: fulfillments, total_pending_fulfillments: fulfillments.length }, { include: [ // { // model: this.app.orm['Customer'] // }, { model: OrderItem, as: 'order_items' }, { model: this.app.orm['Fulfillment'], as: 'fulfillments', include: [ { model: OrderItem, as: 'order_items' } ] }, { model: this.app.orm['Transaction'], as: 'transactions' } ] }) return order.save({transaction: options.transaction || null}) }) .then(order => { if (!order) { throw new Error('Unexpected Error while creating order') } resOrder = order if (resCustomer instanceof Customer && deduction > 0) { return resCustomer.logAccountBalance( 'debit', deduction, resOrder.currency, null, resOrder.id, {transaction: options.transaction || null} ) } else { return } }) .then(() => { if (resCustomer instanceof Customer) { return resCustomer .setTotalSpent(totalPrice) .setLastOrder(resOrder) .setTotalOrders() .setAvgSpent() .save({transaction: options.transaction || null}) } else { return } }) .then(() => { // Group fulfillment by service return resOrder.groupFulfillments({transaction: options.transaction || null}) }) .then(() => { // Group Transactions by Payment Gateway return resOrder.groupTransactions(obj.payment_details, {transaction: options.transaction || null}) }) .then(() => { // Reload to the freshest copy after all the events return resOrder.reload({transaction: options.transaction || null}) }) .then(() => { // Save the status changes return resOrder.saveStatus({transaction: options.transaction || null}) }) .then(() => { if (resOrder.discounted_lines.length > 0) { return resOrder.logDiscountUsage({transaction: options.transaction || null}) } else { return } }) .then(() => { // TODO REMOVE THIS PART WHEN WE CREATE THE EVENT ELSEWHERE if (resCustomer instanceof Customer) { 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.app.services.ProxyEngineService.publish(event.type, event, { save: true, transaction: options.transaction || null }) }) } else { return } }) .then(() => { // Load default return Order.findByIdDefault(resOrder.id, {transaction: options.transaction || null}) }) // }) } /** * * @param order * @param options * @returns {Promise.<T>} */ update(order, options) { options = options || {} const Order = this.app.orm.Order let resOrder return Order.resolve(order, options) .then(_order => { if (!_order) { throw new Error('Order not found') } resOrder = _order if ([FULFILLMENT_STATUS.PENDING, FULFILLMENT_STATUS.NONE, 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.save({transaction: options.transaction || null}) return resOrder.recalculate({transaction: options.transaction || null}) }) .then(() => { return resOrder.sendUpdatedEmail({transaction: options.transaction || null}) }) .then(resOrder => { return Order.findByIdDefault(resOrder.id) }) } /** * Pay an item * @param order * @param paymentDetails * @param options * @returns {*|Promise.<T>} */ // TODO handle payment of remaining balance if provided pay(order, paymentDetails, options) { options = options || {} const Order = this.app.orm['Order'] const Sequelize = Order.sequelize let resOrder return Order.resolve(order, options) .then(_order => { if (!_order) { throw new Errors.FoundError(Error('Order not found')) } if (_order.financial_status !== (ORDER_FINANCIAL.AUTHORIZED || ORDER_FINANCIAL.PARTIALLY_PAID)) { throw new Error(`Order status is ${_order.financial_status} not '${ORDER_FINANCIAL.AUTHORIZED} or ${ORDER_FINANCIAL.PARTIALLY_PAID}'`) } resOrder = _order return resOrder.resolveTransactions({transaction: options.transaction || null}) }) .then(() => { const authorized = resOrder.transactions.filter(transaction => transaction.kind == 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.app.services.ProxyEngineService.publish(event.type, event, { save: true, transaction: options.transaction || null }) }) .then((event) => { if (resOrder.financial_status === ORDER_FINANCIAL.PAID && resOrder.customer_id) { return resOrder.sendPaidEmail({transaciton: options.transaction || null }) } else { return } }) .then((notifications) => { return this.app.orm['Order'].findByIdDefault(resOrder.id, {transaction: options.transaction || null}) }) } /** * Pay multiple orders * @param orders * @param options * @returns {Promise.<*>} */ payOrders(orders, options) { options = options || {} const Sequelize = this.app.orm['Order'].sequelize return Sequelize.Promise.mapSeries(orders, order => { return this.pay(order, {transaction: options.transaction || null}) }) } /** * * @param orderItem * @param options * @returns {Promise.<TResult>} */ refundOrderItem(orderItem, options) { options = options || {} const OrderItem = this.app.orm['OrderItem'] const Order = this.app.orm['Order'] const Refund = this.app.orm['Refund'] let resOrderItem, resOrder return OrderItem.resolve(orderItem, {transaction: options.transaction || null}) .then(orderItem => { if (!orderItem) { throw new Errors.FoundError(Error('OrderItem not found')) } resOrderItem = orderItem return resOrderItem }) .then(() => { return resOrderItem.getOrder({transaction: options.transaction || null}) }) .then(order => { if (!order) { throw new Errors.FoundError('Order not found') } const allowedStatuses = [ ORDER_FINANCIAL.PAID, ORDER_FINANCIAL.PARTIALLY_PAID, ORDER_FINANCIAL.PARTIALLY_REFUNDED ] if (allowedStatuses.indexOf(order.financial_status) === -1) { throw new Error( `Order status is ${order.financial_status} not '${ORDER_FINANCIAL.PAID}, ${ORDER_FINANCIAL.PARTIALLY_PAID}' or '${ORDER_FINANCIAL.PARTIALLY_REFUNDED}'` ) } // Bind DAO resOrder = order // Resolve transactions in case they aren't added yet return resOrder.resolveTransactions({transaction: options.transaction || null}) }) .then(() => { const canRefund = resOrder.transactions.filter(transaction => { return [TRANSACTION_KIND.SALE, TRANSACTION_KIND.CAPTURE].indexOf(transaction.kind) > -1 }) // TODO, refund multiple transactions is necessary const toRefund = canRefund.find(transaction => transaction.amount >= resOrderItem.calculated_price) if (!toRefund) { // TODO CREATE PROPER ERROR 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 === TRANSACTION_KIND.REFUND && transaction.status === 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 => { // Resolve the refunds now that it's been added return resOrder.resolveRefunds({transaction: options.transaction || null}) }) .then(() => { return resOrder.saveFinancialStatus({transaction: options.transaction || null}) }) .then(order => { return Order.findByIdDefault(resOrder.id) }) } /** * Refund an Order or Partially Refund an Order * @param order * @param refunds * @param options * @returns {*|Promise.<TResult>} */ // TODO restock refund(order, refunds, options) { refunds = refunds || [] options = options || {} const Order = this.app.orm['Order'] const Sequelize = Order.sequelize let resOrder return Order.resolve(order, options) .then(order => { if (!order) { throw new Errors.FoundError(Error('Order not found')) } const allowedStatuses = [ORDER_FINANCIAL.PAID, ORDER_FINANCIAL.PARTIALLY_PAID, ORDER_FINANCIAL.PARTIALLY_REFUNDED] if (allowedStatuses.indexOf(order.financial_status) === -1) { throw new Error( `Order status is ${order.financial_status} not '${ORDER_FINANCIAL.PAID}, ${ORDER_FINANCIAL.PARTIALLY_PAID}' or '${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(() => { // Partially Refund because refunds was sent to method if (refunds.length > 0) { return Sequelize.Promise.mapSeries(refunds, refund => { const refundTransaction = resOrder.transactions.find(transaction => transaction.id == refund.transaction) if ( [TRANSACTION_KIND.SALE, TRANSACTION_KIND.CAPTURE].indexOf(refundTransaction.kind) > -1 && refundTransaction.status === TRANSACTION_STATUS.SUCCESS ) { // If this is a full Transaction refund if (refund.amount === refundTransaction.amount) { return this.app.services.TransactionService.refund(refundTransaction, { transaction: options.transaction || null }) } // If this is a partial refund else { return this.app.services.TransactionService.partiallyRefund(refundTransaction, refund.amount, { transaction: options.transaction || null }) } } }) } // Completely Refund the order else { const canRefund = resOrder.transactions.filter(transaction => { if ( [TRANSACTION_KIND.SALE, TRANSACTION_KIND.CAPTURE].indexOf(transaction.kind) > -1 && transaction.status === TRANSACTION_STATUS.SUCCESS ) { return transaction } }) return Sequelize.Promise.mapSeries(canRefund, transaction => { return this.app.services.TransactionService.refund( transaction, { transaction: options.transaction || null } ) }) } }) .then(refundedTransactions => { // Filter the successes const newRefunds = refundedTransactions.filter(transaction => transaction.kind === TRANSACTION_KIND.REFUND && transaction.status === TRANSACTION_STATUS.SUCCESS) // Create the refunds 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({ 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.app.services.ProxyEngineService.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}) }) } /** * * @param order * @param authorizations * @param options * @returns {Promise.<T>} */ authorize(order, authorizations, options) { authorizations = authorizations || [] options = options || {} const Order = this.app.orm['Order'] let resOrder return Order.resolve(order, { transaction: options.transaction || null }) .then(_order => { if (!_order) { throw new Errors.FoundError(Error('Order not found')) } resOrder = _order return resOrder.resolveTransactions({ transaction: options.transaction || null }) }) .then(() => { // Partially Authorize if (authorizations.length > 0) { // Filter the authorizations const toAuthorize = authorizations.map(authorize => { const authorizeTransaction = resOrder.transactions.find(transaction => transaction.id === authorize.transaction) if ( authorizeTransaction && authorizeTransaction.kind === TRANSACTION_KIND.AUTHORIZE && authorizeTransaction.status === TRANSACTION_STATUS.PENDING ) { return authorizeTransaction } }).filter(n => n) // Authorize the pending transactions return Order.sequelize.Promise.mapSeries(toAuthorize, transaction => { return this.app.services.TransactionService.authorize( transaction, {transaction: options.transaction || null} ) }) } // Completely Authorize the order else { const canAuthorize = resOrder.transactions.filter(transaction => { if ( transaction.kind === TRANSACTION_KIND.AUTHORIZE && transaction.status === 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(order => { return Order.findByIdDefault(resOrder.id, {transaction: options.transaction || null}) }) } /** * * @param order * @param captures * @param options * @returns {Promise.<TResult>} */ capture(order, captures, options) { captures = captures || [] options = options || {} const Order = this.app.orm['Order'] const Sequelize = Order.sequelize let resOrder return Order.resolve(order, options) .then(_order => { if (!_order) { throw new Errors.FoundError(Error('Order not found')) } resOrder = _order return resOrder.resolveTransactions({transaction: options.transaction || null}) }) .then(() => { // Partially Capture if (captures.length > 0) { // Filter the captures const toCapture = captures.map(capture => { const captureTransaction = resOrder.transactions.find(transaction => transaction.id === capture.transaction) if ( captureTransaction && captureTransaction.kind === TRANSACTION_KIND.AUTHORIZE && captureTransaction.status === TRANSACTION_STATUS.SUCCESS ) { return captureTransaction } }).filter(n => n) // Capture the authorized transactions return Sequelize.Promise.mapSeries(toCapture, transaction => { return this.app.services.TransactionService.capture( transaction, {transaction: options.transaction || null} ) }) } // Completely Capture the order else { const canCapture = resOrder.transactions.filter(transaction => { if ( transaction.kind === TRANSACTION_KIND.AUTHORIZE && transaction.status === 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 }) }) } /** * * @param order * @param voids * @param options * @returns {Promise.<TResult>} */ void(order, voids, options) { options = options || {} voids = voids || [] const Order = this.app.orm['Order'] const Sequelize = Order.sequelize let resOrder return Order.resolve(order, options) .then(_order => { if (!_order) { throw new Errors.FoundError(Error('Order not found')) } resOrder = _order return resOrder.resolveTransactions({ transaction: options.transaction || null }) }) .then(() => { // Partially Void if (voids.length > 0) { // Filter the voids const toVoid = voids.map(tVoid => { const voidTransaction = resOrder.transactions.find(transaction => transaction.id === tVoid.transaction) if ( voidTransaction && voidTransaction.kind === TRANSACTION_KIND.AUTHORIZE && voidTransaction.status === TRANSACTION_STATUS.SUCCESS ) { return voidTransaction } }).filter(n => n) // Void the authorized transactions return Sequelize.Promise.mapSeries(toVoid, transaction => { return this.app.services.TransactionService.void( transaction, {transaction: options.transaction || null} ) }) } // Completely Void the order else { const canVoid = resOrder.transactions.filter(transaction => { if ( transaction.kind === TRANSACTION_KIND.AUTHORIZE && transaction.status === 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 }) }) } /** * * @param order * @param retries * @param options * @returns {Promise.<T>} */ retry(order, retries, options) { retries = retries || [] options = options || {} const Order = this.app.orm['Order'] const Sequelize = Order.sequelize let resOrder return Order.resolve(order, options) .then(_order => { if (!_order) { throw new Errors.FoundError(Error('Order not found')) } resOrder = _order return resOrder.resolveTransactions({ transaction: options.transaction || null }) }) .then(() => { // Partially retry if (retries.length > 0) { const toRetry = retries.map(tRetry => { const retryTransaction = resOrder.transactions.find(transaction => transaction.id === tRetry.transaction) if ( retryTransaction && [TRANSACTION_STATUS.FAILURE, TRANSACTION_STATUS.PENDING].indexOf(retryTransaction.status) !== -1 ) { return retryTransaction } }).filter(n => n) // Retry the authorized transactions return Sequelize.Promise.mapSeries(toRetry, transaction => { return this.app.services.TransactionService.retry( transaction, {transaction: options.transaction || null} ) }) } // Completely retry the order else { const canRetry = resOrder.transactions.filter(transaction => { if ([TRANSACTION_STATUS.FAILURE, 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 === ORDER_FINANCIAL.PAID) { return resOrder.sendPaidEmail({transaction: options.transaction || null}) } else if (resOrder.financial_status === ORDER_FINANCIAL.PARTIALLY_PAID) { return resOrder.sendPartiallyPaidEmail({transaction: options.transaction || null}) } return }) .then(() => { return Order.findByIdDefault(resOrder.id, {transaction: options.transaction || null}) }) } /** * Cancel an Order * @param order * @param options * @returns {Promise.<TResult>} */ cancel(order, options) { options = options || {} const Order = this.app.orm['Order'] const Sequelize = Order.sequelize const reason = order.cancel_reason || ORDER_CANCEL.OTHER let resOrder, canRefund = [], canVoid = [], canCancel = [], canCancelFulfillment = [] return Order.resolve(order, options) .then(_order => { if (!_order) { throw new Error('Order not found') } resOrder = _order if ([ORDER_FULFILLMENT.NONE, ORDER_FULFILLMENT.PENDING, 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 '${ORDER_FULFILLMENT.NONE}', '${ORDER_FULFILLMENT.PENDING}', '${ORDER_FULFILLMENT.SENT}'`) } return resOrder.resolveTransactions({ transaction: options.transaction || null }) }) .then(() => { // Transactions that can be refunded canRefund = resOrder.transactions.filter(transaction => [TRANSACTION_KIND.SALE, TRANSACTION_KIND.CAPTURE].indexOf(transaction.kind) > -1 && transaction.status === TRANSACTION_STATUS.SUCCESS ) // Transactions that can be voided canVoid = resOrder.transactions.filter(transaction => transaction.kind === TRANSACTION_KIND.AUTHORIZE && transaction.status === TRANSACTION_STATUS.SUCCESS ) // Transactions that can be cancelled canCancel = resOrder.transactions.filter(transaction => transaction.status === TRANSACTION_STATUS.PENDING) // Start Refunds return Sequelize.Promise.mapSeries(canRefund, transaction => { return this.app.services.TransactionService.refund(transaction, { transaction: options.transaction || null }) }) }) .then(() => { // Start Voids return Sequelize.Promise.mapSeries(canVoid, transaction => { return this.app.services.TransactionService.void(transaction, { transaction: options.transaction || null }) }) }) .then(() => { // Start Cancels 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(() => { // Start Cancel fulfillments canCancelFulfillment = resOrder.fulfillments.filter(fulfillment => [FULFILLMENT_STATUS.PENDING, FULFILLMENT_STATUS.NONE, 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(() => { // Track Event 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.app.services.ProxyEngineService.publish(event.type, event, { save: true, transaction: options.transaction || null }) }) .then(() => { return resOrder.sendCancelledEmail({transaction: options.transaction || null}) }) .then(() => { return resOrder.reload({ transaction: options.transaction || null }) // Order.findByIdDefault(resOrder.id) }) } /** * * @param customerAddress * @param address * @returns {*} */ resolveToAddress(customerAddress, address) { const Address = this.app.orm.Address if (address && !_.isEmpty(address)) { address = this.app.services.ProxyCartService.validateAddress(address) return address } else { if (customerAddress instanceof Address) { return customerAddress.get({plain: true}) } else { return customerAddress } } } /** * * @param order * @param tag * @returns {Promise.<TResult>} */ addTag(order, tag, options){ options = options || {} const Order = this.app.orm['Order'] const Tag = this.app.orm['Tag'] let resOrder, resTag return Order.resolve(order, { transaction: options.transaction || null }) .then(order => { if (!order) { throw new Errors.FoundError(Error('Order not found')) } resOrder = order return Tag.resolve(tag, { transaction: options.transaction || null }) }) .then(tag => { if (!tag) { throw new Errors.FoundError(Error('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 }) }) } /** * * @param order * @param tag * @returns {Promise.<TResult>} */ removeTag(order, tag, options){ options = options || {} let resOrder, resTag const Order = this.app.orm['Order'] const Tag = this.app.orm['Tag'] return Order.resolve(order, { transaction: options.transaction || null }) .then(order => { if (!order) { throw new Errors.FoundError(Error('Order not found')) } resOrder = order return Tag.resolve(tag, { transaction: options.transaction || null }) }) .then(tag => { if (!tag) { throw new Errors.FoundError(Error('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 }) }) } /** * * @param overrides * @param id * @param admin * @param options * @returns {Promise} */ pricingOverrides(overrides, id, admin, options){ options = options || {} const Order = this.app.orm['Order'] // Standardize the input if (_.isObject(overrides) && overrides.pricing_overrides){ overrides = overrides.pricing_overrides } overrides = overrides.map(override => { // Add the admin id to the override override.admin_id = override.admin_id ? override.admin_id : admin.id // Make sure price is a number override.price = this.app.services.ProxyCartService.normalizeCurrency(parseInt(override.price)) 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 ([ORDER_STATUS.OPEN, 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(() => { // Track Event 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.app.services.ProxyEngineService.publish(event.type, event, { save: true, transaction: options.transaction || null }) }) .then(event => { return resOrder //Order.findByIdDefault(resOrder.id) }) } /** * * @param order * @param item * @param options * @returns {Promise.<TResult>} */ addItem(order, item, options) { options = options || {} if (!item) { throw new Errors.FoundError(Error('Item is not defined')) } let resOrder, resItem const Order = this.app.orm['Order'] return Order.resolve(order, options) .then(order => { if (!order) { throw new Errors.FoundError(Error('Order not found')) } if (order.status !== ORDER_STATUS.OPEN) { throw new Error(`Order is already ${order.status} and can not be modified`) } // bind the dao resOrder = order return resOrder.resolveOrderItems({ transaction: options.transaction || null }) }) .then(() => { // Resolve the item of the new order item return this.app.services.ProductService.resolveItem(item, { transaction: options.transaction || null }) }) .then(_item => { if (!_item) { throw new Error('Could not resolve product and variant') } // Build the item resItem = resOrder.buildOrderItem(_item, item.quantity, item.properties) // Add the item return resOrder.addItem(resItem, { transaction: options.transaction || null }) }) .then(createdItem => { return resOrder.recalculate({ transaction: options.transaction || null }) }) .then(() => { // Track Event const event = { object_id: resOrder.id, object: 'order', objects: [{ order: resOrder.id }, { customer: resOrder.customer_id }, { product: resItem.product_id }, { productvariant: resItem.variant_id }], type: 'order.item.created', message: `Item added to Order ${resOrder.name}`, data: resItem } return this.app.services.ProxyEngineService.publish(event.type, event, { save: true, transaction: options.transaction || null }) }) .then(event => { return resOrder //Order.findByIdDefault(resOrder.id) }) } /** * * @param order * @param items * @param options * @returns {Promise.<TResult>} */ addItems(order, items, options) { options = options || {} if (!items) { throw new Errors.FoundError(Error('Item is not defined')) } let resOrder, resItems = [] const Order = this.app.orm['Order'] const Sequelize = this.app.orm['Product'].sequelize return Order.resolve(order, options) .then(order => { if (!order) { throw new Errors.FoundError(Error('Order not found')) } if (order.status !== ORDER_STATUS.OPEN) { throw new Error(`Order is already ${order.status} and can not be modified`) } // bind the dao resOrder = order return resOrder.resolveOrderItems({ transaction: options.transactio