Files
balam/bind-api-sandbox/VALIDACION-API.md
T
JohannVelazquez 9ccfbe262e Validación técnica de la API de BIND contra producción (GET-only)
Plan A del saldo CONFIRMADO (Total − Payments − CreditNotes, misma fila);
flujo MVP sostenible 100% en lectura; PDF del CFDI por API. Hallazgo duro:
sin recurso de pagos individuales ni REP (escalar a Pedro). Cliente extendido
(Quotes, Currencies, Locations, pdf/xml, GET por ID estilo REST) y tipos
reales en types.real.ts. Detalle en bind-api-sandbox/VALIDACION-API.md;
reporte crudo y token gitignoreados.
2026-07-06 17:17:27 -06:00

358 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Validación técnica de la API de BIND ERP — cuenta real (Balam)
> **Actividad:** "Validación técnica de la API de BIND" · Etapa 0 (Discovery)
> **Fecha de ejecución:** 6-jul-2026 · **Base URL:** `https://api.bind.com.mx`
> **Token:** entregado por Pedro el 6-jul (correo, [REGISTRO #29]) — generado con el usuario de **Arturo Rosas**, conforme al acuerdo del kickoff ([REGISTRO #22]). Vive en `bind-api-sandbox/.env` (`BIND_API_TOKEN`), fuera de git.
> **Método:** script [`src/validate-real-api.ts`](src/validate-real-api.ts) — **exclusivamente GET** (no existe código de escritura en el script), **117 de 120 peticiones** presupuestadas (límite real: 20K/día). Reporte crudo sanitizado en `validation-output/report.json` (gitignoreado).
> **Política de datos:** este documento contiene **solo estructura** — nombres de campos, tipos, formatos, conteos y códigos de estatus. Ningún valor real de Balam (nombres, RFCs, montos, folios, correos). Los ejemplos son inventados con el mismo shape.
---
## Resumen ejecutivo
| Pregunta | Veredicto |
|---|---|
| ¿El token autentica? | ✅ Sí — `Authorization: Bearer` como único header. Sin subscription key. |
| ¿Alcance del token? | ✅ Cubre todos los recursos existentes que probamos (ningún 401/403 por recurso) — coherente con "cuenta mayor, todos los permisos" del kickoff. |
| ¿Se distingue prefactura de factura timbrada? | ✅ Sí — `UUID eq null` / `IsFiscalInvoice eq false` devuelven filas. |
| ¿PPD/PUE visible? | ✅ Sí, en el **detalle** por factura (`CFDIPaymentTerm`) — no en la lista. |
| **¿Saldo abierto por factura?** | ✅ **Plan A operativo:** `Total Payments CreditNotes`, todos campos de la **misma fila** de `/api/Invoices`. Verificado aritméticamente. |
| ¿Pagos individuales listables? | ❌ **No** — no existe recurso de pagos consultable (19 nombres probados → 404). El acumulado sí (`Invoices.Payments`). |
| ¿Cotizaciones consultables? | ✅ Sí (`/api/Quotes` + detalle) — pero **sin relación visible** cotización→factura. |
| ¿OData? | ⚠️ Parcial — `$filter/$top/$skip/$orderby` sí (sintaxis v3); `$select` y conteo total **no**. |
| ¿Multi-empresa? | ✅ Acotado a **una** empresa (la del usuario del token). Sin recurso `Companies` ni campo de empresa. |
| ¿PDF del CFDI por API? | ✅ `GET /api/Invoices/{id}/pdf``application/pdf`. |
---
## 1 · Autenticación
Esquema confirmado: **un solo header**.
```
GET https://api.bind.com.mx/api/{Recurso}
Authorization: Bearer <token>
Accept: application/json
```
- El `Ocp-Apim-Subscription-Key` que el sandbox contemplaba como posible **no es necesario** — no se envió en ninguna petición y todo funcionó.
| Escenario | Estatus | Cuerpo (estructura) |
|---|---|---|
| Token válido | `200` | Colección OData `{ value: [...] }` |
| Sin header Authorization | `401` | `{ "Message": "Authorization has been denied for this request." }` |
| Token corrupto/inválido | ⚠️ **`500`** | `{ "message": "API Key es inválida. \| Your API Key is invalid.", "code": "0" }` |
> **Hallazgo importante:** un token inválido responde **500, no 401/403**. El monitoreo de la plataforma **no puede fiarse del código HTTP** para distinguir "token revocado" de "error del servidor de BIND" — hay que inspeccionar el mensaje del body (`API Key es inválida`).
---
## 2 · Cobertura de endpoints (inventario)
Todos con `GET {recurso}?$top=1`. **No apareció ningún 401/403 por recurso**: con este token todo existe (200) o no existe (404) — no hay recursos "prohibidos" visibles.
### Responden 200
| Recurso | Método probado | Estatus | Notas |
|---|---|---|---|
| `Invoices` | GET lista / GET `/{id}` / GET `/{id}/pdf` / GET `/{id}/xml` | 200 | Colección OData. Detalle trae más campos que la lista (50 vs 32). |
| `Clients` | GET lista / GET `/{id}` | 200 | **Así se llaman los clientes** (no `Customers`). Detalle 28 campos vs 10 de lista. |
| `Quotes` | GET lista / GET `/{id}` | 200 | Cotizaciones. Detalle 45 campos con partidas `Items[]`. |
| `Products` | GET lista | 200 | 30 campos. Incluye `ChargeVAT`, `CurrencyCode`, unidad. |
| `Currencies` | GET lista | 200 | Catálogo: `ID`, `Name`, `Code` (3 letras), `ExchangeRate`. |
| `Warehouses` | GET lista | 200 | `ID`, `Name`, `LocationID`, `AvailableInOtherLoc`. 1 fila (Matriz). |
| `Locations` | GET lista | 200 | Sucursales/domicilios: `Name`, `Street`, `ZipCode`, `City`, `State`… 1 fila. |
| `Activities` | GET lista | 200 | Devuelve colección **vacía** en esta cuenta (0 filas). |
| `PriceLists` | GET lista | 200 | Listas de precios. |
| `Orders` | GET lista | 200 | Pedidos (no se profundizó — fuera del flujo MVP). |
| `Providers` | GET lista | 200 | Proveedores (fuera del flujo MVP). |
| `Banks` | GET lista | 200 | Catálogo bancario (relevante futuro: conciliación). |
| `BankAccounts` | GET lista | 200 | Cuentas bancarias de la empresa (ídem). |
| `Users` | GET lista | 200 | Usuarios BIND. No se analizó su shape (contiene datos personales, no prioritario). |
### Responden 404 (no existen con ese nombre)
| Grupo | Nombres probados → 404 |
|---|---|
| Clientes (alias) | `Customers` |
| **Pagos** | `Payments`, `Payment`, `ClientPayments`, `CustomerPayments`, `Incomes`, `Income`, `Deposits`, `Collections`, `PaymentComplements`, `Complements`, `CashReceipts`, `AccountsReceivable`, `Receivables`, `Cobros`, `Pagos`, `InvoicePayments`, `PaymentsReceived` |
| Pagos (sub-recurso) | `Invoices/{id}/Payments`, `Invoices/{id}/payments`, `Invoices/{id}/CreditNotes` |
| Cotizaciones (alias) | `Quotations`, `Cotizaciones` |
| Series | `Series`, `InvoiceSeries`, `Folios`, `DocumentSeries` (la serie es **campo** de la factura, no recurso) |
| Sucursales (alias) | `Branches`, `Sucursales` (lo real es `Locations`) |
| Otros | `CreditNotes`, `Taxes`, `Prices`, `SalesOrders`, `PurchaseOrders`, `Suppliers`, `Sellers`, `Employees`, `Companies`, `Expenses`, `Inventory`, `CFDI`, `CFDIs` |
---
## 3 · Inventario de campos por recurso
Solo nombres, tipos y formatos observados (muestras de `$top=5`). `(≈corto/medio/largo)` = longitud aproximada del string; los valores reales nunca se persistieron.
### 3.1 `Invoices` — lista (32 campos)
| Campo | Tipo/formato | Nota |
|---|---|---|
| `ID` | guid | Clave para `GET /api/Invoices/{id}`. |
| `Serie` | string corto (a veces vacío) | ⚠️ En el detalle se llama **`Series`** (inconsistencia del API). |
| `Number` | integer | Folio interno. |
| `UUID` | guid | Folio fiscal del CFDI. **`null` en prefacturas.** |
| `Date` | datetime ISO | Fecha del documento. |
| `ExpirationDate` | datetime ISO | **Fecha de vencimiento** (esto alimenta el aging). No existe campo `DueDate`. |
| `ClientID` / `ClientName` | guid / string | Denormalizado en la propia fila. |
| `RFC` | string formato RFC | Del receptor. |
| `Cost`, `Subtotal`, `Discount`, `Total` | decimal | |
| `VAT`, `IEPS`, `ISRRet`, `VATRet` | decimal | Impuestos y retenciones. |
| `VATRate`, `VATRetRate` | decimal | Tasas (p. ej. `0.16`). |
| **`Payments`** | decimal | **Acumulado pagado de la factura** (ver §4). |
| **`CreditNotes`** | decimal | Acumulado de notas de crédito aplicadas. |
| `CurrencyID` | guid | FK a `Currencies` (la lista no trae el código — el detalle sí). |
| `ExchangeRate` | decimal | TC fijado al emitir. |
| `LocationID`, `WarehouseID`, `PriceListID` | guid | |
| `CFDIUse` | integer | ⚠️ Código **interno** (se observaron `3`, `23`), no la clave SAT (`G03`…). Falta tabla de mapeo. |
| `Comments` | string | Aquí ponen hoy el nº de ticket Jira (Discovery #27). |
| `PurchaseOrder` | string | Orden de compra. |
| `IsFiscalInvoice` | boolean | **`false` = prefactura** (sin timbrar). |
| `ShowIEPS` | boolean | |
| `Status` | integer | Ver semántica abajo. |
**Semántica de `Status` (mapeada contra el propio API, lista→detalle):**
| Código | Etiqueta (campo `Status` del detalle) |
|---|---|
| `0` | Activa |
| `1` | Pagada |
| `2` | Cancelada |
Se probaron códigos 35: sin filas (o no existen o no hay ejemplares). ⚠️ `Status` **no distingue** prefactura de timbrada — el discriminador fiable es `UUID eq null` / `IsFiscalInvoice eq false` (ambos filtros devuelven filas: **las prefacturas sí son visibles por API**).
### 3.2 `Invoices/{id}` — detalle (50 campos; los adicionales)
| Campo | Tipo/formato | Nota |
|---|---|---|
| `Series` | string | La lista lo llama `Serie`. |
| `Status` / `StatusCode` | string / integer | Etiqueta + código (p. ej. "Pagada" / `1`). |
| **`PaymentTerms`** | integer | **Días de crédito** de la factura. Solo en detalle. |
| **`CFDIPaymentTerm`** | string | ⚠️ **El método de pago SAT (PPD/PUE)** — se observó el literal "PAGO EN UNA SOLA EXHIBICIÓN" (=PUE). Puede venir vacío. Solo en detalle. |
| **`CFDIPaymentMethod`** | string | ⚠️ **La forma de pago SAT** (se observaron "Transferencia Electrónica de Fondos", "Por Definir"). Nomenclatura **invertida** respecto al SAT — ver hallazgos. |
| `CFDIAccountNumber` | string | Nº de cuenta (últimos dígitos), puede venir vacío. |
| `CurrencyName` | string 3 letras | Código de moneda (`MXN`/`USD`) — en el detalle es el código, no el nombre. |
| `ClientPhoneNumber`, `ClientContact` | string \| null | |
| `CreatedByID` / `CreatedByName` | guid / string | Quién creó el documento (auditoría). |
| `CreationDate` / `ApplicationDate` | datetime ISO | |
| `PriceListName`, `LocationName`, `WarehouseName` | string | Denormalizados. |
| `FiscalID` | guid | |
| `Address` | string largo | Dirección fiscal del receptor. |
| `Products` | array | Partidas de productos (vacío en la muestra — Balam factura servicios). |
| `Services` | array | **Partidas de servicios.** |
| `Services[].ID`, `Services[].ServiceID` | guid | |
| `Services[].IndexNumber` | integer | Orden de la partida. |
| `Services[].Name`, `Services[].Code` | string | Concepto (p. ej. el 029 "consultoría y servicios" del Discovery). |
| `Services[].Qty`, `Services[].Price` | decimal | |
| `Services[].VATRate` | decimal | **Tasa de IVA por partida** — habilita la validación 16 % / 0 %. |
| `Services[].Discount` | decimal | |
> El detalle **no** trae `CFDIUse` (solo la lista) ni un campo de saldo precalculado.
### 3.3 `Quotes` — lista (11 campos) y detalle (45)
**Lista:** `ID` (guid), `Number` (string), `CreationDate` (datetime), `ClientName`, `Locations` (string), `Comments`, `TotalOriginalCurrency` (decimal), `Currency` (nombre, p. ej. "Peso mexicano"), `Total` (decimal), `Status` (integer), `StatusText` (string).
**Semántica de `Quotes.Status`** (mapeada vía `$filter` + `StatusText` de la misma fila):
| Código | `StatusText` |
|---|---|
| `0` | Activa |
| `1` | Cancelada |
| `2` | Surtida |
**Detalle `Quotes/{id}` agrega:** `QuoteNumber`, `ClientID`/`ClientContact`/`ClientPhone`, `LocationName/ID`, `PriceListName/ID`, `EmployeeName/ID` (comercial que cotizó), `CurrencyCode` (3 letras), `ExchangeRate`, `Subtotal`, `Discount`, `IEPS`, `VAT`/`VATRate`, `ISR`/`ISRRate`, `VatRet`, `Total`, `BaseCurrency` (bool), `OriginalCurrencySubtotal`, `OriginalCurrencyDiscountAmount`, `IsPercentage` (bool), **`ContactEmails`**, `ExternalIDType` (int), `Comments`, y partidas **`Items[]`**: `ID`, `Code`, `ProductID`, `ProductName`, `Unit`, `Qty`, `Price`, `Amount`, `IEPS`, `VAT`, `IndexNumber`.
> ⚠️ **No hay campo que ligue la cotización con la factura generada** (ni `InvoiceID` en Quote, ni `QuoteID` en Invoice). "Surtida" dice que se convirtió, pero no *a qué* factura. Ver implicaciones (§9.2).
### 3.4 `Clients` — lista (10 campos) y detalle (28)
**Lista:** `ID` (guid), `Number` (int), `ClientName`, `LegalName`, `RFC`, `Email`, `Phone`, `NextContactDate`, `LocationID` (guid), `RegimenFiscal` (string).
**Detalle `Clients/{id}` agrega:**
| Campo | Tipo | Nota |
|---|---|---|
| `CommercialName` | string | |
| **`CreditDays`** | integer | **Días de crédito default del cliente** (los 30/45/90 del Discovery). |
| `CreditAmount` | decimal | Límite de crédito. |
| `PaymentMethod` | string | Forma de pago default (se observó "Efectivo"). |
| `PaymentTermType` | string | Puede venir vacío. |
| `Status` | string | "Activo"/… |
| `SalesContact` / `CreditContact` | string | Contactos comercial y de cobranza. |
| **`Loctaion` / `LoctaionID`** | string / guid | ⚠️ **Typo real del API** ("Loctaion", sic) — el cliente tipado debe usar el nombre con typo. |
| `PriceList` / `PriceListID` | string / guid | |
| `Email`, `Telephones` | string \| null | Correos configurados (los que usa el botón "enviar email" de BIND). |
| `AccountNumber`, `DefaultDiscount`, `ClientSource`, `Account` | varios | |
| `City`, `State`, `Addresses[]` | string / array | |
| `RegimenFiscal` | string | Régimen fiscal SAT. |
| `CreationDate` | datetime | |
> **No se observó** un campo "uso CFDI default por cliente" — el `CFDIUse` vive en la factura. La regla "gastos en general vs sin efectos fiscales" tendrá que derivarse de otra señal (p. ej. RFC extranjero/`XEXX010101000`, país, o configuración en la plataforma).
### 3.5 Catálogos
- **`Currencies`:** `ID` (guid), `Name`, `Code` (3 letras), `ExchangeRate` (decimal). 4 filas en la cuenta.
- **`Warehouses`:** `ID`, `Name` ("Matriz"), `LocationID`, `AvailableInOtherLoc` (bool). 1 fila — confirma el "hoy solo matriz" del Discovery.
- **`Locations`:** `ID`, `Name`, `Street`, `ExtNumber`, `IntNumber`, `ZipCode`, `Colonia`, `City`, `State`. 1 fila.
- **`Products`:** 30 campos, incl. `Code`, `Title`, `Cost`, `CostType(+Text)`, `CurrentInventory`, **`ChargeVAT`** (bool), `Unit`, `CurrencyID/Code`, `PricingType(+Text)`, `PurchaseType(+Text)`, `IEPSRate`, `Type(+Text)`, `SKU`, categorías.
### 3.6 Documentos del CFDI
| Endpoint | Estatus | Content-Type | Nota |
|---|---|---|---|
| `GET /api/Invoices/{id}/pdf` | 200 | `application/pdf` | **PDF real descargable por API** — insumo directo del módulo de envío. |
| `GET /api/Invoices/{id}/xml` | 200 | `application/json` | Responde 200 pero como JSON — probablemente envuelve el XML o una URL. El contenido se descartó por política de no persistir datos; **shape pendiente** (§10). |
---
## 4 · La pregunta del saldo — veredicto
**Plan A (operativo). No se necesita Plan B ni Plan C.**
- No existe un campo literal `Balance`/`Saldo`, **pero** cada fila de `/api/Invoices` trae `Total`, **`Payments`** (acumulado pagado) y **`CreditNotes`** (acumulado de notas de crédito):
$$\text{SaldoPorFactura} = \text{Total} - \text{Payments} - \text{CreditNotes}$$
- **Verificación aritmética (en memoria, sin persistir montos):** en 5/5 facturas con `Status=1` (Pagada), `Payments + CreditNotes ≈ Total` (diferencia < 0.01); en 5/5 con `Status=0` (Activa), el residual es positivo. La fórmula cuadra en ambas poblaciones.
- Es "Plan A" en el sentido operativo del riesgo de la propuesta: **una sola llamada a `/api/Invoices` basta** para calcular saldo y aging de toda la cartera — no hay que correlacionar una colección de pagos (Plan B) ni capturar nada a mano (Plan C).
- Matiz honesto: BIND no expone el número ya restado; la resta la hace la plataforma. El costo es cero (mismos campos, misma fila).
---
## 5 · Payments — el hallazgo duro
**No existe recurso consultable de pagos individuales.** Se probaron 17 nombres de colección y 3 sub-recursos (§2) — todos 404.
Lo que **sí** hay:
| Necesidad del MVP | ¿Cubierta? | Cómo |
|---|---|---|
| Saldo por factura | ✅ | `Total Payments CreditNotes` (§4). |
| ¿Factura pagada? | ✅ | `Status = 1` y/o residual ≈ 0. |
| Aging / vencimiento | ✅ | `ExpirationDate` + saldo. |
| **Fecha y monto de cada abono individual** | ❌ | No visible por API con este token. |
| Complementos de pago (REP) de facturas PPD | ❌ | Ningún recurso visible (`PaymentComplements`, `Complements` → 404). |
**Implicación:** el motor de cobranza puede detectar *que* una factura se pagó (transición de `Status`/residual entre sincronizaciones) y registrar el *timestamp de detección* en la plataforma, pero no la fecha valor del pago según BIND. Preguntar a Pedro/soporte BIND si existe un endpoint de pagos/REP no descubierto (la doc completa está tras login en developers.bind.com.mx) — ver §10.
---
## 6 · OData y paginación
| Mecanismo | ¿Funciona? | Evidencia |
|---|---|---|
| `$top` | ✅ con tope | `$top=100` → 200; **`$top=101` → 500**. |
| `$skip` | ✅ | `$top=1&$skip=1` devuelve la fila siguiente (verificado por ID). |
| `$orderby` | ✅ | `Date asc` → orden ascendente verificado. |
| `$filter eq` (int) | ✅ | `Status eq 1`, `CFDIUse eq 3` → 200. |
| `$filter eq null` | ✅ | `UUID eq null` → 200 con filas. |
| `$filter ge` + fecha | ✅ **sintaxis v3** | `Date ge datetime'2020-01-01T00:00:00'` → 200. |
| `$select` | ❌ | → **500**. No se pueden proyectar columnas; el payload siempre viene completo. |
| `$inlinecount=allpages` (v3) | ❌ | → 500. |
| `$count=true` (v4) | ⚠️ | → 200 pero **ignorado**: no devuelve conteo. |
| `odata.nextLink` | ❌ | Nunca apareció. |
**Paginación:** no hay `nextLink` ni conteo total ⇒ **paginación manual** con `$top=100&$skip=N` hasta recibir página corta. ⚠️ `GET` sin `$top` devuelve la colección completa en una respuesta (se observó con una colección de 41 filas) — con colecciones grandes es un riesgo de payload; **siempre** paginar. Sondeo por `$skip` (sin descargar): la colección histórica de `Invoices` supera las 1,000 filas.
**Rate limit:** no se observó **ningún header** de cuota (`X-RateLimit-*`, `Retry-After` en 200s) — el límite de 20K/día no es observable por request; hay que llevarlo con contador local (como ya hace `BindClient`).
**Estabilidad:** ~3 respuestas `500` transitorias en 117 peticiones (resueltas al primer retry). El retry con backoff **no es opcional** en producción. Nota: BIND usa 500 también para errores de sintaxis OData y token inválido — distinguir por body/contexto antes de reintentar a ciegas.
**GET por ID:** estilo **REST**`GET /api/Invoices/{id}` → 200; el estilo OData `Invoices(guid'...')`**404**. (El cliente del sandbox asumía el estilo OData; ya se corrigió.)
---
## 7 · Multi-empresa
- `Companies` → 404; **ningún** recurso expone campo `Company`/`Empresa`.
- `Locations` y `Warehouses` devuelven **1 fila** (Matriz).
- Conclusión: **el token está acotado a la empresa del usuario que lo generó** (Arturo → Balam). La distinción multi-empresa del portal Jira (Balam/Regiotour/Elmstone, Discovery #27) **no viaja a BIND por este token**: para facturar otras empresas se necesitaría una cuenta BIND distinta con su propio token. Anotado para el roadmap — coherente con dejar multi-empresa fuera del MVP.
---
## 8 · Hallazgos inesperados
1. **Token inválido → 500** (no 401/403), con mensaje `"API Key es inválida"` en el body. El 401 solo aparece cuando *falta* el header.
2. **`$select` no funciona** (500): no se puede reducir payload por columnas.
3. **Conteo total inaccesible**: `$inlinecount` truena (500) y `$count=true` se ignora — el total solo se conoce paginando hasta el final.
4. **No hay recurso de pagos** (17 nombres → 404) — el acumulado vive dentro de la factura (§5).
5. **Nomenclatura CFDI invertida respecto al SAT:** `CFDIPaymentTerm` = *Método de pago* SAT (PPD/PUE); `CFDIPaymentMethod` = *Forma de pago* SAT (transferencia, efectivo…). Cablearlo al revés rompería la validación de oro PPD/PUE.
6. **`CFDIUse` es un código interno** (enteros `3`, `23`), no la clave SAT (`G03`, `S01`…). Se necesita la tabla de mapeo (pedir a Pedro o doc tras login).
7. **Typo real en el API:** el detalle de `Clients` trae `Loctaion`/`LoctaionID` (sic).
8. **Inconsistencias lista vs detalle:** `Serie` (lista) vs `Series` (detalle); `Status` int (lista) vs `Status` string + `StatusCode` int (detalle); `CurrencyID` (lista) vs `CurrencyName` con el código (detalle); PPD/PUE y días de crédito **solo** en el detalle.
9. **Prefacturas visibles** en la misma colección `Invoices` (`UUID` null / `IsFiscalInvoice` false) — no hay recurso separado.
10. **`Activities` existe pero está vacío** en esta cuenta (0 filas) — el recurso que la doc pública usa de ejemplo no tiene datos aquí.
11. **500 transitorios** ocasionales que se resuelven con retry inmediato.
12. **Higiene de secretos:** el `.txt` del token estaba en la raíz del repo sin gitignorear (no trackeado aún) — se agregó `bind_token_api.txt` y `*.env` al `.gitignore` raíz. Recomendación vigente: moverlo a un gestor de secretos y borrarlo del correo/disco.
---
## 9 · Implicaciones para el MVP
Cruce contra el flujo objetivo del Discovery (#27): **Jira → cotización BIND → prefactura → validación humana → CFDI → envío**.
### 9.1 Lo que la API ya sostiene (solo lectura, hoy)
| Paso del flujo | Soporte confirmado |
|---|---|
| **Cotización** | `Quotes` legible con partidas, comercial (`EmployeeName`), moneda/TC y estatus (Activa/Cancelada/Surtida). La plataforma puede detectar cotizaciones nuevas y validar el prerequisito "cotización obligatoria" de Ara. |
| **Prefactura** | Listable vía `UUID eq null` / `IsFiscalInvoice eq false` → el dashboard "prefacturas pendientes de validación" es viable 100 % lectura. |
| **CFDI** | `UUID`, `Series`+`Number`, RFC, moneda, `ExchangeRate`, impuestos por partida (`Services[].VATRate`), uso CFDI (código), PPD/PUE (`CFDIPaymentTerm` en detalle), creador y fechas. |
| **Validaciones de oro** | • **PPD/PUE:** auditable por factura (detalle). La plataforma puede alertar "PUE detectado — ¿fue consciente?" apenas aparezca. • **IVA 16 %/0 %:** `VATRate` por partida + moneda + RFC → la regla "extranjero con IVA ≠ 0" (el error que Ara señaló en vivo) es detectable automáticamente. • **Días de crédito:** `Clients.CreditDays` (default) vs `PaymentTerms` (factura) — discrepancias detectables. |
| **Cobranza / aging** | `ExpirationDate` + saldo derivado (§4) + `Status` → aging y alertas internas sin recurso de pagos. |
| **Envío** | PDF real por API (`/{id}/pdf`) + correos del cliente (`Clients.Email`, `ContactEmails`) + las particularidades por cliente (Excel de Ara/Arturo) viven en la plataforma. |
### 9.2 Restricciones de diseño que impone lo encontrado
1. **Sync incremental obligatorio.** PPD/PUE y días de crédito viven en el **detalle** ⇒ 1 llamada por factura. Con ~55 facturas/mes es trivial, pero el histórico (>1,000) exige sincronizar por delta (`Date ge` la última corrida) y guardar en Postgres — nunca re-barrer todo el detalle.
2. **Trazabilidad cotización→factura la lleva la plataforma.** BIND no expone el vínculo; al orquestar la conversión (Etapa 2) la plataforma debe registrar el par `QuoteID→InvoiceID` en su propia BD (y/o convención en `Comments`, como hoy hacen con el ticket Jira).
3. **"Fecha de pago" = fecha de detección.** Sin pagos individuales, la plataforma registra cuándo *observó* el cambio a Pagada — suficiente para cobranza operativa; insuficiente para conciliación contable fina (que de todos modos es fase posterior).
4. **Catálogos internos a mapear:** `CFDIUse` (int→clave SAT) y códigos de `Status` no observados (3+). Confirmar con Pedro.
5. **Cliente HTTP:** paginar siempre (`$top=100`), retry en 500 transitorio, no usar `$select`, IDs estilo REST, contador local de cuota (sin headers de rate limit).
6. **Los complementos de pago (REP) no son visibles** — riesgo para el flujo PPD completo; escalar a Pedro (§10).
### 9.3 Presupuesto de peticiones (viabilidad del sync)
Escenario conservador: lista de facturas delta (12 req) + detalle solo de facturas nuevas/cambiadas (~3/día) + cotizaciones delta (1 req) + clientes delta (1 req) ⇒ **< 10 req por ciclo**. Con polling cada 15 min ≈ **~1,000 req/día**, 5 % del límite de 20K. Holgado.
---
## 10 · Lo que quedó SIN validar y por qué
| Pendiente | Por qué no se validó | Cómo cerrarlo |
|---|---|---|
| **Escritura** (crear cotización/prefactura, convertir, emitir CFDI, cancelar) | **Prohibido en esta actividad**: cuenta de producción, sin sandbox, efecto fiscal. Regla dura de solo-GET. | Doc detallada tras login (developers.bind.com.mx) con Pedro; luego dry-run + confirmación humana en Etapa 2, empezando por un documento de prueba interno coordinado con Arturo. |
| Si la factura creada desde una cotización hereda alguna referencia a ésta | Requiere ejecutar la conversión (= escritura). | Mismo camino que el punto anterior; o preguntar a Arturo si la UI muestra el vínculo. |
| Shape real del `/{id}/xml` (¿XML embebido? ¿URL?) | El body se descartó por política de no persistir datos reales en esta corrida. | 1 GET dirigido leyendo solo las **claves** del JSON (sin valores), en la próxima sesión técnica. |
| Mapa completo `CFDIUse` interno → clave SAT | No hay catálogo expuesto; solo se observaron códigos `3` y `23`. | Pedir tabla a Pedro o doc tras login. |
| Códigos de `Status` > 2 (¿parciales, vencidas?) | Los filtros 35 no devolvieron filas: o no existen o no hay ejemplares en la cuenta. | Doc tras login; observar en operación. |
| **Complementos de pago (REP)** para PPD | Ningún recurso visible con los nombres probados. | **Crítico** — preguntar a Pedro/soporte BIND; el flujo PPD del MVP lo necesita al menos en lectura. |
| Rate limit real (20K/día) y comportamiento al agotarlo | No hay headers de cuota y agotar el límite adrede sería irresponsable en producción. | Aceptar el dato de Noe (20K) y llevar contador local. |
| Shape de `Users`, `Orders`, `Providers`, `Banks`, `BankAccounts`, `PriceLists` | Fuera del flujo del MVP; `Users` además contiene datos personales. | Cuando conciliación (Anexo B) lo requiera. |
| Webhooks / eventos push | No documentados públicamente; no sondeables por GET. | Preguntar a Pedro; mientras, polling incremental. |
---
## Anexo · Reproducir la validación
```powershell
cd bind-api-sandbox
# .env debe tener BIND_API_TOKEN (nunca se versiona; .gitignore lo cubre)
npm run validate:real # ronda 1: auth + inventario + shapes + OData + byId
npx tsx src/validate-real-api.ts --round2 # pagos, estatus, prefactura, detalles, paginación
npx tsx src/validate-real-api.ts --round3 # tope $top, literales CFDI, pdf/xml
npx tsx src/validate-real-api.ts --round4 # aritmética del saldo + xml
```
- Presupuesto acumulado entre rondas (`VALIDATION_BUDGET`, default 120). El script aborta al agotarlo.
- El reporte `validation-output/report.json` está sanitizado (solo estructura) y además gitignoreado por defensa en profundidad.
- El script es GET-only por construcción: no contiene ningún código capaz de emitir escrituras.
[REGISTRO #22]: ../bitacora/REGISTRO.md
[REGISTRO #29]: ../bitacora/REGISTRO.md