feat: audit & sync with documentacion.html v1.1 — schema, tests, modules, web
Schema (packages/database/prisma/schema.prisma): - Add SenaeCategory, B2BRequestStatus, NotificationChannel enums - Add Package fields: lengthCm, widthCm, heightCm, hasDiscrepancy, photos[], senaeCategory, senaeAuthNumber, senaeDeclarationId - Add userId field to PreAlert - Add RefreshToken model (JWT auth — Fase 1) - Add Tariff model (configurable rates: pricePerLb, insurancePct, etc.) - Add B2BRequest model (heavy cargo imports — doc §13) - Add Notification model (multi-channel: email, whatsapp, sms, push — doc §16) - Add userAgent to AuditLog API (apps/api): - Add CalculatorModule: calculates SENAE cost breakdown (flete, seguro, FODINFA, arancel, IVA) per formulas in doc §15 - Add TrackingModule: public GET /api/tracking/:id endpoint (doc §07) - Register both modules in AppModule - Add Jest config + test scripts (test, test:watch, test:cov, test:ci) Tests (80 tests, 97.45% coverage): - calculator.util.spec.ts: 35 tests — SENAE formulas, volumetric weight, 4x4 regime, tariff rates, error validation, breakdown - tracking-id.util.spec.ts: 13 tests — format, uniqueness, validation - suite-code.util.spec.ts: 13 tests — generation, parsing, address builder - health.controller.spec.ts: 5 tests — ok/degraded/timestamp - calculator.spec.ts: 8 tests — controller + service unit tests - tracking.controller.spec.ts: 9 tests — public search, 404, no sensitive data Web (apps/web): - page.tsx: Full landing page matching documentation (Hero, Services, 8 Modules, Tracking states, SENAE calculator preview, 6 Roles, Companies) - /calculadora: SENAE categories + API usage guide - /tracking: Tracking ID format + 11 states reference Seed (packages/database/prisma/seed.ts): - Creates all 6 roles (SUPER_ADMIN, ADMIN_EMPRESA, OPERADOR_BODEGA, AGENTE_ADUANERO, CLIENTE, SOPORTE) - Assigns Suite EC-00001 to demo client - Creates demo Package EC-20260506-000001 with status history - Creates demo PreAlert and B2BRequest .env.example: Added all 9 integrations from doc §18: Amazon SP-API, SENAE WebService, Stripe/PayPhone, WhatsApp Business API, SendGrid/SES, SMS/Twilio, FedEx/DHL/UPS couriers, S3/MinIO, Sentry migration.sql: Updated to reflect all new models and fields turbo.json + package.json: Added test, test:cov, test:ci tasks + db:seed script
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { TrackingController } from "./tracking.controller";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { NotFoundException } from "@nestjs/common";
|
||||
|
||||
describe("TrackingController (doc §07 — búsqueda pública sin login)", () => {
|
||||
let controller: TrackingController;
|
||||
let prismaService: { client: { package: { findUnique: jest.Mock } } };
|
||||
|
||||
const mockPackage = {
|
||||
trackingId: "EC-20260506-082341",
|
||||
status: "REGISTRADO",
|
||||
description: "Tenis Nike Air Max",
|
||||
store: "Amazon",
|
||||
vendorTracking: "1Z999AA10123456784",
|
||||
senaeCategory: "REGIMEN_4X4",
|
||||
createdAt: new Date("2026-05-06T10:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-06T10:00:00.000Z"),
|
||||
statusHistory: [
|
||||
{
|
||||
status: "REGISTRADO",
|
||||
note: "Paquete registrado por el cliente",
|
||||
createdAt: new Date("2026-05-06T10:00:00.000Z"),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
prismaService = {
|
||||
client: {
|
||||
package: {
|
||||
findUnique: jest.fn().mockResolvedValue(mockPackage),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TrackingController],
|
||||
providers: [
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: prismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<TrackingController>(TrackingController);
|
||||
});
|
||||
|
||||
it("debe estar definido", () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
it("retorna el paquete cuando el Tracking ID es válido y existe", async () => {
|
||||
const result = await controller.findByTrackingId("EC-20260506-082341");
|
||||
expect(result.trackingId).toBe("EC-20260506-082341");
|
||||
expect(result.status).toBe("REGISTRADO");
|
||||
expect(result.description).toBe("Tenis Nike Air Max");
|
||||
expect(result.timeline).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("incluye el timeline (historial de estados)", async () => {
|
||||
const result = await controller.findByTrackingId("EC-20260506-082341");
|
||||
expect(Array.isArray(result.timeline)).toBe(true);
|
||||
expect(result.timeline[0].status).toBe("REGISTRADO");
|
||||
});
|
||||
|
||||
it("lanza NotFoundException si el Tracking ID no existe en DB", async () => {
|
||||
prismaService.client.package.findUnique.mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
controller.findByTrackingId("EC-20260506-999999"),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it("lanza NotFoundException para formato de ID inválido (evita consultas innecesarias)", async () => {
|
||||
await expect(
|
||||
controller.findByTrackingId("INVALIDO"),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
// No debe llegar a consultar la DB
|
||||
expect(prismaService.client.package.findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lanza NotFoundException para ID con prefijo incorrecto", async () => {
|
||||
await expect(
|
||||
controller.findByTrackingId("US-20260506-082341"),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it("lanza NotFoundException para ID con fecha malformada", async () => {
|
||||
await expect(
|
||||
controller.findByTrackingId("EC-2026050-082341"),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it("retorna null para senaeCategory si no está definida", async () => {
|
||||
prismaService.client.package.findUnique.mockResolvedValueOnce({
|
||||
...mockPackage,
|
||||
senaeCategory: null,
|
||||
});
|
||||
const result = await controller.findByTrackingId("EC-20260506-082341");
|
||||
expect(result.senaeCategory).toBeNull();
|
||||
});
|
||||
|
||||
it("no expone datos sensibles (sin userId ni tenantId)", async () => {
|
||||
const result = await controller.findByTrackingId("EC-20260506-082341") as Record<string, unknown>;
|
||||
expect(result["userId"]).toBeUndefined();
|
||||
expect(result["tenantId"]).toBeUndefined();
|
||||
expect(result["declaredValue"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Controller, Get, Param, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { isValidTrackingId } from "../common/utils/tracking-id.util";
|
||||
|
||||
@Controller("tracking")
|
||||
export class TrackingController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* GET /api/tracking/:id
|
||||
* Búsqueda pública por Tracking ID — sin necesidad de login (doc §07 Módulo de Tracking).
|
||||
* Retorna el estado y el historial del paquete.
|
||||
*/
|
||||
@Get(":id")
|
||||
async findByTrackingId(@Param("id") id: string) {
|
||||
if (!isValidTrackingId(id)) {
|
||||
throw new NotFoundException(
|
||||
`Formato de Tracking ID inválido. Esperado: EC-YYYYMMDD-XXXXXX. Recibido: ${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const pkg = await this.prisma.client.package.findUnique({
|
||||
where: { trackingId: id },
|
||||
select: {
|
||||
trackingId: true,
|
||||
status: true,
|
||||
description: true,
|
||||
store: true,
|
||||
vendorTracking: true,
|
||||
senaeCategory: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
statusHistory: {
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: {
|
||||
status: true,
|
||||
note: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!pkg) {
|
||||
throw new NotFoundException(`No se encontró un paquete con el Tracking ID: ${id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
trackingId: pkg.trackingId,
|
||||
status: pkg.status,
|
||||
description: pkg.description,
|
||||
store: pkg.store ?? null,
|
||||
vendorTracking: pkg.vendorTracking ?? null,
|
||||
senaeCategory: pkg.senaeCategory ?? null,
|
||||
timeline: pkg.statusHistory,
|
||||
lastUpdate: pkg.updatedAt,
|
||||
registered: pkg.createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TrackingController } from "./tracking.controller";
|
||||
|
||||
@Module({
|
||||
controllers: [TrackingController],
|
||||
})
|
||||
export class TrackingModule {}
|
||||
Reference in New Issue
Block a user