@pimia/sdk 0.20.0 → 0.27.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.
@@ -0,0 +1,257 @@
1
+ import { NotAuthenticatedError, PimiaApiError, RateLimitError } from './errors.js';
2
+ export class PimiaCentralClient {
3
+ baseUrl;
4
+ token;
5
+ doFetch;
6
+ extraHeaders;
7
+ constructor(options) {
8
+ this.baseUrl = options.baseUrl.replace(/\/+$/, '');
9
+ this.token = options.token;
10
+ this.doFetch = options.fetch ?? globalThis.fetch;
11
+ this.extraHeaders = options.headers ?? {};
12
+ }
13
+ // ── La cartera y la salud de la integración (habilidad `desarrollador`) ──
14
+ /** `GET /desarrollador/overview`: la cartera, con la atribución de cada alta. */
15
+ overview() {
16
+ return this.request('/desarrollador/overview');
17
+ }
18
+ /** `GET /desarrollador/salud`: clients, webhooks y propuestas, solo los propios. */
19
+ salud() {
20
+ return this.request('/desarrollador/salud');
21
+ }
22
+ /** `GET /desarrollador/facturacion`: lo que el integrador paga a Pimia (canal y asientos). */
23
+ facturacion() {
24
+ return this.request('/desarrollador/facturacion');
25
+ }
26
+ // ── El vínculo con cada cliente (habilidad `desarrollador`) ─────────────
27
+ get links() {
28
+ return {
29
+ list: () => this.request('/desarrollador/links'),
30
+ /** Un código `DEV-XXXX-XXXX` que el cliente teclea en su instancia. */
31
+ generateCode: () => this.request('/desarrollador/links/generate-code', {
32
+ method: 'POST',
33
+ }),
34
+ accept: (id) => this.request(`/desarrollador/links/${id}/accept`, {
35
+ method: 'POST',
36
+ }),
37
+ reject: (id) => this.request(`/desarrollador/links/${id}/reject`, {
38
+ method: 'POST',
39
+ }),
40
+ };
41
+ }
42
+ // ── Sus clients OAuth (habilidad `desarrollador`) ───────────────────────
43
+ get clients() {
44
+ return {
45
+ list: () => this.request('/desarrollador/clients'),
46
+ /**
47
+ * Reclamar un client confidencial CON su secreto: la única prueba de
48
+ * propiedad que el núcleo acepta (RFC 7591). El secreto no se guarda.
49
+ */
50
+ claim: (body) => this.request('/desarrollador/clients/claim', {
51
+ method: 'POST',
52
+ body,
53
+ }),
54
+ };
55
+ }
56
+ // ── Su catálogo: qué revende, a cuánto y a dónde manda a contratar ──────
57
+ // (habilidad `desarrollador`; punto 12 de DECISIONES.md, regla 4)
58
+ get catalogo() {
59
+ return {
60
+ /**
61
+ * `GET /desarrollador/catalogo`: el catálogo propio (`perfil`, `currency`,
62
+ * `items`) y lo que se puede revender (`disponibles`: Pimia base, los
63
+ * módulos opcionales ofrecidos y las apps integradas activas).
64
+ */
65
+ get: () => this.request('/desarrollador/catalogo'),
66
+ /**
67
+ * `PUT /desarrollador/catalogo`: reemplaza el catálogo ENTERO. Es lo que
68
+ * el cliente del integrador ve en la pantalla de plan de su instancia en
69
+ * vez de los precios de Pimia; el precio es minorista y no toca el
70
+ * dinero de Pimia.
71
+ */
72
+ replace: (body) => this.request('/desarrollador/catalogo', {
73
+ method: 'PUT',
74
+ body,
75
+ }),
76
+ };
77
+ }
78
+ // ── La activación mayorista: lo que activa a cada cliente y paga en su ──
79
+ // canal (habilidad `desarrollador`; regla 4 del punto 12)
80
+ get activaciones() {
81
+ return {
82
+ /**
83
+ * `GET /desarrollador/tenants/{slug}/activaciones`: la base (su asiento),
84
+ * los módulos y apps activos y lo que le cuestan al integrador al mes.
85
+ */
86
+ list: (tenantSlug) => this.request(`/desarrollador/tenants/${encodeURIComponent(tenantSlug)}/activaciones`),
87
+ /**
88
+ * `POST /desarrollador/tenants/{slug}/activaciones`: activar la base, un
89
+ * módulo o una app. La base es el asiento (la primera vez devuelve
90
+ * `checkout_url`); un módulo o una app exigen la base viva y se cobran
91
+ * al integrador como partida de su canal, sin prorrateo, en la factura
92
+ * del mes. Idempotente (`already_active`). Es lo que llama el webhook del
93
+ * integrador cuando su cliente le compra algo.
94
+ */
95
+ activate: (tenantSlug, body) => this.request(`/desarrollador/tenants/${encodeURIComponent(tenantSlug)}/activaciones`, { method: 'POST', body }),
96
+ /**
97
+ * `DELETE /desarrollador/tenants/{slug}/activaciones/{kind}/{item}`: dar de
98
+ * baja. El módulo se apaga y deja de cobrarse; la app se desinstala de
99
+ * todas las empresas; la base suelta el asiento.
100
+ */
101
+ deactivate: (tenantSlug, kind, item) => this.request(`/desarrollador/tenants/${encodeURIComponent(tenantSlug)}/activaciones/${kind}/${encodeURIComponent(item)}`, { method: 'DELETE' }),
102
+ };
103
+ }
104
+ // ── Su login: el nombre que sirve en su servidor y reenvía al AS del ──
105
+ // ápice (habilidad `desarrollador`; regla 5 del punto 12, revisada el
106
+ // 2026-09-07)
107
+ get dominios() {
108
+ return {
109
+ /** `GET /desarrollador/dominios`: sus nombres de login, con el `upstream` y el bloque de proxy de cada uno. */
110
+ list: () => this.request('/desarrollador/dominios'),
111
+ /**
112
+ * `POST /desarrollador/dominios`: declarar el nombre público que el
113
+ * integrador sirve (`host`, p. ej. `login.erpstudio.es`) y su etiqueta
114
+ * interna (`slug`). Pimia no emite certificados ni toca DNS: devuelve
115
+ * `upstream` (`https://login-<slug>.<central>`), a donde su proxy tiene
116
+ * que reenviar con `Host` interno y el nombre público en
117
+ * `X-Forwarded-Host`, y `proxy`, el bloque de Caddy listo para pegar.
118
+ */
119
+ declare: (body) => this.request('/desarrollador/dominios', {
120
+ method: 'POST',
121
+ body,
122
+ }),
123
+ /** `DELETE /desarrollador/dominios/{slug}`: retirar el nombre; el host interno deja de contestar. */
124
+ remove: (slug) => this.request(`/desarrollador/dominios/${encodeURIComponent(slug)}`, { method: 'DELETE' }),
125
+ };
126
+ }
127
+ // ── Sus tokens de máquina (habilidad `desarrollador`; A3 cerrada) ───────
128
+ get tokens() {
129
+ return {
130
+ /** `GET /desarrollador/tokens`: los tokens de máquina vivos (nunca los de una sesión del panel). */
131
+ list: () => this.request('/desarrollador/tokens'),
132
+ /**
133
+ * `POST /desarrollador/tokens`: acuñar un token de máquina, acotado a la
134
+ * habilidad `desarrollador` y nada más. El token en claro (`data.token`)
135
+ * se devuelve UNA vez: guárdalo en tu servidor.
136
+ */
137
+ create: (body) => this.request('/desarrollador/tokens', {
138
+ method: 'POST',
139
+ body,
140
+ }),
141
+ /** `DELETE /desarrollador/tokens/{id}`: revocarlo. */
142
+ revoke: (id) => this.request(`/desarrollador/tokens/${id}`, {
143
+ method: 'DELETE',
144
+ }),
145
+ };
146
+ }
147
+ // ── Lo que hace en el grupo compartido (habilidad `central`) ────────────
148
+ get invitations() {
149
+ return {
150
+ list: () => this.request('/tenant-invitations'),
151
+ /** El cliente se registra y nace dueño; `billing` dice quién paga. */
152
+ create: (body) => this.request('/tenant-invitations', { method: 'POST', body }),
153
+ revoke: (id) => this.request(`/tenant-invitations/${id}`, {
154
+ method: 'DELETE',
155
+ }),
156
+ };
157
+ }
158
+ get billing() {
159
+ return {
160
+ /**
161
+ * `POST /billing/portal` (1.4.0): la URL del portal de Stripe de la
162
+ * cuenta del integrador —las facturas del canal y el método de pago
163
+ * viven allí, no en Pimia—. 404 si la cuenta no tiene cliente de Stripe
164
+ * todavía (nunca patrocinó a nadie).
165
+ */
166
+ portal: (body = {}) => this.request('/billing/portal', { method: 'POST', body }),
167
+ };
168
+ }
169
+ get sponsorship() {
170
+ return {
171
+ /** Asumir la licencia de un cliente: un asiento más en el plan de canal. */
172
+ sponsor: (body) => this.request('/billing/sponsorship', { method: 'POST', body }),
173
+ /** Soltar un cliente patrocinado (queda en gracia y puede rescatarse). */
174
+ release: (tenantSlug) => this.request('/billing/sponsorship', {
175
+ method: 'DELETE',
176
+ body: { tenant_slug: tenantSlug },
177
+ }),
178
+ };
179
+ }
180
+ get tenants() {
181
+ return {
182
+ /**
183
+ * `GET /tenants/{slug}/users` (1.4.0): quién pertenece a la instancia
184
+ * —nombre, correo, rol, `is_owner`—. Es a quién se le puede traspasar:
185
+ * `transferOwnership` pide el `user_id` de alguien que ya está dentro.
186
+ */
187
+ users: (slug) => this.request(`/tenants/${encodeURIComponent(slug)}/users`),
188
+ /** Traspasar la propiedad de la instancia a un usuario de la misma, antes de entregarla. */
189
+ transferOwnership: (slug, body) => this.request(`/tenants/${encodeURIComponent(slug)}/transfer-ownership`, { method: 'POST', body }),
190
+ };
191
+ }
192
+ // ── Transporte ──────────────────────────────────────────────────────────
193
+ async request(path, options = {}) {
194
+ const { data } = await this.requestWithMeta(path, options);
195
+ return data;
196
+ }
197
+ async requestWithMeta(path, options = {}) {
198
+ const token = typeof this.token === 'function' ? await this.token() : this.token;
199
+ if (!token) {
200
+ throw new NotAuthenticatedError('No hay token personal: acuña uno en la cuenta de desarrollador antes de llamar al plano central.');
201
+ }
202
+ const response = await this.doFetch(this.urlFor(path, options.query), {
203
+ method: options.method ?? 'GET',
204
+ headers: {
205
+ accept: 'application/json',
206
+ ...(options.body === undefined ? {} : { 'content-type': 'application/json' }),
207
+ ...this.extraHeaders,
208
+ ...options.headers,
209
+ authorization: `Bearer ${token}`,
210
+ },
211
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
212
+ signal: options.signal,
213
+ });
214
+ const requestId = response.headers.get('x-request-id') ?? undefined;
215
+ const body = await parseBody(response);
216
+ if (response.ok) {
217
+ return { data: body, meta: { status: response.status, requestId } };
218
+ }
219
+ if (response.status === 429) {
220
+ throw new RateLimitError(retryAfterSeconds(response), 429, 'Rate limit alcanzado', body, requestId);
221
+ }
222
+ throw PimiaApiError.from(response.status, body, requestId);
223
+ }
224
+ urlFor(path, query) {
225
+ const clean = path.replace(/^\/+/, '').replace(/^api\/?/, '');
226
+ const url = new URL(`${this.baseUrl}/api/${clean}`);
227
+ for (const [key, value] of Object.entries(query ?? {})) {
228
+ if (value === undefined || value === null)
229
+ continue;
230
+ if (Array.isArray(value)) {
231
+ for (const item of value)
232
+ url.searchParams.append(`${key}[]`, String(item));
233
+ continue;
234
+ }
235
+ url.searchParams.set(key, String(value));
236
+ }
237
+ return url.toString();
238
+ }
239
+ }
240
+ async function parseBody(response) {
241
+ const text = await response.text();
242
+ if (text === '')
243
+ return null;
244
+ try {
245
+ return JSON.parse(text);
246
+ }
247
+ catch {
248
+ return text;
249
+ }
250
+ }
251
+ function retryAfterSeconds(response) {
252
+ const raw = response.headers.get('retry-after');
253
+ if (raw === null)
254
+ return undefined;
255
+ const seconds = Number(raw);
256
+ return Number.isFinite(seconds) ? seconds : undefined;
257
+ }
package/dist/client.d.ts CHANGED
@@ -54,6 +54,33 @@ export type WarehouseRequest = Schemas['WarehouseRequest'];
54
54
  * null no emiten nada al confirmar.
55
55
  */
56
56
  export type StockCountRequest = Schemas['StockCountRequest'];
57
+ /**
58
+ * Cuerpo del alta de una oportunidad: **a quién va dirigida**, y nada más.
59
+ *
60
+ * ⛔ La etapa, la probabilidad y el importe esperado son del CRM que llama —
61
+ * Pimia no los guarda—, así que mandarlos es un 422. Y está bien que lo sea: el
62
+ * día que los aceptara callando, habría dos sitios donde vive el embudo.
63
+ */
64
+ export interface OpportunityRequest {
65
+ name: string;
66
+ contact_name?: string | null;
67
+ email?: string | null;
68
+ phone?: string | null;
69
+ }
70
+ /**
71
+ * Una oportunidad recién creada.
72
+ *
73
+ * ⚠️ **No sale del spec**: `POST /opportunities` es más nueva que la última
74
+ * sincronización del contrato (galeote/factSaas#805), así que el generador no
75
+ * la conoce todavía. Lo único que este tipo promete es `id`, que es lo que hace
76
+ * falta para colgarle presupuestos después; el resto llega y no se nombra.
77
+ * Cuando la ruta entre en el spec, esto pasará a salir de `Ok<…>` como los
78
+ * demás.
79
+ */
80
+ export interface OpportunityResource {
81
+ id: number;
82
+ [key: string]: unknown;
83
+ }
57
84
  /**
58
85
  * El sobre `{ data: … }` de Laravel para las escrituras que el spec **no
59
86
  * tipa**.
@@ -85,6 +112,32 @@ export interface PimiaClientOptions extends OAuthConfig {
85
112
  /** Cabeceras añadidas a cada petición (p. ej. un User-Agent propio). */
86
113
  headers?: Record<string, string>;
87
114
  }
115
+ /**
116
+ * Lo que hace falta para un cliente que **reenvía el token de otro**.
117
+ *
118
+ * Sin `clientId`, sin `clientSecret`, sin `redirectUri` y sin `tokens`: no hay
119
+ * ceremonia OAuth que hacer ni nada tuyo que persistir, porque el grant no es
120
+ * tuyo. Ver {@link PimiaClient.withBorrowedToken}.
121
+ */
122
+ export interface BorrowedTokenOptions {
123
+ /** Base del tenant, con o sin barra final: `https://acme.pimia.es`. */
124
+ baseUrl: string;
125
+ /** El bearer que te llegó, tal cual. */
126
+ accessToken: string;
127
+ fetch?: typeof globalThis.fetch;
128
+ /** Cabeceras fijas de cada llamada (p. ej. la `company` activa). */
129
+ headers?: Record<string, string>;
130
+ /**
131
+ * Reintentos ante 429 (default 2).
132
+ *
133
+ * ⚠️ Ponlo a **0** si atiendes una petición HTTP de un usuario que está
134
+ * esperando: los reintentos ESPERAN, y esperar 30 s dentro de una petición
135
+ * web es una petición colgada y un proceso ocupado.
136
+ */
137
+ maxRateLimitRetries?: number;
138
+ /** Espera máxima por reintento de 429, en ms (default 30 000). */
139
+ maxRetryDelayMs?: number;
140
+ }
88
141
  export interface RequestOptions {
89
142
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
90
143
  /** Query string. Los `undefined`/`null` se omiten; los arrays se repiten. */
@@ -186,7 +239,14 @@ export type WriteOptions = Pick<RequestOptions, 'headers' | 'query' | 'signal' |
186
239
  */
187
240
  export type ReadOptions = Pick<RequestOptions, 'headers' | 'signal'>;
188
241
  export declare class PimiaClient {
189
- readonly oauth: OAuth;
242
+ /**
243
+ * La ceremonia OAuth, o `null` si este cliente no tiene grant propio
244
+ * ({@link PimiaClient.withBorrowedToken}). Es `null` y no un objeto a medias
245
+ * a propósito: un `OAuth` sin `clientId` compondría una URL de autorización
246
+ * con `client_id=` vacío y el fallo aparecería en el navegador del usuario,
247
+ * lejos de aquí.
248
+ */
249
+ readonly oauth: OAuth | null;
190
250
  private readonly baseUrl;
191
251
  private readonly doFetch;
192
252
  private readonly store;
@@ -198,6 +258,43 @@ export declare class PimiaClient {
198
258
  private refreshing;
199
259
  private lastRateLimit;
200
260
  constructor(options: PimiaClientOptions);
261
+ /**
262
+ * Un cliente que **reenvía el token de otro**, sin identidad propia.
263
+ *
264
+ * ── Cuándo es esto lo correcto ─────────────────────────────────────────────
265
+ *
266
+ * Cuando tu servicio se sienta DELANTE de un usuario que ya entró en Pimia:
267
+ * el front te manda su `Authorization` y tú lo reenvías. No necesitas
268
+ * `clientId`, ni `clientSecret`, ni `redirectUri`, ni un `TokenStore` —no hay
269
+ * nada tuyo que guardar— y ganas la propiedad que hace esto seguro: **Pimia
270
+ * sigue decidiendo los permisos**. Tu servicio no puede darle a nadie más de
271
+ * lo que su token ya le daba, así que no hay una credencial de servicio que
272
+ * auditar aparte.
273
+ *
274
+ * ```ts
275
+ * const pimia = PimiaClient.withBorrowedToken({
276
+ * baseUrl: `https://${tenant}.pimia.es`,
277
+ * accessToken: bearerDeQuienLlama,
278
+ * // La empresa activa viaja en cabecera, como en todo el API. OMÍTELA
279
+ * // cuando no la sepas: `company:` vacía es una cabecera presente que no
280
+ * // casa con ninguna empresa.
281
+ * headers: empresa === null ? {} : { company: String(empresa) },
282
+ * // Atiendes una petición web: no esperes dentro de ella.
283
+ * maxRateLimitRetries: 0,
284
+ * })
285
+ *
286
+ * await pimia.bootstrap.currentCompanyId()
287
+ * ```
288
+ *
289
+ * ⚠️ **El token vive lo que viva la petición que lo trajo.** Construye uno por
290
+ * petición y no compartas la instancia: un cliente compartido es una
291
+ * credencial compartida, y aquí la credencial es de un usuario concreto.
292
+ *
293
+ * ⚠️ Un token prestado **no se refresca**: cuando caduca, el 401 sube como
294
+ * {@link UnauthorizedError} y quien tiene que conseguir otro es quien te lo
295
+ * prestó.
296
+ */
297
+ static withBorrowedToken(options: BorrowedTokenOptions): PimiaClient;
201
298
  /** Cabeceras `X-RateLimit-*` de la última respuesta. */
202
299
  get rateLimit(): RateLimit;
203
300
  get invoices(): {
@@ -1085,6 +1182,169 @@ export declare class PimiaClient {
1085
1182
  data: components["schemas"]["StockMovementResource"];
1086
1183
  }>;
1087
1184
  };
1185
+ /**
1186
+ * Oportunidades: **a quién va dirigido** un presupuesto.
1187
+ *
1188
+ * `estimates.opportunity_id` es el enlace transparente del núcleo —funciona
1189
+ * venga el CRM de donde venga—, y es lo que permite preguntar «los
1190
+ * presupuestos de este trato» sin que el trato viva en Pimia. Pero hasta el
1191
+ * 2026-09-08 una oportunidad **sólo podía nacer dentro de un
1192
+ * `POST /estimates`**, así que un CRM de fuera no tenía forma de estrenar una
1193
+ * al dar de alta un lead: habría tenido que fabricar un presupuesto borrador y
1194
+ * quemar un número de la serie del cliente por cada lead. Un lead no es una
1195
+ * oferta.
1196
+ *
1197
+ * No estrena scope: cuelga de `estimates:write`, porque la oportunidad es a
1198
+ * quién va dirigido un presupuesto y no una entidad del embudo.
1199
+ *
1200
+ * ⚠️ **Todavía no está en el spec publicado** (galeote/factSaas#805 es más
1201
+ * nueva que la última sincronización del contrato). Contra una instancia
1202
+ * anterior a esa ruta la llamada contesta 404, y eso es lo que hay que mirar
1203
+ * antes de dar por hecho que el token está mal.
1204
+ */
1205
+ get opportunities(): {
1206
+ /**
1207
+ * Estrena una oportunidad. Manda `idempotencyKey` —una clave estable por
1208
+ * lead, del estilo `lead:{id}:opportunity`— y el reintento tras un timeout
1209
+ * no te estrenará una segunda para el mismo trato.
1210
+ */
1211
+ create: (body: OpportunityRequest, options?: WriteOptions) => Promise<ResourceEnvelope<OpportunityResource>>;
1212
+ };
1213
+ /**
1214
+ * Lo que el CRM de Pimia publica para que OTRO CRM pueda sustituirlo.
1215
+ *
1216
+ * No son los leads —ésos los sirve `/crm/leads` y un integrador que trae su
1217
+ * propio embudo no los usa—: es lo que un CRM sustituto necesita del núcleo
1218
+ * aunque se haya llevado el embudo a su casa.
1219
+ */
1220
+ get crm(): {
1221
+ /**
1222
+ * Las personas a las que se les puede asignar una tarea o un lead.
1223
+ *
1224
+ * **Reenvía lo que conteste**, campos de más incluidos. Recortarlo tú es
1225
+ * aplicar dos veces la misma política desde dos sitios que pueden
1226
+ * divergir: Pimia esconde aquí a los superadmin de la plataforma y a la
1227
+ * gestoría dueña del tenant, y recorta cada fila a `id` y `name`. Si
1228
+ * mañana añade un campo para desempatar dos nombres iguales, tu copia lo
1229
+ * borraría sin que nadie entendiera por qué.
1230
+ *
1231
+ * ⚠️ El scope: el contrato publicado la cobra con `crm:read`, pero el
1232
+ * núcleo la abrió el 2026-09-08 a cualquier token válido de la empresa
1233
+ * —precisamente para que un integrador que SUSTITUYE el CRM no tenga que
1234
+ * pedir el scope del CRM que ya no usa—. Contra una instancia anterior a
1235
+ * ese cambio sigue haciendo falta `crm:read`.
1236
+ */
1237
+ assignableUsers: (options?: ReadOptions) => Promise<{
1238
+ data: {
1239
+ id: number;
1240
+ name: string;
1241
+ }[];
1242
+ }>;
1243
+ };
1244
+ /**
1245
+ * El arranque de la sesión: en qué empresa trabaja este token y con qué
1246
+ * moneda.
1247
+ *
1248
+ * ⛔ **`/bootstrap` NO envuelve en `data`.** Todo lo demás en el API contesta
1249
+ * `{ data: … }`; ésta no: sus claves cuelgan de la raíz. Un desenvolvedor de
1250
+ * `data` escrito «para todas las llamadas» no encuentra nada aquí y devuelve
1251
+ * vacío **sin error**, así que el fallo no se ve como un fallo: se ve como una
1252
+ * empresa sin resolver o como una moneda que cae al respaldo. Medido
1253
+ * construyendo el CRM de la vertical, que tuvo que anotarlo en su código y en
1254
+ * el arnés de sus tests.
1255
+ *
1256
+ * Lectura libre: la alcanza cualquier token válido, **sin scope** y sin
1257
+ * consentimiento adicional del dueño del tenant.
1258
+ *
1259
+ * ⚠️ Cada método hace SU llamada: no hay caché. Es a propósito —el cliente no
1260
+ * sabe cuánto vive una sesión tuya, y una empresa cacheada de más es una fila
1261
+ * escrita en la empresa equivocada—, así que si necesitas las dos cosas en la
1262
+ * misma petición, llama a `get()` una vez y léelas del objeto.
1263
+ */
1264
+ get bootstrap(): {
1265
+ /** El arranque entero, sin envolver. */
1266
+ get: (options?: ReadOptions) => Promise<{
1267
+ current_user: components["schemas"]["UserResource"];
1268
+ current_user_settings: unknown[];
1269
+ current_user_abilities: string[] | Record<string, never>;
1270
+ companies: components["schemas"]["CompanyResource"][];
1271
+ current_company: components["schemas"]["CompanyResource"];
1272
+ current_company_settings: string[];
1273
+ current_company_currency: components["schemas"]["Currency"] | null;
1274
+ config: string;
1275
+ global_settings: string[];
1276
+ main_menu: {
1277
+ title: string | "";
1278
+ link: string | "";
1279
+ icon: string | "";
1280
+ name: string | "";
1281
+ group: string | "";
1282
+ }[] | {
1283
+ title: string;
1284
+ link: string;
1285
+ icon: string;
1286
+ name: string;
1287
+ group: string;
1288
+ }[];
1289
+ setting_menu: {
1290
+ title: string | "";
1291
+ link: string | "";
1292
+ icon: string | "";
1293
+ name: string | "";
1294
+ group: string | "";
1295
+ }[] | {
1296
+ title: string;
1297
+ link: string;
1298
+ icon: string;
1299
+ name: string;
1300
+ group: string;
1301
+ }[];
1302
+ modules: unknown[];
1303
+ installed_modules: string;
1304
+ }>;
1305
+ /**
1306
+ * En qué empresa trabaja ESTA petición, según el núcleo.
1307
+ *
1308
+ * ⛔ No es «la primera empresa del usuario», aunque hoy coincidan. Pimia
1309
+ * resuelve `current_company` con el mismo camino y el mismo respaldo que
1310
+ * usa su middleware de empresa para servir cualquier otra llamada tuya —la
1311
+ * cabecera `company` si vale, y si no la primera del usuario—, así que
1312
+ * preguntarlo aquí es la única forma de que tu lado y el suyo no puedan
1313
+ * discrepar. Deducirlo de la lista de `/me` reproduce la regla en un
1314
+ * segundo sitio, y dos reglas iguales son dos reglas que pueden separarse:
1315
+ * el día que dejaran de coincidir, escribirías con una empresa que Pimia
1316
+ * nunca usó y sin un solo error que lo denuncie.
1317
+ *
1318
+ * `null` si el arranque no la publica. Trátalo como «no se puede servir
1319
+ * esta sesión» y no como un cero: un cero es una empresa que no es de
1320
+ * nadie y que ve cualquiera que también acabe ahí.
1321
+ *
1322
+ * ⚠️ El spec declara `current_company` obligatorio y el tipo generado dice
1323
+ * que siempre está; la comprobación de aquí es de RUNTIME porque se ha
1324
+ * visto llegar sin ella. Cuando eso pasa, lo que hay que devolver es
1325
+ * `null`, no reventar.
1326
+ */
1327
+ currentCompanyId: (options?: ReadOptions) => Promise<number | null>;
1328
+ /**
1329
+ * La moneda de la empresa y su ESCALA.
1330
+ *
1331
+ * ⛔ La moneda no es siempre el euro y los decimales cambian con ella: el
1332
+ * yen tiene 0, el dinar kuwaití 3. Suponer 2 —o peor, multiplicar por 100
1333
+ * a mano— no da un error, da otro resultado: un filtro por importe
1334
+ * devuelve otras filas y un alta guarda una moneda falsa en la ficha. Por
1335
+ * eso la escala se PREGUNTA.
1336
+ *
1337
+ * `null` si el arranque no publica moneda (el campo admite nulo en el
1338
+ * contrato), y ahí el SDK **no se inventa nada**: «EUR con 2 decimales» es
1339
+ * una política de producto, la decide quien llama. Lo que sí se lee a la
1340
+ * defensiva es `precision`, que el contrato declara obligatorio dentro de
1341
+ * la moneda: un cuerpo sin él está roto, no es un caso de negocio.
1342
+ */
1343
+ currency: (options?: ReadOptions) => Promise<{
1344
+ code: string;
1345
+ precision: number;
1346
+ } | null>;
1347
+ };
1088
1348
  get<T = unknown>(path: string, query?: RequestOptions['query'], options?: ReadOptions): Promise<T>;
1089
1349
  post<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
1090
1350
  put<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;