# API de hitos

> Crear, listar, obtener, actualizar y eliminar hitos por proyecto en la API para integradores.

Los hitos son unidades de planificación por proyecto que agrupan ejecuciones de prueba y permiten hacer seguimiento del avance hacia un lanzamiento. Cada hito tiene un ULID asignado por el servidor, un ID legible por proyecto (`M-3`), un ciclo de vida de estado (`upcoming` → `active` → `completed` | `archived`), campos de fecha opcionales y un padre opcional para jerarquías de un nivel.

## Crear

`POST /api/v1/projects/{projectId}/milestones`

`{projectId}` es el **código** del proyecto (por ejemplo `ACME`).

### Cuerpo de la petición

| Campo         | Obligatorio | Notas                                                                                       |
| ------------- | ----------- | ------------------------------------------------------------------------------------------- |
| `name`        | sí          | Cadena no vacía, máximo 255 caracteres. Debe ser único entre los hitos activos del proyecto |
| `description` | no          | Texto libre, máximo 2000 caracteres                                                         |
| `status`      | no          | Uno de `upcoming`, `active`, `completed`, `archived`. Por defecto `upcoming`                |
| `startAt`     | no          | Milisegundos de época Unix. Debe ser ≤ `dueAt` si ambos están presentes                     |
| `dueAt`       | no          | Milisegundos de época Unix                                                                  |
| `forecastAt`  | no          | Milisegundos de época Unix                                                                  |
| `parentId`    | no          | ULID del hito padre en el mismo proyecto (solo 1 nivel)                                     |

Los campos `ulid` enviados por el cliente son rechazados.

Un nombre duplicado activo dentro del mismo proyecto devuelve `409 conflict`. Una violación de orden de fechas (`dueAt < startAt`) o campos requeridos faltantes devuelven `422 validation_failed`.

### Respuesta

`201` con el objeto del hito creado. Ver [Campos de respuesta](#campos-de-respuesta).

### Ejemplo

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Lanzamiento v2.0","status":"upcoming","startAt":1700000000000,"dueAt":1700600000000}' \
  "https://app.probara.net/api/v1/projects/ACME/milestones"
```

## Listar

`GET /api/v1/projects/{projectId}/milestones`

Devuelve los hitos activos (no eliminados) del proyecto, ordenados por ULID de forma ascendente. Paginación basada en páginas.

### Parámetros de consulta

| Parámetro  | Notas                                                                                               |
| ---------- | --------------------------------------------------------------------------------------------------- |
| `page`     | Número de página (base 1, por defecto `1`)                                                          |
| `pageSize` | Elementos por página (por defecto `50`, máximo `200`)                                               |
| `q`        | Búsqueda de subcadena insensible a mayúsculas en el nombre del hito                                 |
| `status`   | Filtrar por estado exacto: `upcoming`, `active`, `completed` o `archived`                           |
| `view`     | Forma de la respuesta: `flat` (por defecto) o `tree`. Ver [Vistas de la lista](#vistas-de-la-lista) |

### Vistas de la lista

El parámetro `view` selecciona la forma de la respuesta. Es aditivo — omitirlo (o pasar `flat`) devuelve exactamente la misma respuesta que antes.

| `view`               | `items`                                                                                                       | `total`                                                           | Unidad de paginación |
| -------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------- |
| `flat` (por defecto) | Todos los hitos vigentes (padres e hijos como hermanos)                                                       | Conteo de todos los hitos vigentes                                | Hito individual      |
| `tree`               | Solo hitos padre (los que no tienen padre), cada uno con un arreglo `children` de sus hijos directos vigentes | Conteo de hitos **padre** únicamente (los hijos nunca se cuentan) | Hito padre           |

En modo `tree`:

- `items` contiene solo padres. Cada elemento padre es el objeto de hito estándar más un arreglo `children`.
- Cada entrada de `children` es un objeto de hito plano (la misma [forma de respuesta](#campos-de-respuesta)); los hijos nunca llevan su propia clave `children` (la jerarquía es de un nivel).
- `q` y `status` filtran **solo padres**. Un padre que coincide con el filtro muestra todos sus hijos vigentes sin importar el estado propio de los hijos.
- `total` es el conteo de padres, por lo que el paginador (`X–Y de Z`) cuenta secciones padre. Un padre y sus hijos nunca se reparten entre páginas.
- Un padre sin hijos tiene `children: []`.

### Respuesta

`200` con `{ items, page, pageSize, total }`. En modo `flat` cada elemento sigue la [forma de respuesta](#campos-de-respuesta); en modo `tree` cada elemento es esa forma extendida con un arreglo `children` (ver [Vistas de la lista](#vistas-de-la-lista)).

### Ejemplo

```bash
# Plana (por defecto)
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/milestones?status=active"

# Secciones agrupadas por padre con hijos anidados
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/milestones?view=tree"
```

Ejemplo de respuesta `tree` (truncada):

```json
{
  "items": [
    {
      "ulid": "01JPARENT...",
      "name": "Lanzamiento 2.4",
      "status": "active",
      "children": [{ "ulid": "01JCHILD...", "name": "Autenticación 2.4", "status": "active" }]
    }
  ],
  "page": 1,
  "pageSize": 50,
  "total": 1
}
```

## Obtener

`GET /api/v1/milestones/{milestoneUlid}`

Devuelve un único hito activo con alcance a la organización activa, incluyendo agregados de progreso en tiempo real.

Devuelve `404 not_found` cuando el hito está fuera de la organización activa o ha sido eliminado.

### Ejemplo

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/milestones/01JXXXXXXXXXXXXXXXXXXXXXXXXX"
```

## Obtener por display ID

`GET /api/v1/projects/{projectId}/milestones/by-display-id/{displayId}`

Devuelve el mismo objeto de hito que `GET /api/v1/milestones/{milestoneUlid}`, identificado por su ID legible. `{projectId}` es el **código** del proyecto y acota la búsqueda; `{displayId}` es `M-{milestoneNumber}` (por ejemplo `M-3`), sin prefijo de proyecto.

- `422 validation_failed` — ID mal formado (`M-0`, `M-01`, `M-x`) o con el prefijo de otro recurso (`R-3`).
- `404 not_found` — no hay un hito con ese número en este proyecto, o el hito fue eliminado.

Los números de hito empiezan en 1, aumentan con cada hito creado en el proyecto y nunca se reutilizan. La aplicación web usa el mismo ID en sus URLs, por ejemplo `/projects/ACME/milestones/M-3`.

### Ejemplo

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/milestones/by-display-id/M-3"
```

## Actualizar

`PATCH /api/v1/milestones/{milestoneUlid}`

Actualización parcial. Se debe proporcionar al menos un campo; un cuerpo vacío devuelve `422 validation_failed`.

### Cuerpo de la petición

| Campo         | Obligatorio | Notas                                                                                                            |
| ------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- |
| `name`        | no          | Cadena no vacía, máximo 255 caracteres                                                                           |
| `description` | no          | Máximo 2000 caracteres, o `null` para borrar                                                                     |
| `status`      | no          | Estado destino. Debe seguir las transiciones válidas (ver [Ciclo de vida del estado](#ciclo-de-vida-del-estado)) |
| `startAt`     | no          | Milisegundos de época Unix, o `null` para borrar                                                                 |
| `dueAt`       | no          | Milisegundos de época Unix. Debe ser ≥ `startAt` si ambos están definidos                                        |
| `forecastAt`  | no          | Milisegundos de época Unix, o `null` para borrar                                                                 |
| `parentId`    | no          | ULID del nuevo padre en el mismo proyecto, o `null` para desvincularlo                                           |

Las transiciones de estado inválidas devuelven `422 validation_failed`. Un nombre duplicado activo devuelve `409 conflict`. Devuelve `404 not_found` cuando el hito está fuera de la organización activa.

### Ejemplo

```bash
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"active","dueAt":1700700000000}' \
  "https://app.probara.net/api/v1/milestones/01JXXXXXXXXXXXXXXXXXXXXXXXXX"
```

## Eliminar

`DELETE /api/v1/milestones/{milestoneUlid}`

Elimina el hito de forma lógica (soft-delete). Devuelve `204` si tiene éxito. Un `GET` posterior devuelve `404 not_found` y el hito queda excluido de los resultados de listado.

La eliminación desvincula atómicamente todas las ejecuciones de prueba, casos de prueba e hitos hijos asociados.

Devuelve `404 not_found` cuando el hito está fuera de la organización activa.

### Ejemplo

```bash
curl -sS -X DELETE \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/milestones/01JXXXXXXXXXXXXXXXXXXXXXXXXX"
```

## Ciclo de vida del estado

Los hitos siguen una máquina de estados dirigida. Solo se aceptan las transiciones indicadas; cualquier otra devuelve `422 validation_failed`.

| Desde       | Transiciones permitidas        |
| ----------- | ------------------------------ |
| `upcoming`  | `active`, `archived`           |
| `active`    | `completed`, `archived`        |
| `completed` | `active` (reabrir), `archived` |
| `archived`  | — (terminal)                   |

La transición a `completed` establece automáticamente `completedAt` con la marca de tiempo actual. Salir del estado `completed` borra `completedAt`.

## Campos de respuesta

Todos los endpoints mutadores (`POST`, `PATCH`) y `GET /api/v1/milestones/{milestoneUlid}` devuelven un objeto de hito.

| Campo                      | Tipo                  | Notas                                                                                                  |
| -------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------ |
| `ulid`                     | string (ULID)         | Identificador único asignado por el servidor                                                           |
| `projectId`                | string                | Código del proyecto al que pertenece el hito                                                           |
| `milestoneNumber`          | number                | Número del hito dentro del proyecto; empieza en 1 y nunca se reutiliza                                 |
| `displayId`                | string                | ID legible `M-{milestoneNumber}` (p. ej. `M-3`), usado en búsquedas y URLs de la web                   |
| `name`                     | string                | Nombre para mostrar                                                                                    |
| `description`              | string \| null        | Descripción opcional                                                                                   |
| `status`                   | string                | Estado actual: `upcoming`, `active`, `completed` o `archived`                                          |
| `startAt`                  | number \| null        | Milisegundos de época Unix                                                                             |
| `dueAt`                    | number \| null        | Milisegundos de época Unix                                                                             |
| `forecastAt`               | number \| null        | Milisegundos de época Unix                                                                             |
| `completedAt`              | number \| null        | Se establece automáticamente al transicionar a `completed`                                             |
| `parentId`                 | string (ULID) \| null | ULID del hito padre, o `null` si es de nivel raíz                                                      |
| `progress.totalRuns`       | number                | Total de ejecuciones de prueba vinculadas al hito                                                      |
| `progress.closedRuns`      | number                | Ejecuciones cerradas (finalizadas)                                                                     |
| `progress.percentComplete` | number                | `closedRuns / totalRuns × 100`, redondeado; `0` sin ejecuciones                                        |
| `progress.passRate`        | number                | `passed / totalResults × 100`, redondeado; `0` sin resultados                                          |
| `progress.passed`          | number                | Resultados de casos de prueba aprobados en todas las ejecuciones vinculadas                            |
| `progress.failed`          | number                | Resultados de casos de prueba fallidos                                                                 |
| `progress.blocked`         | number                | Resultados de casos de prueba bloqueados                                                               |
| `progress.skipped`         | number                | Resultados de casos de prueba omitidos                                                                 |
| `progress.untested`        | number                | Casos de prueba no ejecutados aún                                                                      |
| `author`                   | object \| null        | Usuario que creó el hito, o `null` para hitos creados con token de API y hitos anteriores a este campo |
| `author.kind`              | string                | Siempre `"user"` cuando está presente                                                                  |
| `author.ulid`              | string (ULID)         | Identificador del usuario                                                                              |
| `author.displayName`       | string                | Nombre completo o correo electrónico del autor                                                         |
| `author.avatarKey`         | string \| null        | Clave de almacenamiento de la imagen de avatar del autor                                               |
| `childCount`               | number                | Número de hitos hijos directos                                                                         |
| `testCaseCount`            | number                | Número de casos de prueba vinculados al hito                                                           |
| `createdAt`                | number                | Milisegundos de época Unix                                                                             |
| `updatedAt`                | number                | Milisegundos de época Unix                                                                             |

## Eventos de auditoría

Las mutaciones de hitos emiten los siguientes eventos en el historial de actividad de la organización:

| Acción                     | Disparador                                 |
| -------------------------- | ------------------------------------------ |
| `milestone.created`        | Hito creado                                |
| `milestone.updated`        | Cualquier campo actualizado mediante PATCH |
| `milestone.status_changed` | Campo de estado cambiado mediante PATCH    |
| `milestone.deleted`        | Hito eliminado de forma lógica             |

Ver [Eventos de auditoría](https://docs.probara.net/es/developers/api/audit-events/).
