Files
soft_usite/pkg/services/renovacion_consumo_test.go
T
Lizandro GuarnizoandClaude Sonnet 5 023a8af494 feat(cobro): suma el consumo de uMind al ciclo de cobro existente
El cobro recurrente (link de pago, webhook con firma, renovación
automática de vencimiento) ya funcionaba de punta a punta; lo único que
le faltaba para cobrar por uso era monto variable.

- MontoACobrar = PrecioAcordado + consumo pendiente del cliente. Si el
  cliente no usa uMind el consumo es 0 y el monto queda idéntico al de
  antes, así que los contratos existentes no cambian.
- El consumo se agrega por Cliente, no por tenant: un contrato es de un
  cliente y un cliente puede tener varios tenants.
- El desglose (cuántos tokens, imágenes y transcripciones, y a cuánto)
  va como líneas de servicio, así las plantillas de correo existentes lo
  muestran sin tocarlas. Cobrar un monto variable sin decir de dónde sale
  es pedir una disputa.
- Al confirmarse el pago se cierra el consumo (facturado_at). Solo si el
  UPDATE del contrato afectó filas: si el webhook llega dos veces, la
  segunda no vuelve a cerrar nada.
- Test de que el detalle del correo suma exactamente lo que se cobra.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:53:40 -05:00

57 lines
2.0 KiB
Go

package services
import (
"strings"
"testing"
"time"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// El detalle que va en el correo tiene que sumar exactamente lo que cobra el
// link de pago. Si esto se desincroniza, el cliente recibe un cobro que no
// coincide con su propio desglose — una disputa asegurada.
func TestItemsDesdeResumenCuadraConElTotal(t *testing.T) {
resumen := []models.ResumenUso{
{Tipo: models.UsoTipoIA, Unidad: "tokens", Cantidad: 12000, Costo: 30, Eventos: 8},
{Tipo: models.UsoTipoOCR, Unidad: "imagen", Cantidad: 4, Costo: 40, Eventos: 4},
{Tipo: models.UsoTipoWhisper, Unidad: "transcripcion", Cantidad: 2, Costo: 80, Eventos: 2},
}
items := itemsDesdeResumen(resumen, time.Now())
if len(items) != 3 {
t.Fatalf("esperaba 3 líneas de detalle, obtuve %d", len(items))
}
var total float64
for _, it := range items {
total += it.Precio
}
if total != 150 {
t.Errorf("el detalle suma %.2f, esperaba 150 (30+40+80)", total)
}
// La etiqueta tiene que decir cuánto se consumió, no solo el precio.
if !strings.Contains(items[0].Nombre, "12000") || !strings.Contains(items[0].Nombre, "tokens") {
t.Errorf("la línea de IA no dice la cantidad consumida: %q", items[0].Nombre)
}
}
// Un consumo en cero no debe aparecer como línea de $0 en el correo.
func TestItemsDesdeResumenOmiteCostoCero(t *testing.T) {
resumen := []models.ResumenUso{
{Tipo: models.UsoTipoIA, Unidad: "tokens", Cantidad: 500, Costo: 0},
{Tipo: models.UsoTipoOCR, Unidad: "imagen", Cantidad: 1, Costo: 10},
}
items := itemsDesdeResumen(resumen, time.Now())
if len(items) != 1 {
t.Fatalf("esperaba 1 línea (la de costo 0 se omite), obtuve %d", len(items))
}
}
// Sin consumo no hay líneas extra: el correo queda igual que antes de que
// existiera el cobro por uso.
func TestItemsDesdeResumenVacio(t *testing.T) {
if items := itemsDesdeResumen(nil, time.Now()); len(items) != 0 {
t.Errorf("sin consumo no debería agregar líneas, obtuve %d", len(items))
}
}