Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amazon payment integration using flask-ask #248

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions flask_ask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
elicit_slot,
confirm_slot,
confirm_intent,
setup_payment,
charge_payment,
buy,
upsell,
refund
Expand Down
3 changes: 1 addition & 2 deletions flask_ask/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,6 @@ def _map_player_request_to_func(self, player_request_type):

def _map_purchase_request_to_func(self, purchase_request_type):
"""Provides appropriate parameters to the on_purchase functions."""

if purchase_request_type in self._intent_view_funcs:
view_func = self._intent_view_funcs[purchase_request_type]
else:
Expand All @@ -862,7 +861,7 @@ def _map_purchase_request_to_func(self, purchase_request_type):
arg_names = argspec.args
arg_values = self._map_params_to_view_args(purchase_request_type, arg_names)

print('_map_purchase_request_to_func', arg_names, arg_values, view_func, purchase_request_type)
#print('_map_purchase_request_to_func', arg_names, arg_values, view_func, purchase_request_type)
return partial(view_func, *arg_values)

def _get_slot_value(self, slot_object):
Expand Down
32 changes: 32 additions & 0 deletions flask_ask/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,38 @@ def reprompt(self, reprompt):
return self


class setup_payment(_Response):

def __init__(self, setupPayload=None):
self._response = {
'shouldEndSession': True,
'directives': [{
'type': 'Connections.SendRequest',
'name': 'Setup',
'payload': {
'SetupAmazonPay': setupPayload
},
'token': 'correlationToken'
}]
}


class charge_payment(_Response):

def __init__(self, chargePayload=None):
self._response = {
'shouldEndSession': True,
'directives': [{
'type': 'Connections.SendRequest',
'name': 'Charge',
'payload': {
'ChargeAmazonPay': chargePayload
},
'token': 'correlationToken'
}]
}


class buy(_Response):

def __init__(self, productId=None):
Expand Down
13 changes: 13 additions & 0 deletions samples/payment_integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Porting payment integration code in Amazon Alexa cookbook written in node.js (https://github.com/alexa/alexa-cookbook/tree/master/feature-demos/skill-demo-amazon-pay) to python. Made changes to suit the flask-ask library and fixed a few bugs.

### Deployment Instruction
```bash
# From path_to/samples/payment_integration, run:
python index.py

# If you are testing locally, install ngrok and in a new terminal window, do:
ngrok http -bind-tls=true 5000
```
In Alexa developer console, paste en-US.json to the json editor. Paste the ngrok forwarding url, which looks like https://xxxxxxxx.ngrok.io, to Alexa Endpoint. Enable Amazon payment in skill permissions.

Then, you may ask alexa to "open payments demo" and say "yes" to the purchase confirmation.
81 changes: 81 additions & 0 deletions samples/payment_integration/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import utilities

# TODO:
# 1. Fill in appID and sellerId
# 2. Fill in sandboxCustomerEmailId and set sandboxMode as True
# OR
# provide an empty string to sandboxCustomerEmailId and set sandboxMode as False


# GLOBAL
appID = 'amzn1.ask.skill.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # Required; Alexa Skill ID
sellerId = '' # Required; Amazon Pay seller ID; Used for both Setup and Process Payment

# DIRECTIVE CONFIG
connectionCharge = 'Charge' # Required;
connectionSetup = 'Setup' # Required;
directiveCharge = 'ChargeAmazonPay' # Required;
directiveSetup = 'SetupAmazonPay' # Required;
directiveType = 'Connections.SendRequest' # Required;
version = '1.0' # Required;

# SETUP
checkoutLanguage= 'en_US' # Optional; US must be en_US
countryOfEstablishment= 'US' # Required;
ledgerCurrency= 'USD' # Required; This doesn't exist in web SDK; GBP and EUR
needAmazonShippingAddress= True # Optional; Must be boolean;
sandboxCustomerEmailId= '[email protected]' # Optional; Required if sandboxMode equals true; Must setup Amazon Pay test account first;
sandboxMode= True # Optional; Must be false for certification; Must be boolean;

# PROCESS PAYMENT
paymentAction= 'AuthorizeAndCapture' # Required; Authorize or AuthorizeAndCapture
providerAttributes= '' # Optional; Required if Solution Provider.
sellerOrderAttributes= '' # Optional;

# AUTHORIZE ATTRIBUTES
authorizationReferenceId = utilities.generateRandomString( 32 ) # Required; Must be unique, max 32 chars
sellerAuthorizationNote = utilities.getSimulationString( '' ) # Optional; Max 255 chars
softDescriptor = '16charSoftDesc' # Optional; Max 16 chars
transactionTimeout = 0 # Optional; The default value for Alexa transactions is 0.

# AUTHORIZE AMOUNT
amount = '0.99' # Required; Max $150,000
currencyCode = 'USD' # Required;

# SELLER ORDER ATTRIBUTES
customInformation = 'customInformation max 1024 chars' # Optional; Max 1024 chars
sellerNote = 'sellerNote max 1024 chars' # Optional; Max 1024 chars
sellerOrderId = 'Alexa unique sellerOrderId' # Optional; Merchant specific order ID
sellerStoreName = 'Blitz and Chips' # Optional; Documentation calls this out as storeName not sellerStoreName

# ADDITIONAL ATTRIBUTES
platformId = '' # Optional; Used for Solution Providers
sellerBillingAgreementId = '' # Optional; The merchant-specified identifier of this billing agreement
storeName = sellerStoreName # Optional; Why is there 2 store names?


# The following strings DO NOT interact with Amazon Pay
# They are only here to augment the skill

# INTENT RESPONSE STRINGS
launchRequestWelcomeResponse = 'Welcome to ' + sellerStoreName + '. ' # Optional; Used for demo only
launchRequestQuestionResponse = 'Would you like to buy one Plumbus for $'+ amount +'?' # Optional; Used for demo only
noIntentResponse = 'There is nothing else for sale. Goodbye.' # Optional; Used for demo only
noIntentMessage = 'Please visit us again' # Optional; Used for demo only
orderConfirmationResponse = 'Thank you for ordering from ' + sellerStoreName + '. Goodbye.' # Optional; Used for demo only
orderConfirmationTitle = 'Order Details for Testing' # Optional; Used for demo only
storeURL = 'blitzandchips.com' # Optional; Used for demo only

# ERROR RESPONSE STRINGS
enablePermission = 'Please enable permission for Amazon Pay in your Alexa app.' # Optional; Used for demo only
scope = 'payments:autopay_consent' # Optional; Used for demo only
errorMessage = 'Merchant error occurred. ' # Optional; Used for demo only
errorUnknown = 'Unknown error occurred. '
errorStatusCode = 'Status code: ' # Optional; Used for demo only
errorStatusMessage = ' Status message: ' # Optional; Used for demo only
errorPayloadMessage = ' Payload message: ' # Optional; Used for demo only
errorBillingAgreement = 'Billing agreement state is '
errorBillingAgreementMessage= '. Reach out to the user to resolve this issue.' # Optional; Used for demo only
authorizationDeclineMessage = 'Your order was not placed and you have not been charged.' # Optional; Used for demo only
debug = 'debug' # Optional; Used for demo only

32 changes: 32 additions & 0 deletions samples/payment_integration/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"interactionModel": {
"languageModel": {
"invocationName": "payments demo",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": [
"help"
]
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.YesIntent",
"samples": []
},
{
"name": "AMAZON.NoIntent",
"samples": []
}
],
"types": []
}
}
}
104 changes: 104 additions & 0 deletions samples/payment_integration/error_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import config

from flask_ask import statement


# These are errors that will not be handled by Amazon Pay; Merchant must handle
def handleErrors(request):
errorMessage = ''
permissionsError = False
actionResponseStatusCode = request['status']['code']
actionResponseStatusMessage = request['status']['message']
actionResponsePayloadMessage = ''
if 'errorMessage' in request['payload']:
actionResponsePayloadMessage = request['payload']['errorMessage']

knownPermissionError = {
# Permissions errors - These must be resolved before a user can use Amazon Pay
'ACCESS_DENIED',
'ACCESS_NOT_REQUESTED',
'FORBIDDEN'
}
knownIntegrationOrRuntimeError = {
# Integration errors - These must be resolved before Amazon Pay can run
'BuyerEqualsSeller',
'InvalidParameterValue',
'InvalidSandboxCustomerEmail',
'InvalidSellerId',
'UnauthorizedAccess',
'UnsupportedCountryOfEstablishment',
'UnsupportedCurrency',

# Runtime errors - These must be resolved before a charge action can occur
'DuplicateRequest',
'InternalServerError',
'InvalidAuthorizationAmount',
'InvalidBillingAgreementId',
'InvalidBillingAgreementStatus',
'InvalidPaymentAction',
'PeriodicAmountExceeded',
'ProviderNotAuthorized',
'ServiceUnavailable'
}
if actionResponseStatusMessage in knownPermissionError:
permissionsError = True
errorMessage = config.enablePermission
elif actionResponseStatusMessage in knownIntegrationOrRuntimeError:
errorMessage = config.errorMessage + config.errorStatusCode + actionResponseStatusCode + '.' + config.errorStatusMessage + actionResponseStatusMessage + '.' + config.errorPayloadMessage + actionResponsePayloadMessage
else:
errorMessage = config.errorUnknown

debug('handleErrors', request)

# If it is a permissions error send a permission consent card to the user, otherwise .speak() error to resolve during testing
if permissionsError:
return statement(errorMessage).consent_card(config.scope)
else:
return statement(errorMessage)


# 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
def handleBillingAgreementState( billingAgreementStatus, request):
errorMessage = ''

knownStatus = {
'CANCELED',
'CLOSED',
'SUSPENDED'
}
if billingAgreementStatus in knownStatus:
errorMessage = config.errorBillingAgreement + billingAgreementStatus + config.errorBillingAgreementMessage
else:
errorMessage = config.errorUnknown

debug('handleBillingAgreementState', request)

return statement(errorMessage)


# 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.
def handleAuthorizationDeclines( authorizationStatusReasonCode, request):
errorMessage = ''

knownReasonCode = {
'AmazonRejected',
'InvalidPaymentMethod',
'ProcessingFailure',
'TransactionTimedOut'
}
if authorizationStatusReasonCode in knownReasonCode:
errorMessage = config.authorizationDeclineMessage
else:
errorMessage = config.errorUnknown

debug('handleAuthorizationDeclines', request)

return statement(errorMessage)


# Output object to console for debugging purposes
def debug(funcName, request ):
print('ERROR in %s --- %s\n' % (funcName, str(request)))

98 changes: 98 additions & 0 deletions samples/payment_integration/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import logging
import os

from flask import Flask #. /usr/local/opt/libpcap/bin:/usr/local/opt/gettext/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Users/qiheng/Library/Python/2.7/bin
from flask_ask import Ask, request, context, session, question, statement

import utilities
import config


app = Flask(__name__)
ask = Ask(app, "/")
logging.getLogger('flask_ask').setLevel(logging.DEBUG)


@ask.launch
def launch():
return question(config.launchRequestWelcomeResponse + config.launchRequestQuestionResponse).reprompt(config.launchRequestQuestionResponse)


# No, I do not want to buy something
@ask.intent('AMAZON.NoIntent')
def no():
return question(config.noIntentResponse).simple_card(config.noIntentMessage, config.storeURL)


#Yes, I do want to buy something
@ask.intent('AMAZON.YesIntent')
def yes():
# If you have a valid billing agreement from a previous session, skip the Setup action and call the Charge action instead
consentToken = utilities.getConsentToken(context)
token = utilities.generateRandomString(12)

# If you do not have a billing agreement set the Setup payload in Skill Connections and send the request directive
setupPayload = payload.buildSetup( consentToken );

return directive.sendDirective(config.directiveType, config.connectionSetup, setupPayload, config.directiveSetup, token, context, False)


# You requested the Setup or Charge directive and are now receiving the Connections.Response
@ask.on_purchase_completed( mapping={'payload': 'payload','name':'name','status':'status','token':'token'})
def connectionResponse():
consentToken = utilities.getConsentToken(context)
connectionName = request['name']
connectionPayload = request['payload']
connectionResponseStatusCode = request['status']['code']

if connectionResponseStatusCode != 200:
return error.handleErrors(request)
else:
#Receiving Setup Connections.Response
if connectionName == config.connectionSetup:
token = utilities.generateRandomString( 12 )

# Get the billingAgreementId and billingAgreementStatus from the Setup Connections.Response
billingAgreementId = connectionResponsePayload['billingAgreementDetails']['billingAgreementId']
billingAgreementStatus = connectionResponsePayload['billingAgreementDetails']['billingAgreementStatus']

# If billingAgreementStatus is valid, Charge the payment method
if billingAgreementStatus == 'OPEN':
# Set the Charge payload in Skill Connections and send the request directive
chargePayload = payload.buildCharge( consentToken, billingAgreementId )
return directive.sendDirective(config.directiveType, config.connectionCharge, chargePayload, config.directiveCharge, token, context, True)

# If billingAgreementStatus is not valid, do not Charge the payment method
else:
return error.handleBillingAgreementState(billingAgreementStatus, request)

# Receiving Charge Connections.Response
elif connectionName == config.connectionCharge:
authorizationStatusState = connectionResponsePayload['authorizationDetails']['state']

# Authorization is declined, tell the user their order was not placed
if authorizationStatusState == 'Declined':
authorizationStatusReasonCode = connectionResponsePayload['authorizationDetails']['reasonCode']
return error.handleAuthorizationDeclines(authorizationStatusReasonCode, request)

# Authorization is approved, tell the user their order was placed
else:
return statement(config.orderConfirmationResponse)


@ask.session_ended
def session_ended():
return "{}", 200


def lambda_handler(event, context):
return ask.run_aws_lambda(event)

"""
if __name__ == '__main__':
if 'ASK_VERIFY_REQUESTS' in os.environ:
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
if verify == 'false':
app.config.ASK_VERIFY_REQUESTS = False
app.run(debug=True)
"""
Loading