@medine-tech/backoffice-cli
v0.2.0
Published
CLI de MedineTech ERP para descubrir contratos de Backoffice y operar facturas de compras desde la terminal.
Maintainers
Readme
Backoffice CLI
CLI de MedineTech ERP con catálogo offline, descubrimiento progresivo y
solicitudes validadas para las operaciones de empresa de Backoffice. El ejecutable se llama backoffice; el paquete
@medine-tech/backoffice-cli tiene su propia versión, actualmente 0.2.0.
La interfaz de negocio permite descubrir las referencias de una factura de compra, resolverlas contra el servidor, crearla y verificar el resultado con una lectura posterior. La operación POST genérica sigue disponible y ejecuta el mismo contrato sin verificar su efecto.
Instalación
Requiere Node.js ^24.15.0 || >=26.0.0 y npm ^12.0.2.
La primera publicación en npm aún no ocurre. Hasta entonces, genera el
archivo distribuible desde el workspace apps/backoffice-cli:
npm pack --pack-destination /tmp
npm install --global /tmp/medine-tech-backoffice-cli-0.2.0.tgz
backoffice --version
backoffice --helpUna vez publicado, la instalación será:
npm install --global @medine-tech/backoffice-cliEl paquete es público y se publica bajo la licencia MIT. Sus dependencias de
runtime son @oclif/core 5.0.0, ajv 8.20.0, ajv-draft-04 1.0.0 y
ajv-formats 3.0.1; el cliente de API y el catálogo viajan dentro del paquete,
así que no necesita el workspace del cliente ni el código del ERP.
Descubrir el contrato sin conexión
Estos comandos no leen perfiles ni archivos de entrada y no hacen solicitudes:
backoffice help
backoffice purchases --help
backoffice purchases invoice help
backoffice purchases invoice create --help
backoffice search "factura de compra" --json
backoffice purchases invoice get --schema --json
backoffice purchases invoice create --schema --json
backoffice purchases invoice create --example
backoffice purchases invoice references --json
backoffice api list --module purchases --json
backoffice api describe purchases.invoice.create --jsonLa raíz muestra módulos. Cada módulo separa dos cosas que no son intercambiables:
commands son los sustantivos ejecutables —backoffice purchases invoice— y
catalog_paths son segmentos de ruta del catálogo sin comando de negocio, que
solo se consultan con api list o api describe. Escribir un catalog_paths
como si fuera un comando produce INVALID_INPUT. La factura expone acciones,
estados, referencias y próximos pasos.
El alias posicional help se reconoce en topics y acciones de factura, y en
comandos registrados sin argumentos posicionales (api list help,
profile check help). En search help, api describe help y api request help,
help conserva su significado de argumento. Tampoco se reescriben valores de
flags ni texto después de --.
Cada comando de negocio documenta su propia superficie además del contrato:
--help publica command, usage, arguments y flags, con el nombre de
cada opción, su forma de argumento —--input <archivo|->, --per-page
<entero>— y cuáles son obligatorias. Las opciones publicadas son exactamente
las que declara el comando, más --help y, en las acciones respaldadas por una
operación del catálogo, --schema y --example. Un argumento posicional con
valores cerrados publica sus options, como reference <name>.
INVALID_INPUT nombra lo que recibió y qué acepta. details publica kind
—module, resource, action, command, flag, flag_value o
argument_value—, el token received, las expected disponibles cuando la
lista es cerrada y un next_step. Los dos últimos añaden parameter con la
opción o el argumento que rechazó el valor.
Un valor fuera de un conjunto cerrado se rechaza en el cliente, antes de
cualquier solicitud, y su token sí viaja en received: es una alternativa
publicada, no un dato del usuario. El valor de una opción de texto libre nunca
aparece, ni siquiera en la forma --opcion=valor.
Los identificadores son estables: get:purchases/purchases-invoices/{id} o
su alias purchases.invoice.get. Los hashes operationId no se aceptan.
La búsqueda ignora mayúsculas y acentos. describe muestra contrato, ejemplos,
permisos y soporte; --schema entrega solo los parámetros/body y sus referencias
transitivas, con schema_format: "openapi-3.0" y tenant ligado al perfil.
Cuando el CLI acepta un cuerpo que el contrato publicado no aceptaría, --schema
lo dice junto al contrato en cli_input, con scope: "cli_only" y una regla por
campo: qué exige el contrato, qué acepta el CLI, qué genera cuando el campo falta
y en qué campo del resultado publica esa procedencia. El contrato publicado no
cambia; --help repite la misma sección para que ninguna de las dos superficies
describa una entrada distinta.
{
"scope": "cli_only",
"rules": [
{
"field": "id",
"contract": "required",
"cli": "optional",
"generated_when_omitted": "uuid_v4",
"reported_in": "id_source",
"reported_values": ["client", "generated"]
}
]
}El artefacto actual contiene las 265 operaciones documentadas de empresa;
solo excluye el bootstrap invitado POST /api/backoffice/first-companies.
251 contratos pueden ejecutarse. Las 5 cargas multipart, 6 descargas y 3
operaciones exclusivas de sesión permanecen visibles con support.available:
false y motivos explícitos. La generación selecciona el conjunto desde OpenAPI;
el número no es una lista fija de operaciones permitidas.
Los valores de support.reasons y unsupported_reasons forman un vocabulario
cerrado. Cuando api request --json rechaza una operación por esta clasificación,
error.reason publica el motivo como campo opcional; conserva code y el código
de salida. No contiene nombres, valores de entrada ni errores internos.
| Motivo | Significado |
|---|---|
| session_authentication_required | La operación exige una sesión de usuario. |
| unsupported_request_media | El medio del body no está soportado o su declaración es inválida. |
| download_response | La respuesta es una descarga fuera del alcance del cliente. |
| unsupported_parameter_serialization | La ubicación, forma o serialización de parámetros no está soportada. |
| unsupported_method | El método HTTP no está soportado para ejecución. |
| unsupported_input_contract | El esquema de entrada contiene un contrato que el validador no puede garantizar. |
El último motivo conserva UNSUPPORTED_CONTRACT; los demás usan
UNSUPPORTED_OPERATION. Ambos conservan salida 2. Los demás errores omiten
reason.
Los permisos del piloto se verificaron en rutas/controladores; las nueve consultas
lookup heredan el acceso y membership del grupo. Fuera del piloto se indica
not_verified: el servidor decide los permisos efectivos. El campo
permissions.inherited_requirements publica las restricciones heredadas
verificadas del piloto: authentication, verified_user, tenant_context,
api_key_guardrails, company_access, permission_team_context y
tenant_membership. Un required: [] solo indica ausencia de un permiso
nombrado propio; las restricciones heredadas siguen aplicando. Los efectos se marcan
como verificados o inferidos del método; el POST de búsqueda sensible es lectura.
Las escrituras requieren la habilidad write de la clave además de los permisos
del usuario. Los ejemplos usan IDs ficticios y no acreditan existencia de referencias.
Ejecutar una operación conocida
backoffice api request purchases.invoice.get \
--path '{"id":"019a46ea-4517-70a5-9afe-4b564dc0ab13"}' --json
backoffice api request purchases.providers.lookup \
--query '{"status":"active","page":1,"per_page":20}' --json
backoffice api request post:purchases/contracts --input payload.json --json
cat payload.json | backoffice api request post:purchases/contracts \
--input - --json --non-interactive--path y --query reciben objetos JSON tipados. --input contiene únicamente
el body JSON desde archivo o -; solo esta última opción lee stdin. Stdin TTY
se rechaza inmediatamente. Archivos/stdin tienen un límite de 1 MiB y 30 segundos.
JSON inválido, errores de lectura y límites producen errores sanitizados antes de HTTP.
El catálogo decide método y ruta: no se admiten URLs arbitrarias.
El tenant siempre sale de la conexión validada. Enviar tenant en path/query
se rechaza aunque coincida con el perfil. Los valores de path son escalares,
se codifican por segmento y no admiten vacío, ., .., slash, backslash,
controles, Unicode inválido ni % (incluidas codificaciones de traversal).
?, # y $& se conservan como datos codificados. Se verifica el origen,
el pathname exacto y que ningún valor capture otra ruta literal como lookup.
Query admite primitivos y arrays form: pares repetidos con explode: true
(predeterminado), o elementos codificados individualmente unidos por coma con
explode: false. Se rechazan nombres desconocidos, null, objetos, arrays vacíos,
header/cookie, content, allowReserved y estilos no implementados.
El body admite application/json; ausencia, null y {} son valores distintos.
La validación de entrada conserva tipos, required, nullable, enum, límites
inclusive/exclusivo, multipleOf, longitudes Unicode, pattern, límites/uniqueness
de arrays, límites de propiedades, additionalProperties, referencias locales y
allOf/oneOf/anyOf/not. Usa OpenAPI 3.0 con adaptación a draft-04 y formatos completos
UUID, date, date-time, email y formatos numéricos admitidos por Ajv. readOnly
se excluye de required y se rechaza al enviarlo; writeOnly conserva su obligación declarada.
Composición de acceso ambigua, discriminator, keywords o formatos desconocidos
fallan como contrato no soportado. binary no habilita uploads.
No hay coerción, inserción de defaults ni eliminación de campos. Se rechazan valores no JSON, ciclos, números no finitos y enteros fuera del rango seguro. Las cadenas decimales se transmiten idénticas. Esto no garantiza precisión monetaria para schemas numéricos ni exactitud int64 fuera del rango seguro. La validación corresponde a las restricciones publicadas; permisos, referencias y reglas de negocio adicionales siguen siendo autoridad del servidor.
Toda solicitud se intenta una sola vez, sin confirmaciones, avisos ni reintentos.
Un éxito genérico informa operation, method, http_status, effect y
verification: "not_performed". Cero bytes se representan como
response: {"kind":"empty"}; JSON válido, incluso null o arrays, como
response: {"kind":"json","value":null}. Un 201 vacío no fabrica un ID.
Timeout/desconexión o respuesta exitosa ilegible tras intentar una escritura
producen WRITE_OUTCOME_UNKNOWN, exit 9. No se hace GET de reconciliación.
Un error HTTP conserva el status observado y no acredita rollback del servidor.
Configurar el acceso
Necesitas el origen del servidor HTTP(S), el UUID de la empresa y una clave de API administrada en Backoffice. La clave pertenece a un usuario, está vinculada a una empresa y conserva sus permisos vigentes.
Guarda un perfil leyendo la clave desde stdin. Sustituye el servidor y el
UUID del ejemplo por los de tu entorno; BACKOFFICE_API_KEY debe contener la
clave obtenida previamente:
printf '%s\n' "$BACKOFFICE_API_KEY" | backoffice profile set default \
--server https://erp.example \
--tenant 019a46ea-4517-70a5-9afe-4b564dc0ab12 \
--api-key-stdin --non-interactive
unset BACKOFFICE_API_KEY
backoffice profile check --profile default --json --non-interactiveLa clave se trata como un valor opaco, incluido cualquier prefijo con |.
Se admite un salto final LF o CRLF de stdin. Una clave vacía, con controles
o espacios en los extremos produce un error sin alterarla. Si stdin es una
terminal, el comando falla inmediatamente y no solicita la clave. No existe un flag para pasar la
clave como argumento. El servidor debe ser un origen: sin credenciales,
query, fragmento ni una ruta como /api.
profile check significa comprobar acceso a la empresa configurada.
Ejecuta una consulta de proveedores permitida; no verifica identidad ni
acredita permiso para todas las operaciones. Listar o consultar facturas
requiere el permiso backoffice.purchases.purchases-invoices.view.
Los perfiles se guardan en ~/.config/backoffice/profiles.json. Puedes elegir
otro directorio con BACKOFFICE_CONFIG_DIR. El documento usa la versión 1
y un diccionario de perfiles. En POSIX, el directorio se crea con modo 0700
y el archivo con 0600. Una configuración inválida produce un error; no se
reemplaza silenciosamente.
La selección de conexión sigue este orden:
--profile <nombre>selecciona un perfil guardado y prevalece sobre el entorno.- Sin ese flag, una tripleta completa
BACKOFFICE_SERVER,BACKOFFICE_TENANTyBACKOFFICE_API_KEYconfigura una conexión temporal. - Sin tripleta,
BACKOFFICE_PROFILEselecciona un perfil guardado. - Sin selección, se usa
default.
Una tripleta parcial es un error. El CLI nunca combina el servidor de una fuente con la clave de otra. Por eso el ejemplo elimina la variable de clave tras guardar el perfil.
Consultar facturas
backoffice purchases invoice list \
--status to_be_approved --page 1 --per-page 20 \
--profile default --json --non-interactive
backoffice purchases invoice get 019a46ea-4517-70a5-9afe-4b564dc0ab13 \
--profile default --json --non-interactiveFiltros de purchases invoice list:
| Flag | Parámetro de API | Valor |
| ---------------------- | -------------------- | ------------------------------------ |
| --provider-id | provider_id | UUID del proveedor |
| --status | status | Estado, por ejemplo to_be_approved |
| --code | code | Código de factura |
| --emission-date-from | emission_date_from | Fecha inicial inclusiva YYYY-MM-DD |
| --emission-date-to | emission_date_to | Fecha final inclusiva YYYY-MM-DD |
| --sort-by | sort_by | Campo de orden, por ejemplo code |
| --sort-order | sort_order | asc o desc |
| --page | page | Entero positivo |
| --per-page | per_page | Entero positivo |
El orden predeterminado del servidor es code DESC. --sort-by admite
code, emission_date, total, balance, status y created_at. Las fechas
deben existir y el rango inicial no puede superar el final.
--status, --sort-by y --sort-order tienen conjuntos cerrados y publicados:
backoffice purchases invoice --help --json da los estados en states y
... list --help --json los repite en la forma de argumento de cada opción. Un
valor fuera del conjunto se rechaza con INVALID_INPUT y salida 2 sin llegar
al servidor, nombrando el valor recibido y las alternativas. Un listado vacío
significa entonces que no hay facturas que cumplan el filtro, nunca que el filtro
estaba mal escrito. Los filtros de texto libre —--code, --provider-id— y las
fechas no tienen conjunto cerrado: el servidor decide.
Las consultas usan estas rutas:
GET /api/backoffice/{tenant}/purchases/purchases-invoices
GET /api/backoffice/{tenant}/purchases/purchases-invoices/{id}
GET /api/backoffice/{tenant}/purchases/providers/lookup?per_page=1Cada solicitud lleva Authorization: Bearer <clave> y
Accept: application/json. No se usan cookies. Las redirecciones se
rechazan sin seguirlas; no hay reintentos automáticos y las solicitudes
tienen un timeout de 30 segundos.
Resolver las referencias de una factura
Una factura de compra vincula nueve referencias. El catálogo de referencias es offline: no lee perfiles ni hace solicitudes.
backoffice purchases invoice references --json
backoffice purchases invoice reference providers --json
backoffice purchases invoice reference items --search "tornillo" --json
backoffice purchases invoice reference locations --status inactive --page 2 --per-page 50 --json| Referencia | Campo de la factura | Operación |
| --- | --- | --- |
| providers | provider_id | get:purchases/providers/lookup |
| categories | items[].category_id | get:inventory/categories/lookup |
| items | items[].item_id | get:inventory/items/lookup |
| units | items[].unit_id | get:inventory/units/lookup |
| taxes | items[].tax_id | get:accounting/taxes/lookup |
| accounting-centers | accounting_center_id, items[].accounting_center_id | get:accounting/accounting-centers/lookup |
| accounting-accounts | items[].accounting_account_id | get:accounting/accounting-accounts/lookup |
| payable-accounts | accounting_account_payable_id | get:accounting/payable-accounts/lookup |
| locations | location_id (opcional) | get:inventory/locations/lookup |
El id de payable-accounts es el de la cuenta contable subyacente, no el de
la configuración. Dos comprobaciones evitan los rechazos más frecuentes y salen
gratis de la proyección de items: items[].category_id debe ser el
category_id del artículo, e items[].unit_id debe estar entre sus unit_ids.
La tercera no evita un rechazo: evita una factura equivocada.
items[].tax_percentage debería ser el rate que devuelve
accounting/taxes/lookup para ese items[].tax_id, y el servidor no comprueba
esa correspondencia. Valida por separado que tax_percentage sea un número
entre 0 y 100 y que tax_id exista, esté activo y pertenezca a la empresa;
cualquier combinación de los dos se acepta y el porcentaje enviado queda
almacenado. Resuelve el rate con el lookup en lugar de suponerlo. Las tres
comprobaciones viajan en cross_checks de references --json.
--status es active por omisión, y solo acepta active o inactive: solo las
referencias activas vinculan. Una lista vacía es éxito, con salida 0 y un
next_steps que sugiere --status inactive o la consulta genérica. Un nombre de
referencia fuera de los nueve se rechaza con INVALID_INPUT, salida 2 y las nueve
alternativas en details.expected, sin llegar al servidor.
El servidor no publica búsqueda de texto en ningún lookup. --search filtra
en el cliente las filas que ya descargó: avanza desde --page hasta cubrir el
total o hasta un tope de 25 páginas. La respuesta lo dice explícitamente con
search_scope: "client_side_paged", pages_scanned y truncated. Sin
--search se lee una sola página y search_scope se omite.
truncated significa lo mismo en ambos modos —quedaron filas sin leer— pero se
alcanza por dos caminos: con --search el recorrido se detuvo en el tope de 25
páginas antes de cubrir el total; sin --search la única página leída no
cubrió el total. En los dos casos hay más referencias que las devueltas: sigue
con --page. El recorrido razona siempre con el per_page que aplicó el
servidor, no con el solicitado, por si el servidor acota la página.
Ninguna de las nueve consultas declara un permiso propio, y eso no significa
acceso libre: heredan autenticación, verificación, contexto de empresa,
guardarraíles de clave y pertenencia al tenant. references --json publica esas
restricciones heredadas junto al catálogo.
Crear y verificar una factura
backoffice purchases invoice create --input factura.json --json
cat factura.json | backoffice purchases invoice create --input - --json --non-interactive
backoffice purchases invoice create --help --json--input contiene únicamente el body JSON. El comando valida el cuerpo contra
el contrato publicado antes de cualquier solicitud, envía el POST una sola
vez y después lee la factura con GET .../purchases-invoices/{id} usando el
id con el que escribió. El id de la factura es el único identificador
predecible: los items[].id los asigna el servidor y un items[].id enviado se
descarta.
El id lo aporta el cliente y el CLI puede aportarlo por ti. Lo publica
backoffice purchases invoice create --schema --json en cli_input. El contrato
publicado lo exige y no cambia: si --input no trae id, el CLI genera un UUID
v4, lo usa para el POST y para la lectura de verificación, y lo devuelve en la
respuesta. Envíalo tú cuando quieras controlar el reintento —repetir la creación
con el mismo id es una decisión tuya, no del CLI— y omítelo cuando solo
necesites crear la factura. El resultado publica id_source: client si venía
en --input, generated si lo generó el CLI. Un id presente pero no válido
—null, un número, una cadena que no es UUID— es un error de contrato: nunca se
sustituye por uno generado.
El comando necesita dos permisos, uno por cada tramo. El POST exige
backoffice.purchases.purchases-invoices.create y la habilidad write de la
clave; la lectura de verificación exige
backoffice.purchases.purchases-invoices.view, porque
GET .../purchases-invoices/{id} lo declara en su ruta. Una credencial con
.create pero sin .view crea la factura y termina en CREATE_NOT_VERIFIED
(salida 10) con readback_error_code: "ACCESS_DENIED": la factura existe y
repetir el verify_command fallará igual hasta que se conceda .view.
Los decimales aceptan número o cadena, igual que el validador. Solo una cadena
JSON conserva la escala exacta: "1.50" viaja idéntico, mientras que 1.50
se transmite como 1.5. El CLI nunca convierte entre ambas formas. El valor
almacenado se trunca a dos decimales.
Una creación verificada devuelve:
{
"ok": true,
"data": {
"operation": "post:purchases/purchases-invoices",
"http_status": 201,
"create_response": { "kind": "json", "value": {} },
"verification": "verified",
"id": "019a46ea-4517-70a5-9afe-4b564dc0ab13",
"id_source": "client",
"invoice": { "id": "019a46ea-4517-70a5-9afe-4b564dc0ab13" },
"next_steps": ["backoffice purchases invoice get 019a46ea-4517-70a5-9afe-4b564dc0ab13 --json"]
}
}create_response es la respuesta cruda del POST, que es un objeto vacío sin
identificador. invoice proviene solo de la lectura de verificación. Salida
0 significa que el POST devolvió 2xx y que esa lectura devolvió una factura cuyo
id coincide, sin distinguir mayúsculas.
| Resultado | Código | Exit |
| --- | --- | --- |
| Cuerpo inválido según el contrato, sin ninguna solicitud | REQUEST_VALIDATION_FAILED | 2 |
| Clave sin habilidad write o usuario sin permiso .create | ACCESS_DENIED | 4 |
| Clave de otra empresa | NOT_FOUND | 5 |
| Rechazo del servidor: validación, código duplicado, reglas de orden o cantidad | CREATE_REJECTED | 8 |
| Una o más referencias que la empresa no puede vincular | REFERENCES_NOT_BINDABLE | 8 |
| Escritura intentada con resultado incierto | WRITE_OUTCOME_UNKNOWN | 9 |
| Escritura confirmada y lectura de verificación fallida | CREATE_NOT_VERIFIED | 10 |
CREATE_REJECTED conserva en details las claves que escribe el servidor
—error_code, detail y errors— acotadas en tamaño.
REFERENCES_NOT_BINDABLE conserva details.violations con reference_kind,
reference_id, reason y human_message: nombra cada referencia en lugar de
devolver un error HTTP genérico.
Los tres desenlaces inciertos no son el mismo: exit 9 significa que el CLI no
sabe si la fila existe; exit 10 significa que sí existe —se observó un 2xx— y
no se pudo leer. Exit 10 devuelve details con invoice_id,
create_http_status, readback_error_code, el verify_command a ejecutar y un
readback_guidance que distingue el fallo pasajero —repite el verify_command—
del permanente: un readback_error_code de ACCESS_DENIED significa que la
credencial no puede leer facturas y necesita .view.
Ninguna rama reintenta la escritura: como máximo una solicitud POST y una GET. El POST no aprueba la factura; la aprobación es un flujo aparte.
Salida para scripts
--json escribe un único objeto JSON en stdout, también ante errores, y deja
stderr vacío. No mezcla banners, logs ni prompts. Comprueba siempre el código
de salida del proceso. --non-interactive está disponible en los comandos;
esta versión no solicita datos mediante prompts.
El listado conserva íntegra la respuesta {data, meta} dentro del sobre:
{
"ok": true,
"data": {
"data": [],
"meta": { "total": 0, "per_page": 20, "current_page": 1 }
}
}Una lista vacía es un éxito. meta, las propiedades adicionales y los
decimales representados como strings se conservan sin recalcular ni convertir
a números. En get, data contiene el objeto de factura recibido del servidor.
La comprobación de acceso devuelve:
{
"ok": true,
"data": {
"profile": "default",
"tenant": "019a46ea-4517-70a5-9afe-4b564dc0ab12",
"access": "verified"
}
}Un error de autenticación devuelve:
{
"ok": false,
"error": {
"code": "AUTHENTICATION_FAILED",
"message": "La clave no permite autenticar la solicitud.",
"http_status": 401
}
}http_status solo aparece cuando existe una respuesta HTTP. Los mensajes son
propios: los fallos de schema añaden details.violations con ubicación de entrada,
ruta del schema y keyword (máximo 20). No incluyen rutas de instancia ni nombres
de propiedades desconocidas del payload. No incluyen claves, cabeceras, cuerpos remotos, destinos de
redirección ni stack traces. Sin --json, el resultado legible va a stdout y
los errores sanitizados a stderr.
| Exit | Significado | Código de error |
| ---- | ------------------------------------------ | ---------------------------------------------------------------------------------- |
| 0 | Éxito, incluida una lista vacía | — |
| 2 | Uso, entrada o perfil inválido/faltante | INVALID_INPUT, INVALID_PROFILE, PROFILE_READ_FAILED, PROFILE_WRITE_FAILED o STDIN_ERROR; en API: UNKNOWN_OPERATION, INVALID_JSON, INPUT_READ_FAILED, INPUT_TOO_LARGE, REQUEST_VALIDATION_FAILED, UNSUPPORTED_OPERATION, UNSUPPORTED_CONTRACT, COMMAND_NOT_IMPLEMENTED (acción declarada sin ejecución) |
| 3 | HTTP 401 | AUTHENTICATION_FAILED |
| 4 | HTTP 403 | ACCESS_DENIED |
| 5 | HTTP 404 | NOT_FOUND |
| 6 | Fallo de transporte o timeout | TRANSPORT_ERROR |
| 7 | Respuesta de lectura inválida o redirección | INVALID_RESPONSE o REDIRECT_REJECTED |
| 8 | Otro error HTTP, incluidos 400/422/429/5xx | HTTP_ERROR; en creación de negocio: CREATE_REJECTED o REFERENCES_NOT_BINDABLE |
| 9 | Resultado de escritura incierto | WRITE_OUTCOME_UNKNOWN |
| 10 | Escritura confirmada y verificación fallida | CREATE_NOT_VERIFIED |
| 1 | Error interno sanitizado | INTERNAL_ERROR |
Limitaciones conocidas
- No hay búsqueda de texto en el servidor:
--searchfiltra en el cliente y lo declara consearch_scope,pages_scannedytruncated. emission_datese publica comoYYYY-MM-DD. La regla del validador es más laxa; el contrato se estrechó a propósito para que el valor enviado coincida con el almacenado y devuelto.- Las cadenas decimales rechazan notación exponencial, signo inicial y espacios, que PHP aceptaría. La rama numérica sí los admite cuando el JSON los permite.
- Los
items[].idlos asigna el servidor. Unitems[].idenviado se descarta sin aviso, por lo que solo elidde la factura sirve para verificar. - Siete de las nueve consultas lookup no tienen cobertura automatizada en el backend. El piloto es su única evidencia.
- No hay auto-aprobación, confirmaciones ni reintentos en ninguna operación.
Ayuda y verificación local
La ayuda funciona sin perfil ni acceso a la red:
backoffice --help
backoffice profile --help
backoffice profile check --help
backoffice purchases --help
backoffice purchases invoice --help
backoffice purchases invoice list --help
backoffice purchases invoice get --help
backoffice purchases invoice create --help
backoffice purchases invoice references --help
backoffice purchases invoice reference --helpDesde apps/backoffice-cli:
npm test
npm run lint
npm run check-types
npm run build
npm run test:packnpm run test:pack -- --catalog-growth añade una operación de ejemplo únicamente
a una copia temporal del contrato y del paquete para comprobar que las pruebas
aceptan crecimiento legítimo cuando coinciden los conjuntos de fuente, catálogo
y CLI instalada. La ejecución normal verifica el contrato real sin modificarlo.
test:pack genera e instala el tarball real en un directorio temporal fuera
del repositorio. Ejecuta el bin instalado, comprueba ayuda offline, perfiles,
consultas, decimales, paginación y errores contra servidores HTTP locales, y
verifica que el destino de una redirección no reciba solicitudes. La
instalación de dependencias puede consultar npm; las pruebas de API no usan
datos ni credenciales de producción. Los procesos, servidores y archivos
temporales se limpian al terminar.
Desde packages/backoffice-api, npm run generate genera schema.d.ts,
catalog.ts y checksums.json desde storage/api-docs/api-docs.json y los
metadatos curados. La transformación pura ordena mapas/operaciones, preserva
arrays y calcula hashes canónicos de fuente, metadatos y catálogo.
npm run generate:check compara todos los artefactos sin reescribirlos y falla
si falta uno o hay drift. Las anotaciones PHP se regeneran con make generate-docs.
Backoffice CLI Checks ya ejecuta la comprobación de generación en CI.
Los tipos se generan desde el OpenAPI del ERP. Este cliente conserva propiedades adicionales en las respuestas. El contrato de creación publica las mismas reglas que valida el controlador —uuid, longitudes, mínimo de líneas y decimales como número o cadena—; una prueba de arquitectura compara ambos lados campo por campo. El smoke del paquete usa servidores locales y no sustituye una prueba contra la API Laravel real.
