Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

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'
);
}