npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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

Una vez publicado, la instalación será:

npm install --global @medine-tech/backoffice-cli

El 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 --json

La 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-interactive

La 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:

  1. --profile <nombre> selecciona un perfil guardado y prevalece sobre el entorno.
  2. Sin ese flag, una tripleta completa BACKOFFICE_SERVER, BACKOFFICE_TENANT y BACKOFFICE_API_KEY configura una conexión temporal.
  3. Sin tripleta, BACKOFFICE_PROFILE selecciona un perfil guardado.
  4. 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-interactive

Filtros 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=1

Cada 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: --search filtra en el cliente y lo declara con search_scope, pages_scanned y truncated.
  • emission_date se publica como YYYY-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[].id los asigna el servidor. Un items[].id enviado se descarta sin aviso, por lo que solo el id de 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 --help

Desde apps/backoffice-cli:

npm test
npm run lint
npm run check-types
npm run build
npm run test:pack

npm 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.