41 KiB
US-FUND-004: Infraestructura Técnica Base
Epic: MAI-001 - Fundamentos del Sistema Story Points: 12 Prioridad: Alta Dependencias:
- Ninguna (es la base del proyecto)
Estado: Pendiente Asignado a: DevOps + Backend Lead + Frontend Lead
📋 Historia de Usuario
Como equipo de desarrollo Quiero tener configurada toda la infraestructura técnica base del proyecto Para poder comenzar a desarrollar las funcionalidades del sistema de construcción sobre una base sólida, escalable y mantenible.
🎯 Contexto y Objetivos
Contexto
Esta historia cubre la configuración inicial completa del proyecto antes de comenzar el desarrollo de funcionalidades. Incluye:
- Base de datos PostgreSQL con schemas organizados
- Backend NestJS con estructura modular
- Frontend React + Vite con TypeScript
- Herramientas de desarrollo (linting, formatting, testing)
- CI/CD pipeline básico
- Docker containers para desarrollo local
Objetivos
- ✅ Proyecto ejecutable localmente en < 5 minutos (para nuevos devs)
- ✅ Estructura modular lista para escalar
- ✅ Database migrations automáticas
- ✅ Hot reload en desarrollo (backend y frontend)
- ✅ Code quality garantizada (pre-commit hooks)
- ✅ Tests automatizados en CI/CD
✅ Criterios de Aceptación
CA-1: Database Setup
Dado un PostgreSQL 15+ instalado localmente o en Docker Cuando ejecuto el script de setup inicial Entonces:
- ✅ Se crea la base de datos
erp_construccion - ✅ Se crean los schemas:
auth_management,projects,budgets,purchases,hr,gamification_system - ✅ Se ejecutan todas las migraciones iniciales
- ✅ Se crean las funciones de utilidad (get_current_constructora_id, etc.)
- ✅ Se habilita RLS en todas las tablas de negocio
CA-2: Backend Structure
Dado el proyecto de backend Cuando examino la estructura de carpetas Entonces:
- ✅ Existe una estructura modular clara:
apps/backend/src/ ├── modules/ │ ├── auth/ │ ├── users/ │ ├── constructoras/ │ ├── projects/ │ └── ... (módulos por dominio) ├── common/ │ ├── guards/ │ ├── decorators/ │ ├── interceptors/ │ └── filters/ ├── config/ │ ├── database.config.ts │ ├── jwt.config.ts │ └── ... └── main.ts - ✅ Cada módulo sigue el patrón:
module,controller,service,entity,dto - ✅ TypeORM configurado con migrations automáticas
- ✅ Swagger UI disponible en
/api/docs
CA-3: Frontend Structure
Dado el proyecto de frontend Cuando examino la estructura de carpetas Entonces:
- ✅ Existe una estructura clara:
apps/frontend/src/ ├── features/ │ ├── auth/ │ ├── dashboard/ │ ├── projects/ │ └── ... (features por módulo) ├── components/ │ ├── ui/ (componentes reutilizables) │ └── layout/ ├── stores/ (Zustand stores) ├── services/ (API clients) ├── hooks/ ├── utils/ └── App.tsx - ✅ Vite configurado con hot reload
- ✅ TypeScript strict mode habilitado
- ✅ Path aliases configurados (
@/components,@/features, etc.)
CA-4: Development Tools
Dado el proyecto completo Cuando un nuevo desarrollador clona el repo Entonces:
- ✅
npm installinstala todas las dependencias - ✅
npm run devlevanta backend + frontend + database - ✅ ESLint + Prettier configurados y funcionando
- ✅ Husky pre-commit hooks ejecutan lint + format
- ✅ Tests pueden ejecutarse con
npm test
CA-5: Docker Setup
Dado Docker instalado en el sistema
Cuando ejecuto docker-compose up
Entonces:
- ✅ Se levanta PostgreSQL en puerto 5432
- ✅ Se levanta backend en puerto 3000
- ✅ Se levanta frontend en puerto 5173
- ✅ Hot reload funciona dentro de los containers
- ✅ Migrations se ejecutan automáticamente al iniciar backend
CA-6: CI/CD Pipeline
Dado un commit pusheado a GitHub Cuando se activa el pipeline de CI Entonces:
- ✅ Se ejecutan linters (ESLint)
- ✅ Se ejecutan formatters (Prettier)
- ✅ Se ejecutan tests unitarios
- ✅ Se ejecutan tests de integración
- ✅ Se genera reporte de cobertura
- ✅ El pipeline falla si cualquier check no pasa
🔧 Especificación Técnica Detallada
1. Database Setup
Script de Inicialización
Archivo: apps/database/scripts/init-database.sh
#!/bin/bash
# Configuración
DB_NAME="gamilit_construction"
DB_USER="gamilit_user"
DB_PASSWORD="secure_password_here"
DB_HOST="localhost"
DB_PORT="5432"
# Crear base de datos
psql -U postgres -h $DB_HOST -p $DB_PORT <<EOF
CREATE DATABASE $DB_NAME;
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
ALTER USER $DB_USER WITH SUPERUSER; -- Para crear extensiones
EOF
# Conectar a la BD y crear schemas
psql -U $DB_USER -d $DB_NAME -h $DB_HOST -p $DB_PORT <<EOF
-- Crear extensiones
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Crear schemas
CREATE SCHEMA IF NOT EXISTS auth_management;
CREATE SCHEMA IF NOT EXISTS projects;
CREATE SCHEMA IF NOT EXISTS budgets;
CREATE SCHEMA IF NOT EXISTS purchases;
CREATE SCHEMA IF NOT EXISTS finance;
CREATE SCHEMA IF NOT EXISTS hr;
CREATE SCHEMA IF NOT EXISTS post_sales;
CREATE SCHEMA IF NOT EXISTS gamification_system;
CREATE SCHEMA IF NOT EXISTS audit;
-- Otorgar permisos
GRANT ALL ON SCHEMA auth_management TO $DB_USER;
GRANT ALL ON SCHEMA projects TO $DB_USER;
GRANT ALL ON SCHEMA budgets TO $DB_USER;
GRANT ALL ON SCHEMA purchases TO $DB_USER;
GRANT ALL ON SCHEMA finance TO $DB_USER;
GRANT ALL ON SCHEMA hr TO $DB_USER;
GRANT ALL ON SCHEMA post_sales TO $DB_USER;
GRANT ALL ON SCHEMA gamification_system TO $DB_USER;
GRANT ALL ON SCHEMA audit TO $DB_USER;
-- Crear función de contexto (para RLS)
CREATE OR REPLACE FUNCTION auth_management.get_current_constructora_id()
RETURNS UUID AS $$
BEGIN
RETURN NULLIF(current_setting('app.current_constructora_id', true), '')::UUID;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE;
CREATE OR REPLACE FUNCTION auth_management.get_current_user_id()
RETURNS UUID AS $$
BEGIN
RETURN NULLIF(current_setting('app.current_user_id', true), '')::UUID;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE;
CREATE OR REPLACE FUNCTION auth_management.get_current_user_role()
RETURNS TEXT AS $$
BEGIN
RETURN current_setting('app.current_user_role', true);
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE;
-- Crear ENUM para roles
CREATE TYPE auth_management.construction_role AS ENUM (
'director',
'engineer',
'resident',
'purchases',
'finance',
'hr',
'post_sales'
);
-- Crear ENUM para estados de cuenta
CREATE TYPE auth_management.account_status AS ENUM (
'pending',
'active',
'inactive',
'suspended',
'banned'
);
COMMENT ON FUNCTION auth_management.get_current_constructora_id() IS 'Obtiene el ID de la constructora del contexto de la sesión';
COMMENT ON FUNCTION auth_management.get_current_user_id() IS 'Obtiene el ID del usuario del contexto de la sesión';
COMMENT ON FUNCTION auth_management.get_current_user_role() IS 'Obtiene el rol del usuario del contexto de la sesión';
EOF
echo "✅ Base de datos inicializada correctamente"
Migración Inicial - Tabla de Constructoras
Archivo: apps/backend/src/migrations/1700000000001-CreateConstructoras.ts
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateConstructoras1700000000001 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE auth_management.constructoras (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
rfc VARCHAR(13) UNIQUE NOT NULL,
business_name VARCHAR(255) NOT NULL,
phone VARCHAR(20),
email VARCHAR(255),
address TEXT,
city VARCHAR(100),
state VARCHAR(100),
country VARCHAR(100) DEFAULT 'México',
postal_code VARCHAR(10),
logo_url TEXT,
settings JSONB DEFAULT '{}'::jsonb,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Índices
CREATE INDEX idx_constructoras_rfc ON auth_management.constructoras(rfc);
CREATE INDEX idx_constructoras_is_active ON auth_management.constructoras(is_active) WHERE deleted_at IS NULL;
-- Trigger de actualización
CREATE TRIGGER set_constructoras_updated_at
BEFORE UPDATE ON auth_management.constructoras
FOR EACH ROW
EXECUTE FUNCTION auth_management.update_updated_at_column();
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE auth_management.constructoras CASCADE;`);
}
}
2. Backend NestJS - Estructura
Main Application Bootstrap
Archivo: apps/backend/src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
import helmet from 'helmet';
import * as compression from 'compression';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
});
const configService = app.get(ConfigService);
// Security
app.use(helmet());
app.enableCors({
origin: configService.get('CORS_ORIGINS')?.split(',') || ['http://localhost:5173'],
credentials: true,
});
// Compression
app.use(compression());
// Global prefix
app.setGlobalPrefix('api');
// API Versioning
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
// Global pipes
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
// Global filters
app.useGlobalFilters(new HttpExceptionFilter());
// Global interceptors
app.useGlobalInterceptors(
new LoggingInterceptor(),
new TransformInterceptor(),
);
// Swagger documentation
if (configService.get('NODE_ENV') !== 'production') {
const config = new DocumentBuilder()
.setTitle('Sistema de Gestión de Obra - API')
.setDescription('API RESTful para gestión integral de proyectos de construcción')
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
},
'JWT-auth',
)
.addTag('Auth', 'Autenticación y autorización')
.addTag('Users', 'Gestión de usuarios')
.addTag('Constructoras', 'Gestión de constructoras (multi-tenancy)')
.addTag('Projects', 'Gestión de proyectos')
.addTag('Budgets', 'Gestión de presupuestos')
.addTag('Purchases', 'Gestión de compras y proveedores')
.addTag('HR', 'Recursos humanos y asistencias')
.addTag('Finance', 'Finanzas y tesorería')
.addTag('Post-Sales', 'Post-venta y garantías')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);
}
const port = configService.get('PORT') || 3000;
await app.listen(port);
console.log(`🚀 Application is running on: http://localhost:${port}/api`);
console.log(`📚 Swagger docs available at: http://localhost:${port}/api/docs`);
}
bootstrap();
Database Configuration
Archivo: apps/backend/src/config/database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
export const getDatabaseConfig = (
configService: ConfigService,
): TypeOrmModuleOptions => ({
type: 'postgres',
host: configService.get('DB_HOST', 'localhost'),
port: configService.get('DB_PORT', 5432),
username: configService.get('DB_USERNAME', 'gamilit_user'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE', 'gamilit_construction'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/../migrations/*{.ts,.js}'],
synchronize: false, // SIEMPRE false en producción
logging: configService.get('NODE_ENV') === 'development' ? ['query', 'error'] : ['error'],
migrationsRun: true, // Auto-run migrations on startup
ssl: configService.get('DB_SSL') === 'true' ? { rejectUnauthorized: false } : false,
extra: {
max: 20, // Max pool size
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
},
});
App Module
Archivo: apps/backend/src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { getDatabaseConfig } from './config/database.config';
// Modules
import { AuthModule } from './modules/auth/auth.module';
import { UsersModule } from './modules/users/users.module';
import { ConstructorasModule } from './modules/constructoras/constructoras.module';
import { ProjectsModule } from './modules/projects/projects.module';
import { BudgetsModule } from './modules/budgets/budgets.module';
import { PurchasesModule } from './modules/purchases/purchases.module';
import { HrModule } from './modules/hr/hr.module';
import { FinanceModule } from './modules/finance/finance.module';
import { PostSalesModule } from './modules/post-sales/post-sales.module';
import { DashboardModule } from './modules/dashboard/dashboard.module';
import { NotificationsModule } from './modules/notifications/notifications.module';
// Common
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
@Module({
imports: [
// Configuration
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
}),
// Database
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: getDatabaseConfig,
}),
// Rate limiting
ThrottlerModule.forRoot([
{
ttl: 60000, // 1 minuto
limit: 100, // 100 requests por minuto
},
]),
// Scheduled tasks
ScheduleModule.forRoot(),
// Event emitter
EventEmitterModule.forRoot(),
// Feature modules
AuthModule,
UsersModule,
ConstructorasModule,
ProjectsModule,
BudgetsModule,
PurchasesModule,
HrModule,
FinanceModule,
PostSalesModule,
DashboardModule,
NotificationsModule,
],
providers: [
// Global JWT guard
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
3. Frontend React + Vite - Estructura
Vite Configuration
Archivo: apps/frontend/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@/components': path.resolve(__dirname, './src/components'),
'@/features': path.resolve(__dirname, './src/features'),
'@/stores': path.resolve(__dirname, './src/stores'),
'@/services': path.resolve(__dirname, './src/services'),
'@/hooks': path.resolve(__dirname, './src/hooks'),
'@/utils': path.resolve(__dirname, './src/utils'),
'@/types': path.resolve(__dirname, './src/types'),
},
},
server: {
port: 5173,
host: true,
strictPort: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'zustand-vendor': ['zustand'],
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
},
},
},
},
});
TypeScript Configuration
Archivo: apps/frontend/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/features/*": ["./src/features/*"],
"@/stores/*": ["./src/stores/*"],
"@/services/*": ["./src/services/*"],
"@/hooks/*": ["./src/hooks/*"],
"@/utils/*": ["./src/utils/*"],
"@/types/*": ["./src/types/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Main App Component
Archivo: apps/frontend/src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { Toaster } from 'sonner';
// Layouts
import { AuthLayout } from '@/components/layout/AuthLayout';
import { DashboardLayout } from '@/components/layout/DashboardLayout';
// Features
import { LoginPage } from '@/features/auth/pages/LoginPage';
import { RegisterPage } from '@/features/auth/pages/RegisterPage';
import { DashboardPage } from '@/features/dashboard/pages/DashboardPage';
import { ProjectsPage } from '@/features/projects/pages/ProjectsPage';
import { BudgetsPage } from '@/features/budgets/pages/BudgetsPage';
// Guards
import { ProtectedRoute } from '@/components/guards/ProtectedRoute';
import { RoleGuard } from '@/components/guards/RoleGuard';
// Create query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutos
retry: 1,
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route element={<AuthLayout />}>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
</Route>
{/* Protected routes */}
<Route
element={
<ProtectedRoute>
<DashboardLayout />
</ProtectedRoute>
}
>
<Route path="/dashboard" element={<DashboardPage />} />
{/* Projects - Director, Engineer, Resident */}
<Route
path="/projects"
element={
<RoleGuard allowedRoles={['director', 'engineer', 'resident']}>
<ProjectsPage />
</RoleGuard>
}
/>
{/* Budgets - Director, Engineer */}
<Route
path="/budgets"
element={
<RoleGuard allowedRoles={['director', 'engineer']}>
<BudgetsPage />
</RoleGuard>
}
/>
{/* More routes... */}
</Route>
{/* Redirect */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</BrowserRouter>
{/* Toasts */}
<Toaster position="top-right" richColors />
{/* React Query Devtools */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default App;
API Client Service
Archivo: apps/frontend/src/services/api.service.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { toast } from 'sonner';
class ApiService {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor - Agregar token
this.client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error),
);
// Response interceptor - Manejo de errores
this.client.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Token expirado o inválido
localStorage.removeItem('accessToken');
localStorage.removeItem('constructora-storage');
window.location.href = '/login';
toast.error('Sesión expirada. Por favor inicia sesión nuevamente.');
} else if (error.response?.status === 403) {
toast.error('No tienes permisos para realizar esta acción.');
} else if (error.response?.status >= 500) {
toast.error('Error del servidor. Intenta nuevamente más tarde.');
}
return Promise.reject(error);
},
);
}
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.get(url, config);
return response.data;
}
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.post(url, data, config);
return response.data;
}
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.put(url, data, config);
return response.data;
}
async patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.patch(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.delete(url, config);
return response.data;
}
}
export const apiService = new ApiService();
4. Docker Setup
Docker Compose
Archivo: docker-compose.yml
version: '3.8'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: gamilit-construction-db
restart: unless-stopped
environment:
POSTGRES_USER: gamilit_user
POSTGRES_PASSWORD: secure_password_here
POSTGRES_DB: gamilit_construction
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
- ./apps/database/scripts:/docker-entrypoint-initdb.d
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U gamilit_user -d gamilit_construction']
interval: 10s
timeout: 5s
retries: 5
# Backend NestJS
backend:
build:
context: .
dockerfile: ./apps/backend/Dockerfile
target: development
container_name: gamilit-construction-backend
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
NODE_ENV: development
PORT: 3000
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: gamilit_user
DB_PASSWORD: secure_password_here
DB_DATABASE: gamilit_construction
JWT_SECRET: your_jwt_secret_key_here
JWT_EXPIRES_IN: 15m
ports:
- '3000:3000'
volumes:
- ./apps/backend:/app/apps/backend
- /app/apps/backend/node_modules
- /app/node_modules
command: npm run start:dev
# Frontend React
frontend:
build:
context: .
dockerfile: ./apps/frontend/Dockerfile
target: development
container_name: gamilit-construction-frontend
restart: unless-stopped
depends_on:
- backend
environment:
VITE_API_URL: http://localhost:3000/api
ports:
- '5173:5173'
volumes:
- ./apps/frontend:/app/apps/frontend
- /app/apps/frontend/node_modules
- /app/node_modules
command: npm run dev
volumes:
postgres_data:
driver: local
Backend Dockerfile
Archivo: apps/backend/Dockerfile
# Development stage
FROM node:20-alpine AS development
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY apps/backend/package*.json ./apps/backend/
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Expose port
EXPOSE 3000
# Start development server
CMD ["npm", "run", "start:dev"]
# ===============================
# Production build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
COPY apps/backend/package*.json ./apps/backend/
RUN npm ci --only=production
COPY . .
RUN npm run build
# ===============================
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package*.json ./
EXPOSE 3000
CMD ["node", "dist/main"]
5. Code Quality Tools
ESLint Configuration
Archivo: .eslintrc.json
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "import", "prettier"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier"
],
"rules": {
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
],
"prettier/prettier": "error"
},
"settings": {
"import/resolver": {
"typescript": {
"alwaysTryTypes": true
}
}
}
}
Prettier Configuration
Archivo: .prettierrc
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf"
}
Husky Pre-commit Hook
Archivo: .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run lint-staged
npx lint-staged
# Run type check
npm run type-check
Archivo: package.json (lint-staged config)
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
]
}
}
6. CI/CD Pipeline
GitHub Actions Workflow
Archivo: .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint-and-test:
name: Lint and Test
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: gamilit_user
POSTGRES_PASSWORD: test_password
POSTGRES_DB: gamilit_construction_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run Prettier check
run: npm run format:check
- name: Run TypeScript type check
run: npm run type-check
- name: Run backend unit tests
run: npm run test:backend
env:
DB_HOST: localhost
DB_PORT: 5432
DB_USERNAME: gamilit_user
DB_PASSWORD: test_password
DB_DATABASE: gamilit_construction_test
JWT_SECRET: test_secret_key
NODE_ENV: test
- name: Run frontend tests
run: npm run test:frontend
- name: Run e2e tests
run: npm run test:e2e
env:
DB_HOST: localhost
DB_PORT: 5432
DB_USERNAME: gamilit_user
DB_PASSWORD: test_password
DB_DATABASE: gamilit_construction_test
JWT_SECRET: test_secret_key
NODE_ENV: test
- name: Generate coverage report
run: npm run test:cov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
build:
name: Build
runs-on: ubuntu-latest
needs: lint-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build backend
run: npm run build:backend
- name: Build frontend
run: npm run build:frontend
- name: Archive production artifacts
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: |
apps/backend/dist
apps/frontend/dist
🧪 Test Cases
TC-INFRA-001: Instalación Limpia
Pre-condiciones:
- Sistema con Node.js 20+ y Docker instalados
- Puerto 5432, 3000, 5173 disponibles
Pasos:
- Clonar repositorio:
git clone <repo-url> - Ejecutar:
npm install - Ejecutar:
docker-compose up -d postgres - Esperar a que PostgreSQL esté healthy
- Ejecutar:
npm run migrate - Ejecutar:
npm run dev
Resultado esperado:
- ✅ Backend corriendo en http://localhost:3000
- ✅ Frontend corriendo en http://localhost:5173
- ✅ Swagger docs en http://localhost:3000/api/docs
- ✅ No hay errores en consola
TC-INFRA-002: Hot Reload Backend
Pre-condiciones:
- Backend corriendo en modo desarrollo
Pasos:
- Abrir archivo
apps/backend/src/modules/users/users.controller.ts - Modificar el mensaje de retorno de un endpoint
- Guardar archivo
- Observar la consola del backend
Resultado esperado:
- ✅ NestJS detecta el cambio
- ✅ Recompila automáticamente
- ✅ Servidor se reinicia en < 3 segundos
- ✅ Endpoint refleja el cambio sin reinicio manual
TC-INFRA-003: Hot Reload Frontend
Pre-condiciones:
- Frontend corriendo en modo desarrollo
Pasos:
- Abrir archivo
apps/frontend/src/features/dashboard/pages/DashboardPage.tsx - Modificar un texto en el componente
- Guardar archivo
- Observar el navegador
Resultado esperado:
- ✅ Vite detecta el cambio
- ✅ Hot Module Replacement (HMR) se ejecuta
- ✅ Componente se actualiza sin recargar la página
- ✅ El cambio es visible instantáneamente
TC-INFRA-004: Pre-commit Hooks
Pre-condiciones:
- Husky instalado y configurado
- Archivo con errores de lint
Pasos:
- Modificar un archivo TypeScript introduciendo errores de lint:
// Agregar variable no utilizada const unusedVar = 'test'; // Agregar línea sin punto y coma const x = 5 - Stage el archivo:
git add . - Intentar commit:
git commit -m "Test commit"
Resultado esperado:
- ✅ Pre-commit hook se ejecuta
- ✅ ESLint detecta errores
- ✅ El commit es rechazado
- ✅ Se muestra mensaje con los errores encontrados
TC-INFRA-005: Database Migrations
Pre-condiciones:
- PostgreSQL corriendo
- Backend configurado
Pasos:
- Crear nueva migración:
npm run migration:create CreateTestTable - Implementar migración:
public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(` CREATE TABLE test ( id SERIAL PRIMARY KEY, name VARCHAR(255) ); `); } - Ejecutar:
npm run migration:run - Conectar a la base de datos y verificar
Resultado esperado:
- ✅ Migración se crea correctamente
- ✅ Migración se ejecuta sin errores
- ✅ Tabla
testexiste en la base de datos - ✅ Entry en tabla
migrationsregistra la ejecución
TC-INFRA-006: CI Pipeline
Pre-condiciones:
- Código pusheado a GitHub
- GitHub Actions configurado
Pasos:
- Crear PR con cambios
- Observar GitHub Actions
Resultado esperado:
- ✅ Pipeline de CI se ejecuta automáticamente
- ✅ Job de lint pasa
- ✅ Job de type-check pasa
- ✅ Job de tests pasa
- ✅ Job de build pasa
- ✅ Reporte de cobertura se genera
- ✅ PR muestra status check verde
TC-INFRA-007: RLS Context Injection
Pre-condiciones:
- Backend corriendo
- Usuario autenticado
- Constructora activa
Pasos:
- Hacer request a cualquier endpoint protegido con token JWT válido
- Inspeccionar logs de PostgreSQL (query log habilitado)
- Verificar que se ejecuten los
set_configantes de la query principal
Resultado esperado:
- ✅
set_config('app.current_user_id', '...', true)se ejecuta - ✅
set_config('app.current_constructora_id', '...', true)se ejecuta - ✅
set_config('app.current_user_role', '...', true)se ejecuta - ✅ Query principal usa estas variables en RLS policies
TC-INFRA-008: Swagger Documentation
Pre-condiciones:
- Backend corriendo en modo desarrollo
Pasos:
- Abrir navegador en http://localhost:3000/api/docs
- Explorar endpoints documentados
- Intentar ejecutar endpoint
/api/auth/logindesde Swagger UI
Resultado esperado:
- ✅ Swagger UI carga correctamente
- ✅ Todos los módulos están listados con sus tags
- ✅ Schemas de DTOs están documentados
- ✅ Es posible ejecutar requests desde la UI
- ✅ Bearer token puede configurarse para endpoints protegidos
📋 Tareas de Implementación
Sprint 0 - Semana 1
Backend
-
INFRA-BE-001: Crear estructura de carpetas del proyecto NestJS
- Estimado: 1h
- Asignado a: Backend Lead
-
INFRA-BE-002: Configurar TypeORM con migraciones
- Estimado: 2h
- Asignado a: Backend Lead
-
INFRA-BE-003: Crear script de inicialización de base de datos
- Estimado: 2h
- Asignado a: DevOps
-
INFRA-BE-004: Implementar main.ts con configuración completa
- Estimado: 2h
- Asignado a: Backend Lead
-
INFRA-BE-005: Crear módulos base (Auth, Users, Constructoras)
- Estimado: 3h
- Asignado a: Backend Dev
-
INFRA-BE-006: Configurar Swagger documentation
- Estimado: 1h
- Asignado a: Backend Dev
-
INFRA-BE-007: Implementar guards globales (JWT, Roles)
- Estimado: 2h
- Asignado a: Backend Dev
-
INFRA-BE-008: Crear interceptors (Logging, Transform, RLS Context)
- Estimado: 2h
- Asignado a: Backend Dev
Frontend
-
INFRA-FE-001: Crear estructura de carpetas del proyecto React
- Estimado: 1h
- Asignado a: Frontend Lead
-
INFRA-FE-002: Configurar Vite con path aliases
- Estimado: 1h
- Asignado a: Frontend Lead
-
INFRA-FE-003: Configurar React Router con layouts
- Estimado: 2h
- Asignado a: Frontend Dev
-
INFRA-FE-004: Implementar API service con interceptors
- Estimado: 2h
- Asignado a: Frontend Dev
-
INFRA-FE-005: Crear guards de navegación (ProtectedRoute, RoleGuard)
- Estimado: 2h
- Asignado a: Frontend Dev
-
INFRA-FE-006: Configurar React Query
- Estimado: 1h
- Asignado a: Frontend Dev
-
INFRA-FE-007: Crear componentes de layout base
- Estimado: 2h
- Asignado a: Frontend Dev
DevOps
-
INFRA-DO-001: Configurar Docker Compose para desarrollo
- Estimado: 2h
- Asignado a: DevOps
-
INFRA-DO-002: Crear Dockerfiles (backend y frontend)
- Estimado: 2h
- Asignado a: DevOps
-
INFRA-DO-003: Configurar variables de entorno (.env templates)
- Estimado: 1h
- Asignado a: DevOps
-
INFRA-DO-004: Configurar GitHub Actions CI pipeline
- Estimado: 3h
- Asignado a: DevOps
Code Quality
-
INFRA-CQ-001: Configurar ESLint para backend y frontend
- Estimado: 1h
- Asignado a: Tech Lead
-
INFRA-CQ-002: Configurar Prettier
- Estimado: 0.5h
- Asignado a: Tech Lead
-
INFRA-CQ-003: Configurar Husky + lint-staged
- Estimado: 1h
- Asignado a: Tech Lead
-
INFRA-CQ-004: Configurar Jest para testing (backend y frontend)
- Estimado: 2h
- Asignado a: QA Lead
Documentation
-
INFRA-DOC-001: Crear README.md con instrucciones de instalación
- Estimado: 1h
- Asignado a: Tech Lead
-
INFRA-DOC-002: Documentar estructura de carpetas
- Estimado: 1h
- Asignado a: Tech Lead
-
INFRA-DOC-003: Crear CONTRIBUTING.md con guía de desarrollo
- Estimado: 1h
- Asignado a: Tech Lead
Total estimado: ~40 horas (distribuidas en equipo de 4 devs = 2 semanas)
🔗 Dependencias
Dependencias Externas
- Ninguna (esta es la base del proyecto)
Bloqueantes para
- ✅ Todas las demás historias de usuario
- ✅ MAI-002 (Gestión de Proyectos)
- ✅ MAI-003 (Gestión de Presupuestos)
- ✅ MAI-004 (Gestión de Compras)
- ✅ MAI-005 (Gamificación)
- ✅ MAI-006 (RRHH)
📊 Definición de Hecho (DoD)
- ✅ Backend ejecutable localmente en < 5 minutos
- ✅ Frontend ejecutable localmente en < 5 minutos
- ✅ Base de datos PostgreSQL configurada con schemas
- ✅ Migrations funcionando correctamente
- ✅ Hot reload funcional en backend y frontend
- ✅ Docker Compose levanta todos los servicios
- ✅ ESLint + Prettier configurados y funcionando
- ✅ Pre-commit hooks funcionando
- ✅ CI pipeline ejecutándose en GitHub Actions
- ✅ Swagger documentation accesible en
/api/docs - ✅ Todos los test cases (TC-INFRA-001 a TC-INFRA-008) pasan
- ✅ README.md con instrucciones de instalación completas
- ✅ Variables de entorno documentadas (
.env.example) - ✅ No hay warnings ni errores en consola (dev mode)
📝 Notas Adicionales
Performance Targets
- Backend startup: < 5 segundos
- Frontend startup: < 3 segundos
- Hot reload backend: < 3 segundos
- Hot reload frontend: < 1 segundo
- Build backend: < 30 segundos
- Build frontend: < 20 segundos
Security Considerations
- ✅ No commits de archivos
.env(usar.env.example) - ✅ Secrets en variables de entorno, no hardcoded
- ✅ PostgreSQL password fuerte en producción
- ✅ JWT secret diferente por ambiente
- ✅ Helmet configurado para seguridad HTTP
- ✅ CORS configurado restrictivamente
Monitoring & Logging
- ✅ Winston logger configurado (backend)
- ✅ Request logging con timestamps
- ✅ Error logging con stack traces
- ✅ Query logging en desarrollo (deshabilitado en prod)
🎓 Lecciones de GAMILIT
Qué Reutilizar (80%)
✅ Estructura completa del proyecto:
- Organización de carpetas backend/frontend
- Configuración de TypeORM
- Guards, decorators, interceptors
- API service con interceptors
- Docker setup
✅ Herramientas de desarrollo:
- ESLint + Prettier config
- Husky hooks
- GitHub Actions CI
- Swagger configuration
Qué Adaptar (20%)
🔄 Schemas de base de datos:
- GAMILIT:
auth_management,gamification_system,educational_content - Construcción:
auth_management,projects,budgets,purchases,hr,finance
🔄 Módulos de negocio:
- GAMILIT: Students, Teachers, Courses
- Construcción: Projects, Budgets, Employees
Fecha de creación: 2025-11-17 Última actualización: 2025-11-17 Versión: 1.0