Google Formsで請求する仕組みを考える


毎年Adventカレンダーぐらいでしか、GASのネタ書かなくなってきた大橋です。

最近 とある人と話してて、「Google Formで料金請求(クレジットカード決済)したいのだけどなんかいい方法ある？」って話が出たので、ちょっと簡単に実装してみたいと思います。

なお、クレジットカード決済は色々めんどくさいので、今回はStripeを利用してやってみます。

## 前提・制約

大前提として、
Stripeの支払い画面をGoogle Formsの回答完了画面に表示する事は __不可能__です。


よくGASとGoogle Formsが連携できると聞いて、
Google Formの質問UIをカスタマイズしたり、回答の内容によって回答終了後のメッセージを変更できると勘違いされている方を結構見ますが、
それらは __出来ません__ 。

GASとGoogle Formsでできる連携は以下です。

* GASからGoogle Formsを作成・削除(削除はDriveAPI経由)
* GASからGoogle Formsの質問・設定を変更する
  * この設定変更により、GASから回答終了後のメッセージ自体の変更はできます。
  * ただ回答終了後、最終画面が表示されるまでの間に処理を挟めるわけではないのです。
  * このため、回答の内容によって回答終了後のメッセージを変更することは出来ません。
* GASから回答を作成する

またGASで扱えるGoogle Formsのイベントは以下です。

* Google Formsのエディターが表示される(onOpen)
  * 回答画面の表示では __ありません__ 。
* Google Formsに回答される(onSubmit)
  * 上にも書いていますが、  
  このonSubmitは 「回答終了後、最終画面が表示されるまでの間に処理」 __されるわけではなく__ 、  
  回答完了後非同期的に呼び出されます。
* その他Add-onsのイベント、Time-drivenイベント

上記によりStripeの支払い画面をGoogle Formsの回答完了画面に表示する事は不可能です。
なお今回はStripeを利用しますが、Stripeにアカウントを取得したり、APIの設定したりの話はここには書きません。


## フロー

上記の制約を元に以下のようなフローで決済処理を行います。

1. 回答者がGoogle Formsに回答をする
2. 回答者がクレジットカード決済を希望していたら、Google FormsのonSubmitで回答者にクレジットカード決済用の画面URL(GASのdoGet)をメールで送る
3. 回答者がURLをクリックして、画面を表示する。
4. JSでStripeのクレジットカード決済フォームを表示、Tokenを取得
5. GAS経由でStripeのCharge APIを呼び出して、決済、Google FormsのResponseデータに支払い済みを設定、支払い済み画面を表示

以上でやりたいと思います。


## 実際に作成してみる

フォームは以下です。

![Kobito.RHkKCX.png](https://qiita-image-store.s3.amazonaws.com/0/2965/89e8bab1-6aab-3613-783c-b970f93e1aea.png "Kobito.RHkKCX.png")


回答するとURLが載っているメールがとび、URLアクセスすると以下の画面が表示されます。

![Kobito.w2QWjY.png](https://qiita-image-store.s3.amazonaws.com/0/2965/2489d763-18a1-9908-6213-0558910b2f4b.png "Kobito.w2QWjY.png")

ボタンをクリックすると以下のようにクレジットカード番号入力欄が表示されます

![Kobito.rsfzkd.png](https://qiita-image-store.s3.amazonaws.com/0/2965/cf0976c6-ca67-9af0-a93f-9882287ff2fd.png "Kobito.rsfzkd.png")

そして、カード番号を入力し、1000円払うボタンをクリックすると、GASで処理し、決済処理が走ります。


### コード

コードは以下の感じです。

```js:コード.gs
var PAYMENT_METHOD_TYPE_CARD = "クレジットカード決済";
var PAYMENT_METHOD_TYPE_BANK = "銀行振込";
var FORM_ID = "formのID";
var ITEM_NAME_PAYMENT_METHOD = "支払い方法";
var ITEM_NAME_IS_PAID = "支払い済み";

function onFormSubmit(e) {
  //var e.response = FormApp.getActiveForm().createResponse();
  var form = e.source;
  //var form = FormApp.openById(id);
  
  var email = e.response.getRespondentEmail();
  
  var paymentMethodItem = getItemsByTitle(form, ITEM_NAME_PAYMENT_METHOD)[0];
  var paymentMethodResponse = e.response.getResponseForItem(paymentMethodItem);
  if (PAYMENT_METHOD_TYPE_CARD === paymentMethodResponse.getResponse()) {
    var doGetUrl = ScriptApp.getService().getUrl();
    if (doGetUrl.indexOf("?") >= 0) {
      doGetUrl = doGetUrl + "&responseId=" + e.response.getId();
    } else {
      doGetUrl = doGetUrl + "?responseId=" + e.response.getId();
    }
    MailApp.sendEmail(email, "金払え", doGetUrl);    
  } else {
    MailApp.sendEmail(email, "金払え", "ここに振り込んでください。 ~~~~");
  }
}

function getItemsByTitle(form, title) {
  
  return form.getItems().filter(function(item) {
    //var item = form.getItemById();
    Logger.log(item.getTitle());
    return item.getTitle() === title;
  });
}


function doGet(e) {
  var responseId = e.parameter.responseId;
  
  if (!responseId || responseId === "") {
    return HtmlService.createHtmlOutput("エラーだよ");
  }
  
  var form = FormApp.openById(FORM_ID);
  var response = form.getResponse(responseId);
  var paymentMethodItem = getItemsByTitle(form, ITEM_NAME_PAYMENT_METHOD)[0];
  var paymentMethodResponse = response.getResponseForItem(paymentMethodItem);
  if (PAYMENT_METHOD_TYPE_CARD !== paymentMethodResponse.getResponse()) {
    return HtmlService.createHtmlOutput("銀行振込してください。");
  }
  
  var stripePublishableKey = PropertiesService.getScriptProperties().getProperty("stripePublishableKey");
  
  var template = HtmlService.createTemplateFromFile("stripe.html");
  template.url = ScriptApp.getService().getUrl();
  template.stripePublishableKey = stripePublishableKey;
  template.responseId = responseId;
  return template.evaluate();
}

function doPost(e) {
  var stripeToken = e.parameter.stripeToken;
  var responseId = e.parameter.responseId;
  var stripeEmail = e.parameter.stripeEmail;
  var form = FormApp.openById(FORM_ID);
  var response = form.getResponse(responseId);
  var result = UrlFetchApp.fetch("https://api.stripe.com/v1/charges", {
    'method' : 'post',
    'payload' : {
      'amount': "1000",
      'currency': 'jpy',
      'description': 'Charge',
      'source': stripeToken
    },
    'headers' : {'Authorization' : " Basic " + Utilities.base64Encode(PropertiesService.getScriptProperties().getProperty("stripePrivateKey") + ':')}
  });
  
  return HtmlService.createHtmlOutput("決済が完了しました。");
}
```

HTMLは以下です。

```html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script type="text/javascript" src="https://js.stripe.com/v3/"></script>
  </head>
  <body>
  <form action="<?= url ?>" method="POST">
  <script
    src="https://checkout.stripe.com/checkout.js" class="stripe-button"
    data-key="<?= stripePublishableKey?>"
    data-amount="1000"
    data-currency='jpy'
    data-name="決済するよ"
    data-description="決済"
    data-image="https://stripe.com/img/documentation/checkout/marketplace.png"
    data-locale="auto">
  </script>
  <input type="hidden" name="responseId" value="<?= responseId?>">
  </form>
  </body>
</html>


```


# 注意

* 二重決済対応などはしてないのでいい感じでやってください。
* 法律とかそういうの知りません。
* これで何か不都合起きても知りません。 自己責任でお願いします。