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
347 lines
11 KiB
Plaintext
347 lines
11 KiB
Plaintext
// Moraworld Imports — Schema v0.2 (Fase 0–1)
|
||
// 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")
|
||
}
|
||
|
||
// ─── 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[]
|
||
}
|
||
|
||
// ─── 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[]
|
||
|
||
@@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])
|
||
}
|