Skip to content

Commit 7af09b0

Browse files
committed
Anulación fase 1: migración v42 + tipos de cola + payloads RNDC (9/28/29/32/54).
Cimientos para anular despachos ante el RNDC (el reverso LIFO de la creación): - migracion_v42: columnas de motivo/observaciones/rndc_id/auditoría en manifiesto y remesa; documenta los estados anulacion_pendiente/anulado y el retorno de cumplido a 'pendiente' al anular el cumplido (re-cumplible). - Cola: tipos anular_* en PROCESO (9/28/29/32/54) y ORDEN (reverso 70-100); el 54 (cumplido inicial) queda sin slot fijo porque se inyecta de forma reactiva. - Payloads puros para los 5 procesos, con test de XML exacto byte-a-byte contra el formato de referencia del Ministerio (35 tests en verde). Sin orquestación ni UI todavía (fases 2 y 3).
1 parent 189d69b commit 7af09b0

3 files changed

Lines changed: 195 additions & 1 deletion

File tree

backend/src/modules/cola/cola.repo.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,28 @@ type Row = Record<string, any>;
3333
/** TTL for the nav-badge counts: high-frequency polls collapse to one query per window. */
3434
const BADGE_TTL_MS = 15_000;
3535

36-
/** Send order per document type. */
36+
/**
37+
* Send order per document type. Anulación is the reverse of creation (LIFO):
38+
* you can only "pop" the top of the stack, so the anular_* orders run after the
39+
* forward flow and in reverse (cumplido manifiesto → cumplido remesa →
40+
* manifiesto → remesa). The `dependenciaPendiente()` check enforces this.
41+
*
42+
* anular_cumplido_inicial_remesa (proc 54) has no fixed slot: it is injected
43+
* reactively at runtime with an orden just below the step that the RNDC says
44+
* needs it — see the remediation logic in Phase 2.
45+
*/
3746
const ORDEN: Record<string, number> = {
3847
tercero: 10,
3948
vehiculo: 20,
4049
remesa: 30,
4150
manifiesto: 40,
4251
cumplido_remesa: 50,
4352
cumplido_manifiesto: 60,
53+
anular_cumplido_manifiesto: 70,
54+
anular_cumplido_remesa: 80,
55+
anular_cumplido_inicial_remesa: 85, // nominal; real orden set on reactive injection
56+
anular_manifiesto: 90,
57+
anular_remesa: 100,
4458
};
4559
/** RNDC proceso per document type. */
4660
const PROCESO: Record<string, number> = {
@@ -50,6 +64,11 @@ const PROCESO: Record<string, number> = {
5064
manifiesto: 4,
5165
cumplido_remesa: 5,
5266
cumplido_manifiesto: 6,
67+
anular_cumplido_manifiesto: 29,
68+
anular_cumplido_remesa: 28,
69+
anular_cumplido_inicial_remesa: 54,
70+
anular_manifiesto: 32,
71+
anular_remesa: 9,
5372
};
5473

5574
async function fila(exec: Queryable, sql: string, params: unknown[]): Promise<Row | null> {
@@ -622,6 +641,84 @@ async function payloadCumplidoManifiesto(m: Row): Promise<string> {
622641
return xml;
623642
}
624643

644+
// ---------- Anulación payloads (RNDC procesos 9 / 28 / 29 / 32 / 54) ----------
645+
//
646+
// Pure builders (NIT + motivo + observaciones as params) so they can be
647+
// characterization-tested for exact XML without a DB. Motivo codes differ per
648+
// proceso: cumplidos (28/29/54) usan D/O; manifiesto (32) D/S/R; remesa (9) D/S.
649+
// OBSERVACIONES es opcional en el RNDC pero siempre se emite (aunque vacía) para
650+
// calzar con los XML de referencia del Ministerio.
651+
652+
/** Always-present <OBSERVACIONES> tag (matches the RNDC reference XMLs). */
653+
function tagObservaciones(obs: string): string {
654+
return '<OBSERVACIONES>' + RndcClient.escaparXml(obs ?? '') + '</OBSERVACIONES>';
655+
}
656+
657+
/** procesoid 29 — Anular Cumplido de Manifiesto. */
658+
export function payloadAnularCumplidoManifiesto(nit: string, numManifiesto: string, motivo: string, obs = ''): string {
659+
const vars: RndcVars = {
660+
NUMNITEMPRESATRANSPORTE: nit,
661+
NUMMANIFIESTOCARGA: numManifiesto,
662+
CODMOTIVOANULACIONCUMPLIDO: motivo, // D / O
663+
};
664+
return RndcClient.renderVariables(vars) + tagObservaciones(obs);
665+
}
666+
667+
/** procesoid 28 — Anular Cumplido de Remesa. */
668+
export function payloadAnularCumplidoRemesa(nit: string, numRemesa: unknown, motivo: string, obs = ''): string {
669+
const vars: RndcVars = {
670+
NUMNITEMPRESATRANSPORTE: nit,
671+
CONSECUTIVOREMESA: consecutivoRemesaRndc(numRemesa),
672+
CODMOTIVOANULACIONCUMPLIDO: motivo, // D / O
673+
};
674+
return RndcClient.renderVariables(vars) + tagObservaciones(obs);
675+
}
676+
677+
/**
678+
* procesoid 54 — Anular Cumplido Inicial de Remesa. Se envía sólo de forma
679+
* reactiva: cuando el RNDC rechaza un anular_cumplido_remesa / anular_remesa
680+
* exigiendo que primero se anule el cumplido inicial (ver remediación, Fase 2).
681+
*/
682+
export function payloadAnularCumplidoInicialRemesa(
683+
nit: string,
684+
numRemesa: unknown,
685+
numManifiesto: string,
686+
motivo: string,
687+
obs = '',
688+
): string {
689+
const vars: RndcVars = {
690+
NUMNITEMPRESATRANSPORTE: nit,
691+
CONSECUTIVOREMESA: consecutivoRemesaRndc(numRemesa),
692+
NUMMANIFIESTOCARGA: numManifiesto,
693+
CODMOTIVOANULACIONCUMPLIDO: motivo, // D / O
694+
};
695+
return RndcClient.renderVariables(vars) + tagObservaciones(obs);
696+
}
697+
698+
/** procesoid 32 — Anular Manifiesto de Carga. */
699+
export function payloadAnularManifiesto(nit: string, numManifiesto: string, motivo: string, obs = ''): string {
700+
const vars: RndcVars = {
701+
NUMNITEMPRESATRANSPORTE: nit,
702+
NUMMANIFIESTOCARGA: numManifiesto,
703+
MOTIVOANULACIONMANIFIESTO: motivo, // D / S / R
704+
};
705+
return RndcClient.renderVariables(vars) + tagObservaciones(obs);
706+
}
707+
708+
/**
709+
* procesoid 9 — Anular Remesa Terrestre de Carga. MOTIVOREVERSAREMESA='A'
710+
* (anular; 'L' sería liberar para transbordo, que no usamos aquí).
711+
*/
712+
export function payloadAnularRemesa(nit: string, numRemesa: unknown, motivo: string, obs = ''): string {
713+
const vars: RndcVars = {
714+
NUMNITEMPRESATRANSPORTE: nit,
715+
CONSECUTIVOREMESA: consecutivoRemesaRndc(numRemesa),
716+
MOTIVOREVERSAREMESA: 'A',
717+
MOTIVOANULACIONREMESA: motivo, // D / S
718+
};
719+
return RndcClient.renderVariables(vars) + tagObservaciones(obs);
720+
}
721+
625722
// ---------- Reads for the monitor ----------
626723

627724
const FILTROS: Record<string, string[]> = {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Exact-XML characterization tests for the anulación payload builders
3+
* (RNDC procesos 9 / 28 / 29 / 32 / 54). Byte-for-byte so a change in tag
4+
* order/content is caught — these go to the RNDC and are irreversible.
5+
*/
6+
7+
import { describe, it, expect } from 'vitest';
8+
import {
9+
payloadAnularCumplidoManifiesto,
10+
payloadAnularCumplidoRemesa,
11+
payloadAnularCumplidoInicialRemesa,
12+
payloadAnularManifiesto,
13+
payloadAnularRemesa,
14+
} from '../src/modules/cola/cola.repo.js';
15+
16+
const NIT = '8190041165';
17+
const MANIF = '0101211090';
18+
19+
describe('anulación payloads', () => {
20+
it('29 — anular cumplido manifiesto', () => {
21+
expect(payloadAnularCumplidoManifiesto(NIT, MANIF, 'D', 'Error de digitación')).toBe(
22+
'<NUMNITEMPRESATRANSPORTE>8190041165</NUMNITEMPRESATRANSPORTE>' +
23+
'<NUMMANIFIESTOCARGA>0101211090</NUMMANIFIESTOCARGA>' +
24+
'<CODMOTIVOANULACIONCUMPLIDO>D</CODMOTIVOANULACIONCUMPLIDO>' +
25+
'<OBSERVACIONES>Error de digitación</OBSERVACIONES>',
26+
);
27+
});
28+
29+
it('28 — anular cumplido remesa (num_remesa se formatea a consecutivo de 10 dígitos)', () => {
30+
expect(payloadAnularCumplidoRemesa(NIT, 'REM-00003', 'O', '')).toBe(
31+
'<NUMNITEMPRESATRANSPORTE>8190041165</NUMNITEMPRESATRANSPORTE>' +
32+
'<CONSECUTIVOREMESA>0000000003</CONSECUTIVOREMESA>' +
33+
'<CODMOTIVOANULACIONCUMPLIDO>O</CODMOTIVOANULACIONCUMPLIDO>' +
34+
'<OBSERVACIONES></OBSERVACIONES>',
35+
);
36+
});
37+
38+
it('54 — anular cumplido inicial remesa (incluye NUMMANIFIESTOCARGA)', () => {
39+
expect(payloadAnularCumplidoInicialRemesa(NIT, '3', MANIF, 'D', 'x')).toBe(
40+
'<NUMNITEMPRESATRANSPORTE>8190041165</NUMNITEMPRESATRANSPORTE>' +
41+
'<CONSECUTIVOREMESA>0000000003</CONSECUTIVOREMESA>' +
42+
'<NUMMANIFIESTOCARGA>0101211090</NUMMANIFIESTOCARGA>' +
43+
'<CODMOTIVOANULACIONCUMPLIDO>D</CODMOTIVOANULACIONCUMPLIDO>' +
44+
'<OBSERVACIONES>x</OBSERVACIONES>',
45+
);
46+
});
47+
48+
it('32 — anular manifiesto', () => {
49+
expect(payloadAnularManifiesto(NIT, MANIF, 'S', '')).toBe(
50+
'<NUMNITEMPRESATRANSPORTE>8190041165</NUMNITEMPRESATRANSPORTE>' +
51+
'<NUMMANIFIESTOCARGA>0101211090</NUMMANIFIESTOCARGA>' +
52+
'<MOTIVOANULACIONMANIFIESTO>S</MOTIVOANULACIONMANIFIESTO>' +
53+
'<OBSERVACIONES></OBSERVACIONES>',
54+
);
55+
});
56+
57+
it('9 — anular remesa (MOTIVOREVERSAREMESA=A, no L)', () => {
58+
expect(payloadAnularRemesa(NIT, 'REM-00003', 'D', '')).toBe(
59+
'<NUMNITEMPRESATRANSPORTE>8190041165</NUMNITEMPRESATRANSPORTE>' +
60+
'<CONSECUTIVOREMESA>0000000003</CONSECUTIVOREMESA>' +
61+
'<MOTIVOREVERSAREMESA>A</MOTIVOREVERSAREMESA>' +
62+
'<MOTIVOANULACIONREMESA>D</MOTIVOANULACIONREMESA>' +
63+
'<OBSERVACIONES></OBSERVACIONES>',
64+
);
65+
});
66+
67+
it('escapa caracteres XML en las observaciones', () => {
68+
expect(payloadAnularManifiesto(NIT, MANIF, 'D', 'Ruta A & B <x>')).toContain(
69+
'<OBSERVACIONES>Ruta A &amp; B &lt;x&gt;</OBSERVACIONES>',
70+
);
71+
});
72+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-- migracion_v42_anulacion.sql
2+
-- Anulación de despachos ante el RNDC (procesos 9/28/29/32, y 54 reactivo).
3+
--
4+
-- Máquina de estados (estado_rndc es VARCHAR, no hace falta cambiar enum):
5+
-- remesa/manifiesto.estado_rndc: aceptado -> anulacion_pendiente -> anulado
6+
-- remesa/manifiesto.cumplido_estado_rndc: aceptado -> (anular cumplido) -> pendiente
7+
-- (al anular el cumplido vuelve a 'pendiente' para poder re-cumplir y reenviar)
8+
--
9+
-- Columnas de auditoría/motivo de la anulación del documento base (procs 32/9).
10+
-- Los números de aceptación RNDC de los pasos intermedios (anular cumplido, etc.)
11+
-- quedan visibles en cola_envios.rndc_ingreso_id (monitor de la página Cola).
12+
13+
ALTER TABLE manifiesto
14+
ADD COLUMN IF NOT EXISTS anulacion_motivo VARCHAR(1) NULL COMMENT 'D=digitación S=cancelación R=cambio remesas',
15+
ADD COLUMN IF NOT EXISTS anulacion_observaciones VARCHAR(255) NULL,
16+
ADD COLUMN IF NOT EXISTS anulacion_rndc_id VARCHAR(40) NULL COMMENT 'ingreso RNDC de la anulación (proc 32)',
17+
ADD COLUMN IF NOT EXISTS anulado_por BIGINT UNSIGNED NULL COMMENT 'staff_users.id (auditoría, sin FK)',
18+
ADD COLUMN IF NOT EXISTS anulado_at DATETIME NULL;
19+
20+
ALTER TABLE remesa
21+
ADD COLUMN IF NOT EXISTS anulacion_motivo VARCHAR(1) NULL COMMENT 'D=digitación S=cancelación',
22+
ADD COLUMN IF NOT EXISTS anulacion_observaciones VARCHAR(255) NULL,
23+
ADD COLUMN IF NOT EXISTS anulacion_rndc_id VARCHAR(40) NULL COMMENT 'ingreso RNDC de la anulación (proc 9)',
24+
ADD COLUMN IF NOT EXISTS anulado_por BIGINT UNSIGNED NULL COMMENT 'staff_users.id (auditoría, sin FK)',
25+
ADD COLUMN IF NOT EXISTS anulado_at DATETIME NULL;

0 commit comments

Comments
 (0)