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.
26 KiB
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.mxToken: entregado por Pedro el 6-jul (correo, REGISTRO #29) — generado con el usuario de Arturo Rosas, conforme al acuerdo del kickoff (REGISTRO #22). Vive enbind-api-sandbox/.env(BIND_API_TOKEN), fuera de git. Método: scriptsrc/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 envalidation-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-Keyque 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
InvoiceIDen Quote, niQuoteIDen 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
CFDIUsevive 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/InvoicestraeTotal,Payments(acumulado pagado) yCreditNotes(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 conStatus=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/Invoicesbasta 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 campoCompany/Empresa.LocationsyWarehousesdevuelven 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
- 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. $selectno funciona (500): no se puede reducir payload por columnas.- Conteo total inaccesible:
$inlinecounttruena (500) y$count=truese ignora — el total solo se conoce paginando hasta el final. - No hay recurso de pagos (17 nombres → 404) — el acumulado vive dentro de la factura (§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. CFDIUsees un código interno (enteros3,23), no la clave SAT (G03,S01…). Se necesita la tabla de mapeo (pedir a Pedro o doc tras login).- Typo real en el API: el detalle de
ClientstraeLoctaion/LoctaionID(sic). - Inconsistencias lista vs detalle:
Serie(lista) vsSeries(detalle);Statusint (lista) vsStatusstring +StatusCodeint (detalle);CurrencyID(lista) vsCurrencyNamecon el código (detalle); PPD/PUE y días de crédito solo en el detalle. - Prefacturas visibles en la misma colección
Invoices(UUIDnull /IsFiscalInvoicefalse) — no hay recurso separado. Activitiesexiste pero está vacío en esta cuenta (0 filas) — el recurso que la doc pública usa de ejemplo no tiene datos aquí.- 500 transitorios ocasionales que se resuelven con retry inmediato.
- Higiene de secretos: el
.txtdel token estaba en la raíz del repo sin gitignorear (no trackeado aún) — se agregóbind_token_api.txty*.enval.gitignoreraí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
- 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 gela última corrida) y guardar en Postgres — nunca re-barrer todo el detalle. - 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→InvoiceIDen su propia BD (y/o convención enComments, como hoy hacen con el ticket Jira). - "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).
- Catálogos internos a mapear:
CFDIUse(int→clave SAT) y códigos deStatusno observados (3+). Confirmar con Pedro. - 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). - 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
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.jsonestá 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.