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
241 lines
8.8 KiB
TypeScript
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());
|