Google フォームと GAS(Google Apps Script)で、クレジットカードによる決済機能付きの注文フォームを無料&サーバーレスで実現します。
データベース代わりとしてスプレッドシート、確認メール等の送信に Gmail、オンライン決済には PAY.JP を使用します。
(システムの構築・運用は無料ですが、決済手数料はかかります)
注文を受けた際の流れは以下のようになります。丸数字付きの太字が注文者の動き、その下がシステムの動きの簡単な説明になります。
-----
① Google フォームから注文→ Google フォームの回答をスプレッドシートに記録。決済ページのリンクを記載した確認メールを生成・送信する。
② 決済ページにアクセス
→ メールアドレス、注文合計金額が表示された決済ページを生成する。決済ページには有効期限を設定。
③ 決済
→ PAY.JP から Webhook を受け取り、スプレッドシートに決済完了を記録。決済完了のメールを生成・送信する。
④ 注文完了
-----
今回、在庫管理の部分については省いて作成しています。在庫管理については、「【GAS】Google フォームとスプレッドシートを連携して選択肢を動的に入力する。そして注文フォームを作ってみる。」を参考にしてみてください。
また、GAS によるシンプルな PAY.JP の導入については「GAS でオンライン決済 PAY.JP を導入する」をご参照ください。
***
それでは、以下 Google フォームを使用した決済機能付き注文フォームの作り方になります。PAY.JP のアカウントは作成済みの想定です(テストモードを使用)。
(決済に関するものですので、一応こうやると動くよという参考程度のものとしてください。ご利用の際は自己責任でお願いします)
1)注文フォームの作成
まずは、Google フォームで簡単な注文フォームを作成します。とりあえず、今回は以下のようにしました。
1) 商品 [プルダウン] ※1~複数個
2) お名前 [記述式](必須)
3) メールアドレス [記述式](必須)
4) お電話番号 [記述式]
5) 住所(続き) [記述式](必須)
6) お支払い方法 [ラジオボタン](必須)
2)回答を記録するスプレッドシートの加工
フォームの回答先となる新しいスプレッドシートを作成します。
「フォームの回答 1」シートの末列に「決済金額」「固有番号」「有効期限」「顧客番号」「決済状況」の5つの列を追加します。
「商品情報」という名前のシートを追加、A列に「商品名」、B列に「価格」を入力しておきます。その際、1行目は項目名として使用し、2行目から追記します。フォームの商品数、順番とそろえてください。
3)注文フォームから注文受付処理用の GAS を作成
フォームから GAS を作成します。フォームから作成することで、トリガー作成時に、イベントのソースとしてフォームを選択することができるようになります。
「注文受付.gs」
const sp = SpreadsheetApp.openById("スプレッドシート ID"); const sh = sp.getSheetByName("フォームの回答 1"); const lastRow = sh.getLastRow(); const lastCol = sh.getLastColumn(); const item_sh = sp.getSheetByName("商品情報"); function processOrder(e) { Utilities.sleep(5000); FormApp.getActiveForm(); // フォームへのパーミッションを与えるためのおまじない const res = e.response.getItemResponses(); // 商品情報の取得 const itemCount = item_sh.getLastRow() - 1; const item = item_sh.getRange(2, 1, itemCount, 2).getValues(); // 金額合計を計算 let amount = 0; for(let i = 0; i < itemCount; i++) { if(res[i].getResponse() != "") { amount += res[i].getResponse() * item[i][1]; } } sh.getRange(lastRow, lastCol - 4).setValue(amount); // 合計金額を書き込み const id = generateId(); // 固有番号を生成 sh.getRange(lastRow, lastCol - 3).setValue(id); // 固有番号を書き込み // 固有番号の有効期限を計算 const timeStamp = new Date(sh.getRange(lastRow, 1).getValue()); let expirationTimestamp = new Date(timeStamp.setHours(timeStamp.getHours() + 1)); expirationTimestamp = Utilities.formatDate(expirationTimestamp, "JST", "yyyy/MM/dd HH:mm:ss"); sh.getRange(lastRow, lastCol -2).setValue(expirationTimestamp); // 有効期限を書き込み sendConfirmationEmail(res, itemCount, item, amount, id); // 確認メールの送信 }
1行目の「スプレッドシート ID」を書き換えてください。
processOrder 関数を、フォーム送信時に起動するようにトリガー設定をしておきます。
「固有番号の生成.gs」
function generateId() { const previousIdSet = sh.getRange(2, lastCol - 3, lastRow - 1, 1).getValues(); // 生成済みの固有番号を取得 let id; let isDuplicate; do { id = generateRandomString(); // ランダムな文字列を生成 isDuplicate = checkForDuplicate(id, previousIdSet); // 重複をチェック } while (isDuplicate); return id; } function checkForDuplicate(value, previousValue) { for (var i = 0; i < previousValue.length; i++) { if (previousValue[i][0] === value) { return true; // 重複が見つかった場合 } } return false; // 重複がない場合 } function generateRandomString() { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let randomString = ""; for (var i = 0; i < 30; i++) { let randomIndex = Math.floor(Math.random() * characters.length); randomString += characters.charAt(randomIndex); } return randomString; }
「確認メールの送信.gs」
function sendConfirmationEmail(res, itemCount, item, amount, id) { // メール本文の作成 const name = res[itemCount].getResponse(); // お名前 const email = res[itemCount + 1].getResponse(); // メールアドレス const phone = res[itemCount + 2].getResponse(); // お電話番号 const address = res[itemCount + 3].getResponse(); // 住所 const payment = res[itemCount + 4].getResponse(); // お支払方法 let order = []; for(let i = 0; i < itemCount; i++) { order.push(item[i][0] + " " + res[i].getResponse() + "個"); } let body = name + " 様\n\nご注文ありがとうございます。\n\n以下の内容でご注文を承りました。ご確認ください。\n\n-----\n" + order.join("\n") + "\n\n合計 ¥" + amount + "\n-----\n\nお届け先住所:" + address + "\n電話番号:" + phone + "\n\nお支払い方法:" + payment; if(payment == "クレジットカード") { const url = "ウェブアプリの URL"; body += "\n\nご注文内容を確認の上、以下リンク先よりお支払いを完了してください:\n" + url + "?id=" + id; } else if(payment == "銀行振込") { body += "\n\nご注文内容を確認の上、以下振込先へのお支払いを完了してください:" + "\n〇〇銀行\n△△支店\n普通 ×××××××"; } // 確認メールの送信 GmailApp.sendEmail( email, "ご注文内容の確認と決済ページのご案内", body, { from: "送信元メールアドレス", name: "送信者名" } ); }
28行目の「送信元メールアドレス」、29行目の「送信者名」を書き換えてください。
16行目の「ウェブアプリの URL」は後ほど書き換えます。
「決済ページの生成.gs」
function doGet(e) { const PUBLISH_KEY = "公開鍵"; const html = HtmlService.createTemplateFromFile('index'); // URL から固有番号を取得 const id = e.parameter["id"]; if(id == null) { return HtmlService.createHtmlOutput("<center>無効な URL です。(1)</center>"); // 固有番号がない場合はエラーページを表示 } // 対象の固有番号の行を特定 const idSet = sh.getRange(2, lastCol - 3, sh.getLastRow(), 1).getValues(); let rowNum = 0; for(let i = 0; i < idSet.length; i++) { if(id == idSet[i]) { rowNum = 2 + rowNum + i; break; } } if(rowNum == 0) { return HtmlService.createHtmlOutput("<center>無効な URL です。(2)</center>"); // 有効な固有番号がない場合はエラーページを表示 } else if(sh.getRange(rowNum, lastCol).getValues() != "") { return HtmlService.createHtmlOutput("<center>無効な URL です。(3)</center>"); // 既に決済完了している場合はエラーページを表示 } // 該当の注文を取得 const order = sh.getRange(rowNum, 1, 1, lastCol - 1).getValues(); // 有効期限のチェック const expirationTimestamp = order[0][lastCol - 1]; // 有効期限 const currentTime = new Date(); // 現在時刻 if(expirationTimestamp < currentTime) { return HtmlService.createHtmlOutput("<center>有効期限を過ぎた決済ページです。(4)</center>"); // 有効期限を過ぎた場合はエラーページを表示 } // 決済情報 const email = order[0][lastCol - 9]; // メールアドレス const amount = order[0][lastCol - 5]; // 決済金額 // 決済情報を index.html へ html.rowNum = rowNum; html.email = email; html.amount = amount; html.PUBLISH_KEY = PUBLISH_KEY; return html.evaluate(); } // PAY.JPでの決済処理 function doPost(e) { const card = e.parameter["payjp-token"]; const rowNum = e.parameter["rowNum"]; const amount = e.parameter["amount"]; const SECRET_KEY = PropertiesService.getScriptProperties().getProperty("SECRET_KEY"); let customer = UrlFetchApp.fetch("https://api.pay.jp/v1/customers", { "method" : "post", "payload" : { "card": card }, "headers": {'Authorization': "Basic " + Utilities.base64Encode(SECRET_KEY + ":")} }); customer = JSON.parse(customer); sh.getRange(rowNum, lastCol - 1).setValue(customer.id); // 顧客番号を書き込み UrlFetchApp.fetch("https://api.pay.jp/v1/charges", { "method" : "post", "payload" : { "amount": amount, "currency": "JPY", "customer": customer.id }, 'headers' : {'Authorization': "Basic " + Utilities.base64Encode(SECRET_KEY + ":")} }); return HtmlService.createHtmlOutput("<center>" + amount + "円のお支払いが完了しました。<br /><br />ブラウザを閉じてください。</center>"); }
2行目の「公開鍵」を書き換えてください。公開鍵は、PAY.JP のテスト公開鍵になります。
54行目の「SECRET_KEY」は、「プロジェクトの設定」の「スクリプト プロパティ」に保存してください。値は、PAY.JP のテスト秘密鍵になります。
「index.html」
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <base target="_top"> <title>注文フォーム 決済ページ(テスト)</title> </head> <body> <center> <h1>注文フォーム 決済ページ(テスト)</h1> <p>メールアドレスと決済金額をご確認の上、お支払いを完了させてください。</p><br /> <form action="ウェブアプリの URL" method="post"> <input type="hidden" id="rowNum" name="rowNum" value=<?= rowNum?> /> <label>メールアドレス: <?= email?><input type="hidden" id="email" name="email" value=<?= email?> /></label><br /> <label>決済金額: <?= amount?><input type="hidden" id="amount" name="amount" value=<?= amount?> /></label><br /><br /> <script src="https://checkout.pay.jp/" class="payjp-button" data-key=<?= PUBLISH_KEY?>></script> </form> </center> </body> </html>
12行目の「ウェブアプリの URL」は後ほど書き換えます。
コードが一通りできたら、デプロイをします。
GAS の「デプロイ」→「新しいデプロイ」から「種類の選択」で「ウェブアプリ」を選択。
・説明:任意
・ウェブアプリ / 次のユーザーとして実行:自分
・ウェブアプリ / アクセスできるユーザー:全員
として、「デプロイ」を実行。
「確認メールの送信.gs(16行目)」と「index.html(12行目)」の該当箇所を表示されたウェブアプリの URL に書き換えます。
「デプロイ」→「デプロイの管理」から編集で「新バージョン」を再度デプロイします。
4)Webhook 受取用の GAS の作成
PAY.JP からの Webhook 受取用に、決済ページとは別の URL を持たせたウェブアプリが必要となるため、新たな GAS を作成します。こちらの GAS はスプレッドシートから作成しても、紐づけなしで作成しても構いません。
「webhook受取.gs」
const sp = SpreadsheetApp.openById("スプレッドシート ID"); const sh = sp.getSheetByName("フォームの回答 1"); const lastRow = sh.getLastRow(); const lastCol = sh.getLastColumn(); function doPost(e) { const contents = JSON.parse(e.postData.contents); // Webhook の確認 const cusSet = sh.getRange(2, lastCol - 1, lastRow - 1, 2).getValues(); // 顧客番号一覧の取得 // 顧客番号が存在するかの確認 const cus = contents.data.customer; // 顧客番号 for(let i = 0; i < cusSet.length; i++) { if(cusSet[i][0] == cus) { if(contents.type == "charge.succeeded") { sh.getRange(i + 2, lastCol).setValue("完了"); // 決済完了を書き込み paymentConfirmationEmail(i + 2); // 決済完了メールの送信 } else { sh.getRange(i + 2, lastCol).setValue(contents.type); // 決済失敗 } } } }
1行目の「スプレッドシート ID」を書き換えてください。
※ Webhook のヘッダーに含まれている X-Payjp-Webhook-Token を、GAS の doPost 関数で取得する方法がわからなかった(できない?)ので、顧客番号で Webhook の正当性を判断する形にしています。
「決済完了メールの送信.gs」
function paymentConfirmationEmail(rowNum) { const name = sh.getRange(rowNum, lastCol - 9).getValue(); const email = sh.getRange(rowNum, lastCol - 8).getValue(); let body = name + " 様\n\n決済が完了しました。\n\n発送まで今しばらくお待ちください。\n\nどうぞ宜しくお願い致します。"; // 決済完了メールの送信 GmailApp.sendEmail( email, "決済が完了しました", body, { from: "送信元メールアドレス", name: "送信者名" } ); }
13行目の「送信元メールアドレス」、14行目の「送信者名」を書き換えてください。
こちらもデプロイをします。
GAS の「デプロイ」→「新しいデプロイ」から「種類の選択」で「ウェブアプリ」を選択。
・説明:任意
・ウェブアプリ / 次のユーザーとして実行:自分
・ウェブアプリ / アクセスできるユーザー:全員
として、「デプロイ」を実行。
ウェブアプリの URL をメモしておきます。
こちらの URL は知られないように管理してください。
5)PAY.JP で Webhook の設定
PAY.JP の「API設定」で、Webhookの追加をします。URL は、先ほどメモした Webhook 受取用の GAS のウェブアプリの URL となります。
これで完成となります。
詳解! Google Apps Script完全入門 [第3版]
by SimpleImageLink
***
思ったよりも手順が多くなりました。実際に運用する際には、もう少しエラーハンドリング等しっかりやる必要がありそうですが、とりあえず、一通り動くものができてよかったです。