Files
moraworld/packages/database/prisma/migrations/20260520120000_init/migration.sql
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

261 lines
13 KiB
SQL

-- ══════════════════════════════════════════════════════════════
-- Moraworld Imports — Migración inicial v0.2
-- Sincronizado con documentacion.html v1.1
-- ══════════════════════════════════════════════════════════════
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('SUPER_ADMIN', 'ADMIN_EMPRESA', 'OPERADOR_BODEGA', 'AGENTE_ADUANERO', 'CLIENTE', 'SOPORTE');
-- CreateEnum — 11 estados del ciclo de vida de un paquete (doc §08)
CREATE TYPE "PackageStatus" AS ENUM ('REGISTRADO', 'EN_TRANSITO_BODEGA', 'RECIBIDO_BODEGA', 'EN_VERIFICACION', 'VERIFICADO', 'DECLARACION_ADUANERA', 'EN_TRANSITO_ECUADOR', 'EN_ADUANA_ECUADOR', 'LISTO_ENTREGA', 'ENTREGADO', 'INCIDENCIA');
-- CreateEnum
CREATE TYPE "PreAlertStatus" AS ENUM ('PENDIENTE', 'VINCULADA', 'CANCELADA');
-- CreateEnum — Categorías SENAE (doc §15)
CREATE TYPE "SenaeCategory" AS ENUM ('REGIMEN_4X4', 'CATEGORIA_B', 'CATEGORIA_C', 'CATEGORIA_D');
-- CreateEnum — Estados de solicitud B2B (doc §13)
CREATE TYPE "B2BRequestStatus" AS ENUM ('PENDIENTE', 'EN_COTIZACION', 'COTIZADO', 'ACEPTADO', 'EN_PROCESO', 'COMPLETADO', 'CANCELADO');
-- CreateEnum — Canales de notificación (doc §16)
CREATE TYPE "NotificationChannel" AS ENUM ('EMAIL', 'WHATSAPP', 'SMS', 'PUSH');
-- CreateEnum
CREATE TYPE "NotificationStatus" AS ENUM ('PENDIENTE', 'ENVIADO', 'FALLIDO');
-- ─── Tenant (multi-tenant) ────────────────────────────────────
CREATE TABLE "Tenant" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id")
);
-- ─── Usuarios ─────────────────────────────────────────────────
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"phone" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'CLIENTE',
"mfaEnabled" BOOLEAN NOT NULL DEFAULT false,
"mfaSecret" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"lastLoginAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- ─── Refresh Tokens JWT (doc §05 Auth) ───────────────────────
CREATE TABLE "RefreshToken" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"revokedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id")
);
-- ─── Suite / Casillero (doc §02 y §09) ───────────────────────
-- Dirección: "150 N Day St, Suite EC-XXXXX, City of Orange, NJ 07050"
CREATE TABLE "Suite" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Suite_pkey" PRIMARY KEY ("id")
);
-- ─── Paquetes (doc §07 — Módulo de Tracking) ─────────────────
CREATE TABLE "Package" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"trackingId" TEXT NOT NULL, -- EC-YYYYMMDD-XXXXXX (doc §08)
"status" "PackageStatus" NOT NULL DEFAULT 'REGISTRADO',
"description" TEXT NOT NULL,
"store" TEXT,
"declaredValue" DECIMAL(10,2) NOT NULL,
"declaredWeight" DECIMAL(8,2), -- Peso declarado por cliente (lbs)
"actualWeight" DECIMAL(8,2), -- Peso real confirmado en bodega NJ
"lengthCm" DECIMAL(8,2), -- Para peso volumétrico (doc §15)
"widthCm" DECIMAL(8,2),
"heightCm" DECIMAL(8,2),
"hasDiscrepancy" BOOLEAN NOT NULL DEFAULT false, -- Discrepancia >10% (doc §10)
"vendorTracking" TEXT,
"productUrl" TEXT,
"photos" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], -- S3 keys de fotos
"senaeCategory" "SenaeCategory", -- Categoría SENAE (doc §15)
"senaeAuthNumber" TEXT, -- N.º autorización SENAE
"senaeDeclarationId" TEXT, -- ID declaración en WebService
"paidAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Package_pkey" PRIMARY KEY ("id")
);
-- ─── Historial de estados ─────────────────────────────────────
CREATE TABLE "PackageStatusHistory" (
"id" TEXT NOT NULL,
"packageId" TEXT NOT NULL,
"status" "PackageStatus" NOT NULL,
"note" TEXT,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PackageStatusHistory_pkey" PRIMARY KEY ("id")
);
-- ─── Pre-alertas (doc §07 — Módulo Pre-alerta) ───────────────
CREATE TABLE "PreAlert" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT NOT NULL, -- Cliente que creó la pre-alerta
"packageId" TEXT,
"store" TEXT NOT NULL,
"vendorTracking" TEXT,
"description" TEXT NOT NULL,
"declaredValue" DECIMAL(10,2) NOT NULL,
"invoiceKey" TEXT, -- S3 key de la factura (PDF/imagen)
"estimatedArrival" TIMESTAMP(3), -- Fecha estimada llegada bodega NJ
"status" "PreAlertStatus" NOT NULL DEFAULT 'PENDIENTE',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PreAlert_pkey" PRIMARY KEY ("id")
);
-- ─── Tarifas configurables (doc §12 + §15) ───────────────────
CREATE TABLE "Tariff" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"pricePerLb" DECIMAL(8,4) NOT NULL DEFAULT 3.50,
"insurancePct" DECIMAL(5,4) NOT NULL DEFAULT 0.02,
"fodinfaPct" DECIMAL(5,4) NOT NULL DEFAULT 0.005,
"ivaPct" DECIMAL(5,4) NOT NULL DEFAULT 0.15,
"max4x4Value" DECIMAL(10,2) NOT NULL DEFAULT 400,
"max4x4WeightKg" DECIMAL(6,2) NOT NULL DEFAULT 4,
"max4x4PerYear" INTEGER NOT NULL DEFAULT 4,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Tariff_pkey" PRIMARY KEY ("id")
);
-- ─── Solicitudes B2B — Carga Pesada (doc §13) ────────────────
CREATE TABLE "B2BRequest" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"trackingId" TEXT NOT NULL, -- ID B2B propio
"contactName" TEXT NOT NULL,
"contactEmail" TEXT NOT NULL,
"contactPhone" TEXT,
"companyName" TEXT,
"merchandiseType" TEXT NOT NULL,
"description" TEXT NOT NULL,
"estimatedWeightKg" DECIMAL(10,2),
"pallets" INTEGER,
"commercialValue" DECIMAL(12,2),
"originCity" TEXT,
"requiresInen" BOOLEAN NOT NULL DEFAULT false, -- Requiere certificación INEN
"inenCertNumber" TEXT,
"status" "B2BRequestStatus" NOT NULL DEFAULT 'PENDIENTE',
"quotationAmount" DECIMAL(12,2),
"quotationNotes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "B2BRequest_pkey" PRIMARY KEY ("id")
);
-- ─── Notificaciones (doc §16) ─────────────────────────────────
CREATE TABLE "Notification" (
"id" TEXT NOT NULL,
"packageId" TEXT,
"userId" TEXT,
"channel" "NotificationChannel" NOT NULL,
"status" "NotificationStatus" NOT NULL DEFAULT 'PENDIENTE',
"subject" TEXT,
"body" TEXT NOT NULL,
"sentAt" TIMESTAMP(3),
"error" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
);
-- ─── Auditoría append-only (doc §17 — ISO 27001 A.12) ────────
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL,
"tenantId" TEXT,
"userId" TEXT,
"action" TEXT NOT NULL,
"resource" TEXT,
"resourceId" TEXT,
"metadata" JSONB,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- ─── Índices únicos ───────────────────────────────────────────
CREATE UNIQUE INDEX "Tenant_slug_key" ON "Tenant"("slug");
CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token");
CREATE UNIQUE INDEX "Suite_userId_key" ON "Suite"("userId");
CREATE UNIQUE INDEX "Suite_tenantId_code_key" ON "Suite"("tenantId", "code");
CREATE UNIQUE INDEX "Package_trackingId_key" ON "Package"("trackingId");
CREATE UNIQUE INDEX "PreAlert_packageId_key" ON "PreAlert"("packageId");
CREATE UNIQUE INDEX "Tariff_tenantId_key" ON "Tariff"("tenantId");
CREATE UNIQUE INDEX "B2BRequest_trackingId_key" ON "B2BRequest"("trackingId");
CREATE UNIQUE INDEX "User_tenantId_email_key" ON "User"("tenantId", "email");
-- ─── Índices de búsqueda ──────────────────────────────────────
CREATE INDEX "User_tenantId_idx" ON "User"("tenantId");
CREATE INDEX "RefreshToken_userId_idx" ON "RefreshToken"("userId");
CREATE INDEX "Suite_tenantId_idx" ON "Suite"("tenantId");
CREATE INDEX "Package_tenantId_status_idx" ON "Package"("tenantId", "status");
CREATE INDEX "Package_userId_idx" ON "Package"("userId");
CREATE INDEX "PackageStatusHistory_packageId_idx" ON "PackageStatusHistory"("packageId");
CREATE INDEX "PreAlert_tenantId_idx" ON "PreAlert"("tenantId");
CREATE INDEX "PreAlert_userId_idx" ON "PreAlert"("userId");
CREATE INDEX "B2BRequest_tenantId_status_idx" ON "B2BRequest"("tenantId", "status");
CREATE INDEX "Notification_packageId_idx" ON "Notification"("packageId");
CREATE INDEX "Notification_userId_idx" ON "Notification"("userId");
CREATE INDEX "AuditLog_tenantId_createdAt_idx" ON "AuditLog"("tenantId", "createdAt");
CREATE INDEX "AuditLog_userId_idx" ON "AuditLog"("userId");
-- ─── Claves foráneas ──────────────────────────────────────────
ALTER TABLE "User" ADD CONSTRAINT "User_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Suite" ADD CONSTRAINT "Suite_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Suite" ADD CONSTRAINT "Suite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Package" ADD CONSTRAINT "Package_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Package" ADD CONSTRAINT "Package_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PackageStatusHistory" ADD CONSTRAINT "PackageStatusHistory_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "Package"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PreAlert" ADD CONSTRAINT "PreAlert_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PreAlert" ADD CONSTRAINT "PreAlert_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PreAlert" ADD CONSTRAINT "PreAlert_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "Package"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "Tariff" ADD CONSTRAINT "Tariff_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "B2BRequest" ADD CONSTRAINT "B2BRequest_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "Package"("id") ON DELETE SET NULL ON UPDATE CASCADE;