﻿# Manual QA â€” Refactor Presentaciones (Sprints 0-D)

Fecha generaciÃ³n: 2026-04-16
Entorno objetivo: `http://demo.enterfarmaplus.test` (local Laragon)
Credenciales: `admin@gmail.com` / `123456` â€” RUC `00000000000`
Build Vue: `npm run prod` ejecutado, assets en `/public/js/app.js` (11 MiB)

---

## 0. Datos sembrados para probar

**Item demo creado vÃ­a seed**:
- `internal_id`: `QA-AMOX-DEMO-001`
- DescripciÃ³n: `QA Amoxicilina 500mg (multi-presentaciÃ³n demo)`
- Stock base: 500 unidades en AlmacÃ©n Oficina Principal
- Stock thresholds: min=50, max=1000, reorder=100

**3 presentaciones**:
| DescripciÃ³n | Factor | unit_type | P. PÃºblico | P. Mayor | P. Convenio | Barcode |
|---|---|---|---|---|---|---|
| Unidad | 1 | NIU | 1.20 | 1.00 | 0.90 | QA-AMOX-UNIT |
| Blister x10 | 10 | BL | 10.00 | 9.00 | 8.50 | QA-AMOX-BL10 |
| Caja x100 | 100 | BX | 90.00 | 80.00 | 75.00 | QA-AMOX-BOX100 |

---

## Resumen automatizado (ya pasÃ³)

- **PHPUnit**: 7/7 (18 assertions) â€” ItemPresentationServiceTest
- **Jest**: 45/46 (1 fallo pre-existing DocumentInvoice, no relacionado)
- **Playwright e2e**: 17/18 (1 flake pos-tables post-rebuild)
- **PHPStan**: 0 errores en scope modificado
- **`presentation:verify`**: 5/5 invariantes âœ“
- **`presentation:health-check`**: âœ“ (10 items QA legacy sin presentaciÃ³n, 201 kardex histÃ³ricos sin FK â€” esperado)

**Screenshots visuales** (en `test-results/visual-sprint-c/`):
- `01-items-list.png`
- `02-transfers-list.png`
- `03-inventory-list.png`
- `04-promotions-list.png`
- `05-charge-discounts-list.png`
- `06-pos.png`

---

## 1. C.1 â€” Transferencia con selector presentaciÃ³n

**Ruta**: http://demo.enterfarmaplus.test/transfers â†’ botÃ³n Nuevo

**Pasos**:
1. Abrir listado `Transferencias`.
2. Click `Nuevo` â†’ dialog de creaciÃ³n.
3. Buscar/seleccionar item `QA-AMOX-DEMO-001`.
4. Verificar que aparece dropdown **"PresentaciÃ³n"** con opciones:
   - `Unidad base (x1)` (default)
   - `Unidad (x1)`
   - `Blister x10 (x10)`
   - `Caja x100 (x100)`
5. Seleccionar `Caja x100` â†’ verificar que el campo **Factor** muestra `100` (read-only).
6. Cantidad = `2` â†’ completar traslado a otro almacÃ©n.
7. Submit.

**VerificaciÃ³n DB (tinker)**:
```php
App\Models\Tenant\Inventory::latest('id')->first()
// DeberÃ­a tener: quantity = 200 (base = 2*100), presentation_unit_type_id = id de Caja x100
```

**Esperado**:
- âœ… Transfer creado sin error
- âœ… `inventories.presentation_unit_type_id` tiene el ID de "Caja x100"
- âœ… `inventories.quantity = 200` (conversiÃ³n 2 Ã— 100 aplicada)
- âœ… Stock origen reduce 200 base units, stock destino aumenta 200

---

## 2. C.2 â€” Ajuste fÃ­sico con selector presentaciÃ³n

**Ruta**: http://demo.enterfarmaplus.test/inventory â†’ botÃ³n Nuevo movimiento

**Pasos**:
1. Click Nuevo â†’ dialog ajuste.
2. Tipo = `Ingreso` (input).
3. Buscar `QA-AMOX-DEMO-001`.
4. Verificar aparece **dropdown PresentaciÃ³n** solo si item tiene multi-presentaciÃ³n (deberÃ­a aparecer).
5. Seleccionar `Blister x10` â†’ factor se auto-llena en `10`.
6. Cantidad = `5` â†’ comentario libre.
7. Submit.

**VerificaciÃ³n**:
- âœ… Movimiento creado
- âœ… Stock aumentÃ³ en 50 base units (5 Ã— 10)
- âœ… `inventories.presentation_unit_type_id` poblado

---

## 3. C.3 â€” Inputs stock_min/max/reorder por almacÃ©n

**Ruta**: http://demo.enterfarmaplus.test/items â†’ Editar `QA-AMOX-DEMO-001`

**Pasos**:
1. Click Editar en `QA-AMOX-DEMO-001`.
2. Ir a pestaÃ±a **Almacenes**.
3. Verificar secciÃ³n **"ParÃ¡metros de stock por almacÃ©n"** con tabla editable:
   - Columna `AlmacÃ©n` (read-only)
   - Input `Stock mÃ­nimo`
   - Input `Stock mÃ¡ximo`
   - Input `Punto de reorden`
4. Texto de ayuda: "Deja los campos vacÃ­os para usar el mÃ­nimo global del producto."
5. Para `AlmacÃ©n Oficina Principal` los valores precargados son `50 / 1000 / 100` (del seed).
6. Modificar `Stock mÃ­nimo` a `75`, dejar `Stock mÃ¡ximo` vacÃ­o.
7. Guardar.

**VerificaciÃ³n DB**:
```sql
SELECT * FROM item_warehouse WHERE item_id = <id del item>;
-- stock_min=75, stock_max=NULL, reorder_point=100
```

**Esperado**:
- âœ… `stock_min` actualizado a 75
- âœ… `stock_max` queda NULL (no 0) â†’ fallback a `items.stock_min`
- âœ… `reorder_point` preservado

---

## 4. C.4 â€” Scope presentaciÃ³n en Promotion

**Ruta**: http://demo.enterfarmaplus.test/promotions â†’ Nuevo

**Pasos**:
1. Click Nuevo promociÃ³n.
2. Llenar nombre, descripciÃ³n.
3. Seleccionar item = `QA-AMOX-DEMO-001`.
4. Verificar que aparece **nuevo dropdown "PresentaciÃ³n"**:
   - `Todas las presentaciones` (default, valor null)
   - `Unidad (x1)`
   - `Blister x10 (x10)`
   - `Caja x100 (x100)`
5. Seleccionar `Caja x100`.
6. Guardar.

**VerificaciÃ³n DB**:
```sql
SELECT id, name, item_id, item_unit_type_id FROM promotions ORDER BY id DESC LIMIT 1;
-- item_unit_type_id = id de Caja x100 (no null)
```

**Estado actual (2026-05-05)**: pendiente por cierre manual final.

**Evidencia pendiente**:
- Falta guardar una promocion real con `item_unit_type_id` de `Caja x100` y adjuntar el resultado SQL.
- Falta validar en venta que la promocion aplica solo al vender `Caja x100`.
- Falta validar que la misma promocion no aplica al vender `Blister x10` o `Unidad`.
- En la revision local del 2026-05-05, el ultimo registro en `tenancy_demo.promotions` tenia `item_unit_type_id = NULL`, por lo que no sirve como cierre de este caso.

**Esperado**:
- âœ… PromociÃ³n scoped a presentaciÃ³n Caja x100
- âœ… Si no se selecciona â†’ `item_unit_type_id = NULL` â†’ aplica a todas

**Charge Discounts** (`/charge_discounts`): diferido. No se incluye como pendiente operativo de farmacia en este sprint para no mezclarlo con promociones por presentacion.

---

## 5. C.5 â€” Audit log de presentaciones

**Ruta**: http://demo.enterfarmaplus.test/items â†’ Editar `QA-AMOX-DEMO-001`

**Pasos**:
1. Editar item demo.
2. Ir a pestaÃ±a **"Historial presentaciones"** (solo visible en modo ediciÃ³n, no al crear).
3. Tabla debe estar vacÃ­a inicialmente.
4. Guardar cambio de precio en una presentaciÃ³n (ej: cambiar `price1` de Caja x100 de 90 a 95).
5. Recargar o re-abrir la ediciÃ³n.
6. Tab historial ahora debe mostrar 1 fila:
   - Fecha = now
   - PresentaciÃ³n ID = id de Caja x100
   - Campo = `price1`
   - Valor anterior = 90.00
   - Valor nuevo = 95.00
   - Usuario = admin
7. Cambiar factor `quantity_unit` de Blister de 10 a 12, guardar.
8. Volver al tab â†’ 2 filas.

**VerificaciÃ³n DB**:
```sql
SELECT * FROM item_unit_type_changes ORDER BY id DESC LIMIT 5;
```

**Esperado**:
- âœ… Tab aparece solo en edit (not create)
- âœ… Cambios en `quantity_unit`, `price1`, `price2`, `price3` se registran
- âœ… Otros cambios (`description`, `barcode`) NO se registran (intencional â€” audit sÃ³lo campos numÃ©ricos crÃ­ticos)
- âœ… Columna Usuario muestra nombre, no sÃ³lo id

**Endpoint directo** (curl):
```bash
curl -b cookies "http://demo.enterfarmaplus.test/items/<id>/presentation-audit"
# â†’ { "data": [{ "id", "item_unit_type_id", "field", "old_value", "new_value", "user_id", "user_name", "reason", "created_at" }, ...] }
```

---

## 6. Observer + UNIQUE barcode (backend invariantes)

**Pasos via UI**:
1. Editar item demo, intentar poner `quantity_unit = 0` en una presentaciÃ³n â†’ deberÃ­a rechazar con error.
2. Crear item nuevo con barcode `QA-AMOX-UNIT` (duplicado) â†’ deberÃ­a rechazar (SQLSTATE 23000).
3. Soft delete de presentaciÃ³n "Blister x10" â†’ deberÃ­a ocultarse del POS pero mantenerse en documentos histÃ³ricos.

**VÃ­a CLI rÃ¡pido**:
```bash
php artisan presentation:verify
```
Debe devolver:
```
âœ“ validator rejected quantity_unit=0
âœ“ audit trail recorded price change
âœ“ UNIQUE barcode rejected duplicate (23000)
âœ“ soft delete column present
âœ“ ItemPresentationService parses nested JSON correctly
```

---

## 7. Flujo completo compra â†’ venta â†’ NC

**Estado**: validado con Playwright en navegador local (`demo.enterfarmaplus.test`) el 2026-05-05.

**Evidencia registrada**:
- Stock almacén 1: `269.0000 -> 749.0000` (`+500 -30 +10`).
- Compra `id=117`, `QA32-1637820`, item `182`, `Caja x100`, cantidad `5`.
- Venta `document_id=183`, `B001-976247`, item `182`, `Blister x10`, cantidad `3`.
- Nota de crédito `document_id=184`, `BC01-1`, item `182`, `Blister x10`, cantidad `1`.
- Kardex nuevo:
  - `1097`: `+500.0000`, `presentation_unit_type_id=108` (`Caja x100`).
  - `1098`: `-30.0000`, `presentation_unit_type_id=107` (`Blister x10`).
  - `1099`: `+10.0000`, `presentation_unit_type_id=107` (`Blister x10`).
- Resultado JSON: `storage/playwright/presentation-flow/07-flow-rerun-result.json`.

**Setup**: tener el item demo con stock > 0.

### 7.1 Compra con presentaciÃ³n

1. Ir a `/purchases` â†’ Nueva compra.
2. Proveedor (cualquiera registrado).
3. Item: `QA-AMOX-DEMO-001`, cantidad 5, elegir presentaciÃ³n `Caja x100`.
4. Guardar.

**Esperado**: stock aumenta 500 (5 Ã— 100).
**Verificar DB**: `inventory_kardex.presentation_unit_type_id` poblado con ID Caja.

### 7.2 Venta con presentaciÃ³n

1. Ir a `/pos`.
2. Seleccionar cliente (o Clientes varios).
3. Buscar item demo, elegir presentaciÃ³n `Blister x10`, cantidad 3.
4. Procesar venta.

**Esperado**:
- Stock reduce 30 base units
- Ticket muestra "Blister x10" + precio P. PÃºblico (1.00 por blister, total S/ 30)
- XML UBL usa `unit_type_id = BL`
- `inventory_kardex.presentation_unit_type_id` poblado con ID Blister

### 7.3 NC parcial

1. Ir a factura reciÃ©n creada â†’ emitir NC.
2. Devolver 1 blister (parcial).
3. Verificar XML UBL mantiene `unit_type_id = BL`.
4. Stock vuelve a aumentar 10 base units.

**VerificaciÃ³n DB**:
```sql
SELECT document_id, JSON_EXTRACT(item, '$.presentation.id') FROM document_items
WHERE document_id = <id de la NC>;
```
Debe tener el ID de Blister, igual al documento afectado.

---

## 8. Reporte consolidado ventas (D.1)

**Ruta**: Reportes â†’ Ventas consolidadas.

**Pasos**:
1. Ejecutar el reporte.
2. Ver que ahora la columna presentaciÃ³n estÃ¡ poblada con unit_type del UBL.
3. Revisar logs de BD (debug DB): ahora una sola query `SELECT * FROM items WHERE id IN (...)` en vez de N+1.

**Esperado**:
- âœ… Reporte funciona igual que antes
- âœ… Tiempo de ejecuciÃ³n notablemente menor si hay muchas filas (N+1 â†’ batch)

---

## 9. Deploy remoto (cuando estÃ© listo)

```bash
# En servidor demo.sysfarma.pe:
git pull
composer install --no-dev --optimize-autoloader
php artisan tenancy:migrate                   # 8 migraciones
php artisan presentation:backfill-fk --dry-run  # revisar counts
php artisan presentation:backfill-fk
php artisan presentation:health-check         # diagnÃ³stico final
php artisan presentation:verify               # smoke 5 invariantes
php artisan config:clear && php artisan view:clear
npm run prod                                  # rebuild assets

# Restart
sudo systemctl restart php8.3-fpm
# (o equivalente segÃºn stack)
```

---

## 10. Rollback plan (si algo sale mal)

**Rollback por migration (reversible)**:
```bash
php artisan tenancy:migrate:rollback --step=8   # deshace las 8 nuevas
```

**Rollback de cÃ³digo**:
```bash
git log --oneline -20
git revert <commit-hash-del-deploy>
git push
```

**Backfill NO se revierte** â€” datos FK se quedan poblados (inofensivo, son denormalizaciÃ³n de JSON).

---

## Checklist de aprobaciÃ³n

Marca con âœ“ cada secciÃ³n tras validarla en la UI:

- [ ] 1. Transfer con presentaciÃ³n â€” backend + UI
- [ ] 2. Ajuste con presentaciÃ³n â€” backend + UI
- [ ] 3. Stock min/max/reorder por almacÃ©n â€” UI + DB round-trip
- [ ] 4. Promotion scope por presentaciÃ³n â€” UI + DB
- [ ] 5. Audit log viewer â€” tab + endpoint
- [ ] 6. Observer + UNIQUE + soft delete â€” via `presentation:verify`
- [ ] 7. Flujo completo compra â†’ venta â†’ NC
- [ ] 8. Reporte consolidado (N+1 fix)
- [ ] 9. Deploy staging
- [ ] 10. Deploy prod

---

## Pendientes conocidos (NO son bugs, son decisiones)

- `item_units_per_package` marcada `@deprecated` pero no eliminada (destructivo, requiere OK).
- ChargeDiscount form sin item selector (pre-wired, no expuesto).
- Labels P1/P2/P3 hardcoded ("PÃºblico/Mayor/Convenio").
- DIGEMID export ventas no implementado (requiere spec legal).
- Feature tests PHPUnit multi-tenant pendientes (observer + backfill cubiertos vÃ­a tinker + `presentation:verify`).
