feat: consolidations, aduanero portal, payments admin, bug fixes, tests
## Bugs corregidos - bodega/page.tsx: estados correctos §08 (RECIBIDO_BODEGA, EN_VERIFICACION, EN_TRANSITO_ECUADOR) - admin/page.tsx: carga paquetes reales, alertas de incidencias/B2B/cobros pendientes, barras de estado ## Nuevos portales y páginas - /aduanero/ — portal propio para AGENTE_ADUANERO (layout + dashboard §11 + declaraciones DSI) - /aduanero/declaraciones — cola de declaraciones con formulario DSI y detección DAI automática - /admin/pagos — gestión de cobros: KPIs, filtros por estado, tabla con breakdown - /portal/consolidacion — cliente crea/gestiona consolidaciones §21 - /bodega/consolidacion — operador cierra y despacha consolidaciones (→ EN_TRANSITO_ECUADOR) ## API nueva (ConsolidationsModule) - GET/POST /consolidations - GET /consolidations/:id - POST /consolidations/:id/packages - DELETE /consolidations/:id/packages/:packageId - POST /consolidations/:id/close - POST /consolidations/:id/dispatch (→ actualiza paquetes a EN_TRANSITO_ECUADOR) ## Prisma schema v0.5 - ConsolidationStatus enum (ABIERTA, CERRADA, DESPACHADA, ENTREGADA, CANCELADA) - Consolidation model con totales calculados (totalWeightLb, totalValue) - ConsolidationPackage (tabla intermedia, un paquete = una consolidación) - db push aplicado a remote DB (46.202.93.92) ## Navegación - Login: AGENTE_ADUANERO → /aduanero (ya no /bodega) - Bodega nav: añadido Consolidaciones - Portal nav: añadido Consolidar - Admin nav: añadido Cobros ## Tests (104 total, 8 suites) - payments.service.spec.ts: 16 tests (createIntent, confirm, list, findByPackageForUser) - notifications.service.spec.ts: 12 tests (getTemplates, updateTemplate, seedDefaultTemplates, notifyStatusChange, findByUser) ## Legal §21 - Registro: aviso LOPDP Ecuador + normativa NJ en footer del formulario - Landing footer: aviso detallado de protección de datos LOPDP/NJ ## api.ts: consolidations.* client methods
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { NotFoundException } from "@nestjs/common";
|
||||
import { NotificationsService } from "./notifications.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
|
||||
const mockTemplate = {
|
||||
id: "tpl-1",
|
||||
tenantId: "tenant-1",
|
||||
event: "REGISTRADO",
|
||||
channel: "EMAIL",
|
||||
subject: "Tu paquete {{trackingId}} fue registrado",
|
||||
body: "Hola {{firstName}}, tu paquete {{trackingId}} fue registrado.",
|
||||
isActive: true,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockPackage = {
|
||||
id: "pkg-1",
|
||||
tenantId: "tenant-1",
|
||||
userId: "user-1",
|
||||
trackingId: "EC-20260601-ABCDEF",
|
||||
status: "REGISTRADO",
|
||||
};
|
||||
|
||||
const mockUser = { id: "user-1", firstName: "Juan", lastName: "Pérez", suite: { code: "EC-00001" } };
|
||||
|
||||
const mockPrisma = {
|
||||
client: {
|
||||
notificationTemplate: {
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
},
|
||||
notification: {
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateMany: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("NotificationsService", () => {
|
||||
let service: NotificationsService;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
NotificationsService,
|
||||
{ provide: PrismaService, useValue: mockPrisma },
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<NotificationsService>(NotificationsService);
|
||||
});
|
||||
|
||||
// ─── getTemplates ────────────────────────────────────────────
|
||||
|
||||
describe("getTemplates", () => {
|
||||
it("devuelve plantillas del tenant", async () => {
|
||||
mockPrisma.client.notificationTemplate.findMany.mockResolvedValue([mockTemplate]);
|
||||
const result = await service.getTemplates("tenant-1");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(mockPrisma.client.notificationTemplate.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { tenantId: "tenant-1" } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── updateTemplate ──────────────────────────────────────────
|
||||
|
||||
describe("updateTemplate", () => {
|
||||
it("actualiza una plantilla correctamente", async () => {
|
||||
mockPrisma.client.notificationTemplate.findUnique.mockResolvedValue(mockTemplate);
|
||||
mockPrisma.client.notificationTemplate.update.mockResolvedValue({ ...mockTemplate, body: "Nuevo cuerpo" });
|
||||
const result = await service.updateTemplate("tpl-1", "tenant-1", "Nuevo cuerpo", undefined, true);
|
||||
expect(mockPrisma.client.notificationTemplate.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { id: "tpl-1" } })
|
||||
);
|
||||
});
|
||||
|
||||
it("lanza NotFoundException si la plantilla no existe", async () => {
|
||||
mockPrisma.client.notificationTemplate.findUnique.mockResolvedValue(null);
|
||||
await expect(service.updateTemplate("invalid", "tenant-1", "body"))
|
||||
.rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it("lanza NotFoundException si el tenantId no coincide", async () => {
|
||||
mockPrisma.client.notificationTemplate.findUnique.mockResolvedValue({ ...mockTemplate, tenantId: "otro-tenant" });
|
||||
await expect(service.updateTemplate("tpl-1", "tenant-1", "body"))
|
||||
.rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── seedDefaultTemplates ───────────────────────────────────
|
||||
|
||||
describe("seedDefaultTemplates", () => {
|
||||
it("crea upserts para 11 eventos × 3 canales = 33 plantillas", async () => {
|
||||
mockPrisma.client.notificationTemplate.upsert.mockResolvedValue(mockTemplate);
|
||||
const result = await service.seedDefaultTemplates("tenant-1");
|
||||
expect(result.seeded).toBe(33);
|
||||
expect(mockPrisma.client.notificationTemplate.upsert).toHaveBeenCalledTimes(33);
|
||||
});
|
||||
|
||||
it("es idempotente — usa upsert con update vacío", async () => {
|
||||
mockPrisma.client.notificationTemplate.upsert.mockResolvedValue(mockTemplate);
|
||||
await service.seedDefaultTemplates("tenant-1");
|
||||
const call = mockPrisma.client.notificationTemplate.upsert.mock.calls[0][0];
|
||||
expect(call.update).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── notifyStatusChange ──────────────────────────────────────
|
||||
|
||||
describe("notifyStatusChange", () => {
|
||||
beforeEach(() => {
|
||||
mockPrisma.client.notificationTemplate.findUnique.mockResolvedValue(null); // usa fallback
|
||||
mockPrisma.client.notification.create.mockResolvedValue({ id: "notif-1" });
|
||||
mockPrisma.client.notification.update.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("crea notificaciones para los 3 canales por defecto", async () => {
|
||||
await service.notifyStatusChange(mockPackage, mockUser);
|
||||
expect(mockPrisma.client.notification.create).toHaveBeenCalledTimes(3); // EMAIL, WHATSAPP, PUSH
|
||||
});
|
||||
|
||||
it("interpola {{trackingId}} y {{firstName}} en el body", async () => {
|
||||
mockPrisma.client.notificationTemplate.findUnique.mockResolvedValue({
|
||||
...mockTemplate,
|
||||
body: "Hola {{firstName}}, tu paquete es {{trackingId}}.",
|
||||
});
|
||||
await service.notifyStatusChange(mockPackage, mockUser);
|
||||
const createCall = mockPrisma.client.notification.create.mock.calls[0][0];
|
||||
expect(createCall.data.body).toContain("Juan");
|
||||
expect(createCall.data.body).toContain("EC-20260601-ABCDEF");
|
||||
});
|
||||
|
||||
it("no envía notificación si la plantilla está inactiva", async () => {
|
||||
mockPrisma.client.notificationTemplate.findUnique.mockResolvedValue({ ...mockTemplate, isActive: false });
|
||||
await service.notifyStatusChange(mockPackage, mockUser);
|
||||
// Solo EMAIL está deshabilitado, WHATSAPP y PUSH usan fallback (activo)
|
||||
// El mock devuelve el template inactivo para todos
|
||||
expect(mockPrisma.client.notification.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("no lanza excepción si la creación de notificación falla", async () => {
|
||||
mockPrisma.client.notification.create.mockRejectedValue(new Error("DB error"));
|
||||
await expect(service.notifyStatusChange(mockPackage, mockUser)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── findByUser ──────────────────────────────────────────────
|
||||
|
||||
describe("findByUser", () => {
|
||||
it("devuelve notificaciones del usuario ordenadas por fecha", async () => {
|
||||
mockPrisma.client.notification.findMany.mockResolvedValue([]);
|
||||
await service.findByUser("user-1");
|
||||
expect(mockPrisma.client.notification.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { userId: "user-1" },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 20,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user