workspace-v1/projects/erp-construccion/docs/05-backend-specs
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -06:00
..
modules feat: Workspace-v1 complete migration with NEXUS v3.4 2026-01-04 03:37:42 -06:00
README.md feat: Workspace-v1 complete migration with NEXUS v3.4 2026-01-04 03:37:42 -06:00

Backend Specifications - ERP Construccion

Fecha: 2025-12-05 Version: 1.0.0 Stack: Node.js 20+ / Express.js / TypeScript 5.3+ / TypeORM 0.3.17


Estructura del Directorio

05-backend-specs/
+-- README.md (este archivo)
+-- modules/
|   +-- SPEC-construction.md     [MAI-002, MAI-003, MAI-005]
|   +-- SPEC-compliance.md       [MAI-011]
|   +-- SPEC-finance.md          [MAE-014]
|   +-- SPEC-assets.md           [MAE-015]
|   +-- SPEC-documents.md        [MAE-016]
+-- api/
|   +-- API-construction.yaml    [OpenAPI 3.0]
|   +-- API-compliance.yaml
|   +-- API-finance.yaml
|   +-- API-assets.yaml
|   +-- API-documents.yaml
+-- services/
    +-- SERVICE-patterns.md      [Patrones de servicios]

Arquitectura Backend

Estructura de Modulos

apps/backend/src/
+-- modules/
|   +-- construction/
|   |   +-- construction.module.ts
|   |   +-- controllers/
|   |   |   +-- project.controller.ts
|   |   |   +-- budget.controller.ts
|   |   |   +-- progress.controller.ts
|   |   +-- services/
|   |   |   +-- project.service.ts
|   |   |   +-- budget.service.ts
|   |   |   +-- progress.service.ts
|   |   +-- entities/
|   |   +-- dto/
|   |   +-- repositories/
|   +-- compliance/
|   +-- finance/
|   +-- assets/
|   +-- documents/
+-- shared/
|   +-- guards/
|   +-- interceptors/
|   +-- decorators/
|   +-- filters/
+-- config/

Patrones de Desarrollo

1. Estructura de Controlador

@Controller('api/v1/projects')
@UseGuards(AuthGuard, TenantGuard)
@ApiTags('projects')
export class ProjectController {
  constructor(private readonly projectService: ProjectService) {}

  @Get()
  @ApiOperation({ summary: 'List all projects' })
  @ApiResponse({ status: 200, type: [ProjectDto] })
  async findAll(
    @Query() query: ProjectQueryDto,
    @CurrentTenant() tenantId: UUID
  ): Promise<PaginatedResponse<ProjectDto>> {
    return this.projectService.findAll(tenantId, query);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Get project by ID' })
  async findOne(
    @Param('id') id: UUID,
    @CurrentTenant() tenantId: UUID
  ): Promise<ProjectDto> {
    return this.projectService.findOne(tenantId, id);
  }

  @Post()
  @ApiOperation({ summary: 'Create new project' })
  async create(
    @Body() dto: CreateProjectDto,
    @CurrentTenant() tenantId: UUID,
    @CurrentUser() userId: UUID
  ): Promise<ProjectDto> {
    return this.projectService.create(tenantId, userId, dto);
  }
}

2. Estructura de Servicio

@Injectable()
export class ProjectService {
  constructor(
    @InjectRepository(Project)
    private readonly projectRepo: Repository<Project>,
    private readonly eventEmitter: EventEmitter2
  ) {}

  async findAll(tenantId: UUID, query: ProjectQueryDto): Promise<PaginatedResponse<ProjectDto>> {
    const qb = this.projectRepo.createQueryBuilder('p')
      .where('p.tenant_id = :tenantId', { tenantId })
      .andWhere('p.deleted_at IS NULL');

    if (query.status) {
      qb.andWhere('p.status = :status', { status: query.status });
    }

    const [items, total] = await qb
      .skip(query.offset)
      .take(query.limit)
      .getManyAndCount();

    return { items: items.map(toDto), total, ...query };
  }

  async create(tenantId: UUID, userId: UUID, dto: CreateProjectDto): Promise<ProjectDto> {
    const project = this.projectRepo.create({
      ...dto,
      tenantId,
      createdBy: userId
    });

    await this.projectRepo.save(project);

    this.eventEmitter.emit('project.created', new ProjectCreatedEvent(project));

    return toDto(project);
  }
}

3. Estructura de Entidad

@Entity('projects', { schema: 'construction' })
export class Project {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ name: 'tenant_id', type: 'uuid' })
  tenantId: string;

  @Column({ length: 20 })
  code: string;

  @Column({ length: 200 })
  name: string;

  @Column({ type: 'text', nullable: true })
  description: string;

  @Column({
    type: 'enum',
    enum: ProjectStatus,
    default: ProjectStatus.PLANNING
  })
  status: ProjectStatus;

  @Column({ name: 'progress_percentage', type: 'decimal', precision: 5, scale: 2, default: 0 })
  progressPercentage: number;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @Column({ name: 'created_by', type: 'uuid', nullable: true })
  createdBy: string;

  @UpdateDateColumn({ name: 'updated_at', nullable: true })
  updatedAt: Date;

  @Column({ name: 'deleted_at', nullable: true })
  deletedAt: Date;

  // Relations
  @OneToMany(() => Development, d => d.project)
  developments: Development[];

  @OneToMany(() => Budget, b => b.project)
  budgets: Budget[];
}

4. DTOs

// Create DTO
export class CreateProjectDto {
  @IsString()
  @MaxLength(20)
  code: string;

  @IsString()
  @MaxLength(200)
  name: string;

  @IsOptional()
  @IsString()
  description?: string;

  @IsOptional()
  @IsDateString()
  startDate?: string;

  @IsOptional()
  @IsDateString()
  endDate?: string;
}

// Response DTO
export class ProjectDto {
  id: string;
  code: string;
  name: string;
  description?: string;
  status: ProjectStatus;
  progressPercentage: number;
  startDate?: string;
  endDate?: string;
  createdAt: string;
}

// Query DTO
export class ProjectQueryDto extends PaginationDto {
  @IsOptional()
  @IsEnum(ProjectStatus)
  status?: ProjectStatus;

  @IsOptional()
  @IsString()
  search?: string;
}

Modulos Implementados

Modulo Spec Endpoints Entidades Estado
construction SPEC-construction.md 25+ 15 Documentado
compliance SPEC-compliance.md 30+ 10 Documentado
finance SPEC-finance.md 50+ 15 Documentado
assets SPEC-assets.md 45+ 12 Documentado
documents SPEC-documents.md 40+ 10 Documentado

Resumen de Endpoints: 190+ endpoints documentados


Seguridad

Guards

// Tenant isolation guard
@Injectable()
export class TenantGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const tenantId = request.headers['x-tenant-id'];

    if (!tenantId) {
      throw new UnauthorizedException('Tenant ID required');
    }

    request.tenantId = tenantId;
    return true;
  }
}

RLS Context

// Middleware para establecer contexto RLS
export class RLSMiddleware implements NestMiddleware {
  constructor(private readonly dataSource: DataSource) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const tenantId = req.headers['x-tenant-id'];

    if (tenantId) {
      await this.dataSource.query(
        `SET LOCAL app.current_tenant_id = '${tenantId}'`
      );
    }

    next();
  }
}

Testing

Estructura de Tests

__tests__/
+-- unit/
|   +-- services/
|   +-- controllers/
+-- integration/
|   +-- api/
|   +-- repositories/
+-- e2e/
    +-- projects.e2e-spec.ts

Ejemplo de Test

describe('ProjectService', () => {
  let service: ProjectService;
  let repository: MockType<Repository<Project>>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        ProjectService,
        {
          provide: getRepositoryToken(Project),
          useFactory: repositoryMockFactory
        }
      ]
    }).compile();

    service = module.get(ProjectService);
    repository = module.get(getRepositoryToken(Project));
  });

  describe('create', () => {
    it('should create a project', async () => {
      const dto: CreateProjectDto = {
        code: 'PRJ-001',
        name: 'Test Project'
      };

      repository.create.mockReturnValue({ id: 'uuid', ...dto });
      repository.save.mockResolvedValue({ id: 'uuid', ...dto });

      const result = await service.create('tenant-id', 'user-id', dto);

      expect(result.code).toBe('PRJ-001');
      expect(repository.save).toHaveBeenCalled();
    });
  });
});

Referencias


Ultima actualizacion: 2025-12-05