Twilioで電話ECを実現するRailsコード

# 概要
Smart Communication Award 2014ハッカソンに参加してきて、そこで使った技術の共有をします。

# できること
- RailsをつかったECショップの在庫管理画面
- 購入者側は*完全電話のみ*で購入できる

# デモ
http://buycall.54.178.117.149.xip.io/products

# コード
https://github.com/kiiita/BuyCall

## フロー
![業務フロー.png](https://qiita-image-store.s3.amazonaws.com/0/10272/02f8b870-7a76-b4ea-6e0f-286a502b4f2e.png)

## ER図
![ER図.png](https://qiita-image-store.s3.amazonaws.com/0/10272/7d68f7b4-e95e-6e3a-a9e3-4fb5febef2c6.png)


## ルーティング

基本的には、`1応答1アクション`の原則で作っています。
もうちょっと手を加えれば、`設定ファイルを変えるだけで応答メッセージを変えるDSL`なんてことも出来そうです。

```rb:config/routes.rb

Buycall::Application.routes.draw do
  root :to => 'home#index'

  resources :products
  resources :orders

  # 最初にTwilioのリクエストを受けるURL
  get 'twiml/start'

  get 'twiml/ask_product'
  get 'twiml/receive_product'

  get 'twiml/ask_count/:product_id' => 'twiml#ask_count'
  get 'twiml/receive_product_and_count/:product_id' => 'twiml#receive_product_and_count'

  get 'twiml/confirm_product/:product_id/:count' => 'twiml#confirm_product'

  get 'twiml/ask_name'
  get 'twiml/receive_name'

  get 'twiml/ask_zip'
  get 'twiml/receive_zip'
  get 'twiml/confirm_zip'

  get 'twiml/ask_address'
  get 'twiml/confirm_zip'

  get 'twiml/ask_address2'
  get 'twiml/receive_address2'

  get 'twiml/ask_order'
  get 'twiml/confirm_order'

  get 'twiml/finish'
end
```

## コントローラ

すべて`TwimlController`でTwiMLを生成しています。

基本的には、通話者の電話番号をキーとしてデータを保持しているので、`session`のようなものは無いのですが、一部`confirm_product`のように商品IDや個数を引き継ぐ必要があったので、Tlimlの転送時にURLパラメータを追加しています。

```rb:app/controllers/twiml_controller.rb
require 'uri'

class TwimlController < ApplicationController

  protect_from_forgery with: :null_session

  before_action :set_caller_user

  # Twilioの最初
  def start
    xml_str = Twilio::TwiML::Response.new do |response|
      # response.Play voice_url('こちらは、バイコールです。 案内を開始します。')
      response.Say 'こちらは、バイコールです。 案内を開始します。', language: "ja-jp"
      response.Redirect "#{Setting.app_host}/twiml/ask_product", method: 'GET'
    end.text

    render xml: xml_str
  end

  def ask_product
    xml_str = Twilio::TwiML::Response.new do |response|
      response.Gather timeout: 30, finishOnKey: '#', action: "#{Setting.app_host}/twiml/receive_product", method: 'GET' do |gather|
        # gather.Play voice_url("商品番号を入力してください。 最後に、シャープを押してください。")
        gather.Say "商品番号を入力してください。 最後に、シャープを押してください。", language: "ja-jp"
      end
    end.text

    render xml: xml_str
  end

  def receive_product
    xml_str = Twilio::TwiML::Response.new do |response|
      # 該当する商品がない場合
      if params_pruduct.nil?
        # response.Play voice_url("入力した番号の商品がみつかりません。")
        response.Say "入力した番号の商品がみつかりません。", language: "ja-jp"
        response.Redirect "#{Setting.app_host}/twiml/ask_product", method: 'GET'
      else
        response.Redirect "#{Setting.app_host}/twiml/ask_count/#{params_pruduct.id}", method: 'GET'
        # response.Gather timeout: 30, finishOnKey: '', numDigits: 1, action: "#{Setting.app_host}/twiml/ask_count/#{params_pruduct.id}", method: 'GET' do |gather|
        #   gather.Play voice_url(" 入力した商品 は #{params_pruduct.name_read} ですね。")
        #   gather.Play voice_url(" よろしければ、1を。 修正したいかたは、9を押してください。")
        # end
      end
    end.text

    render xml: xml_str
  end

  # 商品の個数を選択
  def ask_count
    xml_str = Twilio::TwiML::Response.new do |response|
      response.Gather timeout: 30, finishOnKey: '#', action: "#{Setting.app_host}/twiml/receive_product_and_count/#{params_pruduct.id}", method: 'GET' do |gather|
        # gather.Play voice_url("商品の個数を入力してください。最後に、シャープを押してください。")
        gather.Say "商品の個数を入力してください。最後に、シャープを押してください。", language: "ja-jp"
      end
    end.text

    render xml: xml_str
  end

  def receive_product_and_count
    xml_str = Twilio::TwiML::Response.new do |response|
      response.Gather timeout: 30, finishOnKey: '', numDigits: 1, action: "#{Setting.app_host}/twiml/confirm_product/#{params_pruduct.id}/#{params_count}", method: 'GET' do |gather|
        # gather.Play voice_url("ご注文は、#{params_pruduct.name_read}。個数は #{params_count}個、ですね。")
        # gather.Play voice_url("よろしければ、1を。修正したいかたは、9を押してください。")
        gather.Say "ご注文は、#{params_pruduct.name_read}。個数は #{params_count}個、ですね。", language: "ja-jp"
        gather.Say "よろしければ、1を。修正したいかたは、9を押してください。", language: "ja-jp"
      end
    end.text

    render xml: xml_str
  end

  def confirm_product
    xml_str = Twilio::TwiML::Response.new do |response|
      if params[:Digits].to_s == '1'

        # 注文処理
        @current_caller_user.orders.create(product: params_pruduct, count: params_count, confirm_finished: false)

        # 再訪ユーザー
        if @current_caller_user.zip
          response.Redirect "#{Setting.app_host}/twiml/ask_order", method: 'GET'
        else
          response.Redirect "#{Setting.app_host}/twiml/ask_name", method: 'GET'
        end
      else
        response.Redirect "#{Setting.app_host}/twiml/ask_product", method: 'GET'
      end
    end.text

    render xml: xml_str
  end

  def ask_name
    xml_str = Twilio::TwiML::Response.new do |response|
      # response.Play voice_url("お名前をお話ください。最後に、シャープを押してください。")
      response.Say "お名前をお話ください。最後に、シャープを押してください。", language: "ja-jp"
      response.Record maxLength: '30', finishOnKey: '#', action: "#{Setting.app_host}/twiml/receive_name", :method => 'get'
    end.text

    render xml: xml_str
  end

  def receive_name
    @current_caller_user.update name_voice_url: params[:RecordingUrl]

    xml_str = Twilio::TwiML::Response.new do |response|
      response.Redirect "#{Setting.app_host}/twiml/ask_zip", method: 'GET'
    end.text

    render xml: xml_str
  end

  def ask_zip
    xml_str = Twilio::TwiML::Response.new do |response|
      response.Gather timeout: 30, finishOnKey: '', numDigits: 7, action: "#{Setting.app_host}/twiml/receive_zip", method: 'GET' do |gather|
        # response.Play voice_url("郵便番号をハイフン無しで入力してください。")
        response.Say "郵便番号をハイフン無しで入力してください。", language: "ja-jp"
      end

    end.text

    render xml: xml_str
  end

  def receive_zip
    xml_str = Twilio::TwiML::Response.new do |response|

      # 郵便番号が不正だったら
      if params[:Digits].blank? || params[:Digits].length != 7
        # response.Play voice_url("郵便番号に誤りがあります。")
        response.Say "郵便番号に誤りがあります。", language: "ja-jp"
        response.Redirect "#{Setting.app_host}/twiml/ask_zip", method: 'GET'

      elsif ZipCode.find_address_by_code(params[:Digits]).nil?
        # response.Play voice_url("入力した郵便番号の住所がみつかりません。")
        response.Say "入力した郵便番号の住所がみつかりません。", language: "ja-jp"
        response.Redirect "#{Setting.app_host}/twiml/ask_zip", method: 'GET'

      else
        @current_caller_user.update(zip: params[:Digits], address1: ZipCode.find_address_by_code(params[:Digits]))

        # response.Play voice_url(" #{ZipCode.find_address_by_code(params[:Digits])} ですね。")
        response.Say " #{ZipCode.find_address_by_code(params[:Digits])} ですね。", language: "ja-jp"
        response.Gather timeout: 30, finishOnKey: '', numDigits: 1, action: "#{Setting.app_host}/twiml/confirm_zip", method: 'GET' do |gather|
          # gather.Play voice_url("よろしければ、1を。修正したいかたは、9を押してください。")
          gather.Say "よろしければ、1を。修正したいかたは、9を押してください。", language: "ja-jp"
        end
      end
    end.text

    render xml: xml_str
  end

  def confirm_zip
    xml_str = Twilio::TwiML::Response.new do |response|
      if params[:Digits].to_s == '1'
        response.Redirect "#{Setting.app_host}/twiml/ask_address2", method: 'GET'
      else
        response.Redirect "#{Setting.app_host}/twiml/ask_zip", method: 'GET'
      end
    end.text

    render xml: xml_str
  end

  # 追加の住所
  def ask_address2
    xml_str = Twilio::TwiML::Response.new do |response|
      # response.Play voice_url("番地以降の住所をお話ください。最後に、シャープを押してください。")
      response.Say "番地以降の住所をお話ください。最後に、シャープを押してください。", language: "ja-jp"
      response.Record maxLength: '60', finishOnKey: '#', action: "#{Setting.app_host}/twiml/receive_address2", :method => 'get'
    end.text

    render xml: xml_str
  end

  def receive_address2
    @current_caller_user.update address2_voice_url: params[:RecordingUrl]

    xml_str = Twilio::TwiML::Response.new do |response|
      response.Redirect "#{Setting.app_host}/twiml/ask_order", method: 'GET'
    end.text

    render xml: xml_str
  end

  # 注文確認
  def ask_order
    sum = @current_caller_user.orders.last.count * @current_caller_user.orders.last.product.price
    xml_str = Twilio::TwiML::Response.new do |response|
      response.Gather timeout: 30, finishOnKey: '', numDigits: 1, action: "#{Setting.app_host}/twiml/confirm_order", method: 'GET' do |gather|
        # gather.Play voice_url("ご注文を確認いたします。")
        # gather.Play voice_url("ご注文は、#{@current_caller_user.orders.last.product.name_read}。個数は #{@current_caller_user.orders.last.count} 個　。　お会計は、#{sum}円、になります。 ")
        # gather.Play voice_url("よろしければ、1を。 修正したいかたは、9を押してください。")
        gather.Say "ご注文を確認いたします。", language: "ja-jp"
        gather.Say "ご注文は、#{@current_caller_user.orders.last.product.name_read}。個数は #{@current_caller_user.orders.last.count} 個　。　お会計は、#{sum}円、になります。 ", language: "ja-jp"
        gather.Say "よろしければ、1を。 修正したいかたは、9を押してください。", language: "ja-jp"
      end
    end.text

    render xml: xml_str
  end

  def confirm_order
    xml_str = Twilio::TwiML::Response.new do |response|
      if params[:Digits].to_s == '1'
        response.Redirect "#{Setting.app_host}/twiml/finish", method: 'GET'
      else
        response.Redirect "#{Setting.app_host}/twiml/ask_product", method: 'GET'
      end
    end.text

    render xml: xml_str
  end

  def finish
    @current_caller_user.orders.last.update(confirm_finished: true)

    xml_str = Twilio::TwiML::Response.new do |response|
      # response.Play voice_url("ご注文ありがとうございました。 後ほどショップから確認の電話をすることがあります。")
      response.Say "ご注文ありがとうございました。 後ほどショップから確認の電話をすることがあります。", language: "ja-jp"
    end.text

    render xml: xml_str
  end

  private

  # 通話中のユーザーの設定
  def set_caller_user
    raise 'params[:Caller] is blank' if params[:Caller].blank?
    @current_caller_user ||= User.find_by(tel: params[:Caller]) || User.create(tel: params[:Caller])
  end

  # 現在の通話者の注文商品
  def params_pruduct
    @_product ||= Product.find_by(id: (params[:product_id] || params[:Digits]))
  end

  # 現在の通話者の商品個数
  def params_count
    params[:count] || params[:Digits]
  end

  # AI Talkを使いたかったが、レスポンスヘッダが audio/mp3出なかったためTwilioのPlayで再生できなかった。
  # なのでアプリケーションでヘッダーを書き換えるだけのProxyを作っている
  def voice_url(text)
    aitalk_path(url: "http://tts.exaitalk.net/webtts/tts/ttsget.php?username=#{Setting.aitalk.username}&password=#{Setting.aitalk.password}&speaker_id=#{Setting.aitalk.speaker_id}&text=#{URI.escape(text)}")
  end

end
```

こんな感じで、結構簡単にIVRシステムは作れそうです。

# さいごに

Twilioすげー

もっと詳しいことや、フロントに関することは [kiiita](http://qiita.com/kiiita)あたりがpostしてくれるでしょう。