initial push

This commit is contained in:
Marcelo Dares
2026-03-15 15:03:56 +01:00
parent d48b9d5352
commit 65aaf9275e
146 changed files with 70245 additions and 100 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
-- CreateEnum
CREATE TYPE "PriorityLevel" AS ENUM ('LOW', 'MEDIUM', 'HIGH');
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN');
-- CreateEnum
CREATE TYPE "ContentPageType" AS ENUM ('FAQ', 'MANUAL');
-- CreateEnum
CREATE TYPE "OverallScoreMethod" AS ENUM ('EQUAL_ALL_MODULES', 'EQUAL_ANSWERED_MODULES', 'WEIGHTED_ANSWERED_MODULES');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"name" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'USER',
"emailVerifiedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Organization" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"industry" TEXT,
"companySize" TEXT,
"country" TEXT,
"primaryObjective" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DiagnosticModule" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DiagnosticModule_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Question" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"moduleId" TEXT NOT NULL,
"prompt" TEXT NOT NULL,
"helpText" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Question_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AnswerOption" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"questionId" TEXT NOT NULL,
"label" TEXT NOT NULL,
"weight" INTEGER NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AnswerOption_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Response" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"questionId" TEXT NOT NULL,
"answerOptionId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Response_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AssessmentResult" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"moduleId" TEXT,
"overallScore" DOUBLE PRECISION,
"moduleScore" DOUBLE PRECISION,
"metadata" JSONB,
"calculatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AssessmentResult_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Recommendation" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"moduleId" TEXT,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"priority" "PriorityLevel" NOT NULL DEFAULT 'MEDIUM',
"isTemplate" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Recommendation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ContentPage" (
"id" TEXT NOT NULL,
"type" "ContentPageType" NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"isPublished" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ContentPage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ScoringConfig" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"lowScoreThreshold" INTEGER NOT NULL DEFAULT 70,
"overallScoreMethod" "OverallScoreMethod" NOT NULL DEFAULT 'EQUAL_ALL_MODULES',
"moduleWeights" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ScoringConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EmailVerificationToken" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"consumedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "EmailVerificationToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Organization_userId_key" ON "Organization"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "DiagnosticModule_key_key" ON "DiagnosticModule"("key");
-- CreateIndex
CREATE UNIQUE INDEX "Question_key_key" ON "Question"("key");
-- CreateIndex
CREATE INDEX "Question_moduleId_sortOrder_idx" ON "Question"("moduleId", "sortOrder");
-- CreateIndex
CREATE UNIQUE INDEX "AnswerOption_key_key" ON "AnswerOption"("key");
-- CreateIndex
CREATE INDEX "AnswerOption_questionId_sortOrder_idx" ON "AnswerOption"("questionId", "sortOrder");
-- CreateIndex
CREATE INDEX "Response_userId_idx" ON "Response"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Response_userId_questionId_key" ON "Response"("userId", "questionId");
-- CreateIndex
CREATE INDEX "AssessmentResult_userId_calculatedAt_idx" ON "AssessmentResult"("userId", "calculatedAt");
-- CreateIndex
CREATE INDEX "AssessmentResult_moduleId_idx" ON "AssessmentResult"("moduleId");
-- CreateIndex
CREATE UNIQUE INDEX "Recommendation_key_key" ON "Recommendation"("key");
-- CreateIndex
CREATE INDEX "Recommendation_moduleId_idx" ON "Recommendation"("moduleId");
-- CreateIndex
CREATE UNIQUE INDEX "ContentPage_slug_key" ON "ContentPage"("slug");
-- CreateIndex
CREATE INDEX "ContentPage_type_sortOrder_idx" ON "ContentPage"("type", "sortOrder");
-- CreateIndex
CREATE UNIQUE INDEX "ScoringConfig_key_key" ON "ScoringConfig"("key");
-- CreateIndex
CREATE UNIQUE INDEX "EmailVerificationToken_token_key" ON "EmailVerificationToken"("token");
-- CreateIndex
CREATE INDEX "EmailVerificationToken_userId_idx" ON "EmailVerificationToken"("userId");
-- CreateIndex
CREATE INDEX "EmailVerificationToken_expiresAt_idx" ON "EmailVerificationToken"("expiresAt");
-- AddForeignKey
ALTER TABLE "Organization" ADD CONSTRAINT "Organization_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Question" ADD CONSTRAINT "Question_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "DiagnosticModule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AnswerOption" ADD CONSTRAINT "AnswerOption_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "Question"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Response" ADD CONSTRAINT "Response_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Response" ADD CONSTRAINT "Response_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "Question"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Response" ADD CONSTRAINT "Response_answerOptionId_fkey" FOREIGN KEY ("answerOptionId") REFERENCES "AnswerOption"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AssessmentResult" ADD CONSTRAINT "AssessmentResult_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AssessmentResult" ADD CONSTRAINT "AssessmentResult_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "DiagnosticModule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Recommendation" ADD CONSTRAINT "Recommendation_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "DiagnosticModule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmailVerificationToken" ADD CONSTRAINT "EmailVerificationToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,50 @@
-- CreateEnum
CREATE TYPE "OrganizationDocumentType" AS ENUM ('ACTA_CONSTITUTIVA');
-- AlterTable
ALTER TABLE "Organization"
ADD COLUMN "rfc" TEXT,
ADD COLUMN "legalRepresentative" TEXT,
ADD COLUMN "incorporationDate" TEXT,
ADD COLUMN "deedNumber" TEXT,
ADD COLUMN "notaryName" TEXT,
ADD COLUMN "fiscalAddress" TEXT,
ADD COLUMN "businessPurpose" TEXT,
ADD COLUMN "actaExtractedData" JSONB,
ADD COLUMN "actaUploadedAt" TIMESTAMP(3),
ADD COLUMN "onboardingCompletedAt" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "OrganizationDocument" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" "OrganizationDocumentType" NOT NULL,
"fileName" TEXT NOT NULL,
"storedFileName" TEXT NOT NULL,
"filePath" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"sizeBytes" INTEGER NOT NULL,
"checksumSha256" TEXT,
"extractedData" JSONB,
"extractedTextSnippet" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "OrganizationDocument_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "OrganizationDocument_organizationId_type_key" ON "OrganizationDocument"("organizationId", "type");
-- CreateIndex
CREATE UNIQUE INDEX "OrganizationDocument_userId_type_key" ON "OrganizationDocument"("userId", "type");
-- CreateIndex
CREATE INDEX "OrganizationDocument_type_idx" ON "OrganizationDocument"("type");
-- AddForeignKey
ALTER TABLE "OrganizationDocument" ADD CONSTRAINT "OrganizationDocument_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrganizationDocument" ADD CONSTRAINT "OrganizationDocument_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Organization"
ADD COLUMN "stateOfIncorporation" TEXT,
ADD COLUMN "companyType" TEXT,
ADD COLUMN "actaLookupDictionary" JSONB;

View File

@@ -0,0 +1,12 @@
-- AlterTable
ALTER TABLE "Organization"
ADD COLUMN "tradeName" TEXT,
ADD COLUMN "operatingState" TEXT,
ADD COLUMN "municipality" TEXT,
ADD COLUMN "yearsOfOperation" TEXT,
ADD COLUMN "annualRevenueRange" TEXT,
ADD COLUMN "hasGovernmentContracts" BOOLEAN;
-- AlterTable
ALTER TABLE "Response"
ADD COLUMN "evidence" JSONB;

View File

@@ -0,0 +1,42 @@
-- CreateEnum
CREATE TYPE "StrategicDiagnosticEvidenceSection" AS ENUM ('TECHNICAL', 'EXPERIENCE', 'ORGANIZATION', 'PUBLIC_PROCUREMENT');
-- AlterTable
ALTER TABLE "Organization"
ADD COLUMN "strategicDiagnosticData" JSONB,
ADD COLUMN "strategicDiagnosticScores" JSONB,
ADD COLUMN "strategicDiagnosticCompletedAt" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "StrategicDiagnosticEvidenceDocument" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"section" "StrategicDiagnosticEvidenceSection" NOT NULL,
"category" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"storedFileName" TEXT NOT NULL,
"filePath" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"sizeBytes" INTEGER NOT NULL,
"checksumSha256" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "StrategicDiagnosticEvidenceDocument_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "StrategicDiagnosticEvidenceDocument_organizationId_section_idx" ON "StrategicDiagnosticEvidenceDocument"("organizationId", "section");
-- CreateIndex
CREATE INDEX "StrategicDiagnosticEvidenceDocument_userId_section_idx" ON "StrategicDiagnosticEvidenceDocument"("userId", "section");
-- CreateIndex
CREATE INDEX "StrategicDiagnosticEvidenceDocument_createdAt_idx" ON "StrategicDiagnosticEvidenceDocument"("createdAt");
-- AddForeignKey
ALTER TABLE "StrategicDiagnosticEvidenceDocument" ADD CONSTRAINT "StrategicDiagnosticEvidenceDocument_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StrategicDiagnosticEvidenceDocument" ADD CONSTRAINT "StrategicDiagnosticEvidenceDocument_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,134 @@
-- CreateEnum
CREATE TYPE "LicitationSource" AS ENUM ('PNT', 'MUNICIPAL_BACKUP');
-- CreateEnum
CREATE TYPE "LicitationProcedureType" AS ENUM ('LICITACION_PUBLICA', 'INVITACION_RESTRINGIDA', 'ADJUDICACION_DIRECTA', 'UNKNOWN');
-- CreateEnum
CREATE TYPE "LicitationCategory" AS ENUM ('GOODS', 'SERVICES', 'WORKS', 'MIXED', 'UNKNOWN');
-- CreateEnum
CREATE TYPE "SyncRunStatus" AS ENUM ('SUCCESS', 'PARTIAL', 'FAILED');
-- CreateTable
CREATE TABLE "Municipality" (
"id" TEXT NOT NULL,
"stateCode" TEXT NOT NULL,
"stateName" TEXT NOT NULL,
"municipalityCode" TEXT NOT NULL,
"municipalityName" TEXT NOT NULL,
"pntSubjectId" TEXT,
"pntEntityId" TEXT,
"pntSectorId" TEXT,
"pntEntryUrl" TEXT,
"backupUrl" TEXT,
"scrapingEnabled" BOOLEAN NOT NULL DEFAULT true,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Municipality_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Licitation" (
"id" TEXT NOT NULL,
"municipalityId" TEXT NOT NULL,
"source" "LicitationSource" NOT NULL,
"sourceRecordId" TEXT NOT NULL,
"procedureType" "LicitationProcedureType" NOT NULL DEFAULT 'UNKNOWN',
"title" TEXT NOT NULL,
"description" TEXT,
"category" "LicitationCategory" DEFAULT 'UNKNOWN',
"publishDate" TIMESTAMP(3),
"eventDates" JSONB,
"amount" DECIMAL(14,2),
"currency" TEXT,
"status" TEXT,
"supplierAwarded" TEXT,
"documents" JSONB,
"rawSourceUrl" TEXT,
"rawPayload" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Licitation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SyncRun" (
"id" TEXT NOT NULL,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"finishedAt" TIMESTAMP(3),
"municipalityId" TEXT,
"source" "LicitationSource" NOT NULL,
"status" "SyncRunStatus" NOT NULL DEFAULT 'SUCCESS',
"stats" JSONB,
"error" TEXT,
CONSTRAINT "SyncRun_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CompanyProfile" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"organizationId" TEXT,
"locations" JSONB,
"categoriesSupported" JSONB,
"keywords" JSONB,
"minAmount" DECIMAL(14,2),
"maxAmount" DECIMAL(14,2),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CompanyProfile_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Municipality_stateCode_municipalityCode_key" ON "Municipality"("stateCode", "municipalityCode");
-- CreateIndex
CREATE INDEX "Municipality_stateCode_municipalityName_idx" ON "Municipality"("stateCode", "municipalityName");
-- CreateIndex
CREATE INDEX "Municipality_isActive_scrapingEnabled_idx" ON "Municipality"("isActive", "scrapingEnabled");
-- CreateIndex
CREATE UNIQUE INDEX "Licitation_municipalityId_source_sourceRecordId_key" ON "Licitation"("municipalityId", "source", "sourceRecordId");
-- CreateIndex
CREATE INDEX "Licitation_municipalityId_publishDate_idx" ON "Licitation"("municipalityId", "publishDate");
-- CreateIndex
CREATE INDEX "Licitation_procedureType_category_idx" ON "Licitation"("procedureType", "category");
-- CreateIndex
CREATE INDEX "Licitation_amount_idx" ON "Licitation"("amount");
-- CreateIndex
CREATE INDEX "Licitation_createdAt_idx" ON "Licitation"("createdAt");
-- CreateIndex
CREATE INDEX "SyncRun_municipalityId_startedAt_idx" ON "SyncRun"("municipalityId", "startedAt");
-- CreateIndex
CREATE INDEX "SyncRun_source_status_startedAt_idx" ON "SyncRun"("source", "status", "startedAt");
-- CreateIndex
CREATE UNIQUE INDEX "CompanyProfile_userId_key" ON "CompanyProfile"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "CompanyProfile_organizationId_key" ON "CompanyProfile"("organizationId");
-- AddForeignKey
ALTER TABLE "Licitation" ADD CONSTRAINT "Licitation_municipalityId_fkey" FOREIGN KEY ("municipalityId") REFERENCES "Municipality"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SyncRun" ADD CONSTRAINT "SyncRun_municipalityId_fkey" FOREIGN KEY ("municipalityId") REFERENCES "Municipality"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CompanyProfile" ADD CONSTRAINT "CompanyProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CompanyProfile" ADD CONSTRAINT "CompanyProfile_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,23 @@
-- AlterEnum
ALTER TYPE "LicitationSource" ADD VALUE IF NOT EXISTS 'MUNICIPAL_OPEN_PORTAL';
-- CreateEnum
CREATE TYPE "MunicipalOpenPortalType" AS ENUM ('GENERIC', 'SAN_PEDRO_ASPX');
-- AlterTable
ALTER TABLE "Municipality"
ADD COLUMN "openPortalUrl" TEXT,
ADD COLUMN "openPortalType" "MunicipalOpenPortalType" NOT NULL DEFAULT 'GENERIC',
ADD COLUMN "openSyncIntervalDays" INTEGER NOT NULL DEFAULT 7,
ADD COLUMN "lastOpenSyncAt" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "Licitation"
ADD COLUMN "tenderCode" TEXT,
ADD COLUMN "isOpen" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "openingDate" TIMESTAMP(3),
ADD COLUMN "closingDate" TIMESTAMP(3),
ADD COLUMN "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- CreateIndex
CREATE INDEX "Licitation_municipalityId_isOpen_closingDate_idx" ON "Licitation"("municipalityId", "isOpen", "closingDate");

View File

@@ -0,0 +1,99 @@
-- CreateEnum
CREATE TYPE "WorkshopProgressStatus" AS ENUM ('NOT_STARTED', 'WATCHED', 'EVIDENCE_SUBMITTED', 'APPROVED', 'REJECTED', 'SKIPPED');
-- CreateEnum
CREATE TYPE "WorkshopEvidenceValidationStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED', 'ERROR');
-- CreateTable
CREATE TABLE "DevelopmentWorkshop" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"moduleId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT NOT NULL,
"videoUrl" TEXT NOT NULL,
"durationMinutes" INTEGER NOT NULL DEFAULT 0,
"evidenceRequired" TEXT NOT NULL,
"learningObjectives" JSONB,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DevelopmentWorkshop_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DevelopmentWorkshopProgress" (
"id" TEXT NOT NULL,
"workshopId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"status" "WorkshopProgressStatus" NOT NULL DEFAULT 'NOT_STARTED',
"watchedAt" TIMESTAMP(3),
"skippedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DevelopmentWorkshopProgress_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DevelopmentWorkshopEvidence" (
"id" TEXT NOT NULL,
"workshopId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"validationStatus" "WorkshopEvidenceValidationStatus" NOT NULL DEFAULT 'PENDING',
"validationReason" TEXT,
"validationConfidence" DOUBLE PRECISION,
"validatedAt" TIMESTAMP(3),
"fileName" TEXT NOT NULL,
"storedFileName" TEXT NOT NULL,
"filePath" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"sizeBytes" INTEGER NOT NULL,
"checksumSha256" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DevelopmentWorkshopEvidence_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "DevelopmentWorkshop_key_key" ON "DevelopmentWorkshop"("key");
-- CreateIndex
CREATE INDEX "DevelopmentWorkshop_moduleId_sortOrder_idx" ON "DevelopmentWorkshop"("moduleId", "sortOrder");
-- CreateIndex
CREATE INDEX "DevelopmentWorkshop_isActive_sortOrder_idx" ON "DevelopmentWorkshop"("isActive", "sortOrder");
-- CreateIndex
CREATE UNIQUE INDEX "DevelopmentWorkshopProgress_workshopId_userId_key" ON "DevelopmentWorkshopProgress"("workshopId", "userId");
-- CreateIndex
CREATE INDEX "DevelopmentWorkshopProgress_userId_status_idx" ON "DevelopmentWorkshopProgress"("userId", "status");
-- CreateIndex
CREATE INDEX "DevelopmentWorkshopProgress_workshopId_status_idx" ON "DevelopmentWorkshopProgress"("workshopId", "status");
-- CreateIndex
CREATE INDEX "DevelopmentWorkshopEvidence_userId_workshopId_createdAt_idx" ON "DevelopmentWorkshopEvidence"("userId", "workshopId", "createdAt");
-- CreateIndex
CREATE INDEX "DevelopmentWorkshopEvidence_workshopId_validationStatus_idx" ON "DevelopmentWorkshopEvidence"("workshopId", "validationStatus");
-- AddForeignKey
ALTER TABLE "DevelopmentWorkshop" ADD CONSTRAINT "DevelopmentWorkshop_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "DiagnosticModule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DevelopmentWorkshopProgress" ADD CONSTRAINT "DevelopmentWorkshopProgress_workshopId_fkey" FOREIGN KEY ("workshopId") REFERENCES "DevelopmentWorkshop"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DevelopmentWorkshopProgress" ADD CONSTRAINT "DevelopmentWorkshopProgress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DevelopmentWorkshopEvidence" ADD CONSTRAINT "DevelopmentWorkshopEvidence_workshopId_fkey" FOREIGN KEY ("workshopId") REFERENCES "DevelopmentWorkshop"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DevelopmentWorkshopEvidence" ADD CONSTRAINT "DevelopmentWorkshopEvidence_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

477
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,477 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum PriorityLevel {
LOW
MEDIUM
HIGH
}
enum UserRole {
USER
ADMIN
}
enum ContentPageType {
FAQ
MANUAL
}
enum OverallScoreMethod {
EQUAL_ALL_MODULES
EQUAL_ANSWERED_MODULES
WEIGHTED_ANSWERED_MODULES
}
enum OrganizationDocumentType {
ACTA_CONSTITUTIVA
}
enum StrategicDiagnosticEvidenceSection {
TECHNICAL
EXPERIENCE
ORGANIZATION
PUBLIC_PROCUREMENT
}
enum WorkshopProgressStatus {
NOT_STARTED
WATCHED
EVIDENCE_SUBMITTED
APPROVED
REJECTED
SKIPPED
}
enum WorkshopEvidenceValidationStatus {
PENDING
APPROVED
REJECTED
ERROR
}
enum LicitationSource {
MUNICIPAL_OPEN_PORTAL
PNT
MUNICIPAL_BACKUP
}
enum LicitationProcedureType {
LICITACION_PUBLICA
INVITACION_RESTRINGIDA
ADJUDICACION_DIRECTA
UNKNOWN
}
enum LicitationCategory {
GOODS
SERVICES
WORKS
MIXED
UNKNOWN
}
enum SyncRunStatus {
SUCCESS
PARTIAL
FAILED
}
enum MunicipalOpenPortalType {
GENERIC
SAN_PEDRO_ASPX
}
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
name String?
role UserRole @default(USER)
emailVerifiedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization?
companyProfile CompanyProfile?
organizationDocs OrganizationDocument[]
strategicDiagnosticEvidenceDocs StrategicDiagnosticEvidenceDocument[]
workshopProgresses DevelopmentWorkshopProgress[]
workshopEvidenceDocs DevelopmentWorkshopEvidence[]
verificationTokens EmailVerificationToken[]
responses Response[]
results AssessmentResult[]
}
model Organization {
id String @id @default(cuid())
userId String @unique
name String
tradeName String?
rfc String?
legalRepresentative String?
incorporationDate String?
deedNumber String?
notaryName String?
stateOfIncorporation String?
companyType String?
fiscalAddress String?
businessPurpose String?
industry String?
operatingState String?
municipality String?
companySize String?
yearsOfOperation String?
annualRevenueRange String?
hasGovernmentContracts Boolean?
country String?
primaryObjective String?
actaExtractedData Json?
actaLookupDictionary Json?
actaUploadedAt DateTime?
onboardingCompletedAt DateTime?
strategicDiagnosticData Json?
strategicDiagnosticScores Json?
strategicDiagnosticCompletedAt DateTime?
companyProfile CompanyProfile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
documents OrganizationDocument[]
strategicDiagnosticEvidenceDocs StrategicDiagnosticEvidenceDocument[]
}
model OrganizationDocument {
id String @id @default(cuid())
organizationId String
userId String
type OrganizationDocumentType
fileName String
storedFileName String
filePath String
mimeType String
sizeBytes Int
checksumSha256 String?
extractedData Json?
extractedTextSnippet String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([organizationId, type])
@@unique([userId, type])
@@index([type])
}
model StrategicDiagnosticEvidenceDocument {
id String @id @default(cuid())
organizationId String
userId String
section StrategicDiagnosticEvidenceSection
category String
fileName String
storedFileName String
filePath String
mimeType String
sizeBytes Int
checksumSha256 String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([organizationId, section])
@@index([userId, section])
@@index([createdAt])
}
model DiagnosticModule {
id String @id @default(cuid())
key String @unique
name String
description String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
questions Question[]
results AssessmentResult[]
recommendations Recommendation[]
workshops DevelopmentWorkshop[]
}
model DevelopmentWorkshop {
id String @id @default(cuid())
key String @unique
moduleId String
title String
summary String
videoUrl String
durationMinutes Int @default(0)
evidenceRequired String
learningObjectives Json?
sortOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
module DiagnosticModule @relation(fields: [moduleId], references: [id], onDelete: Cascade)
progresses DevelopmentWorkshopProgress[]
evidences DevelopmentWorkshopEvidence[]
@@index([moduleId, sortOrder])
@@index([isActive, sortOrder])
}
model DevelopmentWorkshopProgress {
id String @id @default(cuid())
workshopId String
userId String
status WorkshopProgressStatus @default(NOT_STARTED)
watchedAt DateTime?
skippedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
workshop DevelopmentWorkshop @relation(fields: [workshopId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([workshopId, userId])
@@index([userId, status])
@@index([workshopId, status])
}
model DevelopmentWorkshopEvidence {
id String @id @default(cuid())
workshopId String
userId String
validationStatus WorkshopEvidenceValidationStatus @default(PENDING)
validationReason String?
validationConfidence Float?
validatedAt DateTime?
fileName String
storedFileName String
filePath String
mimeType String
sizeBytes Int
checksumSha256 String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
workshop DevelopmentWorkshop @relation(fields: [workshopId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, workshopId, createdAt])
@@index([workshopId, validationStatus])
}
model Question {
id String @id @default(cuid())
key String @unique
moduleId String
prompt String
helpText String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
module DiagnosticModule @relation(fields: [moduleId], references: [id], onDelete: Cascade)
answerOptions AnswerOption[]
responses Response[]
@@index([moduleId, sortOrder])
}
model AnswerOption {
id String @id @default(cuid())
key String @unique
questionId String
label String
weight Int
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
responses Response[]
@@index([questionId, sortOrder])
}
model Response {
id String @id @default(cuid())
userId String
questionId String
answerOptionId String
evidence Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
answerOption AnswerOption @relation(fields: [answerOptionId], references: [id], onDelete: Cascade)
@@unique([userId, questionId])
@@index([userId])
}
model AssessmentResult {
id String @id @default(cuid())
userId String
moduleId String?
overallScore Float?
moduleScore Float?
metadata Json?
calculatedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
module DiagnosticModule? @relation(fields: [moduleId], references: [id], onDelete: SetNull)
@@index([userId, calculatedAt])
@@index([moduleId])
}
model Recommendation {
id String @id @default(cuid())
key String @unique
moduleId String?
title String
description String
priority PriorityLevel @default(MEDIUM)
isTemplate Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
module DiagnosticModule? @relation(fields: [moduleId], references: [id], onDelete: SetNull)
@@index([moduleId])
}
model ContentPage {
id String @id @default(cuid())
type ContentPageType
slug String @unique
title String
content String
sortOrder Int @default(0)
isPublished Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([type, sortOrder])
}
model ScoringConfig {
id String @id @default(cuid())
key String @unique
lowScoreThreshold Int @default(70)
overallScoreMethod OverallScoreMethod @default(EQUAL_ALL_MODULES)
moduleWeights Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model EmailVerificationToken {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
consumedAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([expiresAt])
}
model Municipality {
id String @id @default(cuid())
stateCode String
stateName String
municipalityCode String
municipalityName String
openPortalUrl String?
openPortalType MunicipalOpenPortalType @default(GENERIC)
openSyncIntervalDays Int @default(7)
lastOpenSyncAt DateTime?
pntSubjectId String?
pntEntityId String?
pntSectorId String?
pntEntryUrl String?
backupUrl String?
scrapingEnabled Boolean @default(true)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
licitations Licitation[]
syncRuns SyncRun[]
@@unique([stateCode, municipalityCode])
@@index([stateCode, municipalityName])
@@index([isActive, scrapingEnabled])
}
model Licitation {
id String @id @default(cuid())
municipalityId String
source LicitationSource
sourceRecordId String
tenderCode String?
procedureType LicitationProcedureType @default(UNKNOWN)
title String
description String?
category LicitationCategory? @default(UNKNOWN)
isOpen Boolean @default(true)
openingDate DateTime?
closingDate DateTime?
publishDate DateTime?
eventDates Json?
amount Decimal? @db.Decimal(14, 2)
currency String?
status String?
supplierAwarded String?
documents Json?
rawSourceUrl String?
rawPayload Json
lastSeenAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
municipality Municipality @relation(fields: [municipalityId], references: [id], onDelete: Cascade)
@@unique([municipalityId, source, sourceRecordId])
@@index([municipalityId, isOpen, closingDate])
@@index([municipalityId, publishDate])
@@index([procedureType, category])
@@index([amount])
@@index([createdAt])
}
model SyncRun {
id String @id @default(cuid())
startedAt DateTime @default(now())
finishedAt DateTime?
municipalityId String?
source LicitationSource
status SyncRunStatus @default(SUCCESS)
stats Json?
error String?
municipality Municipality? @relation(fields: [municipalityId], references: [id], onDelete: SetNull)
@@index([municipalityId, startedAt])
@@index([source, status, startedAt])
}
model CompanyProfile {
id String @id @default(cuid())
userId String @unique
organizationId String? @unique
locations Json?
categoriesSupported Json?
keywords Json?
minAmount Decimal? @db.Decimal(14, 2)
maxAmount Decimal? @db.Decimal(14, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
}

729
prisma/seed.mjs Normal file
View File

@@ -0,0 +1,729 @@
import { PrismaClient, ContentPageType, OverallScoreMethod, PriorityLevel } from "@prisma/client";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const prisma = new PrismaClient();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function yesNoOptions(questionKey) {
return [
{ key: `${questionKey}-opt-yes`, label: "Si", weight: 5, sortOrder: 1 },
{ key: `${questionKey}-opt-no`, label: "No", weight: 0, sortOrder: 2 },
];
}
const moduleSeeds = [
{
key: "liderazgo-vision-estrategica",
name: "Liderazgo y Vision Estrategica",
description: "Capacidad de la direccion para definir y comunicar una vision clara orientada al valor publico.",
sortOrder: 1,
questions: [
{
key: "liderazgo-vision-estrategica-q1",
prompt: "La direccion tiene una vision clara del proposito de la empresa mas alla de las ganancias?",
helpText: null,
sortOrder: 1,
options: yesNoOptions("liderazgo-vision-estrategica-q1"),
},
{
key: "liderazgo-vision-estrategica-q2",
prompt: "Se comunica regularmente la estrategia empresarial a todo el equipo?",
helpText: null,
sortOrder: 2,
options: yesNoOptions("liderazgo-vision-estrategica-q2"),
},
{
key: "liderazgo-vision-estrategica-q3",
prompt: "Existen objetivos medibles alineados con la generacion de valor publico?",
helpText: null,
sortOrder: 3,
options: yesNoOptions("liderazgo-vision-estrategica-q3"),
},
{
key: "liderazgo-vision-estrategica-q4",
prompt: "La direccion participa activamente en la toma de decisiones estrategicas?",
helpText: null,
sortOrder: 4,
options: yesNoOptions("liderazgo-vision-estrategica-q4"),
},
{
key: "liderazgo-vision-estrategica-q5",
prompt: "Se revisan y ajustan los planes estrategicos al menos anualmente?",
helpText: null,
sortOrder: 5,
options: yesNoOptions("liderazgo-vision-estrategica-q5"),
},
],
},
{
key: "cultura-organizacional",
name: "Cultura Organizacional",
description: "Valores, comportamientos y mentalidad orientados a la excelencia y el impacto social.",
sortOrder: 2,
questions: [
{
key: "cultura-organizacional-q1",
prompt: "La empresa promueve valores de integridad y etica en sus operaciones?",
helpText: null,
sortOrder: 1,
options: yesNoOptions("cultura-organizacional-q1"),
},
{
key: "cultura-organizacional-q2",
prompt: "Existe un ambiente de trabajo colaborativo y respetuoso?",
helpText: null,
sortOrder: 2,
options: yesNoOptions("cultura-organizacional-q2"),
},
{
key: "cultura-organizacional-q3",
prompt: "Se fomenta la mejora continua y el aprendizaje organizacional?",
helpText: null,
sortOrder: 3,
options: yesNoOptions("cultura-organizacional-q3"),
},
{
key: "cultura-organizacional-q4",
prompt: "Los empleados conocen y practican los valores de la empresa?",
helpText: null,
sortOrder: 4,
options: yesNoOptions("cultura-organizacional-q4"),
},
{
key: "cultura-organizacional-q5",
prompt: "Se reconoce y celebra el buen desempeno del equipo?",
helpText: null,
sortOrder: 5,
options: yesNoOptions("cultura-organizacional-q5"),
},
],
},
{
key: "estructura-procesos",
name: "Estructura y Procesos",
description: "Organizacion interna, procedimientos y sistemas de gestion eficientes.",
sortOrder: 3,
questions: [
{
key: "estructura-procesos-q1",
prompt: "Existen procesos documentados para las operaciones principales?",
helpText: null,
sortOrder: 1,
options: yesNoOptions("estructura-procesos-q1"),
},
{
key: "estructura-procesos-q2",
prompt: "La empresa cuenta con un organigrama claro y funcional?",
helpText: null,
sortOrder: 2,
options: yesNoOptions("estructura-procesos-q2"),
},
{
key: "estructura-procesos-q3",
prompt: "Se utilizan herramientas digitales para la gestion del negocio?",
helpText: null,
sortOrder: 3,
options: yesNoOptions("estructura-procesos-q3"),
},
{
key: "estructura-procesos-q4",
prompt: "Existe un sistema de control de calidad en productos o servicios?",
helpText: null,
sortOrder: 4,
options: yesNoOptions("estructura-procesos-q4"),
},
{
key: "estructura-procesos-q5",
prompt: "Se llevan registros financieros y contables actualizados?",
helpText: null,
sortOrder: 5,
options: yesNoOptions("estructura-procesos-q5"),
},
],
},
{
key: "innovacion-sostenibilidad",
name: "Innovacion y Sostenibilidad",
description: "Capacidad de adaptacion, mejora continua y practicas sostenibles.",
sortOrder: 4,
questions: [
{
key: "innovacion-sostenibilidad-q1",
prompt: "La empresa invierte en innovacion de productos, servicios o procesos?",
helpText: null,
sortOrder: 1,
options: yesNoOptions("innovacion-sostenibilidad-q1"),
},
{
key: "innovacion-sostenibilidad-q2",
prompt: "Se implementan practicas de sostenibilidad ambiental?",
helpText: null,
sortOrder: 2,
options: yesNoOptions("innovacion-sostenibilidad-q2"),
},
{
key: "innovacion-sostenibilidad-q3",
prompt: "Existe disposicion para adoptar nuevas tecnologias?",
helpText: null,
sortOrder: 3,
options: yesNoOptions("innovacion-sostenibilidad-q3"),
},
{
key: "innovacion-sostenibilidad-q4",
prompt: "Se monitorean tendencias del mercado y competencia?",
helpText: null,
sortOrder: 4,
options: yesNoOptions("innovacion-sostenibilidad-q4"),
},
{
key: "innovacion-sostenibilidad-q5",
prompt: "Se busca activamente la eficiencia en el uso de recursos?",
helpText: null,
sortOrder: 5,
options: yesNoOptions("innovacion-sostenibilidad-q5"),
},
],
},
{
key: "impacto-social-equidad",
name: "Impacto Social y Equidad",
description: "Contribucion a la comunidad, inclusion y responsabilidad social.",
sortOrder: 5,
questions: [
{
key: "impacto-social-equidad-q1",
prompt: "La empresa contribuye positivamente a la comunidad local?",
helpText: null,
sortOrder: 1,
options: yesNoOptions("impacto-social-equidad-q1"),
},
{
key: "impacto-social-equidad-q2",
prompt: "Se promueve la equidad de genero en la organizacion?",
helpText: null,
sortOrder: 2,
options: yesNoOptions("impacto-social-equidad-q2"),
},
{
key: "impacto-social-equidad-q3",
prompt: "Existen politicas de inclusion para grupos vulnerables?",
helpText: null,
sortOrder: 3,
options: yesNoOptions("impacto-social-equidad-q3"),
},
{
key: "impacto-social-equidad-q4",
prompt: "Se considera el impacto social en las decisiones de negocio?",
helpText: null,
sortOrder: 4,
options: yesNoOptions("impacto-social-equidad-q4"),
},
{
key: "impacto-social-equidad-q5",
prompt: "La empresa participa en iniciativas de responsabilidad social?",
helpText: null,
sortOrder: 5,
options: yesNoOptions("impacto-social-equidad-q5"),
},
],
},
];
const recommendationSeeds = [
{
key: "rec-liderazgo-vision",
moduleKey: "liderazgo-vision-estrategica",
title: "Formalizar vision estrategica anual",
description: "Define metas anuales medibles y comunica el avance con revisiones trimestrales para todo el equipo.",
priority: PriorityLevel.HIGH,
},
{
key: "rec-cultura-integridad",
moduleKey: "cultura-organizacional",
title: "Reforzar cultura de integridad y colaboracion",
description: "Establece rutinas de reconocimiento y acciones concretas para fortalecer valores compartidos.",
priority: PriorityLevel.MEDIUM,
},
{
key: "rec-estructura-procesos",
moduleKey: "estructura-procesos",
title: "Estandarizar procesos y controles de calidad",
description: "Documenta procesos clave y asigna responsables para mantener registros operativos y financieros al dia.",
priority: PriorityLevel.HIGH,
},
{
key: "rec-innovacion-sostenibilidad",
moduleKey: "innovacion-sostenibilidad",
title: "Activar plan de innovacion sostenible",
description: "Prioriza proyectos de innovacion con impacto en eficiencia de recursos y seguimiento de tendencias del mercado.",
priority: PriorityLevel.MEDIUM,
},
{
key: "rec-impacto-social-equidad",
moduleKey: "impacto-social-equidad",
title: "Fortalecer estrategia de impacto social",
description: "Define iniciativas de inclusion y equidad con indicadores concretos y resultados verificables.",
priority: PriorityLevel.MEDIUM,
},
{
key: "rec-gobernanza-global",
moduleKey: null,
title: "Instalar comite mensual de madurez empresarial",
description: "Consolida resultados de los cinco modulos para priorizar inversiones y remover bloqueos.",
priority: PriorityLevel.LOW,
},
];
const workshopSeeds = [
{
key: "taller-liderazgo-hoja-ruta",
moduleKey: "liderazgo-vision-estrategica",
title: "Hoja de Ruta de Liderazgo Estrategico",
summary: "Alinea vision, objetivos y ritmos de seguimiento para mejorar la direccion estrategica del equipo.",
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
durationMinutes: 22,
evidenceRequired: "Acta de sesion estrategica con objetivos trimestrales y responsables asignados.",
learningObjectives: [
"Definir prioridades estrategicas medibles",
"Comunicar metas de forma transversal",
"Establecer seguimiento mensual de resultados",
],
sortOrder: 1,
},
{
key: "taller-cultura-colaborativa",
moduleKey: "cultura-organizacional",
title: "Ambiente de Trabajo Colaborativo",
summary: "Fortalece dinamicas de comunicacion y colaboracion para elevar desempeno y compromiso del equipo.",
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
durationMinutes: 18,
evidenceRequired: "Fotografia o acta de una dinamica de integracion aplicada con tu equipo.",
learningObjectives: [
"Implementar dinamicas de integracion",
"Crear canales de comunicacion efectivos",
"Manejar conflictos constructivamente",
],
sortOrder: 1,
},
{
key: "taller-procesos-auditables",
moduleKey: "estructura-procesos",
title: "Procesos Auditables y Control Operativo",
summary: "Documenta procesos clave y define controles para mejorar trazabilidad y cumplimiento.",
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
durationMinutes: 25,
evidenceRequired: "Procedimiento documentado con roles, pasos, entradas y salidas del proceso.",
learningObjectives: [
"Mapear procesos criticos",
"Definir controles de calidad",
"Estandarizar evidencias operativas",
],
sortOrder: 1,
},
{
key: "taller-innovacion-sostenible",
moduleKey: "innovacion-sostenibilidad",
title: "Innovacion Aplicada con Enfoque Sostenible",
summary: "Disena mejoras de alto impacto para reducir costos y aumentar diferenciacion competitiva.",
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
durationMinutes: 20,
evidenceRequired: "Ficha de iniciativa de innovacion con costo estimado, impacto y cronograma.",
learningObjectives: [
"Identificar oportunidades de mejora continua",
"Evaluar impacto de iniciativas sostenibles",
"Priorizar proyectos de innovacion factibles",
],
sortOrder: 1,
},
{
key: "taller-impacto-social-evidencia",
moduleKey: "impacto-social-equidad",
title: "Impacto Social y Equidad con Evidencia",
summary: "Define acciones de impacto social medibles para fortalecer tu posicion en licitaciones.",
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
durationMinutes: 24,
evidenceRequired: "Plan de impacto social con objetivos, indicadores y responsables.",
learningObjectives: [
"Disenar iniciativas inclusivas",
"Definir indicadores de impacto verificables",
"Alinear acciones sociales con estrategia comercial",
],
sortOrder: 1,
},
];
const contentPageSeeds = [
{
slug: "faq-calculo-puntaje",
type: ContentPageType.FAQ,
title: "Como se calcula el puntaje global?",
content:
"El puntaje global se obtiene de la normalizacion de respuestas por modulo y un promedio ponderado entre modulos.",
sortOrder: 1,
},
{
slug: "faq-pausa-diagnostico",
type: ContentPageType.FAQ,
title: "Puedo pausar y continuar luego?",
content:
"Si. Las respuestas quedan guardadas por usuario para retomar en el ultimo punto completado del cuestionario.",
sortOrder: 2,
},
{
slug: "manual-ruta-completa",
type: ContentPageType.MANUAL,
title: "Ruta completa de uso de la plataforma",
content:
"Registro, verificacion de correo, onboarding, diagnostico por modulos, resultados y recomendaciones accionables.",
sortOrder: 1,
},
{
slug: "manual-interpretacion-dashboard",
type: ContentPageType.MANUAL,
title: "Interpretacion de dashboard",
content:
"Use barras para avance relativo por modulo y radar para comparacion de madurez entre capacidades clave.",
sortOrder: 2,
},
];
async function upsertDiagnosticStructure() {
const moduleKeys = moduleSeeds.map((moduleSeed) => moduleSeed.key);
await prisma.diagnosticModule.deleteMany({
where: {
key: {
notIn: moduleKeys,
},
},
});
for (const moduleSeed of moduleSeeds) {
const moduleRecord = await prisma.diagnosticModule.upsert({
where: { key: moduleSeed.key },
update: {
name: moduleSeed.name,
description: moduleSeed.description,
sortOrder: moduleSeed.sortOrder,
},
create: {
key: moduleSeed.key,
name: moduleSeed.name,
description: moduleSeed.description,
sortOrder: moduleSeed.sortOrder,
},
});
const questionKeys = moduleSeed.questions.map((questionSeed) => questionSeed.key);
await prisma.question.deleteMany({
where: {
moduleId: moduleRecord.id,
key: {
notIn: questionKeys,
},
},
});
for (const questionSeed of moduleSeed.questions) {
const questionRecord = await prisma.question.upsert({
where: { key: questionSeed.key },
update: {
moduleId: moduleRecord.id,
prompt: questionSeed.prompt,
helpText: questionSeed.helpText,
sortOrder: questionSeed.sortOrder,
},
create: {
key: questionSeed.key,
moduleId: moduleRecord.id,
prompt: questionSeed.prompt,
helpText: questionSeed.helpText,
sortOrder: questionSeed.sortOrder,
},
});
const optionKeys = questionSeed.options.map((optionSeed) => optionSeed.key);
await prisma.answerOption.deleteMany({
where: {
questionId: questionRecord.id,
key: {
notIn: optionKeys,
},
},
});
for (const optionSeed of questionSeed.options) {
await prisma.answerOption.upsert({
where: { key: optionSeed.key },
update: {
questionId: questionRecord.id,
label: optionSeed.label,
weight: optionSeed.weight,
sortOrder: optionSeed.sortOrder,
},
create: {
key: optionSeed.key,
questionId: questionRecord.id,
label: optionSeed.label,
weight: optionSeed.weight,
sortOrder: optionSeed.sortOrder,
},
});
}
}
}
}
async function upsertRecommendations() {
const recommendationKeys = recommendationSeeds.map((recommendationSeed) => recommendationSeed.key);
const moduleLookup = new Map(
(await prisma.diagnosticModule.findMany({ select: { id: true, key: true } })).map((moduleRecord) => [moduleRecord.key, moduleRecord.id]),
);
await prisma.recommendation.deleteMany({
where: {
key: {
notIn: recommendationKeys,
},
},
});
for (const recommendationSeed of recommendationSeeds) {
const moduleId = recommendationSeed.moduleKey ? moduleLookup.get(recommendationSeed.moduleKey) ?? null : null;
await prisma.recommendation.upsert({
where: { key: recommendationSeed.key },
update: {
moduleId,
title: recommendationSeed.title,
description: recommendationSeed.description,
priority: recommendationSeed.priority,
isTemplate: true,
},
create: {
key: recommendationSeed.key,
moduleId,
title: recommendationSeed.title,
description: recommendationSeed.description,
priority: recommendationSeed.priority,
isTemplate: true,
},
});
}
}
async function upsertDevelopmentWorkshops() {
const workshopKeys = workshopSeeds.map((workshopSeed) => workshopSeed.key);
const moduleLookup = new Map(
(await prisma.diagnosticModule.findMany({ select: { id: true, key: true } })).map((moduleRecord) => [moduleRecord.key, moduleRecord.id]),
);
await prisma.developmentWorkshop.deleteMany({
where: {
key: {
notIn: workshopKeys,
},
},
});
for (const workshopSeed of workshopSeeds) {
const moduleId = moduleLookup.get(workshopSeed.moduleKey);
if (!moduleId) {
// Skip orphan workshop seeds when module is unavailable.
continue;
}
await prisma.developmentWorkshop.upsert({
where: { key: workshopSeed.key },
update: {
moduleId,
title: workshopSeed.title,
summary: workshopSeed.summary,
videoUrl: workshopSeed.videoUrl,
durationMinutes: workshopSeed.durationMinutes,
evidenceRequired: workshopSeed.evidenceRequired,
learningObjectives: workshopSeed.learningObjectives,
sortOrder: workshopSeed.sortOrder,
isActive: true,
},
create: {
key: workshopSeed.key,
moduleId,
title: workshopSeed.title,
summary: workshopSeed.summary,
videoUrl: workshopSeed.videoUrl,
durationMinutes: workshopSeed.durationMinutes,
evidenceRequired: workshopSeed.evidenceRequired,
learningObjectives: workshopSeed.learningObjectives,
sortOrder: workshopSeed.sortOrder,
isActive: true,
},
});
}
}
async function upsertContentPages() {
for (const pageSeed of contentPageSeeds) {
await prisma.contentPage.upsert({
where: { slug: pageSeed.slug },
update: {
type: pageSeed.type,
title: pageSeed.title,
content: pageSeed.content,
sortOrder: pageSeed.sortOrder,
isPublished: true,
},
create: {
slug: pageSeed.slug,
type: pageSeed.type,
title: pageSeed.title,
content: pageSeed.content,
sortOrder: pageSeed.sortOrder,
isPublished: true,
},
});
}
}
async function upsertDefaultScoringConfig() {
await prisma.scoringConfig.upsert({
where: { key: "default" },
update: {
lowScoreThreshold: 70,
overallScoreMethod: OverallScoreMethod.EQUAL_ALL_MODULES,
moduleWeights: {},
},
create: {
key: "default",
lowScoreThreshold: 70,
overallScoreMethod: OverallScoreMethod.EQUAL_ALL_MODULES,
moduleWeights: {},
},
});
}
async function loadMunicipalitySeeds() {
const filePath = path.join(__dirname, "data", "municipalities.json");
const content = await readFile(filePath, "utf-8");
const parsed = JSON.parse(content);
if (!Array.isArray(parsed)) {
throw new Error("Invalid municipalities seed file format.");
}
return parsed.filter((item) => {
return (
item &&
typeof item.stateCode === "string" &&
typeof item.stateName === "string" &&
typeof item.municipalityCode === "string" &&
typeof item.municipalityName === "string"
);
});
}
async function upsertMunicipalities() {
const municipalities = await loadMunicipalitySeeds();
const activeKeys = municipalities.map((item) => `${item.stateCode}-${item.municipalityCode}`);
await prisma.municipality.updateMany({
where: {
NOT: municipalities.map((item) => ({
stateCode: item.stateCode,
municipalityCode: item.municipalityCode,
})),
},
data: {
isActive: false,
},
});
for (const municipality of municipalities) {
await prisma.municipality.upsert({
where: {
stateCode_municipalityCode: {
stateCode: municipality.stateCode,
municipalityCode: municipality.municipalityCode,
},
},
update: {
stateName: municipality.stateName,
municipalityName: municipality.municipalityName,
openPortalUrl: municipality.openPortalUrl ?? null,
openPortalType: municipality.openPortalType ?? "GENERIC",
openSyncIntervalDays:
typeof municipality.openSyncIntervalDays === "number" && municipality.openSyncIntervalDays > 0
? municipality.openSyncIntervalDays
: 7,
pntSubjectId: municipality.pntSubjectId ?? null,
pntEntityId: municipality.pntEntityId ?? null,
pntSectorId: municipality.pntSectorId ?? null,
pntEntryUrl: municipality.pntEntryUrl ?? null,
backupUrl: municipality.backupUrl ?? null,
scrapingEnabled: municipality.scrapingEnabled !== false,
isActive: municipality.isActive !== false,
},
create: {
stateCode: municipality.stateCode,
stateName: municipality.stateName,
municipalityCode: municipality.municipalityCode,
municipalityName: municipality.municipalityName,
openPortalUrl: municipality.openPortalUrl ?? null,
openPortalType: municipality.openPortalType ?? "GENERIC",
openSyncIntervalDays:
typeof municipality.openSyncIntervalDays === "number" && municipality.openSyncIntervalDays > 0
? municipality.openSyncIntervalDays
: 7,
pntSubjectId: municipality.pntSubjectId ?? null,
pntEntityId: municipality.pntEntityId ?? null,
pntSectorId: municipality.pntSectorId ?? null,
pntEntryUrl: municipality.pntEntryUrl ?? null,
backupUrl: municipality.backupUrl ?? null,
scrapingEnabled: municipality.scrapingEnabled !== false,
isActive: municipality.isActive !== false,
},
});
}
return activeKeys.length;
}
async function main() {
await upsertDiagnosticStructure();
await upsertDevelopmentWorkshops();
await upsertRecommendations();
await upsertContentPages();
await upsertDefaultScoringConfig();
const municipalitySeedCount = await upsertMunicipalities();
const moduleCount = await prisma.diagnosticModule.count();
const questionCount = await prisma.question.count();
const optionCount = await prisma.answerOption.count();
const workshopCount = await prisma.developmentWorkshop.count();
const recommendationCount = await prisma.recommendation.count();
const contentPageCount = await prisma.contentPage.count();
const municipalityCount = await prisma.municipality.count({ where: { isActive: true } });
console.log("Seed completed", {
modules: moduleCount,
questions: questionCount,
answerOptions: optionCount,
workshops: workshopCount,
recommendations: recommendationCount,
contentPages: contentPageCount,
municipalities: municipalityCount,
municipalitySeedsProcessed: municipalitySeedCount,
});
}
main()
.catch((error) => {
console.error("Seed failed", error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});