// server/supabase/functions/payple-webhook/index.ts // Payple 웹훅 수신 — 결제완료, 취소, 빌링키 등록/해지 이벤트 처리. // Payple 관리자에서 웹훅 URL을 등록해야 함. // verify_jwt = false (외부 Payple 서버에서 호출) import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { createServiceRoleClient } from '../_shared/quota.ts' interface PaypleWebhookPayload { PCD_PAY_RST: 'success' | 'error' PCD_PAY_CODE: string PCD_PAY_MSG: string PCD_PAY_TYPE: string PCD_PAY_OID: string PCD_PAY_TOTAL?: string PCD_PAYER_ID?: string PCD_PAYER_NO?: string // 우리가 전달한 user_id PCD_PAY_CARDNAME?: string PCD_PAY_CARDNUM?: string PCD_PAY_TIME?: string // 결제 시간 (YYYYMMDDHHMMSS) // 웹훅 이벤트 구분용 PCD_PAY_WORK?: string // 'AUTH' (등록), 'CERT' (등록+결제) PCD_PAYCANCEL_FLAG?: string // 'Y' (취소 이벤트) } function jsonResponse(body: Record, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } // @ts-expect-error — Deno 런타임 전역 Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') { return jsonResponse({ error: 'Method not allowed' }, 405) } try { const payload = (await req.json()) as PaypleWebhookPayload const serviceClient = createServiceRoleClient() // 취소 이벤트 if (payload.PCD_PAYCANCEL_FLAG === 'Y') { if (payload.PCD_PAY_RST === 'success' && payload.PCD_PAY_OID) { // 주문번호로 구독 찾아서 상태 변경 const { data: sub } = await serviceClient .from('subscriptions') .select('user_id') .eq('payple_pay_oid', payload.PCD_PAY_OID) .maybeSingle() if (sub) { await serviceClient .from('subscriptions') .update({ status: 'canceled', tier: 'free', updated_at: new Date().toISOString(), }) .eq('user_id', sub.user_id) await serviceClient .from('profiles') .update({ tier: 'free', updated_at: new Date().toISOString() }) .eq('id', sub.user_id) } } return jsonResponse({ received: true, event: 'cancel' }) } // 결제 완료 이벤트 if (payload.PCD_PAY_RST === 'success' && payload.PCD_PAYER_ID) { // payer_id(빌링키)로 구독 찾기 const { data: sub } = await serviceClient .from('subscriptions') .select('user_id, tier') .eq('payple_payer_id', payload.PCD_PAYER_ID) .maybeSingle() if (sub && payload.PCD_PAY_OID) { // 주문번호 + 구독 기간 갱신 (정기결제 갱신 시) const now = new Date() const end = new Date(now) end.setMonth(end.getMonth() + 1) await serviceClient .from('subscriptions') .update({ payple_pay_oid: payload.PCD_PAY_OID, status: 'active', current_period_start: now.toISOString(), current_period_end: end.toISOString(), updated_at: now.toISOString(), }) .eq('user_id', sub.user_id) } return jsonResponse({ received: true, event: 'payment_complete' }) } // 그 외 이벤트는 로깅만 return jsonResponse({ received: true, event: 'unknown' }) } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error' return jsonResponse({ error: message }, 500) } })