Files
moraworld/packages/database/prisma/schema.prisma
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

347 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Moraworld Imports — Schema v0.2 (Fase 01)
// 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])
}