Files
moraworld/packages/database/prisma/schema.prisma
T
Lizandro Guarnizo 5565eef554 feat: payments module, notification templates, WA float, route fixes
- Add PaymentsModule: POST /payments/intent, POST /:id/confirm, GET /payments, GET /payments/package/:id, GET /payments/track/:trackingId
- Add Payment model to Prisma schema (PaymentStatus enum, Payment table)
- Add NotificationTemplate model + NotificationsController (GET/PUT /notification-templates, POST /notification-templates/seed)
- Update NotificationsService: DB-backed templates with variable interpolation {{trackingId}} {{firstName}} {{status}} {{suiteCode}}
- Fix /bodega/paquetes: replace 11 wrong status strings with correct §08 enum values
- Fix /admin/reportes: replace EN_CAMINO_A_ECUADOR with correct §08 statuses, rewrite report page with proper KPIs and bar charts
- Fix /portal/mis-paquetes: correct §08 statuses, add 'Pagar envío' button for VERIFICADO/DECLARACION_ADUANERA packages
- Add WhatsApp float component (_components/whatsapp-float.tsx, 2 contacts: NJ ops + Cuenca aduana)
- Add /casillero/calculadora and /casillero/registro redirects (§20)
- Add /portal/pago payment page with cost breakdown (§09/§14/§15)
- Add /admin/notificaciones page: view/edit/toggle templates per event×channel
- Admin nav: add Notificaciones link
- api.ts: add notificationTemplates.* and payments.* client methods
- schema.prisma v0.4: PaymentStatus enum, Payment model, NotificationTemplate model
- db push applied to remote DB (46.202.93.92)
- All builds pass (API nest build + Next.js build)
2026-06-01 17:09:05 -05:00

463 lines
15 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.3 (Fase 2 — Bodegas + Integraciones)
// 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 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[]
}
// ─── 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?
@@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])
}
// ─── 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])
}