# ET-BACKEND-MGN-002-002: Configuración de Empresa **RF Asociado:** [RF-MGN-002-002](../../requerimientos-funcionales/mgn-002/RF-MGN-002-002-configuración-de-empresa.md) **Módulo:** MGN-002 **Complejidad:** Baja **Story Points:** 2 SP (Backend) **Estado:** Diseñado **Fecha:** 2025-11-24 ## Resumen Técnico Backend API para configuración de empresa. Implementa endpoints RESTful con NestJS, validaciones de negocio, persistencia con Prisma ORM y soporte multi-tenancy con Row Level Security (RLS). ## Stack Tecnológico - **Runtime:** Node.js 20 LTS - **Framework:** NestJS 10.x - **ORM:** Prisma 5.x (con multi-schema support) - **Validación:** class-validator + class-transformer - **Autenticación:** JWT (Passport.js) - **Base de Datos:** PostgreSQL 16 (multi-schema + RLS) - **Testing:** Jest + Supertest - **Documentación:** Swagger/OpenAPI 3.0 ## Arquitectura del Endpoint ### API Endpoints #### POST /api/v1/companies/:id/settings ```typescript // Request POST /api/v1/companies/:id/settings Authorization: Bearer Content-Type: application/json { // Payload según RF } // Response 201 Created { "data": { ... }, "message": "ConfiguraciónEmpresa creado exitosamente", "timestamp": "2025-11-24T..." } // Response 400 Bad Request { "error": "ValidationError", "message": "Datos inválidos", "details": [...] } // Response 403 Forbidden { "error": "ForbiddenError", "message": "Sin permisos para crear configuraciónempresa" } ``` #### GET /api/v1/companies/:id/settings ```typescript // Request GET /api/v1/companies/:id/settings?page=1&limit=20&sortBy=createdAt&sortOrder=desc Authorization: Bearer // Response 200 OK { "data": [...], "meta": { "page": 1, "limit": 20, "total": 100, "totalPages": 5 } } ``` #### GET /api/v1/companies/:id/settings/:id ```typescript // Request GET /api/v1/companies/:id/settings/{id} Authorization: Bearer // Response 200 OK { "data": { ... } } // Response 404 Not Found { "error": "NotFoundError", "message": "ConfiguraciónEmpresa no encontrado" } ``` #### PUT /api/v1/companies/:id/settings/:id ```typescript // Request PUT /api/v1/companies/:id/settings/{id} Authorization: Bearer Content-Type: application/json { // Payload de actualización } // Response 200 OK { "data": { ... }, "message": "ConfiguraciónEmpresa actualizado exitosamente" } ``` #### DELETE /api/v1/companies/:id/settings/:id ```typescript // Request DELETE /api/v1/companies/:id/settings/{id} Authorization: Bearer // Response 200 OK (soft delete) { "message": "ConfiguraciónEmpresa eliminado exitosamente" } // Response 409 Conflict { "error": "ConflictError", "message": "No se puede eliminar: tiene registros relacionados" } ``` ## Modelo de Datos (Prisma Schema) ```prisma // Basado en el DDL SQL: auth-schema-ddl.sql model ConfiguraciónEmpresa { id String @id @default(uuid()) tenantId String @map("tenant_id") // Campos específicos del negocio name String @db.VarChar(255) code String? @db.VarChar(50) // Auditoría createdAt DateTime @default(now()) @map("created_at") createdBy String? @map("created_by") updatedAt DateTime? @updatedAt @map("updated_at") updatedBy String? @map("updated_by") deletedAt DateTime? @map("deleted_at") deletedBy String? @map("deleted_by") // Relations tenant Tenant @relation(fields: [tenantId], references: [id]) @@map("auth.company_settings (configuración por empresa)") @@index([tenantId]) @@unique([tenantId, code]) } ``` ## DTOs (Data Transfer Objects) ### Create DTO ```typescript // src/modules/mgn-002/dto/create-configuraciónempresa.dto.ts import { IsString, IsEmail, IsOptional, MinLength, MaxLength, IsUUID } from 'class-validator'; export class CreateConfiguraciónEmpresaDto { @IsString() @MinLength(3) @MaxLength(255) name: string; @IsString() @IsOptional() @MaxLength(50) code?: string; // Campos adicionales según RF } ``` ### Update DTO ```typescript // src/modules/mgn-002/dto/update-configuraciónempresa.dto.ts import { PartialType } from '@nestjs/mapped-types'; import { CreateConfiguraciónEmpresaDto } from './create-configuraciónempresa.dto'; export class UpdateConfiguraciónEmpresaDto extends PartialType(CreateConfiguraciónEmpresaDto) {} ``` ### Response DTO ```typescript // src/modules/mgn-002/dto/configuraciónempresa-response.dto.ts export class ConfiguraciónEmpresaResponseDto { id: string; tenantId: string; name: string; code?: string; createdAt: Date; updatedAt?: Date; } ``` ### Filter DTO ```typescript // src/modules/mgn-002/dto/filter-configuraciónempresa.dto.ts import { IsOptional, IsString, IsInt, Min, Max } from 'class-validator'; import { Type } from 'class-transformer'; export class FilterConfiguraciónEmpresaDto { @IsOptional() @IsString() search?: string; @IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number = 1; @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) limit?: number = 20; @IsOptional() @IsString() sortBy?: string = 'createdAt'; @IsOptional() @IsString() sortOrder?: 'asc' | 'desc' = 'desc'; } ``` ## Servicios de Negocio ### Service Principal ```typescript // src/modules/mgn-002/services/configuraciónempresa.service.ts import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common'; import { PrismaService } from '@shared/prisma/prisma.service'; import { CreateConfiguraciónEmpresaDto, UpdateConfiguraciónEmpresaDto, FilterConfiguraciónEmpresaDto } from '../dto'; @Injectable() export class ConfiguraciónEmpresaService { constructor(private readonly prisma: PrismaService) {} async create(tenantId: string, userId: string, dto: CreateConfiguraciónEmpresaDto) { // 1. Validaciones de negocio await this.validateBusinessRules(tenantId, dto); // 2. Verificar duplicados if (dto.code) { const existing = await this.prisma.configuraciónempresa.findFirst({ where: { tenantId, code: dto.code, deletedAt: null, }, }); if (existing) { throw new ConflictException(`ConfiguraciónEmpresa con código ${dto.code} ya existe`); } } // 3. Crear en BD (RLS automático por tenantId) const entity = await this.prisma.configuraciónempresa.create({ data: { tenantId, createdBy: userId, ...dto, }, }); // 4. Eventos/side effects (si aplica) // await this.eventEmitter.emit('configuraciónempresa.created', entity); return entity; } async findAll(tenantId: string, filters: FilterConfiguraciónEmpresaDto) { const { page, limit, sortBy, sortOrder, search } = filters; const skip = (page - 1) * limit; const where = { tenantId, deletedAt: null, ...(search && { OR: [ { name: { contains: search, mode: 'insensitive' } }, { code: { contains: search, mode: 'insensitive' } }, ], }), }; const [data, total] = await Promise.all([ this.prisma.configuraciónempresa.findMany({ where, skip, take: limit, orderBy: { [sortBy]: sortOrder }, }), this.prisma.configuraciónempresa.count({ where }), ]); return { data, meta: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; } async findOne(tenantId: string, id: string) { const entity = await this.prisma.configuraciónempresa.findFirst({ where: { id, tenantId, deletedAt: null, }, }); if (!entity) { throw new NotFoundException(`ConfiguraciónEmpresa con ID ${id} no encontrado`); } return entity; } async update(tenantId: string, userId: string, id: string, dto: UpdateConfiguraciónEmpresaDto) { // 1. Verificar existencia await this.findOne(tenantId, id); // 2. Validar cambios await this.validateBusinessRules(tenantId, dto, id); // 3. Actualizar const entity = await this.prisma.configuraciónempresa.update({ where: { id }, data: { ...dto, updatedBy: userId, updatedAt: new Date(), }, }); // 4. Eventos // await this.eventEmitter.emit('configuraciónempresa.updated', entity); return entity; } async remove(tenantId: string, userId: string, id: string) { // 1. Verificar existencia await this.findOne(tenantId, id); // 2. Verificar dependencias (opcional) // await this.checkDependencies(id); // 3. Soft delete await this.prisma.configuraciónempresa.update({ where: { id }, data: { deletedAt: new Date(), deletedBy: userId, }, }); // 4. Eventos // await this.eventEmitter.emit('configuraciónempresa.deleted', { id }); } private async validateBusinessRules( tenantId: string, dto: Partial, excludeId?: string, ) { // Implementar reglas de negocio del RF // Ejemplo: // if (dto.field && !await this.isValidField(dto.field)) { // throw new BadRequestException('Field inválido'); // } } } ``` ## Controller ```typescript // src/modules/mgn-002/controllers/configuraciónempresa.controller.ts import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; import { PermissionsGuard } from '@modules/auth/guards/permissions.guard'; import { RequirePermissions } from '@shared/decorators/require-permissions.decorator'; import { CurrentTenant } from '@shared/decorators/current-tenant.decorator'; import { CurrentUser } from '@shared/decorators/current-user.decorator'; import { ConfiguraciónEmpresaService } from '../services/configuraciónempresa.service'; import { CreateConfiguraciónEmpresaDto, UpdateConfiguraciónEmpresaDto, FilterConfiguraciónEmpresaDto, ConfiguraciónEmpresaResponseDto } from '../dto'; @ApiTags('ConfiguraciónEmpresa') @ApiBearerAuth() @Controller('api/v1/companies/:id/settings') @UseGuards(JwtAuthGuard, PermissionsGuard) export class ConfiguraciónEmpresaController { constructor(private readonly service: ConfiguraciónEmpresaService) {} @Post() @RequirePermissions('mgn-002.configuraciónempresa.create') @ApiOperation({ summary: 'Crear configuraciónempresa' }) @ApiResponse({ status: 201, description: 'ConfiguraciónEmpresa creado exitosamente', type: ConfiguraciónEmpresaResponseDto }) @ApiResponse({ status: 400, description: 'Datos inválidos' }) @ApiResponse({ status: 403, description: 'Sin permisos' }) async create( @CurrentTenant() tenantId: string, @CurrentUser('id') userId: string, @Body() dto: CreateConfiguraciónEmpresaDto, ) { const entity = await this.service.create(tenantId, userId, dto); return { data: entity, message: 'ConfiguraciónEmpresa creado exitosamente', }; } @Get() @RequirePermissions('mgn-002.configuraciónempresa.read') @ApiOperation({ summary: 'Listar configuraciónempresas' }) @ApiResponse({ status: 200, description: 'Lista de configuraciónempresas', type: [ConfiguraciónEmpresaResponseDto] }) async findAll( @CurrentTenant() tenantId: string, @Query() filters: FilterConfiguraciónEmpresaDto, ) { return this.service.findAll(tenantId, filters); } @Get(':id') @RequirePermissions('mgn-002.configuraciónempresa.read') @ApiOperation({ summary: 'Obtener configuraciónempresa por ID' }) @ApiResponse({ status: 200, description: 'ConfiguraciónEmpresa encontrado', type: ConfiguraciónEmpresaResponseDto }) @ApiResponse({ status: 404, description: 'ConfiguraciónEmpresa no encontrado' }) async findOne( @CurrentTenant() tenantId: string, @Param('id') id: string, ) { const entity = await this.service.findOne(tenantId, id); return { data: entity }; } @Put(':id') @RequirePermissions('mgn-002.configuraciónempresa.update') @ApiOperation({ summary: 'Actualizar configuraciónempresa' }) @ApiResponse({ status: 200, description: 'ConfiguraciónEmpresa actualizado exitosamente' }) @ApiResponse({ status: 404, description: 'ConfiguraciónEmpresa no encontrado' }) async update( @CurrentTenant() tenantId: string, @CurrentUser('id') userId: string, @Param('id') id: string, @Body() dto: UpdateConfiguraciónEmpresaDto, ) { const entity = await this.service.update(tenantId, userId, id, dto); return { data: entity, message: 'ConfiguraciónEmpresa actualizado exitosamente', }; } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) @RequirePermissions('mgn-002.configuraciónempresa.delete') @ApiOperation({ summary: 'Eliminar configuraciónempresa' }) @ApiResponse({ status: 204, description: 'ConfiguraciónEmpresa eliminado exitosamente' }) @ApiResponse({ status: 404, description: 'ConfiguraciónEmpresa no encontrado' }) async remove( @CurrentTenant() tenantId: string, @CurrentUser('id') userId: string, @Param('id') id: string, ) { await this.service.remove(tenantId, userId, id); } } ``` ## Reglas de Negocio (Implementación) ### RN-1: Configuración es por empresa (multi-empresa independiente) ```typescript private async validateRN1(dto: Partial): Promise { // Implementación de la regla: Configuración es por empresa (multi-empresa independiente) // TODO: Implementar validación específica } ``` ### RN-2: Cuentas contables por defecto deben existir en plan de cuentas ```typescript private async validateRN2(dto: Partial): Promise { // Implementación de la regla: Cuentas contables por defecto deben existir en plan de cuentas // TODO: Implementar validación específica } ``` ### RN-3: Almacén principal debe existir en core.warehouses ```typescript private async validateRN3(dto: Partial): Promise { // Implementación de la regla: Almacén principal debe existir en core.warehouses // TODO: Implementar validación específica } ``` ### RN-4: Prefijos de documentos deben ser únicos por empresa ```typescript private async validateRN4(dto: Partial): Promise { // Implementación de la regla: Prefijos de documentos deben ser únicos por empresa // TODO: Implementar validación específica } ``` ### RN-5: Zona horaria afecta fechas de documentos y reportes ```typescript private async validateRN5(dto: Partial): Promise { // Implementación de la regla: Zona horaria afecta fechas de documentos y reportes // TODO: Implementar validación específica } ``` ## Seguridad ### Multi-Tenancy (RLS) - Prisma middleware automático que agrega `tenantId` a todas las queries - Row Level Security en PostgreSQL valida acceso - Soft delete: usa `deletedAt` en lugar de borrado físico ### Permisos (RBAC) ```typescript @UseGuards(JwtAuthGuard, PermissionsGuard) @RequirePermissions('mgn-002.configuraciónempresa.create') @Post() async create(...) { ... } ``` Permisos requeridos: - `mgn-002.configuraciónempresa.create` - Crear registros - `mgn-002.configuraciónempresa.read` - Leer registros - `mgn-002.configuraciónempresa.update` - Actualizar registros - `mgn-002.configuraciónempresa.delete` - Eliminar registros ### Validación de Input - DTOs con class-validator para validación automática - Sanitización de strings (XSS prevention) - Rate limiting: 100 requests/min por usuario - SQL Injection prevention: Prisma ORM con prepared statements ### Auditoría Todos los cambios registran: - `createdBy` / `updatedBy` / `deletedBy` - Usuario que ejecuta acción - `createdAt` / `updatedAt` / `deletedAt` - Timestamp de acción - Integración con MGN-014 (Tracking Automático de Cambios) ## Testing ### Unit Tests ```typescript // configuraciónempresa.service.spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { ConfiguraciónEmpresaService } from './configuraciónempresa.service'; import { PrismaService } from '@shared/prisma/prisma.service'; import { NotFoundException, ConflictException } from '@nestjs/common'; describe('ConfiguraciónEmpresaService', () => { let service: ConfiguraciónEmpresaService; let prisma: PrismaService; const mockPrisma = { configuraciónempresa: { create: jest.fn(), findMany: jest.fn(), findFirst: jest.fn(), update: jest.fn(), count: jest.fn(), }, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ConfiguraciónEmpresaService, { provide: PrismaService, useValue: mockPrisma }, ], }).compile(); service = module.get(ConfiguraciónEmpresaService); prisma = module.get(PrismaService); }); afterEach(() => { jest.clearAllMocks(); }); describe('create', () => { it('should create configuraciónempresa with valid data', async () => { const tenantId = 'tenant-123'; const userId = 'user-456'; const dto = { name: 'Test ConfiguraciónEmpresa', code: 'TEST' }; const expected = { id: 'id-789', tenantId, ...dto }; mockPrisma.configuraciónempresa.findFirst.mockResolvedValue(null); mockPrisma.configuraciónempresa.create.mockResolvedValue(expected); const result = await service.create(tenantId, userId, dto); expect(result).toEqual(expected); expect(mockPrisma.configuraciónempresa.create).toHaveBeenCalledWith({ data: { tenantId, createdBy: userId, ...dto }, }); }); it('should throw ConflictException when code already exists', async () => { const tenantId = 'tenant-123'; const userId = 'user-456'; const dto = { name: 'Test ConfiguraciónEmpresa', code: 'DUPLICATE' }; mockPrisma.configuraciónempresa.findFirst.mockResolvedValue({ id: 'existing' }); await expect(service.create(tenantId, userId, dto)).rejects.toThrow(ConflictException); }); }); describe('findOne', () => { it('should return configuraciónempresa when found', async () => { const tenantId = 'tenant-123'; const id = 'id-789'; const expected = { id, tenantId, name: 'Test' }; mockPrisma.configuraciónempresa.findFirst.mockResolvedValue(expected); const result = await service.findOne(tenantId, id); expect(result).toEqual(expected); }); it('should throw NotFoundException when not found', async () => { const tenantId = 'tenant-123'; const id = 'non-existent'; mockPrisma.configuraciónempresa.findFirst.mockResolvedValue(null); await expect(service.findOne(tenantId, id)).rejects.toThrow(NotFoundException); }); }); describe('update', () => { it('should update configuraciónempresa successfully', async () => { const tenantId = 'tenant-123'; const userId = 'user-456'; const id = 'id-789'; const dto = { name: 'Updated Name' }; const existing = { id, tenantId, name: 'Old Name' }; const expected = { ...existing, ...dto }; mockPrisma.configuraciónempresa.findFirst.mockResolvedValue(existing); mockPrisma.configuraciónempresa.update.mockResolvedValue(expected); const result = await service.update(tenantId, userId, id, dto); expect(result).toEqual(expected); }); }); describe('remove', () => { it('should soft delete configuraciónempresa', async () => { const tenantId = 'tenant-123'; const userId = 'user-456'; const id = 'id-789'; const existing = { id, tenantId, name: 'To Delete' }; mockPrisma.configuraciónempresa.findFirst.mockResolvedValue(existing); mockPrisma.configuraciónempresa.update.mockResolvedValue({ ...existing, deletedAt: new Date() }); await service.remove(tenantId, userId, id); expect(mockPrisma.configuraciónempresa.update).toHaveBeenCalledWith({ where: { id }, data: { deletedAt: expect.any(Date), deletedBy: userId, }, }); }); }); }); ``` ### Integration Tests (e2e) ```typescript // configuraciónempresa.controller.e2e-spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '@/app.module'; describe('ConfiguraciónEmpresaController (e2e)', () => { let app: INestApplication; let authToken: string; let tenantId: string; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); // Login para obtener token const loginResponse = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'test@example.com', password: 'Test1234!' }); authToken = loginResponse.body.data.accessToken; tenantId = loginResponse.body.data.user.tenantId; }); afterAll(async () => { await app.close(); }); describe('POST /api/v1/companies/:id/settings', () => { it('should create configuraciónempresa successfully', () => { return request(app.getHttpServer()) .post('/api/v1/companies/:id/settings') .set('Authorization', `Bearer ${authToken}`) .send({ name: 'Test ConfiguraciónEmpresa', code: 'TEST001', }) .expect(201) .expect((res) => { expect(res.body.data).toHaveProperty('id'); expect(res.body.data.name).toBe('Test ConfiguraciónEmpresa'); expect(res.body.message).toContain('creado exitosamente'); }); }); it('should return 400 for invalid data', () => { return request(app.getHttpServer()) .post('/api/v1/companies/:id/settings') .set('Authorization', `Bearer ${authToken}`) .send({ name: '' }) // Invalid: name too short .expect(400); }); it('should return 401 without token', () => { return request(app.getHttpServer()) .post('/api/v1/companies/:id/settings') .send({ name: 'Test' }) .expect(401); }); }); describe('GET /api/v1/companies/:id/settings', () => { it('should return list of configuraciónempresas', () => { return request(app.getHttpServer()) .get('/api/v1/companies/:id/settings') .set('Authorization', `Bearer ${authToken}`) .expect(200) .expect((res) => { expect(res.body.data).toBeInstanceOf(Array); expect(res.body.meta).toHaveProperty('page'); expect(res.body.meta).toHaveProperty('total'); }); }); it('should filter by search term', () => { return request(app.getHttpServer()) .get('/api/v1/companies/:id/settings?search=test') .set('Authorization', `Bearer ${authToken}`) .expect(200); }); }); describe('GET /api/v1/companies/:id/settings/:id', () => { it('should return configuraciónempresa by ID', async () => { // Crear primero const createRes = await request(app.getHttpServer()) .post('/api/v1/companies/:id/settings') .set('Authorization', `Bearer ${authToken}`) .send({ name: 'Find By ID Test' }); const id = createRes.body.data.id; return request(app.getHttpServer()) .get(`/api/v1/companies/:id/settings/${id}`) .set('Authorization', `Bearer ${authToken}`) .expect(200) .expect((res) => { expect(res.body.data.id).toBe(id); }); }); it('should return 404 for non-existent ID', () => { return request(app.getHttpServer()) .get('/api/v1/companies/:id/settings/non-existent-id') .set('Authorization', `Bearer ${authToken}`) .expect(404); }); }); describe('PUT /api/v1/companies/:id/settings/:id', () => { it('should update configuraciónempresa successfully', async () => { // Crear primero const createRes = await request(app.getHttpServer()) .post('/api/v1/companies/:id/settings') .set('Authorization', `Bearer ${authToken}`) .send({ name: 'Original Name' }); const id = createRes.body.data.id; return request(app.getHttpServer()) .put(`/api/v1/companies/:id/settings/${id}`) .set('Authorization', `Bearer ${authToken}`) .send({ name: 'Updated Name' }) .expect(200) .expect((res) => { expect(res.body.data.name).toBe('Updated Name'); }); }); }); describe('DELETE /api/v1/companies/:id/settings/:id', () => { it('should soft delete configuraciónempresa', async () => { // Crear primero const createRes = await request(app.getHttpServer()) .post('/api/v1/companies/:id/settings') .set('Authorization', `Bearer ${authToken}`) .send({ name: 'To Delete' }); const id = createRes.body.data.id; await request(app.getHttpServer()) .delete(`/api/v1/companies/:id/settings/${id}`) .set('Authorization', `Bearer ${authToken}`) .expect(204); // Verificar que no aparece en listados const listRes = await request(app.getHttpServer()) .get('/api/v1/companies/:id/settings') .set('Authorization', `Bearer ${authToken}`); const deleted = listRes.body.data.find(item => item.id === id); expect(deleted).toBeUndefined(); }); }); }); ``` ## Performance ### Índices Necesarios ```sql -- Índices definidos en auth-schema-ddl.sql CREATE INDEX idx_configuraciónempresa_tenant_code ON auth.configuraciónempresa(tenant_id, code); CREATE INDEX idx_configuraciónempresa_tenant_name ON auth.configuraciónempresa(tenant_id, name); CREATE INDEX idx_configuraciónempresa_created_at ON auth.configuraciónempresa(created_at DESC); -- Para soft delete CREATE INDEX idx_configuraciónempresa_deleted_at ON auth.configuraciónempresa(deleted_at) WHERE deleted_at IS NULL; ``` ### Caching (Redis) ```typescript // Para listas maestras estáticas @Cacheable({ ttl: 3600 }) // 1 hora async findAll(tenantId: string, filters: FilterConfiguraciónEmpresaDto) { // ... } // Invalidación en cambios @CacheEvict({ pattern: 'configuraciónempresa:*' }) async update(...) { ... } @CacheEvict({ pattern: 'configuraciónempresa:*' }) async remove(...) { ... } ``` ### Paginación - **Default:** 20 items por página - **Máximo:** 100 items por página - **Cursor-based:** Para listados grandes (opcional) ### Query Optimization ```typescript // Usar select para reducir payload async findAll(tenantId: string, filters: FilterConfiguraciónEmpresaDto) { return this.prisma.configuraciónempresa.findMany({ where: { ... }, select: { id: true, name: true, code: true, createdAt: true, // Omitir campos pesados }, }); } ``` ## Logging y Monitoreo ```typescript import { Logger } from '@nestjs/common'; export class ConfiguraciónEmpresaService { private readonly logger = new Logger(ConfiguraciónEmpresaService.name); async create(tenantId: string, userId: string, dto: CreateConfiguraciónEmpresaDto) { this.logger.log({ action: 'configuraciónempresa.create', tenantId, userId, data: dto, }); try { const entity = await this.prisma.configuraciónempresa.create({ ... }); this.logger.log({ action: 'configuraciónempresa.created', tenantId, userId, entityId: entity.id, }); return entity; } catch (error) { this.logger.error({ action: 'configuraciónempresa.create.error', tenantId, userId, error: error.message, stack: error.stack, }); throw error; } } } ``` ### Métricas (Prometheus) - `http_requests_total{method="POST",endpoint="/api/v1/companies/:id/settings",status="201"}` - `http_request_duration_seconds{endpoint="/api/v1/companies/:id/settings"}` - `configuraciónempresa_created_total{tenant_id="xxx"}` ## Referencias - [RF Asociado](../../requerimientos-funcionales/mgn-002/RF-MGN-002-002-configuración-de-empresa.md) - [Database Schema](../database-design/schemas/auth-schema-ddl.sql) - [Domain Model](../domain-models/auth-domain.md) - [ADR-001: Stack Tecnológico](../../adr/ADR-001-stack-tecnologico.md) - [ADR-005: API Design](../../adr/ADR-005-api-design.md) ## Dependencias ### Módulos NestJS - `AuthModule` - Autenticación y autorización - `PrismaModule` - ORM y acceso a BD - `LoggerModule` - Logging centralizado - `EventsModule` - Event emitter (opcional) ### RF Bloqueantes - RF-MGN-001-001 (Autenticación de Usuarios) - RF-MGN-001-004 (Multi-Tenancy) ### RF Dependientes - [Listar RF que dependen de este] ## Notas de Implementación - [ ] Crear módulo NestJS: `nest g module mgn-002/configuraciónempresa` - [ ] Crear service: `nest g service mgn-002/configuraciónempresa` - [ ] Crear controller: `nest g controller mgn-002/configuraciónempresa` - [ ] Crear DTOs en `mgn-002/dto/` folder - [ ] Definir Prisma model en `schema.prisma` - [ ] Implementar unit tests (>80% coverage) - [ ] Implementar e2e tests - [ ] Documentar endpoints en Swagger - [ ] Validar con criterios de aceptación del RF - [ ] Code review por Tech Lead - [ ] QA testing ## Estimación - **Backend Development:** 2 SP - **Testing (Unit + e2e):** 1 SP - **Code Review + QA:** 1 SP - **Total:** 5 SP --- **Documento generado:** 2025-11-24 **Versión:** 1.0 **Estado:** Diseñado **Próximo paso:** Implementación