- Schema v0.3: Warehouse + Integration models (db push applied) - WarehousesModule: CRUD, set-default, multi-warehouse support - IntegrationsModule: 26 keys across 7 groups (payment, notifications, customs, courier, marketplace, compliance, warehouse) - AuthService: suite address pulled from default Warehouse in DB (env fallback) - AuthModule: imports WarehousesModule - Seed: creates default warehouse from WAREHOUSE_ADDRESS_* env vars - Web: /admin/configuracion page (3 tabs: Bodegas, Integraciones, Estado APIs) - Web: admin layout adds Configuracion nav link - api.ts: warehouses + integrations client methods
265 lines
9.8 KiB
TypeScript
265 lines
9.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)");
|
||
|
||
// ─── Bodega predeterminada (doc §07 + §12) ────────────────
|
||
const warehouseCount = await prisma.warehouse.count({ where: { tenantId: tenant.id } });
|
||
if (warehouseCount === 0) {
|
||
await prisma.warehouse.create({
|
||
data: {
|
||
tenantId: tenant.id,
|
||
name: "Bodega NJ — City of Orange",
|
||
street: process.env.WAREHOUSE_ADDRESS_STREET ?? "150 N Day St",
|
||
city: process.env.WAREHOUSE_ADDRESS_CITY ?? "City of Orange",
|
||
state: process.env.WAREHOUSE_ADDRESS_STATE ?? "NJ",
|
||
zip: process.env.WAREHOUSE_ADDRESS_ZIP ?? "07050",
|
||
country: process.env.WAREHOUSE_ADDRESS_COUNTRY ?? "US",
|
||
phone: "+1-555-0100",
|
||
contactName: "Operaciones Moraworld NJ",
|
||
schedule: "Lun–Vie 8:00–17:00 ET",
|
||
isDefault: true,
|
||
isActive: true,
|
||
},
|
||
});
|
||
console.log("✓ Bodega predeterminada creada: 150 N Day St, City of Orange, NJ 07050");
|
||
} else {
|
||
console.log("→ Bodega predeterminada ya existe");
|
||
}
|
||
|
||
// ─── 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());
|