Files
FRASA-CAS/api-gateway/src/controllers/borradores.controller.ts
Consult AS Technology 59944668a8 feat(alta): borrador, pre-llenado desde cotización y liga remota de firma
- Borradores por vendedor (guardar/continuar/eliminar), se limpia al registrar
- Pre-llenado con ?cotizacion=<id> (cliente, contacto, proyecto, montos);
  botón 'Iniciar alta' en Mis cotizaciones del cotizador
- Liga remota pública /client/firma/<token> (JWT scope firma-remota, 72h,
  rate limit, solo datos mínimos sin PLD): firma digital + subida de
  documentos que prende checkboxes en Monday Apartado
- Tokens de liga no sirven como sesión y viceversa
- Tests: api 228, alta 82, cotizador 82
2026-08-28 07:29:22 +00:00

82 lines
3.2 KiB
TypeScript

import type { Request, Response } from 'express'
import { z } from 'zod'
import { prisma } from '../services/prisma.js'
import { AppError, NotFoundError } from '../utils/errors.js'
// ---------- Borradores del alta de clientes ----------
const guardarBorradorSchema = z.object({
id: z.string().min(1).optional(),
nombre: z.string().max(200).optional(),
paso: z.number().int().min(0).max(50).default(0),
// Estado del wizard serializado (objeto JSON cualquiera; sin Files)
datos: z.unknown(),
})
/** sub del JWT; con AUTH_ENABLED=false (tests) cae a un id fijo */
const usuarioDe = (req: Request): string => req.usuarioToken?.sub ?? 'anon'
const esAdmin = (req: Request): boolean => req.usuarioToken?.rol === 'admin'
/** 404 si no existe, 403 si es de otro usuario (salvo admin) */
async function borradorPropio(req: Request, id: string) {
const borrador = await prisma.borradorAlta.findUnique({ where: { id } })
if (!borrador) throw new NotFoundError('Borrador no encontrado')
if (borrador.usuarioId !== usuarioDe(req) && !esAdmin(req)) {
throw new AppError(403, 'El borrador pertenece a otro usuario')
}
return borrador
}
// Etiqueta visible: nombre del cliente dentro del estado, o "Sin nombre"
function etiquetaDe(datos: unknown, nombreExplicito: string | undefined): string {
if (nombreExplicito?.trim()) return nombreExplicito.trim()
const dg = (datos as { datosGenerales?: { nombreCompleto?: unknown } } | null)?.datosGenerales
const nombre = typeof dg?.nombreCompleto === 'string' ? dg.nombreCompleto.trim() : ''
return nombre || 'Sin nombre'
}
/** GET /api/borradores — lista los míos, SIN datosJson (metadatos) */
export async function listarBorradores(req: Request, res: Response): Promise<void> {
const borradores = await prisma.borradorAlta.findMany({
where: { usuarioId: usuarioDe(req) },
select: { id: true, nombre: true, paso: true, createdAt: true, updatedAt: true },
orderBy: { updatedAt: 'desc' },
})
res.json(borradores)
}
/** POST /api/borradores — upsert: con `id` actualiza (si es mío), sin `id` crea */
export async function guardarBorrador(req: Request, res: Response): Promise<void> {
const body = guardarBorradorSchema.parse(req.body)
const datosJson = JSON.stringify(body.datos ?? null)
const nombre = etiquetaDe(body.datos, body.nombre)
if (body.id) {
await borradorPropio(req, body.id)
const actualizado = await prisma.borradorAlta.update({
where: { id: body.id },
data: { nombre, datosJson, paso: body.paso },
})
res.json(actualizado)
return
}
const creado = await prisma.borradorAlta.create({
data: { usuarioId: usuarioDe(req), nombre, datosJson, paso: body.paso },
})
res.status(201).json(creado)
}
/** GET /api/borradores/:id — borrador completo (solo el dueño o admin) */
export async function obtenerBorrador(req: Request, res: Response): Promise<void> {
const borrador = await borradorPropio(req, String(req.params['id']))
res.json(borrador)
}
/** DELETE /api/borradores/:id — solo el dueño o admin */
export async function eliminarBorrador(req: Request, res: Response): Promise<void> {
const borrador = await borradorPropio(req, String(req.params['id']))
await prisma.borradorAlta.delete({ where: { id: borrador.id } })
res.status(204).end()
}