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
261 lines
13 KiB
SQL
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;
|