@medine-tech/backoffice-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/bin/run.js +11 -0
- package/dist/index.js +44328 -0
- package/package.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MedineTech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
# Backoffice CLI
|
|
2
|
+
|
|
3
|
+
CLI de MedineTech ERP con catálogo offline, descubrimiento progresivo y
|
|
4
|
+
solicitudes validadas para las operaciones de empresa de Backoffice. El ejecutable se llama `backoffice`; el paquete
|
|
5
|
+
`@medine-tech/backoffice-cli` tiene su propia versión, actualmente `0.2.0`.
|
|
6
|
+
|
|
7
|
+
La interfaz de negocio permite descubrir las referencias de una factura de
|
|
8
|
+
compra, resolverlas contra el servidor, crearla y verificar el resultado con
|
|
9
|
+
una lectura posterior. La operación POST genérica sigue disponible y ejecuta el
|
|
10
|
+
mismo contrato sin verificar su efecto.
|
|
11
|
+
|
|
12
|
+
## Instalación
|
|
13
|
+
|
|
14
|
+
Requiere Node.js `^24.15.0 || >=26.0.0` y npm `^12.0.2`.
|
|
15
|
+
|
|
16
|
+
**La primera publicación en npm aún no ocurre.** Hasta entonces, genera el
|
|
17
|
+
archivo distribuible desde el workspace `apps/backoffice-cli`:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm pack --pack-destination /tmp
|
|
21
|
+
npm install --global /tmp/medine-tech-backoffice-cli-0.2.0.tgz
|
|
22
|
+
backoffice --version
|
|
23
|
+
backoffice --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Una vez publicado, la instalación será:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install --global @medine-tech/backoffice-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
El paquete es público y se publica bajo la licencia MIT. Sus dependencias de
|
|
33
|
+
runtime son `@oclif/core` 5.0.0, `ajv` 8.20.0, `ajv-draft-04` 1.0.0 y
|
|
34
|
+
`ajv-formats` 3.0.1; el cliente de API y el catálogo viajan dentro del paquete,
|
|
35
|
+
así que no necesita el workspace del cliente ni el código del ERP.
|
|
36
|
+
|
|
37
|
+
## Descubrir el contrato sin conexión
|
|
38
|
+
|
|
39
|
+
Estos comandos no leen perfiles ni archivos de entrada y no hacen solicitudes:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
backoffice help
|
|
43
|
+
backoffice purchases --help
|
|
44
|
+
backoffice purchases invoice help
|
|
45
|
+
backoffice purchases invoice create --help
|
|
46
|
+
backoffice search "factura de compra" --json
|
|
47
|
+
backoffice purchases invoice get --schema --json
|
|
48
|
+
backoffice purchases invoice create --schema --json
|
|
49
|
+
backoffice purchases invoice create --example
|
|
50
|
+
backoffice purchases invoice references --json
|
|
51
|
+
backoffice api list --module purchases --json
|
|
52
|
+
backoffice api describe purchases.invoice.create --json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
La raíz muestra módulos. Cada módulo separa dos cosas que no son intercambiables:
|
|
56
|
+
`commands` son los sustantivos ejecutables —`backoffice purchases invoice`— y
|
|
57
|
+
`catalog_paths` son segmentos de ruta del catálogo sin comando de negocio, que
|
|
58
|
+
solo se consultan con `api list` o `api describe`. Escribir un `catalog_paths`
|
|
59
|
+
como si fuera un comando produce `INVALID_INPUT`. La factura expone acciones,
|
|
60
|
+
estados, referencias y próximos pasos.
|
|
61
|
+
El alias posicional `help` se reconoce en topics y acciones de factura, y en
|
|
62
|
+
comandos registrados sin argumentos posicionales (`api list help`,
|
|
63
|
+
`profile check help`). En `search help`, `api describe help` y `api request help`,
|
|
64
|
+
`help` conserva su significado de argumento. Tampoco se reescriben valores de
|
|
65
|
+
flags ni texto después de `--`.
|
|
66
|
+
|
|
67
|
+
Cada comando de negocio documenta su propia superficie además del contrato:
|
|
68
|
+
`--help` publica `command`, `usage`, `arguments` y `flags`, con el nombre de
|
|
69
|
+
cada opción, su forma de argumento —`--input <archivo|->`, `--per-page
|
|
70
|
+
<entero>`— y cuáles son obligatorias. Las opciones publicadas son exactamente
|
|
71
|
+
las que declara el comando, más `--help` y, en las acciones respaldadas por una
|
|
72
|
+
operación del catálogo, `--schema` y `--example`. Un argumento posicional con
|
|
73
|
+
valores cerrados publica sus `options`, como `reference <name>`.
|
|
74
|
+
|
|
75
|
+
`INVALID_INPUT` nombra lo que recibió y qué acepta. `details` publica `kind`
|
|
76
|
+
—`module`, `resource`, `action`, `command`, `flag`, `flag_value` o
|
|
77
|
+
`argument_value`—, el token `received`, las `expected` disponibles cuando la
|
|
78
|
+
lista es cerrada y un `next_step`. Los dos últimos añaden `parameter` con la
|
|
79
|
+
opción o el argumento que rechazó el valor.
|
|
80
|
+
|
|
81
|
+
Un valor fuera de un conjunto cerrado se rechaza en el cliente, antes de
|
|
82
|
+
cualquier solicitud, y su token sí viaja en `received`: es una alternativa
|
|
83
|
+
publicada, no un dato del usuario. El valor de una opción de texto libre nunca
|
|
84
|
+
aparece, ni siquiera en la forma `--opcion=valor`.
|
|
85
|
+
|
|
86
|
+
Los identificadores son estables: `get:purchases/purchases-invoices/{id}` o
|
|
87
|
+
su alias `purchases.invoice.get`. Los hashes `operationId` no se aceptan.
|
|
88
|
+
La búsqueda ignora mayúsculas y acentos. `describe` muestra contrato, ejemplos,
|
|
89
|
+
permisos y soporte; `--schema` entrega solo los parámetros/body y sus referencias
|
|
90
|
+
transitivas, con `schema_format: "openapi-3.0"` y tenant ligado al perfil.
|
|
91
|
+
|
|
92
|
+
Cuando el CLI acepta un cuerpo que el contrato publicado no aceptaría, `--schema`
|
|
93
|
+
lo dice junto al contrato en `cli_input`, con `scope: "cli_only"` y una regla por
|
|
94
|
+
campo: qué exige el contrato, qué acepta el CLI, qué genera cuando el campo falta
|
|
95
|
+
y en qué campo del resultado publica esa procedencia. El contrato publicado no
|
|
96
|
+
cambia; `--help` repite la misma sección para que ninguna de las dos superficies
|
|
97
|
+
describa una entrada distinta.
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"scope": "cli_only",
|
|
102
|
+
"rules": [
|
|
103
|
+
{
|
|
104
|
+
"field": "id",
|
|
105
|
+
"contract": "required",
|
|
106
|
+
"cli": "optional",
|
|
107
|
+
"generated_when_omitted": "uuid_v4",
|
|
108
|
+
"reported_in": "id_source",
|
|
109
|
+
"reported_values": ["client", "generated"]
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
El artefacto actual contiene las 265 operaciones documentadas de empresa;
|
|
116
|
+
solo excluye el bootstrap invitado `POST /api/backoffice/first-companies`.
|
|
117
|
+
251 contratos pueden ejecutarse. Las 5 cargas multipart, 6 descargas y 3
|
|
118
|
+
operaciones exclusivas de sesión permanecen visibles con `support.available:
|
|
119
|
+
false` y motivos explícitos. La generación selecciona el conjunto desde OpenAPI;
|
|
120
|
+
el número no es una lista fija de operaciones permitidas.
|
|
121
|
+
|
|
122
|
+
Los valores de `support.reasons` y `unsupported_reasons` forman un vocabulario
|
|
123
|
+
cerrado. Cuando `api request --json` rechaza una operación por esta clasificación,
|
|
124
|
+
`error.reason` publica el motivo como campo opcional; conserva `code` y el código
|
|
125
|
+
de salida. No contiene nombres, valores de entrada ni errores internos.
|
|
126
|
+
|
|
127
|
+
| Motivo | Significado |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `session_authentication_required` | La operación exige una sesión de usuario. |
|
|
130
|
+
| `unsupported_request_media` | El medio del body no está soportado o su declaración es inválida. |
|
|
131
|
+
| `download_response` | La respuesta es una descarga fuera del alcance del cliente. |
|
|
132
|
+
| `unsupported_parameter_serialization` | La ubicación, forma o serialización de parámetros no está soportada. |
|
|
133
|
+
| `unsupported_method` | El método HTTP no está soportado para ejecución. |
|
|
134
|
+
| `unsupported_input_contract` | El esquema de entrada contiene un contrato que el validador no puede garantizar. |
|
|
135
|
+
|
|
136
|
+
El último motivo conserva `UNSUPPORTED_CONTRACT`; los demás usan
|
|
137
|
+
`UNSUPPORTED_OPERATION`. Ambos conservan salida 2. Los demás errores omiten
|
|
138
|
+
`reason`.
|
|
139
|
+
|
|
140
|
+
Los permisos del piloto se verificaron en rutas/controladores; las nueve consultas
|
|
141
|
+
lookup heredan el acceso y membership del grupo. Fuera del piloto se indica
|
|
142
|
+
`not_verified`: el servidor decide los permisos efectivos. El campo
|
|
143
|
+
`permissions.inherited_requirements` publica las restricciones heredadas
|
|
144
|
+
verificadas del piloto: `authentication`, `verified_user`, `tenant_context`,
|
|
145
|
+
`api_key_guardrails`, `company_access`, `permission_team_context` y
|
|
146
|
+
`tenant_membership`. Un `required: []` solo indica ausencia de un permiso
|
|
147
|
+
nombrado propio; las restricciones heredadas siguen aplicando. Los efectos se marcan
|
|
148
|
+
como verificados o inferidos del método; el POST de búsqueda sensible es lectura.
|
|
149
|
+
Las escrituras requieren la habilidad `write` de la clave además de los permisos
|
|
150
|
+
del usuario. Los ejemplos usan IDs ficticios y no acreditan existencia de referencias.
|
|
151
|
+
|
|
152
|
+
## Ejecutar una operación conocida
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
backoffice api request purchases.invoice.get \
|
|
156
|
+
--path '{"id":"019a46ea-4517-70a5-9afe-4b564dc0ab13"}' --json
|
|
157
|
+
|
|
158
|
+
backoffice api request purchases.providers.lookup \
|
|
159
|
+
--query '{"status":"active","page":1,"per_page":20}' --json
|
|
160
|
+
|
|
161
|
+
backoffice api request post:purchases/contracts --input payload.json --json
|
|
162
|
+
cat payload.json | backoffice api request post:purchases/contracts \
|
|
163
|
+
--input - --json --non-interactive
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`--path` y `--query` reciben objetos JSON tipados. `--input` contiene únicamente
|
|
167
|
+
el body JSON desde archivo o `-`; solo esta última opción lee stdin. Stdin TTY
|
|
168
|
+
se rechaza inmediatamente. Archivos/stdin tienen un límite de 1 MiB y 30 segundos.
|
|
169
|
+
JSON inválido, errores de lectura y límites producen errores sanitizados antes de HTTP.
|
|
170
|
+
El catálogo decide método y ruta: no se admiten URLs arbitrarias.
|
|
171
|
+
|
|
172
|
+
El tenant siempre sale de la conexión validada. Enviar `tenant` en path/query
|
|
173
|
+
se rechaza aunque coincida con el perfil. Los valores de path son escalares,
|
|
174
|
+
se codifican por segmento y no admiten vacío, `.`, `..`, slash, backslash,
|
|
175
|
+
controles, Unicode inválido ni `%` (incluidas codificaciones de traversal).
|
|
176
|
+
`?`, `#` y `$&` se conservan como datos codificados. Se verifica el origen,
|
|
177
|
+
el pathname exacto y que ningún valor capture otra ruta literal como `lookup`.
|
|
178
|
+
|
|
179
|
+
Query admite primitivos y arrays `form`: pares repetidos con `explode: true`
|
|
180
|
+
(predeterminado), o elementos codificados individualmente unidos por coma con
|
|
181
|
+
`explode: false`. Se rechazan nombres desconocidos, null, objetos, arrays vacíos,
|
|
182
|
+
header/cookie, `content`, `allowReserved` y estilos no implementados.
|
|
183
|
+
El body admite `application/json`; ausencia, null y `{}` son valores distintos.
|
|
184
|
+
|
|
185
|
+
La validación de entrada conserva tipos, required, nullable, enum, límites
|
|
186
|
+
inclusive/exclusivo, multipleOf, longitudes Unicode, pattern, límites/uniqueness
|
|
187
|
+
de arrays, límites de propiedades, additionalProperties, referencias locales y
|
|
188
|
+
allOf/oneOf/anyOf/not. Usa OpenAPI 3.0 con adaptación a draft-04 y formatos completos
|
|
189
|
+
UUID, date, date-time, email y formatos numéricos admitidos por Ajv. `readOnly`
|
|
190
|
+
se excluye de required y se rechaza al enviarlo; `writeOnly` conserva su obligación declarada.
|
|
191
|
+
Composición de acceso ambigua, discriminator, keywords o formatos desconocidos
|
|
192
|
+
fallan como contrato no soportado. `binary` no habilita uploads.
|
|
193
|
+
|
|
194
|
+
No hay coerción, inserción de defaults ni eliminación de campos. Se rechazan
|
|
195
|
+
valores no JSON, ciclos, números no finitos y enteros fuera del rango seguro.
|
|
196
|
+
Las cadenas decimales se transmiten idénticas. Esto no garantiza precisión
|
|
197
|
+
monetaria para schemas numéricos ni exactitud int64 fuera del rango seguro.
|
|
198
|
+
La validación corresponde a las restricciones publicadas; permisos, referencias
|
|
199
|
+
y reglas de negocio adicionales siguen siendo autoridad del servidor.
|
|
200
|
+
|
|
201
|
+
Toda solicitud se intenta una sola vez, sin confirmaciones, avisos ni reintentos.
|
|
202
|
+
Un éxito genérico informa `operation`, `method`, `http_status`, `effect` y
|
|
203
|
+
`verification: "not_performed"`. Cero bytes se representan como
|
|
204
|
+
`response: {"kind":"empty"}`; JSON válido, incluso null o arrays, como
|
|
205
|
+
`response: {"kind":"json","value":null}`. Un 201 vacío no fabrica un ID.
|
|
206
|
+
Timeout/desconexión o respuesta exitosa ilegible tras intentar una escritura
|
|
207
|
+
producen `WRITE_OUTCOME_UNKNOWN`, exit 9. No se hace GET de reconciliación.
|
|
208
|
+
Un error HTTP conserva el status observado y no acredita rollback del servidor.
|
|
209
|
+
|
|
210
|
+
## Configurar el acceso
|
|
211
|
+
|
|
212
|
+
Necesitas el origen del servidor HTTP(S), el UUID de la empresa y una clave
|
|
213
|
+
de API administrada en Backoffice. La clave pertenece a un usuario, está
|
|
214
|
+
vinculada a una empresa y conserva sus permisos vigentes.
|
|
215
|
+
|
|
216
|
+
Guarda un perfil leyendo la clave desde stdin. Sustituye el servidor y el
|
|
217
|
+
UUID del ejemplo por los de tu entorno; `BACKOFFICE_API_KEY` debe contener la
|
|
218
|
+
clave obtenida previamente:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
printf '%s\n' "$BACKOFFICE_API_KEY" | backoffice profile set default \
|
|
222
|
+
--server https://erp.example \
|
|
223
|
+
--tenant 019a46ea-4517-70a5-9afe-4b564dc0ab12 \
|
|
224
|
+
--api-key-stdin --non-interactive
|
|
225
|
+
unset BACKOFFICE_API_KEY
|
|
226
|
+
|
|
227
|
+
backoffice profile check --profile default --json --non-interactive
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
La clave se trata como un valor opaco, incluido cualquier prefijo con `|`.
|
|
231
|
+
Se admite un salto final LF o CRLF de stdin. Una clave vacía, con controles
|
|
232
|
+
o espacios en los extremos produce un error sin alterarla. Si stdin es una
|
|
233
|
+
terminal, el comando falla inmediatamente y no solicita la clave. No existe un flag para pasar la
|
|
234
|
+
clave como argumento. El servidor debe ser un origen: sin credenciales,
|
|
235
|
+
query, fragmento ni una ruta como `/api`.
|
|
236
|
+
|
|
237
|
+
`profile check` significa **comprobar acceso a la empresa configurada**.
|
|
238
|
+
Ejecuta una consulta de proveedores permitida; no verifica identidad ni
|
|
239
|
+
acredita permiso para todas las operaciones. Listar o consultar facturas
|
|
240
|
+
requiere el permiso `backoffice.purchases.purchases-invoices.view`.
|
|
241
|
+
|
|
242
|
+
Los perfiles se guardan en `~/.config/backoffice/profiles.json`. Puedes elegir
|
|
243
|
+
otro directorio con `BACKOFFICE_CONFIG_DIR`. El documento usa la versión `1`
|
|
244
|
+
y un diccionario de perfiles. En POSIX, el directorio se crea con modo `0700`
|
|
245
|
+
y el archivo con `0600`. Una configuración inválida produce un error; no se
|
|
246
|
+
reemplaza silenciosamente.
|
|
247
|
+
|
|
248
|
+
La selección de conexión sigue este orden:
|
|
249
|
+
|
|
250
|
+
1. `--profile <nombre>` selecciona un perfil guardado y prevalece sobre el entorno.
|
|
251
|
+
2. Sin ese flag, una tripleta completa `BACKOFFICE_SERVER`, `BACKOFFICE_TENANT`
|
|
252
|
+
y `BACKOFFICE_API_KEY` configura una conexión temporal.
|
|
253
|
+
3. Sin tripleta, `BACKOFFICE_PROFILE` selecciona un perfil guardado.
|
|
254
|
+
4. Sin selección, se usa `default`.
|
|
255
|
+
|
|
256
|
+
Una tripleta parcial es un error. El CLI nunca combina el servidor de una
|
|
257
|
+
fuente con la clave de otra. Por eso el ejemplo elimina la variable de clave
|
|
258
|
+
tras guardar el perfil.
|
|
259
|
+
|
|
260
|
+
## Consultar facturas
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
backoffice purchases invoice list \
|
|
264
|
+
--status to_be_approved --page 1 --per-page 20 \
|
|
265
|
+
--profile default --json --non-interactive
|
|
266
|
+
|
|
267
|
+
backoffice purchases invoice get 019a46ea-4517-70a5-9afe-4b564dc0ab13 \
|
|
268
|
+
--profile default --json --non-interactive
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Filtros de `purchases invoice list`:
|
|
272
|
+
|
|
273
|
+
| Flag | Parámetro de API | Valor |
|
|
274
|
+
| ---------------------- | -------------------- | ------------------------------------ |
|
|
275
|
+
| `--provider-id` | `provider_id` | UUID del proveedor |
|
|
276
|
+
| `--status` | `status` | Estado, por ejemplo `to_be_approved` |
|
|
277
|
+
| `--code` | `code` | Código de factura |
|
|
278
|
+
| `--emission-date-from` | `emission_date_from` | Fecha inicial inclusiva `YYYY-MM-DD` |
|
|
279
|
+
| `--emission-date-to` | `emission_date_to` | Fecha final inclusiva `YYYY-MM-DD` |
|
|
280
|
+
| `--sort-by` | `sort_by` | Campo de orden, por ejemplo `code` |
|
|
281
|
+
| `--sort-order` | `sort_order` | `asc` o `desc` |
|
|
282
|
+
| `--page` | `page` | Entero positivo |
|
|
283
|
+
| `--per-page` | `per_page` | Entero positivo |
|
|
284
|
+
|
|
285
|
+
El orden predeterminado del servidor es `code DESC`. `--sort-by` admite
|
|
286
|
+
`code`, `emission_date`, `total`, `balance`, `status` y `created_at`. Las fechas
|
|
287
|
+
deben existir y el rango inicial no puede superar el final.
|
|
288
|
+
|
|
289
|
+
`--status`, `--sort-by` y `--sort-order` tienen conjuntos cerrados y publicados:
|
|
290
|
+
`backoffice purchases invoice --help --json` da los estados en `states` y
|
|
291
|
+
`... list --help --json` los repite en la forma de argumento de cada opción. Un
|
|
292
|
+
valor fuera del conjunto se rechaza con `INVALID_INPUT` y salida 2 **sin llegar
|
|
293
|
+
al servidor**, nombrando el valor recibido y las alternativas. Un listado vacío
|
|
294
|
+
significa entonces que no hay facturas que cumplan el filtro, nunca que el filtro
|
|
295
|
+
estaba mal escrito. Los filtros de texto libre —`--code`, `--provider-id`— y las
|
|
296
|
+
fechas no tienen conjunto cerrado: el servidor decide.
|
|
297
|
+
|
|
298
|
+
Las consultas usan estas rutas:
|
|
299
|
+
|
|
300
|
+
```text
|
|
301
|
+
GET /api/backoffice/{tenant}/purchases/purchases-invoices
|
|
302
|
+
GET /api/backoffice/{tenant}/purchases/purchases-invoices/{id}
|
|
303
|
+
GET /api/backoffice/{tenant}/purchases/providers/lookup?per_page=1
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Cada solicitud lleva `Authorization: Bearer <clave>` y
|
|
307
|
+
`Accept: application/json`. No se usan cookies. Las redirecciones se
|
|
308
|
+
rechazan sin seguirlas; no hay reintentos automáticos y las solicitudes
|
|
309
|
+
tienen un timeout de 30 segundos.
|
|
310
|
+
|
|
311
|
+
## Resolver las referencias de una factura
|
|
312
|
+
|
|
313
|
+
Una factura de compra vincula nueve referencias. El catálogo de referencias es
|
|
314
|
+
offline: no lee perfiles ni hace solicitudes.
|
|
315
|
+
|
|
316
|
+
```sh
|
|
317
|
+
backoffice purchases invoice references --json
|
|
318
|
+
backoffice purchases invoice reference providers --json
|
|
319
|
+
backoffice purchases invoice reference items --search "tornillo" --json
|
|
320
|
+
backoffice purchases invoice reference locations --status inactive --page 2 --per-page 50 --json
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
| Referencia | Campo de la factura | Operación |
|
|
324
|
+
| --- | --- | --- |
|
|
325
|
+
| `providers` | `provider_id` | `get:purchases/providers/lookup` |
|
|
326
|
+
| `categories` | `items[].category_id` | `get:inventory/categories/lookup` |
|
|
327
|
+
| `items` | `items[].item_id` | `get:inventory/items/lookup` |
|
|
328
|
+
| `units` | `items[].unit_id` | `get:inventory/units/lookup` |
|
|
329
|
+
| `taxes` | `items[].tax_id` | `get:accounting/taxes/lookup` |
|
|
330
|
+
| `accounting-centers` | `accounting_center_id`, `items[].accounting_center_id` | `get:accounting/accounting-centers/lookup` |
|
|
331
|
+
| `accounting-accounts` | `items[].accounting_account_id` | `get:accounting/accounting-accounts/lookup` |
|
|
332
|
+
| `payable-accounts` | `accounting_account_payable_id` | `get:accounting/payable-accounts/lookup` |
|
|
333
|
+
| `locations` | `location_id` (opcional) | `get:inventory/locations/lookup` |
|
|
334
|
+
|
|
335
|
+
El id de `payable-accounts` es el de la **cuenta contable** subyacente, no el de
|
|
336
|
+
la configuración. Dos comprobaciones evitan los rechazos más frecuentes y salen
|
|
337
|
+
gratis de la proyección de `items`: `items[].category_id` debe ser el
|
|
338
|
+
`category_id` del artículo, e `items[].unit_id` debe estar entre sus `unit_ids`.
|
|
339
|
+
|
|
340
|
+
La tercera no evita un rechazo: evita una factura equivocada.
|
|
341
|
+
`items[].tax_percentage` debería ser el `rate` que devuelve
|
|
342
|
+
`accounting/taxes/lookup` para ese `items[].tax_id`, y **el servidor no comprueba
|
|
343
|
+
esa correspondencia**. Valida por separado que `tax_percentage` sea un número
|
|
344
|
+
entre 0 y 100 y que `tax_id` exista, esté activo y pertenezca a la empresa;
|
|
345
|
+
cualquier combinación de los dos se acepta y el porcentaje enviado queda
|
|
346
|
+
almacenado. Resuelve el `rate` con el lookup en lugar de suponerlo. Las tres
|
|
347
|
+
comprobaciones viajan en `cross_checks` de `references --json`.
|
|
348
|
+
|
|
349
|
+
`--status` es `active` por omisión, y solo acepta `active` o `inactive`: solo las
|
|
350
|
+
referencias activas vinculan. Una lista vacía es éxito, con salida 0 y un
|
|
351
|
+
`next_steps` que sugiere `--status inactive` o la consulta genérica. Un nombre de
|
|
352
|
+
referencia fuera de los nueve se rechaza con `INVALID_INPUT`, salida 2 y las nueve
|
|
353
|
+
alternativas en `details.expected`, sin llegar al servidor.
|
|
354
|
+
|
|
355
|
+
**El servidor no publica búsqueda de texto en ningún lookup.** `--search` filtra
|
|
356
|
+
en el cliente las filas que ya descargó: avanza desde `--page` hasta cubrir el
|
|
357
|
+
`total` o hasta un tope de 25 páginas. La respuesta lo dice explícitamente con
|
|
358
|
+
`search_scope: "client_side_paged"`, `pages_scanned` y `truncated`. Sin
|
|
359
|
+
`--search` se lee una sola página y `search_scope` se omite.
|
|
360
|
+
|
|
361
|
+
`truncated` significa lo mismo en ambos modos —quedaron filas sin leer— pero se
|
|
362
|
+
alcanza por dos caminos: con `--search` el recorrido se detuvo en el tope de 25
|
|
363
|
+
páginas antes de cubrir el `total`; sin `--search` la única página leída no
|
|
364
|
+
cubrió el `total`. En los dos casos hay más referencias que las devueltas: sigue
|
|
365
|
+
con `--page`. El recorrido razona siempre con el `per_page` que aplicó el
|
|
366
|
+
servidor, no con el solicitado, por si el servidor acota la página.
|
|
367
|
+
|
|
368
|
+
Ninguna de las nueve consultas declara un permiso propio, y eso no significa
|
|
369
|
+
acceso libre: heredan autenticación, verificación, contexto de empresa,
|
|
370
|
+
guardarraíles de clave y pertenencia al tenant. `references --json` publica esas
|
|
371
|
+
restricciones heredadas junto al catálogo.
|
|
372
|
+
|
|
373
|
+
## Crear y verificar una factura
|
|
374
|
+
|
|
375
|
+
```sh
|
|
376
|
+
backoffice purchases invoice create --input factura.json --json
|
|
377
|
+
cat factura.json | backoffice purchases invoice create --input - --json --non-interactive
|
|
378
|
+
backoffice purchases invoice create --help --json
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`--input` contiene únicamente el body JSON. El comando valida el cuerpo contra
|
|
382
|
+
el contrato publicado **antes** de cualquier solicitud, envía el POST **una sola
|
|
383
|
+
vez** y después lee la factura con `GET .../purchases-invoices/{id}` usando el
|
|
384
|
+
`id` con el que escribió. El `id` de la factura es el único identificador
|
|
385
|
+
predecible: los `items[].id` los asigna el servidor y un `items[].id` enviado se
|
|
386
|
+
descarta.
|
|
387
|
+
|
|
388
|
+
**El `id` lo aporta el cliente y el CLI puede aportarlo por ti.** Lo publica
|
|
389
|
+
`backoffice purchases invoice create --schema --json` en `cli_input`. El contrato
|
|
390
|
+
publicado lo exige y no cambia: si `--input` no trae `id`, el CLI genera un UUID
|
|
391
|
+
v4, lo usa para el POST y para la lectura de verificación, y lo devuelve en la
|
|
392
|
+
respuesta. Envíalo tú cuando quieras controlar el reintento —repetir la creación
|
|
393
|
+
con el mismo `id` es una decisión tuya, no del CLI— y omítelo cuando solo
|
|
394
|
+
necesites crear la factura. El resultado publica `id_source`: `client` si venía
|
|
395
|
+
en `--input`, `generated` si lo generó el CLI. Un `id` presente pero no válido
|
|
396
|
+
—`null`, un número, una cadena que no es UUID— es un error de contrato: nunca se
|
|
397
|
+
sustituye por uno generado.
|
|
398
|
+
|
|
399
|
+
**El comando necesita dos permisos, uno por cada tramo.** El POST exige
|
|
400
|
+
`backoffice.purchases.purchases-invoices.create` y la habilidad `write` de la
|
|
401
|
+
clave; la lectura de verificación exige
|
|
402
|
+
`backoffice.purchases.purchases-invoices.view`, porque
|
|
403
|
+
`GET .../purchases-invoices/{id}` lo declara en su ruta. Una credencial con
|
|
404
|
+
`.create` pero sin `.view` crea la factura y termina en `CREATE_NOT_VERIFIED`
|
|
405
|
+
(salida 10) con `readback_error_code: "ACCESS_DENIED"`: la factura existe y
|
|
406
|
+
repetir el `verify_command` fallará igual hasta que se conceda `.view`.
|
|
407
|
+
|
|
408
|
+
Los decimales aceptan número o cadena, igual que el validador. **Solo una cadena
|
|
409
|
+
JSON conserva la escala exacta**: `"1.50"` viaja idéntico, mientras que `1.50`
|
|
410
|
+
se transmite como `1.5`. El CLI nunca convierte entre ambas formas. El valor
|
|
411
|
+
almacenado se trunca a dos decimales.
|
|
412
|
+
|
|
413
|
+
Una creación verificada devuelve:
|
|
414
|
+
|
|
415
|
+
```json
|
|
416
|
+
{
|
|
417
|
+
"ok": true,
|
|
418
|
+
"data": {
|
|
419
|
+
"operation": "post:purchases/purchases-invoices",
|
|
420
|
+
"http_status": 201,
|
|
421
|
+
"create_response": { "kind": "json", "value": {} },
|
|
422
|
+
"verification": "verified",
|
|
423
|
+
"id": "019a46ea-4517-70a5-9afe-4b564dc0ab13",
|
|
424
|
+
"id_source": "client",
|
|
425
|
+
"invoice": { "id": "019a46ea-4517-70a5-9afe-4b564dc0ab13" },
|
|
426
|
+
"next_steps": ["backoffice purchases invoice get 019a46ea-4517-70a5-9afe-4b564dc0ab13 --json"]
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`create_response` es la respuesta cruda del POST, que es un objeto vacío sin
|
|
432
|
+
identificador. `invoice` proviene **solo** de la lectura de verificación. Salida
|
|
433
|
+
0 significa que el POST devolvió 2xx y que esa lectura devolvió una factura cuyo
|
|
434
|
+
`id` coincide, sin distinguir mayúsculas.
|
|
435
|
+
|
|
436
|
+
| Resultado | Código | Exit |
|
|
437
|
+
| --- | --- | --- |
|
|
438
|
+
| Cuerpo inválido según el contrato, sin ninguna solicitud | `REQUEST_VALIDATION_FAILED` | 2 |
|
|
439
|
+
| Clave sin habilidad `write` o usuario sin permiso `.create` | `ACCESS_DENIED` | 4 |
|
|
440
|
+
| Clave de otra empresa | `NOT_FOUND` | 5 |
|
|
441
|
+
| Rechazo del servidor: validación, código duplicado, reglas de orden o cantidad | `CREATE_REJECTED` | 8 |
|
|
442
|
+
| Una o más referencias que la empresa no puede vincular | `REFERENCES_NOT_BINDABLE` | 8 |
|
|
443
|
+
| Escritura intentada con resultado incierto | `WRITE_OUTCOME_UNKNOWN` | 9 |
|
|
444
|
+
| Escritura confirmada y lectura de verificación fallida | `CREATE_NOT_VERIFIED` | 10 |
|
|
445
|
+
|
|
446
|
+
`CREATE_REJECTED` conserva en `details` las claves que escribe el servidor
|
|
447
|
+
—`error_code`, `detail` y `errors`— acotadas en tamaño.
|
|
448
|
+
`REFERENCES_NOT_BINDABLE` conserva `details.violations` con `reference_kind`,
|
|
449
|
+
`reference_id`, `reason` y `human_message`: nombra cada referencia en lugar de
|
|
450
|
+
devolver un error HTTP genérico.
|
|
451
|
+
|
|
452
|
+
Los tres desenlaces inciertos no son el mismo: exit 9 significa que el CLI **no
|
|
453
|
+
sabe** si la fila existe; exit 10 significa que sí existe —se observó un 2xx— y
|
|
454
|
+
no se pudo leer. Exit 10 devuelve `details` con `invoice_id`,
|
|
455
|
+
`create_http_status`, `readback_error_code`, el `verify_command` a ejecutar y un
|
|
456
|
+
`readback_guidance` que distingue el fallo pasajero —repite el `verify_command`—
|
|
457
|
+
del permanente: un `readback_error_code` de `ACCESS_DENIED` significa que la
|
|
458
|
+
credencial no puede leer facturas y necesita `.view`.
|
|
459
|
+
|
|
460
|
+
Ninguna rama reintenta la escritura: como máximo una solicitud POST y una GET.
|
|
461
|
+
El POST no aprueba la factura; la aprobación es un flujo aparte.
|
|
462
|
+
|
|
463
|
+
## Salida para scripts
|
|
464
|
+
|
|
465
|
+
`--json` escribe un único objeto JSON en stdout, también ante errores, y deja
|
|
466
|
+
stderr vacío. No mezcla banners, logs ni prompts. Comprueba siempre el código
|
|
467
|
+
de salida del proceso. `--non-interactive` está disponible en los comandos;
|
|
468
|
+
esta versión no solicita datos mediante prompts.
|
|
469
|
+
|
|
470
|
+
El listado conserva íntegra la respuesta `{data, meta}` dentro del sobre:
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"ok": true,
|
|
475
|
+
"data": {
|
|
476
|
+
"data": [],
|
|
477
|
+
"meta": { "total": 0, "per_page": 20, "current_page": 1 }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Una lista vacía es un éxito. `meta`, las propiedades adicionales y los
|
|
483
|
+
decimales representados como strings se conservan sin recalcular ni convertir
|
|
484
|
+
a números. En `get`, `data` contiene el objeto de factura recibido del servidor.
|
|
485
|
+
|
|
486
|
+
La comprobación de acceso devuelve:
|
|
487
|
+
|
|
488
|
+
```json
|
|
489
|
+
{
|
|
490
|
+
"ok": true,
|
|
491
|
+
"data": {
|
|
492
|
+
"profile": "default",
|
|
493
|
+
"tenant": "019a46ea-4517-70a5-9afe-4b564dc0ab12",
|
|
494
|
+
"access": "verified"
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Un error de autenticación devuelve:
|
|
500
|
+
|
|
501
|
+
```json
|
|
502
|
+
{
|
|
503
|
+
"ok": false,
|
|
504
|
+
"error": {
|
|
505
|
+
"code": "AUTHENTICATION_FAILED",
|
|
506
|
+
"message": "La clave no permite autenticar la solicitud.",
|
|
507
|
+
"http_status": 401
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
`http_status` solo aparece cuando existe una respuesta HTTP. Los mensajes son
|
|
513
|
+
propios: los fallos de schema añaden `details.violations` con ubicación de entrada,
|
|
514
|
+
ruta del schema y keyword (máximo 20). No incluyen rutas de instancia ni nombres
|
|
515
|
+
de propiedades desconocidas del payload. No incluyen claves, cabeceras, cuerpos remotos, destinos de
|
|
516
|
+
redirección ni stack traces. Sin `--json`, el resultado legible va a stdout y
|
|
517
|
+
los errores sanitizados a stderr.
|
|
518
|
+
|
|
519
|
+
| Exit | Significado | Código de error |
|
|
520
|
+
| ---- | ------------------------------------------ | ---------------------------------------------------------------------------------- |
|
|
521
|
+
| `0` | Éxito, incluida una lista vacía | — |
|
|
522
|
+
| `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) |
|
|
523
|
+
| `3` | HTTP 401 | `AUTHENTICATION_FAILED` |
|
|
524
|
+
| `4` | HTTP 403 | `ACCESS_DENIED` |
|
|
525
|
+
| `5` | HTTP 404 | `NOT_FOUND` |
|
|
526
|
+
| `6` | Fallo de transporte o timeout | `TRANSPORT_ERROR` |
|
|
527
|
+
| `7` | Respuesta de lectura inválida o redirección | `INVALID_RESPONSE` o `REDIRECT_REJECTED` |
|
|
528
|
+
| `8` | Otro error HTTP, incluidos 400/422/429/5xx | `HTTP_ERROR`; en creación de negocio: `CREATE_REJECTED` o `REFERENCES_NOT_BINDABLE` |
|
|
529
|
+
| `9` | Resultado de escritura incierto | `WRITE_OUTCOME_UNKNOWN` |
|
|
530
|
+
| `10` | Escritura confirmada y verificación fallida | `CREATE_NOT_VERIFIED` |
|
|
531
|
+
| `1` | Error interno sanitizado | `INTERNAL_ERROR` |
|
|
532
|
+
|
|
533
|
+
## Limitaciones conocidas
|
|
534
|
+
|
|
535
|
+
- No hay búsqueda de texto en el servidor: `--search` filtra en el cliente y lo
|
|
536
|
+
declara con `search_scope`, `pages_scanned` y `truncated`.
|
|
537
|
+
- `emission_date` se publica como `YYYY-MM-DD`. La regla del validador es más
|
|
538
|
+
laxa; el contrato se estrechó a propósito para que el valor enviado coincida
|
|
539
|
+
con el almacenado y devuelto.
|
|
540
|
+
- Las cadenas decimales rechazan notación exponencial, signo inicial y espacios,
|
|
541
|
+
que PHP aceptaría. La rama numérica sí los admite cuando el JSON los permite.
|
|
542
|
+
- Los `items[].id` los asigna el servidor. Un `items[].id` enviado se descarta
|
|
543
|
+
sin aviso, por lo que solo el `id` de la factura sirve para verificar.
|
|
544
|
+
- Siete de las nueve consultas lookup no tienen cobertura automatizada en el
|
|
545
|
+
backend. El piloto es su única evidencia.
|
|
546
|
+
- No hay auto-aprobación, confirmaciones ni reintentos en ninguna operación.
|
|
547
|
+
|
|
548
|
+
## Ayuda y verificación local
|
|
549
|
+
|
|
550
|
+
La ayuda funciona sin perfil ni acceso a la red:
|
|
551
|
+
|
|
552
|
+
```sh
|
|
553
|
+
backoffice --help
|
|
554
|
+
backoffice profile --help
|
|
555
|
+
backoffice profile check --help
|
|
556
|
+
backoffice purchases --help
|
|
557
|
+
backoffice purchases invoice --help
|
|
558
|
+
backoffice purchases invoice list --help
|
|
559
|
+
backoffice purchases invoice get --help
|
|
560
|
+
backoffice purchases invoice create --help
|
|
561
|
+
backoffice purchases invoice references --help
|
|
562
|
+
backoffice purchases invoice reference --help
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Desde `apps/backoffice-cli`:
|
|
566
|
+
|
|
567
|
+
```sh
|
|
568
|
+
npm test
|
|
569
|
+
npm run lint
|
|
570
|
+
npm run check-types
|
|
571
|
+
npm run build
|
|
572
|
+
npm run test:pack
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
`npm run test:pack -- --catalog-growth` añade una operación de ejemplo únicamente
|
|
576
|
+
a una copia temporal del contrato y del paquete para comprobar que las pruebas
|
|
577
|
+
aceptan crecimiento legítimo cuando coinciden los conjuntos de fuente, catálogo
|
|
578
|
+
y CLI instalada. La ejecución normal verifica el contrato real sin modificarlo.
|
|
579
|
+
|
|
580
|
+
`test:pack` genera e instala el tarball real en un directorio temporal fuera
|
|
581
|
+
del repositorio. Ejecuta el bin instalado, comprueba ayuda offline, perfiles,
|
|
582
|
+
consultas, decimales, paginación y errores contra servidores HTTP locales, y
|
|
583
|
+
verifica que el destino de una redirección no reciba solicitudes. La
|
|
584
|
+
instalación de dependencias puede consultar npm; las pruebas de API no usan
|
|
585
|
+
datos ni credenciales de producción. Los procesos, servidores y archivos
|
|
586
|
+
temporales se limpian al terminar.
|
|
587
|
+
|
|
588
|
+
Desde `packages/backoffice-api`, `npm run generate` genera `schema.d.ts`,
|
|
589
|
+
`catalog.ts` y `checksums.json` desde `storage/api-docs/api-docs.json` y los
|
|
590
|
+
metadatos curados. La transformación pura ordena mapas/operaciones, preserva
|
|
591
|
+
arrays y calcula hashes canónicos de fuente, metadatos y catálogo.
|
|
592
|
+
`npm run generate:check` compara todos los artefactos sin reescribirlos y falla
|
|
593
|
+
si falta uno o hay drift. Las anotaciones PHP se regeneran con `make generate-docs`.
|
|
594
|
+
Backoffice CLI Checks ya ejecuta la comprobación de generación en CI.
|
|
595
|
+
|
|
596
|
+
Los tipos se generan desde el OpenAPI del ERP. Este cliente conserva propiedades
|
|
597
|
+
adicionales en las respuestas. El contrato de creación publica las mismas reglas
|
|
598
|
+
que valida el controlador —uuid, longitudes, mínimo de líneas y decimales como
|
|
599
|
+
número o cadena—; una prueba de arquitectura compara ambos lados campo por campo.
|
|
600
|
+
El smoke del paquete usa servidores locales y no sustituye una prueba contra la
|
|
601
|
+
API Laravel real.
|
package/bin/run.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from "../dist/index.js";
|
|
3
|
+
|
|
4
|
+
process.exitCode = await runCli(process.argv.slice(2), import.meta.url, {
|
|
5
|
+
stdout: (text) => {
|
|
6
|
+
process.stdout.write(text);
|
|
7
|
+
},
|
|
8
|
+
stderr: (text) => {
|
|
9
|
+
process.stderr.write(text);
|
|
10
|
+
},
|
|
11
|
+
});
|