Files
moraworld/packages/database/prisma/seed.ts
T
Lizandro Guarnizo 93db76897d 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
2026-06-01 08:25:01 -05:00

241 lines
8.8 KiB
TypeScript

/**
* 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: { name: "Moraworld Imports", isActive: true },
create: {
slug: "moraworld",
name: "Moraworld Imports",
isActive: true,
},
});
console.log(`✓ Tenant: ${tenant.slug} (id: ${tenant.id})`);
// ─── 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)");
// ─── 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})`);
}
}
// ─── 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()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => prisma.$disconnect());