From 2e03af4bf179fdc2d7949502921fae5391563974 Mon Sep 17 00:00:00 2001 From: Tony Cordova Date: Thu, 21 Mar 2019 14:23:49 -0700 Subject: [PATCH] initial commit --- .ask/config | 20 + .gitignore | 2 + README.md | 53 ++- hooks/post_new_hook.ps1 | 53 +++ hooks/post_new_hook.sh | 42 ++ hooks/pre_deploy_hook.ps1 | 54 +++ hooks/pre_deploy_hook.sh | 45 +++ lambda/custom/config.js | 181 +++++++++ lambda/custom/directive-builder.js | 35 ++ lambda/custom/error-handler.js | 129 ++++++ lambda/custom/index.js | 627 +++++++++++++++++++++++++++++ lambda/custom/package-lock.json | 146 +++++++ lambda/custom/package.json | 12 + lambda/custom/payload-builder.js | 84 ++++ lambda/custom/utilities.js | 103 +++++ models/en-US.json | 344 ++++++++++++++++ skill.json | 60 +++ 17 files changed, 1987 insertions(+), 3 deletions(-) create mode 100644 .ask/config create mode 100644 .gitignore create mode 100755 hooks/post_new_hook.ps1 create mode 100755 hooks/post_new_hook.sh create mode 100755 hooks/pre_deploy_hook.ps1 create mode 100755 hooks/pre_deploy_hook.sh create mode 100644 lambda/custom/config.js create mode 100644 lambda/custom/directive-builder.js create mode 100644 lambda/custom/error-handler.js create mode 100644 lambda/custom/index.js create mode 100644 lambda/custom/package-lock.json create mode 100644 lambda/custom/package.json create mode 100644 lambda/custom/payload-builder.js create mode 100644 lambda/custom/utilities.js create mode 100644 models/en-US.json create mode 100644 skill.json diff --git a/.ask/config b/.ask/config new file mode 100644 index 0000000..9bc0f60 --- /dev/null +++ b/.ask/config @@ -0,0 +1,20 @@ +{ + "deploy_settings": { + "default": { + "skill_id": "", + "was_cloned": false, + "merge": { + "manifest": { + "apis": { + "custom": { + "endpoint": { + "uri": "demo-store-amazon-pay" + } + } + } + } + } + } + } +} +a \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65ce6d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +/lambda/custom/node_modules \ No newline at end of file diff --git a/README.md b/README.md index ac653e5..d4dc2dc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,54 @@ -## Amazon Pay Demo Store for Alexa +# Build An Alexa Skill with Amazon Pay - Demo Store +Do you currently sell goods or services on other channels but want to expand to Alexa to reach new and existing customers? Good news, with [Amazon Pay](https://developer.amazon.com/alexa-skills-kit/make-money/amazon-pay), you can offer a seamless voice purchasing experience to your customers, allowing them to purchase real-world goods and services via Alexa - without having to leave the voice experience. + +This custom skill is a demo store that showcases how to use Amazon Pay with your shopping experiences on Alexa. + +## What You Will Need +Before you start working on this skill, you will need to create the following accounts: +* [Amazon Developer Account](http://developer.amazon.com/alexa) +* [Amazon Web Services Account](http://aws.amazon.com/) +* [Amazon Pay Merchant Account](https://pay.amazon.com/us) +* [Amazon Pay Sandbox Test Account](https://www.youtube.com/watch?v=m5teEFRZB8A) + +## Setting Up the Demo +This repository contains the interaction model and skill code. It is structured to make it easy to deploy if you have the [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html) already setup. If you would like to use the Alexa Developer Console, you can follow the steps outlined in the [Hello World](https://github.com/alexa/skill-sample-nodejs-hello-world) example, substituting the [Model](./models/en-US.json) and the [skill code](./lambda/custom/index.js) when called for. In addition, you will need to configure the additional supporting javascript files found in the custom folder. + +1. Clone repository and navigate the demo's root folder ( lambda/custom ). +1. Open [config.js](./lambda/custom/config.js) and update values `bucketName`, `sellerId`, and `sandboxCustomerEmailId` + * the `bucketName` is the name of your [S3](https://aws.amazon.com/s3/) bucket. + * the `sellerId` is your Amazon Pay Seller Id. You can find that [here](https://youtu.be/oHp4Hv5_MBA?t=38) + * the `sandboxCustomerEmailId` is the email address of the Amazon Pay sandbox test account you created in Seller Central. Instructions [here](https://www.youtube.com/watch?v=m5teEFRZB8A). +1. Give your skill permission to use your Amazon Pay account. You can do that [here](https://sellercentral.amazon.com/external-payments/integration/alexa/). The documentation is [here](https://developer.amazon.com/docs/amazon-pay/integrate-skill-with-amazon-pay-v2.html). +1. Enable the skill using the Alexa app. Be sure to click Settings to show the permissions page if you do not see it. Provide permission to use Amazon Pay. + +## Running the Demo +Launch the demo by saying, 'Alexa, open No Nicks'. If you receive an error, proceed to the [troubleshooting section](#troubleshooting). + +## Troubleshooting + +If you are encountering issues with your skill, double check that you have completed the following: + +1. Confirm that your Seller Central account is in good standing by selecting the Production environment and verify there are no errors on your account. +1. Check the correct skill was linked in Sandbox using in Seller Central. +1. Verify your sandbox test user was created in Seller Central. +1. Verify Amazon Pay permissions are enabled for your skill under Build > Permissions > Amazon Pay. +1. Verify the config.js contains the appropriate values for `bucketName`, `sellerId`, and `sandboxCustomerEmailId`. +1. Verify the correct skill Id is used in your Lambda function. +1. Enable your skill in your Alexa App +1. Consent and give permissions to Amazon Pay in your Alexa App +1. Enable Voice Purchasing in your Alexa App ( with or without the voice code ). + +All other errors and decline handling can be found here: https://developer.amazon.com/docs/amazon-pay/payment-declines-and-processing-errors.html + +## Resources +* [Amazon Pay Alexa Documentation](https://developer.amazon.com/docs/amazon-pay/amazon-pay-overview.html) +* [Amazon Pay Certification Requirements](https://developer.amazon.com/docs/amazon-pay/certify-skill-with-amazon-pay.html) +* [Official Alexa Skills Kit SDK for Node.js](https://ask-sdk-for-nodejs.readthedocs.io/en/latest/) - The Official Node.js SDK Documentation +* [Official Alexa Skills Kit Documentation](https://developer.amazon.com/docs/ask-overviews/build-skills-with-the-alexa-skills-kit.html) - Official Alexa Skills Kit Documentation +* [Amazon Developer Forums](https://forums.developer.amazon.com/spaces/423/index.html) - Join the conversation! +* [Amazon Pay Help Guide](https://pay.amazon.com/us/help) -This demo store showcases how to integrate Amazon Pay into your shopping experiences on Alexa. ## License -This library is licensed under the Amazon Software License. +This library is licensed under the Amazon Software License. \ No newline at end of file diff --git a/hooks/post_new_hook.ps1 b/hooks/post_new_hook.ps1 new file mode 100755 index 0000000..56f2124 --- /dev/null +++ b/hooks/post_new_hook.ps1 @@ -0,0 +1,53 @@ +# Powershell script for ask-cli post-new hook for Node.js +# Script Usage: post_new_hook.ps1 + +# SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. +# DO_DEBUG is boolean value for debug logging + +# Run this script one level outside of the skill root folder + +# The script does the following: +# - Run "npm install" in each sourceDir in skill.json + +param( + [string] $SKILL_NAME, + [bool] $DO_DEBUG = $False +) + +if ($DO_DEBUG) { + Write-Output "###########################" + Write-Output "###### post-new hook ######" + Write-Output "###########################" +} + +function install_dependencies ($CWD, $SOURCE_DIR) { + $INSTALL_PATH = $SKILL_NAME + "\" +$SOURCE_DIR + Set-Location $INSTALL_PATH + Invoke-Expression "npm install" 2>&1 | Out-Null + $EXEC_RESULT = $? + Set-Location $CWD + return $EXEC_RESULT +} + +$SKILL_FILE_PATH = $SKILL_NAME + "\skill.json" +$ALL_SOURCE_DIRS = Get-Content -Path $SKILL_FILE_PATH | select-string -Pattern "sourceDir" -CaseSensitive +Foreach ($SOURCE_DIR in $ALL_SOURCE_DIRS) { + $FILTER_SOURCE_DIR = $SOURCE_DIR -replace "`"", "" -replace "\s", "" -replace ",","" -replace "sourceDir:", "" + $CWD = (Get-Location).Path + if (install_dependencies $CWD $FILTER_SOURCE_DIR) { + if ($DO_DEBUG) { + Write-Output "Codebase ($FILTER_SOURCE_DIR) built successfully." + } + } else { + if ($DO_DEBUG) { + Write-Output "There was a problem installing dependencies for ($FILTER_SOURCE_DIR)." + } + exit 1 + } +} + +if ($DO_DEBUG) { + Write-Output "###########################" +} + +exit 0 diff --git a/hooks/post_new_hook.sh b/hooks/post_new_hook.sh new file mode 100755 index 0000000..9bef428 --- /dev/null +++ b/hooks/post_new_hook.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Shell script for ask-cli post-new hook for Node.js +# Script Usage: post_new_hook.sh + +# SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. +# DO_DEBUG is boolean value for debug logging + +# Run this script one level outside of the skill root folder + +# The script does the following: +# - Run "npm install" in each sourceDir in skill.json + +SKILL_NAME=$1 +DO_DEBUG=${2:-false} + +if [ $DO_DEBUG == false ] +then + exec > /dev/null 2>&1 +fi + +install_dependencies() { + npm install --prefix "$SKILL_NAME/$1" >/dev/null 2>&1 + return $? +} + +echo "###########################" +echo "###### post-new hook ######" +echo "###########################" + +grep "sourceDir" $SKILL_NAME/skill.json | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do + if install_dependencies $SOURCE_DIR; then + echo "Codebase ($SOURCE_DIR) built successfully." + else + echo "There was a problem installing dependencies for ($SOURCE_DIR)." + exit 1 + fi +done +echo "###########################" + +exit 0 + + diff --git a/hooks/pre_deploy_hook.ps1 b/hooks/pre_deploy_hook.ps1 new file mode 100755 index 0000000..39c6c90 --- /dev/null +++ b/hooks/pre_deploy_hook.ps1 @@ -0,0 +1,54 @@ +# Powershell script for ask-cli pre-deploy hook for Node.js +# Script Usage: pre_deploy_hook.ps1 + +# SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. +# DO_DEBUG is boolean value for debug logging +# TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.) + +# Run this script under the skill root folder + +# The script does the following: +# - Run "npm install" in each sourceDir in skill.json + +param( + [string] $SKILL_NAME, + [bool] $DO_DEBUG = $False, + [string] $TARGET = "all" +) + +function install_dependencies ($CWD, $SOURCE_DIR) { + Set-Location $SOURCE_DIR + Invoke-Expression "npm install" 2>&1 | Out-Null + $EXEC_RESULT = $? + Set-Location $CWD + return $EXEC_RESULT +} + +if ($DO_DEBUG) { + Write-Output "###########################" + Write-Output "##### pre-deploy hook #####" + Write-Output "###########################" +} + +if ($TARGET -eq "all" -Or $TARGET -eq "lambda") { + $ALL_SOURCE_DIRS = Get-Content -Path "skill.json" | select-string -Pattern "sourceDir" -CaseSensitive + Foreach ($SOURCE_DIR in $ALL_SOURCE_DIRS) { + $FILTER_SOURCE_DIR = $SOURCE_DIR -replace "`"", "" -replace "\s", "" -replace ",","" -replace "sourceDir:", "" + $CWD = (Get-Location).Path + if (install_dependencies $CWD $FILTER_SOURCE_DIR) { + if ($DO_DEBUG) { + Write-Output "Codebase ($FILTER_SOURCE_DIR) built successfully." + } + } else { + if ($DO_DEBUG) { + Write-Output "There was a problem installing dependencies for ($FILTER_SOURCE_DIR)." + } + exit 1 + } + } + if ($DO_DEBUG) { + Write-Output "###########################" + } +} + +exit 0 diff --git a/hooks/pre_deploy_hook.sh b/hooks/pre_deploy_hook.sh new file mode 100755 index 0000000..28131ee --- /dev/null +++ b/hooks/pre_deploy_hook.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Shell script for ask-cli pre-deploy hook for Node.js +# Script Usage: pre_deploy_hook.sh + +# SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. +# DO_DEBUG is boolean value for debug logging +# TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.) + +# Run this script under skill root folder + +# The script does the following: +# - Run "npm install" in each sourceDir in skill.json + +SKILL_NAME=$1 +DO_DEBUG=${2:-false} +TARGET=${3:-"all"} + +if [ $DO_DEBUG == false ] +then + exec > /dev/null 2>&1 +fi + +install_dependencies() { + npm install --prefix "$1" >/dev/null 2>&1 + return $? +} + +echo "###########################" +echo "##### pre-deploy hook #####" +echo "###########################" + +if [[ $TARGET == "all" || $TARGET == "lambda" ]]; then + grep "sourceDir" ./skill.json | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do + if install_dependencies $SOURCE_DIR; then + echo "Codebase ($SOURCE_DIR) built successfully." + else + echo "There was a problem installing dependencies for ($SOURCE_DIR)." + exit 1 + fi + done + echo "###########################" +fi + +exit 0 + diff --git a/lambda/custom/config.js b/lambda/custom/config.js new file mode 100644 index 0000000..9d9c7f8 --- /dev/null +++ b/lambda/custom/config.js @@ -0,0 +1,181 @@ +/** + To run the skill, the minimum values you need configure are: sellerId, sandboxCustomerEmailId, and bucketName + + A detailed list of attribute descriptions can be found here: + https://developer.integ.amazon.com/docs/amazon-pay/amazon-pay-apis-for-alexa.html +**/ + +'use strict'; + +const utilities = require( 'utilities' ); + +// Setup & Charge Payload attributes +const GENERAL = { + VERSION: '2.0', // Required; + needAmazonShippingAddress: true, // Optional; Must be boolean + paymentAction: 'AuthorizeAndCapture', // Required; 'Authorize' or 'AuthorizeAndCapture' + transactionTimeout: 0, // Optional; The default and recommended value for Alexa transactions is 0 + bucketName: 'INSERT-YOUR-S3-BUCKET-NAME' // Required; Used for skill state management +}; + +const REGIONAL = { + 'en-US': { + sellerId: 'INSERT-YOUR-AMAZON-PAY-SELLER-ID', // Required; Amazon Pay seller ID + checkoutLanguage: 'en_US', // Optional; US must be en_US + countryOfEstablishment: 'US', // Required; + ledgerCurrency: 'USD', // Required; + sandboxMode: true, // Optional; Must be false for certification & production; Must be true for sandbox testing + sandboxCustomerEmailId: 'INSERT-YOUR-SANDBOX-EMAIL-ADDRESS', // Optional; Required if sandboxMode equals true; Must setup Amazon Pay test account first + sellerAuthorizationNote: utilities.getSimulationString( '' ), // Optional; Max 255 chars + softDescriptor: 'No Nicks', // Optional; Max 16 chars; This value is visible on customers credit card statements + amount: '0.01', // Required; Max $150,000.00 USD; Intentionally set to $.01 in the demo for testing purposes. + currencyCode: 'USD', // Required; + + // SELLER ORDER ATTRIBUTES + customInformation: '', // Optional; Max 1024 chars + sellerNote: 'Thanks for shaving with No Nicks', // Optional; Max 1024 chars, visible on confirmation mails to buyers + sellerStoreName: 'No Nicks', // Optional; Documentation calls this out as storeName not sellerStoreName + } +}; + +/** + The following strings DO NOT interact with Amazon Pay + They are here to augment the skill + + Order Summary, Order Confirmation, Cancel and Refund Custom Intents are required for certification: + https://developer.amazon.com/docs/amazon-pay/certify-skill-with-amazon-pay.html +**/ + +// CARD INFORMATION + const storeURL = 'www.nonicks.com'; + const logoURL = 'https://s3-us-west-2.amazonaws.com/tcordov/no-nicks-logo-512.png'; + +// LAUNCH INTENT + const launchRequestWelcomeTitle = 'Welcome to '+ REGIONAL[ 'en-US' ].sellerStoreName +'. '; + const launchRequestWelcomeResponse = launchRequestWelcomeTitle +'We have everything you need for the perfect shave.'; + const launchRequestQuestionResponse = 'Are you interested in a starter kit, or refills?'; + +// NO INTENT + const noIntentResponse = 'Okay. Do you want to order something else?'; + +// CART SUMMARY + const cartSummaryCheckout = ' Do you want to check out now?'; + const cartSummarySubscription = ' Every 2 months, you’ll be charged {subscriptionPrice} dollars for your refill.'; + const cartSummaryResponse = 'Your total for the '+ REGIONAL[ 'en-US' ].sellerStoreName +' {productType} is {productPrice} dollars and will ship to your address at {shippingAddress}.'; + +// CANCEL & REFUND CONTACT DETAILS + const storePhoneNumber = '1-234-567-8910'; + const storeEmail = 'help@nonicks.com'; + const storeEmailPhonetic = 'help at no nicks dot com'; + +// REFUND INTENT - REQUIRED + const refundOrderTitle = 'Refund Order Details'; + const refundOrderIntentResponse = 'To request a refund, email '+ storeEmailPhonetic +', or call us. I sent contact information to your Alexa app.'; + const refundOrderCardResponse = 'Not completely happy with your order? We are here to help.\n To request a refund, contact us at '+ storePhoneNumber +' or email '+ storeEmail +'.'; + +// CANCEL INTENT - REQUIRED + const cancelOrderTitle = 'Cancel Order Details'; + const cancelOrderIntentResponse = 'To request a cancellation, email '+ storeEmailPhonetic +', or call us. I sent contact information to your Alexa app.'; + const cancelOrderCardResponse = 'Want to change or cancel your order? We are here to help.\n Contact us at '+ storePhoneNumber +' or email '+ storeEmail +'.'; + +// ORDER CONFIRMATION - REQUIRED + const confirmationTitle = 'Order Confirmation Details'; + const confirmationPlaceOrder = 'Your order has been placed.'; + const confirmationThanks = 'Thanks for shaving with '+ REGIONAL[ 'en-US' ].sellerStoreName +'.'; + const confirmationIntentResponse = REGIONAL[ 'en-US' ].sellerStoreName + ' will email you when your order ships. Thanks for shaving with '+ REGIONAL[ 'en-US' ].sellerStoreName +'.'; + const confirmationItems = 'Products: 1 {productType}'; + const confirmationTotal = 'Total amount: ${productPrice}'; + const confirmationTracking = 'Tracking number: 9400121699000025552416.'; + const confirmationCardResponse = confirmationPlaceOrder + '\n' + + confirmationItems + '\n' + + confirmationTotal + '\n' + + confirmationThanks + '\n' + + storeURL; +// ORDER TRACKER INTENT + const orderTrackerTitle = 'Order Status'; + const orderTrackerIntentResponse = 'Your order shipped via UPS, and delivery is estimated for this Friday. Check your order email for the tracking number.'; + const orderTrackerCardResponse = 'Your order #19206 was shipped via UPS and is estimated to arrive on Friday.\n You can check the status at any time using tracking number 9400121699000025552416.'; + +// HELP INTENT + const helpCommandsIntentResponse = 'To check order status, say where is my order. To cancel an order, say cancel order. To ask for a refund, say refund.'; + +// FALLBACK INTENT + const fallbackHelpMessage = 'Hmm, I\'m not sure about that. ' + helpCommandsIntentResponse; + +// EXITSKILL INTENT + const exitSkillResponse = 'OK, bye for now'; + + +/** + The following strings are used to output errors to test the skill +**/ + + +// ERROR RESPONSE STRINGS + const scope = 'payments:autopay_consent'; // Required; Used request permissions for Amazon Pay + const enablePermission = 'To make purchases in this skill, you need to enable Amazon Pay and turn on voice purchasing. To help, I sent a card to your Alexa app.'; + const errorMessage = 'Merchant error occurred. '; + const errorUnknown = 'Unknown error occurred. '; + const errorStatusCode = 'Status code: '; + const errorStatusMessage = ' Status message: '; + const errorPayloadMessage = ' Payload message: '; + const errorBillingAgreement = 'Billing agreement state is '; + const errorBillingAgreementMessage = '. Reach out to the user to resolve this issue.'; + const authorizationDeclineMessage = 'Your order was not placed and you have not been charged.'; + const debug = 'debug'; + + +module.exports = { + 'GENERAL': GENERAL, + 'REGIONAL': REGIONAL, + + // INTENT RESPONSE STRINGS + 'launchRequestWelcomeTitle': launchRequestWelcomeTitle, + 'launchRequestWelcomeResponse': launchRequestWelcomeResponse, + 'launchRequestQuestionResponse': launchRequestQuestionResponse, + + 'noIntentResponse': noIntentResponse, + + 'confirmationTitle': confirmationTitle, + 'confirmationIntentResponse': confirmationIntentResponse, + 'confirmationCardResponse': confirmationCardResponse, + + 'storeURL': storeURL, + 'logoURL': logoURL, + 'storePhoneNumber': storePhoneNumber, + + 'cancelOrderTitle': cancelOrderTitle, + 'cancelOrderIntentResponse': cancelOrderIntentResponse, + 'cancelOrderCardResponse': cancelOrderCardResponse, + + 'cartSummaryCheckout': cartSummaryCheckout, + 'cartSummarySubscription': cartSummarySubscription, + 'cartSummaryResponse': cartSummaryResponse, + + 'refundOrderTitle': refundOrderTitle, + 'refundOrderIntentResponse': refundOrderIntentResponse, + 'refundOrderCardResponse': refundOrderCardResponse, + + 'helpCommandsIntentResponse': helpCommandsIntentResponse, + + 'fallbackHelpMessage': fallbackHelpMessage, + + 'orderTrackerTitle': orderTrackerTitle, + 'orderTrackerIntentResponse': orderTrackerIntentResponse, + 'orderTrackerCardResponse': orderTrackerCardResponse, + + 'exitSkillResponse': exitSkillResponse, + + // ERROR RESPONSE STRINGS + 'enablePermission': enablePermission, + 'scope': scope, + 'errorMessage': errorMessage, + 'errorUnknown': errorUnknown, + 'errorStatusCode': errorStatusCode, + 'errorStatusMessage': errorStatusMessage, + 'errorPayloadMessage': errorPayloadMessage, + 'errorBillingAgreement': errorBillingAgreement, + 'errorBillingAgreementMessage': errorBillingAgreementMessage, + 'authorizationDeclineMessage': authorizationDeclineMessage, + 'debug': debug +}; \ No newline at end of file diff --git a/lambda/custom/directive-builder.js b/lambda/custom/directive-builder.js new file mode 100644 index 0000000..588300c --- /dev/null +++ b/lambda/custom/directive-builder.js @@ -0,0 +1,35 @@ +'use strict'; + +const directiveType = 'Connections.SendRequest'; +const setupDirective = { + name: 'Setup', +}; + +const chargeDirective = { + name: 'Charge', +}; + +function createDirective( name, payload, token ) { + var directive = {}; + directive.type = directiveType; + directive.name = name; + directive.payload = payload; + directive.token = token; + + return directive; +} + +function createSetupDirective( payload, token ) { + return createDirective( setupDirective.name, payload, token ); +} + +function createChargeDirective( payload, token ) { + return createDirective( chargeDirective.name, payload, token ); +} + +module.exports = { + 'createSetupDirective': createSetupDirective, + 'createChargeDirective': createChargeDirective, + 'setupDirectiveName': setupDirective.name, + 'chargeDirectiveName': chargeDirective.name, +}; \ No newline at end of file diff --git a/lambda/custom/error-handler.js b/lambda/custom/error-handler.js new file mode 100644 index 0000000..5687a8c --- /dev/null +++ b/lambda/custom/error-handler.js @@ -0,0 +1,129 @@ +'use strict'; + +const config = require( 'config' ); + +/** + A detailed list of all payment declines and processing errors can be found here: + https://developer.integ.amazon.com/docs/amazon-pay/payment-declines-and-processing-errors.html +**/ + + +// These are errors that will not be handled by Amazon Pay; Merchant must handle +function handleErrors( handlerInput ) { + let errorMessage = ''; + let permissionsError = false; + const actionResponseStatusCode = handlerInput.requestEnvelope.request.status.code; + const actionResponseStatusMessage = handlerInput.requestEnvelope.request.status.message; + const actionResponsePayloadMessage = handlerInput.requestEnvelope.request.payload.errorMessage; + + switch ( actionResponseStatusMessage ) { + // Permissions errors - These must be resolved before a user can use Amazon Pay + case 'ACCESS_DENIED': + case 'ACCESS_NOT_REQUESTED': // Amazon Pay permissions not enabled + case 'FORBIDDEN': + case 'VoicePurchaseNotEnabled': // Voice Purchase not enabled TODO: Add this to documentation + permissionsError = true; + errorMessage = config.enablePermission; + break; + + // Integration errors - These must be resolved before Amazon Pay can run + case 'BuyerEqualsSeller': + case 'InvalidParameterValue': + case 'InvalidSandboxCustomerEmail': + case 'InvalidSellerId': + case 'UnauthorizedAccess': + case 'UnsupportedCountryOfEstablishment': + case 'UnsupportedCurrency': + + // Runtime errors - These must be resolved before a charge action can occur + case 'DuplicateRequest': + case 'InternalServerError': + case 'InvalidAuthorizationAmount': + case 'InvalidBillingAgreementId': + case 'InvalidBillingAgreementStatus': + case 'InvalidPaymentAction': + case 'PeriodicAmountExceeded': + case 'ProviderNotAuthorized': + case 'ServiceUnavailable': + errorMessage = config.errorMessage + + config.errorStatusCode + actionResponseStatusCode + '.' + + config.errorStatusMessage + actionResponseStatusMessage + '.' + + config.errorPayloadMessage + actionResponsePayloadMessage; + break; + + default: + errorMessage = config.errorUnknown; + break; + } + + debug( handlerInput ); + + // If it is a permissions error send a permission consent card to the user, otherwise .speak() error to resolve during testing + if ( permissionsError ) { + return handlerInput.responseBuilder + .speak( errorMessage ) + .withAskForPermissionsConsentCard( [ config.scope ] ) + .getResponse( ); + } else { + return handlerInput.responseBuilder + .speak( errorMessage ) + .getResponse( ); + } +} + +// If billing agreement equals any of these states, you need to get the user to update their payment method +// Once payment method is updated, billing agreement state will go back to OPEN and you can charge the payment method +function handleBillingAgreementState( billingAgreementStatus, handlerInput ) { + let errorMessage = ''; + + switch ( billingAgreementStatus ) { + case 'CANCELED': + case 'CLOSED': + case 'SUSPENDED': + errorMessage = config.errorBillingAgreement + billingAgreementStatus + config.errorBillingAgreementMessage; + break; + default: + errorMessage = config.errorUnknown; + } + + debug( handlerInput ); + + return handlerInput.responseBuilder + .speak( errorMessage ) + .getResponse( ); +} + +// Ideal scenario in authorization decline is that you save the session, allow the customer to fix their payment method, +// and allow customer to resume session. This is just a simple message to tell the user their order was not placed. +function handleAuthorizationDeclines( authorizationStatusReasonCode, handlerInput ) { + let errorMessage = ''; + + switch ( authorizationStatusReasonCode ) { + case 'AmazonRejected': + case 'InvalidPaymentMethod': + case 'ProcessingFailure': + case 'TransactionTimedOut': + errorMessage = config.authorizationDeclineMessage; + break; + default: + errorMessage = config.errorUnknown; + } + + debug( handlerInput ); + + return handlerInput.responseBuilder + .speak( errorMessage ) + .getResponse( ); +} + +// Output object to console for debugging purposes +function debug( handlerInput, message ) { + console.log( config.debug + ' ' + message + JSON.stringify( handlerInput ) ); +} + +module.exports = { + 'handleErrors': handleErrors, + 'handleBillingAgreementState': handleBillingAgreementState, + 'handleAuthorizationDeclines': handleAuthorizationDeclines, + 'debug': debug +}; \ No newline at end of file diff --git a/lambda/custom/index.js b/lambda/custom/index.js new file mode 100644 index 0000000..2cb644f --- /dev/null +++ b/lambda/custom/index.js @@ -0,0 +1,627 @@ +/** + This skill is built for Nodejs using Alexa ASK V2.0.3 + Download the SDK here: https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs +**/ + +'use strict'; + +const askSDK = require( 'ask-sdk-core' ); +const config = require( 'config' ); +const error = require( 'error-handler' ); +const utilities = require( 'utilities' ); +const directiveBuilder = require( 'directive-builder' ); +const payloadBuilder = require( 'payload-builder' ); +const s3Adapter = require( 'ask-sdk-s3-persistence-adapter' ).S3PersistenceAdapter; +let persistence = ''; +const products = Object.freeze({ + KIT: 'kit', + UPGRADE: 'upgrade', + REFILL: 'refill' + }); + +// Welcome, are you interested in a starter kit or a refill subscription? +const LaunchRequestHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; + }, + handle( handlerInput ) { + // Prevent a previous session's setup from being called prematurely + utilities.resetSetup( handlerInput ); + + return handlerInput.responseBuilder + .speak( config.launchRequestWelcomeResponse + ' ' + config.launchRequestQuestionResponse ) + .withStandardCard( config.launchRequestWelcomeTitle, config.storeURL, config.logoURL ) + .reprompt( config.launchRequestQuestionResponse ) + .withShouldEndSession( false ) + .getResponse( ); + } +}; + +// Do you want to purchase a starter kit, upgrade the starter kit, or buy something else? +const InProgressStarterKitIntent = { + canHandle( handlerInput ) { + const request = handlerInput.requestEnvelope.request; + + return request.type === 'IntentRequest' && + request.intent.name === 'StarterKitIntent' && + request.dialogState !== 'COMPLETED'; + }, + handle( handlerInput ) { + const currentIntent = handlerInput.requestEnvelope.request.intent; + + for ( const slotName of Object.keys( handlerInput.requestEnvelope.request.intent.slots ) ) { + const currentSlot = currentIntent.slots[ slotName ]; + + if ( currentSlot.confirmationStatus !== 'CONFIRMED' && currentSlot.resolutions && currentSlot.resolutions.resolutionsPerAuthority[ 0 ] ) { + if ( currentSlot.resolutions.resolutionsPerAuthority[ 0 ].status.code === 'ER_SUCCESS_MATCH' ) { + const currentSlotValue = currentSlot.resolutions.resolutionsPerAuthority[ 0 ].values[ 0 ].value.name; + + // No, I do not want to buy the starter kit + if ( currentSlot.name === 'KitPurchaseIntentSlot' && currentSlotValue === 'no' ) { + + // Do you want to buy something else? + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + + attributes.reengage = true; + attributesManager.setSessionAttributes( attributes ); + + return handlerInput.responseBuilder + .speak( config.noIntentResponse ) + .withShouldEndSession( false ) + .getResponse( ); + } + + // No, I do not want to upgrade, just buy the starter kit + if ( currentSlot.name === 'UpgradeKitIntentSlot' && currentSlotValue === 'no' ) { + return amazonPaySetup( handlerInput, products.KIT ); + } + + // Yes, I do want to upgrade the starter kit + if ( currentSlot.name === 'UpgradeKitIntentSlot' && currentSlotValue === 'yes' ) { + return amazonPaySetup( handlerInput, products.UPGRADE ); + } + + // TODO: Combine refill subscription logic here + + } else { + console.log( 'Error: Had no match for products' ); + } + } + } + + return handlerInput.responseBuilder + .addDelegateDirective( currentIntent ) + .getResponse( ); + } +}; + +// Do you want to buy a refill subscription or buy something else? +const CompletedRefillIntentHandler = { + canHandle( handlerInput ) { + const request = handlerInput.requestEnvelope.request; + + return request.type === 'IntentRequest' && + request.intent.name === 'RefillIntent' && + request.dialogState === 'COMPLETED'; + }, + handle( handlerInput ) { + const filledSlots = handlerInput.requestEnvelope.request.intent.slots; + const slotValues = utilities.getSlotValues( filledSlots ); + const yesNoResponse = `${ slotValues.RefillPurchaseIntentSlot.resolved }`; + + // No, I do not want to buy the refill subscription + if ( yesNoResponse === 'no' ){ + + // Do you want to buy something else? + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + + attributes.reengage = true; + attributesManager.setSessionAttributes( attributes ); + + return handlerInput.responseBuilder + .speak( config.noIntentResponse ) + .withShouldEndSession( false ) + .getResponse(); + } else { + // Yes, I want to buy the refill subscription + return amazonPaySetup( handlerInput, products.REFILL ); + } + } +}; + +// I want to place my order +const YesIntentHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'IntentRequest' && + handlerInput.requestEnvelope.request.intent.name === 'AMAZON.YesIntent'; + }, + handle( handlerInput ) { + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + const setupDirectiveSent = attributes.setup; + + if ( attributes.reengage ) { + attributes.reengage = false; + attributesManager.setSessionAttributes( attributes ); + + return handlerInput.responseBuilder + .speak( config.launchRequestQuestionResponse ) + .reprompt( config.launchRequestQuestionResponse ) + .withShouldEndSession( false ) + .getResponse( ); + } + + // Did we already send the setup directive request? + if ( setupDirectiveSent ) { + utilities.resetSetup( handlerInput ); + + // Charge the user + return amazonPayCharge( handlerInput ); + } else { + // This is added with the intent that it is developer facing only + // Do not leave this here for production skills as customers will recieve these messages + return handlerInput.responseBuilder + .speak( 'Error: Check the yes intent handler' ) + .withShouldEndSession( false ) + .getResponse( ); + } + } +}; + +// No, I don't want to do that +const NoIntentHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'IntentRequest' && + handlerInput.requestEnvelope.request.intent.name === 'AMAZON.NoIntent'; + }, + handle( handlerInput ) { + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + + if( attributes.reengage ) { + attributes.reengage = false; + attributesManager.setSessionAttributes( attributes ); + + return ExitSkillIntentHandler.handle( handlerInput ); + } + + if( attributes.setup ) { + // Customer decided to not checkout, while having filled the cart already + attributes.reengage = true; + attributesManager.setSessionAttributes( attributes ); + + return handlerInput.responseBuilder + .speak( config.noIntentResponse ) + .withShouldEndSession( false ) + .getResponse( ); + } + + // Catch unexpected No's + return FallbackIntentHandler.handle( handlerInput ); + } +}; + +// You requested the Setup directive and are now receiving the Connections.Response +const SetupConnectionsResponseHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'Connections.Response' && + handlerInput.requestEnvelope.request.name === directiveBuilder.setupDirectiveName; + }, + handle( handlerInput ) { + const connectionResponsePayload = handlerInput.requestEnvelope.request.payload; + const connectionResponseStatusCode = handlerInput.requestEnvelope.request.status.code; + + // If there are integration or runtime errors, do not charge the payment method + if ( connectionResponseStatusCode != 200 ) { + return error.handleErrors( handlerInput ); + } + + // Get the billingAgreementId and billingAgreementStatus from the Setup Connections.Response + const billingAgreementId = connectionResponsePayload.billingAgreementDetails.billingAgreementId; + const billingAgreementStatus = connectionResponsePayload.billingAgreementDetails.billingAgreementStatus; + + // If billingAgreementStatus is valid, Charge the payment method + if ( billingAgreementStatus === 'OPEN' ) { + + // Save billingAgreementId attributes because directives will close the session + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + + attributes.billingAgreementId = billingAgreementId; + attributes.setup = true; + attributesManager.setSessionAttributes( attributes ); + + const shippingAddress = connectionResponsePayload.billingAgreementDetails.destination.addressLine1; + let productType = attributes.productType; + let cartSummaryResponse = generateResponse( 'summary', config.cartSummaryResponse, productType, shippingAddress ); + + return handlerInput.responseBuilder + .speak( cartSummaryResponse ) + .withShouldEndSession( false ) + .getResponse( ); + + // If billingAgreementStatus is not valid, do not Charge the payment method + } else { + return error.handleBillingAgreementState( billingAgreementStatus, handlerInput ); + } + } +}; + +// You requested the Charge directive and are now receiving the Connections.Response +const ChargeConnectionsResponseHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'Connections.Response' && + handlerInput.requestEnvelope.request.name === directiveBuilder.chargeDirectiveName; + }, + handle( handlerInput ) { + const connectionResponsePayload = handlerInput.requestEnvelope.request.payload; + const connectionResponseStatusCode = handlerInput.requestEnvelope.request.status.code; + + // If there are integration or runtime errors, do not charge the payment method + if ( connectionResponseStatusCode != 200 ) { + return error.handleErrors( handlerInput ); + } + + const authorizationStatusState = connectionResponsePayload.authorizationDetails.state; + + // Authorization is declined, tell the customer their order was not placed + if( authorizationStatusState === 'Declined' ) { + const authorizationStatusReasonCode = connectionResponsePayload.authorizationDetails.reasonCode; + + return error.handleAuthorizationDeclines( authorizationStatusReasonCode, handlerInput ); + + // CERTIFICATION REQUIREMENT + // Authorization is approved, tell the customer their order was placed and send them a card with order details + } else { + // Get the productType attribute + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + const productType = attributes.productType; + let confirmationCardResponse = generateResponse( 'confirmation', config.confirmationCardResponse, productType, null ); + + return handlerInput.responseBuilder + .speak( config.confirmationIntentResponse ) + .withStandardCard( config.confirmationTitle, confirmationCardResponse, config.logoURL ) + .withShouldEndSession( true ) + .getResponse( ); + } + } +}; + +// CERTIFICATION REQUIREMENT +// Tell the customer how they can request a refund +const RefundOrderIntentHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'IntentRequest' && + handlerInput.requestEnvelope.request.intent.name === 'RefundOrderIntent'; + }, + handle( handlerInput ) { + return handlerInput.responseBuilder + .speak( config.refundOrderIntentResponse ) + .withStandardCard( config.refundOrderTitle, config.refundOrderCardResponse, config.logoURL ) + .withShouldEndSession( false ) + .getResponse( ); + } +}; + +// CERTIFICATION REQUIREMENT +// Tell the customer how they can cancel an order +const CancelOrderIntentHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'IntentRequest' && + handlerInput.requestEnvelope.request.intent.name === 'CancelOrderIntent'; + }, + handle( handlerInput ) { + return handlerInput.responseBuilder + .speak( config.cancelOrderIntentResponse ) + .withStandardCard( config.cancelOrderTitle, config.cancelOrderCardResponse, config.logoURL ) + .withShouldEndSession( false ) + .getResponse( ); + } +}; + +// Help the customer +const HelpIntentHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'IntentRequest' && + handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent'; + }, + handle( handlerInput ) { + return handlerInput.responseBuilder + .speak( config.helpCommandsIntentResponse ) + .withShouldEndSession( false ) + .getResponse( ); + } +}; + +// Where is my order? Send customer their tracking information and status +const OrderTrackerIntentHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'IntentRequest' && + handlerInput.requestEnvelope.request.intent.name === 'OrderTrackerIntent'; + }, + handle( handlerInput ) { + // Implement your code here to query the respective shipping service API's. This demo simply returns a static message. + return handlerInput.responseBuilder + .speak( config.orderTrackerIntentResponse ) + .withStandardCard( config.orderTrackerTitle, config.orderTrackerCardResponse, config.logoURL ) + .withShouldEndSession( false ) + .getResponse( ); + } +}; + +// I want to exit the skill +const ExitSkillIntentHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'IntentRequest' && ( + handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent' || + handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'); + }, + handle( handlerInput ) { + return handlerInput.responseBuilder + .speak( config.exitSkillResponse ) + .withShouldEndSession( true ) + .getResponse( ); + } +}; + +// End session +const SessionEndedRequestHandler = { + canHandle(handlerInput) { + return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest'; + }, + handle(handlerInput) { + return handlerInput.responseBuilder + .speak( config.exitSkillResponse ) + .withShouldEndSession( true ) + .getResponse(); + }, +}; + +// Fallback handler +const FallbackIntentHandler = { + canHandle( handlerInput ) { + return handlerInput.requestEnvelope.request.type === 'IntentRequest' && + handlerInput.requestEnvelope.request.intent.name === 'AMAZON.FallbackIntent'; + }, + handle( handlerInput ) { + return handlerInput.responseBuilder + .speak( config.fallbackHelpMessage ) + .withShouldEndSession( false ) + .getResponse( ); + } +}; + +// Generic error handling +const ErrorHandler = { + canHandle( ) { + return true; + }, + handle( handlerInput, error ) { + // This is added with the intent that it is developer facing only + // Do not leave this here for production skills as customers will recieve these messages + const speechText = config.errorUnknown + ' ' + error.message; + + return handlerInput.responseBuilder + .speak( speechText ) + .reprompt( speechText ) + .getResponse(); + } +}; + +// This request interceptor with each new session loads all global persistent attributes +// into the session attributes and increments a launch counter +const PersistenceRequestInterceptor = { + process( handlerInput ) { + if ( handlerInput.requestEnvelope.session[ 'new' ] ) { + return new Promise( ( resolve, reject ) => { + handlerInput.attributesManager.getPersistentAttributes( ) + .then( ( persistentAttributes ) => { + persistentAttributes = persistentAttributes || {}; + + if ( !persistentAttributes.launchCount ) + persistentAttributes.launchCount = 0; + persistentAttributes.launchCount += 1; + handlerInput.attributesManager.setSessionAttributes( persistentAttributes ); + resolve( ); + } ) + .catch( ( err ) => { + reject( err ); + } ); + } ); + } + } +}; + +// This response interceptor stores all session attributes into global persistent attributes +// when the session ends and it stores the skill last used timestamp +const PersistenceResponseInterceptor = { + process( handlerInput, responseOutput ) { + const ses = ( typeof responseOutput.shouldEndSession === 'undefined' ? true : responseOutput.shouldEndSession ); + + if ( ses || handlerInput.requestEnvelope.request.type === 'SessionEndedRequest' ) { // skill was stopped or timed out + let sessionAttributes = handlerInput.attributesManager.getSessionAttributes( ); + + sessionAttributes.lastUseTimestamp = new Date( handlerInput.requestEnvelope.request.timestamp ).getTime( ); + handlerInput.attributesManager.setPersistentAttributes( sessionAttributes ); + + return new Promise( ( resolve, reject ) => { + handlerInput.attributesManager.savePersistentAttributes( ) + .then( ( ) => { + resolve( ); + } ) + .catch( ( err ) => { + reject( err ); + } ); + } ); + } + } +}; + + +// Customer has shown intent to purchase, call Setup to grab the customers shipping address details +function amazonPaySetup ( handlerInput, productType ) { + + // Save session attributes because skill connection directives will close the session + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + + attributes.productType = productType; + attributesManager.setSessionAttributes( attributes ); + + // Permission check + handleMissingAmazonPayPermission( handlerInput ); + + const permissions = handlerInput.requestEnvelope.context.System.user.permissions; + const amazonPayPermission = permissions.scopes[ config.scope ]; + + if ( amazonPayPermission.status === 'DENIED' ) { + return handlerInput.responseBuilder + .speak( config.enablePermission ) + .withAskForPermissionsConsentCard( [ config.scope ] ) + .getResponse(); + } + + var foo = handlerInput.requestEnvelope.request.locale ; + + // If you have a valid billing agreement from a previous session, skip the Setup action and call the Charge action instead + const token = utilities.generateRandomString( 12 ); + + // If you do not have a billing agreement, set the Setup payload and send the request directive + const setupPayload = payloadBuilder.setupPayload( handlerInput.requestEnvelope.request.locale ); + const setupRequestDirective = directiveBuilder.createSetupDirective( setupPayload, token ); + + + return handlerInput.responseBuilder + .addDirective( setupRequestDirective ) + .withShouldEndSession( true ) + .getResponse( ); +} + +// Customer has requested checkout and wants to be charged +function amazonPayCharge ( handlerInput ) { + + // Permission check + handleMissingAmazonPayPermission( handlerInput ); + const permissions = handlerInput.requestEnvelope.context.System.user.permissions; + const amazonPayPermission = permissions.scopes[ config.scope ]; + + if ( amazonPayPermission.status === 'DENIED' ) { + return handlerInput.responseBuilder + .speak( config.enablePermission ) + .withAskForPermissionsConsentCard( [ config.scope ] ) + .getResponse(); + } + + // Get session attributes + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + const billingAgreementId = attributes.billingAgreementId; + const authorizationReferenceId = utilities.generateRandomString( 16 ); + const sellerOrderId = utilities.generateRandomString( 6 ); + const locale = handlerInput.requestEnvelope.request.locale; + const token = utilities.generateRandomString( 12 ); + const amount = config.REGIONAL[locale].amount; + + // Set the Charge payload and send the request directive + const chargePayload = payloadBuilder.chargePayload(billingAgreementId, authorizationReferenceId, sellerOrderId, amount, locale); + const chargeRequestDirective = directiveBuilder.createChargeDirective(chargePayload, token); + + return handlerInput.responseBuilder + .addDirective( chargeRequestDirective ) + .withShouldEndSession( true ) + .getResponse( ); +} + +// Returns product specific string for summary or checkout intent responses +function generateResponse ( stage, template, productType, shippingAddress ) { + let productPrice = ''; + let subscriptionPrice = ''; + let cartSummaryResponse = template; + let cartSummarySubscription = config.cartSummarySubscription; + let confirmationCardResponse = template; + let confirmationItem = ''; + + switch ( productType ) { + case products.KIT: + productType = 'Starter Kit'; + confirmationItem = 'Starter Kit'; + productPrice = 9; + cartSummarySubscription = ''; + break; + + case products.REFILL: + confirmationItem = 'Refill Subscription'; + productPrice = 20; + subscriptionPrice = 20; + cartSummarySubscription = cartSummarySubscription.replace( '{subscriptionPrice}', subscriptionPrice ); + break; + + case products.UPGRADE: + productType = 'Starter Kit'; + confirmationItem = 'Starter Kit + Refill Subscription'; + productPrice = 9; + subscriptionPrice = 18; + cartSummarySubscription = cartSummarySubscription.replace( '{subscriptionPrice}', subscriptionPrice ); + break; + + default: + console.log( 'Setup Error with productType' ); + + // This is added with the intent that it is developer facing only + // Do not leave this here for production skills as customers will recieve these messages + cartSummaryResponse = 'Error Setup with productType'; + break; + } + + cartSummaryResponse = cartSummaryResponse.replace( '{productType}', productType ).replace( '{productPrice}', productPrice ).replace( '{shippingAddress}', shippingAddress ); + cartSummaryResponse += cartSummarySubscription + config.cartSummaryCheckout; + + confirmationCardResponse = confirmationCardResponse.replace( '{productType}' , confirmationItem ).replace( '{productPrice}' , productPrice ); + + if ( stage === 'summary' ) { + return cartSummaryResponse; + } else if ( stage === 'confirmation') { + return confirmationCardResponse; + } +} + +function handleMissingAmazonPayPermission( handlerInput ) { + const permissions = handlerInput.requestEnvelope.context.System.user.permissions; + const amazonPayPermission = permissions.scopes[ config.scope ]; + + if ( amazonPayPermission.status === 'DENIED' ) { + return handlerInput.responseBuilder + .speak( config.enablePermission ) + .withAskForPermissionsConsentCard( [ config.scope ] ) + .getResponse(); + } +} + +exports.handler = askSDK.SkillBuilders + .custom( ) + .addRequestHandlers( + LaunchRequestHandler, + InProgressStarterKitIntent, + CompletedRefillIntentHandler, + YesIntentHandler, + NoIntentHandler, + SetupConnectionsResponseHandler, + ChargeConnectionsResponseHandler, + RefundOrderIntentHandler, + CancelOrderIntentHandler, + HelpIntentHandler, + OrderTrackerIntentHandler, + ExitSkillIntentHandler, + SessionEndedRequestHandler, + FallbackIntentHandler + ) + .addRequestInterceptors( PersistenceRequestInterceptor ) + .addResponseInterceptors( PersistenceResponseInterceptor ) + .withPersistenceAdapter( persistence = new s3Adapter( + { bucketName: config.GENERAL.bucketName } ) ) + .addErrorHandlers( + ErrorHandler ) + .lambda( ); \ No newline at end of file diff --git a/lambda/custom/package-lock.json b/lambda/custom/package-lock.json new file mode 100644 index 0000000..54746fd --- /dev/null +++ b/lambda/custom/package-lock.json @@ -0,0 +1,146 @@ +{ + "name": "skill-sample-nodejs-amazon-pay", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ask-sdk": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ask-sdk/-/ask-sdk-2.4.0.tgz", + "integrity": "sha512-EnfXVoxFbSUhejf5yOq1HD2xSNBoi12I6jn0584HTjuIklJe9BsYuwPfFkZlh0yRw1RESZa1MU/Ej4nUk80bKw==", + "requires": { + "ask-sdk-core": "^2.4.0", + "ask-sdk-dynamodb-persistence-adapter": "^2.4.0", + "ask-sdk-model": "^1.0.0" + } + }, + "ask-sdk-core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.4.0.tgz", + "integrity": "sha512-Ss/Z2mJ2RXx/qZPbRB8P5lRBfyouRks16b2wl2XPAL2DW9BDe1KHFsMQAYMRz9Xs5UcMuVEL+pe8H7tp+QyB8w==", + "requires": { + "ask-sdk-runtime": "^2.4.0" + } + }, + "ask-sdk-dynamodb-persistence-adapter": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ask-sdk-dynamodb-persistence-adapter/-/ask-sdk-dynamodb-persistence-adapter-2.4.0.tgz", + "integrity": "sha512-csXYLQ1OQCQVQLoAPb6t+kOxCWzAtupXjRgxFUlRIRyOoEW4s2m5H76Is2Mb1vOso1AVda/mvJ5+dHaBFRK2iA==", + "requires": { + "aws-sdk": "^2.163.0" + } + }, + "ask-sdk-model": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.11.2.tgz", + "integrity": "sha512-nxXvf3NRfKbsKVegHrYqDaCn0EMylSuPKXn7SsMmgRJyi5HrxVycb1X/moQVoBJ06ZwGtvbhcI4AhtcWDbZFnw==" + }, + "ask-sdk-runtime": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ask-sdk-runtime/-/ask-sdk-runtime-2.4.0.tgz", + "integrity": "sha512-NiTXGeljx3wV9GIbwOvYc3EKPLTR1ZVoJBGNNyoAwg89v1L/hZgg5p3rJM0YJfIqw6HJUHJbtdJsRIHtkscxRA==" + }, + "ask-sdk-s3-persistence-adapter": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ask-sdk-s3-persistence-adapter/-/ask-sdk-s3-persistence-adapter-2.4.0.tgz", + "integrity": "sha512-FbB5pIFM7tfn/Iy6JSWBWCwNvD9dQ2l2Xq4/q3mN5r60bZiv5rQif4c4UFTjvB6Tc5FZPuEXLmCRN7KQSLz5NA==", + "requires": { + "aws-sdk": "^2.163.0" + } + }, + "aws-sdk": { + "version": "2.401.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.401.0.tgz", + "integrity": "sha512-mOI4gzKoP/g8Q0ToAaqTh7TijGG9PvGVVUkKmurXqBKy7GTPmy4JizfVkTrM+iBg7RAsx5H2lBxBFpdEFBa5fg==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + } + } +} diff --git a/lambda/custom/package.json b/lambda/custom/package.json new file mode 100644 index 0000000..4e8df26 --- /dev/null +++ b/lambda/custom/package.json @@ -0,0 +1,12 @@ +{ + "name": "skill-sample-nodejs-amazon-pay", + "version": "1.0.0", + "description": "This demo showcases how to integrate Amazon Pay into a shopping experience.", + "author": "Tony Cordova", + "license": "ISC", + "dependencies": { + "ask-sdk": "^2.4.0", + "ask-sdk-core": "^2.4.0", + "ask-sdk-s3-persistence-adapter": "^2.4.0" + } +} diff --git a/lambda/custom/payload-builder.js b/lambda/custom/payload-builder.js new file mode 100644 index 0000000..88ceb82 --- /dev/null +++ b/lambda/custom/payload-builder.js @@ -0,0 +1,84 @@ +const utilities = require( 'utilities' ); +let config = require( 'config' ); + +const setupPayloadVersioning = { + type: 'SetupAmazonPayRequest', + version: '2' +}; + +const processPayloadVersioning = { + type: 'ChargeAmazonPayRequest', + version: '2' +}; + +var setupPayload = function( language ) { + console.log( language ); + const regionalConfig = config.REGIONAL[ language ]; + const generalConfig = config.GENERAL; + var payload = { + '@type': setupPayloadVersioning.type, + '@version': setupPayloadVersioning.version, + 'sellerId': regionalConfig.sellerId, + 'countryOfEstablishment': regionalConfig.countryOfEstablishment, + 'ledgerCurrency': regionalConfig.ledgerCurrency, + 'checkoutLanguage': language, + 'sandboxCustomerEmailId': regionalConfig.sandboxCustomerEmailId, + 'sandboxMode': regionalConfig.sandboxMode, + 'needAmazonShippingAddress': generalConfig.needAmazonShippingAddress, + 'billingAgreementAttributes': { + '@type': 'BillingAgreementAttributes', + '@version': '2', + 'sellerNote': regionalConfig.sellerNote, + 'platformId': generalConfig.platformId, + 'sellerBillingAgreementAttributes': { + '@type': 'SellerBillingAgreementAttributes', + '@version': '2', + //'sellerBillingAgreementId': SOME RANDOM STRING, + 'storeName': generalConfig.sellerStoreName, + 'customInformation': regionalConfig.customInformation + } + } + }; + + return payload; +}; +var chargePayload = function( billingAgreementId, authorizationReferenceId, sellerOrderId, amount, language ) { + + const regionalConfig = config.REGIONAL[ language ]; + const generalConfig = config.GENERAL; + var payload = { + '@type': processPayloadVersioning.type, + '@version': processPayloadVersioning.version, + 'sellerId': regionalConfig.sellerId, + 'billingAgreementId': billingAgreementId, + 'paymentAction': generalConfig.paymentAction, + 'authorizeAttributes': { + '@type': 'AuthorizeAttributes', + '@version': '2', + 'authorizationReferenceId': authorizationReferenceId, + 'authorizationAmount': { + '@type': 'Price', + '@version': '2', + 'amount': amount.toString( ), + 'currencyCode': regionalConfig.ledgerCurrency + }, + 'transactionTimeout': generalConfig.transactionTimeout, + 'sellerAuthorizationNote': regionalConfig.sellerAuthorizationNote, // util.getSimulationString('AmazonRejected'), + 'softDescriptor': regionalConfig.softDescriptor + }, + 'sellerOrderAttributes': { + '@type': 'SellerOrderAttributes', + '@version': '2', + // 'sellerOrderId': sellerOrderId, + 'storeName': regionalConfig.sellerStoreName, + 'customInformation': regionalConfig.customInformation, + 'sellerNote': regionalConfig.sellerNote + } + }; + return payload; +}; + +module.exports = { + 'setupPayload': setupPayload, + 'chargePayload': chargePayload +}; \ No newline at end of file diff --git a/lambda/custom/utilities.js b/lambda/custom/utilities.js new file mode 100644 index 0000000..c0ea4c5 --- /dev/null +++ b/lambda/custom/utilities.js @@ -0,0 +1,103 @@ +'use strict'; + +/** + A detailed list simulation strings to use in sandboxMode can be found here: + https://pay.amazon.com/us/developer/documentation/lpwa/201956480#201956480 + + Used for testing simulation strings in sandbox mode +**/ + +function getSimulationString( type ) { + let simulationString = ''; + + switch( type ) { + case 'InvalidPaymentMethod': + // PaymentMethodUpdateTimeInMins only works with Async authorizations to change BA back to OPEN; Sync authorizations will not revert + simulationString = '{ "SandboxSimulation": { "State":"Declined", "ReasonCode":"InvalidPaymentMethod", "PaymentMethodUpdateTimeInMins":1, "SoftDecline":"true" } }'; + break; + + case 'AmazonRejected': + simulationString = '{ "SandboxSimulation": { "State":"Declined", "ReasonCode":"AmazonRejected" } }'; + break; + + case 'TransactionTimedOut': + simulationString = '{ "SandboxSimulation": { "State":"Declined", "ReasonCode":"TransactionTimedOut" } }'; + break; + + default: + simulationString = ''; + } + + return simulationString; +} + +// Sometimes you just need a random string, right? +function generateRandomString( length ) { + let randomString = ''; + const stringValues = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for ( let i = 0; i < length; i++ ) + randomString += stringValues.charAt( Math.floor( Math.random( ) * stringValues.length ) ); + + return randomString; +} + +// Get intent slot values +function getSlotValues( filledSlots ) { + const slotValues = {}; + + console.log( `The filled slots: ${JSON.stringify(filledSlots)}` ); + Object.keys( filledSlots ).forEach( ( item ) => { + const name = filledSlots[ item ].name; + + if ( filledSlots[ item ] && + filledSlots[ item ].resolutions && + filledSlots[ item ].resolutions.resolutionsPerAuthority[ 0 ] && + filledSlots[ item ].resolutions.resolutionsPerAuthority[ 0 ].status && + filledSlots[ item ].resolutions.resolutionsPerAuthority[ 0 ].status.code ) { + switch ( filledSlots[ item ].resolutions.resolutionsPerAuthority[ 0 ].status.code ) { + case 'ER_SUCCESS_MATCH': + slotValues[ name ] = { + synonym: filledSlots[ item ].value, + resolved: filledSlots[ item ].resolutions.resolutionsPerAuthority[ 0 ].values[ 0 ].value.name, + isValidated: true, + }; + break; + case 'ER_SUCCESS_NO_MATCH': + slotValues[ name ] = { + synonym: filledSlots[ item ].value, + resolved: filledSlots[ item ].value, + isValidated: false, + }; + break; + default: + break; + } + } else { + slotValues[ name ] = { + synonym: filledSlots[ item ].value, + resolved: filledSlots[ item ].value, + isValidated: false, + }; + } + }, this ); + + return slotValues; +} + +// Prevent a previous session's setup from being called prematurely +function resetSetup ( handlerInput ) { + const { attributesManager } = handlerInput; + let attributes = attributesManager.getSessionAttributes( ); + + attributes.setup = false; + attributesManager.setSessionAttributes( attributes ); +} + +module.exports = { + 'generateRandomString': generateRandomString, + 'getSimulationString': getSimulationString, + 'getSlotValues': getSlotValues, + 'resetSetup': resetSetup, +}; + diff --git a/models/en-US.json b/models/en-US.json new file mode 100644 index 0000000..387e6f9 --- /dev/null +++ b/models/en-US.json @@ -0,0 +1,344 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "no nicks", + "intents": [ + { + "name": "AMAZON.HelpIntent", + "samples": [ + "help" + ] + }, + { + "name": "RefundOrderIntent", + "slots": [], + "samples": [ + "refund", + "refund order", + "refund my order", + "i'd like my order refunded", + "i'd like a refund", + "i need a refund", + "refund the order", + "please give me a refund", + "give me a refund", + "i would like a refund" + ] + }, + { + "name": "CancelOrderIntent", + "slots": [], + "samples": [ + "cancellation", + "cancel order", + "cancel my order", + "cancel the order", + "cancel that order", + "i need to cancel the order", + "i would like to cancel my order", + "cancel order please" + ] + }, + { + "name": "CheckoutIntent", + "slots": [], + "samples": [ + "place my order", + "place order", + "checkout", + "i want to checkout", + "buy", + "buy now", + "checkout please", + "complete order" + ] + }, + { + "name": "OrderTrackerIntent", + "slots": [], + "samples": [ + "did my order ship", + "when will my item ship", + "where's my order", + "where is my order" + ] + }, + { + "name": "StarterKitIntent", + "slots": [ + { + "name": "KitPurchaseIntentSlot", + "type": "YesNoType", + "samples": [ + "no", + "yes" + ] + }, + { + "name": "UpgradeKitIntentSlot", + "type": "YesNoType", + "samples": [ + "no", + "yes" + ] + } + ], + "samples": [ + "order", + "a kit", + "starter kit", + "kit", + "I need a razor", + "the starter kit", + "get a kit", + "get a starter kit", + "I want a starter kit", + "I want a kit", + "I want to order a kit", + "I want to order a starter kit", + "a starter kit" + ] + }, + { + "name": "RefillIntent", + "slots": [ + { + "name": "RefillPurchaseIntentSlot", + "type": "YesNoType" + } + ], + "samples": [ + "{RefillPurchaseIntentSlot} more refills", + "more refills", + "refills", + "replacements", + "blade refills", + "shaving cream", + "schedule refills", + "get refills", + "refill", + "I want refills", + "I want to order refills" + ] + }, + { + "name": "AMAZON.NavigateHomeIntent", + "samples": [] + }, + { + "name": "AMAZON.YesIntent", + "samples": [ + "sure", + "ok", + "yup", + "yes" + ] + },{ + "name": "AMAZON.NoIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [ + "stop", + "exit", + "quit", + "close" + ] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + } + ], + "types": [ + { + "name": "YesNoType", + "values": [ + { + "name": { + "value": "no", + "synonyms": [ + "naw", + "nope" + ] + } + }, + { + "name": { + "value": "yes", + "synonyms": [ + "right", + "ok", + "yup", + "sure" + ] + } + } + ] + } + ] + }, + "dialog": { + "intents": [ + { + "name": "StarterKitIntent", + "confirmationRequired": false, + "prompts": {}, + "slots": [ + { + "name": "KitPurchaseIntentSlot", + "type": "YesNoType", + "confirmationRequired": false, + "elicitationRequired": true, + "prompts": { + "elicitation": "Elicit.Slot.295132847200.649643899751" + } + }, + { + "name": "UpgradeKitIntentSlot", + "type": "YesNoType", + "confirmationRequired": false, + "elicitationRequired": true, + "prompts": { + "elicitation": "Elicit.Slot.81496166760.874607615560" + } + } + ] + }, + { + "name": "RefillIntent", + "delegationStrategy": "ALWAYS", + "confirmationRequired": false, + "prompts": {}, + "slots": [ + { + "name": "RefillPurchaseIntentSlot", + "type": "YesNoType", + "confirmationRequired": false, + "elicitationRequired": true, + "prompts": { + "elicitation": "Elicit.Slot.477688339049.1345714957873" + } + } + ] + } + ], + "delegationStrategy": "SKILL_RESPONSE" + }, + "prompts": [ + { + "id": "Elicit.Slot.639501222016.1000025700327", + "variations": [ + { + "type": "PlainText", + "value": "You can save ten percent today by signing up for refills. Every two months, you'll receive eight blades and a two ounce tube of shaving cream for eightteen dollars including shipping. Cancel or change delivery any time. Do you want to add this to your order?" + } + ] + }, + { + "id": "Elicit.Slot.295132847200.649643899751", + "variations": [ + { + "type": "PlainText", + "value": "Our nine dollar starter kit comes with a weighted razor handle, three blades, and a two ounce tube of menthol shaving cream. Do you want to order it?" + } + ] + }, + { + "id": "Elicit.Slot.295132847200.45782372218", + "variations": [ + { + "type": "PlainText", + "value": "You can save ten percent today by signing up for refills. Every two months, you'll receive eight blades and a two ounce tube of shaving cream for eightteen dollars including shipping. Cancel or change delivery any time. Do you want to add this to your order?" + } + ] + }, + { + "id": "Elicit.Slot.122713613702.883585601394", + "variations": [ + { + "type": "PlainText", + "value": "You can save ten percent today by signing up for refills. Every two months, you'll receive eight blades and a two ounce tube of shaving cream for eighteen dollars including shipping. Cancel or change delivery any time. Do you want to add this to your order?" + } + ] + }, + { + "id": "Elicit.Slot.227603226437.555791052698", + "variations": [ + { + "type": "PlainText", + "value": "Every two months, you'll receive a refill of eight blades and a two ounce tube of shaving cream for twenty dollars including shipping. Cancel or change delivery any time. Do you want to order now?" + } + ] + }, + { + "id": "Elicit.Slot.477688339049.1345714957873", + "variations": [ + { + "type": "PlainText", + "value": "Every two months, you'll receive a refill of eight blades and a two ounce tube of shaving cream for twenty dollars including tax and shipping. You can cancel or change delivery any time. Do you want to order it?" + } + ] + }, + { + "id": "Elicit.Slot.1405619153703.871120215545", + "variations": [ + { + "type": "PlainText", + "value": "You can save ten percent today by signing up for refills. Every two months, you'll receive eight blades and a two ounce tube of shaving cream for eighteen dollars including shipping. Cancel or change delivery any time. Do you want to add this to your order?" + } + ] + }, + { + "id": "Elicit.Slot.1432830506701.27798259708", + "variations": [ + { + "type": "PlainText", + "value": "You can save ten percent today by signing up for refills. Every two months, you'll receive eight blades and a two ounce tube of shaving cream for eighteen dollars including shipping. Cancel or change delivery any time. Do you want to add this to your order?" + } + ] + }, + { + "id": "Elicit.Slot.871466476304.1051732784111", + "variations": [ + { + "type": "PlainText", + "value": "Do you want a dog?" + } + ] + }, + { + "id": "Elicit.Slot.871466476304.473894939028", + "variations": [ + { + "type": "PlainText", + "value": "Do you want a car?" + } + ] + }, + { + "id": "Elicit.Slot.81496166760.874607615560", + "variations": [ + { + "type": "PlainText", + "value": "You can save ten percent today by signing up for refills. Every two months, you'll receive eight blades and a two ounce tube of shaving cream for eighteen dollars including tax and shipping. You can cancel or change delivery any time. Do you want to add this to your order?" + } + ] + }, + { + "id": "Elicit.Slot.350082470046.957699153803", + "variations": [ + { + "type": "PlainText", + "value": "dis dat upgrade doe" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/skill.json b/skill.json new file mode 100644 index 0000000..8c0ca95 --- /dev/null +++ b/skill.json @@ -0,0 +1,60 @@ +{ + "manifest": { + "apis": { + "custom": { + "endpoint": { + "uri": "no-nicks", + "sourceDir": "lambda/custom" + }, + "interfaces": [] + } + }, + "manifestVersion": "1.0", + "permissions": [ + { + "name": "payments:autopay_consent" + } + ], + "privacyAndCompliance": { + "locales": { + "en-US": { + "privacyPolicyUrl": "https://example.com", + "termsOfUseUrl": "https://example.com" + } + }, + "allowsPurchases": true, + "usesPersonalInfo": true, + "isChildDirected": false, + "isExportCompliant": true, + "containsAds": false + }, + "publishingInformation": { + "locales": { + "en-US": { + "name": "No Nicks", + "smallIconUri": "https://s3.amazonaws.com/CAPS-SSE/echo_developer/23bf/c2e52e552ee44a6099b06ac30bc713b4/APP_ICON?versionId=d6YX5pM5JDYLgTcFRTicg7KvFj8rAPBY&AWSAccessKeyId=AKIAJFEYRBGIHK2BBYKA&Expires=1551319004&Signature=gawNKWveGqtpMO7KdLZ4e0RTAwc%3D", + "largeIconUri": "https://s3.amazonaws.com/CAPS-SSE/echo_developer/96f3/c5c84a64a51e498fa462c50974141ca3/APP_ICON_LARGE?versionId=E0Kk1TGSsweWB3o_QcxQ7At0kdNoCy6x&AWSAccessKeyId=AKIAJFEYRBGIHK2BBYKA&Expires=1551319004&Signature=y4qt8ujQmdDeH%2FAWqAHhfLYQln8%3D", + "summary": "This is a demo store that showcases how to use Amazon Pay with your shopping experiences on Alexa.", + "description": "This is a demo store that showcases how to use Amazon Pay with your shopping experiences on Alexa. This demo is using \"Sandbox Mode\" which uses fictional credit cards and will not charge your account. Download the code here: https://github.com/alexa/skill-sample-nodejs-demo-store-amazon-pay. Learn more about developing skills with Amazon Pay here: https://developer.amazon.com/docs/amazon-pay/amazon-pay-overview.html", + "examplePhrases": [ + "Alexa, open No Nicks", + "Alexa, ask No Nicks for a starter kit", + "Alexa, ask No Nicks for refills" + ], + "keywords": [ + "no", + "nicks", + "shaving", + "kit" + ], + "updatesDescription": "" + } + }, + "isAvailableWorldwide": true, + "distributionMode": "PUBLIC", + "testingInstructions": "", + "category": "SHOPPING", + "distributionCountries": [] + } + } +}