feat: mis cobranzas, recordatorios de pago y agente IA con acciones

- PlanPago.vendedorId + filtro ?mios=1; toggle Mis planes/Todos en /pay
- Motor de recordatorios: próximos a vencer (3d) y vencidos (re-cada 7d),
  canal Monday Actividad + stub WhatsApp listo para WA CRM, log persistente,
  cron diario 9:30, run manual con dry-run, historial en /api/cobranza/recordatorios
- Agente IA: memoria por usuario (conversaciones aisladas) y tool calling:
  cotizar_unidad (motor real por desarrollo) y buscar_unidad (inventario Monday)
- Tests: api 261, cobranza 75, agente-ia 13
This commit is contained in:
2026-08-28 08:07:12 +00:00
parent 59944668a8
commit aa27e89c75
18 changed files with 1437 additions and 17 deletions

View File

@@ -58,6 +58,7 @@ export function aDominio(plan: PlanCompleto): PlanPagoDominio {
tasaMoraMensual: plan.tasaMoraMensual,
status: plan.status === 'CANCELADO' ? 'CANCELADO' : 'ACTIVO',
mondayItemId: plan.mondayItemId,
vendedorId: plan.vendedorId,
createdAt: plan.createdAt.toISOString(),
pagos: plan.pagos
.slice()
@@ -142,6 +143,8 @@ export async function crearPlan(req: Request, res: Response): Promise<void> {
mensualidad: calendario.mensualidad,
tasaMoraMensual: body.tasaMoraMensual ?? 0,
mondayItemId: body.mondayItemId ?? null,
// Dueño del plan: sub del JWT (null si AUTH_ENABLED=false o token sin sub)
vendedorId: req.usuarioToken?.sub ?? null,
pagos: {
create: calendario.pagos.map((p) => ({
numPago: p.numPago,
@@ -155,9 +158,29 @@ export async function crearPlan(req: Request, res: Response): Promise<void> {
res.status(201).json(conCalculados(aDominio(plan)))
}
/** GET /api/planes — con saldo/status calculados */
export async function listarPlanes(_req: Request, res: Response): Promise<void> {
// Roles con vista global de cobranza: el filtro ?mios=1 no les aplica
const ROLES_VEN_TODO: readonly string[] = ['admin', 'coordinador']
/**
* Filtro "mis cobranzas": con ?mios=1 un vendedor solo ve sus planes
* (vendedorId = sub del token). Admin/coordinador ven todo aunque pidan mios;
* sin token (AUTH_ENABLED=false) el filtro no aplica.
*/
export function resolverFiltroMios(
mios: unknown,
token: { sub: string; rol: string } | undefined,
): { vendedorId: string } | undefined {
if (mios !== '1') return undefined
if (!token) return undefined
if (ROLES_VEN_TODO.includes(token.rol)) return undefined
return { vendedorId: token.sub }
}
/** GET /api/planes — con saldo/status calculados; ?mios=1 filtra los del vendedor */
export async function listarPlanes(req: Request, res: Response): Promise<void> {
const where = resolverFiltroMios(req.query['mios'], req.usuarioToken)
const planes = await prisma.planPago.findMany({
...(where ? { where } : {}),
include: includeCompleto,
orderBy: { createdAt: 'desc' },
})