poserver
Version:
Server for JD Bot
670 lines (595 loc) • 25.7 kB
JavaScript
/**
* Created by tomdaley on 10/16/16.
*/
;
/**
* G E T P A I D
*
* Create Payment Account at credit card processor (Stripe.com). Add payment method. Charge for services.
*/
/**
* @namespace
* @property {object} results
* @property {object} results.response
* @property {number} results.response.index
* @property {string} results.response.entity
*/
var params = require("../../../poserver-configuration.json");
var prompts = require("./../const/prompts");
var builder = require('botbuilder');
var stripe = require('stripe')(params.STRIPE_API_KEY);
var Util = require('./../JdBotUtil');
var ConfirmDialog = require('./ConfirmDialog');
var LIBNAME = "getPayment";
const library = new builder.Library(LIBNAME);
library.dialog('/',
[
function (session)
{
//FIRST: Validate that we have a proper payment amount. If we don't, then we have a programmer or
//configuration error.
if (!session.userData.hasOwnProperty("paymentAmount"))
{
throw (new Error("Must set 'session.userData.paymentAmount property to USD value to be charged"));
}
//Dereference field to make code easier.
var paymentAmount = session.userData.paymentAmount;
//See if the value provided has any non-numeric characters. The isNaN() method will return false for spaces.
if ((paymentAmount + "").replace(/[^0-9.]/, "") !== paymentAmount + "")
{
throw (new Error("'session.userData.paymentAmount' must only contain digits and up to one decimal. [" + paymentAmount + "]"));
}
//Most basic test for whether this is a number
if (Number.isNaN(paymentAmount))
{
throw (new Error("'session.userData.paymentAmount' must be set to a numeric value. [" + paymentAmount + "]"));
}
//Here to see if the payment amount is some "cute" (i.e. "goofy") value that is not NaN but useless nonetheless.
//Also rounds the payment amount down to the nearest penny.
try
{
paymentAmount = Math.floor(paymentAmount * 100) / 100
}
catch (err)
{
throw (new Error("'session.userData.paymentAmount' must not be a goofy value. [" + paymentAmount + "]"));
}
//No negative charges.
if (paymentAmount <= 0.00)
{
throw (new Error("'session.userData.paymentAmount' must be greater than zero. [" + paymentAmount + "]"));
}
//Safety check - Limit the maximum amount that can be charged. Implemented 10/21/2016 by TJD. If we go a
//considerable amount of time without payment problems, this amount can be raised. DO NOT ELIMINATE THIS
//SAFETY CHECK!!
if (paymentAmount > 250)
{
throw (new Error("For now, paymentAmount cannot be greater than $250.00. This limit is set in PaymentDialogs.js. [" + paymentAmount + "]"));
}
//If we have a stripe account, let the user choose which card to use.
if (session.userData.userProfile.hasOwnProperty("ids") && session.userData.userProfile.ids.hasOwnProperty("stripe"))
{
session.replaceDialog("/chooseCard");
}
else
{
session.replaceDialog("/chargeNewClient");
}
}
]);
library.dialog('/chooseCard',
[
function (session, args, next)
{
var callback = function (err, customer)
{
/** @property {boolean} customer.deleted - Flag from Stripe indicating customer record has been deleted */
if (err || customer.deleted)
{
if (err)
{
session.send(err.message);
console.error(LIBNAME + ":/chooseCard: Error retrieving stripe customer account");
console.error(err);
}
else if (customer.deleted)
{
console.info(LIBNAME + ":/chooseCard: STRIPE account %s has been deleted. Will create new account for %s",
customer.id, session.userData.userProfile.email);
}
//Remove this stripe account id
delete session.userData.userProfile.ids.stripe;
Util.deleteId(session.userData.userProfile.users_id, "stripe");
//Treat as a new client
session.replaceDialog("/chargeNewClient");
}
else
{
session.dialogData.customer = customer;
var paymentOptions = [];
/** @property {[{}]} customer.sources.data - List of Credit Cards on File for this User */
session.userData.cards = customer.sources.data;
for (var ccIdx in customer.sources.data)
{
var cc = customer.sources.data[ccIdx];
/**
* @property {string} cc.brand - Credit card brand (e.g. "American Express")
* @property {string} cc.last4 - Last 4 Digits of credit card number
*/
paymentOptions.push(cc.brand + " " + prompts.endingIn + " " + cc.last4);
}
if (paymentOptions.length === 0)
{
session.replaceDialog("/chargeNewClient");
}
//Uncommenting the following "else" will cause the user's ONLY credit card to be automatically
//selected and charged. That is convenient, but, as the UI is currently arranged, it
//prevents a user from adding a second credit card.
//else if (paymentOptions.length === 1)
//{
// session.replaceDialog("/chargeCard", {
// card : customer.sources.data[0].id,
// customer: customer.id
// });
//}
else
{
paymentOptions.push(prompts.addAnotherCard);
session.dialogData.cards = customer.sources.data;
session.dialogData.paymentOptions = paymentOptions;
next(customer);
}
}
};
/** @property {function} stripe.customers.retrieve - Retrieve one customer's data */
stripe.customers.retrieve(session.userData.userProfile.ids.stripe.id, callback);
},
//SMS Prompt the user to make sure we have the same user who registered the credit card.
function (session, args)
{
args = args || {};
var telephone = (args.hasOwnProperty("metadata") && args.metadata.hasOwnProperty("telephone")) ?
args.metadata.telephone : session.userData.userProfile.cellPhone;
session.beginDialog("userProfile:/AuthenticateUser", {telephone: telephone});
},
//Result of SMS check
function (session, args, next)
{
if (args.success === false)
{
session.send(prompts.sayCantVerifyIdentity);
session.endConversation();
}
else
{
next(session.dialogData.paymentOptions);
}
},
//Here to prompt for WHICH card to charge
function (session, args)
{
var prompt = session.gettext(prompts.askPaymentMethod, {amount: session.userData.paymentAmount});
builder.Prompts.choice(session, prompt, args);
},
//Customer has chosen a payment method
function (session, results)
{
//User chose the "Add Another Card" option we appended to the array.
if (results.response.entity === prompts.addAnotherCard)
{
session.userData.returnTo = "/";
session.replaceDialog("/chargeNewClient");
}
//User selected a pre-existing credit card. Verify user really wants to charge this card.
else
{
var card = session.dialogData.cards[results.response.index];
var customer = session.userData.userProfile.ids.stripe.id;
session.replaceDialog('/chargeCard', {card: card, customer: customer});
session.userData.card = card;
}
}
]);
library.dialog('/chargeCard',
[
function (session, args)
{
var options;
var prompt;
if (args.hasOwnProperty("card") && (args.hasOwnProperty("customer") || args.card.hasOwnProperty("customer")))
{
session.dialogData.card = args.card;
session.dialogData.customer = (args.hasOwnProperty("customer")) ? args.customer : args.card.customer;
options = {
amount: session.userData.paymentAmount,
brand : args.card.brand,
last4 : args.card.last4
};
prompt = session.gettext(prompts.askOkToChargeCC, options);
ConfirmDialog.confirm(session, prompt);
}
else
{
//Programmer error
throw (new Error("Must specify [card and customer] or [card with a customer property]"));
}
},
//If user says YES, proceed to charge the card. If user said NO, go back to the top.
function (session, results)
{
//TODO: Handle Navigation
if (results.response)
{
var charge = {
amount : session.userData.paymentAmount * 100,
currency : "usd",
source : session.dialogData.card.id,
description : prompts.paymentDescription,
statement_descriptor: prompts.paymentDescription.slice(0, 22).toUpperCase(),
receipt_email : session.userData.userProfile.email,
customer : session.dialogData.card.customer
};
var callback = function (err) //, charge)
{
if (err)
{
session.send(err.message);
console.error(LIBNAME + ":/chargeCard: Error charging credit card");
console.error(err);
session.replaceDialog("/postChargeCard", {success: false});
}
else
{
session.userData.case.paid = "Y";
Util.setQueuedItemPaymentFlag(session.userData.case.queuedItemId, "Y");
session.send(prompts.sayThankYouForPayment);
session.replaceDialog("/postChargeCard", {success: true});
}
};
/** @property {function} stripe.charges.create - Create a charge to the given payment method */
stripe.charges.create(charge, callback); // <== Commented out for testing logic.
//callback(false, true); // <== Uncomment for testing.
}
else
{
session.replaceDialog("/");
}
}
]);
library.dialog('/postChargeCard',
[
//We've attempted to charge the card--what comes next?
function (session, results)
{
if (results.success === true)
{
//Charge went through fine. Proceed to customer survey.
session.replaceDialog('customerSurvey:/');
}
else
{
//Charge failed. See if they want to try another card or abort the transaction.
builder.Prompts.choice(session, prompts.askWhatNextFollowingFailedCharge, prompts.choicesAfterFailedCharge);
}
},
//Payment failed. User has told us what to do next, so do it.
function (session, results)
{
if (results.response.index === 0)
{
//Try again
session.replaceDialog("/");
}
else
{
//Give up
session.replaceDialog("customerSurvey:/");
}
}
]);
library.dialog('/chargeNewClient',
[
function (session)
{
session.send(session.gettext(prompts.sayNeedToGatherPaymentDetails, {amount: session.userData.paymentAmount}));
//Create Payment Account
if (!session.userData.userProfile.ids.hasOwnProperty("stripe"))
{
var customer = {
description: session.userData.userProfile.name.fullName(),
email : session.userData.userProfile.email,
metadata : {
users_id : session.userData.userProfile.users_id,
telephone: session.userData.userProfile.telephone
}
};
var callback = function (err, customer)
{
if (err)
{
console.error(LIBNAME + ":/chargeNewClient: Error creating user account");
console.error(err);
session.send(prompts.sayUnableToCreatPaymentAccount);
session.replaceDialog("customerSurvery:/");
}
else
{
session.userData.userProfile.ids.stripe = {"id": customer.id};
Util.saveIds(session.userData.userProfile);
session.beginDialog('/promptCardDetails');
}
};
stripe.customers.create(customer, callback);
}
else
{
session.beginDialog('/promptCardDetails');
}
},
//We have prompted for credit card details. Either we got good details or we didn't.
function (session, results)
{
if (results.success === false)
{
//Did not get good credit card details and could not fix it.
session.replaceDialog("customerSurvey:/");
}
else
{
//Got good credit card details...now go charage the card.
session.beginDialog('/chargeCard', {success: results.success, card: results.card});
}
}
]);
library.dialog('/promptCardDetails',
[
//Here to prompt for credit card number
function (session)
{
builder.Prompts.text(session, prompts.askCreditCardNumber);
},
//Here to process the credit card number
function (session, results)
{
if (Util.validateCheckDigit(results.response))
{
var creditCardNumber = results.response.replace(/[^0-9]/g, "");
session.userData.card = {};
session.userData.card.number = creditCardNumber;
session.replaceDialog('/getCvc');
}
else
{
var prompt = session.gettext(prompts.sayInvalidCreditCardNumber, {card: results.response});
ConfirmDialog.confirm(session, prompt);
}
},
//Get to here if the credit card appeared to be invalid and we asked the user to confirm the card number
function (session, results)
{
if (session.navigation)
{
switch (session.navigation)
{
case "back":
session.replaceDialog("/promptCardDetails");
break;
case "restart":
case "quit":
session.endConversation();
break;
}
}
else if (results.response)
{
session.replaceDialog("/getCvc");
}
else
{
session.replaceDialog("/promptCardDetails");
}
}
]);
library.dialog('/getCvc',
[
function (session)
{
builder.Prompts.text(session, prompts.askCreditCardCvc);
},
function (session, results)
{
var cvc = results.response.replace(/[^0-9]]/g, "");
//TODO: Handle Navigation
/* if (results.navigation)
{
back: session.replaceDialog('/promptCardDetails');
}
else
*/
if (isNaN(cvc) || !cvc.match(/^[0-9]{3,4}$/))
{
session.send(prompts.sayValidCvcRange);
session.replaceDialog("/getCvc");
}
else
{
session.userData.card.cvc = cvc;
session.replaceDialog('/getExpMonth');
}
}
]);
library.dialog('/getExpMonth',
[
function (session)
{
builder.Prompts.choice(session, prompts.askCreditCardExpirationMonth, prompts.months);
},
function (session, results)
{
var month = results.response.index + 1; //Zero-based index where January = 0
if (isNaN(month) || month < 1 || month > 12)
{
session.send(prompts.sayValidMonthRange + " You entered: " + month);
session.replaceDialog("/getExpMonth");
}
else
{
session.userData.card.exp_month = ("0" + (results.response.index + 1)).slice(-2);
session.replaceDialog('/getExpYear');
}
}
]);
library.dialog('/getExpYear',
[
function (session)
{
/*
* Build an array of years. Hopefully cards don't last more than 15 years.
* Note that I'm appending a space to the end of each year. I am doing this so that the year is
* coerced into a string. Deep inside the botbuilder API, it wants to call .trim() on the result
* and if the result is a number, then the operation fails. By coercing it into a string, the .trim()
* method succeeds (although, interestingly, fails to trim off the trailing space). That is also why,
* in the next waterfall step, you see that we are getting rid of all nonnumeric characters - it's to undo the
* appending of the " " to each year.
*/
var years = [];
var thisYear = (new Date()).getFullYear();
for (var year = thisYear; year < thisYear + 15; year++) years.push(year + " ");
session.dialogData.minYear = thisYear;
session.dialogData.maxYear = year;
builder.Prompts.choice(session, prompts.askCreditCardExpirationYear, years);
},
function (session, results)
{
var year = results.response.entity.replace(/[^0-9]/g, "");
if (isNaN(year) || year < session.dialogData.minYear || year > session.dialogData.maxYear)
{
let prompt = session.gettext(prompts.sayValidYearRange,
{min: session.dialogData.minYear, max: session.dialogData.maxYear});
session.send(prompt);
session.replaceDialog("/getExpYear");
}
session.userData.card.exp_year = year;
session.replaceDialog('/getCardHolderName');
}
]);
library.dialog('/getCardHolderName',
[
function (session)
{
var prompt = session.gettext(prompts.askCreditCardNameIsYourName, {name: session.userData.userProfile.name.fullName()});
ConfirmDialog.confirm(session, prompt);
},
function (session, results)
{
// TODO: Handle Navigation
if (results.response)
{
session.userData.card.name = session.userData.userProfile.name.fullName();
session.replaceDialog('/getBillingZip');
}
else
{
builder.Prompts.text(session, prompts.askCreditCardName);
}
},
function (session, results)
{
session.userData.card.name = results.response;
session.replaceDialog('/getBillingZip');
}
]);
library.dialog('/getBillingZip',
[
//Ask if billing zip is the same as our user's ZIP code.
function (session, args, next)
{
if (session.userData.userProfile.hasOwnProperty("address") && session.userData.userProfile.address.hasOwnProperty("postalCode"))
{
var prompt = session.gettext(prompts.askCreditCardZipIsYourZip, {zip: session.userData.userProfile.address.postalCode});
ConfirmDialog.confirm(session, prompt);
}
else
{
next({response: false});
}
},
//If billing ZIP is different, prompt for billing ZIP. Otherwise, use user's address ZIP
function (session, results, next)
{
if (results.response)
{
next({response: session.userData.userProfile.address.postalCode});
}
else
{
builder.Prompts.text(session, prompts.askCreditCardZip);
}
},
//Validate the ZIP or Postal code
//Note that for now, we only handle US-based cases and accept US credit cards. If we start accpeting credit
//cards from other countries, we'll need to prompt for the billing country. This example assumes the billing
//country is always the same as the litigation country, which is not a perfect assumption.
function (session, results)
{
//TODO: Handle Navigation
var validationPattern = /^[0-9]+$/; //Default useless pattern.
var countryName = "";
var ZIP = results.response.toUpperCase().trim();
//Determine which format the ZIP or postal code should be in. Get a country name for use in a validation
//message.
switch (session.userData.jurisdiction.country)
{
case "CA": //just as an example. We're not handling Canadian cases right now
validationPattern = /^([A-Z][0-9][A-Z] [0-9][A-Z][0-9]|[A-Z][0-9][A-Z][0-9][A-Z][0-9])$/;
countryName = "Canadian";
break;
case "US":
validationPattern = /^([0-9]{5}|[0-9]{9}|[0-9]{5}-[0-9]{4})$/;
countryName = "US";
break;
}
//See if the ZIP provided matches the pattern for that country. If not, give the user an error and
//try again.
if (!ZIP.match(validationPattern))
{
var prompt = session.gettext(prompts.sayValidPostalCode, {country: countryName,});
session.send(prompt);
session.replaceDialog("/getBillingZip");
}
//By the time we get here, we have a syntactically valid ZIP or postal code. Let's see . . .
session.userData.card.address_zip = ZIP;
//Verify the card
var source = session.userData.card;
source.object = "card";
var callback = function (err, source)
{
var dialogFieldMap =
{
"exp_month" : "/getExpMonth",
"exp_year" : "/getExpYear",
"number" : "/promptCardDetails",
"cvc" : "/getCvc",
"address_zip": "/getBillingZip",
"name" : "/getBillingName"
};
if (err)
{
console.error(LIBNAME + ":/getBillingZip: Error creating source for customer");
console.error(err);
session.send(err.message);
if (dialogFieldMap.hasOwnProperty(err.param))
session.replaceDialog(dialogFieldMap[err.param]);
else
session.replaceDialog("/");
}
else
{
session.endDialogWithResult({card: source, success: true});
}
};
/** @property {function} stripe.customers.createSource - Function to add a payment method to a user's account */
stripe.customers.createSource(session.userData.userProfile.ids.stripe.id, {source: source}, callback);
}
]);
module.exports = library;