# Matriz MCP Playwright - Importacion XML Compras (Medicamentos)

Fecha de ejecucion: 2026-03-10/11
Entorno: `http://demo.enterfarmaplus.test` (tenant DEMO)
Metodo: Playwright MCP (`preview/confirm` via navegador autenticado)

## Resumen

- **Ronda 1** (2026-03-10): 37 casos ejecutados — 28 PASS, 9 FAIL
- **Ronda 2** (2026-03-11): 46 casos adicionales — 33 PASS, 8 FAIL, 3 SKIP, 2 INFO
- **Total acumulado: 83 casos — 61 PASS, 17 FAIL, 3 SKIP, 2 INFO**

---

## Ronda 1 — Resultado por caso funcional (2026-03-10)

| Categoria | Caso | Estado |
|---|---|---|
| Producto/Alias | XML con nombre distinto pero mismo `supplier_item_code` mapea automatico | PASS |
| Producto/Alias | XML sin codigo, descripcion parecida -> sugerir y no auto-confirmar en modo estricto | PASS |
| Producto/Alias | Modo default crea borrador automaticamente para no resuelto | PASS (comportamiento actual) |
| Producto/Alias | Dos productos internos muy parecidos -> resolver manual obligatorio | FAIL |
| Producto/Alias | Producto no existe -> preview pendiente + confirm crea borrador | PASS |
| Producto/Alias | Proveedor cambia nombre comercial pero mismo codigo alias | PASS |
| Producto/Alias | Misma familia, distinta presentacion (x10/x100) no automapea (modo estricto) | PASS |
| Lote/Vencimiento | Producto con lote y XML sin lote bloquea | PASS |
| Lote/Vencimiento | Producto con lote y XML con lote pero sin vencimiento | FAIL |
| Lote/Vencimiento | Producto sin control lote y XML sin lote | PASS |
| Lote/Vencimiento | Producto sin control lote y XML con lote | PASS |
| Lote/Vencimiento | Division manual de linea en varios lotes con suma validada | FAIL |
| Lote/Vencimiento | Lote con fecha invalida bloquea | FAIL |
| Lote/Vencimiento | Lote vencido bloquea | FAIL |
| Impuestos/Descuentos/Gratuitas | Descuento cabecera + descuento linea | PASS |
| Impuestos/Descuentos/Gratuitas | Cargo cabecera + cargo linea | PASS |
| Impuestos/Descuentos/Gratuitas | Linea gratuita (11/9996) con valor referencial | PASS |
| Impuestos/Descuentos/Gratuitas | Multiples `TaxSubtotal` (IGV+ISC+ICBPER) | PASS |
| Impuestos/Descuentos/Gratuitas | Redondeo pequeno dentro de tolerancia | PASS |
| Impuestos/Descuentos/Gratuitas | Redondeo grande fuera de tolerancia bloquea | PASS |
| Duplicidad/Idempotencia | Mismo XML 2 veces -> fingerprint | PASS |
| Duplicidad/Idempotencia | XML editado misma clave fiscal -> documento duplicado | PASS |
| Duplicidad/Idempotencia | Misma serie/numero con proveedor distinto | PASS |
| Duplicidad/Idempotencia | Confirm doble clic no duplica compra | PASS |
| Inventario/Transaccion | `preview` no mueve stock | PASS |
| Inventario/Transaccion | `confirm` con error en una linea hace rollback total | PASS |
| Inventario/Transaccion | `confirm` exitoso incrementa stock por linea/lote | PASS (retest) |
| Inventario/Transaccion | Reintento tras error no deja movimientos huerfanos | PASS |
| Robustez XML | Prefijos/namespaces diferentes | PASS |
| Robustez XML | Nodos unicos vs arreglos | PASS |
| Robustez XML | BOM UTF-8 + tildes/caracteres especiales | PASS |
| Robustez XML | XML mal formado -> error claro controlado | PASS |
| Robustez XML | XML grande (250 lineas) | PASS |

---

## Ronda 2 — Resultado por caso funcional (2026-03-11)

### Grupo 1: Resolucion de productos — Nombres diferentes

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| PA7 | Descripcion con tildes/acentos vs item sin tildes | FAIL | MySQL collation `utf8mb4_unicode_ci` matchea `Recubiérta` = `Recubierta`. Auto-match inesperado. |
| PA8 | Abreviaturas diferentes (`TAB 500MG` vs `Tableta 500 mg`) | PASS | No matchea, queda pendiente. Correcto. |
| PA9 | Solo `CommodityClassification` (sin `SellersItemIdentification`) | PASS | Matchea via Phase 2 (`internal_id`/`item_code`). |
| PA10 | GTIN en `StandardItemIdentification` | PASS | Matchea via Phase 2 (`barcode`/`item_code_gs1`). |
| PA11 | Alias de otro proveedor con mismo codigo | INFO | Matchea por Phase 2 (item_code), no por alias. Alias scoped por proveedor funciona. |
| PA12 | Mismo codigo de proveedor para productos distintos en facturas diferentes | PASS | Alias guardado en 1ra importacion matchea automaticamente en 2da. |
| PA13 | Dos lineas con misma descripcion, codigos distintos | PASS | Ambas quedan pendientes independientemente. |
| PA14 | Item inactivo (`active=false`) matchea por codigo | FAIL | Acepta items inactivos sin warning. Deberia advertir o bloquear. |

### Grupo 2: Lotes y vencimiento — Ingreso manual

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| LV7 | Lote en `AdditionalItemProperty` (`NRO LOTE`) | PASS | Extrae correctamente lote y fecha de `AdditionalItemProperty`. |
| LV8 | Fecha vencimiento formato DD/MM/YYYY (`15/06/2027`) | FAIL | `Carbon::parse()` no parsea `15/06/2027` — queda `null`. Solo acepta ISO `YYYY-MM-DD`. |
| LV9 | Item `lots_enabled=true`, XML con lote sin fecha vencimiento | FAIL | Error 500: `Column 'date' cannot be null` en `item_lots`. Bug conocido. |
| LV10 | Lote con codigo largo (65 caracteres) | PASS | Aceptado sin truncar ni error. |
| LV11 | Dos lineas del mismo item con lotes diferentes | PASS | Ambos lotes se crean correctamente como `ItemLotsGroup` independientes. |
| LV12 | XML con lote pero item tiene `lots_enabled=false` | PASS | Lote se guarda en `PurchaseItemLot` pero no crea `ItemLotsGroup`. Correcto. |
| LV13 | Confirm con resolucion manual de lote (preview bloqueo LOT_REQUIRED) | PASS | Usuario envia `lot_code` + `date_of_due` en resolutions, desbloquea y crea lote. |
| LV14 | Fecha de vencimiento ya pasada (2020-01-01) | FAIL | Acepta fecha vencida sin warning ni bloqueo. Bug conocido. |

### Grupo 3: Proveedor

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| SP1 | RUC existe como cliente, no como proveedor | PASS | `SUPPLIER_NOT_FOUND` blocking. Filtra correctamente por `type=suppliers`. |
| SP2 | RUC en `CustomerAssignedAccountID` (fallback path) | PASS | Extrae RUC del nodo alternativo. |
| SP3 | XML sin `AccountingSupplierParty` | PASS | `REQUIRED_FIELD_MISSING` blocking: "RUC de proveedor". |
| SP4 | Proveedor encontrado pero nombre diferente al del XML | FAIL | No advierte diferencia de razon social. Podria ser suplantacion. |

### Grupo 4: Moneda y tipo de cambio

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| MC1 | XML en USD con `PricingExchangeRate` | FAIL | Payload no expone `currency_type_id` ni `exchange_rate` en response (campos internos). Moneda se guarda pero no es visible en preview. |
| MC2 | XML en USD sin tipo de cambio | FAIL | TC default=1 sin warning. Compra en USD con TC=1 es incorrecta. |
| MC3 | XML con moneda vacia | FAIL | No aplica default PEN. El campo queda vacio internamente. |

### Grupo 5: Cantidades y precios

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| QP1 | Linea con cantidad 0 | PASS | Cantidad 0 se convierte a 1 automaticamente. |
| QP2 | Linea sin `Price/PriceAmount` | FAIL | Precio queda 0 en vez de calcularse de `LineExtensionAmount/quantity`. |
| QP5 | XML con 1 sola linea (minimo) | PASS | Importacion exitosa. |
| QP6 | Mismo item en 2 lineas diferentes | PASS | Ambas lineas crean `PurchaseItem` independientes. Stock se suma. |

### Grupo 6: Opciones de confirmacion

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| OC1 | `auto_create_missing_items=false` con item sin resolver | PASS | `ITEM_NOT_FOUND` blocking. No crea borrador. |
| OC2 | `save_aliases=false` | PASS | Alias no guardado. Segundo import no matchea por alias. |
| OC3 | Resolucion manual override auto-match | PASS | Usuario fuerza `item_id` distinto al auto-match. Funciona. |
| OC4 | Resolucion con `item_id` inexistente (999999) | FAIL | Importa exitosamente con `item_id=999999`. No valida existencia del item en resolutions. |
| OC5 | Resolucion con `line_index` fuera de rango (99) | PASS | Ignora resolucion fuera de rango sin error. |

### Grupo 7: Documento y series

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| DS1 | Boleta con serie B (`document_type_id=03`) | PASS | Tipo inferido correctamente. |
| DS2 | Serie en minusculas (`f001`) | FAIL | Serie no normalizada a mayuscula. Se guarda `f001` tal cual. |
| DS3 | Numero con ceros a la izquierda (`00000123`) | PASS | Aceptado sin error. |
| DS4 | ID con formato inesperado (`F001/70039` con /) | PASS | Bloqueado: `REQUIRED_FIELD_MISSING` para series y number. |
| DS5 | XML sin `InvoiceTypeCode` | PASS | Tipo inferido por prefijo de serie (`F` -> `01`). |

### Grupo 8: Concurrencia y estados

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| CE1 | Preview -> confirm sin expiracion | PASS | No hay TTL en import records. |
| CE2 | Segundo confirm con mismo `import_id` | PASS | Respuesta idempotente: "La importacion ya estaba confirmada." |
| CE3 | XML identico dos veces (fingerprint) | PASS | `DUPLICATE_FINGERPRINT` blocking en segundo preview. |
| CE4 | Preview OK -> proveedor eliminado -> confirm | SKIP | Requiere eliminar proveedor (destructivo). |

### Grupo 9: Items borrador (auto-created)

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| IB1 | Item borrador tiene `active=false` | INFO | Confirm exitoso, pero `/items/search-items` filtra items inactivos. No verificable via API de busqueda. |
| IB2 | Segundo import con alias guardado | PASS | Ya validado en PA12. |
| IB3 | XML sin descripcion ni codigo -> borrador generico | PASS | Borrador creado con descripcion default. |
| IB4 | Borrador auto-created sin lots_enabled + XML trae lote | PASS | Lote se guarda en `PurchaseItemLot`, no crea `ItemLotsGroup`. |

### Grupo 10: Totales y tolerancia

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| TT1 | Total difiere en exactamente 0.10 (limite tolerancia) | PASS | Dentro de tolerancia, no bloquea. |
| TT2 | Total difiere en 0.11 (fuera tolerancia) | PASS | `TOTAL_MISMATCH` blocking. |
| TT3 | XML sin nodo de total declarado | PASS | Usa total calculado, no bloquea. |
| TT4 | Todas las lineas gratuitas (total = 0) | PASS | Compra con total 0 importada correctamente. |

### Grupo 11: Almacen

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| WH1 | Almacen unico se asigna automaticamente | PASS | 1 almacen en demo, asignado automaticamente. |
| WH2 | Multiples almacenes | SKIP | Solo 1 almacen en demo, no testeable. |

### Grupo 12: Seguridad XML

| Caso | Descripcion | Estado | Detalle |
|---|---|---|---|
| SX1 | XXE (External Entity Injection) | PASS | Entity `&xxe;` no resuelta gracias a `LIBXML_NONET`. No filtra archivos. |
| SX2 | Billion laughs (entity expansion DoS) | PASS | `XML_INVALID` blocking. Parser rechaza expansion de entidades. |
| SX3 | Archivo no-XML (PDF renombrado a .xml) | PASS | HTTP 422 rechaza el archivo. |
| SX4 | Tags HTML/script en descripcion | PASS | Tags removidos por DOMDocument. `<script>` queda como texto plano `alert("XSS")`. |

---

## Hallazgos criticos reproducibles

### De Ronda 1

1. **`LV2`** - Lote sin vencimiento termina en error 500 en `confirm`
   Error: `SQLSTATE[23000] ... Column 'date' cannot be null` al insertar en `item_lots`.

2. **`PA3`** - Ambiguedad por descripcion exacta duplicada no se bloquea
   El sistema hace `description_exact` y toma el primer item.

3. **`LV5`** - No existe soporte real para sublotes por linea
   Se envia mas de una resolucion para la misma linea y prevalece la ultima.

4. **`LV6A/LV6B`** - No hay bloqueo por fecha de vencimiento invalida o vencida
   Fecha invalida queda `null`; fecha vencida pasa sin bloqueo.

5. **`BUG_confirm_ignora_blocking_series_too_long`**
   Preview marca `SERIES_TOO_LONG` como blocking, pero `confirm` puede completar.

### De Ronda 2

6. **`LV8`** - Formato de fecha DD/MM/YYYY no parseado
   `Carbon::parse('15/06/2027')` devuelve `null`. Solo acepta ISO `YYYY-MM-DD`.
   **Impacto**: Proveedores que usen formato peruano/europeo pierden fecha de vencimiento silenciosamente.

7. **`LV9`** (confirma LV2) - Lote sin fecha vencimiento = error 500
   Reproducido: `Column 'date' cannot be null`. Afecta items con `lots_enabled=true`.

8. **`LV14`** (confirma LV6B) - Fecha vencida aceptada sin warning
   Lote con `expiryDate=2020-01-01` importado sin incidencia.

9. **`OC4`** - `item_id` inexistente (999999) en resolutions no se valida
   Confirm exitoso con `item_id=999999`. Puede crear compras con FK rota.
   **Severidad**: Alta. Potencial integridad referencial comprometida.

10. **`DS2`** - Serie en minusculas no se normaliza
    `f001` se guarda tal cual. Puede causar duplicados (`f001-123` vs `F001-123`).

11. **`SP4`** - No advierte diferencia de razon social
    Proveedor RUC correcto pero nombre completamente distinto pasa sin warning.

12. **`PA14`** - Items inactivos matchean sin advertencia
    Un item con `active=false` se usa en importacion sin warning al usuario.

13. **`MC2/MC3`** - Moneda USD sin tipo de cambio = TC=1 sin warning
    Compra en dolares con tipo de cambio 1.00 es probablemente incorrecta.

14. **`QP2`** - Linea sin `Price/PriceAmount` = precio 0
    Deberia calcular de `LineExtensionAmount / quantity` como fallback.

15. **`PA7`** - Tildes matchean por collation MySQL
    `Recubiérta` matchea `Recubierta`. Comportamiento depende de collation DB, no del codigo PHP.
    **Impacto bajo**: En farmacias los nombres suelen ser consistentes.

---

## Priorizacion de fixes recomendados

### Critico (arreglar antes de produccion)
| # | Bug | Impacto |
|---|-----|---------|
| 9 | OC4: `item_id` inexistente aceptado | FK rota en `purchase_items` |
| 1/7 | LV2/LV9: Lote sin fecha = error 500 | UX rota, datos inconsistentes |
| 5 | confirm ignora blocking SERIES_TOO_LONG | Bypass de validacion |

### Alto (corregir pronto)
| # | Bug | Impacto |
|---|-----|---------|
| 6 | LV8: Formato DD/MM/YYYY no parseado | Fechas perdidas silenciosamente |
| 8 | LV14: Fecha vencida no bloqueada | Lotes vencidos en inventario |
| 10 | DS2: Serie no normalizada a mayuscula | Duplicados potenciales |
| 14 | QP2: Precio 0 sin PriceAmount | Compras con precio incorrecto |
| 13 | MC2/MC3: USD sin TC = 1.00 | Montos incorrectos |

### Medio (mejora recomendada)
| # | Bug | Impacto |
|---|-----|---------|
| 11 | SP4: Nombre proveedor no se compara | Posible suplantacion |
| 12 | PA14: Items inactivos aceptados | Confusion en inventario |
| 2 | PA3: Ambiguedad por descripcion duplicada | Item incorrecto seleccionado |

### Bajo (nice to have)
| # | Bug | Impacto |
|---|-----|---------|
| 15 | PA7: Tildes matchean por collation | Comportamiento implicito |
| 3 | LV5: Sin sublotes por linea | Limitacion funcional |
