Cuentas, pedidos y créditos
Comprende cómo se relacionan los usuarios, los pedidos y el libro mayor de créditos en el template Sushi SaaS. Aprende la fórmula del saldo, el manejo de caducidad y las APIs/servicios para otorgar, consumir y consultar créditos.
Por qué importa
Este template incluye autenticación, seguimiento de pedidos compatible con Stripe y un sistema de saldo. Comprender cómo se relacionan las tablas te permitirá ampliar el catálogo o conectar tu propio proveedor de pagos.
El reparto (tablas clave)
users
— Fuente de verdad del perfil.uuid
(clave común), email, locale, rol. Poblado por Better Auth.accounts
ysessions
— Internas de Better Auth para inicio de sesión. Útiles para auditoría; fuera del cálculo del saldo.orders
— Cada intento/resultado de compra.order_no
,status
,amount
, columnas de suscripción (sub_*
),credits
,expired_at
.credits
— Libro mayor append‑only. Positivos = otorgamientos; negativos = consumos. Columnas:trans_no
,user_uuid
,trans_type
,credits
,order_no
,expired_at
.tasks
— Trabajos de uso (p. ej., texto→video).credits_used
,credits_trans_no
enlazan el gasto con la tarea.- Cameos:
apikeys
,affiliates
también cuelgan deuser_uuid
.
Atajos de relación (sin FKs, pero estables):
users.uuid
→orders.user_uuid
|credits.user_uuid
|tasks.user_uuid
|apikeys.user_uuid
|affiliates.user_uuid
orders.order_no
→credits.order_no
credits.trans_no
→tasks.credits_trans_no
Todas las tablas tienen timestamps para reconstruir la historia y hacer analíticas.
La analogía: Billetera + Recibos
- Piensa en
credits
como una billetera transparente. Cada depósito o gasto es un recibo que guardas. - Los depósitos (filas positivas) suman dinero; los gastos (negativas) lo restan. ¿Tarjeta regalo vencida? El recibo sigue, pero ya no paga nada.
- El saldo = suma de positivos no vencidos − suma de negativos.
Por eso el libro es append‑only: no mutamos el pasado, añadimos nuevos recibos para explicar todo.
Una acción → varias filas (recorridos)
Pago exitoso (Stripe)
orders
: la filaorder_no
pasa apaid
con metadatos del cargo.credits
: añadimos una fila positiva{ trans_type: "order_pay", credits: <order.credits>, order_no }
.affiliates
: actualización opcional de recompensa enlazada al pedido.
Gasto en una tarea (Texto→Video)
credits
: fila negativa{ trans_type: "task_text_to_video", credits: -N }
y devolvemos sutrans_no
.tasks
: fila{ credits_used: N, credits_trans_no: <ese trans_no> }
para trazar el gasto.
Mini ping (1 crédito)
credits
: fila negativa{ trans_type: "ping", credits: -1 }
.
Caducidad
- Una fila positiva con
expired_at
en el pasado ya no suma al balance, pero queda para auditoría.
Servicios y APIs útiles
src/services/credit.ts#getUserCreditSummary
—balance
,granted
,consumed
,expired
,expiringSoon[]
,ledger[]
(recortado).src/services/credit.ts#getUserCredits
— snapshot ligero para gating:{ left_credits, is_pro, is_recharged }
.src/services/credit.ts#decreaseCredits
— agrega fila negativa; lanza error si faltan créditos.src/services/credit.ts#increaseCredits
— agrega fila positiva; usado por Stripe y otorgamientos.src/services/stripe.ts#handleCheckoutSession
— convierte una sesión pagada en estado del pedido + fila de crédito.src/services/tasks.ts#createTextToVideoTask
— descuenta créditos y crea una filatasks
que referencia ese gasto.
Endpoints HTTP
POST /api/account/credits
— resumen; acepta{ includeLedger, ledgerLimit, includeExpiring }
.POST /api/account/profile
— perfil + resumen;{ includeCreditLedger: false }
para aligerar.POST /api/ping
— demo de consumo (1 crédito por defecto).
Todas responden con respJson
: { code, message, data }
.
Prueba el flujo en local
-
Inicia la app:
pnpm dev
(requiere una URL de Postgres en.env.local
). -
Crea un usuario: visita
/es/signup
, regístrate e inicia sesión. -
Añade créditos manualmente (modo desarrollo) insertando en
credits
:insert into credits (trans_no, user_uuid, trans_type, credits, created_at) values ('dev-grant-1', '<uuid-del-usuario>', 'manual_adjustment', 100, now());
Puedes ejecutarlo con
pnpm drizzle-kit studio
o tu cliente SQL favorito. -
Consulta el saldo: llama a
/api/account/credits
con HTTPie o curl:curl -X POST http://localhost:3000/api/account/credits \ -H "Content-Type: application/json" \ -H "Cookie: <copia las cookies de autenticación>"
-
Consume créditos: llama a
/api/ping
con un mensaje para descontarCreditsAmount.PingCost
(1 crédito por defecto) y vuelve a consultar el saldo.curl -X POST http://localhost:3000/api/ping \ -H "Content-Type: application/json" \ -H "Cookie: <copia las cookies de autenticación>" \ -d '{\"message\":\"hola\"}'
Personaliza el sistema
Cambia la ventana de caducidad
Ajusta EXPIRING_WINDOW_DAYS
en src/services/credit.ts
para modificar el rango de aviso “expira pronto”. Si quieres forzar la caducidad automática, crea un cron que añada ajustes negativos cuando expired_at
haya pasado.
Bonos de bienvenida
Usa insertCredit
de src/models/credit.ts
justo después de insertUser
para dar saldo inicial. Asegúrate de mantener único trans_no
.
Integra otro procesador de pagos
- El template asume Stripe, pero puedes llamar a
insertOrder
con la carga de tu proveedor. - Registra los créditos en
credits
y enlaza elorder_no
para facilitar las conciliaciones. - Actualiza la UI o la documentación si utilizas las columnas de suscripción (
sub_*
).
Guarda más contexto
Agrega columnas (por ejemplo meta
en JSON) a credits
/orders
para flags o experimentos. El sistema de migraciones de Drizzle actualizará el snapshot automáticamente.
Trucos y gotchas
- El balance son sumas; no “gastamos” una fila positiva concreta. Las negativas referencian una fuente probable vía
order_no
para trazabilidad. getUserValidCredits
ordena porexpired_at
para etiquetar una fuente cercana al consumir.- ¿Necesitas FIFO estricto o consumos por lote? Extiende el modelo y conserva el libro append‑only para auditoría.
- Mantén
users.uuid
estable; es el pegamento entre orders, credits, tasks, keys y affiliates.
Próximos pasos
- Crea dashboards usando el resumen de
getUserCreditSummary
. - Usa el endpoint
/api/ping
como plantilla para otras operaciones que consuman créditos. - Escribe pruebas unitarias para la lógica de créditos; simula operaciones y valida
balance
,expired
yexpiringSoon
. - ¿Quieres esta guía en otro idioma? Puedo sincronizarla.
Configuración de la base de datos
Configura Postgres y Drizzle ORM — la columna vertebral para auth, cobros, almacenamiento, tareas y tablas de la app.
Tareas con Créditos — Texto a Video
Añade monetización por uso con una tabla genérica de tareas, un libro mayor de créditos y un generador de texto a video enchufable. Aprende el esquema, APIs, constantes de configuración y una UI mínima para lanzar funciones de IA.