ZERADO
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
// src/services/subscriptionIntents.js
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
function applyFilters(query, { q, status, planKey, interval }) {
|
||||
// --------------------------------------
|
||||
// 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)
|
||||
@@ -9,9 +12,31 @@ function applyFilters(query, { q, status, planKey, interval }) {
|
||||
return query
|
||||
}
|
||||
|
||||
export async function listSubscriptionIntents(filters = {}) {
|
||||
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')
|
||||
.from('subscription_intents') // ✅ VIEW
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
@@ -22,34 +47,144 @@ export async function listSubscriptionIntents(filters = {}) {
|
||||
return data || []
|
||||
}
|
||||
|
||||
export async function markIntentPaid(intentId, notes = '') {
|
||||
// 1) marca como pago
|
||||
const { data: updated, error: upErr } = await supabase
|
||||
.from('subscription_intents')
|
||||
.update({
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString(),
|
||||
notes: notes || null
|
||||
/**
|
||||
* 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 subscription do tenant (Modelo B)
|
||||
// 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
|
||||
|
||||
return { intent: updated, subscription: sub }
|
||||
// 3) retorna visão unificada + assinatura
|
||||
const merged = await fetchIntentFromView(intentId)
|
||||
return { intent: merged || updated, subscription: sub || null }
|
||||
}
|
||||
|
||||
export async function cancelIntent(intentId, notes = '') {
|
||||
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('subscription_intents')
|
||||
.from(table)
|
||||
.update({
|
||||
status: 'canceled',
|
||||
notes: notes || null
|
||||
@@ -59,5 +194,8 @@ export async function cancelIntent(intentId, notes = '') {
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
// devolve a visão unificada
|
||||
const merged = await fetchIntentFromView(intentId)
|
||||
return merged || data
|
||||
}
|
||||
Reference in New Issue
Block a user