200 lines
7.0 KiB
JavaScript
200 lines
7.0 KiB
JavaScript
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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;
|
|
}
|