# stripe checkout の server side を gin でつくる
stripe は go の API も用意してくれていて親切です。なんとなく php から疎遠になり、バージョン間の差異の煩わしさにキレて python とは絶縁状態、ruby は前世紀に最初に読んだ本が相性が悪く食わず嫌い状態、java なんて JAVA だった頃から逆縁だらけだった上に C++ は永遠の入門レベル(追いつくまもなく膨らんでいくのでたぶん死ぬまでそうなんだと思う、死ぬときに「ああ、僕は C++ の手のひらの中から一歩も出ることができなかった」って思いながら死んでいくんだと思う)な偏食な身にはとても有り難い次第です
gin を使うと go でも (python 並に、とは言えずとも遠からずという歯がゆさはのこりますが)楽にサーバーが書ける上に速いので嬉しいです
# ポイント
stripe のAccept a payment (opens new window) の通りなのですが一箇所落とし穴がありまして、フィードバックしておいたのでいまでもそうなのかはわからないのですが必要な import が抜けていて、つど補ってあげないとコンパイルも通りません、詳細はこちら (opens new window)に
# Set up Stripe
こちら (opens new window) のとおりなのですが、後で session と webhook を使うのでそれらも import しておく必要があります
import (
stripe "github.com/stripe/stripe-go/v71"
"github.com/stripe/stripe-go/v71/checkout/session"
"github.com/stripe/stripe-go/v71/webhook"
)
# リクエストハンドラの作成
func main() の中にCheckout Session の生成 (opens new window)を受け付ける API を例えば /buy
として、 Webhook のエンドポイント (opens new window) になる API を例えば /webhook
としてそれぞれ用意します
stripe API を使う実際の session の生成と webhook のハンドリングはそれぞれ例えば後で作る stripeGetSessionID() や handleWebhook(c) で行います
main.go の中では stripe の API は直接つかわないので stripe のための import の追加は不要です
r := gin.Default()
r.GET("/buy", func(c *gin.Context) {
sessionid, err := stripeGetSessionID()
if err != nil {
log.Println(err)
c.JSON(500, gin.H{
"message": err,
})
} else {
c.JSON(http.StatusOK, gin.H{
"SessionID": sessionid,
})
}
});
r.GET("/buy", func(c *gin.Context) {
err := handleWebhook(c)
if err != nil {
c.JSON(400, gin.H{
"message": err,
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "OK",
})
}
});
r.Run(":8080")
}
# stripe API の利用
checkout session の生成と webhook の endpoint は例えば以下のように、例えば stripe.go みたいなファイルにを用意して
package main
import (
"github.com/UedaTakeyuki/erapse"
"github.com/gin-gonic/gin"
"log"
"time"
stripe "github.com/stripe/stripe-go/v71"
"github.com/stripe/stripe-go/v71/checkout/session"
"github.com/stripe/stripe-go/v71/webhook"
)
func stripeInit() {
stripe.Key = "sk_test_YOURKEY"
}
func stripeGetSessionID() (sessionid string, err error) {
defer erapse.ShowErapsedTIme(time.Now())
stripeInit()
params := &stripe.CheckoutSessionParams{
PaymentMethodTypes: stripe.StringSlice([]string{
"card",
// "ideal",
}),
LineItems: []*stripe.CheckoutSessionLineItemParams{
&stripe.CheckoutSessionLineItemParams{
PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
Currency: stripe.String("usd"),
Product: stripe.String("prod_YOURPRODUCT"),
/*
ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{
Name: stripe.String("T-shirt"),
},
*/
UnitAmount: stripe.Int64(200),
},
Quantity: stripe.Int64(1),
},
},
Mode: stripe.String("payment"),
SuccessURL: stripe.String("https://example.com/success?session_id={CHECKOUT_SESSION_ID}"),
CancelURL: stripe.String("https://example.com/cancel"),
}
params.AddMetadata("user", "aabbcc")
s, err := session.New(params)
sessionid = s.ID
log.Println("session id = ",sessionid)
return
}
func handleWebhook(c *gin.Context) (err error){
defer erapse.ShowErapsedTIme(time.Now())
stripeInit()
buf := make([]byte, 2048)
n, _ := c.Request.Body.Read(buf)
body := buf[0:n]
log.Println("body = ", string(body))
endpointSecret := "whsec_YOURSECRET"
event, err := webhook.ConstructEvent(body, c.Request.Header.Get("Stripe-Signature"), endpointSecret)
if err != nil {
return
}
// Handle the checkout.session.completed event
if event.Type == "checkout.session.completed" {
log.Println("user", event.Data.Object["metadata"].(map[string]interface {})["user"].(string))
}
return
}
sk_test_YOURKEY, prod_YOURPRODUCT, whsec_YOURSECRET はそれぞれ自分のキー、商品ID、シークレットの文字列におきかえてください
事前定義した商品を使わず動的に商品を生成する場合は Product: のかわりに ProductData: でもいいです
webhook event を取得するために生の request body と Stripe-Signature の request header を取得して webhook.ConstructEvent に渡してあげる必要があるのですが、gin で request header を取ったり request body を生で触ったりする方法が ドキュメンt (opens new window) から見つけられずジタバタとググって上記のように書いてみたのですが、もっとオサレな書き方があるのかは自信ないです
defer erapse.ShowErapsedTIme(time.Now()) は関数の先頭にこれを書いておくだけで経過時間が表示されるので、性能的に「あれっ?」って思う処理を書いてしまってもすぐにその場で原因が見つかるので好きでいつもつかっているのですが、stripe とは関係ないのでなくてもいいです。ちなみになんで i まで大文字にしてしまったのか今となっては思い出せません...
チュートリアル (opens new window) にはないのですが metadata は以下のように使います
- session の生成時に AddMetadata() で登録
- webhook のイベントからは event.Data.Object["metadata"] を (map[string]interface {}) に型アサーションして、取り出した interface {} を再度実際の型にアサーション
gin のおかげでだいぶ楽とはいえ、実行時の型をちゃんとわかってるくせによしなに変換してくれない石頭ぶりがちょっとうざいです。型アサーションって間違ってたときに「正解は○○なんだけどお前は☓☓を指定したぞ」っていう上から目線なエラーメッセージを賜るのですが、正解わかってるんだったら意地悪言ってないで暗黙で変換してよねって思います