簡易的なテイクアウト予約システムを Amazon Connect で作ってみた – Amazon Connect アドベントカレンダー 2022

2022.12.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!森田です。

この記事は「Amazon Connect アドベントカレンダー 2022」の23日目の記事となります!

Amazon Connectアドベントカレンダー2022は、クラスメソッドと株式会社ギークフィードさんでチャレンジしている企画となっており、他にもAmazon Connect関する様々な記事がありますのでぜひご参照ください!!

この記事では、Amazon Connect を利用して簡易的なテイクアウト予約システムを作ってみます!

やりたいこと

一般的な飲食店向けのテイクアウトの仕組みを Amazon Connect で作っていきます。

ただ、上記の仕組みを完璧に作り上げることは時間的に難しいので、細かいところは作成せず、雰囲気でそれっぽく作っていきます。

具体的には、以下のことができるように作っていきます。

  • ユーザから電話をかけることで、自動的にテイクアウトのオーダをとる
  • 店舗側は簡単に現在のテイクアウトのオーダを確認することができる

構成

以下のような構成で作成していきます。

今回は、オーダの確認がしやすいように、Google Calendarにオーダ内容を反映させていくようにします。

また、ユーザにメニューを共有する方法としては今回は、SMSでメニュー内容を送信するようにしています。

Google Calendar の設定

Google Calendar API 設定

Google Calendar をAPI から操作するため、Google Cloudで設定を行なっていきます。 まずは、「APIとサービス」→「有効なAPIとサービス」から Google Calendar API を有効化します。

続いて、「IAMと管理」→「サービスアカウント」→「サービスアカウントを作成」からサービスアカウントを作成します。

作成後は、上記のようにサービスアカウントのメールアドレスが払い出されます。

サービスアカウントを選択し、APIを実行するための鍵をJSONで作成し、ダウンロードします。(後ほどLambdaへアップします。)

Google Calendar の新規作成・設定

以下のリンクから新しいカレンダーの作成行っていきます。

https://calendar.google.com/calendar/u/1/r/settings/createcalendar

作成後、マイカレンダーの設定より「特定のユーザーとの共有」へサービスアカウントのメールアドレスを追加し、変更および共有の管理権限を設定します。

 

後ほど、カレンダー IDも利用するのでメモしておきます。

Lambdaの作成

環境変数の設定

環境変数にcalendar_idを設定します。値については、先ほどメモしたものを入力します。

コードの作成

以下のコードで Lambdaを作成します。

Lambdaのソースコード(クリックして展開)
import json
import boto3
from boto3.dynamodb.conditions import Key, Attr
import datetime
import googleapiclient.discovery
import google.auth
import os

StoreInfo = {}

table = boto3.resource("dynamodb").Table("storeItem")

def lambda_handler(event, context):
    if len(StoreInfo)==0:
        store_get()
    if event["Details"]['Parameters'].get("menuSend"):
        print(event["Details"]['Parameters'].get("menuSend"))
        sms_send(event["Details"]['Parameters'].get("menuSend"))
        return {
            "status":"ok"
        }
    elif event["Details"]['Parameters'].get("storeInfo"):
        key = event["Details"]['Parameters'].get("storeInfo")
        return {
            key:StoreInfo[key]
        }
    elif event["Details"]['Parameters'].get("takeoutCartAdd"):
        if event['Details']['ContactData']['Attributes']['takeout'] == "None":
            takeout = "{},{}".format(event['Details']['ContactData']['Attributes']['item_id'], event['Details']['ContactData']['Attributes']['item_num'])
        else:
            takeout = event['Details']['ContactData']['Attributes']['takeout']
            takeout += ",{},{}".format(event['Details']['ContactData']['Attributes']['item_id'], event['Details']['ContactData']['Attributes']['item_num'])
        return {
            "takeout":takeout
        }
    elif event["Details"]['Parameters'].get("dateCheck"):
        date = event['Details']['ContactData']['Attributes']['date']
        text = "{}{}".format(datetime.datetime.now().year, date)
        try:
            target = datetime.datetime.strptime(text+"23:59:59", '%Y%m%d%H:%M:%S')
            if datetime.datetime.now() >= target:
                return {
                    "result": "False",
                    "message": "日付が誤っています"
                }
            if block_check_day(text):
                return {
                    "result": "False",
                    "message": "ご入力いただいた日付は、現在、新規のご予約を行っておりません。"
                }
            return {
                "result": "True"
            }
        except:
            return {
                "result": "False",
                "message": "日付が誤っています"
            }
        
            
    elif event["Details"]['Parameters'].get("timeCheck"):
        time = event['Details']['ContactData']['Attributes']['time']
        try:
            datetime.datetime.strptime(time, "%H%M")
        except:
            return {
                "result": "False",
                "message": "時間が誤っています"
            }
        if int(time[2:4])%15 !=0:
            return {
                "result": "False",
                "message": "時間は15分単位としてください"
            }
        minutes = StoreInfo["store_takeout_min_minutes"]
        date = event['Details']['ContactData']['Attributes']['date']
        now = datetime.datetime.now()+ datetime.timedelta(hours=9)
        target = datetime.datetime.strptime(now.strftime('%Y')+date+time, '%Y%m%d%H%M')
        if block_check(target):
            print("時間が予約ずみ")
            return {
                "result": "False",
                "message": "ご入力いただいた時間は、現在、新規のご予約を行っておりません。"
            }
        dates = now + datetime.timedelta(minutes=int(minutes))
        print(target, dates)
        if  dates >= target:
            m = dates.minute
            h = dates.hour
            if m >0 and m<=15: m = "15" elif m >15 and m<=30: m = "30" elif m >30 and m<=45: m = "45" elif m >45 :
                m = "00"
                h = h+1
            else:
                m = "00"
            return {
                "result": "False",
                "message": "最短で予約できる時間は、{}時{}分となっています".format(h, m)
            }
        data = event['Details']['ContactData']['Attributes']['takeout'].split(",")
        message = "ご入力いただきありがとうございます。ご注文内容は以下の通りです。"
        sumarry = ""
        description = "電話番号:"+event['Details']['ContactData']['CustomerEndpoint']['Address']+"\n受け取り日時: "+target.strftime('%Y/%m/%d %H:%M')
        for i in range(0, len(data), 2):
            item = StoreInfo["items"][data[i]]
            message += item["kana"] + "を" + str(data[i+1])+"個。"
            sumarry += item["name"]+"×"+ str(data[i+1])+"個 "
        date = event['Details']['ContactData']['Attributes']['date']
        message += "受け取り日時は、"+date[0:2]+"月"+date[2:4]+"日"+time[0:2]+"時"+time[2:4]+ "分となっております。"
        google_calendar(target, target+ datetime.timedelta(minutes=30), sumarry, description)
        print(message)
        return {
            "result": "True",
            "message":message
        }
    item_id = event['Details']['ContactData']['Attributes']['item_id']
    try:
        item = StoreInfo["items"].get(item_id)
        if item:
            # TODO implement
            return {
                "result": str(item_id),
                "message":"{}が選択されました。".format(item["kana"]),
                "kana":item["kana"]
            }
        return {
            "result": "False"
        }
    except:
        return {
            "result": "False"
        }

def google_calendar(start, stop, summary, description):
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    calendar_id = os.getenv('calendar_id')
    gapi_creds = google.auth.load_credentials_from_file('credentials.json', SCOPES)[0]
    service = googleapiclient.discovery.build('calendar', 'v3', credentials=gapi_creds)
    body = {
        'summary': summary,
        "description":description,
        'start': {
            'dateTime': start.isoformat(),
            'timeZone': 'Japan'
        },
        'end': {
            'dateTime': stop.isoformat(),
            'timeZone': 'Japan'
        },
    }
    event = service.events().insert(calendarId=calendar_id, body=body).execute()
    
def sms_send(phone):
    sns = boto3.client('sns')
    response = sns.publish(
        PhoneNumber = phone,
        Subject = '【{}】テイクアウトメニューのお知らせ'.format(StoreInfo["store_name"]),
        Message = StoreInfo["body"]
    )
    
def store_get():
    response = table.get_item(Key={"item_type": "store_name", "item_id":0})
    store_name = response.get("Item")["name"]
    StoreInfo["store_name"] = store_name
    response = table.get_item(Key={"item_type": "store_takeout_min", "item_id":0})
    StoreInfo["store_takeout_min_minutes"] = response.get("Item")["minutes"]
    res = table.query(
        KeyConditionExpression=Key('item_type').eq('item'),
    )
    d = {}
    body = "以下がメニューとなります。\n商品番号:商品名:金額\n"
    for item in res.get("Items"):
        d[str(item["item_id"])] = item
        body += "{} : {} : {}円\n".format(item["item_id"], item["name"], item["price"])
    StoreInfo["items"] = d
    StoreInfo["body"] = body


def block_check(target):
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    calendar_id = os.getenv('calendar_id')
    gapi_creds = google.auth.load_credentials_from_file('credentials.json', SCOPES)[0]
    service = googleapiclient.discovery.build('calendar', 'v3', credentials=gapi_creds)
    target_utc = target - datetime.timedelta(hours=9)
    event_list = service.events().list(
        calendarId=calendar_id, timeMin=target_utc.isoformat() + 'Z', q="ブロック",
        maxResults=3, singleEvents=True,
        orderBy='startTime').execute()

    if len(event_list['items'])!=0:
        for item in event_list['items']:
            start = item["start"]["dateTime"].split("+")[0]
            start = datetime.datetime.strptime(start, '%Y-%m-%dT%H:%M:%S')
            stop = item["end"]["dateTime"].split("+")[0]
            stop = datetime.datetime.strptime(stop, '%Y-%m-%dT%H:%M:%S')
            if target >= start and target < stop:
                return True
    return False
    
def block_check_day(target_text):
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    calendar_id = os.getenv('calendar_id')
    gapi_creds = google.auth.load_credentials_from_file('credentials.json', SCOPES)[0]
    service = googleapiclient.discovery.build('calendar', 'v3', credentials=gapi_creds)
    target =  datetime.datetime.strptime(target_text, '%Y%m%d')
    target_utc = target - datetime.timedelta(hours=9)
    event_list = service.events().list(
        calendarId=calendar_id, q="休み",
        maxResults=3, singleEvents=True,
        orderBy='startTime').execute()

    if len(event_list['items'])!=0:
        for item in event_list['items']:
            text = "".join(item["start"]["date"].split("-"))
            if text == target_text:
                return True
    return False

Lambda レイヤーの準備

上記のコードではライブラリとして、google-api-python-client、google-authを利用するため、以下のコマンドでレイヤー化して、Lambadaの追加します。

$ mkdir python
$ cd python
$ pip3 install google-api-python-client -t .
$ pip3 install google-auth -t .
$ zip -r  ../python .

Google Calendar API の鍵ファイル(JSON)を作成

Lambda のコンソールから直接、鍵ファイル(JSON)をコピーアンドペーストで作成します。 作成後は、以下のような状態となります。

IAMロールの設定

IAMロールについては、DynamoDBとSNSの権限を与えます。

DynamoDB の作成

テーブル名をstoreItem、パーティションキーをitem_type、ソートキーをitem_idでDynamoDBのテーブルを作成します。

テーブル作成後、以下のように店舗情報を項目として追加していきます。

店舗名の項目作成

以下の項目を作成します。ここで設定したnameの値が電話をかけると再生されるようになります。

属性名 タイプ
item_type store_name 文字列
item_id 0 数値
name 店舗名 文字列

店舗最短予約時間の項目作成

以下の項目を作成します。ここで設定したminutesの数値に基づいて最短予約時間の設定ができます。 例えば、minutes=30で 12:20ごろ電話をかけた場合の最短予約時間は、 13:00となります。(15分単位で切り上げされます。)

属性名 タイプ
item_type store_takeout_min 文字列
item_id 0 数値
minutes 好きな数値 数値

 

テイクアウト商品の項目作成

以下の項目では、item_idを0以上で作成していくことで、テイクアウト商品の情報の作成ができます。

属性名 タイプ
item_type item 文字列
item_id 0以上の値を設定 数値
name 商品名 文字列
kana 読み上げるときのテキスト 文字列
price 金額 数値

SNSの設定

SMS送信を行う際には、サンドボックス環境であれば、事前に電話番号の検証が必要となります。 以下を参考に対応します。

Amazon Connect

Lambdaをコンタクトフローを関連付け

Amazon Connect コンソールの「問い合わせフロー」→「AWS Lambda」に先ほど作成したLambdaを追加します。

コンタクトフローを作成

以下のファイルをインポートしてコンタクトフローを作成します。 コンタクトフロー内の、「AWS Lambda 関数を呼び出す」ブロックについては、必要に応じて正しいLambda名で再設定してください。

takeout_reserved_flow(クリックして展開)
{"Version":"2019-10-30","StartAction":"027e4767-3994-4d5f-b60c-ae785af752f5","Metadata":{"entryPointPosition":{"x":-336,"y":-210.4},"ActionMetadata":{"98a99788-d2b7-4ba1-9164-586e19bd81e6":{"position":{"x":-129.6,"y":23.200000000000003},"children":["ffa9a8dc-7a3f-470f-82db-9895218faa50"],"overrideConsoleVoice":false,"fragments":{"SetContactData":"ffa9a8dc-7a3f-470f-82db-9895218faa50"},"overrideLanguageAttribute":true},"ffa9a8dc-7a3f-470f-82db-9895218faa50":{"position":{"x":-129.6,"y":23.200000000000003},"dynamicParams":[]},"4bdaaa66-3d35-44e9-ab82-cf445e4954cb":{"position":{"x":2060.8,"y":-525.6}},"c4301af1-770d-4089-8623-ed9a49dd3dd6":{"position":{"x":2920,"y":-63.2}},"756c0616-554a-4a74-aa7a-cd9049d6773b":{"position":{"x":2591.2000000000003,"y":-844.8000000000001}},"ce1982c5-106c-49be-9238-a4993943f3c1":{"position":{"x":3054.4,"y":-743.2},"conditionMetadata":[{"id":"080d3052-888c-472c-8b3b-93ca4f7c79a4","operator":{"name":"Equals","value":"Equals","shortDisplay":"="},"value":"0"}]},"b27fc9e4-83d2-4f65-9346-93508fa94af5":{"position":{"x":2933.6000000000004,"y":-1875.2}},"3f6cb8fc-5f47-4671-bb75-220c329fc978":{"position":{"x":2307.2000000000003,"y":-1426.4},"parameters":{"LambdaFunctionARN":{"displayName":"reserved-item-checker"}},"dynamicMetadata":{"dateCheck":false}},"530ccc53-e13e-4186-8fac-23b46a9ef186":{"position":{"x":2574.4,"y":-1496},"conditionMetadata":[{"id":"8bb41874-dfa9-4bc8-91c0-97ce95942495","operator":{"name":"Equals","value":"Equals","shortDisplay":"="},"value":"False"}]},"c347c5f6-3805-4770-9c84-da4a9156c8b1":{"position":{"x":2819.2000000000003,"y":-1516},"conditionMetadata":[],"countryCodePrefix":"+1"},"9185bee7-afb4-455c-aab5-6742e0fb182c":{"position":{"x":1455.2,"y":108.80000000000001}},"fd0762e8-ce67-4bf9-8c40-c9271340dc33":{"position":{"x":1765.6000000000001,"y":-546.4}},"45013b74-d6f6-4c5b-9958-229c8152eb9e":{"position":{"x":1240,"y":-320.8}},"dc55d7f4-fd0a-45d9-94e2-14affd19518c":{"position":{"x":2314.4,"y":-33.6}},"18a120d2-4b63-4e67-8dd5-e32a5649454a":{"position":{"x":1812,"y":-303.2},"parameters":{"Attributes":{"item_id":{"useDynamic":true}}},"dynamicParams":["item_id"]},"f15721f5-6b7e-4795-84c5-a8222a44e20c":{"position":{"x":2051.2000000000003,"y":-304.8},"dynamicMetadata":{}},"027e4767-3994-4d5f-b60c-ae785af752f5":{"position":{"x":-141.6,"y":-236},"dynamicParams":[]},"b23e0773-c39f-4e3e-954e-63b5db4399cb":{"position":{"x":132,"y":-95.2}},"773dd1e8-48f8-48fb-906f-25422a02df46":{"position":{"x":112.80000000000001,"y":305.6}},"9eef11c7-4562-4dcd-b3d1-d43e2e6086cc":{"position":{"x":1131.2,"y":200.8}},"ef8298b2-4673-47e1-afbf-804180261780":{"position":{"x":992,"y":-268},"parameters":{"LambdaFunctionARN":{"displayName":"reserved-item-checker"},"LambdaInvocationAttributes":{"menuSend":{"useDynamic":true}}},"dynamicMetadata":{"menuSend":true}},"31f7f1b6-d1fb-466a-b08c-fbea085f0c78":{"position":{"x":1344.8000000000002,"y":352.8}},"fcd0da64-5e41-43f1-bef5-10876537cdab":{"position":{"x":439.20000000000005,"y":48.800000000000004}},"d36c5bb5-1ab8-4a2d-badd-0bca06864526":{"position":{"x":775.2,"y":35.2},"conditionMetadata":[{"id":"e5de5fca-850d-4815-99e2-f157738e0bc5","value":"0"},{"id":"4c4cd52c-3f8d-4362-b19d-dcc40530212c","value":"1"}]},"8f61a260-9ec8-41b9-99ff-ef2a7e14049c":{"position":{"x":3172.8,"y":-336}},"f0cf9fb1-83fd-4539-825a-0086b371d530":{"position":{"x":2950.4,"y":-485.6},"conditionMetadata":[],"countryCodePrefix":"+1"},"dee019af-e351-47d3-87a0-047b006caecf":{"position":{"x":2333.6,"y":-312.8},"conditionMetadata":[{"id":"822e6646-5495-493b-bf5d-5aa7b029cae5","operator":{"name":"Equals","value":"Equals","shortDisplay":"="},"value":"False"}]},"91a15660-b149-4343-8f1e-1dca33a8a8a7":{"position":{"x":2612,"y":-452},"parameters":{"Text":{"useDynamic":true}},"useDynamic":true},"4c95d891-2a7c-4062-a662-7bd62407ab23":{"position":{"x":3310.4,"y":-788},"parameters":{"Attributes":{"item_num":{"useDynamic":true},"kana":{"useDynamic":true}}},"dynamicParams":["item_num","kana"]},"dc46043c-4201-4d76-9461-3709d3be3e83":{"position":{"x":3564.8,"y":-840},"parameters":{"LambdaFunctionARN":{"displayName":"reserved-item-checker"}},"dynamicMetadata":{"takeoutCartAdd":false}},"1d73241e-85f2-41b6-95fd-8c89fbcc1704":{"position":{"x":3800,"y":-943.2},"parameters":{"Attributes":{"takeout":{"useDynamic":true}}},"dynamicParams":["takeout"]},"1cf0652b-eb96-407c-b288-89399ffd1dfa":{"position":{"x":1345.6000000000001,"y":-1607.2}},"6eb5ba9b-5895-4905-b2ea-3f89a56bfa46":{"position":{"x":1919.2,"y":-1454.4}},"ba4beb4d-0897-4835-b4d5-ec14f2437477":{"position":{"x":1549.6000000000001,"y":-305.6},"conditionMetadata":[],"countryCodePrefix":"+1"},"eea70488-2b74-4f54-8ed2-4e07309ccae0":{"position":{"x":4472.8,"y":-603.2}},"67138f75-d10a-4c25-a5c7-14b7667d5821":{"position":{"x":4045.6000000000004,"y":-996.8000000000001}},"8c2c2cab-6d36-4681-bfe1-859207db364e":{"position":{"x":2535.2000000000003,"y":-1114.4}},"830d2712-87f7-4be7-a343-97f38af0cdfc":{"position":{"x":2081.6,"y":-1320},"parameters":{"Attributes":{"date":{"useDynamic":true}}},"dynamicParams":["date"]},"1a81baeb-66f7-4e48-9e12-440f2d8326f0":{"position":{"x":1644,"y":-1657.6000000000001},"parameters":{"Text":{"useDynamic":true}},"useDynamic":true},"0b467922-e192-4211-af53-66bf62b33269":{"position":{"x":1447.2,"y":-1268},"conditionMetadata":[{"id":"e91319fa-ce86-4e2d-ae04-c8e0de2c8dbb","value":"0"},{"id":"27b0e2ea-32ae-433c-8a48-cbdb310260e4","value":"1"}]},"963d038b-935c-485f-856e-7d24432c8dbc":{"position":{"x":1852,"y":-1223.2},"conditionMetadata":[],"countryCodePrefix":"+1"},"c695637d-d921-480a-b55e-1d1868862f23":{"position":{"x":2627.2000000000003,"y":-1917.6000000000001},"parameters":{"Text":{"useDynamic":true}},"useDynamic":true},"629650f4-96c9-4511-a994-383bb70a6d38":{"position":{"x":3814.4,"y":-1686.4},"parameters":{"Text":{"useDynamic":true}},"useDynamic":true},"14f7fe64-5844-435d-a748-5447338af00f":{"position":{"x":3053.6000000000004,"y":-1584},"parameters":{"Attributes":{"time":{"useDynamic":true}}},"dynamicParams":["time"]},"1c5107ba-f7c9-4ad8-a409-3984eb09203e":{"position":{"x":4214.400000000001,"y":-1438.4}},"b0645600-d53c-4496-aa21-5d34b8a7944c":{"position":{"x":3557.6000000000004,"y":-1742.4},"conditionMetadata":[{"id":"17cf1234-f08b-4458-a77d-08b7695982fb","operator":{"name":"Equals","value":"Equals","shortDisplay":"="},"value":"False"}]},"b09fd57b-06d7-4186-adef-a168450360de":{"position":{"x":3291.2000000000003,"y":-1653.6000000000001},"parameters":{"LambdaFunctionARN":{"displayName":"reserved-item-checker"}},"dynamicMetadata":{"timeCheck":false}},"b7a461e6-047c-4331-bebb-8116c8a31396":{"position":{"x":420,"y":692}},"66925e31-a71c-4544-b45a-8400440f72cd":{"position":{"x":148.8,"y":455.20000000000005}},"f80cddb4-96b8-412e-aa74-f43c8ac641f4":{"position":{"x":-128,"y":214.4}},"75cfc998-9a25-427e-b054-1a5b296f24d7":{"position":{"x":-133.6,"y":459.20000000000005},"parameters":{"LambdaFunctionARN":{"displayName":"reserved-item-checker"}},"dynamicMetadata":{"storeInfo":false}}},"name":"takeout_reserved_flow","description":"","type":"contactFlow","status":"published","hash":{}},"Actions":[{"Parameters":{"TextToSpeechVoice":"Takumi"},"Identifier":"98a99788-d2b7-4ba1-9164-586e19bd81e6","Type":"UpdateContactTextToSpeechVoice","Transitions":{"NextAction":"ffa9a8dc-7a3f-470f-82db-9895218faa50"}},{"Parameters":{"LanguageCode":"ja-JP"},"Identifier":"ffa9a8dc-7a3f-470f-82db-9895218faa50","Type":"UpdateContactData","Transitions":{"NextAction":"f80cddb4-96b8-412e-aa74-f43c8ac641f4","Errors":[{"NextAction":"f80cddb4-96b8-412e-aa74-f43c8ac641f4","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"4bdaaa66-3d35-44e9-ab82-cf445e4954cb","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{},"Identifier":"c4301af1-770d-4089-8623-ed9a49dd3dd6","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"Text":"個数をゼロにすることはできません。\nもう一度お聞きします。"},"Identifier":"756c0616-554a-4a74-aa7a-cd9049d6773b","Type":"MessageParticipant","Transitions":{"NextAction":"f0cf9fb1-83fd-4539-825a-0086b371d530","Errors":[{"NextAction":"c4301af1-770d-4089-8623-ed9a49dd3dd6","ErrorType":"NoMatchingError"}]}},{"Parameters":{"ComparisonValue":"$.StoredCustomerInput"},"Identifier":"ce1982c5-106c-49be-9238-a4993943f3c1","Type":"Compare","Transitions":{"NextAction":"4c95d891-2a7c-4062-a662-7bd62407ab23","Conditions":[{"NextAction":"756c0616-554a-4a74-aa7a-cd9049d6773b","Condition":{"Operator":"Equals","Operands":["0"]}}],"Errors":[{"NextAction":"4c95d891-2a7c-4062-a662-7bd62407ab23","ErrorType":"NoMatchingCondition"}]}},{"Parameters":{},"Identifier":"b27fc9e4-83d2-4f65-9346-93508fa94af5","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"LambdaFunctionARN":"arn:aws:lambda:ap-northeast-1::function:reserved-item-checker","InvocationTimeLimitSeconds":"3","LambdaInvocationAttributes":{"dateCheck":"True"}},"Identifier":"3f6cb8fc-5f47-4671-bb75-220c329fc978","Type":"InvokeLambdaFunction","Transitions":{"NextAction":"530ccc53-e13e-4186-8fac-23b46a9ef186","Errors":[{"NextAction":"8c2c2cab-6d36-4681-bfe1-859207db364e","ErrorType":"NoMatchingError"}]}},{"Parameters":{"ComparisonValue":"$.External.result"},"Identifier":"530ccc53-e13e-4186-8fac-23b46a9ef186","Type":"Compare","Transitions":{"NextAction":"c347c5f6-3805-4770-9c84-da4a9156c8b1","Conditions":[{"NextAction":"1a81baeb-66f7-4e48-9e12-440f2d8326f0","Condition":{"Operator":"Equals","Operands":["False"]}}],"Errors":[{"NextAction":"c347c5f6-3805-4770-9c84-da4a9156c8b1","ErrorType":"NoMatchingCondition"}]}},{"Parameters":{"StoreInput":"True","InputTimeLimitSeconds":"5","Text":"受け取り時間をお聞きします。\n時間を入力してください。\nなお、15分単位で入力を行ってください。\n例えば、16時30分希望であれば、1630と入力してください。","DTMFConfiguration":{"DisableCancelKey":"False"},"InputValidation":{"CustomValidation":{"MaximumLength":"4"}}},"Identifier":"c347c5f6-3805-4770-9c84-da4a9156c8b1","Type":"GetParticipantInput","Transitions":{"NextAction":"14f7fe64-5844-435d-a748-5447338af00f","Errors":[{"NextAction":"1c5107ba-f7c9-4ad8-a409-3984eb09203e","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"9185bee7-afb4-455c-aab5-6742e0fb182c","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"Text":"入力いただいた商品番号は存在しません。もう一度お聞きします。"},"Identifier":"fd0762e8-ce67-4bf9-8c40-c9271340dc33","Type":"MessageParticipant","Transitions":{"NextAction":"ba4beb4d-0897-4835-b4d5-ec14f2437477","Errors":[{"NextAction":"4bdaaa66-3d35-44e9-ab82-cf445e4954cb","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Text":"ショートメールでメニュー表を送信しましたのでご確認ください。"},"Identifier":"45013b74-d6f6-4c5b-9958-229c8152eb9e","Type":"MessageParticipant","Transitions":{"NextAction":"ba4beb4d-0897-4835-b4d5-ec14f2437477","Errors":[{"NextAction":"9185bee7-afb4-455c-aab5-6742e0fb182c","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"dc55d7f4-fd0a-45d9-94e2-14affd19518c","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"Attributes":{"item_id":"$.StoredCustomerInput"}},"Identifier":"18a120d2-4b63-4e67-8dd5-e32a5649454a","Type":"UpdateContactAttributes","Transitions":{"NextAction":"f15721f5-6b7e-4795-84c5-a8222a44e20c","Errors":[{"NextAction":"dc55d7f4-fd0a-45d9-94e2-14affd19518c","ErrorType":"NoMatchingError"}]}},{"Parameters":{"LambdaFunctionARN":"arn:aws:lambda:ap-northeast-1::function:reserved-item-checker","InvocationTimeLimitSeconds":"3"},"Identifier":"f15721f5-6b7e-4795-84c5-a8222a44e20c","Type":"InvokeLambdaFunction","Transitions":{"NextAction":"dee019af-e351-47d3-87a0-047b006caecf","Errors":[{"NextAction":"dc55d7f4-fd0a-45d9-94e2-14affd19518c","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Attributes":{"takeout":"None"}},"Identifier":"027e4767-3994-4d5f-b60c-ae785af752f5","Type":"UpdateContactAttributes","Transitions":{"NextAction":"98a99788-d2b7-4ba1-9164-586e19bd81e6","Errors":[{"NextAction":"b23e0773-c39f-4e3e-954e-63b5db4399cb","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"b23e0773-c39f-4e3e-954e-63b5db4399cb","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{},"Identifier":"773dd1e8-48f8-48fb-906f-25422a02df46","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"Text":"もう一度お聞きします"},"Identifier":"9eef11c7-4562-4dcd-b3d1-d43e2e6086cc","Type":"MessageParticipant","Transitions":{"NextAction":"d36c5bb5-1ab8-4a2d-badd-0bca06864526","Errors":[{"NextAction":"31f7f1b6-d1fb-466a-b08c-fbea085f0c78","ErrorType":"NoMatchingError"}]}},{"Parameters":{"LambdaFunctionARN":"arn:aws:lambda:ap-northeast-1::function:reserved-item-checker","InvocationTimeLimitSeconds":"3","LambdaInvocationAttributes":{"menuSend":"$.CustomerEndpoint.Address"}},"Identifier":"ef8298b2-4673-47e1-afbf-804180261780","Type":"InvokeLambdaFunction","Transitions":{"NextAction":"45013b74-d6f6-4c5b-9958-229c8152eb9e","Errors":[{"NextAction":"9185bee7-afb4-455c-aab5-6742e0fb182c","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"31f7f1b6-d1fb-466a-b08c-fbea085f0c78","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"Text":"お手元にメニュー表をお持ちでしょうか。\n"},"Identifier":"fcd0da64-5e41-43f1-bef5-10876537cdab","Type":"MessageParticipant","Transitions":{"NextAction":"d36c5bb5-1ab8-4a2d-badd-0bca06864526","Errors":[{"NextAction":"31f7f1b6-d1fb-466a-b08c-fbea085f0c78","ErrorType":"NoMatchingError"}]}},{"Parameters":{"StoreInput":"False","InputTimeLimitSeconds":"5","Text":"お持ちでない方は、ぜろを入力してください。\nお持ちの方は、1と入力してください。"},"Identifier":"d36c5bb5-1ab8-4a2d-badd-0bca06864526","Type":"GetParticipantInput","Transitions":{"NextAction":"9eef11c7-4562-4dcd-b3d1-d43e2e6086cc","Conditions":[{"NextAction":"ef8298b2-4673-47e1-afbf-804180261780","Condition":{"Operator":"Equals","Operands":["0"]}},{"NextAction":"ba4beb4d-0897-4835-b4d5-ec14f2437477","Condition":{"Operator":"Equals","Operands":["1"]}}],"Errors":[{"NextAction":"9eef11c7-4562-4dcd-b3d1-d43e2e6086cc","ErrorType":"InputTimeLimitExceeded"},{"NextAction":"9eef11c7-4562-4dcd-b3d1-d43e2e6086cc","ErrorType":"NoMatchingCondition"},{"NextAction":"31f7f1b6-d1fb-466a-b08c-fbea085f0c78","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"8f61a260-9ec8-41b9-99ff-ef2a7e14049c","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"StoreInput":"True","InputTimeLimitSeconds":"5","Text":"商品の個数を入力してください。最後にシャープを入力してください。","DTMFConfiguration":{"DisableCancelKey":"False","InputTerminationSequence":"#"},"InputValidation":{"CustomValidation":{"MaximumLength":"2"}}},"Identifier":"f0cf9fb1-83fd-4539-825a-0086b371d530","Type":"GetParticipantInput","Transitions":{"NextAction":"ce1982c5-106c-49be-9238-a4993943f3c1","Errors":[{"NextAction":"8f61a260-9ec8-41b9-99ff-ef2a7e14049c","ErrorType":"NoMatchingError"}]}},{"Parameters":{"ComparisonValue":"$.External.result"},"Identifier":"dee019af-e351-47d3-87a0-047b006caecf","Type":"Compare","Transitions":{"NextAction":"91a15660-b149-4343-8f1e-1dca33a8a8a7","Conditions":[{"NextAction":"fd0762e8-ce67-4bf9-8c40-c9271340dc33","Condition":{"Operator":"Equals","Operands":["False"]}}],"Errors":[{"NextAction":"91a15660-b149-4343-8f1e-1dca33a8a8a7","ErrorType":"NoMatchingCondition"}]}},{"Parameters":{"Text":"$.External.message"},"Identifier":"91a15660-b149-4343-8f1e-1dca33a8a8a7","Type":"MessageParticipant","Transitions":{"NextAction":"f0cf9fb1-83fd-4539-825a-0086b371d530","Errors":[{"NextAction":"8f61a260-9ec8-41b9-99ff-ef2a7e14049c","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Attributes":{"item_num":"$.StoredCustomerInput","kana":"$.External.kana"}},"Identifier":"4c95d891-2a7c-4062-a662-7bd62407ab23","Type":"UpdateContactAttributes","Transitions":{"NextAction":"dc46043c-4201-4d76-9461-3709d3be3e83","Errors":[{"NextAction":"eea70488-2b74-4f54-8ed2-4e07309ccae0","ErrorType":"NoMatchingError"}]}},{"Parameters":{"LambdaFunctionARN":"arn:aws:lambda:ap-northeast-1::function:reserved-item-checker","InvocationTimeLimitSeconds":"3","LambdaInvocationAttributes":{"takeoutCartAdd":"True"}},"Identifier":"dc46043c-4201-4d76-9461-3709d3be3e83","Type":"InvokeLambdaFunction","Transitions":{"NextAction":"1d73241e-85f2-41b6-95fd-8c89fbcc1704","Errors":[{"NextAction":"eea70488-2b74-4f54-8ed2-4e07309ccae0","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Attributes":{"takeout":"$.External.takeout"}},"Identifier":"1d73241e-85f2-41b6-95fd-8c89fbcc1704","Type":"UpdateContactAttributes","Transitions":{"NextAction":"67138f75-d10a-4c25-a5c7-14b7667d5821","Errors":[{"NextAction":"eea70488-2b74-4f54-8ed2-4e07309ccae0","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Text":"もう一度お聞きします"},"Identifier":"1cf0652b-eb96-407c-b288-89399ffd1dfa","Type":"MessageParticipant","Transitions":{"NextAction":"0b467922-e192-4211-af53-66bf62b33269","Errors":[{"NextAction":"6eb5ba9b-5895-4905-b2ea-3f89a56bfa46","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"6eb5ba9b-5895-4905-b2ea-3f89a56bfa46","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"StoreInput":"True","InputTimeLimitSeconds":"5","Text":"ご希望の商品の番号を入力して、最後にシャープを入力してください。","DTMFConfiguration":{"DisableCancelKey":"False","InputTerminationSequence":"#"},"InputValidation":{"CustomValidation":{"MaximumLength":"20"}}},"Identifier":"ba4beb4d-0897-4835-b4d5-ec14f2437477","Type":"GetParticipantInput","Transitions":{"NextAction":"18a120d2-4b63-4e67-8dd5-e32a5649454a","Errors":[{"NextAction":"dc55d7f4-fd0a-45d9-94e2-14affd19518c","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"eea70488-2b74-4f54-8ed2-4e07309ccae0","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"Text":"$.Attributes.kana が $.Attributes.item_num 個、カートに追加されました。"},"Identifier":"67138f75-d10a-4c25-a5c7-14b7667d5821","Type":"MessageParticipant","Transitions":{"NextAction":"0b467922-e192-4211-af53-66bf62b33269","Errors":[{"NextAction":"eea70488-2b74-4f54-8ed2-4e07309ccae0","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"8c2c2cab-6d36-4681-bfe1-859207db364e","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"Attributes":{"date":"$.StoredCustomerInput"}},"Identifier":"830d2712-87f7-4be7-a343-97f38af0cdfc","Type":"UpdateContactAttributes","Transitions":{"NextAction":"3f6cb8fc-5f47-4671-bb75-220c329fc978","Errors":[{"NextAction":"8c2c2cab-6d36-4681-bfe1-859207db364e","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Text":"$.External.message"},"Identifier":"1a81baeb-66f7-4e48-9e12-440f2d8326f0","Type":"MessageParticipant","Transitions":{"NextAction":"963d038b-935c-485f-856e-7d24432c8dbc","Errors":[{"NextAction":"6eb5ba9b-5895-4905-b2ea-3f89a56bfa46","ErrorType":"NoMatchingError"}]}},{"Parameters":{"StoreInput":"False","InputTimeLimitSeconds":"5","Text":"続けて商品を追加する場合はゼロを入力してください。\n商品の選択を終了する場合は、いちを入力してください。"},"Identifier":"0b467922-e192-4211-af53-66bf62b33269","Type":"GetParticipantInput","Transitions":{"NextAction":"1cf0652b-eb96-407c-b288-89399ffd1dfa","Conditions":[{"NextAction":"ba4beb4d-0897-4835-b4d5-ec14f2437477","Condition":{"Operator":"Equals","Operands":["0"]}},{"NextAction":"963d038b-935c-485f-856e-7d24432c8dbc","Condition":{"Operator":"Equals","Operands":["1"]}}],"Errors":[{"NextAction":"1cf0652b-eb96-407c-b288-89399ffd1dfa","ErrorType":"InputTimeLimitExceeded"},{"NextAction":"1cf0652b-eb96-407c-b288-89399ffd1dfa","ErrorType":"NoMatchingCondition"},{"NextAction":"eea70488-2b74-4f54-8ed2-4e07309ccae0","ErrorType":"NoMatchingError"}]}},{"Parameters":{"StoreInput":"True","InputTimeLimitSeconds":"5","Text":"受け取り日時をお聞きします。\n日付を入力してください。\n例えば、12月8日であれば、1208と入力してください。","DTMFConfiguration":{"DisableCancelKey":"False"},"InputValidation":{"CustomValidation":{"MaximumLength":"4"}}},"Identifier":"963d038b-935c-485f-856e-7d24432c8dbc","Type":"GetParticipantInput","Transitions":{"NextAction":"830d2712-87f7-4be7-a343-97f38af0cdfc","Errors":[{"NextAction":"8c2c2cab-6d36-4681-bfe1-859207db364e","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Text":"$.External.message"},"Identifier":"c695637d-d921-480a-b55e-1d1868862f23","Type":"MessageParticipant","Transitions":{"NextAction":"c347c5f6-3805-4770-9c84-da4a9156c8b1","Errors":[{"NextAction":"b27fc9e4-83d2-4f65-9346-93508fa94af5","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Text":"$.External.message"},"Identifier":"629650f4-96c9-4511-a994-383bb70a6d38","Type":"MessageParticipant","Transitions":{"NextAction":"1c5107ba-f7c9-4ad8-a409-3984eb09203e","Errors":[{"NextAction":"1c5107ba-f7c9-4ad8-a409-3984eb09203e","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Attributes":{"time":"$.StoredCustomerInput"}},"Identifier":"14f7fe64-5844-435d-a748-5447338af00f","Type":"UpdateContactAttributes","Transitions":{"NextAction":"b09fd57b-06d7-4186-adef-a168450360de","Errors":[{"NextAction":"1c5107ba-f7c9-4ad8-a409-3984eb09203e","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"1c5107ba-f7c9-4ad8-a409-3984eb09203e","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"ComparisonValue":"$.External.result"},"Identifier":"b0645600-d53c-4496-aa21-5d34b8a7944c","Type":"Compare","Transitions":{"NextAction":"629650f4-96c9-4511-a994-383bb70a6d38","Conditions":[{"NextAction":"c695637d-d921-480a-b55e-1d1868862f23","Condition":{"Operator":"Equals","Operands":["False"]}}],"Errors":[{"NextAction":"629650f4-96c9-4511-a994-383bb70a6d38","ErrorType":"NoMatchingCondition"}]}},{"Parameters":{"LambdaFunctionARN":"arn:aws:lambda:ap-northeast-1::function:reserved-item-checker","InvocationTimeLimitSeconds":"8","LambdaInvocationAttributes":{"timeCheck":"True"}},"Identifier":"b09fd57b-06d7-4186-adef-a168450360de","Type":"InvokeLambdaFunction","Transitions":{"NextAction":"b0645600-d53c-4496-aa21-5d34b8a7944c","Errors":[{"NextAction":"1c5107ba-f7c9-4ad8-a409-3984eb09203e","ErrorType":"NoMatchingError"}]}},{"Parameters":{},"Identifier":"b7a461e6-047c-4331-bebb-8116c8a31396","Type":"DisconnectParticipant","Transitions":{}},{"Parameters":{"Text":"$.External.store_name です。\nこちらの電話はテイクアウト専門窓口です。"},"Identifier":"66925e31-a71c-4544-b45a-8400440f72cd","Type":"MessageParticipant","Transitions":{"NextAction":"fcd0da64-5e41-43f1-bef5-10876537cdab","Errors":[{"NextAction":"b7a461e6-047c-4331-bebb-8116c8a31396","ErrorType":"NoMatchingError"}]}},{"Parameters":{"Text":"お電話ありがとうございます。"},"Identifier":"f80cddb4-96b8-412e-aa74-f43c8ac641f4","Type":"MessageParticipant","Transitions":{"NextAction":"75cfc998-9a25-427e-b054-1a5b296f24d7","Errors":[{"NextAction":"773dd1e8-48f8-48fb-906f-25422a02df46","ErrorType":"NoMatchingError"}]}},{"Parameters":{"LambdaFunctionARN":"arn:aws:lambda:ap-northeast-1::function:reserved-item-checker","InvocationTimeLimitSeconds":"3","LambdaInvocationAttributes":{"storeInfo":"store_name"}},"Identifier":"75cfc998-9a25-427e-b054-1a5b296f24d7","Type":"InvokeLambdaFunction","Transitions":{"NextAction":"66925e31-a71c-4544-b45a-8400440f72cd","Errors":[{"NextAction":"b7a461e6-047c-4331-bebb-8116c8a31396","ErrorType":"NoMatchingError"}]}}]}

動作確認

では、動作確認してみます。商品については以下の情報を登録しています。

通常の電話予約

以下の動画のようにガイダンスが流れ、メニューのSMS送信、商品の選択から予約日時までの入力ができております。

また、予約完了後は、以下のようにGoogle Calendar上に予約内容は反映されております。

ユーザから予約をさせたくないとき

事前に以下のように Google Calendar にブロックを入れることでユーザから予約できないようになります。

日単位で予約をさせたくないときは、以下のように終日のスケジュールを追加します。

 

最後に

今回はざっくりと作ってみましたが、結構規模が大きいので、時間がある時にCloudFormation化したいと考えています。 また、その際に細かい機能追加とかもやってみたいですね。