質問するログイン新規登録

Q&A

0回答

128閲覧

テスト送信すると400エラーが出る

test_user_0139

総合スコア0

Firebase

Firebaseは、Googleが提供するBasSサービスの一つ。リアルタイム通知可能、並びにアクセス制御ができるオブジェクトデータベース機能を備えます。さらに認証機能、アプリケーションのログ解析機能などの利用も可能です。

Stripe

Stripeとは、米国のオンライン決済システム提供企業、及び同社が提供する決裁システムを指します。Webサイトやモバイルアプリにコードを組み込むことでクレジットカードなどの決済サービスが簡潔に追加できます。

0グッド

0クリップ

投稿2025/11/05 06:50

編集2025/11/05 07:48

0

0

実現したいこと

Fiebase と Stripe を組み合わせてサブスクなどを実装したいです。
Firebase側とStripe側の設定は終わっています。

【目標】
・テスト送信して実際に成功
・Firebaseにusers/userId/に書き込む
・成功メールを送信する

発生している問題・分からないこと

stripe trigger checkout.session.completed

すると以下のエラーが出る。

【問題】
・テスト送信失敗
・Firebaseにusers/userId/に書き込み失敗
・成功メールを未送信

が出ます。

エラーメッセージ

error

12025-11-05 13:42:38 <-- [400] POST https://your-products-id.cloudfunctions.net/stripeWebhook [....]

該当のソースコード

index.js

1// functions/index.js 2import { onRequest, onCall } from "firebase-functions/v2/https"; 3import { setGlobalOptions } from "firebase-functions/v2"; 4import { logger } from "firebase-functions"; 5import admin from "firebase-admin"; 6import Stripe from "stripe"; 7 8setGlobalOptions({ maxInstances: 10 }); 9 10// ====================================================== 11// Firebase 初期化 12// ====================================================== 13if (!admin.apps.length) { 14 admin.initializeApp(); 15} 16const db = admin.firestore(); 17 18// ====================================================== 19// Stripe 初期化 20// ====================================================== 21const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { 22 apiVersion: "2024-06-20", 23}); 24 25// ====================================================== 26// Webhook 関数(rawBody 対応) 27// ====================================================== 28export const stripeWebhook = onRequest( 29 { 30 secrets: ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"], 31 region: "us-central1", 32 memory: "256MiB", 33 timeoutSeconds: 60, 34 rawBody: true, // ここが必須 35 }, 36 async (req, res) => { 37 const sig = req.headers["stripe-signature"]; 38 if (!sig) { 39 logger.warn("No stripe-signature"); 40 return res.status(400).send("Missing signature"); 41 } 42 43 let event; 44 try { 45 // rawBody は Buffer になっている 46 event = stripe.webhooks.constructEvent( 47 req.rawBody, 48 sig, 49 process.env.STRIPE_WEBHOOK_SECRET 50 ); 51 } catch (err) { 52 logger.error("Webhook signature failed", err.message); 53 return res.status(400).send(`Webhook Error: ${err.message}`); 54 } 55 56 logger.info(`✅ Event received: ${event.type}`); 57 58 if (event.type === "checkout.session.completed") { 59 const session = event.data.object; 60 const userId = session.client_reference_id; 61 62 if (!userId) { 63 logger.warn("No client_reference_id", session.id); 64 return res.status(400).send("No userId"); 65 } 66 67 // Firestore 書き込み 68 try { 69 await db.collection("users").doc(userId).set( 70 { 71 stripeCustomerId: session.customer, 72 subscription: { 73 active: true, 74 stripeSubscriptionId: session.subscription, 75 plan: session.metadata?.plan || "basic", 76 updatedAt: admin.firestore.FieldValue.serverTimestamp(), 77 }, 78 }, 79 { merge: true } 80 ); 81 logger.info("Subscription activated", { userId }); 82 } catch (error) { 83 logger.error("Firestore write failed", error); 84 return res.status(500).send("Firestore error"); 85 } 86 87 // 歓迎メール送信 88 const email = session.customer_details?.email; 89 if (email) { 90 await sendWelcomeEmail(email, session.metadata?.plan || "basic"); 91 } 92 } 93 94 res.json({ received: true }); 95 } 96); 97 98// ====================================================== 99// Checkout セッション作成 100// ====================================================== 101export const createCheckoutSession = onCall( 102 { 103 secrets: ["STRIPE_SECRET_KEY"], 104 region: "us-central1", 105 }, 106 async (request) => { 107 const uid = request.auth?.uid; 108 if (!uid) throw new Error("ログインが必要です。"); 109 110 const priceId = request.data.priceId; 111 const customerId = await getOrCreateCustomer(uid, stripe, db); 112 113 const session = await stripe.checkout.sessions.create({ 114 mode: "subscription", 115 payment_method_types: ["card"], 116 customer: customerId, 117 line_items: [{ price: priceId, quantity: 1 }], 118 client_reference_id: uid, 119 metadata: { plan: request.data.plan || "basic" }, 120 success_url: "https://your-domain.com/success.html", 121 cancel_url: "https://your-domain.com/cancel.html", 122 }); 123 124 return { url: session.url }; 125 } 126); 127 128// ====================================================== 129// カスタマー取得 or 作成 130// ====================================================== 131async function getOrCreateCustomer(uid, stripe, db) { 132 const userRef = db.collection("users").doc(uid); 133 const userSnap = await userRef.get(); 134 135 if (userSnap.exists && userSnap.data()?.stripeCustomerId) { 136 return userSnap.data().stripeCustomerId; 137 } 138 139 const userRecord = await admin.auth().getUser(uid); 140 const customer = await stripe.customers.create({ 141 email: userRecord.email, 142 metadata: { uid }, 143 }); 144 145 await userRef.set({ stripeCustomerId: customer.id }, { merge: true }); 146 return customer.id; 147} 148 149// ====================================================== 150// 歓迎メール送信関数 151// ====================================================== 152async function sendWelcomeEmail(email, plan) { 153 const nodemailer = await import("nodemailer"); // 関数内 import 154 const transporter = nodemailer.createTransport({ 155 service: "gmail", 156 auth: { 157 user: "your-gmail@gmail.com", 158 pass: process.env.GMAIL_APP_PASSWORD, 159 }, 160 }); 161 162 const mailOptions = { 163 from: "メルカリ売上管理ツール <noreply@your-domain.com>", 164 to: email, 165 subject: "有料プランへようこそ!", 166 html: ` 167 <h2>おめでとうございます!</h2> 168 <p><strong>${plan}プラン</strong>が有効になりました!</p> 169 <p>今すぐ自動売上取得がスタートします。</p> 170 <p><a href="https://your-tool.com">ツールにログイン</a></p> 171 <hr> 172 <small>30日間返金保証付き | support@your-domain.com</small> 173 `, 174 }; 175 176 await transporter.sendMail(mailOptions); 177 logger.info("Welcome email sent", { email }); 178} 179

firebase.json

1{ 2 "functions": { 3 "source": "functions" 4 }, 5 "emulators": { 6 "functions": { 7 "port": 8358 8 }, 9 "firestore": { 10 "port": 8082 11 }, 12 "ui": { 13 "enabled": true 14 } 15 } 16} 17

packege.json

1{ 2 "name": "functions", 3 "type": "module", 4 "scripts": { 5 "lint": "eslint .", 6 "serve": "firebase emulators:start --only functions", 7 "shell": "firebase functions:shell", 8 "start": "npm run shell", 9 "deploy": "firebase deploy --only functions", 10 "logs": "firebase functions:log" 11 }, 12 "engines": { 13 "node": "20" 14 }, 15 "main": "index.js", 16 "dependencies": { 17 "express": "^5.1.0", 18 "firebase-admin": "^12.6.0", 19 "firebase-functions": "^5.1.1", 20 "nodemailer": "^7.0.10", 21 "stripe": "^16.12.0" 22 }, 23 "devDependencies": { 24 "eslint": "^8.15.0", 25 "eslint-config-google": "^0.14.0" 26 }, 27 "private": true 28} 29

試したこと・調べたこと

  • teratailやGoogle等で検索した
  • ソースコードを自分なりに変更した
  • 知人に聞いた
  • その他
上記の詳細・結果

ChatGPTおよびgrokを使ってエラー解決に臨んだもののうまくいきませんでした。

補足

特になし

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだ回答がついていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.29%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問