3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PAY.JP】紙の領収書が必要な予約金制度にモヤっとした

Last updated at Posted at 2025-12-08

はじめに

「次回から3,000円の予約金が発生します」

通っている美容院で、突然言われました。

最近無断キャンセルが増えているようで、お店側もやむなく試験的に導入とのこと。

「わかりました。大変ですね」

自分には関係ないと思っていたら...


予約金の返金には紙の領収書が必要です

紛失したら返金できません


ルールを守っている客が不便を強いられる仕組みにモヤモヤしていたので、自分で作ってみることにしました。

目次


予約金の仕組み Before / After

reservation_mechanism_slide (1).png

デモ:予約から会計までの流れ

  1. 予約画面と支払いフォームから決済

スクリーンショット 2025-12-08 22.40.08.png

スクリーンショット 2025-12-08 22.40.39.png

支払いが正常に完了すると、このようなレスポンス

スクリーンショット 2025-12-08 22.41.59.png

ダッシュボードの売上一覧に反映されていることを確認できます

スクリーンショット 2025-12-08 22.43.59.png

  1. 管理画面で当日会計or無断キャンセルの操作

スクリーンショット 2025-12-08 22.44.52.png

会計ボタンを押下すると、金額を入力して決済できます

スクリーンショット 2025-12-08 22.45.14.png

ダッシュボードの売上一覧に反映されていることを確認できます

スクリーンショット 2025-12-08 22.45.56.png

また、ダッシュボードのイベント一覧画面で支払いまでの一連のイベント情報を確認できます

スクリーンショット 2025-12-08 22.48.28.png

機能

  1. 予約機能: クレジットカードで3,000円の予約金決済
  2. 管理画面: 予約一覧、来店確認、無断キャンセル処理
  3. 会計機能: 予約金返金 + 施術料金決済を自動処理

技術スタック

項目 技術
フロントエンド React
バックエンド Go + Gin
決済API PAY.JP
環境 Docker Compose

PAY.JPが提供しているライブラリ一覧はこちら

アーキテクチャ図

┌─────────────┐
│   React     │ トークン生成
│  (Frontend) │────────────┐
└─────────────┘            │
                           ▼
                    ┌─────────────┐
                    │  Go + Gin   │ 決済処理
                    │  (Backend)  │────────┐
                    └─────────────┘        │
                                           ▼
                                    ┌─────────────┐
                                    │  PAY.JP API │
                                    └─────────────┘

実装解説

事前準備:PAY.JP のアカウント作成

PAY.JP でアカウント作成(無料)

ダッシュボードのAPI設定からAPIキーを取得

スクリーンショット 2025-12-08 23.55.48.png

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にコーディングをある程度丸投げしてみたりもしたのですが、まあ動かないです。

スクリーンショット 2025-12-09 0.30.32.png

今の時代は非エンジニアでもプロトタイプ作成できてしまいますので、この記事をみて店員さん作ってくれないかな〜と思いましたが、そもそも当日無断キャンセルする人が減ってほしいものですね。

そもそも予約したことすら忘れているのでしょうか?
それなら予約リマインドを・・・いやリマインド通知すら見ていなかったり、忘れているのでしょうか?:persevere:

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?