# Levels / Orders — estado y pendientes

Rama: `update/complete-providers`. Sin commitear al momento de escribir esto.
Módulos: `CategoryProvider` (columna `CategoryProvider_Level`) y `ProvisionRate` (columna `ProvisionRate_Order`).

Origen: `PUT /categoryProvider` con `CategoryProvider_Level: 4` lo guardaba siempre en el último nivel.

---

## Hecho

### 1. Clamp invertido en el PUT (era el bug reportado)
- `UpdateCategoryProviderQueryHandler:38-43` y `UpdateProvisionRateQueryHandler:48-52`
- Era `max($query->level, $nextLevel - 1)` → con MAX=5, `max(4, 5) = 5`: siempre empujaba al último.
- Ahora `max(1, min($query->level, $nextLevel->value() - 1))`, y `null` si no se manda el campo (no toca la columna).
- El tope es `nextLevel - 1` porque la fila ya está contada en el MAX. En el POST el tope es `nextLevel` a secas (fila nueva).

### 2. SQL de `shiftUp` roto
- `MysqlCategoryProviderLevelRepository` / `MysqlProvisionRateOrderRepository`
- Filtraba `Id_CategoryProvider = ?` pasándole el valor de `Id_TypeProvider`, más un `AND \`Id_TypeProvider\`` colgado (columna sola = truthy), y sin cota superior.
- Ahora: `Id_TypeProvider = ? AND level >= new AND level < old`.

### 3. `shiftDown` con rango invertido
- La interfaz declaraba `(id, newLevel, oldLevel)` pero los updaters llaman `(id, oldLevel, newLevel)`. El rango quedaba `level <= old AND level > new` → conjunto vacío. Nunca cerraba el hueco.
- Firmas reordenadas a `(id, oldLevel, newLevel)` (orden real de llamada) y rango corregido a `level > old AND level <= new`.

### 4. Filtro de status en los shifts
- `shiftUp` / `shiftDown` / `makeSpace` de ambos repos ahora filtran `Status IN (validValues())` vía `validStatusCondition()`, igual que hacen `NextCategoryProviderLevel` / `NextProvisionRateOrder`. Antes empujaban también a las borradas.

### 5. Validación del POST /categoryProvider
- `CategoryProviderInsertDTO:14-15`: `'CategoryProvider_Level' => 'required|int|min:1'` y `'CategoryProvider_Status' => 'required|in:' . implode(',', CategoryProviderStatus::validValues())`.
- `CategoryProviderInserter:34`: piso en 1 → `max(1, min($model->level()->value(), $nextLevel->value()))`.

### 6. `GET /categoryProvider` no ordenaba
- `ListCategoryProvider:46-49`: agregado `ORDER BY \`CategoryProvider_Level\` ASC` (`ListProvisionRate` ya lo tenía).
- Rompió `CategoryProviderListControllerTest::testList`, que asumía orden de inserción. Adaptado: inserta niveles 3, 1, 2 y espera 1, 2, 3.
- `TSCategoryProviderMother::random/pushRandom`: nuevo param opcional `?CategoryProviderLevel $level = null` al final (las otras 30 llamadas no se tocaron).

### 7. `LIKE` sobre columna int → `equal`
- `NextCategoryProviderLevel:21` y `NextProvisionRateOrder:20`.

### 8. DELETE cierra el hueco (solo CategoryProvider)
- Nuevo `CategoryProviderLevelRepository::closeGap(idTypeProvider, level)` + impl en `MysqlCategoryProviderLevelRepository:62-73`: `level - 1 WHERE Id_TypeProvider = ? AND level > ? AND Status IN (validValues())`.
- `CategoryProviderDeleter:39`: se llama después de `updatePartial`, ya con la fila en `Status = DELETED`.
- Test nuevo `CategoryProviderDeleteControllerTest::testDeleteClosesGap` (niveles 1, 2, 3 → borra la 2 → la 3 pasa a 2). Verificado que falla si se comenta `closeGap`.

**Tests al cerrar:** `tests/Routes/Owner` → OK (704 tests, 2215 assertions). `tests/Modules` → OK (11, 80).
Ojo: PHPUnit 9 solo toma el primer path, hay que correr un directorio por vez.

---

## Pendiente

### A. Revivir una categoría borrada (PUT `status: 0 → 1/2`)
La fila borrada conserva su nivel viejo en BD. Al revivirla entra con ese nivel obsoleto: no se hace `makeSpace` y el tope `nextLevel - 1` queda uno corto (la borrada no está contada en el MAX). Simétrico: `PUT status: 2 → 0` tampoco cierra el hueco (solo lo hace el DELETE).

### B. Cambio de `Id_TypeProvider` en el PUT
Hoy: el tope se calcula sobre el TypeProvider viejo, y `CategoryProviderUpdater:37-39` corre los shifts contra `$model->idTypeProvider()` (el nuevo). Faltaría `closeGap` en la lista vieja + `makeSpace` en la nueva, y calcular el tope con `nextLevel` (no `-1`) porque la fila es nueva en esa lista.

### C. Concurrencia (decidido: dejar para otro cambio)
Las lecturas que deciden el nivel corren fuera de la transacción: `UpdateCategoryProviderQueryHandler:37-38` y `UpdateProvisionRateQueryHandler:48-50` (indexer + next finder + versionChecker) suceden antes del `transaction->begin()` de los updaters. Dos PUT simultáneos leen el mismo MAX y el mismo `oldLevel` → los shifts se pisan. Igual en el POST: `CategoryProviderInserter:33` y `ProvisionRateInserter:37` leen el MAX dentro de la transacción pero sin lock. Ambos criterios ya aceptan `lockInShare`, nadie lo usa.

### D. `ProvisionRate` — decidido: no tocar (el order no se está usando)
- `ProvisionRateDeleter` no cierra el hueco.
- `MysqlProvisionRateBulkRepository::nextOrderByProvision:99-116` no filtra status (cuenta las borradas), a diferencia de `NextProvisionRateOrder`.
- `ProvisionRateInheritedSyncer:446` cae a `?? 0` cuando la provision no tiene tarifas, mientras el resto del sistema es base 1.
- `ProvisionRateInsertController:51` manda `order: 1` fijo → intencional, confirmado.

### E. Cosmético, decidido: dejar como está
- `CategoryProviderUpdater:46`: un tercer `find(new IndexCategoryProvider(...))` cuyo resultado se descarta.
- `app/Modules/CategoryProviderCategoryRoom/Domain/NextCategoryProviderCategoryRoomLevel.php`: archivo muerto, nadie lo instancia y apunta a la clase inexistente `CategoryProviderCategoryRoomLevel`.

---

## Archivos tocados

```
app/DBRealTestSupport/TransactionSupported/TSCategoryProviderMother.php
app/Http/Controllers/Owner/CategoryProvider/Dto/CategoryProviderInsertDTO.php
app/Modules/CategoryProvider/Application/Delete/CategoryProviderDeleter.php
app/Modules/CategoryProvider/Application/FindNextLevel/CategoryProviderNextLevelFinder.php
app/Modules/CategoryProvider/Application/Insert/CategoryProviderInserter.php
app/Modules/CategoryProvider/Application/Update/UpdateCategoryProviderQueryHandler.php
app/Modules/CategoryProvider/Domain/CategoryProviderLevelRepository.php
app/Modules/CategoryProvider/Domain/ListCategoryProvider.php
app/Modules/CategoryProvider/Domain/NextCategoryProviderLevel.php
app/Modules/CategoryProvider/Infrastructure/MysqlCategoryProviderLevelRepository.php
app/Modules/ProvisionRate/Application/Update/UpdateProvisionRateQueryHandler.php
app/Modules/ProvisionRate/Domain/NextProvisionRateOrder.php
app/Modules/ProvisionRate/Domain/ProvisionRateOrderRepository.php
app/Modules/ProvisionRate/Infrastructure/MysqlProvisionRateOrderRepository.php
tests/Routes/Owner/CategoryProvider/CategoryProviderDeleteControllerTest.php
tests/Routes/Owner/CategoryProvider/CategoryProviderListControllerTest.php
```
