@fabrix/spool-cart
Version:
Spool - eCommerce Spool for Fabrix
359 lines (358 loc) • 13 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@fabrix/fabrix/dist/common");
const spool_sequelize_1 = require("@fabrix/spool-sequelize");
const errors_1 = require("@fabrix/spool-sequelize/dist/errors");
const lodash_1 = require("lodash");
const shortId = require("shortid");
const moment = require("moment");
const enums_1 = require("../../enums");
const enums_2 = require("../../enums");
const enums_3 = require("../../enums");
const enums_4 = require("../../enums");
class TransactionResolver extends spool_sequelize_1.SequelizeResolver {
batch(options, batch) {
const self = this;
options.limit = options.limit || 100;
options.offset = options.offset || 0;
options.regressive = options.regressive || false;
const recursiveQuery = function (options) {
let count = 0;
return self.findAndCountAll(options)
.then(results => {
count = results.count;
return batch(results.rows);
})
.then(batched => {
if (count >= (options.regressive ? options.limit : options.offset + options.limit)) {
options.offset = options.regressive ? 0 : options.offset + options.limit;
return recursiveQuery(options);
}
else {
return Promise.resolve();
}
});
};
return recursiveQuery(options);
}
resolveByInstance(transaction, options = {}) {
return Promise.resolve(transaction);
}
resolveById(transaction, options = {}) {
return this.findById(transaction.id, options)
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Transaction ${transaction.id} not found`);
}
return resUser;
});
}
resolveByToken(transaction, options = {}) {
return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, {
where: {
token: transaction.token
}
}))
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Transaction token ${transaction.token} not found`);
}
return resUser;
});
}
resolveByNumber(transaction, options = {}) {
return this.findById(transaction, options)
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Transaction ${transaction.token} not found`);
}
return resUser;
});
}
resolveByString(transaction, options = {}) {
return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, {
where: {
code: transaction
}
}))
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Transaction ${transaction} not found`);
}
return resUser;
});
}
resolve(transaction, options = {}) {
const resolvers = {
'instance': transaction instanceof this.instance,
'id': !!(transaction && lodash_1.isObject(transaction) && transaction.id),
'token': !!(transaction && lodash_1.isObject(transaction) && transaction.token),
'number': !!(transaction && lodash_1.isNumber(transaction)),
'string': !!(transaction && lodash_1.isString(transaction))
};
const type = Object.keys(resolvers).find((key) => resolvers[key]);
switch (type) {
case 'instance': {
return this.resolveByInstance(transaction, options);
}
case 'id': {
return this.resolveById(transaction, options);
}
case 'token': {
return this.resolveByToken(transaction, options);
}
case 'number': {
return this.resolveByNumber(transaction, options);
}
case 'string': {
return this.resolveByString(transaction, options);
}
default: {
const err = new Error(`Unable to resolve Transaction ${transaction}`);
return Promise.reject(err);
}
}
}
}
exports.TransactionResolver = TransactionResolver;
class Transaction extends common_1.FabrixModel {
static get resolver() {
return TransactionResolver;
}
static config(app, Sequelize) {
return {
options: {
underscored: true,
enums: {
TRANSACTION_ERRORS: enums_1.TRANSACTION_ERRORS,
TRANSACTION_STATUS: enums_2.TRANSACTION_STATUS,
TRANSACTION_KIND: enums_3.TRANSACTION_KIND,
TRANSACTION_DEFAULTS: enums_4.TRANSACTION_DEFAULTS
},
description: 'A Transaction is a representation of a purchasing event.',
scopes: {
live: {
where: {
live_mode: true
}
},
authorized: {
where: {
kind: 'authorize',
status: 'success'
}
},
captured: {
where: {
kind: ['capture', 'sale'],
status: 'success'
}
},
voided: {
where: {
kind: 'void',
status: 'success'
}
},
refunded: {
where: {
kind: 'refund',
status: 'success'
}
}
},
hooks: {
beforeCreate: (transaction, options) => {
if (!transaction.token) {
transaction.token = `transaction_${shortId.generate()}`;
}
},
afterCreate: (transaction, options) => {
return app.services.TransactionService.afterCreate(transaction, options)
.catch(err => {
return Promise.reject(err);
});
},
afterUpdate: (transaction, options) => {
return app.services.TransactionService.afterUpdate(transaction, options)
.catch(err => {
return Promise.reject(err);
});
}
}
}
};
}
static schema(app, Sequelize) {
return {
token: {
type: Sequelize.STRING,
unique: true
},
customer_id: {
type: Sequelize.INTEGER,
allowNull: true
},
order_id: {
type: Sequelize.INTEGER,
allowNull: false
},
source_id: {
type: Sequelize.INTEGER,
allowNull: true
},
account_id: {
type: Sequelize.INTEGER,
allowNull: true
},
foreign_key: {
type: Sequelize.STRING
},
foreign_id: {
type: Sequelize.STRING
},
amount: {
type: Sequelize.INTEGER,
defaultValue: 0
},
amount_refunded: {
type: Sequelize.INTEGER,
defaultValue: 0
},
authorization: {
type: Sequelize.STRING
},
authorization_exp: {
type: Sequelize.DATE,
defaultValue: moment()
.subtract(app.config.get('cart.transactions.authorization_exp_days') || 0, 'days')
.format('YYYY-MM-DD HH:mm:ss')
},
device_id: {
type: Sequelize.STRING
},
gateway: {
type: Sequelize.STRING
},
source_name: {
type: Sequelize.STRING,
defaultValue: enums_4.TRANSACTION_DEFAULTS.SOURCE_NAME
},
payment_details: {
type: Sequelize.JSONB,
defaultValue: {}
},
kind: {
type: Sequelize.ENUM,
values: Object.values(enums_3.TRANSACTION_KIND),
allowNull: false
},
receipt: {
type: Sequelize.JSONB,
defaultValue: {}
},
error_code: {
type: Sequelize.ENUM,
values: Object.values(enums_1.TRANSACTION_ERRORS)
},
status: {
type: Sequelize.ENUM,
values: Object.values(enums_2.TRANSACTION_STATUS),
defaultValue: enums_2.TRANSACTION_STATUS.PENDING
},
currency: {
type: Sequelize.STRING,
defaultValue: app.config.get('cart.default_currency') || enums_4.TRANSACTION_DEFAULTS.CURRENCY
},
description: {
type: Sequelize.STRING
},
retry_at: {
type: Sequelize.DATE
},
total_retry_attempts: {
type: Sequelize.INTEGER,
defaultValue: 0
},
shop_id: {
type: Sequelize.INTEGER,
},
cancelled_at: {
type: Sequelize.DATE
},
live_mode: {
type: Sequelize.BOOLEAN,
defaultValue: app.config.get('cart.live_mode')
}
};
}
static associate(models) {
models.Transaction.belongsTo(models.Order, {});
models.Transaction.belongsTo(models.Customer, {});
models.Transaction.belongsTo(models.Account, {});
models.Transaction.belongsTo(models.Source, {});
models.Transaction.belongsTo(models.Shop, {
foreignKey: 'shop_id',
});
}
}
exports.Transaction = Transaction;
Transaction.prototype.retry = function () {
this.retry_at = new Date(Date.now());
this.total_retry_attempts++;
if (this.description && Boolean(this.description.match(/retry (\d+)/g))) {
this.description = this.description.replace(/retry (\d+)/g, `retry ${this.total_retry_attempts}`);
}
else {
this.description = `${this.description || 'transaction'} retry ${this.total_retry_attempts}`;
}
return this;
};
Transaction.prototype.cancel = function () {
this.cancelled_at = new Date(Date.now());
this.status = enums_2.TRANSACTION_STATUS.CANCELLED;
if (this.description && !this.description.includes('cancelled')) {
this.description = `${this.description || 'transaction'} cancelled`;
}
return this;
};
Transaction.prototype.resolveOrder = function (options = {}) {
const Order = this.app.models['Order'];
if (this.Order
&& this.Order instanceof Order.instance
&& options.reload !== true) {
return Promise.resolve(this);
}
else {
return this.getOrder({ transaction: options.transaction || null })
.then(order => {
order = order || null;
this.Order = order;
this.setDataValue('Order', order);
this.set('Order', order);
});
}
};
Transaction.prototype.reconcileOrderFinancialStatus = function (options = {}) {
const Order = this.app.models['Order'];
if (!this.changed('status') && !this.changed('kind')) {
return Promise.resolve(this);
}
let resOrder;
return Order.findById(this.order_id, {
transaction: options.transaction || null
})
.then(foundOrder => {
if (!foundOrder) {
throw new Error('Order could not be resolved for transaction');
}
resOrder = foundOrder;
return resOrder.saveFinancialStatus({ transaction: options.transaction || null });
})
.then(() => {
return resOrder.saveStatus({ transaction: options.transaction || null });
})
.then(() => {
return this;
});
};