/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Arquivo: src/services/tenantFeatureAdminService.js | | Service de admin SaaS pra gestão de features por tenant. Substitui supabase | direto que estava em SaasTenantFeaturesPage.vue (audit alta — 4 queries | inline + 1 RPC). | | Diferente de `tenantFeaturesStore` (que gerencia features do user/tenant ATIVO), | este service opera em QUALQUER tenant selecionado pelo SaaS admin. |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; // SELECTs canônicos const TENANT_LIST_SELECT = 'id, name'; const FEATURE_CATALOG_SELECT = 'id, key, name, descricao'; const ENTITLEMENT_SELECT = 'feature_key'; const TENANT_FEATURE_SELECT = 'feature_key, enabled'; const SUBSCRIPTION_SELECT = 'plan_key'; const EXCEPTIONS_LOG_SELECT = 'feature_key, enabled, reason, created_by, created_at'; /** * Lista todos os tenants (apenas SaaS admin tem acesso via RLS). */ export async function listTenants() { const { data, error } = await supabase.from('tenants').select(TENANT_LIST_SELECT).order('name', { ascending: true }); if (error) throw error; return data || []; } /** * Lista o catálogo completo de features do sistema. */ export async function listFeatureCatalog() { const { data, error } = await supabase.from('features').select(FEATURE_CATALOG_SELECT).order('key', { ascending: true }); if (error) throw error; return data || []; } /** * Carrega o estado completo de features de um tenant: entitlements via plano, * overrides (exceções), plano ativo, e log das últimas 50 exceções. * * @param {string} tenantId * @returns {Promise<{planAllowed: Set, planKey: string|null, overrides: Object, exceptionsLog: Array}>} */ export async function loadTenantFeatureState(tenantId) { if (!tenantId) { return { planAllowed: new Set(), planKey: null, overrides: {}, exceptionsLog: [] }; } const [{ data: ent, error: e1 }, { data: ovr, error: e2 }, { data: sub, error: e3 }, { data: log, error: e4 }] = await Promise.all([ supabase.from('v_tenant_entitlements').select(ENTITLEMENT_SELECT).eq('tenant_id', tenantId), supabase.from('tenant_features').select(TENANT_FEATURE_SELECT).eq('tenant_id', tenantId), supabase.from('v_tenant_active_subscription').select(SUBSCRIPTION_SELECT).eq('tenant_id', tenantId).maybeSingle(), supabase.from('tenant_feature_exceptions_log').select(EXCEPTIONS_LOG_SELECT).eq('tenant_id', tenantId).order('created_at', { ascending: false }).limit(50) ]); if (e1) throw e1; if (e2) throw e2; if (e3) throw e3; if (e4) throw e4; const planAllowed = new Set(); for (const r of ent || []) planAllowed.add(r.feature_key); const overrides = {}; for (const r of ovr || []) overrides[r.feature_key] = !!r.enabled; return { planAllowed, planKey: sub?.plan_key || null, overrides, exceptionsLog: log || [] }; } /** * Aplica exceção comercial (force enable/disable) numa feature de um tenant. * RPC `set_tenant_feature_exception` faz UPSERT em tenant_features + * INSERT em tenant_feature_exceptions_log. */ export async function setFeatureException(tenantId, featureKey, enabled, reason = null) { if (!tenantId) throw new Error('tenantId obrigatório.'); if (!featureKey) throw new Error('featureKey obrigatória.'); const { error } = await supabase.rpc('set_tenant_feature_exception', { p_tenant_id: tenantId, p_feature_key: featureKey, p_enabled: enabled, p_reason: reason ? String(reason).trim() || null : null }); if (error) throw error; }