changes
This commit is contained in:
117
src/lib/compliance/__tests__/m3-m5-m7.integration.test.ts
Normal file
117
src/lib/compliance/__tests__/m3-m5-m7.integration.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildM7Dataset, computeM3Counters } from "@/lib/compliance/m7";
|
||||
import { ProposalWorkflowStateSchema } from "@/lib/proposals/workflow-state";
|
||||
|
||||
function buildWorkflowWithDeadline(dateIso: string) {
|
||||
return ProposalWorkflowStateSchema.parse({
|
||||
step: 5,
|
||||
info: {
|
||||
title: "Licitacion infraestructura",
|
||||
issuingEntity: "Gobierno municipal",
|
||||
procedureType: "Publica",
|
||||
jurisdiction: "Estatal",
|
||||
state: "Nuevo Leon",
|
||||
municipality: "Monterrey",
|
||||
sector: "Obra publica",
|
||||
description: "Proceso de prueba",
|
||||
convocatoriaUrl: "https://example.com/convocatoria",
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
id: "req-critical",
|
||||
title: "Garantia critica",
|
||||
description: "Incumplimiento puede descalificar la propuesta",
|
||||
category: "legal",
|
||||
mandatory: true,
|
||||
source: "critical_requirement",
|
||||
status: "pending",
|
||||
note: "",
|
||||
evidences: [],
|
||||
},
|
||||
],
|
||||
technicalSections: [{ id: "tech-1", title: "Alcance tecnico", description: "Detalle tecnico", completed: true }],
|
||||
economicItems: [{ id: "eco-1", concept: "Servicio", unit: "lote", quantity: 1, unitPrice: 1000 }],
|
||||
milestones: [{ id: "mil-1", title: "Entrega", dateIso, location: "", note: "", source: "manual" }],
|
||||
signatureCompliance: {
|
||||
policyStatus: "condicionado",
|
||||
policyName: "Politica piloto",
|
||||
jurisdictionLabel: "Nuevo Leon",
|
||||
sourceUrl: "https://www.nl.gob.mx/",
|
||||
minimumEvidence: ["Acuse"],
|
||||
notes: "",
|
||||
validatedByLegal: false,
|
||||
},
|
||||
readyMarked: false,
|
||||
});
|
||||
}
|
||||
|
||||
describe("M3 -> M5 -> M7 integration", () => {
|
||||
it("combines M3 preferences with active M5 workflow into M7 KPI snapshot", () => {
|
||||
const now = new Date("2026-04-02T10:00:00.000Z");
|
||||
const m3 = computeM3Counters({
|
||||
totalOpenLicitations: 8,
|
||||
preferences: [{ status: "REVIEWED" }, { status: "INTERESTED" }, { status: "INTERESTED" }],
|
||||
activeLinked: 1,
|
||||
});
|
||||
|
||||
const dataset = buildM7Dataset({
|
||||
proposals: [
|
||||
{
|
||||
id: "proposal-1",
|
||||
title: "Propuesta M5 activa",
|
||||
status: "IN_PROGRESS",
|
||||
sourceLicitationId: "licit-1",
|
||||
workflowDraft: buildWorkflowWithDeadline("2026-04-03T09:00:00.000Z"),
|
||||
updatedAt: now,
|
||||
},
|
||||
],
|
||||
m3,
|
||||
dueVerifications: [],
|
||||
now,
|
||||
});
|
||||
|
||||
expect(dataset.kpis.activeLicitations).toBe(1);
|
||||
expect(dataset.m3States.consulted).toBe(1);
|
||||
expect(dataset.m3States.interested).toBe(2);
|
||||
expect(dataset.m3States.active).toBe(1);
|
||||
expect(dataset.tabs.checklist.length).toBe(1);
|
||||
expect(dataset.tabs.panelKpi.some((item) => item.label === "M3 Consultadas")).toBe(true);
|
||||
});
|
||||
|
||||
it("raises critical pending alert when high-risk pending items meet near deadlines", () => {
|
||||
const now = new Date("2026-04-02T10:00:00.000Z");
|
||||
const m3 = computeM3Counters({
|
||||
totalOpenLicitations: 3,
|
||||
preferences: [],
|
||||
activeLinked: 1,
|
||||
});
|
||||
|
||||
const dataset = buildM7Dataset({
|
||||
proposals: [
|
||||
{
|
||||
id: "proposal-critical",
|
||||
title: "Propuesta critica",
|
||||
status: "DRAFT",
|
||||
sourceLicitationId: "licit-2",
|
||||
workflowDraft: buildWorkflowWithDeadline("2026-04-02T12:00:00.000Z"),
|
||||
updatedAt: now,
|
||||
},
|
||||
],
|
||||
m3,
|
||||
dueVerifications: [
|
||||
{
|
||||
sourceId: "nl-reg",
|
||||
sourceTitle: "Reglamento NL",
|
||||
authorityName: "Gobierno NL",
|
||||
dueAt: "2026-04-02T11:00:00.000Z",
|
||||
overdue: false,
|
||||
},
|
||||
],
|
||||
now,
|
||||
});
|
||||
|
||||
expect(dataset.kpis.criticalPending).toBeGreaterThan(0);
|
||||
expect(dataset.tabs.alertas.some((item) => item.kind === "critical_requirement_pending")).toBe(true);
|
||||
expect(dataset.tabs.alertas.some((item) => item.kind === "deadline_soon")).toBe(true);
|
||||
});
|
||||
});
|
||||
124
src/lib/compliance/__tests__/m7.test.ts
Normal file
124
src/lib/compliance/__tests__/m7.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildM7Dataset, computeM3Counters } from "@/lib/compliance/m7";
|
||||
import { ProposalWorkflowStateSchema } from "@/lib/proposals/workflow-state";
|
||||
|
||||
function buildWorkflow(overrides?: Partial<ReturnType<typeof ProposalWorkflowStateSchema.parse>>) {
|
||||
const base = ProposalWorkflowStateSchema.parse({
|
||||
step: 5,
|
||||
info: {
|
||||
title: "Proceso de prueba",
|
||||
issuingEntity: "Entidad",
|
||||
procedureType: "Publica",
|
||||
jurisdiction: "Estatal",
|
||||
state: "Nuevo Leon",
|
||||
municipality: "Monterrey",
|
||||
sector: "Tecnologia",
|
||||
description: "Servicio",
|
||||
convocatoriaUrl: "https://example.com",
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
id: "req-1",
|
||||
title: "Requisito critico",
|
||||
description: "Debe cumplirse",
|
||||
category: "legal",
|
||||
mandatory: true,
|
||||
source: "critical_requirement",
|
||||
status: "pending",
|
||||
note: "",
|
||||
evidences: [],
|
||||
},
|
||||
],
|
||||
technicalSections: [
|
||||
{
|
||||
id: "tech-1",
|
||||
title: "Tecnica",
|
||||
description: "Seccion",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
economicItems: [
|
||||
{
|
||||
id: "eco-1",
|
||||
concept: "Concepto",
|
||||
unit: "pieza",
|
||||
quantity: 1,
|
||||
unitPrice: 100,
|
||||
},
|
||||
],
|
||||
milestones: [
|
||||
{
|
||||
id: "mil-1",
|
||||
title: "Entrega",
|
||||
dateIso: "2026-04-03T10:00:00.000Z",
|
||||
location: "",
|
||||
note: "",
|
||||
source: "manual",
|
||||
},
|
||||
],
|
||||
signatureCompliance: {
|
||||
policyStatus: "requiere_validacion_legal",
|
||||
policyName: "Politica",
|
||||
jurisdictionLabel: "Nuevo Leon",
|
||||
sourceUrl: "",
|
||||
minimumEvidence: ["Opinion legal"],
|
||||
notes: "",
|
||||
validatedByLegal: false,
|
||||
},
|
||||
readyMarked: false,
|
||||
});
|
||||
|
||||
return {
|
||||
...base,
|
||||
...(overrides ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildM7Dataset", () => {
|
||||
it("computes KPIs and tabs using M3+M5 inputs", () => {
|
||||
const now = new Date("2026-04-02T10:00:00.000Z");
|
||||
const m3 = computeM3Counters({
|
||||
totalOpenLicitations: 10,
|
||||
preferences: [{ status: "REVIEWED" }, { status: "INTERESTED" }],
|
||||
activeLinked: 2,
|
||||
});
|
||||
|
||||
const dataset = buildM7Dataset({
|
||||
proposals: [
|
||||
{
|
||||
id: "p-1",
|
||||
title: "Propuesta activa",
|
||||
status: "IN_PROGRESS",
|
||||
sourceLicitationId: "l-1",
|
||||
workflowDraft: buildWorkflow(),
|
||||
updatedAt: now,
|
||||
},
|
||||
{
|
||||
id: "p-2",
|
||||
title: "Archivada",
|
||||
status: "ARCHIVED",
|
||||
sourceLicitationId: "l-2",
|
||||
workflowDraft: buildWorkflow(),
|
||||
updatedAt: now,
|
||||
},
|
||||
],
|
||||
m3,
|
||||
dueVerifications: [
|
||||
{
|
||||
sourceId: "nl-1",
|
||||
sourceTitle: "Reglamento NL",
|
||||
authorityName: "Gobierno NL",
|
||||
dueAt: "2026-04-04T09:00:00.000Z",
|
||||
overdue: false,
|
||||
},
|
||||
],
|
||||
now,
|
||||
});
|
||||
|
||||
expect(dataset.kpis.activeLicitations).toBe(1);
|
||||
expect(dataset.kpis.upcoming7Days).toBeGreaterThanOrEqual(2);
|
||||
expect(dataset.kpis.criticalPending).toBeGreaterThan(0);
|
||||
expect(dataset.m3States.consulted).toBe(1);
|
||||
expect(dataset.m3States.interested).toBe(1);
|
||||
});
|
||||
});
|
||||
78
src/lib/compliance/__tests__/regulations.test.ts
Normal file
78
src/lib/compliance/__tests__/regulations.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
listRegulationChangeAlerts,
|
||||
resetRegulationsStateForTests,
|
||||
runPeriodicNormativeVerification,
|
||||
verifyOfficialNormativeSources,
|
||||
} from "@/lib/compliance/regulations";
|
||||
|
||||
function buildResponse(etag: string, status = 200) {
|
||||
return new Response("", {
|
||||
status,
|
||||
headers: {
|
||||
etag,
|
||||
"last-modified": "Thu, 02 Apr 2026 00:00:00 GMT",
|
||||
"content-length": "100",
|
||||
"content-type": "text/html",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("regulations verification", () => {
|
||||
beforeEach(async () => {
|
||||
await resetRegulationsStateForTests();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-04-02T10:00:00.000Z"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
await resetRegulationsStateForTests();
|
||||
});
|
||||
|
||||
it("detects source changes and emits regulation update alerts", async () => {
|
||||
const fetchMock = vi
|
||||
.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>()
|
||||
.mockResolvedValueOnce(buildResponse("v1"))
|
||||
.mockResolvedValueOnce(buildResponse("v2"));
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await verifyOfficialNormativeSources(["nl-reglamento-adquisiciones-estatal"]);
|
||||
const changed = await verifyOfficialNormativeSources(["nl-reglamento-adquisiciones-estatal"]);
|
||||
|
||||
expect(changed[0]?.status).toBe("warning");
|
||||
expect(changed[0]?.changed).toBe(true);
|
||||
|
||||
const alerts = await listRegulationChangeAlerts(new Date("2026-04-02T10:30:00.000Z"), 30);
|
||||
expect(alerts.some((item) => item.kind === "normative_regulation_update" && item.severity === "high")).toBe(true);
|
||||
});
|
||||
|
||||
it("records failed verifications and emits pending alerts", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>().mockRejectedValue(new Error("timeout")),
|
||||
);
|
||||
|
||||
const result = await verifyOfficialNormativeSources(["spgg-reglamento-adquisiciones"]);
|
||||
expect(result[0]?.status).toBe("failed");
|
||||
|
||||
const alerts = await listRegulationChangeAlerts(new Date("2026-04-02T11:00:00.000Z"), 30);
|
||||
expect(alerts.some((item) => item.kind === "normative_verification_pending")).toBe(true);
|
||||
});
|
||||
|
||||
it("runs periodic verification only when due", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>().mockResolvedValue(buildResponse("v1")));
|
||||
|
||||
const first = await runPeriodicNormativeVerification(new Date("2026-04-02T10:00:00.000Z"));
|
||||
expect(first.checked).toBe(true);
|
||||
expect(first.dueCount).toBeGreaterThan(0);
|
||||
expect(first.results.length).toBe(first.dueCount);
|
||||
|
||||
const second = await runPeriodicNormativeVerification(new Date("2026-04-02T10:05:00.000Z"));
|
||||
expect(second.checked).toBe(false);
|
||||
expect(second.dueCount).toBe(0);
|
||||
});
|
||||
});
|
||||
35
src/lib/compliance/__tests__/signature-policy.test.ts
Normal file
35
src/lib/compliance/__tests__/signature-policy.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { evaluateSignaturePolicy } from "@/lib/compliance/signature-policy";
|
||||
|
||||
describe("evaluateSignaturePolicy", () => {
|
||||
it("returns conservative municipal rule for San Pedro", () => {
|
||||
const evaluation = evaluateSignaturePolicy({
|
||||
stateCode: "NL",
|
||||
municipalityName: "San Pedro Garza Garcia",
|
||||
documentType: "BASES_LICITACION",
|
||||
});
|
||||
|
||||
expect(evaluation.policyStatus).toBe("requiere_validacion_legal");
|
||||
expect(evaluation.evidenceRequired.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("returns conditioned rule for Nuevo Leon state scope", () => {
|
||||
const evaluation = evaluateSignaturePolicy({
|
||||
stateCode: "NL",
|
||||
municipalityName: "Monterrey",
|
||||
documentType: "CONVOCATORIA",
|
||||
});
|
||||
|
||||
expect(evaluation.policyStatus).toBe("condicionado");
|
||||
expect(evaluation.policyName.toLowerCase()).toContain("firma electronica");
|
||||
});
|
||||
|
||||
it("defaults to legal validation when jurisdiction is unknown", () => {
|
||||
const evaluation = evaluateSignaturePolicy({
|
||||
stateName: "Estado no configurado",
|
||||
municipalityName: "Municipio X",
|
||||
});
|
||||
|
||||
expect(evaluation.policyStatus).toBe("requiere_validacion_legal");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user