# 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 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