# ESTANDARES: API REST Generico ERP

**Proyecto:** ERP Core - Base Generica Reutilizable
**Version:** 1.0.0
**Fecha:** 2025-12-05
**Aplicable a:** ERP Core y todas las verticales
**Estado:** OBLIGATORIO

---

## PRINCIPIOS FUNDAMENTALES

> **APIs consistentes, predecibles y autodocumentadas**

Todas las APIs del ERP siguen patrones RESTful estandar para facilitar integracion y mantenimiento.

---

## ESTRUCTURA DE URLs

### Patron Base

```
/api/{version}/{recurso}
/api/{version}/{recurso}/{id}
/api/{version}/{recurso}/{id}/{sub-recurso}
```

### Ejemplos

```
# Core
GET    /api/v1/partners              # Lista partners
POST   /api/v1/partners              # Crear partner
GET    /api/v1/partners/:id          # Obtener partner
PATCH  /api/v1/partners/:id          # Actualizar parcial
PUT    /api/v1/partners/:id          # Actualizar completo
DELETE /api/v1/partners/:id          # Eliminar (soft delete)

# Sub-recursos
GET    /api/v1/partners/:id/contacts         # Contactos de partner
POST   /api/v1/partners/:id/contacts         # Crear contacto
GET    /api/v1/sale-orders/:id/lines         # Lineas de orden
POST   /api/v1/sale-orders/:id/lines         # Agregar linea

# Acciones
POST   /api/v1/sale-orders/:id/confirm       # Confirmar orden
POST   /api/v1/sale-orders/:id/cancel        # Cancelar orden
POST   /api/v1/invoices/:id/validate         # Validar factura

# Verticales (namespace propio)
GET    /api/v1/construccion/proyectos-obra
POST   /api/v1/construccion/avances
GET    /api/v1/vidrio/ordenes-produccion
```

---

## CONVENCIONES DE NOMBRES

### URLs
- **Sustantivos en plural:** `/partners`, `/products`, `/sale-orders`
- **Kebab-case:** `/sale-orders`, `/product-categories`
- **Sin verbos en URL:** `/partners` no `/getPartners`
- **Jerarquia logica:** `/partners/:id/contacts`

### Query Parameters
- **snake_case:** `?page=1&per_page=20&sort_by=name`
- **Filtros con prefijo:** `?filter[status]=active&filter[type]=customer`
- **Busqueda:** `?search=texto`
- **Ordenamiento:** `?sort_by=created_at&sort_order=desc`

### Body (Request/Response)
- **camelCase:** `{ "partnerId": "uuid", "taxId": "RFC123" }`

---

## PAGINACION

### Request

```
GET /api/v1/partners?page=2&per_page=20&sort_by=name&sort_order=asc
```

### Response

```json
{
  "data": [
    { "id": "uuid-1", "name": "Partner 1", ... },
    { "id": "uuid-2", "name": "Partner 2", ... }
  ],
  "meta": {
    "currentPage": 2,
    "perPage": 20,
    "totalPages": 15,
    "totalCount": 287,
    "hasNextPage": true,
    "hasPrevPage": true
  },
  "links": {
    "self": "/api/v1/partners?page=2&per_page=20",
    "first": "/api/v1/partners?page=1&per_page=20",
    "prev": "/api/v1/partners?page=1&per_page=20",
    "next": "/api/v1/partners?page=3&per_page=20",
    "last": "/api/v1/partners?page=15&per_page=20"
  }
}
```

### Limites

| Parametro | Default | Maximo |
|-----------|---------|--------|
| per_page | 20 | 100 |
| offset | 0 | - |

---

## FILTRADO

### Sintaxis

```
GET /api/v1/partners?filter[status]=active&filter[type]=customer
GET /api/v1/partners?filter[created_at][gte]=2025-01-01
GET /api/v1/partners?filter[name][like]=Constructora
```

### Operadores

| Operador | Descripcion | Ejemplo |
|----------|-------------|---------|
| (default) | Igual | `filter[status]=active` |
| `[ne]` | No igual | `filter[status][ne]=cancelled` |
| `[gt]` | Mayor que | `filter[amount][gt]=1000` |
| `[gte]` | Mayor o igual | `filter[date][gte]=2025-01-01` |
| `[lt]` | Menor que | `filter[amount][lt]=5000` |
| `[lte]` | Menor o igual | `filter[date][lte]=2025-12-31` |
| `[like]` | Contiene | `filter[name][like]=Corp` |
| `[in]` | En lista | `filter[status][in]=draft,confirmed` |
| `[null]` | Es nulo | `filter[deleted_at][null]=true` |

### Ejemplo de Implementacion

```typescript
// Query builder con filtros dinamicos
@Injectable()
export class FilterService {
  applyFilters<T>(
    qb: SelectQueryBuilder<T>,
    filters: Record<string, any>,
    allowedFields: string[]
  ): SelectQueryBuilder<T> {
    for (const [key, value] of Object.entries(filters)) {
      if (!allowedFields.includes(key.split('[')[0])) continue;

      if (typeof value === 'object') {
        // Operador especificado
        const [field, operator] = key.split('[');
        const op = operator?.replace(']', '');
        this.applyOperator(qb, field, op, value);
      } else {
        // Igualdad simple
        qb.andWhere(`entity.${key} = :${key}`, { [key]: value });
      }
    }
    return qb;
  }

  private applyOperator(qb: SelectQueryBuilder<any>, field: string, op: string, value: any) {
    switch (op) {
      case 'ne':
        qb.andWhere(`entity.${field} != :${field}`, { [field]: value });
        break;
      case 'gt':
        qb.andWhere(`entity.${field} > :${field}`, { [field]: value });
        break;
      case 'gte':
        qb.andWhere(`entity.${field} >= :${field}`, { [field]: value });
        break;
      case 'lt':
        qb.andWhere(`entity.${field} < :${field}`, { [field]: value });
        break;
      case 'lte':
        qb.andWhere(`entity.${field} <= :${field}`, { [field]: value });
        break;
      case 'like':
        qb.andWhere(`entity.${field} ILIKE :${field}`, { [field]: `%${value}%` });
        break;
      case 'in':
        const values = value.split(',');
        qb.andWhere(`entity.${field} IN (:...${field})`, { [field]: values });
        break;
      case 'null':
        if (value === 'true') {
          qb.andWhere(`entity.${field} IS NULL`);
        } else {
          qb.andWhere(`entity.${field} IS NOT NULL`);
        }
        break;
    }
  }
}
```

---

## ORDENAMIENTO

### Request

```
GET /api/v1/partners?sort_by=name&sort_order=asc
GET /api/v1/partners?sort=-created_at,name   # Alternativa: - para desc
```

### Campos Ordenables por Defecto

- `id`
- `name`
- `created_at`
- `updated_at`

Cada endpoint debe documentar campos ordenables adicionales.

---

## RESPUESTAS

### Estructura Exitosa

```json
// GET /api/v1/partners/:id (Recurso individual)
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Constructora ABC",
    "taxId": "CAB123456AB1",
    "isCompany": true,
    "email": "contacto@constructoraabc.com",
    "createdAt": "2025-01-15T10:30:00Z",
    "updatedAt": "2025-06-20T14:45:00Z"
  }
}

// POST /api/v1/partners (Creacion)
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440001",
    "name": "Nuevo Partner",
    ...
  },
  "message": "Partner creado exitosamente"
}

// DELETE /api/v1/partners/:id
{
  "data": null,
  "message": "Partner eliminado exitosamente"
}
```

### Estructura de Error

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Los datos proporcionados no son validos",
    "details": [
      {
        "field": "email",
        "message": "Formato de email invalido",
        "code": "INVALID_FORMAT"
      },
      {
        "field": "taxId",
        "message": "RFC ya registrado",
        "code": "DUPLICATE_VALUE"
      }
    ],
    "timestamp": "2025-12-05T10:30:00Z",
    "path": "/api/v1/partners",
    "requestId": "req-123-456-789"
  }
}
```

---

## CODIGOS HTTP

### Exito (2xx)

| Codigo | Uso |
|--------|-----|
| 200 OK | GET exitoso, PATCH/PUT exitoso |
| 201 Created | POST exitoso (recurso creado) |
| 204 No Content | DELETE exitoso |

### Error Cliente (4xx)

| Codigo | Uso |
|--------|-----|
| 400 Bad Request | Validacion fallida, formato invalido |
| 401 Unauthorized | No autenticado |
| 403 Forbidden | Sin permisos |
| 404 Not Found | Recurso no existe |
| 409 Conflict | Conflicto (duplicado, estado invalido) |
| 422 Unprocessable Entity | Logica de negocio fallida |
| 429 Too Many Requests | Rate limit excedido |

### Error Servidor (5xx)

| Codigo | Uso |
|--------|-----|
| 500 Internal Server Error | Error no manejado |
| 502 Bad Gateway | Servicio externo fallo |
| 503 Service Unavailable | Servicio temporalmente no disponible |

---

## CODIGOS DE ERROR

### Catalogo de Errores

```typescript
export enum ErrorCode {
  // Validacion (VAL-*)
  VALIDATION_ERROR = 'VAL-001',
  INVALID_FORMAT = 'VAL-002',
  REQUIRED_FIELD = 'VAL-003',
  INVALID_LENGTH = 'VAL-004',

  // Autenticacion (AUTH-*)
  UNAUTHORIZED = 'AUTH-001',
  TOKEN_EXPIRED = 'AUTH-002',
  INVALID_TOKEN = 'AUTH-003',
  SESSION_EXPIRED = 'AUTH-004',

  // Autorizacion (AUTHZ-*)
  FORBIDDEN = 'AUTHZ-001',
  INSUFFICIENT_PERMISSIONS = 'AUTHZ-002',
  TENANT_MISMATCH = 'AUTHZ-003',

  // Recursos (RES-*)
  NOT_FOUND = 'RES-001',
  ALREADY_EXISTS = 'RES-002',
  CONFLICT = 'RES-003',
  DELETED = 'RES-004',

  // Negocio (BIZ-*)
  BUSINESS_RULE_VIOLATION = 'BIZ-001',
  INVALID_STATE_TRANSITION = 'BIZ-002',
  BUDGET_EXCEEDED = 'BIZ-003',
  STOCK_INSUFFICIENT = 'BIZ-004',

  // Sistema (SYS-*)
  INTERNAL_ERROR = 'SYS-001',
  DATABASE_ERROR = 'SYS-002',
  EXTERNAL_SERVICE_ERROR = 'SYS-003',
}
```

---

## AUTENTICACION

### Headers Requeridos

```http
Authorization: Bearer <jwt_token>
X-Tenant-Id: <tenant_uuid>
```

### Respuesta 401

```json
{
  "error": {
    "code": "AUTH-001",
    "message": "Token no proporcionado o invalido",
    "timestamp": "2025-12-05T10:30:00Z"
  }
}
```

---

## MULTI-TENANCY

### Header Obligatorio

```http
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
```

### Validacion en Middleware

```typescript
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const tenantId = req.headers['x-tenant-id'] as string;

    if (!tenantId) {
      throw new BadRequestException({
        code: 'AUTHZ-003',
        message: 'Header X-Tenant-Id requerido',
      });
    }

    // Validar que tenant existe y usuario tiene acceso
    // ...

    req['tenantId'] = tenantId;
    next();
  }
}
```

### Filtrado Automatico

```typescript
// Base repository con filtro de tenant
@Injectable()
export class TenantAwareRepository<T extends BaseEntity> {
  constructor(
    private repository: Repository<T>,
    private tenantId: string,
  ) {}

  async find(options?: FindManyOptions<T>): Promise<T[]> {
    return this.repository.find({
      ...options,
      where: {
        ...options?.where,
        tenantId: this.tenantId,
      } as FindOptionsWhere<T>,
    });
  }

  async findOne(id: string): Promise<T | null> {
    return this.repository.findOne({
      where: {
        id,
        tenantId: this.tenantId,
      } as FindOptionsWhere<T>,
    });
  }
}
```

---

## ACCIONES (RPC-style)

Para operaciones que no son CRUD, usar POST con accion en URL:

```
POST /api/v1/sale-orders/:id/confirm
POST /api/v1/sale-orders/:id/cancel
POST /api/v1/invoices/:id/validate
POST /api/v1/invoices/:id/send-email
POST /api/v1/stock-moves/:id/process
```

### Request/Response

```json
// POST /api/v1/sale-orders/:id/confirm
// Request body (opcional)
{
  "skipValidation": false,
  "notifyCustomer": true
}

// Response
{
  "data": {
    "id": "uuid",
    "state": "confirmed",
    "confirmedAt": "2025-12-05T10:30:00Z",
    "confirmedBy": "user-uuid"
  },
  "message": "Orden confirmada exitosamente"
}
```

---

## BULK OPERATIONS

### Crear Multiples

```
POST /api/v1/partners/bulk
```

```json
{
  "items": [
    { "name": "Partner 1", "email": "p1@test.com" },
    { "name": "Partner 2", "email": "p2@test.com" }
  ]
}
```

Response:
```json
{
  "data": {
    "created": 2,
    "failed": 0,
    "items": [
      { "id": "uuid-1", "name": "Partner 1", "status": "created" },
      { "id": "uuid-2", "name": "Partner 2", "status": "created" }
    ]
  }
}
```

### Actualizar Multiples

```
PATCH /api/v1/partners/bulk
```

```json
{
  "ids": ["uuid-1", "uuid-2", "uuid-3"],
  "update": {
    "isActive": false
  }
}
```

### Eliminar Multiples

```
DELETE /api/v1/partners/bulk?ids=uuid-1,uuid-2,uuid-3
```

---

## DOCUMENTACION SWAGGER

### Decoradores Obligatorios

```typescript
@Controller('partners')
@ApiTags('Partners')
@ApiBearerAuth()
export class PartnersController {

  @Get()
  @ApiOperation({ summary: 'Lista paginada de partners' })
  @ApiQuery({ name: 'page', required: false, type: Number })
  @ApiQuery({ name: 'per_page', required: false, type: Number })
  @ApiQuery({ name: 'filter[status]', required: false, type: String })
  @ApiResponse({ status: 200, description: 'Lista de partners', type: PartnerListResponseDto })
  @ApiResponse({ status: 401, description: 'No autenticado' })
  async findAll(@Query() query: PartnerQueryDto): Promise<PaginatedResponse<PartnerDto>> {
    // ...
  }

  @Post()
  @ApiOperation({ summary: 'Crear nuevo partner' })
  @ApiBody({ type: CreatePartnerDto })
  @ApiResponse({ status: 201, description: 'Partner creado', type: PartnerResponseDto })
  @ApiResponse({ status: 400, description: 'Datos invalidos' })
  @ApiResponse({ status: 409, description: 'Partner ya existe' })
  async create(@Body() dto: CreatePartnerDto): Promise<PartnerDto> {
    // ...
  }
}
```

### DTOs Documentados

```typescript
export class CreatePartnerDto {
  @ApiProperty({
    description: 'Nombre o razon social del partner',
    example: 'Constructora ABC S.A. de C.V.',
    minLength: 3,
    maxLength: 200,
  })
  @IsString()
  @Length(3, 200)
  name: string;

  @ApiProperty({
    description: 'RFC del partner (Mexico)',
    example: 'CAB123456AB1',
    required: false,
  })
  @IsOptional()
  @IsString()
  @Matches(/^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/, {
    message: 'RFC con formato invalido',
  })
  taxId?: string;

  @ApiProperty({
    description: 'Indica si es persona moral',
    example: true,
    default: false,
  })
  @IsBoolean()
  @IsOptional()
  isCompany?: boolean = false;
}
```

---

## VERSIONAMIENTO

### Estrategia: URL Path

```
/api/v1/partners    # Version actual
/api/v2/partners    # Nueva version (cambios breaking)
```

### Cuando Crear Nueva Version

- Cambios en estructura de respuesta
- Eliminar campos existentes
- Cambiar tipos de datos
- Cambiar comportamiento de endpoints

### Header de Deprecacion

```http
Deprecation: true
Sunset: Sat, 01 Jun 2026 00:00:00 GMT
Link: </api/v2/partners>; rel="successor-version"
```

---

## RATE LIMITING

### Headers de Respuesta

```http
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1701792000
```

### Respuesta 429

```json
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Demasiadas solicitudes. Intente nuevamente en 60 segundos.",
    "retryAfter": 60
  }
}
```

---

## REFERENCIAS

### Directivas Relacionadas
- `DIRECTIVA-PATRONES-ODOO.md` - Patrones de datos
- `DIRECTIVA-MULTI-TENANT.md` - Aislamiento de datos
- `core/orchestration/directivas/DIRECTIVA-CALIDAD-CODIGO.md`

### Estandares Externos
- [JSON:API Specification](https://jsonapi.org/)
- [REST API Guidelines (Microsoft)](https://github.com/microsoft/api-guidelines)
- [OpenAPI 3.0](https://spec.openapis.org/oas/v3.0.3)

---

**Version:** 1.0.0
**Ultima actualizacion:** 2025-12-05
**Estado:** ACTIVA Y OBLIGATORIA
