feat(umind): vincula tenants a clientes y agrega planes con precios
Base para que el cliente administre uMind desde su portal y para el cobro por consumo. Todavía no cambia nada del comportamiento actual. - UmindTenant gana ClienteID (punteros, sin not null: los tenants creados antes del portal quedan sin asignar y el ALTER TABLE no falla sobre datos existentes) y PlanID. - GetUmindTenantsByClientes es fail-closed: sin clientes, no ve nada. - UmindPlan define el máximo de agentes y los precios por 1k tokens, por imagen OCR, por transcripción y la mensualidad, más un tope de consumo que solo avisa. CRUD para staff en /app/umind-planes. - El modelo va en AMBAS listas de AutoMigrate (main.go y migrations), no repetir el error de agregarlo solo en una. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
84506a98e2
commit
c69b904b03
@@ -61,6 +61,8 @@ type umindTenantReq struct {
|
||||
Nombre string `json:"nombre"`
|
||||
DominiosPermitidos []string `json:"dominios_permitidos"`
|
||||
Activo bool `json:"activo"`
|
||||
ClienteID *uint `json:"cliente_id"`
|
||||
PlanID *uint `json:"plan_id"`
|
||||
}
|
||||
|
||||
func (r umindTenantReq) dominiosLimpios() []string {
|
||||
@@ -92,6 +94,8 @@ func CreateUmindTenantHandler(c *fiber.Ctx) error {
|
||||
DominiosPermitidos: strings.Join(dominios, ","),
|
||||
Activo: true,
|
||||
CreadoPorID: extraerUserID(c),
|
||||
ClienteID: req.ClienteID,
|
||||
PlanID: req.PlanID,
|
||||
}
|
||||
if err := models.CreateUmindTenant(tenant); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -113,6 +117,8 @@ func UpdateUmindTenantHandler(c *fiber.Ctx) error {
|
||||
"nombre": strings.TrimSpace(req.Nombre),
|
||||
"dominios_permitidos": strings.Join(dominios, ","),
|
||||
"activo": req.Activo,
|
||||
"cliente_id": req.ClienteID,
|
||||
"plan_id": req.PlanID,
|
||||
}
|
||||
if err := models.UpdateUmindTenant(uint(id), updates); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
// UmindPlanesPage renderiza la administración de planes de uMind (solo staff).
|
||||
func UmindPlanesPage(c *fiber.Ctx) error {
|
||||
return c.Render("umind_planes", fiber.Map{
|
||||
"user": c.Locals("user"),
|
||||
"modules": c.Locals("userModules"),
|
||||
}, "layouts/main")
|
||||
}
|
||||
|
||||
func GetUmindPlanesHandler(c *fiber.Ctx) error {
|
||||
items, err := models.GetUmindPlanes()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"items": items})
|
||||
}
|
||||
|
||||
type umindPlanReq struct {
|
||||
Nombre string `json:"nombre"`
|
||||
MaxAgentes int `json:"max_agentes"`
|
||||
PrecioMensual float64 `json:"precio_mensual"`
|
||||
PrecioPor1kTokens float64 `json:"precio_por_1k_tokens"`
|
||||
PrecioPorOCR float64 `json:"precio_por_ocr"`
|
||||
PrecioPorTranscripcion float64 `json:"precio_por_transcripcion"`
|
||||
Moneda string `json:"moneda"`
|
||||
TopeConsumoMensual float64 `json:"tope_consumo_mensual"`
|
||||
Activo bool `json:"activo"`
|
||||
}
|
||||
|
||||
func (r umindPlanReq) moneda() string {
|
||||
m := strings.ToUpper(strings.TrimSpace(r.Moneda))
|
||||
if len(m) != 3 {
|
||||
return "COP"
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func CreateUmindPlanHandler(c *fiber.Ctx) error {
|
||||
var req umindPlanReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
if strings.TrimSpace(req.Nombre) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"})
|
||||
}
|
||||
if req.MaxAgentes < 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "max_agentes no puede ser negativo"})
|
||||
}
|
||||
|
||||
plan := &models.UmindPlan{
|
||||
Nombre: strings.TrimSpace(req.Nombre),
|
||||
MaxAgentes: req.MaxAgentes,
|
||||
PrecioMensual: req.PrecioMensual,
|
||||
PrecioPor1kTokens: req.PrecioPor1kTokens,
|
||||
PrecioPorOCR: req.PrecioPorOCR,
|
||||
PrecioPorTranscripcion: req.PrecioPorTranscripcion,
|
||||
Moneda: req.moneda(),
|
||||
TopeConsumoMensual: req.TopeConsumoMensual,
|
||||
Activo: true,
|
||||
}
|
||||
if err := models.CreateUmindPlan(plan); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": plan.ID})
|
||||
}
|
||||
|
||||
func UpdateUmindPlanHandler(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
var req umindPlanReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
if req.MaxAgentes < 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "max_agentes no puede ser negativo"})
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"nombre": strings.TrimSpace(req.Nombre),
|
||||
"max_agentes": req.MaxAgentes,
|
||||
"precio_mensual": req.PrecioMensual,
|
||||
"precio_por_1k_tokens": req.PrecioPor1kTokens,
|
||||
"precio_por_ocr": req.PrecioPorOCR,
|
||||
"precio_por_transcripcion": req.PrecioPorTranscripcion,
|
||||
"moneda": req.moneda(),
|
||||
"tope_consumo_mensual": req.TopeConsumoMensual,
|
||||
"activo": req.Activo,
|
||||
}
|
||||
if err := models.UpdateUmindPlan(uint(id), updates); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
func DeleteUmindPlanHandler(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := models.DeleteUmindPlan(uint(id)); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
Reference in New Issue
Block a user