Google フォームと GAS(Google Apps Script)で、クレジットカードによる決済機能付きの注文フォームを無料&サーバーレスで実現します。
データベース代わりとしてスプレッドシート、確認メール等の送信に Gmail、オンライン決済には PAY.JP を使用します。
(システムの構築・運用は無料ですが、決済手数料はかかります)
注文を受けた際の流れは以下のようになります。丸数字付きの太字が注文者の動き、その下がシステムの動きの簡単な説明になります。
-----
① Google フォームから注文→ Google フォームの回答をスプレッドシートに記録。決済ページのリンクを記載した確認メールを生成・送信する。
② 決済ページにアクセス
→ メールアドレス、注文合計金額が表示された決済ページを生成する。決済ページには有効期限を設定。
③ 決済
→ PAY.JP から Webhook を受け取り、スプレッドシートに決済完了を記録。決済完了のメールを生成・送信する。
④ 注文完了
-----
***
それでは、以下 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 となります。
これで完成となります。
***
思ったよりも手順が多くなりました。実際に運用する際には、もう少しエラーハンドリング等しっかりやる必要がありそうですが、とりあえず、一通り動くものができてよかったです。