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:
@@ -1,5 +1,6 @@
|
||||
// Moraworld Imports — Schema v0 (Fase 0–1)
|
||||
// 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"
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user