@fabrix/spool-cart
Version:
Spool - eCommerce Spool for Fabrix
435 lines (434 loc) • 20.8 kB
JavaScript
"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;