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
23 KiB
23 KiB
Guía de Implementación: Feature Flags
Versión: 1.0.0 Tiempo estimado: 1-2 horas Complejidad: Media
Pre-requisitos
- Proyecto NestJS existente
- TypeORM configurado
- PostgreSQL como base de datos
Paso 1: Crear Estructura de Directorios
mkdir -p src/modules/feature-flags/entities
mkdir -p src/modules/feature-flags/services
mkdir -p src/modules/feature-flags/controllers
mkdir -p src/modules/feature-flags/dto
mkdir -p src/modules/feature-flags/guards
mkdir -p src/modules/feature-flags/decorators
Paso 2: Crear Entidad FeatureFlag
// src/modules/feature-flags/entities/feature-flag.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Check,
} from 'typeorm';
@Entity({ schema: 'config', name: 'feature_flags' })
@Index('idx_feature_flags_key', ['featureKey'])
@Index('idx_feature_flags_enabled', ['isEnabled'], { where: '"is_enabled" = true' })
@Check('"rollout_percentage" >= 0 AND "rollout_percentage" <= 100')
export class FeatureFlag {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId?: string;
@Column({ name: 'feature_key', type: 'varchar', length: 100, unique: true })
featureKey: string;
@Column({ name: 'feature_name', type: 'varchar', length: 255 })
featureName: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'is_enabled', type: 'boolean', default: false })
isEnabled: boolean;
@Column({ name: 'rollout_percentage', type: 'integer', default: 0 })
rolloutPercentage: number;
@Column({ name: 'target_users', type: 'uuid', array: true, nullable: true })
targetUsers?: string[];
@Column({ name: 'target_roles', type: 'varchar', array: true, nullable: true })
targetRoles?: string[];
@Column({ name: 'target_conditions', type: 'jsonb', default: {} })
targetConditions: Record<string, any>;
@Column({ name: 'starts_at', type: 'timestamp with time zone', nullable: true })
startsAt?: Date;
@Column({ name: 'ends_at', type: 'timestamp with time zone', nullable: true })
endsAt?: Date;
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy?: string;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy?: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
Paso 3: Crear DTOs
// src/modules/feature-flags/dto/create-feature-flag.dto.ts
import {
IsString,
IsBoolean,
IsOptional,
IsInt,
Min,
Max,
IsArray,
IsObject,
MaxLength,
Matches,
IsDateString,
} from 'class-validator';
export class CreateFeatureFlagDto {
@IsString()
@MaxLength(100)
@Matches(/^[a-z][a-z0-9_]*$/, {
message: 'Key debe ser snake_case y comenzar con letra',
})
key: string;
@IsString()
@MaxLength(255)
name: string;
@IsString()
@IsOptional()
description?: string;
@IsBoolean()
@IsOptional()
isEnabled?: boolean;
@IsInt()
@Min(0)
@Max(100)
@IsOptional()
rolloutPercentage?: number;
@IsArray()
@IsString({ each: true })
@IsOptional()
targetUsers?: string[];
@IsArray()
@IsString({ each: true })
@IsOptional()
targetRoles?: string[];
@IsObject()
@IsOptional()
targetConditions?: Record<string, any>;
@IsDateString()
@IsOptional()
startsAt?: string;
@IsDateString()
@IsOptional()
endsAt?: string;
@IsString()
@IsOptional()
category?: string;
@IsObject()
@IsOptional()
metadata?: Record<string, any>;
}
// src/modules/feature-flags/dto/update-feature-flag.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateFeatureFlagDto } from './create-feature-flag.dto';
export class UpdateFeatureFlagDto extends PartialType(CreateFeatureFlagDto) {}
// src/modules/feature-flags/dto/feature-flag-query.dto.ts
import { IsBoolean, IsOptional, IsString } from 'class-validator';
import { Transform } from 'class-transformer';
export class FeatureFlagQueryDto {
@IsBoolean()
@IsOptional()
@Transform(({ value }) => value === 'true')
isEnabled?: boolean;
@IsString()
@IsOptional()
category?: string;
}
// src/modules/feature-flags/dto/check-result.dto.ts
export class FeatureFlagCheckResultDto {
enabled: boolean;
reason: string;
}
// src/modules/feature-flags/dto/index.ts
export * from './create-feature-flag.dto';
export * from './update-feature-flag.dto';
export * from './feature-flag-query.dto';
export * from './check-result.dto';
Paso 4: Crear Servicio
// src/modules/feature-flags/services/feature-flags.service.ts
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { createHash } from 'crypto';
import { FeatureFlag } from '../entities/feature-flag.entity';
import {
CreateFeatureFlagDto,
UpdateFeatureFlagDto,
FeatureFlagQueryDto,
FeatureFlagCheckResultDto,
} from '../dto';
@Injectable()
export class FeatureFlagsService {
constructor(
@InjectRepository(FeatureFlag)
private readonly featureFlagRepo: Repository<FeatureFlag>,
) {}
/**
* Obtener todas las feature flags con filtros opcionales
*/
async findAll(query?: FeatureFlagQueryDto): Promise<FeatureFlag[]> {
const qb = this.featureFlagRepo.createQueryBuilder('ff');
if (query?.isEnabled !== undefined) {
qb.andWhere('ff.is_enabled = :isEnabled', { isEnabled: query.isEnabled });
}
if (query?.category) {
qb.andWhere("ff.metadata->>'category' = :category", {
category: query.category,
});
}
return qb.orderBy('ff.feature_name', 'ASC').getMany();
}
/**
* Obtener una feature flag por su key
*/
async findOne(key: string): Promise<FeatureFlag> {
const flag = await this.featureFlagRepo.findOne({
where: { featureKey: key },
});
if (!flag) {
throw new NotFoundException(`Feature flag "${key}" no encontrada`);
}
return flag;
}
/**
* Crear una nueva feature flag
*/
async create(dto: CreateFeatureFlagDto, createdBy?: string): Promise<FeatureFlag> {
const existing = await this.featureFlagRepo.findOne({
where: { featureKey: dto.key },
});
if (existing) {
throw new ConflictException(`Feature flag "${dto.key}" ya existe`);
}
const flag = this.featureFlagRepo.create({
featureKey: dto.key,
featureName: dto.name,
description: dto.description,
isEnabled: dto.isEnabled ?? false,
rolloutPercentage: dto.rolloutPercentage ?? 0,
targetUsers: dto.targetUsers,
targetRoles: dto.targetRoles,
targetConditions: dto.targetConditions ?? {},
startsAt: dto.startsAt ? new Date(dto.startsAt) : undefined,
endsAt: dto.endsAt ? new Date(dto.endsAt) : undefined,
metadata: {
...dto.metadata,
category: dto.category,
},
createdBy,
});
return this.featureFlagRepo.save(flag);
}
/**
* Actualizar una feature flag existente
*/
async update(
key: string,
dto: UpdateFeatureFlagDto,
updatedBy?: string,
): Promise<FeatureFlag> {
const flag = await this.findOne(key);
if (dto.name !== undefined) flag.featureName = dto.name;
if (dto.description !== undefined) flag.description = dto.description;
if (dto.isEnabled !== undefined) flag.isEnabled = dto.isEnabled;
if (dto.rolloutPercentage !== undefined) flag.rolloutPercentage = dto.rolloutPercentage;
if (dto.targetUsers !== undefined) flag.targetUsers = dto.targetUsers;
if (dto.targetRoles !== undefined) flag.targetRoles = dto.targetRoles;
if (dto.targetConditions !== undefined) flag.targetConditions = dto.targetConditions;
if (dto.startsAt !== undefined) flag.startsAt = new Date(dto.startsAt);
if (dto.endsAt !== undefined) flag.endsAt = new Date(dto.endsAt);
if (dto.metadata !== undefined || dto.category !== undefined) {
flag.metadata = {
...flag.metadata,
...dto.metadata,
...(dto.category && { category: dto.category }),
};
}
flag.updatedBy = updatedBy;
return this.featureFlagRepo.save(flag);
}
/**
* Eliminar una feature flag
*/
async remove(key: string): Promise<void> {
const flag = await this.findOne(key);
await this.featureFlagRepo.remove(flag);
}
/**
* Verificar si una feature está habilitada para un usuario
*/
async isEnabled(
key: string,
userId?: string,
userRoles?: string[],
): Promise<FeatureFlagCheckResultDto> {
try {
const flag = await this.findOne(key);
// 1. Feature habilitada globalmente?
if (!flag.isEnabled) {
return { enabled: false, reason: 'Feature deshabilitada globalmente' };
}
// 2. Verificar período de validez
const now = new Date();
if (flag.startsAt && now < flag.startsAt) {
return { enabled: false, reason: 'Feature no ha iniciado aún' };
}
if (flag.endsAt && now > flag.endsAt) {
return { enabled: false, reason: 'Feature ha expirado' };
}
// 3. Usuario en target_users (early access)?
if (userId && flag.targetUsers?.length > 0) {
if (flag.targetUsers.includes(userId)) {
return { enabled: true, reason: 'Usuario en lista de early access' };
}
}
// 4. Usuario tiene target_role?
if (userRoles && flag.targetRoles?.length > 0) {
const hasRole = userRoles.some((role) => flag.targetRoles?.includes(role));
if (hasRole) {
return { enabled: true, reason: 'Usuario tiene rol objetivo' };
}
}
// 5. Rollout 100%?
if (flag.rolloutPercentage === 100) {
return { enabled: true, reason: 'Rollout al 100%' };
}
// 6. Rollout 0%?
if (flag.rolloutPercentage === 0) {
return { enabled: false, reason: 'Rollout al 0%' };
}
// 7. Hash del userId para rollout gradual
if (userId) {
const hash = this.hashUserId(userId, key);
const isInRollout = hash < flag.rolloutPercentage;
return {
enabled: isInRollout,
reason: isInRollout
? `Usuario en grupo de rollout ${flag.rolloutPercentage}%`
: `Usuario fuera del grupo de rollout ${flag.rolloutPercentage}%`,
};
}
// Sin userId, usar probabilidad
return {
enabled: Math.random() * 100 < flag.rolloutPercentage,
reason: `Selección aleatoria basada en ${flag.rolloutPercentage}%`,
};
} catch (error) {
if (error instanceof NotFoundException) {
return { enabled: false, reason: 'Feature flag no encontrada' };
}
throw error;
}
}
/**
* Hash consistente para rollout gradual
* Retorna número entre 0 y 100
*/
private hashUserId(userId: string, featureKey: string): number {
const hash = createHash('sha256')
.update(`${userId}:${featureKey}`)
.digest('hex');
const hashInt = parseInt(hash.substring(0, 8), 16);
return hashInt % 101;
}
/**
* Habilitar feature flag
*/
async enable(key: string, updatedBy?: string): Promise<FeatureFlag> {
return this.update(key, { isEnabled: true }, updatedBy);
}
/**
* Deshabilitar feature flag
*/
async disable(key: string, updatedBy?: string): Promise<FeatureFlag> {
return this.update(key, { isEnabled: false }, updatedBy);
}
/**
* Actualizar porcentaje de rollout
*/
async updateRollout(
key: string,
percentage: number,
updatedBy?: string,
): Promise<FeatureFlag> {
return this.update(key, { rolloutPercentage: percentage }, updatedBy);
}
}
Paso 5: Crear Guard
// src/modules/feature-flags/guards/feature-flag.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { FeatureFlagsService } from '../services/feature-flags.service';
import { FEATURE_FLAG_KEY } from '../decorators/feature-flag.decorator';
@Injectable()
export class FeatureFlagGuard implements CanActivate {
constructor(
private reflector: Reflector,
private featureFlagsService: FeatureFlagsService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const featureKey = this.reflector.getAllAndOverride<string>(
FEATURE_FLAG_KEY,
[context.getHandler(), context.getClass()],
);
if (!featureKey) {
return true; // No feature flag requerida
}
const request = context.switchToHttp().getRequest();
const userId = request.user?.id;
const userRoles = request.user?.roles || [];
const result = await this.featureFlagsService.isEnabled(
featureKey,
userId,
userRoles,
);
if (!result.enabled) {
throw new ForbiddenException(
`Feature "${featureKey}" no disponible: ${result.reason}`,
);
}
return true;
}
}
Paso 6: Crear Decoradores
// src/modules/feature-flags/decorators/feature-flag.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const FEATURE_FLAG_KEY = 'feature_flag';
/**
* Decorador para marcar rutas que requieren una feature flag
* @param key - Clave de la feature flag
*/
export const FeatureFlag = (key: string) => SetMetadata(FEATURE_FLAG_KEY, key);
// src/modules/feature-flags/decorators/check-feature.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { FeatureFlagsService } from '../services/feature-flags.service';
/**
* Decorador de parámetro para verificar feature inline
* Nota: Este decorador requiere inyección manual del servicio
*/
export const CheckFeature = createParamDecorator(
async (featureKey: string, ctx: ExecutionContext): Promise<boolean> => {
// Nota: Los decoradores de parámetro no pueden inyectar servicios directamente
// Este decorador retorna un placeholder, la verificación real se hace en el servicio
return featureKey;
},
);
Paso 7: Crear Controller
// src/modules/feature-flags/controllers/feature-flags.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
UseGuards,
Request,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { AdminGuard } from '../../auth/guards/admin.guard';
import { FeatureFlagsService } from '../services/feature-flags.service';
import {
CreateFeatureFlagDto,
UpdateFeatureFlagDto,
FeatureFlagQueryDto,
FeatureFlagCheckResultDto,
} from '../dto';
import { FeatureFlag } from '../entities/feature-flag.entity';
@Controller('admin/feature-flags')
@UseGuards(JwtAuthGuard, AdminGuard)
export class FeatureFlagsController {
constructor(private readonly featureFlagsService: FeatureFlagsService) {}
@Get()
async findAll(@Query() query: FeatureFlagQueryDto): Promise<FeatureFlag[]> {
return this.featureFlagsService.findAll(query);
}
@Get(':key')
async findOne(@Param('key') key: string): Promise<FeatureFlag> {
return this.featureFlagsService.findOne(key);
}
@Post()
async create(
@Body() dto: CreateFeatureFlagDto,
@Request() req: any,
): Promise<FeatureFlag> {
return this.featureFlagsService.create(dto, req.user?.id);
}
@Put(':key')
async update(
@Param('key') key: string,
@Body() dto: UpdateFeatureFlagDto,
@Request() req: any,
): Promise<FeatureFlag> {
return this.featureFlagsService.update(key, dto, req.user?.id);
}
@Delete(':key')
async remove(@Param('key') key: string): Promise<void> {
return this.featureFlagsService.remove(key);
}
@Post(':key/enable')
async enable(
@Param('key') key: string,
@Request() req: any,
): Promise<FeatureFlag> {
return this.featureFlagsService.enable(key, req.user?.id);
}
@Post(':key/disable')
async disable(
@Param('key') key: string,
@Request() req: any,
): Promise<FeatureFlag> {
return this.featureFlagsService.disable(key, req.user?.id);
}
@Put(':key/rollout')
async updateRollout(
@Param('key') key: string,
@Body('percentage') percentage: number,
@Request() req: any,
): Promise<FeatureFlag> {
return this.featureFlagsService.updateRollout(key, percentage, req.user?.id);
}
}
// Controller público para verificar features
@Controller('feature-flags')
export class FeatureFlagsPublicController {
constructor(private readonly featureFlagsService: FeatureFlagsService) {}
@Post(':key/check')
@UseGuards(JwtAuthGuard)
async checkFeature(
@Param('key') key: string,
@Request() req: any,
): Promise<FeatureFlagCheckResultDto> {
return this.featureFlagsService.isEnabled(
key,
req.user?.id,
req.user?.roles,
);
}
}
Paso 8: Crear Módulo
// src/modules/feature-flags/feature-flags.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlag } from './entities/feature-flag.entity';
import { FeatureFlagsService } from './services/feature-flags.service';
import {
FeatureFlagsController,
FeatureFlagsPublicController,
} from './controllers/feature-flags.controller';
import { FeatureFlagGuard } from './guards/feature-flag.guard';
@Module({
imports: [TypeOrmModule.forFeature([FeatureFlag])],
controllers: [FeatureFlagsController, FeatureFlagsPublicController],
providers: [FeatureFlagsService, FeatureFlagGuard],
exports: [FeatureFlagsService, FeatureFlagGuard],
})
export class FeatureFlagsModule {}
Paso 9: Registrar en AppModule
// src/app.module.ts
import { Module } from '@nestjs/common';
import { FeatureFlagsModule } from './modules/feature-flags/feature-flags.module';
@Module({
imports: [
// ... otros módulos
FeatureFlagsModule,
],
})
export class AppModule {}
Paso 10: Migraciones SQL
-- migrations/001_create_feature_flags.sql
CREATE SCHEMA IF NOT EXISTS config;
CREATE TABLE config.feature_flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
feature_key VARCHAR(100) UNIQUE NOT NULL,
feature_name VARCHAR(255) NOT NULL,
description TEXT,
is_enabled BOOLEAN DEFAULT false,
rollout_percentage INTEGER DEFAULT 0 CHECK (rollout_percentage >= 0 AND rollout_percentage <= 100),
target_users UUID[],
target_roles VARCHAR(50)[],
target_conditions JSONB DEFAULT '{}',
starts_at TIMESTAMP WITH TIME ZONE,
ends_at TIMESTAMP WITH TIME ZONE,
metadata JSONB DEFAULT '{}',
created_by UUID,
updated_by UUID,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Índices
CREATE INDEX idx_feature_flags_key ON config.feature_flags(feature_key);
CREATE INDEX idx_feature_flags_enabled ON config.feature_flags(is_enabled) WHERE is_enabled = true;
CREATE INDEX idx_feature_flags_dates ON config.feature_flags(starts_at, ends_at) WHERE is_enabled = true;
-- Trigger para updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_feature_flags_updated_at
BEFORE UPDATE ON config.feature_flags
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
Paso 11: Uso en Código
Verificación programática
// En cualquier servicio
@Injectable()
export class CheckoutService {
constructor(
private readonly featureFlagsService: FeatureFlagsService,
) {}
async processCheckout(userId: string, cart: Cart) {
const useNewCheckout = await this.featureFlagsService.isEnabled(
'new_checkout_flow',
userId,
);
if (useNewCheckout.enabled) {
return this.processNewCheckout(cart);
}
return this.processLegacyCheckout(cart);
}
}
Proteger rutas con Guard
@Controller('experiments')
@UseGuards(JwtAuthGuard)
export class ExperimentsController {
@Get('new-dashboard')
@UseGuards(FeatureFlagGuard)
@FeatureFlag('new_dashboard')
async getNewDashboard() {
return { message: 'Bienvenido al nuevo dashboard!' };
}
}
Helper para frontend
// src/modules/feature-flags/feature-flags.helper.ts
import { FeatureFlagsService } from './services/feature-flags.service';
export async function getFeatureFlagsForUser(
service: FeatureFlagsService,
userId: string,
userRoles: string[],
featureKeys: string[],
): Promise<Record<string, boolean>> {
const results: Record<string, boolean> = {};
await Promise.all(
featureKeys.map(async (key) => {
const result = await service.isEnabled(key, userId, userRoles);
results[key] = result.enabled;
}),
);
return results;
}
// Uso: endpoint que retorna todas las features del usuario
@Get('my-features')
@UseGuards(JwtAuthGuard)
async getMyFeatures(@Request() req: any) {
const keys = ['new_checkout', 'dark_mode', 'beta_features'];
return getFeatureFlagsForUser(
this.featureFlagsService,
req.user.id,
req.user.roles,
keys,
);
}
Variables de Entorno
# Feature Flags (opcional)
FEATURE_FLAGS_CACHE_TTL=300
FEATURE_FLAGS_DEFAULT=false
Checklist de Implementación
- Entidad FeatureFlag creada
- DTOs de validación creados
- FeatureFlagsService implementado con hash consistente
- FeatureFlagGuard creado
- Decorador @FeatureFlag creado
- Controllers (admin y público) implementados
- Módulo registrado en AppModule
- Migración SQL ejecutada
- Build pasa sin errores
- Test: crear flag y verificar isEnabled
Verificar Funcionamiento
# 1. Crear feature flag
curl -X POST http://localhost:3000/api/admin/feature-flags \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"key": "test_feature",
"name": "Test Feature",
"isEnabled": true,
"rolloutPercentage": 50
}'
# 2. Verificar si está habilitada para un usuario
curl -X POST http://localhost:3000/api/feature-flags/test_feature/check \
-H "Authorization: Bearer $USER_TOKEN"
# 3. Actualizar rollout
curl -X PUT http://localhost:3000/api/admin/feature-flags/test_feature/rollout \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"percentage": 100}'
# 4. Deshabilitar
curl -X POST http://localhost:3000/api/admin/feature-flags/test_feature/disable \
-H "Authorization: Bearer $ADMIN_TOKEN"
Troubleshooting
Feature siempre retorna false
- Verificar que
is_enabled = true - Verificar que
rollout_percentage > 0o usuario entarget_users - Verificar período de validez (
starts_at,ends_at)
Rollout no es consistente
- Verificar que se pase el mismo
userId - El hash depende de
userId + featureKey
Guard no funciona
- Verificar que
FeatureFlagGuardesté registrado en providers - Verificar que
@FeatureFlag('key')esté en el handler
Versión: 1.0.0 Sistema: SIMCO Catálogo