// Moraworld Imports — Schema v0.5 (Fase 3 — Consolidación + Pagos + Plantillas) // Multi-tenant por tenant_id en todas las tablas de negocio // Sincronizado con documentacion.html v1.1 generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum ConsolidationStatus { ABIERTA // Recibiendo paquetes CERRADA // Lista para despacho DESPACHADA // Enviada al courier internacional ENTREGADA // Entregada en Ecuador CANCELADA } enum PaymentStatus { PENDIENTE PROCESANDO COMPLETADO FALLIDO REEMBOLSADO } // ─── Enums ─────────────────────────────────────────────────── enum UserRole { SUPER_ADMIN ADMIN_EMPRESA OPERADOR_BODEGA AGENTE_ADUANERO CLIENTE SOPORTE } /// 11 estados del ciclo de vida de un paquete (doc §08) enum PackageStatus { REGISTRADO EN_TRANSITO_BODEGA RECIBIDO_BODEGA EN_VERIFICACION VERIFICADO DECLARACION_ADUANERA EN_TRANSITO_ECUADOR EN_ADUANA_ECUADOR LISTO_ENTREGA ENTREGADO INCIDENCIA } enum PreAlertStatus { PENDIENTE VINCULADA CANCELADA } /// Categorías SENAE para el cálculo de aranceles (doc §15) enum SenaeCategory { REGIMEN_4X4 // 0% — hasta $400, 4 kg, 4 envíos/año CATEGORIA_B // 10% — bienes generales CATEGORIA_C // 20% — textiles, calzado, hogar CATEGORIA_D // 0-15% — electrónicos y telecomunicaciones } enum B2BRequestStatus { PENDIENTE EN_COTIZACION COTIZADO ACEPTADO EN_PROCESO COMPLETADO CANCELADO } enum NotificationChannel { EMAIL WHATSAPP SMS PUSH } enum NotificationStatus { PENDIENTE ENVIADO FALLIDO } // ─── Tenant (multi-tenant) ─────────────────────────────────── model Tenant { id String @id @default(cuid()) slug String @unique name String isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt users User[] suites Suite[] packages Package[] preAlerts PreAlert[] tariffs Tariff[] b2bRequests B2BRequest[] warehouses Warehouse[] integrations Integration[] notificationTemplates NotificationTemplate[] payments Payment[] consolidations Consolidation[] } // ─── Usuarios ──────────────────────────────────────────────── model User { id String @id @default(cuid()) tenantId String email String passwordHash String firstName String lastName String phone String? role UserRole @default(CLIENTE) mfaEnabled Boolean @default(false) mfaSecret String? isActive Boolean @default(true) lastLoginAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) suite Suite? packages Package[] preAlerts PreAlert[] refreshTokens RefreshToken[] @@unique([tenantId, email]) @@index([tenantId]) } /// Tokens de refresco JWT — expiración y revocación (doc §05 Auth) model RefreshToken { id String @id @default(cuid()) userId String token String @unique expiresAt DateTime revokedAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) } // ─── Casillero / Suite ─────────────────────────────────────── /// Dirección virtual asignada al registrarse: "150 N Day St, Suite EC-XXXXX, City of Orange, NJ 07050" model Suite { id String @id @default(cuid()) tenantId String userId String @unique code String // ej: EC-00345 createdAt DateTime @default(now()) tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([tenantId, code]) @@index([tenantId]) } // ─── Paquetes ──────────────────────────────────────────────── model Package { id String @id @default(cuid()) tenantId String userId String /// Formato: EC-YYYYMMDD-XXXXXX (doc §08) trackingId String @unique status PackageStatus @default(REGISTRADO) description String store String? declaredValue Decimal @db.Decimal(10, 2) /// Peso declarado por el cliente en libras declaredWeight Decimal? @db.Decimal(8, 2) /// Peso real confirmado por el operador de bodega en libras actualWeight Decimal? @db.Decimal(8, 2) /// Largo en cm — para peso volumétrico (doc §15) lengthCm Decimal? @db.Decimal(8, 2) /// Ancho en cm widthCm Decimal? @db.Decimal(8, 2) /// Alto en cm heightCm Decimal? @db.Decimal(8, 2) /// Hay discrepancia >10% entre peso declarado y real (doc §10) hasDiscrepancy Boolean @default(false) vendorTracking String? productUrl String? /// URL de fotos del paquete en S3 photos String[] /// Categoría SENAE asignada al calcular (doc §15) senaeCategory SenaeCategory? /// N.º de autorización SENAE al completar la DSI (doc §08 estado 06) senaeAuthNumber String? /// ID de la declaración enviada al WebService SENAE senaeDeclarationId String? paidAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) statusHistory PackageStatusHistory[] preAlert PreAlert? notifications Notification[] payment Payment? consolidation ConsolidationPackage? @@index([tenantId, status]) @@index([userId]) } model PackageStatusHistory { id String @id @default(cuid()) packageId String status PackageStatus note String? createdBy String? createdAt DateTime @default(now()) package Package @relation(fields: [packageId], references: [id], onDelete: Cascade) @@index([packageId]) } // ─── Pre-alertas ───────────────────────────────────────────── model PreAlert { id String @id @default(cuid()) tenantId String /// Cliente que creó la pre-alerta userId String packageId String? @unique store String vendorTracking String? description String declaredValue Decimal @db.Decimal(10, 2) /// S3 key de la factura cargada (PDF o imagen) invoiceKey String? /// Fecha estimada de llegada a bodega NJ estimatedArrival DateTime? status PreAlertStatus @default(PENDIENTE) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) package Package? @relation(fields: [packageId], references: [id]) @@index([tenantId]) @@index([userId]) } // ─── Tarifas (configurables por Admin) ─────────────────────── /// Configuración de tarifas por tenant. Flete $/lb, seguro %, etc. (doc §12 + §15) model Tariff { id String @id @default(cuid()) tenantId String @unique /// Precio por libra en USD — default: $3.50 (doc §15) pricePerLb Decimal @db.Decimal(8, 4) @default(3.50) /// Porcentaje de seguro sobre valor declarado — default: 2% (doc §15) insurancePct Decimal @db.Decimal(5, 4) @default(0.02) /// FODINFA — fijo SENAE: 0.5% (doc §15) fodinfaPct Decimal @db.Decimal(5, 4) @default(0.005) /// IVA Ecuador — 15% (doc §15) ivaPct Decimal @db.Decimal(5, 4) @default(0.15) /// Límite 4×4: valor máx. USD (doc §08 estado 06 / §15) max4x4Value Decimal @db.Decimal(10, 2) @default(400) /// Límite 4×4: peso máx. kg max4x4WeightKg Decimal @db.Decimal(6, 2) @default(4) /// Máx. envíos/año bajo régimen 4×4 max4x4PerYear Int @default(4) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) } // ─── Carga Pesada B2B ───────────────────────────────────────── /// Solicitudes de importación mayorista: pallets, contenedores (doc §13) model B2BRequest { id String @id @default(cuid()) tenantId String /// Tracking ID propio para B2B trackingId String @unique contactName String contactEmail String contactPhone String? companyName String? /// Tipo de mercancía merchandiseType String /// Descripción detallada description String /// Peso estimado en kg estimatedWeightKg Decimal? @db.Decimal(10, 2) /// Número de pallets pallets Int? /// Valor comercial total en USD commercialValue Decimal? @db.Decimal(12, 2) /// Ciudad de origen en EE.UU. originCity String? /// Requiere certificación INEN (doc §13) requiresInen Boolean @default(false) inenCertNumber String? status B2BRequestStatus @default(PENDIENTE) /// Cotización enviada por el equipo Moraworld quotationAmount Decimal? @db.Decimal(12, 2) quotationNotes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) @@index([tenantId, status]) } // ─── Notificaciones ────────────────────────────────────────── /// Historial de notificaciones enviadas por canal (doc §16) model Notification { id String @id @default(cuid()) packageId String? userId String? channel NotificationChannel status NotificationStatus @default(PENDIENTE) subject String? body String sentAt DateTime? error String? createdAt DateTime @default(now()) package Package? @relation(fields: [packageId], references: [id]) @@index([packageId]) @@index([userId]) } // ─── Auditoría (append-only) ─────────────────────────────── /// Logs inmutables — ISO 27001 A.12 (doc §17) model AuditLog { id String @id @default(cuid()) tenantId String? userId String? action String resource String? resourceId String? metadata Json? ipAddress String? userAgent String? createdAt DateTime @default(now()) @@index([tenantId, createdAt]) @@index([userId]) } // ─── Bodegas (multi-bodega, doc §07 + §12) ─────────────────── /// Bodega física. Cada tenant puede tener varias; una es la default. model Warehouse { id String @id @default(cuid()) tenantId String name String // ej: "Bodega NJ — City of Orange" street String // ej: "150 N Day St" city String // ej: "City of Orange" state String // ej: "NJ" zip String // ej: "07050" country String @default("US") phone String? email String? contactName String? /// Horario de operación (texto libre) schedule String? /// Capacidad aproximada (m²) capacityM2 Decimal? @db.Decimal(10, 2) isDefault Boolean @default(false) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) @@index([tenantId]) } // ─── Consolidaciones (§21) ──────────────────────────────────── /// Agrupa múltiples paquetes del mismo cliente en un solo envío. /// Optimiza costos de flete y reduce declaraciones aduaneras. model Consolidation { id String @id @default(cuid()) tenantId String userId String // cliente dueño de los paquetes /// Código de consolidación: CON-YYYYMMDD-XXXXXX code String @unique status ConsolidationStatus @default(ABIERTA) /// Peso total real en libras (suma de paquetes) totalWeightLb Decimal? @db.Decimal(8, 2) /// Valor declarado total en USD totalValue Decimal? @db.Decimal(10, 2) /// Notas del operador notes String? /// Número de seguimiento del courier internacional (FedEx / DHL / UPS) courierTracking String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt createdBy String? tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) packages ConsolidationPackage[] @@index([tenantId, status]) @@index([userId]) } /// Tabla intermedia — paquetes incluidos en una consolidación model ConsolidationPackage { id String @id @default(cuid()) consolidationId String packageId String @unique // un paquete solo puede estar en una consolidación addedAt DateTime @default(now()) consolidation Consolidation @relation(fields: [consolidationId], references: [id], onDelete: Cascade) package Package @relation(fields: [packageId], references: [id], onDelete: Cascade) @@index([consolidationId]) } // ─── Pagos (§09 paso 8 / §14 paso 5) ───────────────────────── /// Registro de pagos vinculados a un paquete. /// El `providerRef` es el PaymentIntent ID de Stripe (o equivalente). model Payment { id String @id @default(cuid()) tenantId String packageId String @unique userId String amount Decimal @db.Decimal(10, 2) currency String @default("USD") /// "stripe" | "payphone" | "paypal" provider String @default("stripe") /// PaymentIntent ID de Stripe u equivalente providerRef String? status PaymentStatus @default(PENDIENTE) paidAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) package Package @relation(fields: [packageId], references: [id], onDelete: Cascade) @@index([tenantId, status]) @@index([userId]) } // ─── Plantillas de notificación (§12) ──────────────────────── /// Plantillas de mensajes para cada evento del ciclo de vida del paquete. /// Soporta variables: {{trackingId}}, {{firstName}}, {{status}}, {{suiteCode}} model NotificationTemplate { id String @id @default(cuid()) tenantId String /// Evento que dispara la notificación — coincide con PackageStatus event String // ej: "REGISTRADO", "ENTREGADO" channel NotificationChannel subject String? // Solo relevante para EMAIL body String isActive Boolean @default(true) updatedAt DateTime @updatedAt updatedBy String? tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) @@unique([tenantId, event, channel]) @@index([tenantId, event]) } // ─── Integraciones (API keys por tenant) ───────────────────── /// Configuración de integraciones externas (API keys, endpoints, webhooks). /// El valor se almacena en texto plano en dev; en prod debe cifrarse (AES-256). model Integration { id String @id @default(cuid()) tenantId String /// Clave identificadora: ej. "whatsapp_token", "senae_endpoint", "stripe_secret" key String /// Valor (API key, URL, token — cifrar en prod) value String? /// Descripción legible del campo label String? /// Grupo de integración: "payment", "notifications", "customs", "courier", "warehouse" group String? isActive Boolean @default(false) updatedAt DateTime @updatedAt updatedBy String? tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) @@unique([tenantId, key]) @@index([tenantId, group]) }