// server/supabase/functions/stripe-checkout/index.ts // Stripe Checkout Session 생성 — 사용자가 업그레이드 버튼 클릭 시 호출. // 응답: { url: 'https://checkout.stripe.com/...' } → 클라이언트가 redirect. import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' interface CheckoutRequest { tier: 'pro' | 'team' success_url: string cancel_url: string } // Stripe Price ID는 환경변수로 주입 (대시보드에서 생성한 product의 price ID) // 실제 운영 시: // supabase secrets set STRIPE_PRICE_PRO=price_xxx // supabase secrets set STRIPE_PRICE_TEAM=price_yyy // supabase secrets set STRIPE_SECRET_KEY=sk_live_xxx // @ts-expect-error — Deno 런타임 전역 Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } try { const user = await requireUser(req) const body = (await req.json()) as CheckoutRequest // @ts-expect-error — Deno.env const stripeKey = Deno.env.get('STRIPE_SECRET_KEY') ?? '' // @ts-expect-error — Deno.env const priceMap: Record = { pro: Deno.env.get('STRIPE_PRICE_PRO') ?? '', team: Deno.env.get('STRIPE_PRICE_TEAM') ?? '' } const priceId = priceMap[body.tier] if (!stripeKey || !priceId) { return new Response( JSON.stringify({ error: 'stripe_not_configured', message: 'STRIPE_SECRET_KEY 또는 STRIPE_PRICE_* 환경변수가 설정되지 않았습니다.' }), { status: 503, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // 기존 customer_id 조회 (subscriptions 테이블에서) const serviceClient = createServiceRoleClient() const { data: sub } = await serviceClient .from('subscriptions') .select('stripe_customer_id') .eq('user_id', user.id) .maybeSingle() let customerId = (sub?.stripe_customer_id as string | null | undefined) ?? null // 없으면 새 customer 생성 if (!customerId) { const customerResp = await fetch('https://api.stripe.com/v1/customers', { method: 'POST', headers: { Authorization: `Bearer ${stripeKey}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ email: user.email ?? '', 'metadata[user_id]': user.id }) }) if (!customerResp.ok) { const errText = await customerResp.text() throw new Error(`Stripe customer 생성 실패: ${errText}`) } const customerData = (await customerResp.json()) as { id: string } customerId = customerData.id // subscriptions에 저장 await serviceClient .from('subscriptions') .upsert({ user_id: user.id, stripe_customer_id: customerId, tier: 'free' }) } // Checkout session 생성 const sessionResp = await fetch('https://api.stripe.com/v1/checkout/sessions', { method: 'POST', headers: { Authorization: `Bearer ${stripeKey}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ customer: customerId, mode: 'subscription', 'line_items[0][price]': priceId, 'line_items[0][quantity]': '1', success_url: body.success_url, cancel_url: body.cancel_url, 'metadata[user_id]': user.id, 'metadata[tier]': body.tier }) }) if (!sessionResp.ok) { const errText = await sessionResp.text() throw new Error(`Stripe checkout 생성 실패: ${errText}`) } const sessionData = (await sessionResp.json()) as { url: string } return new Response(JSON.stringify({ url: sessionData.url }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } catch (err) { if (err && typeof err === 'object' && 'status' in err && 'message' in err) { return authErrorResponse(err as AuthError, corsHeaders) } const message = err instanceof Error ? err.message : 'Unknown error' return new Response(JSON.stringify({ error: message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } })