Twilio StudioからTwilio Payを使って日本円で決済する

:::note alert
2023年5月1日を持ちまして、株式会社KDDIウェブコミュニケーションズのTwilioリセール事業が終了したため、本記事に記載されている内容は正確ではないことを予めご了承ください。
:::

# はじめに

昨年の SIGNAL イベントで発表された「 Twilio Pay 」を使うと、安全に電話（音声）でクレジットカード決済が可能になります。
Twilio Pay の使い方については、以下の記事を御覧ください。
[ Stripe と Twilio Pay で電話決済](https://qiita.com/mobilebiz/items/31b061af62b35fd82724)

上記の記事では、予め決まった金額を TwiML Bins を使って決済していました。
Twilio にはドラッグ・アンド・ドロップでコールフローが作れる [Twilio Studio](https://twilio.kddi-web.com/function/studio/) があるので、こちらを使って同様のことができないでしょうか。
実は、Twilio Studio にも、決済用のウィジェット（ Capture Payments ）が用意されているので、これを使えばできそうなのですが、 Capture Payments ウィジェットは現時点で日本円の決済に対応していません（ US Dollar / EURO / British Pond のみ）。

![スクリーンショット 2019-01-23 13.30.15.png](https://qiita-image-store.s3.amazonaws.com/0/86046/cfff8beb-b3cf-4bc1-ec73-6463130d07f3.png)

そこで、本記事では Twilio Functions を連携させることで、 Twilio Studio から日本円での決済に対応できるようにしてみたいと思います。 

## 準備

事前に Twilio と Stripe のセットアップを行っておく必要があります。
先に紹介した[こちらの記事](https://qiita.com/mobilebiz/items/31b061af62b35fd82724)を参考に、 Stripe 連携までは終わらせておいてください。

## Twilio Function の作成

今回は、動的に決済金額を受け取り、 Pay 動詞を含む TwiML を動的に生成する Function を作成します。

### Functionsの環境設定

- [こちらの画面](https://jp.twilio.com/login/kddi-web)からログインをして、Twilioの管理コンソールを開きます。
- 左側に表示されているドットが３つ表示されているアイコンをクリックすると、スライドメニューが開きます。

![kintone連携_スライドメニューアイコン.png](https://qiita-image-store.s3.amazonaws.com/0/86046/52235af7-defd-26a6-ff62-571fb9047654.png)

- **DEVELOPER TOOLS**の中にある**Runtime**を選択します。
- その中の**Functions**を選択し、さらに**Configure**を選びます。
- **Enable ACCOUNT_SID and AUTH_TOKEN**のチェックを入れます。

<img width="329" alt="スクリーンショット 2018-08-08 15.34.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/86046/6d4c5440-3587-ab70-e70f-f7a78aaf6f88.png">

- **Dependencies**の設定内にある、「Twilio」の**VERSION**を「3.23.2」に変更します。

![スクリーンショット 2019-01-23 14.39.55.png](https://qiita-image-store.s3.amazonaws.com/0/86046/50b2c96a-1d2f-5e97-16aa-a18b5fe29c03.png)

- **Save**ボタンを押して、設定を保存します。

### Twilio Functionsの作成

今回は３つの Function を作成します。
まずは、決済が完了もしくは失敗したときに呼び出される Function です。

- Twilioの管理コンソールで、先ほどと同じように**Runtime**の**Functions**を選択します。
- さらに**Manage**を選択し、**Create a new Function**もしくは、赤い＋アイコンを押して、新しいFunctionを作成します。
- テンプレートを選択するダイアログが表示されるので、**Blank**を選択した後、**Create**ボタンを押します。

<img width="802" alt="スクリーンショット 2018-08-08 21.56.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/86046/9fbea563-33cc-5480-c583-09f617e23339.png">

- **FUNCTION NAME**欄に「PayAction」と入力します。
- **PATH**欄に「/pay-action」と入力します。

![スクリーンショット 2019-01-24 10.44.31.png](https://qiita-image-store.s3.amazonaws.com/0/86046/1bbed300-f1c1-9b9c-f5b0-7197a3a6e817.png)

- **ACCESS CONTROL**のチェックボックスを外します。
- **CODE**欄に予め書かれているコードをすべて削除し、以下のコードを貼り付けます。

```javascript
exports.handler = function(context, event, callback) {
    Object.keys(event).forEach(key => {
        console.log(key, event[key]);
    });
	let twiml = new Twilio.twiml.VoiceResponse();
	const sayParam = {
	    language: 'ja-JP',
	    voice:'Polly.Takumi'
	};
	if (event.Result === 'success') {
	    twiml.say(sayParam, "決済が完了しました。ご利用ありがとうございました。");
	} else {
	    twiml.say(sayParam, "申し訳ございません。決済処理は失敗しました。");
	}
	callback(null, twiml);
};
```

- **Save**ボタンを押します。
- デプロイが自動で実行され、しばらくするとデプロイ完了の緑色のバナーが表示されれば完成です。

つぎに、決済の途中のステータスを受け取るための Function を作成します。

- **Manage**を選択し、赤い＋アイコンを押して、新しい Function を作成します。
- テンプレートを選択するダイアログが表示されるので、 **Blank** を選択した後、 **Create** ボタンを押します。
- **FUNCTION NAME**欄に「PayStatus」と入力します。
- **PATH**欄に「/pay-status」と入力します。

![スクリーンショット 2019-01-24 10.48.27.png](https://qiita-image-store.s3.amazonaws.com/0/86046/9012bc25-477c-53a3-a7f3-c4f8fc9357f5.png)

- **ACCESS CONTROL**のチェックボックスを外します。
- **CODE**欄に予め書かれているコードをすべて削除し、以下のコードを貼り付けます。

```javascript
exports.handler = function(context, event, callback) {
    Object.keys(event).forEach(key => {
        console.log(key, event[key]);
    });
	callback(null, 'OK');
};
```

- **Save**ボタンを押します。
- デプロイが自動で実行され、しばらくするとデプロイ完了の緑色のバナーが表示されれば完成です。

最後に Pay 動詞を生成する Function を作成します。

- **Manage**を選択し、赤い＋アイコンを押して、新しい Function を作成します。
- テンプレートを選択するダイアログが表示されるので、 **Blank** を選択した後、 **Create** ボタンを押します。
- **FUNCTION NAME**欄に「PayTwiML」と入力します。
- **PATH**欄に「/pay-twiml」と入力します。

![スクリーンショット 2019-01-23 14.42.22.png](https://qiita-image-store.s3.amazonaws.com/0/86046/15e127d9-97a5-1ed1-9282-e369ebe02d63.png)

- **ACCESS CONTROL**のチェックボックスを外します。
- **CODE**欄に予め書かれているコードをすべて削除し、以下のコードを貼り付けます。

```javascript
exports.handler = function(context, event, callback) {
    const amount = event.amount || 0;

	let twiml = new Twilio.twiml.VoiceResponse();
	const sayParam = {
	    language: 'ja-JP',
	    voice:'Polly.Takumi'
	};

    if (amount === 0) {
        twiml.say(sayParam, `決済金額の指定が正しくありません。`);
        callback(null, twiml);
    } else {
        twiml.say(sayParam, `これより、${amount}円のカード決済を行います。`);
    }

	const payParam = {
	    timeout: 5,
	    maxAttempts: 2,
	    postalCode: false,
	    currency: 'jpy',
	    chargeAmount: amount,
	    statusCallback: `https://${context.DOMAIN_NAME}/pay-status`,
	    action: `https://${context.DOMAIN_NAME}/pay-action`
	};
	let pay = twiml.pay(payParam);
	
	// カード番号
	let prompt = pay.prompt({
	    for: "payment-card-number"
	});
	prompt.say(sayParam, "クレジットカード番号を入力してください。");
	prompt = pay.prompt({
	    for: "payment-card-number",
	    errorType: "timeout"
	});
	prompt.say(sayParam, "入力を確認できませんでした。再度、クレジットカード番号を入力してください。");
	prompt = pay.prompt({
	    for: "payment-card-number",
	    errorType: "invalid-card-number"
	});
	prompt.say(sayParam, "ご入力頂いた番号は正しくありませんでした。再度お試しください。");
	prompt = pay.prompt({
	    for: "payment-card-number",
	    errorType: "invalid-card-type"
	});
	prompt.say(sayParam, "ご入力頂いたカードは受付できません。別のカードで再度お試しください。");
	
	// 有効期限
	prompt = pay.prompt({
	    for: "expiration-date"
	});
	prompt.say(sayParam, "有効期限を入力してください。まずは月を二桁で入力し、続けてねんを二桁で入力してください。");
	prompt = pay.prompt({
	    for: "expiration-date",
	    errorType: "timeout"
	});
	prompt.say(sayParam, "ご入力を確認できませんでした。月を二桁で入力し、続けてねんを二桁で入力してください");
	prompt = pay.prompt({
	    for: "expiration-date",
	    errorType: "invalid-date"
	});
	prompt.say(sayParam, "ご入力いただいた日付が正しくありません。月を二桁で入力し、続けてねんを二桁で入力してください");
	prompt = pay.prompt({
	    for: "expiration-date",
	    errorType: "date-validation-failed"
	});
	prompt.say(sayParam, "過去の日付はご指定いただけません。月を二桁で入力し、続けてねんを二桁で入力してください");

    // セキュリティコード
	prompt = pay.prompt({
	    for: "security-code"
	});
	prompt.say(sayParam, "カードのうら面に記載された3桁のセキュリティコードを入力してください");
	prompt = pay.prompt({
	    for: "security-code",
	    cardType: "Amex"
	});
	prompt.say(sayParam, "カードに記載された4桁のセキュリティコードを入力してください");
	prompt = pay.prompt({
	    for: "security-code",
	    errorType: "timeout"
	});
	prompt.say(sayParam, "ご入力を確認できませんでした。カードのうら面に記載された3桁のセキュリティコードを入力してください");
	prompt = pay.prompt({
	    for: "security-code",
	    cardType: "Amex",
	    errorType: "timeout"
	});
	prompt.say(sayParam, "ご入力を確認できませんでした。カードに記載された4桁のセキュリティコードを入力してください");
	prompt = pay.prompt({
	    for: "security-code",
	    errorType: "invalid-security-code"
	});
	prompt.say(sayParam, "セキュリティコードが正しくありません。カードのうら面に記載された3桁のセキュリティコードを入力してください");
	prompt = pay.prompt({
	    for: "security-code",
	    cardType: "Amex",
	    errorType: "invalid-security-code"
	});
	prompt.say(sayParam, "セキュリティコードが正しくありません。カードに記載された4桁のセキュリティコードを入力してください");
	
	// 郵便番号
	prompt = pay.prompt({
	    for: "postal-code"
	});
	prompt.say(sayParam, "郵便番号を5桁で入力してください");
	prompt = pay.prompt({
	    for: "postal-code",
	    errorType: "timeout"
	});
	prompt.say(sayParam, "入力を確認できませんでした。郵便番号を5桁で入力してください");
	prompt = pay.prompt({
	    for: "postal-code",
	    errorType: "invalid-postal-code"
	});
	prompt.say(sayParam, "郵便番号が正しくありません。郵便番号を5桁で入力してください");
	
	// 支払い処理中
	prompt = pay.prompt({
	    for: "payment-processing"
	});
	prompt.say(sayParam, "ご入力ありがとうございました。支払い処理の完了までしばらくお待ち下さい。");
	
	callback(null, twiml);
};
```

- **Save**ボタンを押します。
- デプロイが自動で実行され、しばらくするとデプロイ完了の緑色のバナーが表示されれば完成です。
- **PATH**欄の右側にあるコピーアイコンをクリックしてURLをコピーします。
- ブラウザで新しいタブを開き、URLを貼り付けて実行します。
- 以下のような内容が表示されれば成功です。

![スクリーンショット 2019-01-23 14.45.35.png](https://qiita-image-store.s3.amazonaws.com/0/86046/992254ad-e70a-365b-2d30-4eaadd7b17ae.png)

決済金額が正しくないというメッセージが表示されますが、パラメータで決済金額を渡していないので、この時点ではこのメッセージが表示されるのが正しい結果となります。

### Studio フローの作成

今回は、以下のシナリオでフローを作成します。

1. 電話が着信する。
2. ウェルカムガイダンスを流して、決済するかを確認する。
3. 決済に同意した場合は、決済フローに入る。
4. 同意しない場合は、終了する。

- 管理コンソールにログインし、スライドメニューから **Studio** を選択します。
- 赤いプラスアイコンを押すか、 **Create a flow** のボタンを押します。
- **FLOW NAME** に「Pay」と入力して、 **Next** ボタンを押します。

![スクリーンショット 2019-01-24 10.52.11.png](https://qiita-image-store.s3.amazonaws.com/0/86046/da381ac2-3352-8a64-95ca-f6a6661dded3.png)
- テンプレートの一覧から **Import from JSON** を選択して、 **Next** ボタンを押します。

![スクリーンショット 2019-01-24 10.54.10.png](https://qiita-image-store.s3.amazonaws.com/0/86046/8712c72b-2fb4-2f88-fa7c-6bc31ebe8354.png)
- **New Flow** ダイアログが開くので、すでに入力されている **{}** を削除し、以下のJSONを貼り付けます。

```json
{
  "description": "A New Flow",
  "states": [
    {
      "type": "InitialState",
      "name": "Trigger",
      "properties": {
        "offset": {
          "x": 0,
          "y": 0
        },
        "flow_url": "https://webhooks.twilio.com/v1/Accounts/ACxxxxx/Flows/FWxxxxx"
      },
      "transitions": [
        {
          "event": "incomingMessage",
          "conditions": [],
          "next": null,
          "uuid": "a6d7eae3-cbfd-4f99-a28e-8b725193012d"
        },
        {
          "event": "incomingCall",
          "conditions": [],
          "next": "FF1a9110dfd6de76268cfab095496cca9b",
          "uuid": "0930b5f8-da19-4ad1-bee8-d41eeb8f61e7"
        },
        {
          "event": "incomingRequest",
          "conditions": [],
          "next": null,
          "uuid": "4ebe0ffa-138c-42ed-913d-b3cb3ba884c4"
        }
      ],
      "sid": "FFb5d6499ccbd02baa04da1d7768243e3e"
    },
    {
      "type": "Gather",
      "name": "Gather",
      "properties": {
        "offset": {
          "x": 0,
          "y": 200
        },
        "timeout": 5,
        "finish_on_key": "#",
        "stop_gather": true,
        "number_of_digits": 1,
        "save_response_as": null,
        "say": "お電話ありがとうございます。トゥイリオ決済システムです。\nこれより、2000円の決済を行います。\nよろしければ数字の１を、中止する場合は、それ以外のキーを押してください。",
        "play": null,
        "voice": "Polly.Takumi",
        "language": "ja-JP",
        "loop": 1,
        "hints": null,
        "gather_language": "en"
      },
      "transitions": [
        {
          "event": "keypress",
          "conditions": [],
          "next": "FFdf43a2312788acf373d890b1d4e9290b",
          "uuid": "104a861f-18d6-4a2c-9444-b64443229233"
        },
        {
          "event": "speech",
          "conditions": [],
          "next": null,
          "uuid": "c2e6f020-28f8-40ba-80d8-8193c64dc737"
        },
        {
          "event": "timeout",
          "conditions": [],
          "next": null,
          "uuid": "47c501a0-4089-42d3-a813-e74436699c23"
        }
      ],
      "sid": "FF1a9110dfd6de76268cfab095496cca9b"
    },
    {
      "type": "Branch",
      "name": "Split",
      "properties": {
        "offset": {
          "x": 10,
          "y": 430
        },
        "input": "{{widgets.Gather.Digits}}"
      },
      "transitions": [
        {
          "event": "noMatch",
          "conditions": [],
          "next": "FFdddfb45308ae195b6acdad54e3fe24da",
          "uuid": "bc106591-1706-4003-8a10-dafdc4379b53"
        },
        {
          "event": "match",
          "conditions": [
            {
              "friendly_name": "If value equal_to 1",
              "type": "equal_to",
              "arguments": [
                "{{widgets.Gather.Digits}}"
              ],
              "value": "1"
            }
          ],
          "next": "FFb9696e9d5c63dd3770f91460f7fef208",
          "uuid": "d354190c-7e29-40f5-a14e-b92ea30e47a6"
        }
      ],
      "sid": "FFdf43a2312788acf373d890b1d4e9290b"
    },
    {
      "type": "Function",
      "name": "PayTwiML",
      "properties": {
        "offset": {
          "x": 170,
          "y": 690
        },
        "url": "https://xxxxx.twil.io/pay-twiml",
        "timeout": null,
        "parameters": [
          {
            "key": "amount",
            "value": "2000"
          }
        ]
      },
      "transitions": [
        {
          "event": "success",
          "conditions": [],
          "next": null,
          "uuid": "ce12558f-51cc-474c-bdb3-84cf1ed61461"
        },
        {
          "event": "fail",
          "conditions": [],
          "next": null,
          "uuid": "d5294424-3d0c-4fb5-a878-2051140d7546"
        }
      ],
      "sid": "FFb9696e9d5c63dd3770f91460f7fef208"
    },
    {
      "type": "SayPlay",
      "name": "SayAbort",
      "properties": {
        "offset": {
          "x": -170,
          "y": 690
        },
        "say": "決済処理を中止します。",
        "play": null,
        "voice": "Polly.Takumi",
        "language": "ja-JP",
        "loop": 1
      },
      "transitions": [
        {
          "event": "audioComplete",
          "conditions": [],
          "next": null,
          "uuid": "ff3328e3-0c9e-4d45-b266-78f4d9636280"
        }
      ],
      "sid": "FFdddfb45308ae195b6acdad54e3fe24da"
    }
  ]
}
```

- **Next** ボタンを押します。

しばらくすると、以下のようなフローが読み込まれます。

![スクリーンショット 2019-01-24 10.59.56.png](https://qiita-image-store.s3.amazonaws.com/0/86046/26f85267-ba0a-a0e3-749b-81ded98461a3.png)

- **PayTwiML** ウィジェットを選択します。
- **FUNCTION URL** のプルダウンリストから「PayTwiML」を選択します。
- **Save** ボタンを押します。
- 最後に忘れずに **Publish** ボタンを押して、フローをパブリッシュします。

### テスト

あとは、購入した電話番号に今作成した **Pay** フローを割り当てて、電話をかけてみます。
確認ガイダンスに「１」で応答すると、決済が動くことを確認します。

**Functions**の **PayStatus** を開いたままにしておくと、カード番号を入れたりするたびにコールバックが発生していることも確認できます。
コールバックの値を判定することで、現在どこまで入力がされたのかを知ることもできます。

# まとめ

今回は、 **Twilio Studio** から日本円での決済を行うサンプルコードをご紹介しました。各ガイダンスの日本語化設定なども参考になるはずです。
ぜひ活用してください。

---
### Twilio（トゥイリオ）とは

https://twilio.kddi-web.com
Twilioは音声通話、メッセージング（SMS/チャット）、ビデオなどの 様々なコミュニケーション手段をアプリケーションやビジネスへ容易に組み込むことのできるクラウドAPIサービスです。初期費用不要な従量課金制で、各種開発言語に対応しているため、多くのハッカソンイベントやスタートアップなどにも、ご利用いただいております。