Handling Purchases

There’re two paths through which a user may buy the Magnificent Moons package. They can either buy it directly by saying, “Buy the Magnificent Moons pack” or as a side effect of attempting to plan a trip to one of the moons. We’ll start by defining an intent to handle the direct approach. Later we’ll see how to create an upsell directive to offer the package if the user tries to plan a trip to a premium destination.

Defining a Buy Intent

A user of our skill could potentially decide to purchase the Magnificent Moons package at any time after launching the skill. To enable them to do so, we should define an intent that is triggered whenever they say something like, “buy the Magnificent Moons pack.” The following definition of BuyIntent shows such an intent:

 { "name": "BuyIntent", "samples": [ "buy the {productName} pack", "buy the {productName} package", "buy the {productName} extension" ], "slots": [ { "name": "productName", "type": "ADDON_PRODUCTS" } ] },

The first thing to notice is that the intent isn’t specific to the Magnificent Moons product. Although we could have defined an intent specifically for the Magnificent Moons package, this one intent is able to handle a multitude of products and will prevent unnecessary duplication should we decide to add another product later.

The “productName” slot is where we’ll capture the desired product. The slot type is a custom type called ADDON_PRODUCTS. For now, the ADDON_PRODUCTS type will only include a single product, the Magnificent Moons package:

 "types": [ ... { "name": "ADDON_PRODUCTS", "values": [ { "id": "MAGNIFICENT_MOONS", "name": { "value": "Magnificent Moons", "synonyms": [ "Moons", "Massive Moons" ] } } ] } ]

Because some users might misspeak or forget the name of the package, we’ve also declared a couple of synonyms to make it more flexible. Also notice that the id property is set to “MAGNIFICENT_MOONS”, which is not-so-coincidentally the same as the value we gave in referenceName property when creating the product definition. We’ll take advantage of that in the intent handler to lookup the product.

Should Star Port 75 Travel later decide to start offering trips to asteroids or destinations outside of our solar system as add-on products, then we’d only need to define the new products as values under ADDON_PRODUCTS.

Handling Buy Intent Requests

Just as we’ve done for all of our skill’s other intents, we’ll need to create a request handler that handles the intent whose name is BuyIntent. The following handler does that:

 const​ Alexa = require(​'ask-sdk-core'​);
 const​ ProductUtils = require(​'./productUtils'​);
 const​ StandardHandlers = require(​'./StandardHandlers'​);
 
 const​ BuyIntentHandler = {
  canHandle(handlerInput) {
 const​ requestEnvelope = handlerInput.requestEnvelope;
 return​ Alexa.getRequestType(requestEnvelope) === ​'IntentRequest'
  && Alexa.getIntentName(requestEnvelope) === ​'BuyIntent'​;
  },
 async​ handle(handlerInput) {
 const​ slot = Alexa.getSlot(handlerInput.requestEnvelope, ​'productName'​);
 const​ referenceName =
  slot.resolutions.resolutionsPerAuthority[0].values[0].value.id;
 const​ productId =
  (​await​ ProductUtils.getProduct(handlerInput, referenceName))
  .productId;
 
 const​ rsp = handlerInput.responseBuilder
  .addDirective({
  type: ​"Connections.SendRequest"​,
  name: ​"Buy"​,
  payload: {
  InSkillProduct: {
  productId: productId
  }
  },
  token: ​"correlationToken"
  })
  .getResponse();
 return​ rsp;
  }
 };

Much like the intent handlers we’ve created before, the canHandle() function checks that this is an intent request and that the intent name matches the intent to be handled (BuyIntent in this case). If it matches, then the handle() function retrieves the “productName” slot from the request and extracts the id property from the resolved entity. Since we only have one possible value for the slot at this point, “MAGNIFICENT_MOONS” should be assigned to the referenceName property.

Ultimately, we’ll need the product’s ID to be able to complete the purchase. Rather than hard-code the product ID in the handler code, we’ll use referenceName to lookup the product and extract the product’s ID. To achieve that, let’s create a separate utility module that looks up a product using Alexa’s monetization service:

 module.exports = {
 async​ getProduct(handlerInput, referenceName) {
 const​ ispService = handlerInput
  .serviceClientFactory
  .getMonetizationServiceClient();
 const​ locale = handlerInput.requestEnvelope.request.locale;
 return​ ispService.getInSkillProducts(locale).then(
 function​(result) {
 const​ matches = result.inSkillProducts.filter(
  product => product.referenceName === referenceName);
 return​ (matches.length > 0) ? matches[0] : ​null​;
  }
  );
  }
 };

This module starts by retrieving the monetization service by calling getMonetizationServiceClient() on the service client factory (similar to how we obtained a UpsServiceClient in Chapter 5, Integrating User Data). Using the monetization service client, the getProduct() function calls getInSkillProducts() which returns a promise with the response. From the response, it filters the results down to a single product that matches the given product reference name. Assuming such a product can be found, it is returned. Otherwise, the getProduct() function returns null.

Back in the handler’s handle() function, the only thing we really need from the product is the product ID. Therefore, the productId property is extracted from the product and assigned to productId. With the product ID in hand, the handle() function finishes up by creating and returning a response that includes a Connections.SendRequest directive. The name property is set to “Buy”, which indicates the user is buying the product whose ID is specified under the payload property.

The token property can be set to any value you want. Alexa does nothing with this value but will send it back in a purchase response request so you can correlate the purchase request with the response. For that reason, the value should typically be unique to the transaction. We’re not going to use it in our skill, so setting it to a placeholder value of “correlationToken” will be fine.

Although the handler returns a buy directive, the purchase flow is not yet complete. Behind the scenes, Alexa will process the order, charge the user’s account, and send them an email receipt. After all of that, Alexa will send a request to our skill indicating that the transaction has completed. Let’s see how to write response request handlers.

Handling Purchase Responses

After Alexa handles a purchase as specified in a Connections.SendRequest directive, she will send a request to the skill with the slightly confusing request type of Connections.Response. This is essentially a callback to let our skill know that the purchase has been handled. From that point, our skill may transition right into the trip planning dialog flow or may simply thank the user for their purchase.

Assuming the purchase completes successfully, the request’s payload will have a purchaseResult property set to “ACCEPTED”. This indicates that the user accepted the purchase and there were no errors in processing it. But the transaction could fail for a handful of reasons as well. In that case, the purchaseResult property could be any one of the following values:

  • DECLINED—The user declined the purchase.

  • ALREADY_PURCHASED—The user has previously purchased the product. (Only applies to entitlements.)

  • ERROR—There was some error in processing the transaction.

The following request handler covers all of these cases:

 const​ ContinueAfterPurchaseHandler = {
  canHandle(handlerInput) {
 const​ requestEnvelope = handlerInput.requestEnvelope;
 return​ Alexa.getRequestType(requestEnvelope) === ​'Connections.Response'​;
  },
  handle(handlerInput) {
 const​ requestEnvelope = handlerInput.requestEnvelope;
 const​ purchaseResult = requestEnvelope.request.payload.purchaseResult;
 return​ handlerInput.responseBuilder
  .speak(handlerInput.t(​'PURCHASE_RESPONSE_'​ + purchaseResult))
  .getResponse();
  }
 };

The canHandle() function checks for a request type of Connections.Response. If it matches, then the handle() function simply looks up the localized text to speak, where the lookup key is the value of the purchaseResult property prefixed with “PURCHASE_RESPONSE_”. In the languageStrings.js module, those localized strings look like this (for English):

 PURCHASE_RESPONSE_ACCEPTED: ​'Thanks for purchasing the Magnificent '​ +
 'Moons extension! You may now book travel to several of the '​ +
 'most prominent moons in our solar system.'​,
 PURCHASE_RESPONSE_DECLINED: ​"That's too bad. If you change your "​ +
 'mind let me know by saying "Buy the Magnificent Moons pack".'​,
 PURCHASE_RESPONSE_ALREADY_PURCHASED: ​'You have already purchased the '​ +
 'Magnificent Moons package.'​,
 PURCHASE_RESPONSE_ERROR: ​'There was an error processing your '​ +
 'purchase. Try again later.'

With the request handlers defined, don’t forget to export them from the PurchaseHandlers.js module:

 module.exports = {
  BuyIntentHandler: BuyIntentHandler,
  ContinueAfterPurchaseHandler: ContinueAfterPurchaseHandler
 };

And then import them and register them with the skill builder:

 const​ PurchaseIntentHandlers = require(​'./PurchaseHandlers'​);
 
 exports.handler = Alexa.SkillBuilders.custom()
  .addRequestHandlers(
  ...
» PurchaseIntentHandlers.BuyIntentHandler,
» PurchaseIntentHandlers.ContinueAfterPurchaseHandler,
  ...
  )
  .addErrorHandlers(
  StandardHandlers.ErrorHandler)
  .withApiClient(​new​ Alexa.DefaultApiClient())
  .addRequestInterceptors(
  LocalisationRequestInterceptor)
  .lambda();

Now you can deploy the skill and try it out. Assuming that you haven’t previously purchased the Magnificent Moons package, the conversation with Alexa might look like the screenshot.

images/sell/buy.png

Notice that Alexa injects some of her own content into the response when she says, “Great! Your order is complete and I’ve emailed you a receipt.” This is just a generic response from Alexa in-skill purchasing, letting the user know to expect a receipt in their email. Just as payment processing is automatically handled by the platform, so is sending of receipts and this message regarding receipt emails.

Then, if you were to try to buy the Magnificent Moons package again, Alexa would stop you and tell you that you’ve already bought it:

images/sell/buy_already.png

 

Once again, Alexa has injected a generic phrase into the response: “Good news! You already own that.” But the response message we provided follows the generic message.

The “ALREADY_PURCHASED” response only applies to entitlements. You’ll never get it for subscription or consumable products. But while testing entitlements, you may want to reset your purchase so that you can try it again. To do that, the ask smapi reset-entitlement-for-product command can be very handy:

 $ ​​ask​​ ​​smapi​​ ​​reset-entitlement-for-product​​ ​​
  ​​--product-id​​ ​​amzn1.adg.product.0791a588-c7a8-411a-8f3d-36d38cfc01b2

The ask smapi reset-entitlement-for-product command requires the product ID, specified with the --product-id parameter.

You’ll be happy to know that as the author of the skill, you won’t be actually charged for any purchases you make while testing. So, feel free to buy the Magnificent Moons package as many times as you like!

Our skill now lets users buy the Magnificent Moons package. But there’s nothing stopping them from planning a trip to one of the package’s destinations if they don’t buy it. Let’s make a change to the trip scheduling flow to only allow trips to moons if the user has purchased the Magnificent Moons package. And, in doing so, we’ll offer the package as an upsell.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset