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:
@@ -1,31 +1,235 @@
|
||||
import { PrismaClient, UserRole } from "@prisma/client";
|
||||
/**
|
||||
* Seed de desarrollo — Moraworld Imports
|
||||
* Crea datos de prueba para todos los roles documentados en §06 de documentacion.html
|
||||
*
|
||||
* Usuarios de prueba:
|
||||
* super@moraworld.test → SUPER_ADMIN
|
||||
* admin@moraworld.test → ADMIN_EMPRESA
|
||||
* bodega@moraworld.test → OPERADOR_BODEGA
|
||||
* aduanero@moraworld.test → AGENTE_ADUANERO
|
||||
* cliente@moraworld.test → CLIENTE (con Suite EC-00001)
|
||||
* soporte@moraworld.test → SOPORTE
|
||||
*
|
||||
* NOTA: Las contraseñas son placeholders (bcrypt se implementa en Fase 1 — auth).
|
||||
*/
|
||||
|
||||
import { PrismaClient, UserRole, PackageStatus, SenaeCategory } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Placeholder hash (en Fase 1 se reemplaza por bcrypt.hash("Test1234!", 10))
|
||||
const PLACEHOLDER_HASH = "$2b$10$PLACEHOLDER_CHANGE_IN_PHASE_1_AUTH_MODULE";
|
||||
|
||||
async function main() {
|
||||
// ─── Tenant ─────────────────────────────────────────────────
|
||||
const tenant = await prisma.tenant.upsert({
|
||||
where: { slug: "moraworld" },
|
||||
update: {},
|
||||
update: { name: "Moraworld Imports", isActive: true },
|
||||
create: {
|
||||
slug: "moraworld",
|
||||
name: "Moraworld Imports",
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
console.log(`✓ Tenant: ${tenant.slug} (id: ${tenant.id})`);
|
||||
|
||||
console.log("✓ Tenant:", tenant.slug);
|
||||
|
||||
const suiteCode = "EC-00001";
|
||||
const demoEmail = "demo@moraworld.test";
|
||||
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { tenantId_email: { tenantId: tenant.id, email: demoEmail } },
|
||||
// ─── Tarifa base (doc §15) ────────────────────────────────
|
||||
await prisma.tariff.upsert({
|
||||
where: { tenantId: tenant.id },
|
||||
update: {},
|
||||
create: {
|
||||
tenantId: tenant.id,
|
||||
pricePerLb: 3.50,
|
||||
insurancePct: 0.02,
|
||||
fodinfaPct: 0.005,
|
||||
ivaPct: 0.15,
|
||||
max4x4Value: 400,
|
||||
max4x4WeightKg: 4,
|
||||
max4x4PerYear: 4,
|
||||
},
|
||||
});
|
||||
console.log("✓ Tarifa base configurada ($3.50/lb, 2% seguro, 0.5% FODINFA, 15% IVA)");
|
||||
|
||||
if (!existing) {
|
||||
console.log("ℹ Usuario demo se creará en Fase 1 (auth con bcrypt)");
|
||||
// ─── Usuarios de prueba (uno por cada rol del §06) ────────
|
||||
|
||||
const usersToSeed: Array<{
|
||||
email: string;
|
||||
role: UserRole;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone?: string;
|
||||
}> = [
|
||||
{ email: "super@moraworld.test", role: UserRole.SUPER_ADMIN, firstName: "System", lastName: "Admin" },
|
||||
{ email: "admin@moraworld.test", role: UserRole.ADMIN_EMPRESA, firstName: "Carlos", lastName: "Mora" },
|
||||
{ email: "bodega@moraworld.test", role: UserRole.OPERADOR_BODEGA, firstName: "James", lastName: "Wilson", phone: "+1-555-0100" },
|
||||
{ email: "aduanero@moraworld.test", role: UserRole.AGENTE_ADUANERO, firstName: "Sofía", lastName: "Estrella" },
|
||||
{ email: "cliente@moraworld.test", role: UserRole.CLIENTE, firstName: "Andrés", lastName: "Gutiérrez", phone: "+593-99-999-0001" },
|
||||
{ email: "soporte@moraworld.test", role: UserRole.SOPORTE, firstName: "Laura", lastName: "Vásquez" },
|
||||
];
|
||||
|
||||
const createdUsers: Record<string, string> = {};
|
||||
|
||||
for (const u of usersToSeed) {
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { tenantId_email: { tenantId: tenant.id, email: u.email } },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
email: u.email,
|
||||
passwordHash: PLACEHOLDER_HASH,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
phone: u.phone ?? null,
|
||||
role: u.role,
|
||||
mfaEnabled: false,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
createdUsers[u.role] = user.id;
|
||||
console.log(`✓ Usuario creado: ${u.email} (${u.role})`);
|
||||
} else {
|
||||
createdUsers[u.role] = existing.id;
|
||||
console.log(`→ Usuario ya existe: ${u.email} (${u.role})`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✓ Seed completado");
|
||||
// ─── Suite para el cliente demo ───────────────────────────
|
||||
// Formato: EC-00001 (doc §02 y §09)
|
||||
const clientId = createdUsers[UserRole.CLIENTE];
|
||||
if (clientId) {
|
||||
const existingSuite = await prisma.suite.findUnique({ where: { userId: clientId } });
|
||||
if (!existingSuite) {
|
||||
await prisma.suite.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
userId: clientId,
|
||||
code: "EC-00001",
|
||||
},
|
||||
});
|
||||
console.log("✓ Suite EC-00001 asignada a cliente demo");
|
||||
} else {
|
||||
console.log(`→ Suite ya existe: ${existingSuite.code}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Paquete demo con historial de estados ────────────────
|
||||
const existingPkg = await prisma.package.findUnique({
|
||||
where: { trackingId: "EC-20260506-000001" },
|
||||
});
|
||||
|
||||
if (!existingPkg && clientId) {
|
||||
const pkg = await prisma.package.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
userId: clientId,
|
||||
trackingId: "EC-20260506-000001",
|
||||
status: PackageStatus.VERIFICADO,
|
||||
description: "Tenis Nike Air Max 270 (demo seed)",
|
||||
store: "Amazon",
|
||||
declaredValue: 150.00,
|
||||
declaredWeight: 2.5,
|
||||
actualWeight: 2.6,
|
||||
lengthCm: 32,
|
||||
widthCm: 22,
|
||||
heightCm: 14,
|
||||
hasDiscrepancy: false,
|
||||
vendorTracking: "1Z999AA10123456784",
|
||||
productUrl: "https://www.amazon.com/dp/DEMO",
|
||||
photos: [],
|
||||
senaeCategory: SenaeCategory.REGIMEN_4X4,
|
||||
paidAt: new Date("2026-05-06T10:00:00.000Z"),
|
||||
},
|
||||
});
|
||||
|
||||
// Historial de estados hasta VERIFICADO
|
||||
const historialEstados: Array<{ status: PackageStatus; note: string; date: string }> = [
|
||||
{ status: PackageStatus.REGISTRADO, note: "Paquete registrado por el cliente", date: "2026-05-06T10:00:00Z" },
|
||||
{ status: PackageStatus.EN_TRANSITO_BODEGA, note: "Amazon confirmó despacho", date: "2026-05-07T09:00:00Z" },
|
||||
{ status: PackageStatus.RECIBIDO_BODEGA, note: "Recibido en 150 N Day St, NJ", date: "2026-05-09T14:30:00Z" },
|
||||
{ status: PackageStatus.EN_VERIFICACION, note: "Operador revisando paquete", date: "2026-05-09T15:00:00Z" },
|
||||
{ status: PackageStatus.VERIFICADO, note: "Peso real: 2.6 lbs. Sin discrepancia.", date: "2026-05-09T15:45:00Z" },
|
||||
];
|
||||
|
||||
for (const h of historialEstados) {
|
||||
await prisma.packageStatusHistory.create({
|
||||
data: {
|
||||
packageId: pkg.id,
|
||||
status: h.status,
|
||||
note: h.note,
|
||||
createdBy: clientId,
|
||||
createdAt: new Date(h.date),
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(`✓ Paquete demo creado: ${pkg.trackingId} (estado: ${pkg.status})`);
|
||||
} else {
|
||||
console.log("→ Paquete demo ya existe");
|
||||
}
|
||||
|
||||
// ─── Pre-alerta demo ──────────────────────────────────────
|
||||
if (clientId) {
|
||||
const existingPreAlert = await prisma.preAlert.findFirst({
|
||||
where: { tenantId: tenant.id, userId: clientId },
|
||||
});
|
||||
if (!existingPreAlert) {
|
||||
await prisma.preAlert.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
userId: clientId,
|
||||
store: "eBay",
|
||||
vendorTracking: "9400111899223397992682",
|
||||
description: "Audífonos Sony WH-1000XM5 (demo pre-alerta)",
|
||||
declaredValue: 280.00,
|
||||
estimatedArrival: new Date("2026-05-20T00:00:00.000Z"),
|
||||
status: "PENDIENTE",
|
||||
},
|
||||
});
|
||||
console.log("✓ Pre-alerta demo creada");
|
||||
} else {
|
||||
console.log("→ Pre-alerta demo ya existe");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Solicitud B2B demo (doc §13) ─────────────────────────
|
||||
const existingB2B = await prisma.b2BRequest.findUnique({
|
||||
where: { trackingId: "B2B-20260506-000001" },
|
||||
});
|
||||
if (!existingB2B) {
|
||||
await prisma.b2BRequest.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
trackingId: "B2B-20260506-000001",
|
||||
contactName: "Roberto Andrade",
|
||||
contactEmail: "roberto@importadora.ec",
|
||||
contactPhone: "+593-98-765-4321",
|
||||
companyName: "Importadora Andrade S.A.",
|
||||
merchandiseType: "Calzado deportivo",
|
||||
description: "1,000 pares de zapatos deportivos Nike (demo B2B)",
|
||||
estimatedWeightKg: 1500,
|
||||
pallets: 4,
|
||||
commercialValue: 18000.00,
|
||||
originCity: "Miami, FL",
|
||||
requiresInen: true,
|
||||
status: "PENDIENTE",
|
||||
},
|
||||
});
|
||||
console.log("✓ Solicitud B2B demo creada: B2B-20260506-000001");
|
||||
} else {
|
||||
console.log("→ Solicitud B2B demo ya existe");
|
||||
}
|
||||
|
||||
console.log("\n✅ Seed completado correctamente.");
|
||||
console.log("\n📋 Usuarios de prueba:");
|
||||
console.log(" super@moraworld.test → SUPER_ADMIN");
|
||||
console.log(" admin@moraworld.test → ADMIN_EMPRESA");
|
||||
console.log(" bodega@moraworld.test → OPERADOR_BODEGA");
|
||||
console.log(" aduanero@moraworld.test → AGENTE_ADUANERO");
|
||||
console.log(" cliente@moraworld.test → CLIENTE (Suite EC-00001)");
|
||||
console.log(" soporte@moraworld.test → SOPORTE");
|
||||
console.log("\n ⚠️ Contraseñas: placeholder — implementar bcrypt en Fase 1 (auth).");
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user