857 lines
19 KiB
Markdown
857 lines
19 KiB
Markdown
# ET-{modulo}-backend: Especificacion Tecnica Backend
|
|
|
|
> **INSTRUCCIONES:** Copiar este template para cada modulo.
|
|
> Eliminar esta seccion antes de guardar.
|
|
|
|
---
|
|
|
|
## Metadatos
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Modulo** | {MGN-NNN / MAI-NNN} |
|
|
| **Nombre** | {nombre del modulo} |
|
|
| **Estado** | {Borrador / Aprobado / Implementado} |
|
|
| **Version** | 1.0.0 |
|
|
| **Autor** | {nombre} |
|
|
| **Fecha** | {YYYY-MM-DD} |
|
|
|
|
---
|
|
|
|
## Resumen de Implementacion
|
|
|
|
### Estructura de Modulo
|
|
|
|
```
|
|
modules/{modulo}/
|
|
├── {modulo}.module.ts
|
|
├── {modulo}.controller.ts
|
|
├── {modulo}.service.ts
|
|
├── entities/
|
|
│ ├── {entidad}.entity.ts
|
|
│ └── index.ts
|
|
├── dto/
|
|
│ ├── create-{entidad}.dto.ts
|
|
│ ├── update-{entidad}.dto.ts
|
|
│ ├── query-{entidad}.dto.ts
|
|
│ └── index.ts
|
|
├── interfaces/
|
|
│ └── {modulo}.interface.ts
|
|
└── __tests__/
|
|
├── {modulo}.service.spec.ts
|
|
└── {modulo}.controller.spec.ts
|
|
```
|
|
|
|
### Dependencias del Modulo
|
|
|
|
| Modulo | Tipo | Proposito |
|
|
|--------|------|-----------|
|
|
| `AuthModule` | Import | Autenticacion |
|
|
| `TenantsModule` | Import | Multi-tenant |
|
|
| `{OtroModule}` | Import | {proposito} |
|
|
|
|
---
|
|
|
|
## Entities
|
|
|
|
### {NombreEntity}
|
|
|
|
**Archivo:** `entities/{nombre}.entity.ts`
|
|
**Tabla:** `{schema}.{tabla}`
|
|
|
|
```typescript
|
|
import { Entity, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
|
|
import { BaseEntity } from '@shared/entities/base.entity';
|
|
|
|
/**
|
|
* Entity para {descripcion}
|
|
*
|
|
* @see database/ddl/schemas/{schema}/tables/{NN}-{tabla}.sql
|
|
* @see docs/01-requerimientos/RF-{modulo}/
|
|
*/
|
|
@Entity({ schema: '{schema}', name: '{tabla}' })
|
|
export class {Nombre}Entity extends BaseEntity {
|
|
/**
|
|
* {Descripcion del campo}
|
|
* @example "{ejemplo}"
|
|
*/
|
|
@Column({ length: 200 })
|
|
name: string;
|
|
|
|
/**
|
|
* {Descripcion}
|
|
*/
|
|
@Column({ name: 'internal_code', length: 50, nullable: true })
|
|
internalCode?: string;
|
|
|
|
/**
|
|
* Estado del registro
|
|
*/
|
|
@Column({
|
|
type: 'enum',
|
|
enum: {Nombre}Status,
|
|
default: {Nombre}Status.DRAFT
|
|
})
|
|
status: {Nombre}Status;
|
|
|
|
// Relaciones
|
|
@ManyToOne(() => {RelatedEntity})
|
|
@JoinColumn({ name: '{fk_column}' })
|
|
{related}: {RelatedEntity};
|
|
|
|
@OneToMany(() => {ChildEntity}, child => child.{parent})
|
|
{children}: {ChildEntity}[];
|
|
}
|
|
```
|
|
|
|
### Enums
|
|
|
|
```typescript
|
|
// enums/{modulo}.enum.ts
|
|
export enum {Nombre}Status {
|
|
DRAFT = 'draft',
|
|
ACTIVE = 'active',
|
|
INACTIVE = 'inactive',
|
|
CANCELLED = 'cancelled'
|
|
}
|
|
|
|
export enum {Nombre}Type {
|
|
TYPE_A = 'type_a',
|
|
TYPE_B = 'type_b'
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DTOs
|
|
|
|
### Create{Nombre}Dto
|
|
|
|
**Archivo:** `dto/create-{nombre}.dto.ts`
|
|
|
|
```typescript
|
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
import {
|
|
IsString,
|
|
IsOptional,
|
|
IsUUID,
|
|
IsEnum,
|
|
Length,
|
|
IsNumber,
|
|
Min,
|
|
Max,
|
|
IsBoolean,
|
|
IsArray,
|
|
ValidateNested
|
|
} from 'class-validator';
|
|
import { Type } from 'class-transformer';
|
|
|
|
export class Create{Nombre}Dto {
|
|
@ApiProperty({
|
|
description: '{Descripcion del campo}',
|
|
example: '{ejemplo}',
|
|
minLength: 3,
|
|
maxLength: 200
|
|
})
|
|
@IsString()
|
|
@Length(3, 200, { message: 'Nombre debe tener entre 3 y 200 caracteres' })
|
|
name: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: '{Descripcion}',
|
|
example: '{ejemplo}'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@Length(0, 50)
|
|
internalCode?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'ID de {relacion}',
|
|
example: 'uuid-example'
|
|
})
|
|
@IsOptional()
|
|
@IsUUID('4', { message: '{Relacion} ID debe ser UUID valido' })
|
|
{related}Id?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: '{Descripcion}',
|
|
type: [Create{Child}Dto]
|
|
})
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ValidateNested({ each: true })
|
|
@Type(() => Create{Child}Dto)
|
|
{children}?: Create{Child}Dto[];
|
|
}
|
|
```
|
|
|
|
### Update{Nombre}Dto
|
|
|
|
```typescript
|
|
import { PartialType } from '@nestjs/swagger';
|
|
import { Create{Nombre}Dto } from './create-{nombre}.dto';
|
|
|
|
export class Update{Nombre}Dto extends PartialType(Create{Nombre}Dto) {}
|
|
```
|
|
|
|
### Query{Nombre}Dto
|
|
|
|
```typescript
|
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
import { IsOptional, IsEnum, IsUUID, IsString } from 'class-validator';
|
|
import { PaginationDto } from '@shared/dto/pagination.dto';
|
|
|
|
export class Query{Nombre}Dto extends PaginationDto {
|
|
@ApiPropertyOptional({ description: 'Filtrar por estado' })
|
|
@IsOptional()
|
|
@IsEnum({Nombre}Status)
|
|
status?: {Nombre}Status;
|
|
|
|
@ApiPropertyOptional({ description: 'Filtrar por {relacion}' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
{related}Id?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Buscar por nombre' })
|
|
@IsOptional()
|
|
@IsString()
|
|
search?: string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### Resumen de Endpoints
|
|
|
|
| Metodo | Ruta | Descripcion | Permisos |
|
|
|--------|------|-------------|----------|
|
|
| GET | `/api/v1/{recursos}` | Lista paginada | `{modulo}:read` |
|
|
| GET | `/api/v1/{recursos}/:id` | Obtener por ID | `{modulo}:read` |
|
|
| POST | `/api/v1/{recursos}` | Crear nuevo | `{modulo}:create` |
|
|
| PATCH | `/api/v1/{recursos}/:id` | Actualizar parcial | `{modulo}:update` |
|
|
| PUT | `/api/v1/{recursos}/:id` | Actualizar completo | `{modulo}:update` |
|
|
| DELETE | `/api/v1/{recursos}/:id` | Eliminar (soft) | `{modulo}:delete` |
|
|
| POST | `/api/v1/{recursos}/:id/{accion}` | Accion especifica | `{modulo}:{accion}` |
|
|
|
|
---
|
|
|
|
### GET /api/v1/{recursos}
|
|
|
|
**Descripcion:** Lista paginada de {recursos}
|
|
|
|
**Query Parameters:**
|
|
|
|
| Param | Tipo | Requerido | Default | Descripcion |
|
|
|-------|------|-----------|---------|-------------|
|
|
| `page` | number | No | 1 | Pagina actual |
|
|
| `per_page` | number | No | 20 | Items por pagina (max 100) |
|
|
| `sort_by` | string | No | created_at | Campo de ordenamiento |
|
|
| `sort_order` | string | No | desc | asc/desc |
|
|
| `filter[status]` | string | No | - | Filtrar por estado |
|
|
| `filter[{related}Id]` | uuid | No | - | Filtrar por relacion |
|
|
| `search` | string | No | - | Busqueda por nombre |
|
|
|
|
**Response 200:**
|
|
|
|
```json
|
|
{
|
|
"data": [
|
|
{
|
|
"id": "uuid",
|
|
"name": "Nombre ejemplo",
|
|
"internalCode": "CODE-001",
|
|
"status": "active",
|
|
"{related}": {
|
|
"id": "uuid",
|
|
"name": "Relacionado"
|
|
},
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-01-01T00:00:00Z"
|
|
}
|
|
],
|
|
"meta": {
|
|
"currentPage": 1,
|
|
"perPage": 20,
|
|
"totalPages": 5,
|
|
"totalCount": 100,
|
|
"hasNextPage": true,
|
|
"hasPrevPage": false
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### GET /api/v1/{recursos}/:id
|
|
|
|
**Descripcion:** Obtener {recurso} por ID
|
|
|
|
**Path Parameters:**
|
|
|
|
| Param | Tipo | Descripcion |
|
|
|-------|------|-------------|
|
|
| `id` | uuid | ID del {recurso} |
|
|
|
|
**Response 200:**
|
|
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "uuid",
|
|
"name": "Nombre ejemplo",
|
|
"internalCode": "CODE-001",
|
|
"status": "active",
|
|
"{related}": {
|
|
"id": "uuid",
|
|
"name": "Relacionado"
|
|
},
|
|
"{children}": [
|
|
{
|
|
"id": "uuid",
|
|
"description": "Detalle"
|
|
}
|
|
],
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-01-01T00:00:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response 404:**
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "RES-001",
|
|
"message": "{Recurso} no encontrado"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### POST /api/v1/{recursos}
|
|
|
|
**Descripcion:** Crear nuevo {recurso}
|
|
|
|
**Request Body:** `Create{Nombre}Dto`
|
|
|
|
```json
|
|
{
|
|
"name": "Nuevo recurso",
|
|
"internalCode": "CODE-002",
|
|
"{related}Id": "uuid-relacion"
|
|
}
|
|
```
|
|
|
|
**Response 201:**
|
|
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "uuid-nuevo",
|
|
"name": "Nuevo recurso",
|
|
"status": "draft",
|
|
"createdAt": "2025-01-01T00:00:00Z"
|
|
},
|
|
"message": "{Recurso} creado exitosamente"
|
|
}
|
|
```
|
|
|
|
**Response 400 (Validacion):**
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "VAL-001",
|
|
"message": "Datos de entrada invalidos",
|
|
"details": [
|
|
{
|
|
"field": "name",
|
|
"message": "Nombre debe tener entre 3 y 200 caracteres"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response 409 (Duplicado):**
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "RES-002",
|
|
"message": "Ya existe un {recurso} con ese codigo"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### PATCH /api/v1/{recursos}/:id
|
|
|
|
**Descripcion:** Actualizar {recurso} parcialmente
|
|
|
|
**Request Body:** `Update{Nombre}Dto` (campos opcionales)
|
|
|
|
```json
|
|
{
|
|
"name": "Nombre actualizado"
|
|
}
|
|
```
|
|
|
|
**Response 200:**
|
|
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "uuid",
|
|
"name": "Nombre actualizado",
|
|
"updatedAt": "2025-01-02T00:00:00Z"
|
|
},
|
|
"message": "{Recurso} actualizado exitosamente"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### DELETE /api/v1/{recursos}/:id
|
|
|
|
**Descripcion:** Eliminar {recurso} (soft delete)
|
|
|
|
**Response 200:**
|
|
|
|
```json
|
|
{
|
|
"data": null,
|
|
"message": "{Recurso} eliminado exitosamente"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### POST /api/v1/{recursos}/:id/{accion}
|
|
|
|
**Descripcion:** Ejecutar accion sobre {recurso}
|
|
|
|
**Ejemplo:** `POST /api/v1/sale-orders/:id/confirm`
|
|
|
|
**Request Body:** (opcional, segun accion)
|
|
|
|
```json
|
|
{
|
|
"notifyCustomer": true
|
|
}
|
|
```
|
|
|
|
**Response 200:**
|
|
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "uuid",
|
|
"status": "confirmed",
|
|
"{accion}At": "2025-01-02T00:00:00Z",
|
|
"{accion}By": "user-uuid"
|
|
},
|
|
"message": "{Recurso} {accion} exitosamente"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Service
|
|
|
|
### {Nombre}Service
|
|
|
|
**Archivo:** `{modulo}.service.ts`
|
|
|
|
```typescript
|
|
import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository, DataSource } from 'typeorm';
|
|
import { {Nombre}Entity } from './entities/{nombre}.entity';
|
|
import { Create{Nombre}Dto, Update{Nombre}Dto, Query{Nombre}Dto } from './dto';
|
|
import { PaginatedResponse } from '@shared/interfaces/pagination.interface';
|
|
|
|
@Injectable()
|
|
export class {Nombre}Service {
|
|
constructor(
|
|
@InjectRepository({Nombre}Entity)
|
|
private readonly repository: Repository<{Nombre}Entity>,
|
|
private readonly dataSource: DataSource,
|
|
) {}
|
|
|
|
/**
|
|
* Lista paginada con filtros
|
|
*/
|
|
async findAll(
|
|
tenantId: string,
|
|
query: Query{Nombre}Dto
|
|
): Promise<PaginatedResponse<{Nombre}Entity>> {
|
|
const { page = 1, perPage = 20, sortBy = 'createdAt', sortOrder = 'DESC' } = query;
|
|
|
|
const qb = this.repository.createQueryBuilder('entity')
|
|
.where('entity.tenantId = :tenantId', { tenantId })
|
|
.andWhere('entity.isActive = :isActive', { isActive: true });
|
|
|
|
// Aplicar filtros
|
|
if (query.status) {
|
|
qb.andWhere('entity.status = :status', { status: query.status });
|
|
}
|
|
|
|
if (query.search) {
|
|
qb.andWhere('entity.name ILIKE :search', { search: `%${query.search}%` });
|
|
}
|
|
|
|
// Ordenamiento y paginacion
|
|
qb.orderBy(`entity.${sortBy}`, sortOrder as 'ASC' | 'DESC')
|
|
.skip((page - 1) * perPage)
|
|
.take(perPage);
|
|
|
|
const [data, totalCount] = await qb.getManyAndCount();
|
|
|
|
return {
|
|
data,
|
|
meta: {
|
|
currentPage: page,
|
|
perPage,
|
|
totalPages: Math.ceil(totalCount / perPage),
|
|
totalCount,
|
|
hasNextPage: page * perPage < totalCount,
|
|
hasPrevPage: page > 1,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Obtener por ID
|
|
*/
|
|
async findOne(tenantId: string, id: string): Promise<{Nombre}Entity> {
|
|
const entity = await this.repository.findOne({
|
|
where: { id, tenantId, isActive: true },
|
|
relations: ['{related}', '{children}'],
|
|
});
|
|
|
|
if (!entity) {
|
|
throw new NotFoundException({
|
|
code: 'RES-001',
|
|
message: '{Recurso} no encontrado',
|
|
});
|
|
}
|
|
|
|
return entity;
|
|
}
|
|
|
|
/**
|
|
* Crear nuevo
|
|
*/
|
|
async create(
|
|
tenantId: string,
|
|
userId: string,
|
|
dto: Create{Nombre}Dto
|
|
): Promise<{Nombre}Entity> {
|
|
// Validar unicidad si aplica
|
|
if (dto.internalCode) {
|
|
const exists = await this.repository.findOne({
|
|
where: { tenantId, internalCode: dto.internalCode, isActive: true },
|
|
});
|
|
|
|
if (exists) {
|
|
throw new ConflictException({
|
|
code: 'RES-002',
|
|
message: 'Ya existe un {recurso} con ese codigo',
|
|
});
|
|
}
|
|
}
|
|
|
|
const entity = this.repository.create({
|
|
...dto,
|
|
tenantId,
|
|
createdBy: userId,
|
|
updatedBy: userId,
|
|
});
|
|
|
|
return this.repository.save(entity);
|
|
}
|
|
|
|
/**
|
|
* Actualizar
|
|
*/
|
|
async update(
|
|
tenantId: string,
|
|
userId: string,
|
|
id: string,
|
|
dto: Update{Nombre}Dto
|
|
): Promise<{Nombre}Entity> {
|
|
const entity = await this.findOne(tenantId, id);
|
|
|
|
Object.assign(entity, dto, { updatedBy: userId });
|
|
|
|
return this.repository.save(entity);
|
|
}
|
|
|
|
/**
|
|
* Eliminar (soft delete)
|
|
*/
|
|
async remove(tenantId: string, userId: string, id: string): Promise<void> {
|
|
const entity = await this.findOne(tenantId, id);
|
|
|
|
entity.isActive = false;
|
|
entity.updatedBy = userId;
|
|
|
|
await this.repository.save(entity);
|
|
}
|
|
|
|
/**
|
|
* Accion: {accion}
|
|
*/
|
|
async {accion}(tenantId: string, userId: string, id: string): Promise<{Nombre}Entity> {
|
|
const entity = await this.findOne(tenantId, id);
|
|
|
|
// Validar transicion de estado
|
|
if (entity.status !== {Nombre}Status.DRAFT) {
|
|
throw new BadRequestException({
|
|
code: 'BIZ-002',
|
|
message: 'Solo se puede {accion} un {recurso} en estado borrador',
|
|
});
|
|
}
|
|
|
|
entity.status = {Nombre}Status.ACTIVE;
|
|
entity.updatedBy = userId;
|
|
|
|
return this.repository.save(entity);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Controller
|
|
|
|
### {Nombre}Controller
|
|
|
|
**Archivo:** `{modulo}.controller.ts`
|
|
|
|
```typescript
|
|
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Patch,
|
|
Delete,
|
|
Body,
|
|
Param,
|
|
Query,
|
|
UseGuards,
|
|
ParseUUIDPipe,
|
|
} from '@nestjs/common';
|
|
import {
|
|
ApiTags,
|
|
ApiOperation,
|
|
ApiResponse,
|
|
ApiBearerAuth,
|
|
ApiParam,
|
|
ApiQuery,
|
|
} from '@nestjs/swagger';
|
|
import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard';
|
|
import { PermissionsGuard } from '@modules/auth/guards/permissions.guard';
|
|
import { RequirePermissions } from '@modules/auth/decorators/permissions.decorator';
|
|
import { CurrentUser } from '@modules/auth/decorators/current-user.decorator';
|
|
import { TenantId } from '@shared/decorators/tenant.decorator';
|
|
import { {Nombre}Service } from './{modulo}.service';
|
|
import { Create{Nombre}Dto, Update{Nombre}Dto, Query{Nombre}Dto } from './dto';
|
|
|
|
@Controller('{recursos}')
|
|
@ApiTags('{Recursos}')
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
|
export class {Nombre}Controller {
|
|
constructor(private readonly service: {Nombre}Service) {}
|
|
|
|
@Get()
|
|
@ApiOperation({ summary: 'Lista paginada de {recursos}' })
|
|
@ApiResponse({ status: 200, description: 'Lista obtenida exitosamente' })
|
|
@RequirePermissions('{modulo}:read')
|
|
async findAll(
|
|
@TenantId() tenantId: string,
|
|
@Query() query: Query{Nombre}Dto,
|
|
) {
|
|
return this.service.findAll(tenantId, query);
|
|
}
|
|
|
|
@Get(':id')
|
|
@ApiOperation({ summary: 'Obtener {recurso} por ID' })
|
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
@ApiResponse({ status: 200, description: '{Recurso} encontrado' })
|
|
@ApiResponse({ status: 404, description: '{Recurso} no encontrado' })
|
|
@RequirePermissions('{modulo}:read')
|
|
async findOne(
|
|
@TenantId() tenantId: string,
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
) {
|
|
return { data: await this.service.findOne(tenantId, id) };
|
|
}
|
|
|
|
@Post()
|
|
@ApiOperation({ summary: 'Crear nuevo {recurso}' })
|
|
@ApiResponse({ status: 201, description: '{Recurso} creado' })
|
|
@ApiResponse({ status: 400, description: 'Datos invalidos' })
|
|
@ApiResponse({ status: 409, description: '{Recurso} ya existe' })
|
|
@RequirePermissions('{modulo}:create')
|
|
async create(
|
|
@TenantId() tenantId: string,
|
|
@CurrentUser('id') userId: string,
|
|
@Body() dto: Create{Nombre}Dto,
|
|
) {
|
|
const entity = await this.service.create(tenantId, userId, dto);
|
|
return { data: entity, message: '{Recurso} creado exitosamente' };
|
|
}
|
|
|
|
@Patch(':id')
|
|
@ApiOperation({ summary: 'Actualizar {recurso}' })
|
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
@ApiResponse({ status: 200, description: '{Recurso} actualizado' })
|
|
@ApiResponse({ status: 404, description: '{Recurso} no encontrado' })
|
|
@RequirePermissions('{modulo}:update')
|
|
async update(
|
|
@TenantId() tenantId: string,
|
|
@CurrentUser('id') userId: string,
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@Body() dto: Update{Nombre}Dto,
|
|
) {
|
|
const entity = await this.service.update(tenantId, userId, id, dto);
|
|
return { data: entity, message: '{Recurso} actualizado exitosamente' };
|
|
}
|
|
|
|
@Delete(':id')
|
|
@ApiOperation({ summary: 'Eliminar {recurso}' })
|
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
@ApiResponse({ status: 200, description: '{Recurso} eliminado' })
|
|
@ApiResponse({ status: 404, description: '{Recurso} no encontrado' })
|
|
@RequirePermissions('{modulo}:delete')
|
|
async remove(
|
|
@TenantId() tenantId: string,
|
|
@CurrentUser('id') userId: string,
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
) {
|
|
await this.service.remove(tenantId, userId, id);
|
|
return { data: null, message: '{Recurso} eliminado exitosamente' };
|
|
}
|
|
|
|
@Post(':id/{accion}')
|
|
@ApiOperation({ summary: '{Accion} {recurso}' })
|
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
@ApiResponse({ status: 200, description: '{Recurso} {accion} exitosamente' })
|
|
@RequirePermissions('{modulo}:{accion}')
|
|
async {accion}(
|
|
@TenantId() tenantId: string,
|
|
@CurrentUser('id') userId: string,
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
) {
|
|
const entity = await this.service.{accion}(tenantId, userId, id);
|
|
return { data: entity, message: '{Recurso} {accion} exitosamente' };
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Validaciones
|
|
|
|
### Tabla de Validaciones
|
|
|
|
| Campo | Regla | Mensaje Error | Codigo |
|
|
|-------|-------|---------------|--------|
|
|
| `name` | required, 3-200 chars | "Nombre debe tener entre 3 y 200 caracteres" | VAL-001 |
|
|
| `internalCode` | optional, unique per tenant | "Ya existe un {recurso} con ese codigo" | RES-002 |
|
|
| `{related}Id` | valid UUID, exists | "{Relacion} no encontrado" | RES-001 |
|
|
| `status` | valid enum value | "Estado invalido" | VAL-002 |
|
|
|
|
---
|
|
|
|
## Tests
|
|
|
|
### Unit Tests
|
|
|
|
**Archivo:** `__tests__/{modulo}.service.spec.ts`
|
|
|
|
```typescript
|
|
describe('{Nombre}Service', () => {
|
|
describe('findAll', () => {
|
|
it('should return paginated results', async () => {});
|
|
it('should filter by status', async () => {});
|
|
it('should search by name', async () => {});
|
|
});
|
|
|
|
describe('findOne', () => {
|
|
it('should return entity by id', async () => {});
|
|
it('should throw NotFoundException if not found', async () => {});
|
|
it('should not return inactive entities', async () => {});
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('should create new entity', async () => {});
|
|
it('should throw ConflictException on duplicate code', async () => {});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('should update entity', async () => {});
|
|
it('should throw NotFoundException if not found', async () => {});
|
|
});
|
|
|
|
describe('remove', () => {
|
|
it('should soft delete entity', async () => {});
|
|
});
|
|
|
|
describe('{accion}', () => {
|
|
it('should change status to active', async () => {});
|
|
it('should throw if status is not draft', async () => {});
|
|
});
|
|
});
|
|
```
|
|
|
|
### Coverage Minimo
|
|
|
|
| Metrica | Minimo |
|
|
|---------|--------|
|
|
| Statements | 80% |
|
|
| Branches | 70% |
|
|
| Functions | 80% |
|
|
| Lines | 80% |
|
|
|
|
---
|
|
|
|
## Referencias
|
|
|
|
| Documento | Path |
|
|
|-----------|------|
|
|
| DDL Spec | `docs/02-modelado/database-design/DDL-SPEC-{schema}.md` |
|
|
| Requerimientos | `docs/01-requerimientos/RF-{modulo}/` |
|
|
| API Standards | `orchestration/directivas/ESTANDARES-API-REST-GENERICO.md` |
|
|
|
|
---
|
|
|
|
## Checklist Pre-Implementacion
|
|
|
|
- [ ] DDL Specification aprobada
|
|
- [ ] Requerimientos funcionales aprobados
|
|
- [ ] DTOs con validaciones completas
|
|
- [ ] Endpoints documentados con Swagger
|
|
- [ ] Tests unitarios definidos
|
|
- [ ] Permisos identificados
|
|
|
|
---
|
|
|
|
## Historial
|
|
|
|
| Version | Fecha | Autor | Cambios |
|
|
|---------|-------|-------|---------|
|
|
| 1.0 | {YYYY-MM-DD} | {nombre} | Creacion inicial |
|
|
|
|
---
|
|
*Template version 1.0 - ERP Suite*
|