trailpack-proxy-cart
Version:
eCommerce - Trailpack for Proxy Engine
443 lines (433 loc) • 15.1 kB
JavaScript
/* eslint new-cap: [0] */
/* eslint no-console: [0] */
'use strict'
const Model = require('trails/model')
const Errors = require('proxy-engine-errors')
const helpers = require('proxy-engine-helpers')
const shortId = require('shortid')
const TRANSACTION_ERRORS = require('../../lib').Enums.TRANSACTION_ERRORS
const TRANSACTION_STATUS = require('../../lib').Enums.TRANSACTION_STATUS
const TRANSACTION_KIND = require('../../lib').Enums.TRANSACTION_KIND
const TRANSACTION_DEFAULTS = require('../../lib').Enums.TRANSACTION_DEFAUTLS
const _ = require('lodash')
const moment = require('moment')
/**
* @module Transaction
* @description Transaction Model
*/
module.exports = class Transaction extends Model {
static config (app, Sequelize) {
return {
options: {
underscored: true,
enums: {
TRANSACTION_ERRORS: TRANSACTION_ERRORS,
TRANSACTION_STATUS: TRANSACTION_STATUS,
TRANSACTION_KIND: TRANSACTION_KIND,
TRANSACTION_DEFAULTS: TRANSACTION_DEFAULTS
},
description: 'A Transaction is a representation of a purchasing event.',
// defaultScope: {
// where: {
// live_mode: app.config.proxyEngine.live_mode
// }
// },
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: (values, options) => {
if (!values.token) {
values.token = `transaction_${shortId.generate()}`
}
},
afterCreate: (values, options) => {
return app.services.TransactionService.afterCreate(values, options)
.catch(err => {
return Promise.reject(err)
})
},
afterUpdate: (values, options) => {
return app.services.TransactionService.afterUpdate(values, options)
.catch(err => {
return Promise.reject(err)
})
}
},
classMethods: {
/**
* Associate the Model
* @param models
*/
associate: (models) => {
// models.Transaction.belongsTo(models.Refund, {
//
// })
models.Transaction.belongsTo(models.Order, {
// as: 'Order',
// allowNull: false
})
models.Transaction.belongsTo(models.Customer, {
// as: 'Customer',
// allowNull: true
})
models.Transaction.belongsTo(models.Source, {
// as: 'Source',
// allowNull: true
})
},
/**
*
* @param options
* @param batch
* @returns Promise.<T>
*/
batch: function (options, batch) {
const self = this
options = options || {}
options.limit = options.limit || 10
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(() => {
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)
},
/**
*
* @param transaction
* @param options
* @returns {*}
*/
resolve: function(transaction, options){
const Transaction = this
if (transaction instanceof Transaction){
return Promise.resolve(transaction)
}
else if (transaction && _.isObject(transaction) && transaction.id) {
return Transaction.findById(transaction.id, options)
.then(resTransaction => {
if (!resTransaction) {
throw new Errors.FoundError(Error(`Transaction ${transaction.id} not found`))
}
return resTransaction
})
}
else if (transaction && _.isObject(transaction) && transaction.token) {
return Transaction.findOne({
where: {
token: transaction.token
},
transaction: options.transaction || null
})
.then(resTransaction => {
if (!resTransaction) {
throw new Errors.FoundError(Error(`Transaction ${transaction.token} not found`))
}
return resTransaction
})
}
else if (transaction && _.isNumber(transaction)) {
return Transaction.findById(transaction, options)
.then(resTransaction => {
if (!resTransaction) {
throw new Errors.FoundError(Error(`Transaction ${transaction} not found`))
}
return resTransaction
})
}
else if (transaction && _.isString(transaction)) {
return Transaction.findOne({
where: {
token: transaction
},
transaction: options.transaction || null
})
.then(resTransaction => {
if (!resTransaction) {
throw new Errors.FoundError(Error(`Transaction ${transaction} not found`))
}
return resTransaction
})
}
else {
const err = new Error('Unable to resolve Transaction')
return Promise.reject(err)
}
}
},
instanceMethods: {
/**
*
* @returns {*}
*/
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
},
/**
*
* @returns {*}
*/
cancel: function() {
this.cancelled_at = new Date(Date.now())
this.status = TRANSACTION_STATUS.CANCELLED
if (this.description && !this.description.includes('cancelled')) {
this.description = `${this.description || 'transaction'} cancelled`
}
return this
},
/**
*
* @param options
* @returns {*}
*/
resolveOrder: function(options) {
options = options || {}
const Order = app.orm['Order']
if (
this.Order
&& this.Order instanceof Order
&& 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)
})
}
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
reconcileOrderFinancialStatus: function(options) {
options = options || {}
const Order = app.orm['Order']
// If the status or the kind have not changed
if (!this.changed('status') && !this.changed('kind')) {
return Promise.resolve(this)
}
let resOrder
return Order.findById(this.order_id, {
// attributes: [
// 'id',
// 'name',
// 'customer_id',
// 'financial_status',
// 'total_authorized',
// 'total_captured',
// 'total_refunds',
// 'total_voided',
// 'total_cancelled',
// 'total_pending',
// 'total_due'
// ],
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(() => {
// Save the status changes
return resOrder.saveStatus({transaction: options.transaction || null})
})
.then(() => {
return this
})
}
}
}
}
}
static schema (app, Sequelize) {
return {
// Unique identifier for a particular order.
token: {
type: Sequelize.STRING,
unique: true
},
customer_id: {
type: Sequelize.INTEGER,
// references: {
// model: 'Customer',
// key: 'id'
// },
allowNull: true
},
order_id: {
type: Sequelize.INTEGER,
// references: {
// model: 'Order',
// key: 'id'
// },
allowNull: false
},
source_id: {
type: Sequelize.INTEGER,
// references: {
// model: 'Order',
// key: 'id'
// },
allowNull: true
},
// TODO Enable User
// The unique identifier for the user.
// user_id: {
// type: Sequelize.INTEGER,
// references: {
// model: 'User',
// key: 'id'
// }
// },
// The amount of money that the transaction was for.
amount: {
type: Sequelize.INTEGER,
defaultValue: 0
},
// The authorization code associated with the transaction.
authorization: {
type: Sequelize.STRING
},
// The date the authorization expires
authorization_exp: {
type: Sequelize.DATE,
defaultValue: moment()
.subtract(app.config.get('proxyCart.transactions.authorization_exp_days') || 0, 'days')
.format('YYYY-MM-DD HH:mm:ss')
},
// The unique identifier for the device.
device_id: {
type: Sequelize.STRING
},
// The name of the gateway the transaction was issued through
gateway: {
type: Sequelize.STRING
},
// The origin of the transaction.
source_name: {
type: Sequelize.STRING,
defaultValue: TRANSACTION_DEFAULTS.SOURCE_NAME
},
// An object containing information about the credit card used for this transaction. Normally It has the following properties:
// type: The type of Source: credit_card, debit_card, prepaid_card, apple_pay, bitcoin
// gateway: the Gateway used
// avs_result_code: The Response code from AVS the address verification system. The code is a single letter; see this chart for the codes and their definitions.
// credit_card_iin: The issuer identification number (IIN), formerly known as bank identification number (BIN) ] of the customer's credit card. This is made up of the first few digits of the credit card number.
// credit_card_company: The name of the company who issued the customer's credit card.
// credit_card_number: The customer's credit card number, with most of the leading digits redacted with Xs.
// cvv_result_code: The Response code from the credit card company indicating whether the customer entered the card security code, a.k.a. card verification value, correctly. The code is a single letter or empty string; see this chart http://www.emsecommerce.net/avs_cvv2_response_codes.htm for the codes and their definitions.
// token: The card token from the Gateway
payment_details: helpers.JSONB('Transaction', app, Sequelize, 'payment_details', {
defaultValue: {}
}),
// The kind of transaction:
kind: {
type: Sequelize.ENUM,
values: _.values(TRANSACTION_KIND),
allowNull: false
},
// A transaction reciept attached to the transaction by the gateway. The value of this field will vary depending on which gateway the shop is using.
receipt: helpers.JSONB('Transaction', app, Sequelize, 'receipt', {
defaultValue: {}
}),
// A standardized error code, independent of the payment provider. Value can be null.
error_code: {
type: Sequelize.ENUM,
values: _.values(TRANSACTION_ERRORS)
},
// The status of the transaction. Valid values are: pending, failure, success or error.
status: {
type: Sequelize.ENUM,
values: _.values(TRANSACTION_STATUS),
defaultValue: TRANSACTION_STATUS.PENDING
},
// The three letter code (ISO 4217) for the currency used for the payment.
currency: {
type: Sequelize.STRING,
defaultValue: app.config.proxyCart.default_currency || TRANSACTION_DEFAULTS.CURRENCY
},
// A description of the Transaction
description: {
type: Sequelize.STRING
},
// The datetime the last retry was at
retry_at: {
type: Sequelize.DATE
},
// The total amounts of retries
total_retry_attempts: {
type: Sequelize.INTEGER,
defaultValue: 0
},
// The datetime the transaction was cancelled
cancelled_at: {
type: Sequelize.DATE
},
// Live Mode
live_mode: {
type: Sequelize.BOOLEAN,
defaultValue: app.config.proxyEngine.live_mode
}
}
}
}