// server/supabase/functions/payple-manage/index.ts // Payple 구독 관리 — 취소 (빌링키 해지 + tier 다운그레이드) // stripe-portal 대체. // verify_jwt = false (requireUser로 직접 인증) import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { getPaypleConfig, paypleAuth, paypleDeleteBillingKey, } from '../_shared/payple.ts' interface ManageRequest { action: 'cancel' | 'info' } 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 user = await requireUser(req) const body = (await req.json()) as ManageRequest const serviceClient = createServiceRoleClient() // 현재 구독 정보 조회 const { data: sub } = await serviceClient .from('subscriptions') .select('tier, status, payment_provider, payple_payer_id, current_period_end') .eq('user_id', user.id) .maybeSingle() if (!sub) { return jsonResponse({ error: 'No subscription found' }, 404) } // info: 현재 구독 상태 반환 if (body.action === 'info') { return jsonResponse({ tier: sub.tier, status: sub.status, payment_provider: sub.payment_provider, current_period_end: sub.current_period_end, has_billing_key: !!sub.payple_payer_id, }) } // cancel: 구독 취소 if (body.action === 'cancel') { if (sub.payment_provider !== 'payple' || !sub.payple_payer_id) { return jsonResponse({ error: 'No active Payple subscription to cancel' }, 400) } // 1. Payple 빌링키 해지 const config = getPaypleConfig() const auth = await paypleAuth(config, { payWork: 'PUSERDEL' }) await paypleDeleteBillingKey(config, auth, sub.payple_payer_id) // 2. DB 업데이트 — 현재 구독 기간이 끝날 때까지 유지 await serviceClient .from('subscriptions') .update({ status: 'canceled', cancel_at: sub.current_period_end ?? new Date().toISOString(), payple_payer_id: null, updated_at: new Date().toISOString(), }) .eq('user_id', user.id) return jsonResponse({ success: true, message: 'Subscription will be canceled at the end of the current period', cancel_at: sub.current_period_end, }) } return jsonResponse({ error: 'Invalid action. Must be cancel or info' }, 400) } 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 jsonResponse({ error: message }, 500) } })