はじめに
「次回から3,000円の予約金が発生します」
通っている美容院で、突然言われました。
最近無断キャンセルが増えているようで、お店側もやむなく試験的に導入とのこと。
「わかりました。大変ですね」
自分には関係ないと思っていたら...
予約金の返金には紙の領収書が必要です
紛失したら返金できません
ルールを守っている客が不便を強いられる仕組みにモヤモヤしていたので、自分で作ってみることにしました。
目次
予約金の仕組み Before / After
デモ:予約から会計までの流れ
- 予約画面と支払いフォームから決済
支払いが正常に完了すると、このようなレスポンス
ダッシュボードの売上一覧に反映されていることを確認できます
- 管理画面で当日会計or無断キャンセルの操作
会計ボタンを押下すると、金額を入力して決済できます
ダッシュボードの売上一覧に反映されていることを確認できます
また、ダッシュボードのイベント一覧画面で支払いまでの一連のイベント情報を確認できます
機能
- 予約機能: クレジットカードで3,000円の予約金決済
- 管理画面: 予約一覧、来店確認、無断キャンセル処理
- 会計機能: 予約金返金 + 施術料金決済を自動処理
技術スタック
| 項目 | 技術 |
|---|---|
| フロントエンド | React |
| バックエンド | Go + Gin |
| 決済API | PAY.JP |
| 環境 | Docker Compose |
PAY.JPが提供しているライブラリ一覧はこちら
アーキテクチャ図
┌─────────────┐
│ React │ トークン生成
│ (Frontend) │────────────┐
└─────────────┘ │
▼
┌─────────────┐
│ Go + Gin │ 決済処理
│ (Backend) │────────┐
└─────────────┘ │
▼
┌─────────────┐
│ PAY.JP API │
└─────────────┘
実装解説
事前準備:PAY.JP のアカウント作成
PAY.JP でアカウント作成(無料)
ダッシュボードのAPI設定からAPIキーを取得
2種類のキーがあります:
| キー | 用途 | 使用場所 | 公開 |
|---|---|---|---|
| 公開可能キー (PK) | トークン生成 | フロントエンド | 公開OK |
| 秘密鍵 (SK) | 決済・返金処理 | バックエンド | 絶対に公開しない |
テストカード番号:
カード情報を入力するときはこちらを使います
カード番号: 4242 4242 4242 4242
有効期限: 12/25
CVC: 123
Phase 1: 予約と決済の基本実装
ゴール
- PAY.JP Checkoutでカード情報入力
- トークン生成
- バックエンドで決済
PAY.JP Checkoutの導入
<script src="https://checkout.pay.jp/" class="payjp-button"></script>
Checkoutを利用してフロントエンドでトークン生成
// scriptタグを動的生成
const script = document.createElement('script');
script.src = 'https://checkout.pay.jp/';
script.setAttribute('data-key', process.env.REACT_APP_PAYJP_PUBLIC_KEY);
script.setAttribute('data-amount', '3000');
script.setAttribute('data-on-created', 'payjpTokenCallback');
// コールバックでトークン受け取り
window.payjpTokenCallback = async (token) => {
console.log(token.id); // tok_xxxxx
};
PK読み込みのenvファイルを別途用意
REACT_APP_PAYJP_PUBLIC_KEY=pk_test_xxxxx
ポイント:
- React内でscriptタグを動的生成
-
data-on-createdでコールバック設定 - トークンを受け取る
- カード情報はバックエンドに送らない(PCI DSS 準拠)
SKに関してはバックエンドで以下のように読み込む
package main
import (
"os"
"net/http"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
payjp "github.com/payjp/payjp-go/v1"
)
func main() {
secretKey := os.Getenv("PAYJP_SECRET_KEY")
if secretKey == "" {
panic("PAYJP_SECRET_KEY is not set")
}
// PAY.JP 初期化
pay := payjp.New(secretKey, nil)
r := gin.Default()
// ...
}
SK読み込みのenvファイルを別途用意
PAYJP_SECRET_KEY=sk_test_xxxxx
バックエンドで決済
charge, err := pay.Charge.Create(3000, payjp.Charge{
CardToken: req.Token, // フロントから受け取ったトークン
})
ポイント:
- トークンを受け取る
-
pay.Charge.Create()で決済 - 予約データを保存
Phase 2: 管理機能の実装
ゴール
- 予約一覧表示
- 来店確認 → 返金
- 無断キャンセル → 徴収
管理画面UI
予約一覧取得
const [reservations, setReservations] = useState([]);
useEffect(() => {
const fetchReservations = async () => {
const response = await axios.get('http://localhost:8080/api/reservations');
setReservations(response.data);
};
fetchReservations();
}, []);
来店確認ボタン(返金)
const handleRefund = async (id) => {
await axios.post(`http://localhost:8080/api/reservations/${id}/refund`);
alert('返金完了');
// 一覧を再取得
};
無断キャンセルボタン
const handleNoShow = async (id) => {
await axios.post(`http://localhost:8080/api/reservations/${id}/no-show`);
alert('無断キャンセル処理完了');
};
返金API
_, err := pay.Charge.Refund(chargeID, "来店確認のため返金")
Phase 3: Customer機能の実装
ゴール
予約金と施術料金を紐付ける
Customer作成
customer, err := pay.Customer.Create(payjp.Customer{
CardToken: payjp.String(req.Token),
})
-
payjp.String()でラップが必要 - CardTokenでカード情報を登録
会計処理(返金 + 決済)
処理フロー:
1. 予約金返金(3,000円)
↓
2. 施術料金決済(例:10,000円)
↓
支払い金額:7,000円
Customer使って決済
// デポジット決済
depositCharge, err := pay.Charge.Create(3000, payjp.Charge{
CustomerID: customer.ID,
Description: "予約金",
})
会計処理(返金 + 再決済)
// Step 1: デポジット返金
_, err := pay.Charge.Refund(depositChargeID, "予約金返金")
// Step 2: 施術料金決済(同じCustomer)
serviceCharge, err := pay.Charge.Create(10000, payjp.Charge{
CustomerID: customerID, // 保存したCustomer ID
Description: "施術料金",
})
参考資料
おわりに
はじめて決済機能を実装してみたのですが、思っていたより簡単にできました。
セキュリティ関連、定期課金、Webhookなども試したいと感じるくらい試しやすかったです。
理由をまとめると👇
日本語ドキュメントが充実
- エラーメッセージも日本語
- 英語ドキュメントを読む手間がない
Checkout機能が便利
- カード入力フォームを自前で作る必要なし
- セキュリティ対策も不要
- 実装が簡単
テスト環境が使いやすい
- テストカードですぐ動作確認できる
- ダッシュボードで決済・返金が可視化
ドキュメントも読みやすかったです。
生成AIにコーディングをある程度丸投げしてみたりもしたのですが、まあ動かないです。
今の時代は非エンジニアでもプロトタイプ作成できてしまいますので、この記事をみて店員さん作ってくれないかな〜と思いましたが、そもそも当日無断キャンセルする人が減ってほしいものですね。
そもそも予約したことすら忘れているのでしょうか?
それなら予約リマインドを・・・いやリマインド通知すら見ていなかったり、忘れているのでしょうか?![]()










