# ET-BACKEND-MGN-003-004: Gestión de Unidades de Medida (UoM)

**RF Asociado:** [RF-MGN-003-004](../../requerimientos-funcionales/mgn-003/RF-MGN-003-004-gestión-de-unidades-de-medida-uom.md)
**Módulo:** MGN-003
**Complejidad:** Baja
**Story Points:** 2 SP (Backend)
**Estado:** Diseñado
**Fecha:** 2025-11-24

## Resumen Técnico

Backend API para gestión de unidades de medida (uom). 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/uom
```typescript
// Request
POST /api/v1/uom
Authorization: Bearer <JWT>
Content-Type: application/json

{
  // Payload según RF
}

// Response 201 Created
{
  "data": { ... },
  "message": "GestiónUnidadesMedidauom 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 gestiónunidadesmedidauom"
}
```

#### GET /api/v1/uom
```typescript
// Request
GET /api/v1/uom?page=1&limit=20&sortBy=createdAt&sortOrder=desc
Authorization: Bearer <JWT>

// Response 200 OK
{
  "data": [...],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5
  }
}
```

#### GET /api/v1/uom/:id
```typescript
// Request
GET /api/v1/uom/{id}
Authorization: Bearer <JWT>

// Response 200 OK
{
  "data": { ... }
}

// Response 404 Not Found
{
  "error": "NotFoundError",
  "message": "GestiónUnidadesMedidauom no encontrado"
}
```

#### PUT /api/v1/uom/:id
```typescript
// Request
PUT /api/v1/uom/{id}
Authorization: Bearer <JWT>
Content-Type: application/json

{
  // Payload de actualización
}

// Response 200 OK
{
  "data": { ... },
  "message": "GestiónUnidadesMedidauom actualizado exitosamente"
}
```

#### DELETE /api/v1/uom/:id
```typescript
// Request
DELETE /api/v1/uom/{id}
Authorization: Bearer <JWT>

// Response 200 OK (soft delete)
{
  "message": "GestiónUnidadesMedidauom 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: core-schema-ddl.sql
model GestiónUnidadesMedidauom {
  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("core.uoms")
  @@index([tenantId])
  @@unique([tenantId, code])
}
```

## DTOs (Data Transfer Objects)

### Create DTO
```typescript
// src/modules/mgn-003/dto/create-gestiónunidadesmedidauom.dto.ts
import { IsString, IsEmail, IsOptional, MinLength, MaxLength, IsUUID } from 'class-validator';

export class CreateGestiónUnidadesMedidauomDto {
  @IsString()
  @MinLength(3)
  @MaxLength(255)
  name: string;

  @IsString()
  @IsOptional()
  @MaxLength(50)
  code?: string;

  // Campos adicionales según RF
}
```

### Update DTO
```typescript
// src/modules/mgn-003/dto/update-gestiónunidadesmedidauom.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateGestiónUnidadesMedidauomDto } from './create-gestiónunidadesmedidauom.dto';

export class UpdateGestiónUnidadesMedidauomDto extends PartialType(CreateGestiónUnidadesMedidauomDto) {}
```

### Response DTO
```typescript
// src/modules/mgn-003/dto/gestiónunidadesmedidauom-response.dto.ts
export class GestiónUnidadesMedidauomResponseDto {
  id: string;
  tenantId: string;
  name: string;
  code?: string;
  createdAt: Date;
  updatedAt?: Date;
}
```

### Filter DTO
```typescript
// src/modules/mgn-003/dto/filter-gestiónunidadesmedidauom.dto.ts
import { IsOptional, IsString, IsInt, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';

export class FilterGestiónUnidadesMedidauomDto {
  @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-003/services/gestiónunidadesmedidauom.service.ts
import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common';
import { PrismaService } from '@shared/prisma/prisma.service';
import { CreateGestiónUnidadesMedidauomDto, UpdateGestiónUnidadesMedidauomDto, FilterGestiónUnidadesMedidauomDto } from '../dto';

@Injectable()
export class GestiónUnidadesMedidauomService {
  constructor(private readonly prisma: PrismaService) {}

  async create(tenantId: string, userId: string, dto: CreateGestiónUnidadesMedidauomDto) {
    // 1. Validaciones de negocio
    await this.validateBusinessRules(tenantId, dto);

    // 2. Verificar duplicados
    if (dto.code) {
      const existing = await this.prisma.gestiónunidadesmedidauom.findFirst({
        where: {
          tenantId,
          code: dto.code,
          deletedAt: null,
        },
      });

      if (existing) {
        throw new ConflictException(`GestiónUnidadesMedidauom con código ${dto.code} ya existe`);
      }
    }

    // 3. Crear en BD (RLS automático por tenantId)
    const entity = await this.prisma.gestiónunidadesmedidauom.create({
      data: {
        tenantId,
        createdBy: userId,
        ...dto,
      },
    });

    // 4. Eventos/side effects (si aplica)
    // await this.eventEmitter.emit('gestiónunidadesmedidauom.created', entity);

    return entity;
  }

  async findAll(tenantId: string, filters: FilterGestiónUnidadesMedidauomDto) {
    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.gestiónunidadesmedidauom.findMany({
        where,
        skip,
        take: limit,
        orderBy: { [sortBy]: sortOrder },
      }),
      this.prisma.gestiónunidadesmedidauom.count({ where }),
    ]);

    return {
      data,
      meta: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  async findOne(tenantId: string, id: string) {
    const entity = await this.prisma.gestiónunidadesmedidauom.findFirst({
      where: {
        id,
        tenantId,
        deletedAt: null,
      },
    });

    if (!entity) {
      throw new NotFoundException(`GestiónUnidadesMedidauom con ID ${id} no encontrado`);
    }

    return entity;
  }

  async update(tenantId: string, userId: string, id: string, dto: UpdateGestiónUnidadesMedidauomDto) {
    // 1. Verificar existencia
    await this.findOne(tenantId, id);

    // 2. Validar cambios
    await this.validateBusinessRules(tenantId, dto, id);

    // 3. Actualizar
    const entity = await this.prisma.gestiónunidadesmedidauom.update({
      where: { id },
      data: {
        ...dto,
        updatedBy: userId,
        updatedAt: new Date(),
      },
    });

    // 4. Eventos
    // await this.eventEmitter.emit('gestiónunidadesmedidauom.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.gestiónunidadesmedidauom.update({
      where: { id },
      data: {
        deletedAt: new Date(),
        deletedBy: userId,
      },
    });

    // 4. Eventos
    // await this.eventEmitter.emit('gestiónunidadesmedidauom.deleted', { id });
  }

  private async validateBusinessRules(
    tenantId: string,
    dto: Partial<CreateGestiónUnidadesMedidauomDto>,
    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-003/controllers/gestiónunidadesmedidauom.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 { GestiónUnidadesMedidauomService } from '../services/gestiónunidadesmedidauom.service';
import { CreateGestiónUnidadesMedidauomDto, UpdateGestiónUnidadesMedidauomDto, FilterGestiónUnidadesMedidauomDto, GestiónUnidadesMedidauomResponseDto } from '../dto';

@ApiTags('GestiónUnidadesMedidauom')
@ApiBearerAuth()
@Controller('api/v1/uom')
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class GestiónUnidadesMedidauomController {
  constructor(private readonly service: GestiónUnidadesMedidauomService) {}

  @Post()
  @RequirePermissions('mgn-003.gestiónunidadesmedidauom.create')
  @ApiOperation({ summary: 'Crear gestiónunidadesmedidauom' })
  @ApiResponse({ status: 201, description: 'GestiónUnidadesMedidauom creado exitosamente', type: GestiónUnidadesMedidauomResponseDto })
  @ApiResponse({ status: 400, description: 'Datos inválidos' })
  @ApiResponse({ status: 403, description: 'Sin permisos' })
  async create(
    @CurrentTenant() tenantId: string,
    @CurrentUser('id') userId: string,
    @Body() dto: CreateGestiónUnidadesMedidauomDto,
  ) {
    const entity = await this.service.create(tenantId, userId, dto);
    return {
      data: entity,
      message: 'GestiónUnidadesMedidauom creado exitosamente',
    };
  }

  @Get()
  @RequirePermissions('mgn-003.gestiónunidadesmedidauom.read')
  @ApiOperation({ summary: 'Listar gestiónunidadesmedidauoms' })
  @ApiResponse({ status: 200, description: 'Lista de gestiónunidadesmedidauoms', type: [GestiónUnidadesMedidauomResponseDto] })
  async findAll(
    @CurrentTenant() tenantId: string,
    @Query() filters: FilterGestiónUnidadesMedidauomDto,
  ) {
    return this.service.findAll(tenantId, filters);
  }

  @Get(':id')
  @RequirePermissions('mgn-003.gestiónunidadesmedidauom.read')
  @ApiOperation({ summary: 'Obtener gestiónunidadesmedidauom por ID' })
  @ApiResponse({ status: 200, description: 'GestiónUnidadesMedidauom encontrado', type: GestiónUnidadesMedidauomResponseDto })
  @ApiResponse({ status: 404, description: 'GestiónUnidadesMedidauom 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-003.gestiónunidadesmedidauom.update')
  @ApiOperation({ summary: 'Actualizar gestiónunidadesmedidauom' })
  @ApiResponse({ status: 200, description: 'GestiónUnidadesMedidauom actualizado exitosamente' })
  @ApiResponse({ status: 404, description: 'GestiónUnidadesMedidauom no encontrado' })
  async update(
    @CurrentTenant() tenantId: string,
    @CurrentUser('id') userId: string,
    @Param('id') id: string,
    @Body() dto: UpdateGestiónUnidadesMedidauomDto,
  ) {
    const entity = await this.service.update(tenantId, userId, id, dto);
    return {
      data: entity,
      message: 'GestiónUnidadesMedidauom actualizado exitosamente',
    };
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  @RequirePermissions('mgn-003.gestiónunidadesmedidauom.delete')
  @ApiOperation({ summary: 'Eliminar gestiónunidadesmedidauom' })
  @ApiResponse({ status: 204, description: 'GestiónUnidadesMedidauom eliminado exitosamente' })
  @ApiResponse({ status: 404, description: 'GestiónUnidadesMedidauom 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: Validaciones de integridad según modelo de datos
```typescript
private async validateRN1(dto: Partial<CreateGestiónUnidadesMedidauomDto>): Promise<void> {
  // Implementación de la regla: Validaciones de integridad según modelo de datos
  // TODO: Implementar validación específica
}
```

### RN-2: RLS filtra registros por tenant y empresa
```typescript
private async validateRN2(dto: Partial<CreateGestiónUnidadesMedidauomDto>): Promise<void> {
  // Implementación de la regla: RLS filtra registros por tenant y empresa
  // TODO: Implementar validación específica
}
```

### RN-3: Cambios quedan registrados en audit log
```typescript
private async validateRN3(dto: Partial<CreateGestiónUnidadesMedidauomDto>): Promise<void> {
  // Implementación de la regla: Cambios quedan registrados en audit log
  // TODO: Implementar validación específica
}
```

### RN-4: Notificaciones según configuración de usuario
```typescript
private async validateRN4(dto: Partial<CreateGestiónUnidadesMedidauomDto>): Promise<void> {
  // Implementación de la regla: Notificaciones según configuración de usuario
  // 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-003.gestiónunidadesmedidauom.create')
@Post()
async create(...) { ... }
```

Permisos requeridos:
- `mgn-003.gestiónunidadesmedidauom.create` - Crear registros
- `mgn-003.gestiónunidadesmedidauom.read` - Leer registros
- `mgn-003.gestiónunidadesmedidauom.update` - Actualizar registros
- `mgn-003.gestiónunidadesmedidauom.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
// gestiónunidadesmedidauom.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { GestiónUnidadesMedidauomService } from './gestiónunidadesmedidauom.service';
import { PrismaService } from '@shared/prisma/prisma.service';
import { NotFoundException, ConflictException } from '@nestjs/common';

describe('GestiónUnidadesMedidauomService', () => {
  let service: GestiónUnidadesMedidauomService;
  let prisma: PrismaService;

  const mockPrisma = {
    gestiónunidadesmedidauom: {
      create: jest.fn(),
      findMany: jest.fn(),
      findFirst: jest.fn(),
      update: jest.fn(),
      count: jest.fn(),
    },
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        GestiónUnidadesMedidauomService,
        { provide: PrismaService, useValue: mockPrisma },
      ],
    }).compile();

    service = module.get<GestiónUnidadesMedidauomService>(GestiónUnidadesMedidauomService);
    prisma = module.get<PrismaService>(PrismaService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('create', () => {
    it('should create gestiónunidadesmedidauom with valid data', async () => {
      const tenantId = 'tenant-123';
      const userId = 'user-456';
      const dto = { name: 'Test GestiónUnidadesMedidauom', code: 'TEST' };
      const expected = { id: 'id-789', tenantId, ...dto };

      mockPrisma.gestiónunidadesmedidauom.findFirst.mockResolvedValue(null);
      mockPrisma.gestiónunidadesmedidauom.create.mockResolvedValue(expected);

      const result = await service.create(tenantId, userId, dto);

      expect(result).toEqual(expected);
      expect(mockPrisma.gestiónunidadesmedidauom.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 GestiónUnidadesMedidauom', code: 'DUPLICATE' };

      mockPrisma.gestiónunidadesmedidauom.findFirst.mockResolvedValue({ id: 'existing' });

      await expect(service.create(tenantId, userId, dto)).rejects.toThrow(ConflictException);
    });
  });

  describe('findOne', () => {
    it('should return gestiónunidadesmedidauom when found', async () => {
      const tenantId = 'tenant-123';
      const id = 'id-789';
      const expected = { id, tenantId, name: 'Test' };

      mockPrisma.gestiónunidadesmedidauom.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.gestiónunidadesmedidauom.findFirst.mockResolvedValue(null);

      await expect(service.findOne(tenantId, id)).rejects.toThrow(NotFoundException);
    });
  });

  describe('update', () => {
    it('should update gestiónunidadesmedidauom 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.gestiónunidadesmedidauom.findFirst.mockResolvedValue(existing);
      mockPrisma.gestiónunidadesmedidauom.update.mockResolvedValue(expected);

      const result = await service.update(tenantId, userId, id, dto);

      expect(result).toEqual(expected);
    });
  });

  describe('remove', () => {
    it('should soft delete gestiónunidadesmedidauom', async () => {
      const tenantId = 'tenant-123';
      const userId = 'user-456';
      const id = 'id-789';
      const existing = { id, tenantId, name: 'To Delete' };

      mockPrisma.gestiónunidadesmedidauom.findFirst.mockResolvedValue(existing);
      mockPrisma.gestiónunidadesmedidauom.update.mockResolvedValue({ ...existing, deletedAt: new Date() });

      await service.remove(tenantId, userId, id);

      expect(mockPrisma.gestiónunidadesmedidauom.update).toHaveBeenCalledWith({
        where: { id },
        data: {
          deletedAt: expect.any(Date),
          deletedBy: userId,
        },
      });
    });
  });
});
```

### Integration Tests (e2e)
```typescript
// gestiónunidadesmedidauom.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('GestiónUnidadesMedidauomController (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/uom', () => {
    it('should create gestiónunidadesmedidauom successfully', () => {
      return request(app.getHttpServer())
        .post('/api/v1/uom')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          name: 'Test GestiónUnidadesMedidauom',
          code: 'TEST001',
        })
        .expect(201)
        .expect((res) => {
          expect(res.body.data).toHaveProperty('id');
          expect(res.body.data.name).toBe('Test GestiónUnidadesMedidauom');
          expect(res.body.message).toContain('creado exitosamente');
        });
    });

    it('should return 400 for invalid data', () => {
      return request(app.getHttpServer())
        .post('/api/v1/uom')
        .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/uom')
        .send({ name: 'Test' })
        .expect(401);
    });
  });

  describe('GET /api/v1/uom', () => {
    it('should return list of gestiónunidadesmedidauoms', () => {
      return request(app.getHttpServer())
        .get('/api/v1/uom')
        .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/uom?search=test')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);
    });
  });

  describe('GET /api/v1/uom/:id', () => {
    it('should return gestiónunidadesmedidauom by ID', async () => {
      // Crear primero
      const createRes = await request(app.getHttpServer())
        .post('/api/v1/uom')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'Find By ID Test' });

      const id = createRes.body.data.id;

      return request(app.getHttpServer())
        .get(`/api/v1/uom/${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/uom/non-existent-id')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(404);
    });
  });

  describe('PUT /api/v1/uom/:id', () => {
    it('should update gestiónunidadesmedidauom successfully', async () => {
      // Crear primero
      const createRes = await request(app.getHttpServer())
        .post('/api/v1/uom')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'Original Name' });

      const id = createRes.body.data.id;

      return request(app.getHttpServer())
        .put(`/api/v1/uom/${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/uom/:id', () => {
    it('should soft delete gestiónunidadesmedidauom', async () => {
      // Crear primero
      const createRes = await request(app.getHttpServer())
        .post('/api/v1/uom')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'To Delete' });

      const id = createRes.body.data.id;

      await request(app.getHttpServer())
        .delete(`/api/v1/uom/${id}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(204);

      // Verificar que no aparece en listados
      const listRes = await request(app.getHttpServer())
        .get('/api/v1/uom')
        .set('Authorization', `Bearer ${authToken}`);

      const deleted = listRes.body.data.find(item => item.id === id);
      expect(deleted).toBeUndefined();
    });
  });
});
```

## Performance

### Índices Necesarios
```sql
-- Índices definidos en core-schema-ddl.sql
CREATE INDEX idx_gestiónunidadesmedidauom_tenant_code ON core.gestiónunidadesmedidauom(tenant_id, code);
CREATE INDEX idx_gestiónunidadesmedidauom_tenant_name ON core.gestiónunidadesmedidauom(tenant_id, name);
CREATE INDEX idx_gestiónunidadesmedidauom_created_at ON core.gestiónunidadesmedidauom(created_at DESC);

-- Para soft delete
CREATE INDEX idx_gestiónunidadesmedidauom_deleted_at ON core.gestiónunidadesmedidauom(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: FilterGestiónUnidadesMedidauomDto) {
  // ...
}

// Invalidación en cambios
@CacheEvict({ pattern: 'gestiónunidadesmedidauom:*' })
async update(...) { ... }

@CacheEvict({ pattern: 'gestiónunidadesmedidauom:*' })
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: FilterGestiónUnidadesMedidauomDto) {
  return this.prisma.gestiónunidadesmedidauom.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 GestiónUnidadesMedidauomService {
  private readonly logger = new Logger(GestiónUnidadesMedidauomService.name);

  async create(tenantId: string, userId: string, dto: CreateGestiónUnidadesMedidauomDto) {
    this.logger.log({
      action: 'gestiónunidadesmedidauom.create',
      tenantId,
      userId,
      data: dto,
    });

    try {
      const entity = await this.prisma.gestiónunidadesmedidauom.create({ ... });

      this.logger.log({
        action: 'gestiónunidadesmedidauom.created',
        tenantId,
        userId,
        entityId: entity.id,
      });

      return entity;
    } catch (error) {
      this.logger.error({
        action: 'gestiónunidadesmedidauom.create.error',
        tenantId,
        userId,
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }
}
```

### Métricas (Prometheus)
- `http_requests_total{method="POST",endpoint="/api/v1/uom",status="201"}`
- `http_request_duration_seconds{endpoint="/api/v1/uom"}`
- `gestiónunidadesmedidauom_created_total{tenant_id="xxx"}`

## Referencias

- [RF Asociado](../../requerimientos-funcionales/mgn-003/RF-MGN-003-004-gestión-de-unidades-de-medida-uom.md)
- [Database Schema](../database-design/schemas/core-schema-ddl.sql)
- [Domain Model](../domain-models/core-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-003/gestiónunidadesmedidauom`
- [ ] Crear service: `nest g service mgn-003/gestiónunidadesmedidauom`
- [ ] Crear controller: `nest g controller mgn-003/gestiónunidadesmedidauom`
- [ ] Crear DTOs en `mgn-003/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
