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
This commit is contained in:
81
api-gateway/src/controllers/borradores.controller.ts
Normal file
81
api-gateway/src/controllers/borradores.controller.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user