続・Amazon Connectで「営業日/休業日」「受付時間内/受付時間外」を判定する手法

今回は「具体的な仕組みの実装方法」「日付・時刻を変えてテストを行う方法」なども詳しく解説します!
2022.11.27

みなさん、こんにちは!
福岡オフィスの青柳です。

「Amazon Connect」で「営業日/休業日」「受付時間内/受付時間外」を判定する手法について、以前、下記の記事を公開しました。

今回は、前回よりも更に踏み込んで、「営業日・受付時間を判定する具体的な仕組みの実装方法」や「日付・時刻を変えてテストを行う方法」についてご紹介したいと思います。

前回のおさらい

前回の記事では、「営業日/休業日」「受付時間内/受付時間外」を判定する手法として、以下の4つのパターンを紹介しました。

  • パターン1: Amazon Connectの「オペレーション時間」を使って判定する
  • パターン2: Amazon ConnectからLambdaを使って「設定ファイル」を読み込み、判定を行う
  • パターン3: Amazon ConnectからLambdaを使って「設定データベース」を読み込み、判定を行う
  • パターン4: 「Systems Manager Change Calendar」を使って判定する

これらのうち「パターン1」は、「曜日」毎に受付時間が完全に決まっており「祝日」「(企業や店舗独自の) 特別休業日」などの考慮が不要な場合にのみ使用することができる方法です。

「祝日」「特別休業日」などを考慮しなければならない場合は、パターン2~4のいずれかを選択する必要があります。 (これらはいずれもLambdaを使ってプログラムコードを記述する必要があります)

今回は、これらの中から「パターン3: Amazon ConnectからLambdaを使って「設定データベース」を読み込み、判定を行う」について、更に深掘りをしていきます。

今回ご紹介するパターン

  • パターン3-a: 営業日は「Lambda関数+DynamoDB」を使って判定を行い、受付時間はAmazon Connect標準の「オペレーション時間」を使って判定する
  • パターン3-b: 営業日は「Lambda関数+DynamoDB」を使って判定を行い、受付時間は「オペレーション時間」を複数使って判定する
  • パターン3-c: 営業日、受付時間を共に「Lambda関数+DynamoDB」を使って判定する

なお、今回の記事では、処理を実現する仕組み (データベースの構造、Lambda関数コード、Amazon Connectコンタクトフローなど) の説明を中心に行い、「Lambda実行IAMロールの設定」「Amazon ConnectインスタンスへのLambda関数の登録」などについては説明を割愛します。 (必要に応じてAWSドキュメントなどで確認してください)

パターン3-a: 営業日は「Lambda関数+DynamoDB」を使って判定を行い、受付時間はAmazon Connect標準の「オペレーション時間」を使って判定する

「営業日/休業日」「受付時間内/受付時間外」を判定する手法として、恐らく最も使われているのではないかと思われる手法です。

ここでは、営業日・受付時間が以下のように決められているものとします。

日付の種類 受付時間
平日 (月曜日~金曜日) 08:30 ~ 17:30
土曜日・日曜日・祝日 休業日

それでは、設定を行っていきましょう。

DynamoDBテーブル

まず、「休業日」(祝日や特別休業日) の一覧を記述したデータベースをDynamoDBで準備します。

Date (S)(PK) Description (S)
2023-01-01 元日
2023-01-02 振替休日
2023-01-09 成人の日
2023-02-11 建国記念の日
・・・ ・・・

「祝日って『年』を指定する必要あるの?」と思われる方がいらっしゃるかもしれません。

日本の祝日は以下のように年によって日付が変わる要素があるため、各年ごとの祝日の情報をデータベースとして持たせる必要があるのです。

  • 春分の日、秋分の日
  • いわゆる「ハッピーマンデー」制度による祝日 (成人の日、スポーツの日など)
  • 振替休日

Lambda関数

次に、このデータベースを使って「営業日/休業日」の判定を行うLambda関数を記述します。

import boto3
import json
import os
from datetime import datetime
from zoneinfo import ZoneInfo

# 環境変数から設定値を取得
table_name_days = os.environ['DYNAMODB_TABLE_NAME_DAYS']    # DynamoDBテーブル名(カレンダー)
time_zone = os.environ['TIME_ZONE']                         # タイムゾーン(例:日本の場合"Asia/Tokyo")

# DynamoDBアクセス (リソースAPIを使用)
dynamodb = boto3.resource('dynamodb')


########################################
# Lambdaハンドラーメソッド
########################################
def lambda_handler(event, context):
    # for debug
    print(json.dumps(event))

    # 現在の日付を取得する (YYYY-MM-DD形式)
    dt_now = datetime.now(ZoneInfo(time_zone))
    cur_date = dt_now.strftime('%Y-%m-%d')

    # DynamoDBテーブルから現在日付をキーにしてアイテム取得を試みる
    table = dynamodb.Table(table_name_days)
    response = table.get_item(Key={'Date': cur_date})

    # 取得結果を確認する
    if 'Item' in response:
        is_holiday  = True
        description = response['Item'].get('Description', '')
    else:
        is_holiday  = False
        description = ''

    # 戻り値をセット
    return {
        'IsHoliday'  : is_holiday,
        'Description': description
    }

Lambda関数の入力値 (入力イベント) は取りません。(関数内部で現在日付を取得するため)
戻り値として以下を返します。

  • IsHoliday: 現在日付が休業日であるかどうか (True/False)
  • Description: 現在日付が祝日だった場合の祝日名

Lambda関数の処理内容は下図フローチャートを参照してください。

Amazon Connectオペレーション時間

続いて、Amazon Connectの「オペレーション時間」を定義します。

開始 終了
月曜日 08:30 AM 05:30 PM
火曜日 08:30 AM 05:30 PM
水曜日 08:30 AM 05:30 PM
木曜日 08:30 AM 05:30 PM
金曜日 08:30 AM 05:30 PM

平日 (月曜日~金曜日) の受付時間を定義しています。
土曜日・日曜日は休業日であるため定義はありません。

Amazon Connectコンタクトフロー

最後に、Amazon Connectのコンタクトフローを作成します。

まず最初に、Lambda関数を呼び出します。・・・(1)

  • 関数のARN: 「手動で設定」を選択
  • 関数を追加: <作成したLambda関数のARN>を指定

続いて、Lambda関数の実行結果を確認するために「コンタクト属性を確認する」ブロックを配置します。・・・(2)

  • 確認する属性
    • 名前空間: 「外部」を選択
    • 値: IsHolidayを指定
  • チェックする条件
    • 条件1
      • 条件: 「次と等しい」を選択
      • 値: falseを指定
    • 条件2
      • 条件: 「次と等しい」を選択
      • 値: trueを指定

※ Lambda関数 (Python) でbool型の戻り値を返すと、Amazon Connectコンタクトフロー側では文字列値「true」「false」と認識されるようです。(先頭が小文字であることに注意)

Lambda関数の戻り値「IsHoliday」の値が「false」であった場合、「オペレーション時間の確認」ブロックへ進みます。・・・(3)

作成した「オペレーション時間」を指定します。

オペレーション時間を確認した結果、「時間内」であれば、そのまま電話を受け付ける処理へと進みます。・・・(4)

一方、「時間外」であれば、受付時間外の処理へ進みます。・・・(5)
また、「IsHoliday」の値が「true」であった場合も、同様に受付時間外の処理へ進みます。

以上で「パターン3-a」の仕組みが完成しました。

パターン3-b: 営業日は「Lambda関数+DynamoDB」を使って判定を行い、受付時間は「オペレーション時間」を複数使って判定する

では、前提条件を少し変えて、営業日・受付時間が以下のように決められている場合を考えてみましょう。

日付の種類 受付時間
平日 (月曜日~金曜日) 08:30 ~ 17:30
土曜日・日曜日・祝日 08:30 ~ 12:00

さきほどのパターンと異なるのは、休日 (土・日・祝日) が休業日ではなく「平日とは異なる受付時間での営業」という点です。

はて、これは困りました。
Lambda関数を使って「祝日」と判定した後に、受付時間の判定を行わなければなりません。
しかし、祝日は月曜日かもしれないし、火曜日かもしれないし、日曜日かもしれません。
どうやって受付時間の判定を行えばよいのでしょうか?

答えは、「オペレーション時間」を「通常の場合」「祝日専用」の2通り用意しておく、です。

「通常の場合」の「オペレーション時間」は以下のように定義します。

開始 終了
月曜日 08:30 AM 05:30 PM
火曜日 08:30 AM 05:30 PM
水曜日 08:30 AM 05:30 PM
木曜日 08:30 AM 05:30 PM
金曜日 08:30 AM 05:30 PM
土曜日 08:30 AM 12:00 PM
日曜日 08:30 AM 12:00 PM

「祝日専用」の「オペレーション時間」は以下のように定義します。

開始 終了
月曜日 08:30 AM 12:00 PM
火曜日 08:30 AM 12:00 PM
水曜日 08:30 AM 12:00 PM
木曜日 08:30 AM 12:00 PM
金曜日 08:30 AM 12:00 PM
土曜日 08:30 AM 12:00 PM
日曜日 08:30 AM 12:00 PM

ここでのポイントは「曜日に関わらず休日の受付時間を定義する」という点です。

次に「Lambda関数」と「DynamoDB」ですが、これらは「パターン3-a」と同じもので構いません。

最後に、コンタクトフローを作成します。

「Lambda関数の呼び出し」「コンタクト属性の確認」については、「パターン3-a」から変わりません。・・・(1)(2)

Lambda関数の実行結果を確認した結果により、後続の処理を以下のように分けます。

  • false」であった場合: オペレーション時間 (通常の場合) を確認する ・・・(3)
  • true」であった場合: オペレーション時間 (祝日専用) を確認する ・・・(4)

あとは、それぞれの「オペレーション時間の確認」の結果に従って「時間内」の処理 (電話を受け付ける)、「時間外」の処理 (メッセージを流して電話を切る) へ振り分けます。・・・(5)(6)

このようにすることで、祝日でない場合は曜日に応じた受付時間の判定、祝日の場合は「祝日専用」の受付時間判定を行うことができる訳です。

「営業日」判定のテストを行いたい

「パターン3-c」へ進む前に、少し切り口を変えて考えてみます。

Amazon Connectにおいて「営業日」を判定する処理を実装した際、恐らく多くの方が悩んでいることの一つに「判定処理のテストを行いたい」という点があるのではないかと思います。

  • 判定処理自体のテスト (祝日を正しく判定できるか等)
  • 判定処理の結果、分岐する各フロー処理のテスト

これがPC上で動くアプリケーションなどであれば、システム内蔵時計を任意の日付に変えてテストを行ったりするところです。

しかし、Amazon Connectでは現在の日付を変えるようなことはできないため、テストを行おうとすると、実際に日曜日や祝日に電話を掛けてみるしかないのです。

そこで、判定処理の仕組みを少し変えて、任意の日付でテストを行えるようにしたいと思います。

「パターン3-a」で使ったLambda関数のコードを以下のように書き換えます。

(省略)

########################################
# Lambdaハンドラーメソッド
########################################
def lambda_handler(event, context):
    # for debug
    print(json.dumps(event))

    # イベントパラメーターで日付が指定されているかチェックする
    cur_date = event['Details']['Parameters'].get('Date', '')

    # 日付がパラメーターで与えられていない場合
    if cur_date == '':
        # 現在の日付を取得する (YYYY-MM-DD形式)
        dt_now = datetime.now(ZoneInfo(time_zone))
        cur_date = dt_now.strftime('%Y-%m-%d')

    # DynamoDBテーブルから現在日付をキーにしてアイテム取得を試みる
    table = dynamodb.Table(table_name_days)
    response = table.get_item(Key={'Date': cur_date})

(省略)

そして、コンタクトフローの「Lambda関数を呼び出す」ブロックで、テスト用のパラメーターを設定します。

これによって、パラメーター「Date」の設定有無によって、実際の日付を使うのか、テスト用の日付を使うのかを選択することができます。

  • パラーメーター「Date」が設定されていない場合: 現在の日付を取得する
  • パラーメーター「Date」に何か値が設定されている場合: 設定されたDateの値を現在の日付とする

「受付時間」判定のテストも行いたい

「営業日」判定のテストが行えることは分かりました。

そうなると、「受付時間」の判定についてもテストを行いたくなるのが人間の性ですよね (?)

しかし、Amazon Connectの「オペレーション時間」を使って受付時間を判定する場合、現在の時刻を任意に指定することはできません。

そこで、受付時間の判定を「オペレーション時間」を使わずに行う手法を考えてみます。

パターン3-c: 営業日、受付時間を共に「Lambda関数+DynamoDB」を使って判定する

DynamoDBテーブル

「受付時間」を判定するために、日付の種類 (パターン) 毎の受付時間を定義するテーブルをDynamoDBで作成します。

Pattern (N)(PK) Description (S) StartTime (S) EndTime (S)
1 月曜日 08:30 17:30
2 火曜日 08:30 17:30
3 水曜日 08:30 17:30
4 木曜日 08:30 17:30
5 金曜日 08:30 17:30
6 土曜日 08:30 12:00
7 日曜日 08:30 12:00
8 祝日 08:30 12:00

パターン「1」~「7」は、曜日の「月曜日」~「日曜日」に該当します。
加えて、祝日を表すパターン番号として「8」を定義しています。

続いて、「祝日・休業日一覧」テーブルに「パターン番号」項目を追加します。

Date (S)(PK) Description (S) Pattern (N)
2023-01-01 元日 8
2023-01-02 振替休日 8
2023-01-09 成人の日 8
2023-02-11 建国記念の日 8
・・・ ・・・ ・・・

各アイテムの「パターン番号」は、「祝日」を表す「8」をセットしておきます。

Lambda関数

そして、これらのテーブルを使って「営業日」「受付時間」を判定するLambda関数を記述します。

import boto3
import json
import os
from datetime import datetime
from datetime import date
from zoneinfo import ZoneInfo
from decimal import Decimal

# 環境変数から設定値を取得
table_name_days = os.environ['DYNAMODB_TABLE_NAME_DAYS']    # DynamoDBテーブル名(カレンダー)
table_name_hours = os.environ['DYNAMODB_TABLE_NAME_HOURS']  # DynamoDBテーブル名(営業時間)
time_zone = os.environ['TIME_ZONE']                         # タイムゾーン(例:日本の場合"Asia/Tokyo")

# DynamoDBアクセス (リソースAPIを使用)
dynamodb = boto3.resource('dynamodb')


######################################################################
# Lambdaハンドラーメソッド
######################################################################
def lambda_handler(event, context):
    # for debug
    print(json.dumps(event))

    # イベントパラメーターで日付・時刻が指定されているかチェックする
    cur_date = event['Details']['Parameters'].get('Date', '')
    cur_time = event['Details']['Parameters'].get('Time', '')

    # 日付と時刻がパラメーターで与えられていない場合
    if cur_date == '' or cur_time == '':
        # 現在の日付・時刻を取得する (YYYY-MM-DD形式、HH:MM形式)
        dt_now = datetime.now(ZoneInfo(time_zone))
        cur_date = dt_now.strftime('%Y-%m-%d')
        cur_time = dt_now.strftime('%H:%M')

    # 指定した日付が、どの営業時間パターンに該当するのかをチェックする
    response = check_operating_day(cur_date)

    pattern      = response.get('Pattern')
    holiday_name = response.get('Description')

    # 指定した営業時間パターンにおいて、指定した時刻が、営業時間内なのか時間外なのかをチェックする
    response = check_operating_hours(pattern, cur_time)

    is_open      = response.get('IsOpen')
    pattern_Name = response.get('Description')

    # 戻り値
    return {
        'IsOpen'     : is_open,
        'Pattern'    : pattern,
        'PatternName': pattern_Name,
        'HolidayName': holiday_name
    }


######################################################################
# カレンダーテーブルを参照して営業時間パターンを返す
######################################################################
def check_operating_day(cur_date: str):
    # カレンダーDynamoDBテーブルから現在日付をキーにしてアイテム取得を試みる
    table = dynamodb.Table(table_name_days)
    response = table.get_item(Key={'Date': cur_date})

    # 取得結果を確認する
    if 'Item' in response:
        # カレンダーDBに日付が存在した場合: 該当するパターンNo.を返す
        pattern     = int(response['Item'].get('Pattern'))  # DynamoDBでの「数値型」はPythonではDecimal型であるため、int型へキャストする
        description = response['Item'].get('Description', '')
    else:
        # カレンダーDBに日付が存在しなかった場合: 曜日の数値を返す (1:月曜日, 2:火曜日, ... , 7:日曜日)
        pattern     = date.fromisoformat(cur_date).isoweekday()
        description = ''

    return {
        'Pattern'    : pattern,
        'Description': description
    }


######################################################################
# 営業時間テーブルを参照して営業時間内・時間外の判定結果を返す
######################################################################
def check_operating_hours(pattern: int, cur_time: str):
    # 営業時間DynamoDBテーブルから指定した「営業時間パターン」をキーにしてアイテム取得を試みる
    table = dynamodb.Table(table_name_hours)
    response = table.get_item(Key={'Pattern': Decimal(pattern)})  # DynamoDBでの「数値型」はPythonではDecimal型であるため、int型からキャストする

    # 取得結果を確認して、営業時間 (開始時刻・終了時刻) を取得する
    if 'Item' in response:
        start_time = response['Item'].get('StartTime', '99:99')
        end_time   = response['Item'].get('EndTime'  , '99:99')
    else:
        start_time = '99:99'
        end_time   = '99:99'

    # 指定した時刻が営業時間内なのか時間外なのかを判定する
    if cur_time >= start_time and cur_time < end_time:
        is_open = True
    else:
        is_open = False

    return {
        'IsOpen'     : is_open,
        'Description': response['Item'].get('Description', '')
    }

Lambda関数の入力値 (入力イベント) として、テスト用の日付・時刻を与えることができます。
(指定しない場合は、関数内部で現在の日付・取得を取得します)

  • Date: テスト用の日付を指定 (YYYY-MM-DD形式)
  • Time: テスト用の時刻を指定 (HH:MM形式)

戻り値として以下を返します。

  • IsOpen: 現在の日付・時刻が受付時間内であるかどうか (True/False)
  • Pattern: 現在の日付が該当する「営業時間パターン」番号
  • PatternName: 「営業時間パターン」の名前
  • HolidayName: 現在の日付が祝日だった場合の祝日名

Lambda関数の処理内容は下図フローチャートを参照してください。

Amazon Connectコンタクトフロー

コンタクトフローは以下のようになります。

「オペレーション時間の確認」ブロックが不要になったため、「パターン3-a」「パターン3-b」のフローに比べてシンプルになっています。

まず、作成したLambda関数を呼び出します。・・・(1)

続いて、「コンタクト属性を確認する」ブロックでLambda関数の戻り値を確認します。・・・(2)

「パターン3-a」「パターン3-b」とは異なり、戻り値は「現在の日付・時刻が受付時間内かどうか」を示す「IsOpen」となっています。
判定する値true/falseの記述順序も変わっていますので、気を付けてください。

あとは、Lambda関数の戻り値に応じて「営業時間内」「営業時間外」へ振り分けるだけです。・・・(3)(4)

テストのために「日付」「時刻」を指定するには、「Lambda関数を呼び出す」ブロックにパラメーター「Date」「Time」を設定します。

「パターン3-c」の応用

「営業時間パターン」の定義において、祝日だけでなく夏期休業・年末年始休業・会社創立記念日などの「特別休業日」を定義することができます。

Pattern (N)(PK) Description (S) StartTime (S) EndTime (S)
1 月曜日 08:30 17:30
2 火曜日 08:30 17:30
3 水曜日 08:30 17:30
4 木曜日 08:30 17:30
5 金曜日 08:30 17:30
6 土曜日 08:30 12:00
7 日曜日 08:30 12:00
8 祝日 08:30 12:00
9 休業日 99:99 99:99

休業日には営業を行いませんので、営業時間は開始・終了ともに「99:99」を設定しておきます。
(こうすることで、時刻の比較の際に、現在時刻がどのような時刻であっても条件にヒットしないようにできます)

「祝日・休業日一覧」テーブルには、以下のように「特別休業日」の定義を追加します。

Date (S)(PK) Description (S) Pattern (N)
2022-11-03 文化の日 8
2022-11-23 勤労感謝の日 8
2022-12-29 年末年始休業 9
2022-12-30 年末年始休業 9
2022-12-31 年末年始休業 9
2023-01-01 年末年始休業 (元日) 9
2023-01-02 年末年始休業 (振替休日) 9
2023-01-03 年末年始休業 9
2023-01-09 成人の日 8
2023-02-11 建国記念の日 8
・・・ ・・・ ・・・

このようにすることで、かなり融通の利く「営業日」「営業時間」の設定・運用ができるのではないかと思います。

おわりに

今回は、「営業日/休業日」を判定する手法として「Lambda関数+DynamoDB」で実現する方法をベースにしつつ、「受付時間内/受付時間外」を判定する手法としてはAmazon Connectの「オペレーション時間」を使う方法と「Lambda関数+DynamoDB」で実現する方法をご紹介しました。

「受付時間内/受付時間外」を判定する2通りの方法については、以下のようにメリット・デメリットが考えられます。

方法 メリット デメリット
「オペレーション時間」を使う ・Lambda関数の保守が不要
・コンタクトフロー運用者が受付時間の変更を行える
・任意の時刻を指定したテストが行えない
Lambda関数+DynamoDBを使う ・任意の時刻を指定してテストを行える ・Lambda関数の保守が必要
・DynamoDBのデータ変更はシステム管理者が行うか、専用画面を作り込む必要がある

ただ、営業日の判定において「祝日」を考慮するのであれば、その時点で「Lambda関数+DynamoDB」を使う必要が発生するため、営業時間の判定で「Lambda関数+DynamoDB」を使うことのデメリットはあまり考えなくてもよいかもしれません。

皆さんの要件に応じて、今回ご紹介したパターンを使い分けて頂ければと思います。