'use strict'; const crypto = require('crypto'); /** * Stripe Webhook ハンドラー * * 処理するイベント: * checkout.session.completed * - metadata.product === 'ponshu_room_pro' * -> ponshu_licenses にライセンスキーを発行し Resend でメール送信 * - それ以外 * -> users テーブルに購入記録(既存の Posimai 購読フロー) * customer.subscription.deleted * -> users.plan を free に戻す * * 使用方法: * const { handleWebhook } = require('./routes/stripe')(pool); * app.post('/api/stripe/webhook', express.raw({ type: 'application/json' }), * (req, res) => handleWebhook(req, res)); */ module.exports = function createStripeHandler(pool) { const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || ''; const RESEND_API_KEY = process.env.RESEND_API_KEY || ''; const APP_SUPPORT_EMAIL = process.env.APP_SUPPORT_EMAIL || 'support@posimai.soar-enrich.com'; const FROM_EMAIL = process.env.FROM_EMAIL || 'noreply@posimai.soar-enrich.com'; // ── ライセンスキー生成 (PONSHU-XXXX-XXXX-XXXX) ── function generateLicenseKey() { const hex = crypto.randomBytes(6).toString('hex').toUpperCase(); return `PONSHU-${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}`; } // ── Resend でライセンスキーをメール送信 ── async function sendLicenseEmail(toEmail, licenseKey) { if (!RESEND_API_KEY) { console.warn('[Stripe] RESEND_API_KEY not set — skipping license email'); return; } const emailRes = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RESEND_API_KEY}`, }, body: JSON.stringify({ from: FROM_EMAIL, to: toEmail, subject: 'Ponshu Room Pro ライセンスキーのお知らせ', html: `
この度は Ponshu Room Pro をご購入いただきありがとうございます。
以下のライセンスキーをアプリ内「設定 → ライセンスを有効化」から入力してください。
${licenseKey}
ご不明な点は ${APP_SUPPORT_EMAIL} までお問い合わせください。
`, }), }); if (!emailRes.ok) { const errBody = await emailRes.text(); console.error('[Stripe] Resend API error:', emailRes.status, errBody); } else { console.log(`[Stripe] License email sent to ${toEmail}`); } } // ── Ponshu Room Pro 購入処理 ── async function handlePonshuPurchase(session) { const email = session.customer_details?.email?.toLowerCase(); if (!email) { console.error('[Stripe] Ponshu purchase: no email in session', session.id); return; } // 冪等性チェック — Stripe がリトライしても重複発行しない const existing = await pool.query( `SELECT license_key FROM ponshu_licenses WHERE stripe_session_id = $1`, [session.id] ); if (existing.rows.length > 0) { console.log(`[Stripe] Ponshu: duplicate webhook for session ${session.id}, skipping`); return; } const licenseKey = generateLicenseKey(); await pool.query( `INSERT INTO ponshu_licenses (license_key, email, plan, status, stripe_session_id) VALUES ($1, $2, 'pro', 'active', $3)`, [licenseKey, email, session.id] ); console.log(`[Stripe] Ponshu license issued: ${licenseKey.substring(0, 12)}... -> ${email}`); await sendLicenseEmail(email, licenseKey); } // ── 購入完了メール(マジックリンク付き)送信 ── async function sendWelcomeMagicLink(email) { if (!RESEND_API_KEY) { console.warn('[Stripe] RESEND_API_KEY not set — skipping welcome email'); return; } try { // マジックリンク用トークンを生成(15分有効) const token = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); await pool.query( `INSERT INTO magic_link_tokens (email, token, expires_at) VALUES ($1, $2, $3)`, [email, token, expiresAt] ); const loginBase = process.env.MAGIC_LINK_BASE_URL || 'https://posimai.soar-enrich.com'; const magicLinkUrl = `${loginBase}/auth/verify?token=${token}&redirect=${encodeURIComponent('https://brief.posimai.soar-enrich.com')}`; const emailRes = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RESEND_API_KEY}`, }, body: JSON.stringify({ from: FROM_EMAIL, to: email, subject: 'Posimai へようこそ — ログインリンク', html: `Posimai パスの購入が完了しました。
下のボタンをクリックするとすぐにアプリをご利用いただけます。
このリンクは15分間有効です。
期限切れの場合は ${loginBase}/login からログインしてください。
ご不明な点は ${APP_SUPPORT_EMAIL} までお問い合わせください。