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
This commit is contained in:
Lizandro Guarnizo
2026-06-01 08:25:01 -05:00
parent 094ea9cf81
commit 93db76897d
26 changed files with 4651 additions and 201 deletions
+176 -9
View File
@@ -1,5 +1,6 @@
// Moraworld Imports — Schema v0 (Fase 01)
// 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"
@@ -21,6 +22,7 @@ enum UserRole {
SOPORTE
}
/// 11 estados del ciclo de vida de un paquete (doc §08)
enum PackageStatus {
REGISTRADO
EN_TRANSITO_BODEGA
@@ -41,6 +43,37 @@ enum PreAlertStatus {
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 {
@@ -51,10 +84,12 @@ model Tenant {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
suites Suite[]
packages Package[]
preAlerts PreAlert[]
users User[]
suites Suite[]
packages Package[]
preAlerts PreAlert[]
tariffs Tariff[]
b2bRequests B2BRequest[]
}
// ─── Usuarios ────────────────────────────────────────────────
@@ -75,16 +110,33 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
suite Suite?
packages Package[]
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
@@ -105,15 +157,34 @@ model Package {
id String @id @default(cuid())
tenantId String
userId String
trackingId String @unique // EC-YYYYMMDD-XXXXXX
/// 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
@@ -122,6 +193,7 @@ model Package {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
statusHistory PackageStatusHistory[]
preAlert PreAlert?
notifications Notification[]
@@index([tenantId, status])
@@index([userId])
@@ -145,24 +217,118 @@ model PackageStatusHistory {
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?
@@ -172,6 +338,7 @@ model AuditLog {
resourceId String?
metadata Json?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([tenantId, createdAt])