UNPKG

@fabrix/spool-cart

Version:

Spool - eCommerce Spool for Fabrix

435 lines (434 loc) 20.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const common_1 = require("@fabrix/fabrix/dist/common"); const lodash_1 = require("lodash"); const errors_1 = require("@fabrix/spool-sequelize/dist/errors"); const enums_1 = require("../../enums"); const enums_2 = require("../../enums"); const moment = require("moment"); class TransactionService 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(transaction, options = {}) { const Transaction = this.app.models.Transaction; return Transaction.create(transaction, options); } authorize(transaction, options = {}) { const Transaction = this.app.models['Transaction']; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction not found'); } return this.app.services.PaymentService.authorize(_transaction, { transaction: options.transaction || null }); }); } capture(transaction, options = {}) { const Transaction = this.app.models['Transaction']; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction not found'); } if (_transaction.kind !== enums_2.TRANSACTION_KIND.AUTHORIZE) { throw new errors_1.ModelError('E_NOT_FOUND', `Transaction must be first be ${enums_2.TRANSACTION_KIND.AUTHORIZE} to ${enums_2.TRANSACTION_KIND.CAPTURE}`); } return this.app.services.PaymentService.capture(_transaction, { transaction: options.transaction || null }); }); } sale(transaction, options = {}) { const Transaction = this.app.models['Transaction']; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction not found'); } return this.app.services.PaymentService.sale(_transaction, { transaction: options.transaction || null }); }); } void(transaction, options = {}) { const Transaction = this.app.models['Transaction']; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction not found'); } if (_transaction.status !== enums_1.TRANSACTION_STATUS.SUCCESS) { throw new Error('Transaction must have successful to be refunded'); } return this.app.services.PaymentService.void(_transaction, { transaction: options.transaction || null }); }); } partiallyVoid(transaction, amount, options = {}) { const Transaction = this.app.models['Transaction']; let resTransaction; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction Not Found'); } if (_transaction.status !== enums_1.TRANSACTION_STATUS.SUCCESS) { throw new Error('Transaction must have successful to be voided'); } if (_transaction.kind !== enums_2.TRANSACTION_KIND.AUTHORIZE) { throw new Error(`Transaction must be ${enums_2.TRANSACTION_KIND.AUTHORIZE} to be partially voided`); } resTransaction = _transaction; resTransaction.amount = Math.max(0, resTransaction.amount - amount); return resTransaction.save({ transaction: options.transaction || null }); }); } refund(transaction, options = {}) { const Transaction = this.app.models['Transaction']; let resTransaction; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction not found'); } if (_transaction.status !== enums_1.TRANSACTION_STATUS.SUCCESS) { throw new Error('Transaction must have been successful to be refunded'); } if ([enums_2.TRANSACTION_KIND.CAPTURE, enums_2.TRANSACTION_KIND.SALE].indexOf(_transaction.kind) === -1) { throw new Error(`Only Transactions that are ${enums_2.TRANSACTION_KIND.CAPTURE} or ${enums_2.TRANSACTION_KIND.SALE} can be refunded`); } resTransaction = _transaction; return this.app.services.PaymentService.refund(resTransaction, { transaction: options.transaction || null }); }); } partiallyRefund(transaction, amount, options = {}) { const Transaction = this.app.models['Transaction']; let resTransaction; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction Not Found'); } if (!(_transaction instanceof Transaction.instance)) { throw new Error('Transaction did not resolve an instance'); } if (_transaction.status !== enums_1.TRANSACTION_STATUS.SUCCESS) { throw new Error('Transaction must have been successful to be refunded'); } if ([enums_2.TRANSACTION_KIND.CAPTURE, enums_2.TRANSACTION_KIND.SALE].indexOf(_transaction.kind) === -1) { throw new Error(`Only Transactions that are ${enums_2.TRANSACTION_KIND.CAPTURE} or ${enums_2.TRANSACTION_KIND.SALE} can be refunded`); } resTransaction = _transaction; resTransaction.amount = Math.max(0, resTransaction.amount - amount); return resTransaction.save({ transaction: options.transaction || null }); }) .then(() => { const newTransaction = lodash_1.omit(resTransaction.get({ plain: true }), ['id', 'token']); newTransaction.amount = amount; return this.create(newTransaction, { transaction: options.transaction || null }); }) .then(newTransaction => { return this.app.services.PaymentService.refund(newTransaction, { transaction: options.transaction || null }); }); } cancel(transaction, options = {}) { const Transaction = this.app.models['Transaction']; let resTransaction; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction Not Found'); } if (!(_transaction instanceof Transaction.instance)) { throw new Error('Transaction did not resolve an instance'); } if ([enums_1.TRANSACTION_STATUS.PENDING, enums_1.TRANSACTION_STATUS.FAILURE].indexOf(_transaction.status) === -1) { throw new Error('Transaction can not be cancelled if it is not pending or failed'); } resTransaction = _transaction; return this.app.services.PaymentService.cancel(resTransaction, { transaction: options.transaction || null }); }); } retry(transaction, options = {}) { const Transaction = this.app.models['Transaction']; let resTransaction; return Transaction.resolve(transaction, { transaction: options.transaction || null }) .then(_transaction => { if (!_transaction) { throw new errors_1.ModelError('E_NOT_FOUND', 'Transaction Not Found'); } if (!(_transaction instanceof Transaction.instance)) { throw new Error('Transaction did not resolve an instance'); } if ([enums_1.TRANSACTION_STATUS.PENDING, enums_1.TRANSACTION_STATUS.FAILURE].indexOf(_transaction.status) === -1) { throw new Error('Transaction can not be tried if it is not pending or has not failed'); } resTransaction = _transaction; return this.app.services.PaymentService.retry(resTransaction, { transaction: options.transaction || null }); }); } reconcileCreate(order, amount, options = {}) { const Order = this.app.models['Order']; const Transaction = this.app.models['Transaction']; const Customer = this.app.models['Customer']; let resOrder, totalNew = 0, availablePending = []; return Order.resolve(order, { transaction: options.transaction || null }) .then(_order => { if (!_order) { throw new errors_1.ModelError('E_NOT_FOUND', 'Order Not Found'); } if (!(_order instanceof Order.instance)) { throw new Error('Order did not resolve an instance'); } resOrder = _order; return resOrder.resolveTransactions({ transaction: options.transaction || null }); }) .then(() => { return resOrder.resolveCustomer({ transaction: options.transaction || null }); }) .then(() => { totalNew = amount; availablePending = resOrder.transactions.filter(transaction => transaction.status === enums_1.TRANSACTION_STATUS.PENDING && [enums_2.TRANSACTION_KIND.AUTHORIZE, enums_2.TRANSACTION_KIND.SALE].indexOf(transaction.kind) > -1); if (availablePending.length > 0) { availablePending[0].amount = availablePending[0].amount + totalNew; return availablePending[0].save({ hooks: false, transaction: options.transaction || null }); } else { if (!(resOrder.Customer instanceof Customer.instance)) { return null; } else { return resOrder.Customer.getDefaultSource({ transaction: options.transaction || null }) .then(resSource => { if (!resSource) { throw new Error('Order Customer is missing a default source'); } const transaction = Transaction.build({ customer_id: resOrder.customer_id, order_id: resOrder.id, source_id: resSource ? resSource.id : null, account_id: resSource ? resSource.account_id : null, currency: resOrder.currency, amount: totalNew, payment_details: { gateway: resSource ? resSource.gateway : null, source: resSource, }, gateway: resSource.gateway, device_id: resSource ? resSource.device_id : null, kind: resOrder.payment_kind, description: `Order ${resOrder.name} original transaction ${resOrder.payment_kind}` }); return this.app.services.PaymentService[resOrder.transaction_kind](transaction, { hooks: false, transaction: options.transaction || null }); }) .then(transaction => { if (transaction) { const transactions = resOrder.transactions.concat(transaction); resOrder.transactions = transactions; resOrder.setDataValue('transactions', transactions); resOrder.set('transactions', transactions); } return transaction; }); } } }) .then(() => { return resOrder; }); } reconcileUpdate(order, amount, options = {}) { const Order = this.app.models['Order']; let resOrder, totalNew = 0, availablePending = [], availableAuthorized = [], availableRefund = [], toUpdate = []; return Order.resolve(order, { transaction: options.transaction || null }) .then(_order => { if (!_order) { throw new errors_1.ModelError('E_NOT_FOUND', 'Order Not Found'); } if (!(_order instanceof Order.instance)) { throw new Error('Order did not resolve an instance'); } resOrder = _order; return resOrder.resolveTransactions({ transaction: options.transaction || null }); }) .then(() => { totalNew = amount; availablePending = resOrder.transactions.filter(transaction => transaction.status === enums_1.TRANSACTION_STATUS.PENDING && [enums_2.TRANSACTION_KIND.SALE, enums_2.TRANSACTION_KIND.CAPTURE, enums_2.TRANSACTION_KIND.AUTHORIZE].indexOf(transaction.kind) > -1) .sort((a, b) => { return b.amount - a.amount; }); availableAuthorized = resOrder.transactions.filter(transaction => transaction.status === enums_1.TRANSACTION_STATUS.SUCCESS && [enums_2.TRANSACTION_KIND.AUTHORIZE].indexOf(transaction.kind) > -1) .sort((a, b) => { return b.amount - a.amount; }); availableRefund = resOrder.transactions.filter(transaction => transaction.status === enums_1.TRANSACTION_STATUS.SUCCESS && [enums_2.TRANSACTION_KIND.CAPTURE, enums_2.TRANSACTION_KIND.SALE].indexOf(transaction.kind) > -1) .sort((a, b) => { return b.amount - a.amount; }); availablePending.forEach(transaction => { if (totalNew > 0) { const oldAmount = transaction.amount; const newAmount = Math.max(0, transaction.amount - totalNew); totalNew = totalNew - oldAmount; transaction.amount = newAmount; toUpdate.push(transaction.save({ hooks: false, transaction: options.transaction || null })); } }); availableAuthorized.forEach(transaction => { if (totalNew > 0) { const oldAmount = transaction.amount; const newAmount = Math.max(0, transaction.amount - totalNew); totalNew = totalNew - oldAmount; toUpdate.push(this.partiallyVoid(transaction, oldAmount - newAmount, { hooks: false, transaction: options.transaction || null })); } }); availableRefund.forEach(transaction => { if (totalNew > 0) { const oldAmount = transaction.amount; const newAmount = Math.max(0, transaction.amount - totalNew); totalNew = totalNew - oldAmount; toUpdate.push(this.partiallyRefund(transaction, oldAmount - newAmount, { hooks: false, transaction: options.transaction || null })); } }); return Order.sequelize.Promise.mapSeries(toUpdate, update => { return update; }); }) .then(() => { return resOrder; }); } retryThisHour() { const Transaction = this.app.models['Transaction']; const start = moment().startOf('hour'); const errors = []; let transactionsTotal = 0; this.app.log.debug('TransactionService.retryThisHour', start.format('YYYY-MM-DD HH:mm:ss')); return Transaction.batch({ where: { retry_at: { $or: { $lte: start.format('YYYY-MM-DD HH:mm:ss'), $eq: null } }, total_retry_attempts: { $gte: 0, $lte: this.app.config.get('cart.transactions.retry_attempts') || 1 }, status: enums_1.TRANSACTION_STATUS.FAILURE }, regressive: true }, (transactions) => { const Sequelize = Transaction.sequelize; return Sequelize.Promise.mapSeries(transactions, transaction => { return this.retry(transaction); }) .then(results => { transactionsTotal = transactionsTotal + results.length; return; }) .catch(err => { this.app.log.error(err); errors.push(err); return; }); }) .then(transactions => { const results = { transactions: transactionsTotal, errors: errors }; this.app.log.info(results); this.app.services.EventsService.publish('transactions.retry.complete', results); return results; }) .catch(err => { this.app.log.error(err); return; }); } cancelThisHour() { const Transaction = this.app.models['Transaction']; const errors = []; const start = moment().startOf('hour') .subtract(this.app.config.get('cart.transactions.authorization_exp_days') || 0, 'days'); let transactionsTotal = 0; this.app.log.debug('TransactionService.cancelThisHour', start.format('YYYY-MM-DD HH:mm:ss')); return Transaction.batch({ where: { authorization_exp: { $gte: start.format('YYYY-MM-DD HH:mm:ss') }, total_retry_attempts: { $gte: this.app.config.get('cart.transactions.retry_attempts') || 1 }, status: enums_1.TRANSACTION_STATUS.FAILURE }, regressive: true }, (transactions) => { const Sequelize = Transaction.sequelize; return Sequelize.Promise.mapSeries(transactions, transaction => { return this.cancel(transaction); }) .then(results => { transactionsTotal = transactionsTotal + results.length; return; }) .catch(err => { this.app.log.error(err); errors.push(err); return; }); }) .then(transactions => { const results = { transactions: transactionsTotal, errors: errors }; this.app.log.info(results); this.app.services.EventsService.publish('transactions.cancel.complete', results); return results; }) .catch(err => { this.app.log.error(err); return; }); } afterCreate(transaction, options = {}) { return transaction.reconcileOrderFinancialStatus(options) .catch(err => { this.app.log.error(err); return transaction; }); } afterUpdate(transaction, options = {}) { return transaction.reconcileOrderFinancialStatus(options) .catch(err => { this.app.log.error(err); return transaction; }); } } exports.TransactionService = TransactionService;