やべぇ!? クレカの口座引き落とし忘れてた...からのGAS+Amazon Connect自動電話通知システム構築

# 本記事について
- これはmediba Adventカレンダーの14日目の記事です。テクノロジーセンター Eng5Gのずーんが担当いたします。インフラエンジニアのマネージャーを普段やってます。
- Amazon Connect + Google Apps Scriptの連携をやろうと思っている方向けの事例として記載しています。
- あくまでも個人的に作っているものなので、不具合や💩等があると思いますのでご承知おき下さい。

# ダメージの大きなやらかし
突然ですが皆さん。クレジットカード等の口座引き落としをうっかり忘れてたなんてことありませんか？私はつい先日やらかしました。Googleカレンダーにも引き落としの日を入力していたのですが、カレンダーからの通知を見逃してお金を移動することを忘れました。支払いが遅れるとクレジットヒストリーにも傷がついてしまうのでかなり落ち込みました。

# カレンダーの通知って目立ちにくい
再発防止をするために見逃してしまう原因を考えたのですが、カレンダーの通知って押しが弱いのでスルーしがちだと思いました。ちょろっと音が鳴って予定が表示されるだけなので仕事で忙しい時などは後で見よとなってそのまま忘れることがしばしばあります。

# そこで考えた方法
普段インフラエンジニアをやっている関係でTwilioなどを利用したアラート電話などを受け取ることが多いのですが、電話の着信は何かしらのアクションを行わない限り止まらないのと作業を中断して見なければならない気持ちがただの通知よりも強く働くことに気が付きました。

そこでGoogleカレンダーに登録されている引き落としイベントの前日に電話で自動的に通知するシステムを構築しようと考えました。

# アーキテクチャ
自動電話通知システムの構成は次のようにしてみました。

![ブログ.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/50787cf9-f271-cb9c-7515-dbf87182e6d5.png)

- 採用理由
    - Amazon Connect
        - 電話と連携できるAPIかつ個人でも利用できるサービスはかなり限られています。Twilio等を検討しましたが電話フローをコードレスで構築できてかつ、AWSサービスとの連携が容易であるため採用しました。
    - Google Apps Script (GAS)
        - 今回はGoogleカレンダーからのデータを取得するということでGASを採用しました。いままで触ったことがほとんどなかったため触りたかったというのも理由になります。
        - 基本JavaScriptで処理を書くことになります。
    - AWS Lambda
        - サーバレスでお手軽に処理を書くと言ったらAWS LambdaやCloud Funcionsが思い浮かびますが、今回はAmazon Connectとの連携が必要なためAWS Lambdaを採用しました。
        - 言語はPythonでPython3.9を採用しました。

# 構築方法

1. Amazon Connectの構築
    1. 東京リージョンでAmazon Connectのコンソールを開きます。
    1. インスタンスを追加するボタンをクリックして、インスタンスを作成します。パラメータは下記をご参考ください。
        - IDを管理
            - Amazon Connectにユーザを保存
        - アクセス URL
            - 任意の名前を入力
                - インスタンスにアクセスするときのURLになります。
        - 管理者を指定
            - 自分のアカウント情報を適宜入力
                - IAMとは全く別なので自由に設定してもよい
        - テレフォニーオプション
            - 着信を許可するのみにチェック
        - データストレージ
            - デフォルトのまま
            - 利用しないのでCustomer Profilesを有効化のチェックは解除する
    1. インスタンスの作成ボタンを押下するとインスタンスの構築が実行されます。しばらくするとインスタンス作成が完了するのでアクセスURLからAmazon Connectの管理画面にアクセスする。
    1. 先ほど管理者に指定したアカウント情報を入力し、ログインする。
    1. ログインが完了するとこのような画面になります。
        - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/696d957c-1949-dd9c-438f-e8a992ec3c69.png)
    1. 左のメニューから下記のアイコンのメニューを展開し、電話番号を選択します。
        - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/9183fe07-ef2e-2b12-9fd9-27e59f0e0407.png)
    1. 電話番号の取得ボタンをクリックして、電話番号取得の手続きを行います。
        - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/af86cfed-8e2e-f2ee-458e-a4bbddaa20aa.png)
    1. 電話番号取得のためにチャンネルと国を選択します。
        - **注意1. 日本の電話番号は取得に際して、法人登記等の証明書が必要になるため個人では取得不可能です。日本の法律で厳しい規則があるのでしょうがないです。。。**
            - https://docs.aws.amazon.com/ja_jp/connect/latest/adminguide/phone-number-requirements.html#japan-requirements
        - **注意2. Amazon Connectでは日本の携帯電話のプレフィックスナンバーにあたる`+8109, +8108, +8107`に対して初期状態ではコールできません。サポート窓口から緩和申請を行う必要があります。SESと似たようなものですね。**
            - 手続きの参考にしたページ→  https://dev.classmethod.jp/articles/from-2021-early-aug-tokyo-connect-instance/
            - 思いのほか完了までに時間が掛かります。
        - **注意3. 電話番号を取得して保持している間は１日単位で課金が発生します。どのタイプの電話番号を持つかによって単価が変動します。オーストラリアの電話番号はDIDだと1日0.1USD掛かります。**
        - このため今回はオーストラリアの電話番号を選択し、取得します。
        - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/aba1d426-668b-9e93-a1c9-d87fc44ada26.png)
    1. 電話番号を取得出来たらフローメニューを選択して、フローを作成をクリックする。処理を作成して、保存の上公開ボタンをクリックする。
        - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/eb2d4dd1-2140-9dd8-380a-ca0015ec5b20.png)
    1. 左のブロックメニューからアクションを選択して、繋げていくことで簡単に電話でやりたいことを指示できます。
        - ここで利用しているブロック
            - 音声の設定
                - 合成音声を作成するための初期設定。言語や音声の種類を選択できます。ここでは`日本語・Kazuha`にしています。
            - プロンプトの再生
                - 合成音声を利用してしゃべらせる内容を定義しています。ただ単に固定のテキストをしゃべらせるだけではなく、動的に定義される属性を利用して内容を差し替えることが可能です。ユーザに関する属性なども使えるようになっています。
                - ここでは、どの引き落としイベントがあるのかをAPIから差し込みできるように`withdraw`という属性を定義して読み上げる処理を入れています。
            - 切断
                - 処理の終端になります。ここでは、エラー等が起きても切断するように設定しています。
        - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/66dd2101-d6b7-3437-a173-f9ed965a885b.png)

    1. ここまで出来たら次の情報をメモしておきます。
        - Amazon Connect Instance ID
            - コンソールのARNから取得可能
        - Contact Flow ID
            - 作成したフローのIDです。取得にはコツがいります。
            - フロー編集画面のURLの`contact-flow%2F`から末尾にかけての文字列
                -例 https://hogehoge.my.connect.aws/contact-flows/edit?id=arn%3Aaws%3Aconnect%3Aap-northeast-1%0123456789%3Ainstance%2Fea0a7544-hoge-hoge-8cea-e7f9d5b6f57b%2Fcontact-flow%2Fb7a5426a-4807-hoge-hoge-c60d1ccf5336
                - この場合、`b7a5426a-4807-hoge-hoge-c60d1ccf5336`がIDになる


1. AWS Lambdaの構築
    1. 今回はSAM CLIを利用して構築します。長くなるので細かな内容は省きます。
        - ソースはこちら→ https://github.com/jd10ne/event_notify_caller/tree/main/event-notify-caller
        - デプロイ前に`samconfig_example.toml`内の`parameter_overrides`に先ほどメモした内容を記入したのち、名前を`samconfig.toml`にします。
        ```
        parameter_overrides = [
            "DestinationPhoneNumber=<電話をかける先の番号>",
            "ContactFlowId=<Contact Flow ID>",
            "AmazonConnectInstanceId=<Amazon Connect Instance ID>",
            "SourcePhoneNumber=<今回取得した電話番号>"
        ]
        ```
        - その他、Profileなどを適宜指定して、`sam deploy`でデプロイします。
    1. 肝心のPythonのコードではboto3を利用して、Amazon Connectの`start_outbound_voice_contact`のAPIを叩いています。**APIを実行する際の引数に`Attributes`という項目がありますが、こちらでコンタクトフローに渡したいパラメータを入力することができます。今回は`withdraw`という属性にGASから受け取ったイベント内容を突っ込んで渡しています。**（サクッと作っているのでコードが汚いのは許してください）
        - https://github.com/jd10ne/event_notify_caller/blob/main/event-notify-caller/caller/app.py
    1. また、template.yamlで作成しているリソースはLambdaとAmazon Connectを操作するためのロールです。今回はAPI Gatewayを利用せずに関数URLを利用するためにURLの発行設定も記述しています。
    1. SAMのデプロイができたら関数URLをLambda Functionのコンソールの設定画面より取得してメモしておきます。

1. GASの構築
    1. GASへのアクセスはGoogleスプレッドシートから可能です。
        1. 拡張機能を選択し、Apps Scriptを選択します。
            - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/598a0f12-70d0-427f-3f94-94f045d8bf31.png)
        1. ファイルの+ボタンを押して、スクリプトを追加してコードを記述します。記述が終わったら保存ボタンを押下します。
            - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/8d421d38-e989-b052-a176-23acc4d43931.png)

            ```javascript
            const LAMBDA_URL = PropertiesService.getScriptProperties().getProperty("LAMBDA_URL") // AWS Lambdaの呼び出しURL
            const regx = /引き落とし/g // イベントのタイトルで引き落としのイベントに含まれるワード
            
            
            const main = function() {
              var calendars = CalendarApp.getAllOwnedCalendars();
            
              // 引き落としがあるイベント
              let withdraw_event = []
            
              // スキャン対象日付
              const now = new Date()
              console.log(now)
              const nextday_begin = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0)
              const nextday_end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 23, 59, 0, 0)
              console.log(nextday_begin, nextday_end)
            
              // 対象期間のイベント取得
              const events = CalendarApp.getEvents(nextday_begin, nextday_end)
              console.log(events.map((x) => x.getTitle()))
            
              // 対象期間中のイベントをスキャンし、regxに当てはまるイベントがあればtrueを配列に格納する。なければFalseを配列に格納する。
              const withdraw_eventflags = events.map((x) => {
                const title = x.getTitle()
                const result = title.match(regx)
                if (result === null) {
                  return false
                }
                withdraw_event.push(title)
                return true
              })
            
              // 最終的にregxに含まれるイベントがあったかを判定
              const withdraw_flag = withdraw_eventflags.reduce((x, y) => x || y, false)
              console.log(withdraw_flag)
            
              // regxが含まれるイベントがあった場合
              if (withdraw_flag) {
                console.log(withdraw_event)
                const event_name = withdraw_event.join('、')
                const base_url = LAMBDA_URL
            
            
                // AWS Lambdaに対してHTTPリクエスト
                // クエリパラメータであるwithdrawに引き落としイベント名を包含して引き渡す。
                const url = base_url + '?withdraw=' + event_name
                const options = {
                  method: "GET",
                }
                UrlFetchApp.fetch(url, options)
              }
            
              return
            }
            ```

        1. 左メニューからプロジェクトの設定をクリックして、スクリプトプロパティとして、LAMBDA_URLを設定します。値には先ほどメモしたLambdaのURLを入力し保存します。
            - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/f0ec1f10-6908-6a26-d56d-feb453fe3219.png)
            - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/6e294d54-41bc-304e-b8ed-8c918d0e2507.png)
        1. エディタ画面に戻り、一度実行ボタンをクリックします。そうすると自分のGoogleアカウントへのアクセスをGASから許可するかを尋ねるプロンプトが出てくるため許可を押下します。
        1. ここまで来たらあとは、実行トリガーを設定します。左メニューのトリガーを押下します。
            - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/de1b4152-e5c9-01dd-e4ba-48e1483a1569.png)
        1. 右下のトリガーを追加ボタンをクリックして、画像のように設定し、保存します。
            - ここでは毎晩7-8時の間に実行される設定になっています。
            - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181467/d1d78c22-b0e6-2d85-d1a4-a68e76a01dac.png)


# 実際の挙動

実際にこのシステムが動くとこのような電話が掛かってきます。

<iframe width="560" height="315" src="https://www.youtube.com/embed/A9kwyHmTj7Y?si=QKG7JpORjHebQEIK" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

# 感想
GASを今回初めて触りましたが想像以上に簡単にGoogleサービスとの連携ができたので、今後会社の業務でもより活用していきたいモチベーションが上がりました。また、Amazon Connectとの連携はLambdaを嚙ませれば簡単にできるので2日程度で構築ができました。これで二度と口座引き落としを忘れることはないでしょう。

# おわりに
現在medibaでは、メンバーを大募集しています。

[募集・応募ページ](https://www.mediba.jp/recruit)
medibaってどんな会社だろう？ と興味を持っていただいた方は、
カジュアル面談もやっておりますので、お気軽にお申込み頂ければと思います。

特にAWS周りのインフラ強い人待ってます！