diff --git a/CSharp/sample-payments/PaymentsBot/Controllers/MessagesController.cs b/CSharp/sample-payments/PaymentsBot/Controllers/MessagesController.cs index 9fb87c3d4a..fa197f15d4 100644 --- a/CSharp/sample-payments/PaymentsBot/Controllers/MessagesController.cs +++ b/CSharp/sample-payments/PaymentsBot/Controllers/MessagesController.cs @@ -148,7 +148,7 @@ private async Task OnInvoke(IInvokeActivity invoke, IConnectorClient connectorCl } } - if (result.Details.ShippingOptions.Count(option => option.Selected.HasValue && option.Selected.Value) != 1) + if (result.Details.ShippingOptions.Count(option => option.Selected.HasValue && option.Selected.Value) > 1) { throw new ArgumentException("Expected exactly zero or one selected shipping option."); } diff --git a/CSharp/sample-payments/README.md b/CSharp/sample-payments/README.md index 1283016d76..23aee98f4e 100644 --- a/CSharp/sample-payments/README.md +++ b/CSharp/sample-payments/README.md @@ -27,19 +27,311 @@ This sample has been developed based on Microsoft Bot Builder Dialog system. You #### 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/). +### Code Highlights + +The Bot Framework provides ways of interacting with external actors (for example ecommerce sites as is shown within this sample) and resume the conversation after. For this purpose the Bot Framework uses a `ConversationReference` instance on the `Conversation` helper static methods. With this reference you can send a message to your Bot and continue the conversation at the point where you left it. + +The Bot-Ecommerce interaction starts at the [`WelcomeMessageAsync`](PaymentsBot/Dialogs/RootDialog.cs#L32-L59) method from the [`RootDialog`](PaymentsBot/Dialogs/RootDialog.cs) that starts the conversation with the user. If you take a look on that method, there is a internal call to [`BuildBuyCardAsync`](PaymentsBot/Dialogs/RootDialog.cs#L54) which builds a `HeroCard` and returns it as an attachment. + +The [`BuildBuyCardAsync`](PaymentsBot/Dialogs/RootDialog.cs#L127-L153) uses a [`CardAction`](PaymentsBot/Dialogs/RootDialog.cs#L143-L148) with a particular action type held at the following constant `PaymentRequest.PaymentActionType` within the referenced payment class. As you can see the `CardAction` contains a payment payload will all the information required to trigger a Microsoft Wallet payment. + +````C# +private static Task BuildBuyCardAsync(string cartId, CatalogItem item) +{ + var heroCard = new HeroCard + { + Title = item.Title, + Subtitle = $"{item.Currency} {item.Price.ToString("F")}", + Text = item.Description, + Images = new List + { + new CardImage + { + Url = item.ImageUrl + } + }, + Buttons = new List + { + new CardAction + { + Title = "Buy", + Type = PaymentRequest.PaymentActionType, + Value = BuildPaymentRequest(cartId, item, PaymentService.GetAllowedPaymentMethods()) + } + } + }; + + return Task.FromResult(heroCard.ToAttachment()); +} +```` + +In addition, if you take a look at the payload you can see it is actually a [`PaymentRequest`](PaymentsBot/Dialogs/RootDialog.cs#L61) created and set to the `CardAction` `Value` by the [`BuildPaymentRequest`](PaymentsBot/Dialogs/RootDialog.cs#L147) method. All the values set there are used to set up the payment options within Microsoft Wallet services. + +````C# +private static PaymentRequest BuildPaymentRequest(string cartId, CatalogItem item, MicrosoftPayMethodData methodData) +{ + return new PaymentRequest + { + Id = cartId, + Expires = TimeSpan.FromDays(1).ToString(), + MethodData = new List + { + methodData.ToPaymentMethodData() + }, + Details = new PaymentDetails + { + Total = new PaymentItem + { + Label = Resources.Wallet_Label_Total, + Amount = new PaymentCurrencyAmount + { + Currency = item.Currency, + Value = Convert.ToString(item.Price, CultureInfo.InvariantCulture) + }, + Pending = true + }, + DisplayItems = new List + { + new PaymentItem + { + Label = item.Title, + Amount = new PaymentCurrencyAmount + { + Currency = item.Currency, + Value = item.Price.ToString(CultureInfo.InvariantCulture) + } + }, + new PaymentItem + { + Label = Resources.Wallet_Label_Shipping, + Amount = new PaymentCurrencyAmount + { + Currency = item.Currency, + Value = "0.00" + }, + Pending = true + }, + new PaymentItem + { + Label = Resources.Wallet_Label_Tax, + Amount = new PaymentCurrencyAmount + { + Currency = item.Currency, + Value = "0.00" + }, + Pending = true + } + } + }, + Options = new PaymentOptions + { + RequestShipping = true, + RequestPayerEmail = true, + RequestPayerName = true, + RequestPayerPhone = true, + ShippingType = PaymentShippingTypes.Shipping + } + }; +} +```` + +As said the `CardAction` provides to the channel all the information required to trigger the payment flow when the user clicks on the `HeroCard` `Buy` button. + +The payment interface interacts with your Bot by the ussual means, ie. POSTing messages to the REST controller. If you take a look on the POST method of the [`MessagesController`](PaymentsBot/Controllers/MessagesController.cs#L48-L57) you see that all activities received there are handled using a dispatcher. Under the hood this dispatcher takes a look on the activity type and calls the appropiate bound method within your controller (bound by using the `MethodBind` attribute). Within the referenced controller there are two bound methods for dispatching which are: [`OnMessageActivity`](PaymentsBot/Controllers/MessagesController.cs#L62-L64) and [`OnInvoke`](PaymentsBot/Controllers/MessagesController.cs#L72-L74) - resolution is done according to the method's signature. + +All messages (actually activities) received from the payment interface are of type `invoke`. So from now on we will focus on the [`OnInvoke`](PaymentsBot/Controllers/MessagesController.cs#L74-L127) method of the controller which looks this way: + +````C# +private async Task OnInvoke(IInvokeActivity invoke, IConnectorClient connectorClient, IStateClient stateClient, HttpResponseMessage response, CancellationToken token) +{ + MicrosoftAppCredentials.TrustServiceUrl(invoke.RelatesTo.ServiceUrl); + + var jobject = invoke.Value as JObject; + if (jobject == null) + { + throw new ArgumentException("Request payload must be a valid json object."); + } + + // 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; + + if (invoke.RelatesTo.User == null) + { + // Bot keeps the userId in context.ConversationData[cartId] + var conversationData = await stateClient.BotState.GetConversationDataAsync(invoke.RelatesTo.ChannelId, invoke.RelatesTo.Conversation.Id, token); + var cartId = conversationData.GetProperty(RootDialog.CARTKEY); + + if (!string.IsNullOrEmpty(cartId)) + { + invoke.RelatesTo.User = new ChannelAccount + { + Id = conversationData.GetProperty(cartId) + }; + } + } + + var updateResponse = default(object); + switch (invoke.Name) + { + case PaymentOperations.UpdateShippingAddressOperationName: + updateResponse = await this.ProcessShippingUpdate(jobject.ToObject(), ShippingUpdateKind.Address, token); + break; + + case PaymentOperations.UpdateShippingOptionOperationName: + updateResponse = await this.ProcessShippingUpdate(jobject.ToObject(), ShippingUpdateKind.Options, token); + break; + + case PaymentOperations.PaymentCompleteOperationName: + updateResponse = await this.ProcessPaymentComplete(invoke, jobject.ToObject(), token); + break; + + default: + throw new ArgumentException("Invoke activity name is not a supported request type."); + } + + response.Content = new ObjectContent( + updateResponse, + this.Configuration.Formatters.JsonFormatter, + JsonMediaTypeFormatter.DefaultMediaType); + + response.StatusCode = HttpStatusCode.OK; +} +```` + +There the received `invoke` instance has the operation's name that triggered the call in its `Name` property. This operation is inspected by the switch cases and dispatched to the proper method accordingly (ie. update the shipping address, or update the shipping options, or complete the payment after the user confirmed it). + +The [`ProcessShippingUpdate`](PaymentsBot/Controllers/MessagesController.cs#L129-L160) handles shipping address or options changes, and the [`ProcessPaymentComplete`](PaymentsBot/Controllers/MessagesController.cs#L162-L215) handles the payment confirmation from the user. + +Let's focus on the last one. + +````C# +private async Task ProcessPaymentComplete(IInvokeActivity invoke, PaymentRequestComplete paymentRequestComplete, CancellationToken token = default(CancellationToken)) +{ + var paymentRequest = paymentRequestComplete.PaymentRequest; + var paymentResponse = paymentRequestComplete.PaymentResponse; + + paymentRequest.Details = (await this.ProcessShippingUpdate( + new PaymentRequestUpdate() + { + Id = paymentRequest.Id, + Details = paymentRequest.Details, + ShippingAddress = paymentResponse.ShippingAddress, + ShippingOption = paymentResponse.ShippingOption + }, + ShippingUpdateKind.Both, + token)).Details; + + PaymentRecord paymentRecord = null; + PaymentRequestCompleteResult result = null; + Exception paymentProcessingException = null; + try + { + paymentRecord = await this.paymentService.ProcessPaymentAsync(paymentRequest, paymentResponse); + result = new PaymentRequestCompleteResult("success"); + } + catch (Exception ex) + { + paymentProcessingException = ex; + // TODO: If payment is captured but not charged this would be considered "unknown" (charge the captured amount after shipping scenario). + result = new PaymentRequestCompleteResult("failure"); + } + + try + { + var message = invoke.RelatesTo.GetPostToBotMessage(); + if (result.Result == "success") + { + // Resume the conversation with the receipt to user + message.Text = paymentRequestComplete.Id; + message.Value = paymentRecord; + } + else + { + // Resume the conversation with error message + message.Text = $"Failed to process payment with error: {paymentProcessingException?.Message}"; + } + await Conversation.ResumeAsync(invoke.RelatesTo, message, token); + } + catch (Exception ex) + { + Trace.TraceError($"Failed to resume the conversation using ConversationReference: {JsonConvert.SerializeObject(invoke.RelatesTo)} and exception: {ex.Message}"); + } + + return result; +} +```` + +As you can see the method calls the [`PaymentService`](PaymentsBot/Controllers/MessagesController.cs#L183) which completes the payment by registering the user's action and calls the external payment service (in this case Stripe), and sets the result value with a 'success' or 'failure' depending on the result of that operation. It also returns a `PaymentRecord` which will be sent after to the Bot. + +Finally, a message is built in order to send it to the Bot to end the payment conversation by showing a receipt if the operation was successful or show an error otherwise. To build this message a `ConversationReference` instance at [`invoke.RelatesTo`](PaymentsBot/Controllers/MessagesController.cs#L195) is used, and the conversation is resumed using the [`Conversation.ResumeAsync`](PaymentsBot/Controllers/MessagesController.cs#L207) helper. + +The conversation is resumed where it was left after showing the `HeroCard` with the `Buy` button to the user. There, the [`AfterPurchaseAsync`](PaymentsBot/Dialogs/RootDialog.cs#L58) handler was stacked within the `IDialogContext` in order to continue the conversation flow. This handler receives the `PaymentRecord` generated by the payment service within the activity's value, and builds a `ReceiptCard` by calling the [`BuildReceiptCardAsync`](PaymentsBot/Dialogs/RootDialog.cs#L239) method (if no payment record is present we assume an error occurred and display the activity message). + +````C# +private async Task AfterPurchaseAsync(IDialogContext context, IAwaitable argument) +{ + // clean up state store after completion + var cartId = context.ConversationData.GetValue(CARTKEY); + context.ConversationData.RemoveValue(CARTKEY); + context.ConversationData.RemoveValue(cartId); + + var activity = await argument as Activity; + var paymentRecord = activity?.Value as PaymentRecord; + + if (paymentRecord == null) + { + // show error + var errorMessage = activity.Text; + var message = context.MakeMessage(); + message.Text = errorMessage; + + await this.StartOverAsync(context, argument, message); + } + else + { + // show receipt + var message = context.MakeMessage(); + message.Text = string.Format( + CultureInfo.CurrentCulture, + Resources.RootDialog_Receipt_Text, + paymentRecord.OrderId, + paymentRecord.PaymentProcessor); + + message.Attachments.Add(await BuildReceiptCardAsync(paymentRecord)); + + await this.StartOverAsync(context, argument, message); + } +} +```` + ### 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/). +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/). + +You first need to update the following `web.config` settings shown below: + +````XML + + + + +```` + * 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. +You can use the webchat control in bot framework developer portal or the bot emulator to interact with your bot. + +The sample is configured by default to run in `test` mode. + +When you run it in the Bot emulator you will have the following interaction flow: + +![Running in Bot Emulator](images/payments-flow-emulator.png) ### 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) +* [Request Payment](https://docs.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-request-payment) diff --git a/CSharp/sample-payments/images/payments-flow-emulator.png b/CSharp/sample-payments/images/payments-flow-emulator.png new file mode 100644 index 0000000000..b2fd2a986f Binary files /dev/null and b/CSharp/sample-payments/images/payments-flow-emulator.png differ diff --git a/Node/sample-payments/README.md b/Node/sample-payments/README.md new file mode 100644 index 0000000000..a796722022 --- /dev/null +++ b/Node/sample-payments/README.md @@ -0,0 +1,334 @@ +# 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 .env](.env#L3-L4) 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 see the following [sample](https://github.com/Microsoft/BotBuilder-Samples/tree/master/Node/core-MultiDialogs) to become familiar with different kind of dialogs and dialog stack in Bot Builder framework. + +#### 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 **.env** file to set `PAYMENTS_MERCHANT_ID` 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.microsoft.com/en-us/bot-framework/nodejs/bot-builder-nodejs-quickstart). 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/). + +### Code Highlights + +The Bot-Payment interaction starts at the [`default message handler`](app.js#L25-L52) function provided when created the [`UniversalBot`](app.js#L24) instance. If you take a look on that function, a `HeroCard` is built and returned as an attachment. + +The [`HeroCard`](app.js#L36-L48) built there uses a [`CardAction`](app.js#L44-L47) with a particular action type held at the following constant `paymentRequest.PaymentActionType` within the referenced module. As you can see the `CardAction` contains a [`paymentRequest`](app.js#L34) payload will all the information required to trigger a Microsoft Wallet payment. + +````JavaScript +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)); +}); +```` + +In addition, if you can take a look at the function that builds the payload there at [`createPaymentRequest`](app.js#L175-L236). All the values set there are used to set up the payment options within Microsoft Wallet services. + +````JavaScript +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 + }; +} +```` + +As said the `CardAction` provides to the channel all the information required to trigger the payment flow when the user clicks on the `HeroCard` `Buy` button. + +The payment interface interacts with your Bot by the ussual means, ie. POSTing messages to the REST controller. The `ChatConnector` provides an `onInvoke` callback that is used to handle activities of a particular type (ie. not ussual messages). All messages (actually activities) received from the payment interface are of type `invoke`. + +We are using this handler within the sample as you can see on the function bind to [`connector.onInvoke`](app.js#L57-L136). As activities received from the payment interface are of type `invoke` as said, this function will handle all the request made by the payment interface: + +````JavaScript +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; + } + + }); +}); +```` + +There the received `invoke` instance has the operation's name that triggered the call in its `name` property. This operation is inspected by the switch cases and handled by the proper logic accordingly (ie. update the shipping address, or update the shipping options, or complete the payment after the user confirmed it). + +The [`first cases`](app.js#L81-L100) handles shipping address or options changes, and the [`last case`](app.js#L102-L132) handles the payment confirmation from the user. + +Let's focus on the last one. + +````JavaScript +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; +```` + +As you can see the [`checkout`](checkout.js#L137-L140) module exports helper functions to handle these requests. When the user confirms the payment at the payment interface, this handler calls the [`processPayment`](checkout.js#L62-L135) function which completes the payment by registering the user's action and calls the external payment service (in this case Stripe), and sets the result value with a 'success' or 'failure' depending on the result of that operation. It also returns a `PaymentRecord` which will be sent after to the Bot. + +Finally, a dialog is being used in order instruct the Bot to end the payment conversation by showing a receipt if the operation was [successful](app.js#L118-L121) or show an [error](app.js#L127-L129) otherwise. These dialogs are registered within the bot [here](app.js#L138-L167) and [here](app.js#L169-L172). + +The payment flow then is completed by showing a [`ReceiptCard`](app.js#L153-L162) to the user, and the child dialog is [ended immediatelly](app.js#L164-L166) as well giving control back to the default handler we described before. + +````JavaScript +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)); +}); +```` + +### Outcome + +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/). + +You first need to update the following `.env` settings shown below: + +````JavaScript +# Bot Framework Credentials + +MICROSOFT_APP_ID= +MICROSOFT_APP_PASSWORD= + +PAYMENTS_LIVEMODE=false +PAYMENTS_MERCHANT_ID=merk_123 +PAYMENTS_STRIPE_API_KEY=stripe_123 +```` + +* Running Bot app + 1. First install all dependencies by running: npm install, + 2. And then execute: node app.js. + +You can use the webchat control in bot framework developer portal or the bot emulator to interact with your bot. + +The sample is configured by default to run in `test` mode. + +When you run it in the Bot emulator you will have the following interaction flow: + +![Running in Bot Emulator](images/payments-flow-emulator.png) + +### 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 Node.js Reference](https://docs.microsoft.com/en-us/bot-framework/nodejs/) +* [Request Payment](https://docs.microsoft.com/en-us/bot-framework/nodejs/bot-builder-nodejs-request-payment) \ No newline at end of file diff --git a/Node/sample-payments/checkout.js b/Node/sample-payments/checkout.js index f6f3ae5993..e51951a535 100644 --- a/Node/sample-payments/checkout.js +++ b/Node/sample-payments/checkout.js @@ -74,16 +74,16 @@ function processPayment(paymentRequest, paymentResponse) { var paymentToken = PaymentToken.parse(paymentResponse.details.paymentToken); checkParam(paymentToken, 'parsed paymentToken'); checkParam(paymentToken.source, 'Payment token source is empty.'); - if (paymentToken.header.Format !== 'Stripe') { + if (paymentToken.header.format !== PaymentToken.tokenFormat.Stripe) { throw new Error('Payment token format is not Stripe.'); } - if (paymentToken.header.MerchantId !== process.env.PAYMENTS_MERCHANT_ID) { + 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) { + 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.'); } diff --git a/Node/sample-payments/images/payments-flow-emulator.png b/Node/sample-payments/images/payments-flow-emulator.png new file mode 100644 index 0000000000..c1c5e39205 Binary files /dev/null and b/Node/sample-payments/images/payments-flow-emulator.png differ diff --git a/Node/sample-payments/payment-token.js b/Node/sample-payments/payment-token.js index 28367336f5..124f42962d 100644 --- a/Node/sample-payments/payment-token.js +++ b/Node/sample-payments/payment-token.js @@ -38,5 +38,10 @@ function parseSignature(signatureString) { } module.exports = { - parse: parse + parse: parse, + tokenFormat: { + Invalid: 0, + Error: 1, + Stripe: 2 + } }; \ No newline at end of file