1078 lines
34 KiB
JavaScript
1078 lines
34 KiB
JavaScript
/**
|
|
* Controller to manage the invoices functions
|
|
*/
|
|
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const Q = require('q');
|
|
const httpStatus = require('http-status-codes');
|
|
const mongodb = require('mongodb');
|
|
|
|
const templates = require(global.pathPrefix + '../utils/templates.js');
|
|
const debug = require('debug')('webconsole-api:controllers:invoices');
|
|
|
|
const mainDB = require(global.pathPrefix + 'mainDB.js');
|
|
const utils = require(global.pathPrefix + 'utils.js');
|
|
const mailer = require(global.pathPrefix + 'mailer.js');
|
|
const valid = require(global.pathPrefix + 'valid.js');
|
|
const swaggerUtils = require(global.pathPrefix + '../utils/swaggerUtils.js');
|
|
const apiHelpers = require(global.pathPrefix + '../utils/api_helpers.js');
|
|
const formattingUtils = require(global.pathPrefix + '../utils/formatting.js');
|
|
const references = require(global.pathPrefix + '../utils/references.js');
|
|
const config = require(global.configFile);
|
|
|
|
module.exports = {
|
|
getInvoices,
|
|
getInvoice,
|
|
addInvoice,
|
|
updateInvoice,
|
|
cancelInvoice
|
|
};
|
|
|
|
/**
|
|
* Define a constant for the valid "invoice" transaction statuses
|
|
*/
|
|
const INVOICE_TRANSACTION_STATUSES = [20, 21, 22];
|
|
|
|
/**
|
|
* Definition for the renames we use between "Transactions" and "Invoices"
|
|
*/
|
|
const INVOICE_TO_TRANSACTION = {
|
|
_id: 'InvoiceID',
|
|
CustomerClientName: 'CustomerEmail',
|
|
TransactionStatus: 'InvoiceStatus'
|
|
};
|
|
|
|
/**
|
|
* Validation errors
|
|
*/
|
|
const ERRORS = {
|
|
NO_MERCHANT_ACCOUNT: 'BRIDGE: Merchant account not found',
|
|
NO_CUSTOMER: 'BRIDGE: Customer not found',
|
|
INSERT_INVOICE_INVALID_NUMBER: 'BRIDGE: Failed to find a valid invoice number'
|
|
};
|
|
|
|
/**
|
|
* Get the invoice list
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} res - Express response object
|
|
*/
|
|
function getInvoices(req, res) {
|
|
//
|
|
// Check that the client is a merchant
|
|
//
|
|
if (!req.session.data.isMerchant) {
|
|
res.status(httpStatus.FORBIDDEN).json({
|
|
code: 30701,
|
|
info: 'Not a merchant'
|
|
});
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Get the query params from the request and the session
|
|
//
|
|
const clientID = req.session.data.clientID;
|
|
|
|
//
|
|
// Define the query according to the params
|
|
//
|
|
const query = {
|
|
MerchantClientID: clientID,
|
|
TransactionStatus: {
|
|
$in: INVOICE_TRANSACTION_STATUSES
|
|
}
|
|
};
|
|
|
|
//
|
|
// Define the projection based on the Swagger definition
|
|
//
|
|
const projection = swaggerUtils.swaggerToMongoProjection(
|
|
req.swagger.operation,
|
|
true, // include _id so we know how to select an individual invoice.
|
|
undefined, // No subdocument
|
|
INVOICE_TO_TRANSACTION // Renames
|
|
);
|
|
|
|
//
|
|
// Make the query. Note limit & skip have defaults defined in the
|
|
// swagger definition, so will always exist even if not requested
|
|
//
|
|
mainDB.collectionTransaction.find(query)
|
|
.project(projection)
|
|
.sort({LastUpdate: -1}) // Hard-coded reverse sort by time
|
|
.toArray((err, invoices) => {
|
|
if (err) {
|
|
debug('- failed to getInvoices', err);
|
|
res.status(httpStatus.BAD_GATEWAY).json({
|
|
code: 30702,
|
|
info: 'Database offline'
|
|
});
|
|
} else {
|
|
//
|
|
// Rename _id to InvoiceID before returning them
|
|
// Rename CustomerClientName to CustomerEmail
|
|
//
|
|
apiHelpers.renameFields(invoices, INVOICE_TO_TRANSACTION);
|
|
|
|
//
|
|
// Null any nullable fields
|
|
//
|
|
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, invoices);
|
|
|
|
//
|
|
// Move invoice number to the top level
|
|
// Fix any missing due dates
|
|
//
|
|
for (let i = 0; i < invoices.length; ++i) {
|
|
if (!_.isUndefined(invoices[i].MerchantInvoiceNumber)) {
|
|
invoices[i].MerchantInvoiceNumber =
|
|
invoices[i].MerchantInvoiceNumber.InvoiceNumber;
|
|
}
|
|
if (_.isUndefined(invoices[i].DueDate)) {
|
|
invoices[i].DueDate = '1970-01-01T00:00:00.000Z';
|
|
}
|
|
}
|
|
res.status(httpStatus.OK).json(invoices);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the invoice details for a specific invoice.
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} res - Express response object
|
|
*/
|
|
function getInvoice(req, res) {
|
|
//
|
|
// Check that the client is a merchant
|
|
//
|
|
if (!req.session.data.isMerchant) {
|
|
res.status(httpStatus.FORBIDDEN).json({
|
|
code: 30703,
|
|
info: 'Not a merchant'
|
|
});
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Get the query params from the request and the session
|
|
//
|
|
const clientID = req.session.data.clientID;
|
|
const invoiceId = req.swagger.params.objectId.value;
|
|
|
|
//
|
|
// Build the query. The limits are:
|
|
// - Must match the id of the invoice we are looking for
|
|
// - Current user must be the invoice owner (to protect against Insecure
|
|
// Direct Object References).
|
|
//
|
|
const query = {
|
|
_id: mongodb.ObjectID(invoiceId),
|
|
MerchantClientID: clientID,
|
|
TransactionStatus: {
|
|
$in: INVOICE_TRANSACTION_STATUSES
|
|
}
|
|
};
|
|
|
|
//
|
|
// Define the fields based on the Swagger definition.
|
|
// Need to also request the CustomerClientName
|
|
//
|
|
const projection = swaggerUtils.swaggerToMongoProjection(
|
|
req.swagger.operation,
|
|
true,
|
|
undefined, // No subdocument
|
|
INVOICE_TO_TRANSACTION // Renames
|
|
);
|
|
|
|
//
|
|
// Add the CustomerClientID so we can find out the email address later
|
|
//
|
|
projection.CustomerClientID = 1;
|
|
|
|
//
|
|
// Build the options to encapsulate the projection
|
|
//
|
|
const options = {
|
|
fields: projection,
|
|
comment: 'WebConsole:getInvoice' // For profiler logs use
|
|
};
|
|
|
|
//
|
|
// Make the request
|
|
//
|
|
mainDB.findOneObject(mainDB.collectionTransaction, query, options, false,
|
|
(err, invoice) => {
|
|
if (err) {
|
|
debug('- failed to getInvoice', err);
|
|
res.status(httpStatus.BAD_GATEWAY).json({
|
|
code: 30704,
|
|
info: 'Database offline'
|
|
});
|
|
} else if (invoice === null) {
|
|
//
|
|
// Nothing found
|
|
//
|
|
res.status(httpStatus.NOT_FOUND).json({
|
|
code: 30705,
|
|
info: 'Not found'
|
|
});
|
|
} else {
|
|
//
|
|
// Get the email address for the client
|
|
//
|
|
const emailP = references.getEmailAddress(invoice.CustomerClientID);
|
|
delete invoice.CustomerClientID;
|
|
|
|
//
|
|
// Add a creation date field from the _id
|
|
//
|
|
invoice.CreationDate = invoice._id.getTimestamp();
|
|
|
|
//
|
|
// Rename fields
|
|
//
|
|
apiHelpers.renameFields(invoice, INVOICE_TO_TRANSACTION);
|
|
|
|
//
|
|
// Null any nullable fields
|
|
//
|
|
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, invoice);
|
|
|
|
//
|
|
// Move the invoice number to the top level, and fix potentially
|
|
// missing due date
|
|
//
|
|
if (!_.isUndefined(invoice.MerchantInvoiceNumber)) {
|
|
invoice.MerchantInvoiceNumber =
|
|
invoice.MerchantInvoiceNumber.InvoiceNumber;
|
|
}
|
|
if (_.isUndefined(invoice.DueDate)) {
|
|
invoice.DueDate = '1970-01-01T00:00:00.000Z';
|
|
}
|
|
|
|
//
|
|
// Wait for the client email address and complete the invoice
|
|
//
|
|
emailP.then((email) => {
|
|
invoice.CustomerEmail = email;
|
|
return res.status(httpStatus.OK).json(invoice);
|
|
}).catch((error) => {
|
|
if (error.name && error.name === references.ERRORS.INVALID_CLIENT) {
|
|
// No customer email, so just send it without one.
|
|
// This will allow the merchant to update the customer, cancel it, etc.
|
|
res.status(httpStatus.OK).json(invoice);
|
|
} else {
|
|
debug('- failed to get customer email', error);
|
|
res.status(httpStatus.BAD_GATEWAY).json({
|
|
code: 30704,
|
|
info: 'Database offline'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds a new invoice.
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} res - Express response object
|
|
*/
|
|
function addInvoice(req, res) {
|
|
//
|
|
// Check that the client is a merchant
|
|
//
|
|
if (!req.session.data.isMerchant) {
|
|
res.status(httpStatus.FORBIDDEN).json({
|
|
code: 30706,
|
|
info: 'Not a merchant'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const merchantID = req.session.data.clientID;
|
|
const validatedInvoice = req.swagger.params.body.value;
|
|
|
|
//
|
|
// Step 0: Validate the merchant invoice values add up correctly
|
|
//
|
|
const INVALID_MERCHANT_INVOICE = 'BRIDGE: MerchantInvoice items are not valid';
|
|
let invoiceValidationP = Q.resolve();
|
|
if (_.isArray(validatedInvoice.MerchantInvoice)) {
|
|
/**
|
|
* Validate the invoice, allowing for items to be repeated
|
|
*/
|
|
const result = valid.validateFieldMerchantInvoice(
|
|
validatedInvoice.MerchantInvoice,
|
|
validatedInvoice.RequestAmount,
|
|
true
|
|
);
|
|
if (result) {
|
|
invoiceValidationP = Q.reject({name: INVALID_MERCHANT_INVOICE});
|
|
}
|
|
}
|
|
|
|
//
|
|
// Step 1: Get the merchant's client details
|
|
//
|
|
const NO_MERCHANT = 'BRIDGE: Merchant not found';
|
|
const findMerchantQuery = {
|
|
ClientID: merchantID
|
|
};
|
|
const findMerchantOptions = {
|
|
comment: 'webconsole: addInvoice validate merchant'
|
|
};
|
|
const findMerchantPromise = Q.nfcall(
|
|
mainDB.findOneObject,
|
|
mainDB.collectionClient,
|
|
findMerchantQuery,
|
|
findMerchantOptions,
|
|
false // Don't suppress errors
|
|
).then((merchant) => {
|
|
return merchant ? merchant : Q.reject({name: NO_MERCHANT});
|
|
});
|
|
|
|
//
|
|
// Step 2: Validate the merchant's account ID
|
|
//
|
|
const findAccountPromise = validateMerchantAccount(
|
|
merchantID,
|
|
validatedInvoice.MerchantAccountID
|
|
);
|
|
|
|
//
|
|
// Step 3: Validate the customer.
|
|
//
|
|
const findCustomerPromise = validateCustomer(validatedInvoice.CustomerEmail);
|
|
|
|
//
|
|
// Step 4: Check everything is valid, so now we create the Transactions we are
|
|
// going to make.
|
|
//
|
|
const addPromise = Q.all([
|
|
invoiceValidationP,
|
|
findMerchantPromise,
|
|
findAccountPromise,
|
|
findCustomerPromise]).then((results) => {
|
|
const merchantDetails = results[1];
|
|
const customer = results[3];
|
|
|
|
//
|
|
// Create a blank transaction, then update it with our specific values
|
|
//
|
|
const newTransaction = mainDB.blankTransaction();
|
|
_.assignWith(
|
|
newTransaction,
|
|
{
|
|
CustomerClientID: customer.ClientID,
|
|
CustomerDisplayName: customer.DisplayName,
|
|
CustomerImage: customer.Selfie,
|
|
MerchantSessionToken: req.session.id,
|
|
MerchantAccountID: validatedInvoice.MerchantAccountID,
|
|
DueDate: validatedInvoice.DueDate,
|
|
MerchantClientID: merchantID,
|
|
MerchantDisplayName: merchantDetails.Merchant[0].CompanyAlias,
|
|
MerchantSubDisplayName: merchantDetails.Merchant[0].CompanySubName,
|
|
MerchantImage: merchantDetails.Merchant[0].CompanyLogo,
|
|
MerchantVATNo: merchantDetails.Merchant[0].VATNo,
|
|
MerchantInvoice: validatedInvoice.MerchantInvoice,
|
|
MerchantComment: validatedInvoice.MerchantComment,
|
|
TransactionStatus: utils.TransactionStatus.PENDING_INVOICE,
|
|
StatusInfo: 'Pending Invoice',
|
|
RequestAmount: validatedInvoice.RequestAmount,
|
|
LastUpdate: new Date(),
|
|
|
|
// Invoice Numbering
|
|
MerchantInvoiceNumber: {
|
|
InvoiceNumber: 1,
|
|
MerchantID: merchantID,
|
|
MerchantIndex: 0 // Always 0 at present, but allows future support
|
|
}
|
|
},
|
|
(objectValue, sourceValue) => {
|
|
/* Only merge values that aren't received as undefined */
|
|
return _.isUndefined(sourceValue) ? objectValue : sourceValue;
|
|
}
|
|
);
|
|
|
|
//
|
|
// Built up the transaction so add it.
|
|
//
|
|
return addMonotonicallyNumberedInvoice(
|
|
newTransaction,
|
|
config.maxInvoiceNumberAttempts
|
|
);
|
|
});
|
|
|
|
//
|
|
// Step 4. Run all the promises and wait for the result
|
|
//
|
|
Q.all([findMerchantPromise, findAccountPromise, findCustomerPromise, addPromise])
|
|
.then((result) => {
|
|
//
|
|
// Succeeded
|
|
// The _id is in result[2][0] because:
|
|
// Result is an array of results from the 3 promises in .all()
|
|
// Thus result[2] is the result of addPromise
|
|
// This is an addObject() which returns an array itself. But we
|
|
// are only adding one so we know it is result[2][0].
|
|
//
|
|
const insertedInvoice = result[3][0];
|
|
res.status(201).json({
|
|
InvoiceID: insertedInvoice._id
|
|
});
|
|
|
|
//
|
|
// Send an email to the customer
|
|
// Note that we are not going to let the success/failure affect
|
|
// the success of adding an invoice
|
|
//
|
|
return notifyNewInvoice(validatedInvoice.CustomerEmail, insertedInvoice);
|
|
})
|
|
.catch((error) => {
|
|
debug('-- error adding invoice: ', error);
|
|
if (
|
|
error &&
|
|
error.hasOwnProperty('name')
|
|
) {
|
|
switch (error.name) {
|
|
case ERRORS.NO_MERCHANT_ACCOUNT:
|
|
res.status(httpStatus.CONFLICT).json({
|
|
code: 30708,
|
|
info: 'Invalid Merchant Account'
|
|
});
|
|
break;
|
|
|
|
case ERRORS.NO_CUSTOMER:
|
|
res.status(httpStatus.CONFLICT).json({
|
|
code: 30709,
|
|
info: 'Customer not found'
|
|
});
|
|
break;
|
|
|
|
case ERRORS.INSERT_INVOICE_INVALID_NUMBER:
|
|
res.status(httpStatus.CONFLICT).json({
|
|
code: 30718,
|
|
info: 'Unable to find a valid invoice number.'
|
|
});
|
|
break;
|
|
|
|
case 'MongoError':
|
|
res.status(httpStatus.BAD_GATEWAY).json({
|
|
code: 30707,
|
|
info: 'Database Unavailable'
|
|
});
|
|
break;
|
|
|
|
case INVALID_MERCHANT_INVOICE:
|
|
res.status(httpStatus.BAD_REQUEST).json({
|
|
code: 30718,
|
|
info: 'Invalid MerchantInvoice values'
|
|
});
|
|
break;
|
|
|
|
case NO_MERCHANT: // This should never happen as we have a session
|
|
default:
|
|
//
|
|
// Unknown error
|
|
//
|
|
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
|
|
code: -1,
|
|
info: 'Unexpected error'
|
|
});
|
|
break;
|
|
}
|
|
} else {
|
|
//
|
|
// Unknown error
|
|
//
|
|
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
|
|
code: -1,
|
|
info: 'Unexpected error'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This attempts to add a new invoice with an ID that is monotonically
|
|
* increasing for each invoice. As we don't have db transactions we can't
|
|
* do something like have a merchant-level counter we increment and apply to
|
|
* invoices.
|
|
*
|
|
* Instead we have to use an "optimistic loop" together with a unique index on
|
|
* the collection. See:
|
|
* https://docs.mongodb.com/v3.0/tutorial/create-an-auto-incrementing-field/#auto-increment-optimistic-loop
|
|
*
|
|
* The basic operation is:
|
|
* 1. Query for the highest current invoice number
|
|
* 2. Add 1 to it, and try and insert document with that invoice number
|
|
* 3. If (2) fails due to duplicate index THEN goto 1 (e.g. race condition with
|
|
* another process which is also adding invoices)
|
|
* 4. If (2) fails <x> times then report an error (overloading or similar).
|
|
*
|
|
* @param {Object} newInvoice - the new invoice we want to insert
|
|
* @param {integer} maxAttempts - the max attempts we can make
|
|
*
|
|
* @returns {Promise} - a promise that resolves when the operation completes
|
|
* or rejects on failure
|
|
*/
|
|
function addMonotonicallyNumberedInvoice(newInvoice, maxAttempts) {
|
|
debug('addMonotonicallyNumberedInvoice', maxAttempts, newInvoice.MerchantInvoiceNumber.MerchantID);
|
|
|
|
//
|
|
// Have we run out of attempts?
|
|
//
|
|
if (maxAttempts === 0) {
|
|
return Q.reject({name: ERRORS.INSERT_INVOICE_INVALID_NUMBER});
|
|
}
|
|
|
|
//
|
|
// Try and find a valid number to insert with
|
|
//
|
|
const query = {
|
|
'MerchantInvoiceNumber.MerchantID': newInvoice.MerchantInvoiceNumber.MerchantID,
|
|
'MerchantInvoiceNumber.MerchantIndex': newInvoice.MerchantInvoiceNumber.MerchantIndex
|
|
};
|
|
const sortOrder = {
|
|
'MerchantInvoiceNumber.InvoiceNumber': -1
|
|
};
|
|
const projection = {
|
|
MerchantInvoiceNumber: 1
|
|
};
|
|
|
|
//
|
|
// Run the query to find the largest number
|
|
//
|
|
debug('addMonotonicallyNumberedInvoice: finding: ', query, projection);
|
|
const cursor = mainDB.collectionTransaction
|
|
.find(query, projection)
|
|
.sort(sortOrder)
|
|
.limit(1);
|
|
|
|
const findPromise = cursor.next();
|
|
|
|
//
|
|
// Use the result of that query to try and insert a new invoice.
|
|
// This could fail if we are racing another insertion, so we need to expect
|
|
// that posibility, and try again
|
|
//
|
|
const insertPromise = findPromise.then((result) => {
|
|
debug('- addMonotonicallyNumberedInvoice: found:', result);
|
|
|
|
let nextNumber = 1;
|
|
if (result) { // result === null if no entries match the query
|
|
nextNumber = result.MerchantInvoiceNumber.InvoiceNumber + 1;
|
|
}
|
|
newInvoice.MerchantInvoiceNumber.InvoiceNumber = nextNumber;
|
|
|
|
return Q.nfcall(
|
|
mainDB.addObject,
|
|
mainDB.collectionTransaction,
|
|
newInvoice,
|
|
undefined, // No options
|
|
true // Expect errors, so don't kill the DB if we get any
|
|
).catch((error) => {
|
|
debug('- addMonotonicallyNumberedInvoice: insertError:', error);
|
|
|
|
//
|
|
// This may or may not be an expected error
|
|
//
|
|
if (error.name === 'MongoError' && error.code === 11000) {
|
|
//
|
|
// This is the duplicate unqiue key error we expect might
|
|
// happen during a race condition. So try again with 1 less
|
|
// retry limit
|
|
return addMonotonicallyNumberedInvoice(newInvoice, maxAttempts - 1);
|
|
} else {
|
|
//
|
|
// Its some other error, so pass it on
|
|
//
|
|
return Q.reject(error);
|
|
}
|
|
});
|
|
});
|
|
|
|
//
|
|
// Return the insertPromise so the caller can wait until its done
|
|
//
|
|
return insertPromise;
|
|
}
|
|
|
|
/**
|
|
* Updates an invoice with new values. Note that this can only be done for
|
|
* Invoices in the RejectedInvoice state. PendingInvoices should be cancelled
|
|
* and re-submitted.
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} res - Express response object
|
|
*/
|
|
function updateInvoice(req, res) {
|
|
//
|
|
// Check that the client is a merchant
|
|
//
|
|
if (!req.session.data.isMerchant) {
|
|
res.status(httpStatus.FORBIDDEN).json({
|
|
code: 30710,
|
|
info: 'Not a merchant'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const merchantClientID = req.session.data.clientID;
|
|
const validatedBody = req.swagger.params.body.value;
|
|
const invoiceId = req.swagger.params.objectId.value;
|
|
const resubmit = req.swagger.params.resubmit.value;
|
|
|
|
//
|
|
// Step 1: Validate the customer and merchant account
|
|
//
|
|
const findAccountPromise = validateMerchantAccount(
|
|
merchantClientID,
|
|
validatedBody.MerchantAccountID
|
|
);
|
|
const findCustomerPromise = validateCustomer(validatedBody.CustomerEmail);
|
|
|
|
//
|
|
// Step 2: Setup the find query. Limitations:
|
|
// - Must belong to me as merchant
|
|
// - Must be the given id
|
|
// - Must be in the Pending or Rejected state
|
|
//
|
|
const findQuery = {
|
|
MerchantClientID: merchantClientID,
|
|
_id: mongodb.ObjectID(invoiceId),
|
|
TransactionStatus: {
|
|
$in: [
|
|
utils.TransactionStatus.PENDING_INVOICE,
|
|
utils.TransactionStatus.REJECTED_INVOICE
|
|
]
|
|
}
|
|
};
|
|
|
|
//
|
|
// Step 3: Setup the update parameters from what we have been given.
|
|
//
|
|
|
|
const update = {
|
|
$set: {},
|
|
$inc: {
|
|
LastVersion: 1
|
|
},
|
|
$currentDate: {
|
|
LastUpdate: true
|
|
}
|
|
};
|
|
const required = ['MerchantAccountID', 'DueDate', 'RequestAmount'];
|
|
const optional = ['MerchantInvoice', 'MerchantComment'];
|
|
let idx = 0;
|
|
for (idx = 0; idx < required.length; ++idx) {
|
|
update.$set[required[idx]] = validatedBody[required[idx]];
|
|
}
|
|
for (idx = 0; idx < optional.length; ++idx) {
|
|
const key = optional[idx];
|
|
if (validatedBody.hasOwnProperty(key) && validatedBody[key] !== null) {
|
|
update.$set[key] = validatedBody[key];
|
|
}
|
|
}
|
|
|
|
if (resubmit) {
|
|
update.$set.TransactionStatus = utils.TransactionStatus.PENDING_INVOICE;
|
|
}
|
|
|
|
const options = {
|
|
projection: {
|
|
_id: 1,
|
|
CustomerClientName: 1, // Needed for email notification
|
|
MerchantDisplayName: 1 // Needed for email notification
|
|
},
|
|
upsert: false,
|
|
returnOriginal: false // Need the updated value, not the old one
|
|
};
|
|
|
|
//
|
|
// Step 4. Check that we validated everything ok, then Run the findAndUpdate
|
|
//
|
|
const updatePromise = Q.all([findAccountPromise, findCustomerPromise])
|
|
.then((results) => {
|
|
const customer = results[1];
|
|
|
|
//
|
|
// CustomerClientID is special as we have to wait to find it from
|
|
// the customer data. We also need to update the display name at
|
|
// the same time
|
|
//
|
|
update.$set.CustomerClientID = customer.ClientID;
|
|
update.$set.CustomerDisplayName = customer.DisplayName;
|
|
|
|
return Q.ninvoke(
|
|
mainDB.collectionTransaction,
|
|
'findOneAndUpdate',
|
|
findQuery,
|
|
update,
|
|
options
|
|
);
|
|
});
|
|
|
|
//
|
|
// Step 5. Check everything ran ok, then respond as appropriate
|
|
//
|
|
Q.all([findAccountPromise, findCustomerPromise, updatePromise])
|
|
.then((results) => {
|
|
//
|
|
// Ran the operation successfully, but need to check if it actually
|
|
// updated anything.
|
|
//
|
|
const updateResult = results[2];
|
|
if (updateResult.value) {
|
|
res.status(200).json();
|
|
|
|
//
|
|
// Notify the customer that the invoice has been updated.
|
|
// Note that we don't make the result contingent on the email
|
|
// sending correctly
|
|
//
|
|
return notifyUpdatedInvoice(
|
|
validatedBody.CustomerEmail,
|
|
updateResult.value
|
|
);
|
|
} else {
|
|
return res.status(404).json({
|
|
code: 30714,
|
|
info: 'Invoice not found, or not in Pending or Rejected state'
|
|
});
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (
|
|
error &&
|
|
error.hasOwnProperty('name')
|
|
) {
|
|
switch (error.name) {
|
|
case ERRORS.NO_MERCHANT_ACCOUNT:
|
|
res.status(httpStatus.CONFLICT).json({
|
|
code: 30712,
|
|
info: 'Invalid Merchant Account'
|
|
});
|
|
break;
|
|
|
|
case ERRORS.NO_CUSTOMER:
|
|
res.status(httpStatus.CONFLICT).json({
|
|
code: 30713,
|
|
info: 'Customer not found'
|
|
});
|
|
break;
|
|
|
|
case 'MongoError':
|
|
res.status(httpStatus.BAD_GATEWAY).json({
|
|
code: 30711,
|
|
info: 'Database Unavailable'
|
|
});
|
|
break;
|
|
|
|
default:
|
|
//
|
|
// Unknown error
|
|
//
|
|
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
|
|
code: -1,
|
|
info: 'Unexpected error'
|
|
});
|
|
break;
|
|
}
|
|
} else {
|
|
//
|
|
// Unknown error
|
|
//
|
|
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
|
|
code: -1,
|
|
info: 'Unexpected error'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Cancels an invoice (moving the transaction into the Cancelled status
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} res - Express response object
|
|
*/
|
|
function cancelInvoice(req, res) {
|
|
//
|
|
// Check that the client is a merchant
|
|
//
|
|
if (!req.session.data.isMerchant) {
|
|
res.status(httpStatus.FORBIDDEN).json({
|
|
code: 30715,
|
|
info: 'Not a merchant'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const merchantClientID = req.session.data.clientID;
|
|
const invoiceId = req.swagger.params.objectId.value;
|
|
|
|
//
|
|
// Step 1: Setup the find query. Limitations:
|
|
// - Must belong to me as merchant
|
|
// - Must be the given id
|
|
// - Must not be a pending or rejected invoice
|
|
//
|
|
const findQuery = {
|
|
MerchantClientID: merchantClientID,
|
|
_id: mongodb.ObjectID(invoiceId),
|
|
TransactionStatus: {
|
|
$in: [
|
|
utils.TransactionStatus.PENDING_INVOICE,
|
|
utils.TransactionStatus.REJECTED_INVOICE
|
|
]
|
|
}
|
|
};
|
|
|
|
//
|
|
// Step 2: Setup the update parameters from what we have been given.
|
|
//
|
|
const update = {
|
|
$set: {
|
|
TransactionStatus: utils.TransactionStatus.CANCELLED_INVOICE
|
|
},
|
|
$inc: {
|
|
LastVersion: 1
|
|
},
|
|
$currentDate: {
|
|
LastUpdate: true
|
|
}
|
|
};
|
|
const options = {
|
|
projection: {
|
|
_id: 1,
|
|
|
|
// Needed for the email notification
|
|
MerchantDisplayName: 1,
|
|
MerchantInvoiceNumber: 1,
|
|
CustomerClientID: 1
|
|
},
|
|
upsert: false
|
|
};
|
|
|
|
//
|
|
// Step 3. Run the findAndUpdate
|
|
//
|
|
Q.ninvoke(
|
|
mainDB.collectionTransaction,
|
|
'findOneAndUpdate',
|
|
findQuery,
|
|
update,
|
|
options
|
|
)
|
|
.then((result) => {
|
|
//
|
|
// Ran the operation successfully, but need to check if it actually
|
|
// updated anything.
|
|
//
|
|
if (result.value) {
|
|
res.status(200).json();
|
|
|
|
//
|
|
// Notify the customer that the invoice has been cancelled
|
|
//
|
|
return notifyCancelledInvoice(
|
|
result.value.CustomerClientID,
|
|
result.value
|
|
);
|
|
} else {
|
|
return res.status(404).json({
|
|
code: 30717,
|
|
info: 'Invoice not found, or already Cancelled'
|
|
});
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (
|
|
error &&
|
|
error.hasOwnProperty('name')
|
|
) {
|
|
switch (error.name) {
|
|
case 'MongoError':
|
|
res.status(httpStatus.BAD_GATEWAY).json({
|
|
code: 30716,
|
|
info: 'Database Unavailable'
|
|
});
|
|
break;
|
|
|
|
default:
|
|
//
|
|
// Unknown error
|
|
//
|
|
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
|
|
code: -1,
|
|
info: 'Unexpected error'
|
|
});
|
|
break;
|
|
}
|
|
} else {
|
|
//
|
|
// Unknown error
|
|
//
|
|
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
|
|
code: -1,
|
|
info: 'Unexpected error'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validates that the merchant account exists, and belongs to this user
|
|
*
|
|
* @param {string} merchantID - the merchant's client id
|
|
* @param {string} accountId - the account Id of the merchant account to use
|
|
*
|
|
* @returns {Promise} - a promise for finding the account
|
|
*/
|
|
function validateMerchantAccount(merchantID, accountId) {
|
|
const findAccountQuery = {
|
|
_id: mongodb.ObjectID(accountId),
|
|
ClientID: merchantID,
|
|
ReceivingAccount: 1,
|
|
AccountStatus: {
|
|
$bitsAllClear: utils.AccountDeleted
|
|
}
|
|
};
|
|
const findAccountOptions = {
|
|
fields: {
|
|
_id: 1,
|
|
ClientID: 1
|
|
},
|
|
comment: 'webconsole: add/updateInvoice validate account'
|
|
};
|
|
return Q.nfcall(
|
|
mainDB.findOneObject,
|
|
mainDB.collectionAccount,
|
|
findAccountQuery,
|
|
findAccountOptions,
|
|
false // Don't suppress errors
|
|
).then((account) => {
|
|
return account ? account : Q.reject({name: ERRORS.NO_MERCHANT_ACCOUNT});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validates that the customer email points to a real customer
|
|
*
|
|
* @param {string} customerEmail - The customer's email address
|
|
*
|
|
* @returns {Promise} - A promise for finding the customer
|
|
*/
|
|
function validateCustomer(customerEmail) {
|
|
const findClientQuery = {
|
|
ClientName: customerEmail,
|
|
ClientStatus: {$bitsAllClear: utils.ClientBarredMask}
|
|
};
|
|
const findClientOptions = {
|
|
fields: {
|
|
_id: 1,
|
|
ClientID: 1,
|
|
ClientName: 1,
|
|
DisplayName: 1,
|
|
Selfie: 1
|
|
},
|
|
comment: 'webconsole: addInvoice validate account'
|
|
};
|
|
return Q.nfcall(
|
|
mainDB.findOneObject,
|
|
mainDB.collectionClient,
|
|
findClientQuery,
|
|
findClientOptions,
|
|
false // Don't suppress errors
|
|
).then((client) => {
|
|
return client ? client : Q.reject({name: ERRORS.NO_CUSTOMER});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notifies the customer that a new invoice has been raised against them.
|
|
*
|
|
* @param {string} customerEmail - the customer's email address
|
|
* @param {Object} invoice - the invoice (for adding info to the email)
|
|
*
|
|
* @returns {Promise} - a promise for the result of notifying the customer
|
|
*/
|
|
function notifyNewInvoice(customerEmail, invoice) {
|
|
/**
|
|
* Render the html for the email
|
|
*/
|
|
const htmlEmail = templates.render('invoice-new', {
|
|
merchant: invoice.MerchantDisplayName,
|
|
requestAmount: formattingUtils.formatMoney(invoice.RequestAmount)
|
|
});
|
|
|
|
return Q.nfcall(
|
|
mailer.sendEmail,
|
|
'', // Mode ('Test' to just log, anything else to send)
|
|
customerEmail, // Destination
|
|
'New Invoice', // Subject
|
|
htmlEmail,
|
|
'notifyNewInvoice'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notifies the customer that an existing invoice has been updated.
|
|
*
|
|
* @param {string} customerEmail - the customer's email address
|
|
* @param {Object} invoice - the invoice (for adding info to the email)
|
|
*
|
|
* @returns {Promise} - a promise for the result of notifying the customer
|
|
*/
|
|
function notifyUpdatedInvoice(customerEmail, invoice) {
|
|
/**
|
|
* Render the html for the email
|
|
*/
|
|
const htmlEmail = templates.render('invoice-updated', {
|
|
merchant: invoice.MerchantDisplayName
|
|
});
|
|
|
|
return Q.nfcall(
|
|
mailer.sendEmail,
|
|
'', // Mode ('Test' to just log, anything else to send)
|
|
customerEmail, // Destination
|
|
'Updated Invoice', // Subject
|
|
htmlEmail,
|
|
'notifyUpdatedInvoice'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notifies the customer that an existing invoice has been cancelled by the merchant.
|
|
*
|
|
* @param {string} customerID - the customer's client ID
|
|
* @param {Object} invoice - the invoice (for adding info to the email)
|
|
*
|
|
* @returns {Promise} - a promise for the result of notifying the customer
|
|
*/
|
|
function notifyCancelledInvoice(customerID, invoice) {
|
|
/**
|
|
* Render the html for the email
|
|
*/
|
|
const htmlEmail = templates.render('invoice-cancelled', {
|
|
merchant: invoice.MerchantDisplayName,
|
|
number: invoice.MerchantInvoiceNumber.InvoiceNumber
|
|
});
|
|
|
|
return Q.nfcall(
|
|
mailer.sendEmailByID,
|
|
'', // Mode ('Test' to just log, anything else to send)
|
|
customerID, // Destination
|
|
'Cancelled Invoice', // Subject
|
|
htmlEmail,
|
|
'notifyCancelledInvoice'
|
|
);
|
|
}
|