9ccfbe262e
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.
358 lines
26 KiB
Markdown
358 lines
26 KiB
Markdown
# 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 3–5: 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 (1–2 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 3–5 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
|