From 78af7ceae19e9cea463d2fc4c04537cfd1e49494 Mon Sep 17 00:00:00 2001
From: Shahin Shayandeh
Date: Wed, 10 May 2017 08:40:33 -0700
Subject: [PATCH] Add payment sample
---
CSharp/sample-payments/.gitignore | 239 +++++++++++++++++
CSharp/sample-payments/PaymentsBot.sln | 22 ++
.../ActivityControllerDispatcher.cs | 47 ++++
.../PaymentsBot/AppSettings.cs | 39 +++
.../PaymentsBot/App_Start/WebApiConfig.cs | 33 +++
.../Controllers/MessagesController.cs | 217 +++++++++++++++
.../PaymentsBot/Dialogs/RootDialog.cs | 252 ++++++++++++++++++
.../sample-payments/PaymentsBot/Global.asax | 1 +
.../PaymentsBot/Global.asax.cs | 12 +
.../PaymentsBot/Helpers/Base64Url.cs | 46 ++++
.../Helpers/PaymentAddressExtensions.cs | 15 ++
.../PaymentsBot/Helpers/PaymentItemBuilder.cs | 26 ++
.../PaymentsBot/Models/CatalogItem.cs | 19 ++
.../PaymentsBot/Models/PaymentException.cs | 15 ++
.../PaymentsBot/Models/PaymentRecord.cs | 27 ++
.../PaymentsBot/Models/PaymentToken.cs | 112 ++++++++
.../PaymentsBot/Models/PaymentTokenFormat.cs | 24 ++
.../PaymentsBot/Models/PaymentTokenHeader.cs | 71 +++++
.../PaymentsBot/PaymentsBot.csproj | 236 ++++++++++++++++
.../PaymentsBot/Properties/AssemblyInfo.cs | 35 +++
.../Properties/Resources.Designer.cs | 173 ++++++++++++
.../PaymentsBot/Properties/Resources.resx | 159 +++++++++++
.../PaymentsBot/Services/CatalogService.cs | 35 +++
.../PaymentsBot/Services/PaymentService.cs | 134 ++++++++++
.../PaymentsBot/Services/ShippingService.cs | 118 ++++++++
.../PaymentsBot/Web.Debug.config | 30 +++
.../PaymentsBot/Web.Release.config | 31 +++
CSharp/sample-payments/PaymentsBot/Web.config | 87 ++++++
.../sample-payments/PaymentsBot/default.htm | 12 +
.../PaymentsBot/packages.config | 20 ++
CSharp/sample-payments/README.md | 45 ++++
CSharp/sample-payments/Settings.StyleCop | 219 +++++++++++++++
Node/sample-payments/.env | 8 +
Node/sample-payments/.gitignore | 3 +
Node/sample-payments/app.js | 242 +++++++++++++++++
Node/sample-payments/checkout.js | 217 +++++++++++++++
Node/sample-payments/package.json | 33 +++
Node/sample-payments/payment-token.js | 42 +++
Node/sample-payments/payments.js | 15 ++
Node/sample-payments/services/catalog.js | 24 ++
40 files changed, 3135 insertions(+)
create mode 100644 CSharp/sample-payments/.gitignore
create mode 100644 CSharp/sample-payments/PaymentsBot.sln
create mode 100644 CSharp/sample-payments/PaymentsBot/Activities/ActivityControllerDispatcher.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/AppSettings.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/App_Start/WebApiConfig.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Controllers/MessagesController.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Dialogs/RootDialog.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Global.asax
create mode 100644 CSharp/sample-payments/PaymentsBot/Global.asax.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Helpers/Base64Url.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Helpers/PaymentAddressExtensions.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Helpers/PaymentItemBuilder.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Models/CatalogItem.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Models/PaymentException.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Models/PaymentRecord.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Models/PaymentToken.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Models/PaymentTokenFormat.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Models/PaymentTokenHeader.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/PaymentsBot.csproj
create mode 100644 CSharp/sample-payments/PaymentsBot/Properties/AssemblyInfo.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Properties/Resources.Designer.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Properties/Resources.resx
create mode 100644 CSharp/sample-payments/PaymentsBot/Services/CatalogService.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Services/PaymentService.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Services/ShippingService.cs
create mode 100644 CSharp/sample-payments/PaymentsBot/Web.Debug.config
create mode 100644 CSharp/sample-payments/PaymentsBot/Web.Release.config
create mode 100644 CSharp/sample-payments/PaymentsBot/Web.config
create mode 100644 CSharp/sample-payments/PaymentsBot/default.htm
create mode 100644 CSharp/sample-payments/PaymentsBot/packages.config
create mode 100644 CSharp/sample-payments/README.md
create mode 100644 CSharp/sample-payments/Settings.StyleCop
create mode 100644 Node/sample-payments/.env
create mode 100644 Node/sample-payments/.gitignore
create mode 100644 Node/sample-payments/app.js
create mode 100644 Node/sample-payments/checkout.js
create mode 100644 Node/sample-payments/package.json
create mode 100644 Node/sample-payments/payment-token.js
create mode 100644 Node/sample-payments/payments.js
create mode 100644 Node/sample-payments/services/catalog.js
diff --git a/CSharp/sample-payments/.gitignore b/CSharp/sample-payments/.gitignore
new file mode 100644
index 0000000000..fba4c25f31
--- /dev/null
+++ b/CSharp/sample-payments/.gitignore
@@ -0,0 +1,239 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+ecf/
+rcf/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+*.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+## ignore imporeted publish xml files
+*intercom-botdirectory-scratch\ -\ FTP.pubxml
+*intercom-botdirectory-scratch\ -\ Web\ Deploy.pubxml
+
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+/Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll
+/Microsoft.CodeDom.Providers.DotNetCompilerPlatform.xml
+/PublishScripts/Scripts/Deploy-AzureResourceGroup-5.ps1
+/PublishScripts
+
+
+
+
+
+
+
+
+
+# User-specific files
+Documentation/Doxygen_warnings.txt
+
+# Build results
+Documentation/docs/
+
+# Azure publish profiles
+*.pubxml
+PublishProfiles/
diff --git a/CSharp/sample-payments/PaymentsBot.sln b/CSharp/sample-payments/PaymentsBot.sln
new file mode 100644
index 0000000000..a1e105bc2e
--- /dev/null
+++ b/CSharp/sample-payments/PaymentsBot.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaymentsBot", "PaymentsBot\PaymentsBot.csproj", "{A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/CSharp/sample-payments/PaymentsBot/Activities/ActivityControllerDispatcher.cs b/CSharp/sample-payments/PaymentsBot/Activities/ActivityControllerDispatcher.cs
new file mode 100644
index 0000000000..4454db1ec7
--- /dev/null
+++ b/CSharp/sample-payments/PaymentsBot/Activities/ActivityControllerDispatcher.cs
@@ -0,0 +1,47 @@
+namespace PaymentsBot.Activities
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Net.Http;
+ using System.Web.Http;
+ using Microsoft.Bot.Builder.Internals.Fibers;
+ using Microsoft.Bot.Builder.Scorables;
+ using Microsoft.Bot.Connector;
+
+ public sealed class ActivityControllerDispatcher : Dispatcher
+ {
+ private readonly ApiController controller;
+ private readonly Activity activity;
+ private readonly HttpResponseMessage response;
+
+ public ActivityControllerDispatcher(ApiController controller, Activity activity, HttpResponseMessage response)
+ {
+ SetField.NotNull(out this.controller, nameof(controller), controller);
+ SetField.NotNull(out this.activity, nameof(activity), activity);
+ SetField.NotNull(out this.response, nameof(response), response);
+ }
+
+ protected override Type MakeType()
+ {
+ return this.controller.GetType();
+ }
+
+ protected override IReadOnlyList
+
+
diff --git a/CSharp/sample-payments/PaymentsBot/packages.config b/CSharp/sample-payments/PaymentsBot/packages.config
new file mode 100644
index 0000000000..c15b623ee2
--- /dev/null
+++ b/CSharp/sample-payments/PaymentsBot/packages.config
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CSharp/sample-payments/README.md b/CSharp/sample-payments/README.md
new file mode 100644
index 0000000000..1283016d76
--- /dev/null
+++ b/CSharp/sample-payments/README.md
@@ -0,0 +1,45 @@
+# Payment Bot Sample
+
+A sample bot showing how to integrate with Microsoft Seller Center for payment processing.
+
+### Prerequisites
+
+The minimum prerequisites to run this sample are:
+* The latest update of Visual Studio 2015. You can download the community version [here](http://www.visualstudio.com) for free.
+* Register your bot with the Microsoft Bot Framework. Please refer to [this](https://docs.botframework.com/en-us/csharp/builder/sdkreference/gettingstarted.html#registering) for the instructions. Once you complete the registration, update the [Bot's Web.config](PaymentsBot/Web.config#L9-L11) file with the registered config values (MicrosoftAppId and MicrosoftAppPassword).
+
+#### Microsoft Bot Builder
+
+This sample has been developed based on Microsoft Bot Builder Dialog system. You can follow the following [sample](https://github.com/Microsoft/BotBuilder-Samples/tree/master/CSharp/core-MultiDialogs) to become familiar with different kind of dialogs and dialog stack in Bot Builder.
+
+#### Microsoft Seller Center
+
+1. Create and activate a Stripe account if you don't have one already.
+
+2. Sign in to Seller Center with your Microsoft account.
+
+3. Within Seller Center, connect your account with Stripe.
+
+4. Within Seller Center, navigate to the Dashboard and copy the value of **MerchantID**.
+
+5. Update your bot's **web.config** file to set `MerchantId` to the value that you copied from the Seller Center Dashboard.
+
+#### Publish
+Also, in order to be able to run and test this sample you must [publish your bot, for example to Azure](https://docs.botframework.com/en-us/csharp/builder/sdkreference/gettingstarted.html#publishing). Alternatively, you can use [Ngrok to interact with your local bot in the cloud](https://blogs.msdn.microsoft.com/jamiedalton/2016/07/29/ms-bot-framework-ngrok/).
+
+### Outcome
+
+To run the sample, you'll need to publish Bot to Azure or use [Ngrok to interact with your local bot in the cloud](https://blogs.msdn.microsoft.com/jamiedalton/2016/07/29/ms-bot-framework-ngrok/).
+* Running Bot app
+ 1. In the Visual Studio Solution Explorer window, right click on the **PaymentsBot** project.
+ 2. In the contextual menu, select Debug, then Start New Instance and wait for the _Web application_ to start.
+
+You can use the webchat control in bot framework developer portal to interact with your bot.
+
+### More Information
+
+To get more information about how to get started in Bot Builder for .NET and Conversations please review the following resources:
+* [Bot Builder for .NET](https://docs.botframework.com/en-us/csharp/builder/sdkreference/index.html)
+* [Bot Framework FAQ](https://docs.botframework.com/en-us/faq/#i-have-a-communication-channel-id-like-to-be-configurable-with-bot-framework-can-i-work-with-microsoft-to-do-that)
+* [Bot Builder samples](https://github.com/microsoft/botbuilder-samples)
+* [Bot Framework Emulator](https://github.com/microsoft/botframework-emulator/wiki/Getting-Started)
diff --git a/CSharp/sample-payments/Settings.StyleCop b/CSharp/sample-payments/Settings.StyleCop
new file mode 100644
index 0000000000..b49c38ebd5
--- /dev/null
+++ b/CSharp/sample-payments/Settings.StyleCop
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Node/sample-payments/.env b/Node/sample-payments/.env
new file mode 100644
index 0000000000..76e4ef20f0
--- /dev/null
+++ b/Node/sample-payments/.env
@@ -0,0 +1,8 @@
+# Bot Framework Credentials
+
+MICROSOFT_APP_ID=
+MICROSOFT_APP_PASSWORD=
+
+PAYMENTS_LIVEMODE=false
+PAYMENTS_MERCHANT_ID=merk_123
+PAYMENTS_STRIPE_API_KEY=stripe_123
\ No newline at end of file
diff --git a/Node/sample-payments/.gitignore b/Node/sample-payments/.gitignore
new file mode 100644
index 0000000000..805e724b14
--- /dev/null
+++ b/Node/sample-payments/.gitignore
@@ -0,0 +1,3 @@
+/**/node_modules
+/**/.vscode
+/**/*.VC.db
\ No newline at end of file
diff --git a/Node/sample-payments/app.js b/Node/sample-payments/app.js
new file mode 100644
index 0000000000..1fcfb1e07f
--- /dev/null
+++ b/Node/sample-payments/app.js
@@ -0,0 +1,242 @@
+// This loads the environment variables from the .env file
+require('dotenv-extended').load();
+
+var util = require('util');
+var builder = require('botbuilder');
+var restify = require('restify');
+var payments = require('./payments');
+var checkout = require('./checkout');
+var catalog = require('./services/catalog');
+
+var connector = new builder.ChatConnector({
+ appId: process.env.MICROSOFT_APP_ID,
+ appPassword: process.env.MICROSOFT_APP_PASSWORD
+});
+
+var server = restify.createServer();
+server.listen(process.env.port || process.env.PORT || 3978, function () {
+ console.log('%s listening to %s', server.name, server.url);
+});
+server.post('/api/messages', connector.listen());
+
+var CartIdKey = 'CardId';
+
+var bot = new builder.UniversalBot(connector, (session) => {
+
+ catalog.getPromotedItem().then(product => {
+
+ // Store userId for later, when reading relatedTo to resume dialog with the receipt
+ var cartId = product.id;
+ session.conversationData[CartIdKey] = cartId;
+ session.conversationData[cartId] = session.message.address.user.id;
+
+ // Create PaymentRequest obj based on product information
+ var paymentRequest = createPaymentRequest(cartId, product);
+
+ var buyCard = new builder.HeroCard(session)
+ .title(product.name)
+ .subtitle(util.format('%s %s', product.currency, product.price))
+ .text(product.description)
+ .images([
+ new builder.CardImage(session).url(product.imageUrl)
+ ])
+ .buttons([
+ new builder.CardAction(session)
+ .title('Buy')
+ .type(payments.PaymentActionType)
+ .value(paymentRequest)
+ ]);
+
+ session.send(new builder.Message(session)
+ .addAttachment(buyCard));
+ });
+});
+
+bot.set('persistConversationData', true);
+
+connector.onInvoke((invoke, callback) => {
+ console.log('onInvoke', invoke);
+
+ // This is a temporary workaround for the issue that the channelId for "webchat" is mapped to "directline" in the incoming RelatesTo object
+ invoke.relatesTo.channelId = invoke.relatesTo.channelId === 'directline' ? 'webchat' : invoke.relatesTo.channelId;
+
+ var storageCtx = {
+ address: invoke.relatesTo,
+ persistConversationData: true,
+ conversationId: invoke.relatesTo.conversation.id
+ };
+
+ connector.getData(storageCtx, (err, data) => {
+ var cartId = data.conversationData[CartIdKey];
+ if (!invoke.relatesTo.user && cartId) {
+ // Bot keeps the userId in context.ConversationData[cartId]
+ var userId = data.conversationData[cartId];
+ invoke.relatesTo.useAuth = true;
+ invoke.relatesTo.user = { id: userId };
+ }
+
+ // Continue based on PaymentRequest event
+ var paymentRequest = null;
+ switch (invoke.name) {
+ case payments.Operations.UpdateShippingAddressOperation:
+ case payments.Operations.UpdateShippingOptionOperation:
+ paymentRequest = invoke.value;
+
+ // Validate address AND shipping method (if selected)
+ checkout
+ .validateAndCalculateDetails(paymentRequest, paymentRequest.shippingAddress, paymentRequest.shippingOption)
+ .then(updatedPaymentRequest => {
+ // return new paymentRequest with updated details
+ callback(null, updatedPaymentRequest, 200);
+ }).catch(err => {
+ // return error to onInvoke handler
+ callback(err);
+ // send error message back to user
+ bot.beginDialog(invoke.relatesTo, 'checkout_failed', {
+ errorMessage: err.message
+ });
+ });
+
+ break;
+
+ case payments.Operations.PaymentCompleteOperation:
+ var paymentRequestComplete = invoke.value;
+ paymentRequest = paymentRequestComplete.paymentRequest;
+ var paymentResponse = paymentRequestComplete.paymentResponse;
+
+ // Validate address AND shipping method
+ checkout
+ .validateAndCalculateDetails(paymentRequest, paymentResponse.shippingAddress, paymentResponse.shippingOption)
+ .then(updatedPaymentRequest =>
+ // Process Payment
+ checkout
+ .processPayment(updatedPaymentRequest, paymentResponse)
+ .then(chargeResult => {
+ // return success
+ callback(null, { result: "success" }, 200);
+ // send receipt to user
+ bot.beginDialog(invoke.relatesTo, 'checkout_receipt', {
+ paymentRequest: updatedPaymentRequest,
+ chargeResult: chargeResult
+ });
+ })
+ ).catch(err => {
+ // return error to onInvoke handler
+ callback(err);
+ // send error message back to user
+ bot.beginDialog(invoke.relatesTo, 'checkout_failed', {
+ errorMessage: err.message
+ });
+ });
+
+ break;
+ }
+
+ });
+});
+
+bot.dialog('checkout_receipt', function (session, args) {
+ console.log('checkout_receipt', args);
+
+ cleanupConversationData(session);
+
+ var paymentRequest = args.paymentRequest;
+ var chargeResult = args.chargeResult;
+ var shippingAddress = chargeResult.shippingAddress;
+ var shippingOption = chargeResult.shippingOption;
+ var orderId = chargeResult.orderId;
+
+ // send receipt card
+ var items = paymentRequest.details.displayItems
+ .map(o => builder.ReceiptItem.create(session, o.amount.currency + ' ' + o.amount.value, o.label));
+
+ var receiptCard = new builder.ReceiptCard(session)
+ .title('Contoso Order Receipt')
+ .facts([
+ builder.Fact.create(session, orderId, 'Order ID'),
+ builder.Fact.create(session, chargeResult.methodName, 'Payment Method'),
+ builder.Fact.create(session, [shippingAddress.addressLine, shippingAddress.city, shippingAddress.region, shippingAddress.country].join(', '), 'Shipping Address'),
+ builder.Fact.create(session, shippingOption, 'Shipping Option')
+ ])
+ .items(items)
+ .total(paymentRequest.details.total.amount.currency + ' ' + paymentRequest.details.total.amount.value);
+
+ session.endDialog(
+ new builder.Message(session)
+ .addAttachment(receiptCard));
+});
+
+bot.dialog('checkout_failed', function (session, args) {
+ cleanupConversationData(session);
+ session.endDialog('Could not process your payment: %s', args.errorMessage);
+});
+
+// PaymentRequest with default options
+function createPaymentRequest(cartId, product) {
+ if (!cartId) {
+ throw new Error('cartId is missing');
+ }
+
+ if (!product) {
+ throw new Error('product is missing');
+ }
+
+ // PaymentMethodData[]
+ var paymentMethods = [{
+ supportedMethods: [payments.MicrosoftPayMethodName],
+ data: {
+ mode: process.env.PAYMENTS_LIVEMODE === 'true' ? null : 'TEST',
+ merchantId: process.env.PAYMENTS_MERCHANT_ID,
+ supportedNetworks: ['visa', 'mastercard'],
+ supportedTypes: ['credit']
+ }
+ }];
+
+ // PaymentDetails
+ var paymentDetails = {
+ total: {
+ label: 'Total',
+ amount: { currency: product.currency, value: product.price.toFixed(2) },
+ pending: true
+ },
+ displayItems: [
+ {
+ label: product.name,
+ amount: { currency: product.currency, value: product.price.toFixed(2) }
+ }, {
+ label: 'Shipping',
+ amount: { currency: product.currency, value: '0.00' },
+ pending: true
+ }, {
+ label: 'Sales Tax',
+ amount: { currency: product.currency, value: '0.00' },
+ pending: true
+ }],
+ // until a shipping address is selected, we can't offer shipping options or calculate taxes or shipping costs
+ shippingOptions: []
+ };
+
+ // PaymentOptions
+ var paymentOptions = {
+ requestPayerName: true,
+ requestPayerEmail: true,
+ requestPayerPhone: true,
+ requestShipping: true,
+ shippingType: 'shipping'
+ };
+
+ // PaymentRequest
+ return {
+ id: cartId,
+ expires: '1.00:00:00', // 1 day
+ methodData: paymentMethods, // paymethodMethods: paymentMethods,
+ details: paymentDetails, // paymentDetails: paymentDetails,
+ options: paymentOptions // paymentOptions: paymentOptions
+ };
+}
+
+function cleanupConversationData(session) {
+ var cartId = session.conversationData[CartIdKey];
+ delete session.conversationData[CartIdKey];
+ delete session.conversationData[cartId];
+}
diff --git a/Node/sample-payments/checkout.js b/Node/sample-payments/checkout.js
new file mode 100644
index 0000000000..f6f3ae5993
--- /dev/null
+++ b/Node/sample-payments/checkout.js
@@ -0,0 +1,217 @@
+var _ = require('lodash');
+var uuid = require('uuid');
+var util = require('util');
+var Promise = require('bluebird');
+var Stripe = require('stripe')(process.env.PAYMENTS_STRIPE_API_KEY);
+var PaymentToken = require('./payment-token');
+var payments = require('./payments');
+var catalog = require('./services/catalog');
+
+function validateAndCalculateDetails(paymentRequest, shippingAddress, selectedShippingOption) {
+ // selectedShippingOption may not be set the first time
+
+ var productIds = [paymentRequest.id];
+
+ // get product items
+ return Promise
+ .all(productIds.map(id => catalog.getItemById(id)))
+ .then(products => {
+ if (products.length === 0) {
+ throw new Error('Product not available');
+ }
+
+ // get available shipping methods for address and validate
+ var shippingMethods = getShippingMethodsForAddress(shippingAddress);
+ var selectedShippingMethod = shippingMethods.find(o => o.id === selectedShippingOption);
+ if (!selectedShippingMethod && shippingMethods.length > 0) {
+ // preselect first option if none is found
+ selectedShippingMethod = shippingMethods[0];
+ }
+
+ if (selectedShippingMethod) {
+ selectedShippingMethod.selected = true;
+ }
+
+ // details
+ var paymentDetails = {
+ total: {
+ label: 'Total',
+ amount: { currency: 'USD', value: '0.00' }
+ },
+ displayItems: products.map(asDisplayItem)
+ .concat([
+ {
+ label: 'Shipping',
+ amount: { currency: 'USD', value: '0.00' },
+ pending: true
+ }, {
+ label: 'Sales Tax',
+ amount: { currency: 'USD', value: '0.00' },
+ pending: true
+ }]),
+ shippingOptions: shippingMethods
+ };
+
+ // calculate tax, shipping cost and total
+ recalculateTaxAndTotal(paymentDetails, shippingAddress, selectedShippingMethod);
+
+ return _.assign({}, paymentRequest, { details: paymentDetails })
+ });
+}
+
+function processPayment(paymentRequest, paymentResponse) {
+ try {
+ // sanity checks
+ checkParam(paymentRequest, 'paymentRequest');
+ checkParam(paymentResponse, 'paymentResponse');
+ if (paymentResponse.methodName !== payments.MicrosoftPayMethodName) {
+ throw new Error('Payment method is not supported.');
+ }
+
+ checkParam(paymentResponse.details, 'paymentResponse.details');
+ checkParam(paymentResponse.details.paymentToken, 'paymentResponse.details.paymentToken');
+
+ var paymentToken = PaymentToken.parse(paymentResponse.details.paymentToken);
+ checkParam(paymentToken, 'parsed paymentToken');
+ checkParam(paymentToken.source, 'Payment token source is empty.');
+ if (paymentToken.header.Format !== 'Stripe') {
+ throw new Error('Payment token format is not Stripe.');
+ }
+
+ if (paymentToken.header.MerchantId !== process.env.PAYMENTS_MERCHANT_ID) {
+ throw new Error('MerchantId is not supported.');
+ }
+
+ if (paymentToken.header.Amount.currency !== paymentRequest.details.total.amount.currency ||
+ paymentToken.header.Amount.value !== paymentRequest.details.total.amount.value) {
+ throw new Error('Payment token amount currency/amount mismatch.');
+ }
+
+ var paymentRecord = {
+ orderId: paymentRequest.id,
+ transactionId: uuid.v1(),
+ methodName: paymentResponse.methodName,
+ paymentProcessor: paymentToken.header.Format,
+ shippingAddress: paymentResponse.shippingAddress,
+ shippingOption: paymentResponse.shippingOption,
+ items: paymentRequest.details.displayItems,
+ total: paymentRequest.details.total,
+ liveMode: !paymentToken.isEmulated
+ };
+
+ // If the payment token is microsoft emulated do not charge (as it will fail)
+ if (paymentToken.isEmulated) {
+ return Promise.resolve(paymentRecord);
+ } else {
+ // Charge using Stripe
+ var chargeOptions = {
+ amount: Math.floor(parseFloat(paymentRequest.details.total.amount.value) * 100), // Amount in cents
+ currency: paymentRequest.details.total.amount.currency,
+ description: paymentRequest.id,
+ source: paymentToken.source
+ };
+
+ return Stripe.charges
+ .create(chargeOptions)
+ .then((charge) => {
+ console.log('Strige.charge.result', charge);
+ if (charge.status === 'succeeded' && charge.captured) {
+ // Charge succeeded, return payment paymentRecord
+ // Ideally, you should register the transaction using the paymentRecord and charge.id
+ return paymentRecord;
+ }
+
+ // Other statuses may include processing "pending" or "success" with non captured funds. It is up to the merchant how to handle these cases.
+ // If payment is captured but not charged this would be considered "unknown" (charge the captured amount after shipping scenario)
+ // Merchant might choose to handle "pending" and "failed" status or handle "success" status with funds captured null or false
+ // More information @ https://stripe.com/docs/api#charge_object-captured
+ throw new Error(util.format('Could not process charge using Stripe with charge.status: %s and charge.captured: %s', change.status, change.captured));
+ });
+ }
+
+ } catch (err) {
+ return Promise.reject(err);
+ }
+}
+
+module.exports = {
+ validateAndCalculateDetails: validateAndCalculateDetails,
+ processPayment: processPayment
+};
+
+// Available Shipping Methods
+function getShippingMethodsForAddress(shippingAddress) {
+ console.log('getAvailableShippingMethods', shippingAddress.country);
+
+ if (shippingAddress.country.toLowerCase() === 'zw') {
+ throw new Error('ZW country not supported');
+ }
+
+ if (shippingAddress.country.toLowerCase() === 'us') {
+ return [{
+ id: 'STANDARD',
+ label: 'Standard - (5-6 Business days)',
+ amount: { currency: 'USD', value: '0.00' }
+ }, {
+ id: 'EXPEDITED',
+ label: 'Expedited - (2 Business days)',
+ amount: { currency: 'USD', value: '2.50' }
+ }];
+ } else {
+ return [{
+ id: 'INTERNATIONAL',
+ label: 'International',
+ amount: { currency: 'USD', value: '25.00' }
+ }];
+ }
+}
+
+function recalculateTaxAndTotal(details, shippingAddress, shippingOption) {
+ console.log('updateTotalWithShipping', details);
+
+ // shipping price
+ if (shippingOption) {
+ var shippingItem = details.displayItems.find(i => i.label === 'Shipping');
+ shippingItem.amount = shippingOption.amount;
+ shippingItem.pending = false;
+ }
+
+ // tax
+ var subTotal = details.displayItems
+ .filter(i => i.label !== 'Sales Tax')
+ .reduce((a, b) => a + parseFloat(b.amount.value), 0);
+ var taxItem = details.displayItems.find(i => i.label === 'Sales Tax');
+
+ // only for US
+ var tax = shippingAddress.country.toLowerCase() === 'us' ? subTotal * 0.085 : 0;
+ taxItem.amount.value = tax.toFixed(2);
+ taxItem.pending = false;
+
+ // new total
+ details.total.amount.value = details.displayItems
+ .reduce((a, b) => a + parseFloat(b.amount.value), 0)
+ .toFixed(2);
+}
+
+function asDisplayItem(product) {
+ return {
+ label: product.name,
+ amount: {
+ currency: product.currency,
+ value: product.price.toFixed(2)
+ }
+ };
+}
+
+// helpers
+function checkParam(param, name) {
+ if (param === undefined) {
+ throw new Error('Mising Parameter: \'' + name + '\'');
+ }
+}
+
+function checkType(param, expectedType, name) {
+ if (!(param instanceof expectedType)) {
+ throw new Error('Expected type \'' + expectedType.name + '\' for parameter \'' + name + '\'');
+ }
+}
diff --git a/Node/sample-payments/package.json b/Node/sample-payments/package.json
new file mode 100644
index 0000000000..67994d6923
--- /dev/null
+++ b/Node/sample-payments/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "botbuilder-sample-payments",
+ "version": "1.0.0",
+ "description": "Bot Builder Sample - Payments",
+ "scripts": {
+ "start": "node app.js"
+ },
+ "author": "Microsoft Corp.",
+ "license": "MIT",
+ "keywords": [
+ "botbuilder",
+ "bots",
+ "chatbots",
+ "botbuilder-samples"
+ ],
+ "bugs": {
+ "url": "https://github.com/Microsoft/BotBuilder-Samples/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Microsoft/BotBuilder-Samples.git"
+ },
+ "dependencies": {
+ "base64url": "^2.0.0",
+ "bluebird": "^3.5.0",
+ "botbuilder": "^3.8.0",
+ "dotenv-extended": "^1.0.4",
+ "lodash": "^4.17.4",
+ "restify": "^4.3.0",
+ "stripe": "^4.15.1",
+ "uuid": "^3.0.1"
+ }
+}
diff --git a/Node/sample-payments/payment-token.js b/Node/sample-payments/payment-token.js
new file mode 100644
index 0000000000..28367336f5
--- /dev/null
+++ b/Node/sample-payments/payment-token.js
@@ -0,0 +1,42 @@
+var base64url = require('base64url');
+
+var MsPayEmulatedStripeTokenSource = 'tok_18yWDMKVgMv7trmwyE21VqO';
+
+function parse(tokenString) {
+ if (!tokenString) {
+ throw new Error('PaymentToken string expected');
+ }
+
+ var tokenParts = tokenString.split('.');
+ if (tokenParts.length !== 3) {
+ throw new Error('Invalid PaymentToken');
+ }
+
+ var header = parseHeader(tokenParts[0]);
+ var source = parseSource(tokenParts[1]);
+ var signature = parseSignature(tokenParts[2]);
+
+ return {
+ header,
+ source,
+ signature,
+ isEmulated: MsPayEmulatedStripeTokenSource === source
+ };
+}
+
+function parseHeader(headerString) {
+ var json = base64url.decode(headerString);
+ return JSON.parse(json);
+}
+
+function parseSource(sourceString) {
+ return base64url.decode(sourceString);
+}
+
+function parseSignature(signatureString) {
+ return base64url.toBuffer(signatureString);
+}
+
+module.exports = {
+ parse: parse
+};
\ No newline at end of file
diff --git a/Node/sample-payments/payments.js b/Node/sample-payments/payments.js
new file mode 100644
index 0000000000..920c441ed6
--- /dev/null
+++ b/Node/sample-payments/payments.js
@@ -0,0 +1,15 @@
+const Operations = {
+ UpdateShippingAddressOperation: 'payments/update/shippingAddress',
+ UpdateShippingOptionOperation: 'payments/update/shippingOption',
+ PaymentCompleteOperation: 'payments/complete'
+};
+
+const PaymentActionType = 'payment';
+
+const MicrosoftPayMethodName = 'https://pay.microsoft.com/microsoftpay';
+
+module.exports = {
+ Operations: Operations,
+ PaymentActionType: PaymentActionType,
+ MicrosoftPayMethodName: MicrosoftPayMethodName
+};
\ No newline at end of file
diff --git a/Node/sample-payments/services/catalog.js b/Node/sample-payments/services/catalog.js
new file mode 100644
index 0000000000..598cb30b79
--- /dev/null
+++ b/Node/sample-payments/services/catalog.js
@@ -0,0 +1,24 @@
+var Promise = require('bluebird');
+
+const catalogItems = [
+ {
+ id: 'bc861179-46a5-4645-a249-7eba2a4d9846',
+ name: 'Scott Gu - Favorite Shirt',
+ description: 'Shiny red, ready to rock on Keynotes',
+ price: 1.99,
+ currency: 'USD',
+ imageUrl: 'https://pbs.twimg.com/profile_images/565139568/redshirt_400x400.jpg'
+ }
+];
+
+const catalog = {
+
+ getItemById: (id) => Promise.resolve(
+ catalogItems.find((e) => e.id.toLowerCase() === id.toLowerCase())),
+
+ getPromotedItem: () => Promise.resolve(
+ catalogItems[0])
+
+};
+
+module.exports = catalog;
\ No newline at end of file