/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Criado e desenvolvido por Leonardo Nohama | | Tecnologia aplicada à escuta. | Estrutura para o cuidado. | | Arquivo: src/services/subscriptionIntents.js | Data: 2026 | Local: São Carlos/SP — Brasil |-------------------------------------------------------------------------- | © 2026 — Todos os direitos reservados |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; // -------------------------------------- // Helpers // -------------------------------------- function applyFilters(query, { q, status, planKey, interval }) { if (q) query = query.ilike('email', `%${q}%`); if (status) query = query.eq('status', status); if (planKey) query = query.eq('plan_key', planKey); if (interval) query = query.eq('interval', interval); return query; } function getWriteTableByTarget(planTarget) { const t = String(planTarget || '').toLowerCase(); if (t === 'clinic') return 'subscription_intents_tenant'; if (t === 'therapist') return 'subscription_intents_personal'; return null; } async function fetchIntentFromView(intentId) { const { data, error } = await supabase .from('subscription_intents') // ✅ VIEW (read) .select('*') .eq('id', intentId) .maybeSingle(); if (error) throw error; if (!data) throw new Error('Intenção não encontrada.'); return data; } // -------------------------------------- // Public API // -------------------------------------- export async function listSubscriptionIntents(filters = {}) { let query = supabase .from('subscription_intents') // ✅ VIEW .select('*') .order('created_at', { ascending: false }); query = applyFilters(query, filters); const { data, error } = await query; if (error) throw error; return data || []; } /** * Busca a assinatura mais recente coerente com a intenção. * Não depende de subscription_id existir. * * opts: * - strictPlanKey (default true): filtra por plan_key (evita pegar plano errado) * - onlyActive (default false): se true, busca somente status 'active' */ export async function findLatestSubscriptionForIntent(intentOrId, opts = {}) { const { strictPlanKey = true, onlyActive = false } = opts; const intent = typeof intentOrId === 'string' ? await fetchIntentFromView(intentOrId) : intentOrId; const target = String(intent?.plan_target || '').toLowerCase(); const planKey = intent?.plan_key || null; const intentUserId = intent?.user_id || intent?.created_by_user_id || null; const tenantId = intent?.tenant_id || null; let query = supabase.from('subscriptions').select('*').order('created_at', { ascending: false }).limit(1); if (onlyActive) query = query.eq('status', 'active'); if (target === 'clinic') { if (!tenantId) throw new Error('Intenção clinic sem tenant_id.'); query = query.eq('tenant_id', tenantId); if (strictPlanKey && planKey) query = query.eq('plan_key', planKey); } else if (target === 'therapist') { if (!intentUserId) throw new Error('Intenção therapist sem user_id.'); query = query.eq('user_id', intentUserId).is('tenant_id', null); if (strictPlanKey && planKey) query = query.eq('plan_key', planKey); } else { throw new Error('plan_target inválido na intenção (esperado clinic/therapist).'); } const { data, error } = await query.maybeSingle(); if (error) throw error; return data || null; } /** * Retorna a assinatura para uma intenção: * 1) se existir intent.subscription_id, tenta carregar ela * 2) fallback: busca a mais recente coerente com o target */ export async function getSubscriptionForIntent(intentOrId, opts = {}) { const intent = typeof intentOrId === 'string' ? await fetchIntentFromView(intentOrId) : intentOrId; const subId = intent?.subscription_id || null; if (subId) { const { data, error } = await supabase.from('subscriptions').select('*').eq('id', subId).maybeSingle(); if (error) throw error; if (data?.id) return data; } return await findLatestSubscriptionForIntent(intent, opts); } export async function markIntentPaid(intentId, notes = '') { // 0) pega intenção na VIEW (pra descobrir plan_target e validar estado) const intent = await fetchIntentFromView(intentId); if (intent.status === 'paid') { // idempotente: ainda tenta ativar a subscription a partir do intent (caso tenha falhado antes) const { data: sub, error: rpcErr } = await supabase.rpc('activate_subscription_from_intent', { p_intent_id: intentId }); if (rpcErr) throw rpcErr; const merged = await fetchIntentFromView(intentId); return { intent: merged || intent, subscription: sub || null }; } if (intent.status === 'canceled') { throw new Error('Intenção cancelada não pode ser marcada como paga.'); } const table = getWriteTableByTarget(intent.plan_target); if (!table) { throw new Error('plan_target inválido na intenção (esperado clinic/therapist).'); } // 1) marca como pago na TABELA REAL (write) const patch = { status: 'paid', paid_at: new Date().toISOString(), notes: notes || null }; const { data: updated, error: upErr } = await supabase.from(table).update(patch).eq('id', intentId).select('*').maybeSingle(); if (upErr) throw upErr; // 2) ativa assinatura a partir da intenção const { data: sub, error: rpcErr } = await supabase.rpc('activate_subscription_from_intent', { p_intent_id: intentId }); if (rpcErr) throw rpcErr; // 3) retorna visão unificada + assinatura const merged = await fetchIntentFromView(intentId); return { intent: merged || updated, subscription: sub || null }; } export async function cancelIntent(intentId, notes = '') { // 0) pega intenção na VIEW (pra descobrir plan_target e validar estado) const intent = await fetchIntentFromView(intentId); if (intent.status === 'canceled') return intent; if (intent.status === 'paid') { // regra de negócio: se você quiser permitir cancelar paid, mude aqui. throw new Error('Intenção já paga não deve ser cancelada. Cancele a assinatura, não a intenção.'); } const table = getWriteTableByTarget(intent.plan_target); if (!table) { throw new Error('plan_target inválido na intenção (esperado clinic/therapist).'); } const { data, error } = await supabase .from(table) .update({ status: 'canceled', notes: notes || null }) .eq('id', intentId) .select('*') .maybeSingle(); if (error) throw error; // devolve a visão unificada const merged = await fetchIntentFromView(intentId); return merged || data; }