Práctico

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 y sessions — 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 de user_uuid.

Atajos de relación (sin FKs, pero estables):

  • users.uuidorders.user_uuid | credits.user_uuid | tasks.user_uuid | apikeys.user_uuid | affiliates.user_uuid
  • orders.order_nocredits.order_no
  • credits.trans_notasks.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)

  1. orders: la fila order_no pasa a paid con metadatos del cargo.
  2. credits: añadimos una fila positiva { trans_type: "order_pay", credits: <order.credits>, order_no }.
  3. affiliates: actualización opcional de recompensa enlazada al pedido.

Gasto en una tarea (Texto→Video)

  1. credits: fila negativa { trans_type: "task_text_to_video", credits: -N } y devolvemos su trans_no.
  2. tasks: fila { credits_used: N, credits_trans_no: <ese trans_no> } para trazar el gasto.

Mini ping (1 crédito)

  1. 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#getUserCreditSummarybalance, 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 fila tasks 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

  1. Inicia la app: pnpm dev (requiere una URL de Postgres en .env.local).

  2. Crea un usuario: visita /es/signup, regístrate e inicia sesión.

  3. 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.

  4. 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>"
  5. Consume créditos: llama a /api/ping con un mensaje para descontar CreditsAmount.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 el order_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 por expired_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 y expiringSoon.
  • ¿Quieres esta guía en otro idioma? Puedo sincronizarla.