UNPKG

poserver

Version:
670 lines (595 loc) 25.7 kB
/** * Created by tomdaley on 10/16/16. */ "use strict"; /** * 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;