@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.
- package/README.md +80 -0
- package/dist/api.d.ts +1020 -116
- package/dist/central-api.d.ts +1928 -0
- package/dist/central-api.js +9 -0
- package/dist/central.d.ts +607 -0
- package/dist/central.js +257 -0
- package/dist/client.d.ts +261 -1
- package/dist/client.js +212 -2
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +23 -3
- package/dist/index.js +21 -2
- package/dist/tokens.d.ts +45 -0
- package/dist/tokens.js +53 -0
- package/package.json +5 -1
package/dist/central.js
ADDED
|
@@ -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
|
-
|
|
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>;
|