From 082970ec8b13bdf6f1c2d1f2bf5fffb1a54a1715 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:48:38 +0000 Subject: [PATCH] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/payments-webhook/index.ts | 125 +++++++++++++++++-- 1 file changed, 114 insertions(+), 11 deletions(-) diff --git a/supabase/functions/payments-webhook/index.ts b/supabase/functions/payments-webhook/index.ts index 9db9800..a207cf7 100644 --- a/supabase/functions/payments-webhook/index.ts +++ b/supabase/functions/payments-webhook/index.ts @@ -6,6 +6,48 @@ const supabase = createClient( Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ); +async function notifyAdmin(message: string) { + const adminBotToken = Deno.env.get('ADMIN_TELEGRAM_BOT_TOKEN'); + const adminChatId = Deno.env.get('ADMIN_TELEGRAM_CHAT_ID'); + if (!adminBotToken || !adminChatId) return; + try { + await fetch(`https://api.telegram.org/bot${adminBotToken}/sendMessage`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: adminChatId, + text: message, + parse_mode: 'HTML', + }), + }); + } catch (e) { + console.error('Admin notification failed:', e); + } +} + +async function lookupEmailByUserId(userId: string): Promise { + try { + const { data } = await supabase.auth.admin.getUserById(userId); + return data?.user?.email ?? null; + } catch (e) { + console.error('Failed to lookup email:', e); + return null; + } +} + +async function lookupEmailByCustomerId(customerId: string, env: PaddleEnv): Promise { + const { data } = await supabase + .from('subscriptions') + .select('user_id') + .eq('paddle_customer_id', customerId) + .eq('environment', env) + .order('created_at', { ascending: false }) + .limit(1) + .maybeSingle(); + if (!data?.user_id) return null; + return await lookupEmailByUserId(data.user_id); +} + Deno.serve(async (req) => { if (req.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); @@ -29,7 +71,6 @@ Deno.serve(async (req) => { environment: env, payload: event as any, }); - // Se já existe (unique violation), ignora silenciosamente if (insertErr && insertErr.code !== '23505') { console.error('Failed to record event:', insertErr); } else if (insertErr?.code === '23505') { @@ -43,8 +84,10 @@ Deno.serve(async (req) => { switch (event.eventType) { case EventName.SubscriptionCreated: + await upsertSubscription((event as any).data, env, true); + break; case EventName.SubscriptionUpdated: - await upsertSubscription((event as any).data, env); + await upsertSubscription((event as any).data, env, false); break; case EventName.SubscriptionCanceled: await markCanceled((event as any).data, env); @@ -52,9 +95,25 @@ Deno.serve(async (req) => { case EventName.TransactionCompleted: console.log('Transaction completed:', (event as any).data.id); break; - case EventName.TransactionPaymentFailed: - console.log('Payment failed:', (event as any).data.id); + case EventName.TransactionPaymentFailed: { + const data = (event as any).data; + console.log('Payment failed:', data.id); + const customerEmail = + data.customer?.email || + (data.customerId ? await lookupEmailByCustomerId(data.customerId, env) : null) || + 'N/A'; + try { + await notifyAdmin( + `🔴 Pagamento falhou\n\n` + + `📧 Email: ${customerEmail}\n` + + `📅 Data: ${new Date().toLocaleDateString('pt-BR')}\n\n` + + `O acesso do cliente será suspenso em breve.` + ); + } catch (e) { + console.error('notifyAdmin (payment_failed) error:', e); + } break; + } default: console.log('Unhandled event:', event.eventType); } @@ -69,7 +128,7 @@ Deno.serve(async (req) => { } }); -async function upsertSubscription(data: any, env: PaddleEnv) { +async function upsertSubscription(data: any, env: PaddleEnv, isCreated: boolean) { const { id, customerId, items, status, currentBillingPeriod, scheduledChange, customData } = data; const userId = customData?.userId; @@ -83,8 +142,6 @@ async function upsertSubscription(data: any, env: PaddleEnv) { const productExt = item?.product?.importMeta?.externalId || item?.product?.id; const billingCycle = item?.price?.billingCycle?.interval === 'year' ? 'yearly' : 'monthly'; - // Resolve plan_id local pelo slug correspondente ao product externalId - // Mapeamento: basic_plan -> basic, starter_plan -> starter, professional_plan -> professional const slugMap: Record = { basic_plan: 'basic', starter_plan: 'starter', @@ -121,18 +178,50 @@ async function upsertSubscription(data: any, env: PaddleEnv) { throw error; } - // Atualiza paddle_customer_id no profile await supabase.from('profiles').update({ paddle_customer_id: customerId }).eq('id', userId); - // Provisiona agent_instance se ainda não existir (idempotente via unique index em user_id). - // TODO Fase 5: dispatch provisioning job (criar container Docker na VPS via Coolify API) + let agentJustCreated = false; if (status === 'active' || status === 'trialing') { const { error: agentErr } = await supabase .from('agent_instances') .insert({ user_id: userId, status: 'provisioning' }); - // 23505 = já existe (esperado para usuários retornando), ignora if (agentErr && agentErr.code !== '23505') { console.error('Failed to provision agent_instance:', agentErr); + } else if (!agentErr) { + agentJustCreated = true; + } + } + + // Notifica admin no Telegram quando é uma assinatura nova / agente recém-criado + if (isCreated || agentJustCreated) { + try { + const [{ data: profile }, { data: plan }, customerEmail] = await Promise.all([ + supabase.from('profiles').select('full_name, phone').eq('id', userId).single(), + planId + ? supabase.from('plans').select('name, price_monthly_brl, slug').eq('id', planId).single() + : Promise.resolve({ data: null as any }), + lookupEmailByUserId(userId), + ]); + + const planEmoji: Record = { + basic: '🟢', + starter: '🟡', + professional: '🔵', + enterprise: '🟣', + }; + const slug = (plan as any)?.slug as string | undefined; + + await notifyAdmin( + `${planEmoji[slug ?? ''] || '⚪'} Novo cliente Mika!\n\n` + + `👤 Nome: ${(profile as any)?.full_name || 'N/A'}\n` + + `📧 Email: ${customerEmail || 'N/A'}\n` + + `📱 Telefone: ${(profile as any)?.phone || 'N/A'}\n` + + `💳 Plano: ${(plan as any)?.name || 'N/A'} — R$ ${(plan as any)?.price_monthly_brl ?? '?'}/mês\n` + + `🤖 Bot: Aguardando configuração\n\n` + + `➡️ Provisionar agora` + ); + } catch (e) { + console.error('notifyAdmin (subscription_created) error:', e); } } } @@ -143,4 +232,18 @@ async function markCanceled(data: any, env: PaddleEnv) { .update({ status: 'canceled', updated_at: new Date().toISOString() }) .eq('paddle_subscription_id', data.id) .eq('environment', env); + + try { + const customerEmail = + data.customer?.email || + (data.customerId ? await lookupEmailByCustomerId(data.customerId, env) : null) || + 'N/A'; + await notifyAdmin( + `⚠️ Assinatura cancelada\n\n` + + `📧 Email: ${customerEmail}\n` + + `📅 Data: ${new Date().toLocaleDateString('pt-BR')}` + ); + } catch (e) { + console.error('notifyAdmin (canceled) error:', e); + } }