mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 11:16:46 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
149fe7b4b9
commit
082970ec8b
1 changed files with 114 additions and 11 deletions
|
|
@ -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<string | null> {
|
||||
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<string | null> {
|
||||
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(
|
||||
`🔴 <b>Pagamento falhou</b>\n\n` +
|
||||
`📧 <b>Email:</b> ${customerEmail}\n` +
|
||||
`📅 <b>Data:</b> ${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<string, string> = {
|
||||
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<string, string> = {
|
||||
basic: '🟢',
|
||||
starter: '🟡',
|
||||
professional: '🔵',
|
||||
enterprise: '🟣',
|
||||
};
|
||||
const slug = (plan as any)?.slug as string | undefined;
|
||||
|
||||
await notifyAdmin(
|
||||
`${planEmoji[slug ?? ''] || '⚪'} <b>Novo cliente Mika!</b>\n\n` +
|
||||
`👤 <b>Nome:</b> ${(profile as any)?.full_name || 'N/A'}\n` +
|
||||
`📧 <b>Email:</b> ${customerEmail || 'N/A'}\n` +
|
||||
`📱 <b>Telefone:</b> ${(profile as any)?.phone || 'N/A'}\n` +
|
||||
`💳 <b>Plano:</b> ${(plan as any)?.name || 'N/A'} — R$ ${(plan as any)?.price_monthly_brl ?? '?'}/mês\n` +
|
||||
`🤖 <b>Bot:</b> Aguardando configuração\n\n` +
|
||||
`➡️ <a href="https://mika.domco.ai/admin">Provisionar agora</a>`
|
||||
);
|
||||
} 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(
|
||||
`⚠️ <b>Assinatura cancelada</b>\n\n` +
|
||||
`📧 <b>Email:</b> ${customerEmail}\n` +
|
||||
`📅 <b>Data:</b> ${new Date().toLocaleDateString('pt-BR')}`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('notifyAdmin (canceled) error:', e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue