Skip to content

Commit

Permalink
Merge pull request #243 from ag1le/master
Browse files Browse the repository at this point in the history
Added in-skill-purchase capability
  • Loading branch information
johnwheeler authored Aug 13, 2018
2 parents 6df0570 + ecfab43 commit fe40764
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 1 deletion.
5 changes: 4 additions & 1 deletion flask_ask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,8 @@
delegate,
elicit_slot,
confirm_slot,
confirm_intent
confirm_intent,
buy,
upsell,
refund
)
48 changes: 48 additions & 0 deletions flask_ask/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,37 @@ def wrapper(*args, **kw):
return f


def on_purchase_completed(self, mapping={'payload': 'payload','name':'name','status':'status','token':'token'}, convert={}, default={}):
"""Decorator routes an Connections.Response to the wrapped function.
Request is sent when Alexa completes the purchase flow.
See https://developer.amazon.com/docs/in-skill-purchase/add-isps-to-a-skill.html#handle-results
The wrapped view function may accept parameters from the Request.
In addition to locale, requestId, timestamp, and type
@ask.on_purchase_completed( mapping={'payload': 'payload','name':'name','status':'status','token':'token'})
def completed(payload, name, status, token):
logger.info(payload)
logger.info(name)
logger.info(status)
logger.info(token)
"""
def decorator(f):
self._intent_view_funcs['Connections.Response'] = f
self._intent_mappings['Connections.Response'] = mapping
self._intent_converts['Connections.Response'] = convert
self._intent_defaults['Connections.Response'] = default
@wraps(f)
def wrapper(*args, **kwargs):
self._flask_view_func(*args, **kwargs)
return f
return decorator


def on_playback_started(self, mapping={'offset': 'offsetInMilliseconds'}, convert={}, default={}):
"""Decorator routes an AudioPlayer.PlaybackStarted Request to the wrapped function.
Expand Down Expand Up @@ -779,6 +810,8 @@ def _flask_view_func(self, *args, **kwargs):
result = self._map_player_request_to_func(self.request.type)()
# routes to on_playback funcs
# user can also access state of content.AudioPlayer with current_stream
elif 'Connections.Response' in request_type:
result = self._map_purchase_request_to_func(self.request.type)()

if result is not None:
if isinstance(result, models._Response):
Expand Down Expand Up @@ -817,6 +850,21 @@ def _map_player_request_to_func(self, player_request_type):

return partial(view_func, *arg_values)

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:
raise NotImplementedError('Request type "{}" not found and no default view specified.'.format(purchase_request_type))

argspec = inspect.getargspec(view_func)
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)
return partial(view_func, *arg_values)

def _get_slot_value(self, slot_object):
slot_name = slot_object.name
slot_value = getattr(slot_object, 'value', None)
Expand Down
53 changes: 53 additions & 0 deletions flask_ask/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,59 @@ def reprompt(self, reprompt):
return self


class buy(_Response):

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


class refund(_Response):

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

class upsell(_Response):

def __init__(self, productId=None, msg=None):
self._response = {
'shouldEndSession': True,
'directives': [{
'type': 'Connections.SendRequest',
'name': 'Upsell',
'payload': {
'InSkillProduct': {
'productId': productId
},
'upsellMessage': msg
},
'token': 'correlationToken'
}]
}

class delegate(_Response):

def __init__(self, updated_intent=None):
Expand Down
81 changes: 81 additions & 0 deletions samples/purchase/IntentSchema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"interactionModel": {
"languageModel": {
"invocationName": "demo",
"intents": [
{
"name": "AMAZON.FallbackIntent",
"samples": []
},
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "BuySkillItemIntent",
"slots": [
{
"name": "ProductName",
"type": "LIST_OF_PRODUCT_NAMES"
}
],
"samples": [
"{ProductName}",
"buy",
"shop",
"buy {ProductName}",
"purchase {ProductName}",
"want {ProductName}",
"would like {ProductName}"
]
},
{
"name": "RefundSkillItemIntent",
"slots": [
{
"name": "ProductName",
"type": "LIST_OF_PRODUCT_NAMES"
}
],
"samples": [
"cancel {ProductName}",
"return {ProductName}",
"refund {ProductName}",
"want a refund for {ProductName}",
"would like to return {ProductName}"
]
}
],
"types": [
{
"name": "LIST_OF_PRODUCT_NAMES",
"values": [
{
"name": {
"value": "monthly subscription"
}
},
{
"name": {
"value": "start smoking"
}
},
{
"name": {
"value": "stop smoking"
}
}
]
}
]
}
}
}
79 changes: 79 additions & 0 deletions samples/purchase/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import requests
from flask import json
from flask_ask import logger

class Product():
'''
Object model for inSkillProducts and methods to access products.
{"inSkillProducts":[
{"productId":"amzn1.adg.product.your_product_id",
"referenceName":"product_name",
"type":"ENTITLEMENT",
"name":"product name",
"summary":"This product has helped many people.",
"entitled":"NOT_ENTITLED",
"purchasable":"NOT_PURCHASABLE"}],
"nextToken":null,
"truncated":false}
'''

def __init__(self, apiAccessToken):
self.token = apiAccessToken
self.product_list = self.query()


def query(self):
# Information required to invoke the API is available in the session
apiEndpoint = "https://api.amazonalexa.com"
apiPath = "/v1/users/~current/skills/~current/inSkillProducts"
token = "bearer " + self.token
language = "en-US" #self.event.request.locale

url = apiEndpoint + apiPath
headers = {
"Content-Type" : 'application/json',
"Accept-Language" : language,
"Authorization" : token
}
#Call the API
res = requests.get(url, headers=headers)
logger.info('PRODUCTS:' + '*' * 80)
logger.info(res.status_code)
logger.info(res.text)
if res.status_code == 200:
data = json.loads(res.text)
return data['inSkillProducts']
else:
return None

def list(self):
""" return list of purchasable and not entitled products"""
mylist = []
for prod in self.product_list:
if self.purchasable(prod) and not self.entitled(prod):
mylist.append(prod)
return mylist

def purchasable(self, product):
""" return True if purchasable product"""
return 'PURCHASABLE' == product['purchasable']

def entitled(self, product):
""" return True if entitled product"""
return 'ENTITLED' == product['entitled']


def productId(self, name):
print(self.product_list)
for prod in self.product_list:
if name == prod['name'].lower():
return prod['productId']
return None

def productName(self, id):
for prod in self.product_list:
if id == prod['productId']:
return prod['name']
return None
88 changes: 88 additions & 0 deletions samples/purchase/purchase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import logging
import os
import requests

from flask import Flask, json, render_template
from flask_ask import Ask, request, session, question, statement, context, buy, upsell, refund, logger
from model import Product

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


PRODUCT_KEY = "PRODUCT"



@ask.on_purchase_completed( mapping={'payload': 'payload','name':'name','status':'status','token':'token'})
def completed(payload, name, status, token):
products = Product(context.System.apiAccessToken)
logger.info('on-purchase-completed {}'.format( request))
logger.info('payload: {} {}'.format(payload.purchaseResult, payload.productId))
logger.info('name: {}'.format(name))
logger.info('token: {}'.format(token))
logger.info('status: {}'.format( status.code == 200))
product_name = products.productName(payload.productId)
logger.info('Product name'.format(product_name))
if status.code == '200' and ('ACCEPTED' in payload.purchaseResult):
return question('To listen it just say - play {} '.format(product_name))
else:
return question('Do you want to buy another product?')

@ask.launch
def launch():
products = Product(context.System.apiAccessToken)
question_text = render_template('welcome', products=products.list())
reprompt_text = render_template('welcome_reprompt')
return question(question_text).reprompt(reprompt_text).simple_card('Welcome', question_text)


@ask.intent('BuySkillItemIntent', mapping={'product_name': 'ProductName'})
def buy_intent(product_name):
products = Product(context.System.apiAccessToken)
logger.info("PRODUCT: {}".format(product_name))
buy_card = render_template('buy_card', product=product_name)
productId = products.productId(product_name)
if productId is not None:
session.attributes[PRODUCT_KEY] = productId
else:
return statement("I didn't find a product {}".format(product_name))
raise NotImplementedError()
return buy(productId).simple_card('Welcome', question_text)

#return upsell(product,'get this great product')


@ask.intent('RefundSkillItemIntent', mapping={'product_name': 'ProductName'})
def refund_intent(product_name):
refund_card = render_template('refund_card')
logger.info("PRODUCT: {}".format(product_name))

products = Product(context.System.apiAccessToken)
productId = products.productId(product_name)

if productId is not None:
session.attributes[PRODUCT_KEY] = productId
else:
raise NotImplementedError()
return refund(productId)


@ask.intent('AMAZON.FallbackIntent')
def fallback_intent():
return statement("FallbackIntent")


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


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

0 comments on commit fe40764

Please sign in to comment.