@orvello/sdk 0.1.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/CONTRACT_SHA256 +1 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/contract.cjs +4968 -0
- package/dist/contract.cjs.map +1 -0
- package/dist/contract.d.cts +30468 -0
- package/dist/contract.d.ts +30468 -0
- package/dist/contract.js +4937 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.cjs +748 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +222 -0
- package/dist/index.d.ts +222 -0
- package/dist/index.js +737 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi.json +53764 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
// src/contract-hash.ts
|
|
2
|
+
var CONTRACT_SHA256 = "4efd4e4b669168c9cab5419fa04dd4e26a2029de2d4b10a59918e366189e1072";
|
|
3
|
+
|
|
4
|
+
// src/diagnose.ts
|
|
5
|
+
var SDK_VERSION = "0.1.0" ;
|
|
6
|
+
function normalizeBaseUrl(raw) {
|
|
7
|
+
const trimmed = raw.trim();
|
|
8
|
+
if (!trimmed) throw new Error("Base URL cannot be empty");
|
|
9
|
+
return trimmed.replace(/\/+$/, "");
|
|
10
|
+
}
|
|
11
|
+
async function resolveBaseUrl(options) {
|
|
12
|
+
const provider = options.client?.baseUrl ?? options.baseUrl;
|
|
13
|
+
if (!provider) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Diagnosing requires a target platform: pass { baseUrl } (the services checks its own baseUrl) \u2014 the SDK ships no baked-in default endpoint."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const value = typeof provider === "function" ? await provider() : provider;
|
|
19
|
+
return normalizeBaseUrl(value);
|
|
20
|
+
}
|
|
21
|
+
async function diagnoseOrvello(options = {}) {
|
|
22
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23
|
+
let baseUrl = null;
|
|
24
|
+
try {
|
|
25
|
+
baseUrl = await resolveBaseUrl(options);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
baseUrl: null,
|
|
31
|
+
sdkVersion: SDK_VERSION,
|
|
32
|
+
reachable: false,
|
|
33
|
+
contract: { status: "unknown", sdk: CONTRACT_SHA256, live: null, files: null },
|
|
34
|
+
problems: [`Invalid base URL: ${detail}`],
|
|
35
|
+
checkedAt
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
39
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
40
|
+
const base = {
|
|
41
|
+
ok: false,
|
|
42
|
+
baseUrl,
|
|
43
|
+
sdkVersion: SDK_VERSION,
|
|
44
|
+
reachable: false,
|
|
45
|
+
contract: { status: "unknown", sdk: CONTRACT_SHA256, live: null, files: null },
|
|
46
|
+
problems: [],
|
|
47
|
+
checkedAt
|
|
48
|
+
};
|
|
49
|
+
if (typeof doFetch !== "function") {
|
|
50
|
+
return { ...base, problems: ["No fetch implementation available; pass `fetch` explicitly."] };
|
|
51
|
+
}
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
54
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
55
|
+
if (options.signal?.aborted) controller.abort(options.signal.reason);
|
|
56
|
+
const timer = setTimeout(() => controller.abort(new Error(`Contract probe timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = await doFetch(`${baseUrl}/health/contract`, {
|
|
60
|
+
method: "GET",
|
|
61
|
+
headers: { accept: "application/json" },
|
|
62
|
+
signal: controller.signal
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return { ...base, problems: [`the orvello service at ${baseUrl} could not be reached: ${detail}`] };
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
70
|
+
}
|
|
71
|
+
if (response.status === 404) {
|
|
72
|
+
return {
|
|
73
|
+
...base,
|
|
74
|
+
reachable: true,
|
|
75
|
+
problems: [
|
|
76
|
+
`the orvello service at ${baseUrl} does not serve GET /health/contract, so the contract cannot be verified.`
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
return {
|
|
82
|
+
...base,
|
|
83
|
+
reachable: true,
|
|
84
|
+
problems: [`the orvello service at ${baseUrl} answered ${response.status} for GET /health/contract; contract not verified.`]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
body = await response.json();
|
|
90
|
+
} catch {
|
|
91
|
+
return { ...base, reachable: true, problems: [`the orvello service at ${baseUrl} returned a non-JSON contract fingerprint.`] };
|
|
92
|
+
}
|
|
93
|
+
const live = typeof body.contractSha256 === "string" && body.contractSha256 ? body.contractSha256 : null;
|
|
94
|
+
const files = typeof body.files === "number" ? body.files : null;
|
|
95
|
+
if (!live) {
|
|
96
|
+
return {
|
|
97
|
+
...base,
|
|
98
|
+
reachable: true,
|
|
99
|
+
contract: { ...base.contract, files },
|
|
100
|
+
problems: [`the orvello service at ${baseUrl} could not compute its own contract fingerprint; contract not verified.`]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (live !== CONTRACT_SHA256) {
|
|
104
|
+
return {
|
|
105
|
+
...base,
|
|
106
|
+
reachable: true,
|
|
107
|
+
contract: { status: "drift", sdk: CONTRACT_SHA256, live, files },
|
|
108
|
+
problems: [
|
|
109
|
+
`Contract drift: the orvello service at ${baseUrl} speaks ${live.slice(0, 12)}, @orvello/sdk ${SDK_VERSION} was built against ${CONTRACT_SHA256.slice(0, 12)}. Pin a newer SDK version, or deploy the orvello service the SDK was built from.`
|
|
110
|
+
]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
ok: true,
|
|
116
|
+
reachable: true,
|
|
117
|
+
contract: { status: "match", sdk: CONTRACT_SHA256, live, files }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/helpers.ts
|
|
122
|
+
function buildBase64Receipt(data, fileName, mimeType) {
|
|
123
|
+
let base64String;
|
|
124
|
+
let size;
|
|
125
|
+
if (typeof data === "string") {
|
|
126
|
+
base64String = data.startsWith("data:") ? data.split(",")[1] || data : data;
|
|
127
|
+
size = Math.round(base64String.length * 3 / 4);
|
|
128
|
+
} else {
|
|
129
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
130
|
+
size = bytes.byteLength;
|
|
131
|
+
if (typeof Buffer !== "undefined") {
|
|
132
|
+
base64String = Buffer.from(bytes).toString("base64");
|
|
133
|
+
} else {
|
|
134
|
+
let binary = "";
|
|
135
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
136
|
+
binary += String.fromCharCode(bytes[i] ?? 0);
|
|
137
|
+
}
|
|
138
|
+
base64String = globalThis.btoa(binary);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
base64String,
|
|
143
|
+
fileName,
|
|
144
|
+
mimeType,
|
|
145
|
+
size
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function isKleinunternehmer(context) {
|
|
149
|
+
return context.taxation?.defaultVatTreatmentHint === "KLEINUNTERNEHMER_0" || context.taxation?.regime?.toLowerCase() === "kleinunternehmer" || context.taxation?.shouldShowVatOnInvoices === false;
|
|
150
|
+
}
|
|
151
|
+
function calculateVat(netAmount, vatRatePercent) {
|
|
152
|
+
const netNum = typeof netAmount === "string" ? parseFloat(netAmount) : netAmount;
|
|
153
|
+
if (isNaN(netNum)) {
|
|
154
|
+
throw new Error(`Invalid net amount: "${netAmount}"`);
|
|
155
|
+
}
|
|
156
|
+
const vatNum = Math.round(netNum * vatRatePercent) / 100;
|
|
157
|
+
const grossNum = Math.round((netNum + vatNum) * 100) / 100;
|
|
158
|
+
return {
|
|
159
|
+
net: netNum.toFixed(2),
|
|
160
|
+
vat: vatNum.toFixed(2),
|
|
161
|
+
gross: grossNum.toFixed(2)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function formatAmount(amount, currency = "EUR", locale = "de-DE") {
|
|
165
|
+
const num = typeof amount === "string" ? parseFloat(amount) : amount;
|
|
166
|
+
if (isNaN(num)) return String(amount);
|
|
167
|
+
return new Intl.NumberFormat(locale, {
|
|
168
|
+
style: "currency",
|
|
169
|
+
currency
|
|
170
|
+
}).format(num);
|
|
171
|
+
}
|
|
172
|
+
function mapPaymentAccountToBankDetails(account) {
|
|
173
|
+
return {
|
|
174
|
+
id: account.id,
|
|
175
|
+
iban: account.iban || "",
|
|
176
|
+
bic: account.bic || "",
|
|
177
|
+
bank: account.bankName || "",
|
|
178
|
+
accountHolder: account.accountOwner || "",
|
|
179
|
+
currency: account.currency,
|
|
180
|
+
isDefault: account.isDefault
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/client.ts
|
|
185
|
+
var OrvelloApiError = class extends Error {
|
|
186
|
+
constructor(status, statusText, endpoint, body, message) {
|
|
187
|
+
super(message || `orvello request to ${endpoint} failed with status ${status}: ${statusText}`);
|
|
188
|
+
this.status = status;
|
|
189
|
+
this.statusText = statusText;
|
|
190
|
+
this.endpoint = endpoint;
|
|
191
|
+
this.body = body;
|
|
192
|
+
this.name = "OrvelloApiError";
|
|
193
|
+
}
|
|
194
|
+
status;
|
|
195
|
+
statusText;
|
|
196
|
+
endpoint;
|
|
197
|
+
body;
|
|
198
|
+
/** Readable alias for the primary `status` field. */
|
|
199
|
+
get statusCode() {
|
|
200
|
+
return this.status;
|
|
201
|
+
}
|
|
202
|
+
/** Readable alias for the raw `body` payload. */
|
|
203
|
+
get responseBody() {
|
|
204
|
+
return this.body;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var OrvelloClient = class {
|
|
208
|
+
baseUrl;
|
|
209
|
+
auth;
|
|
210
|
+
doFetch;
|
|
211
|
+
timeoutMs;
|
|
212
|
+
maxRetries;
|
|
213
|
+
retryBaseDelayMs;
|
|
214
|
+
cachedOAuthToken = null;
|
|
215
|
+
// Cached default bank account (5 minutes TTL)
|
|
216
|
+
_bankAccountCache = null;
|
|
217
|
+
_bankAccountCacheTime = 0;
|
|
218
|
+
constructor(config = {}) {
|
|
219
|
+
const baseUrl = config.baseUrl ?? this.envBaseUrl();
|
|
220
|
+
if (!baseUrl) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"OrvelloClient requires a target platform: pass { baseUrl } or set the ORVELLO_URL environment variable. The SDK deliberately ships no baked-in default endpoint."
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
this.baseUrl = baseUrl;
|
|
226
|
+
if (config.auth) {
|
|
227
|
+
this.auth = config.auth;
|
|
228
|
+
} else {
|
|
229
|
+
this.auth = this.envAuth();
|
|
230
|
+
}
|
|
231
|
+
this.doFetch = config.fetch || globalThis.fetch;
|
|
232
|
+
this.timeoutMs = config.timeoutMs ?? 15e3;
|
|
233
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
234
|
+
this.retryBaseDelayMs = config.retryBaseDelayMs ?? 500;
|
|
235
|
+
}
|
|
236
|
+
envBaseUrl() {
|
|
237
|
+
if (typeof process === "undefined" || !process?.env) return void 0;
|
|
238
|
+
const url = process.env.ORVELLO_URL;
|
|
239
|
+
return url ? url : void 0;
|
|
240
|
+
}
|
|
241
|
+
envAuth() {
|
|
242
|
+
if (typeof process === "undefined" || !process?.env) return void 0;
|
|
243
|
+
const clientId = process.env.ORVELLO_OAUTH_CLIENT_ID;
|
|
244
|
+
const clientSecret = process.env.ORVELLO_OAUTH_CLIENT_SECRET;
|
|
245
|
+
const apiKey = process.env.ORVELLO_API_KEY;
|
|
246
|
+
const scope = process.env.ORVELLO_OAUTH_SCOPE;
|
|
247
|
+
if (clientId && clientSecret && clientId !== "todo" && clientSecret !== "todo") {
|
|
248
|
+
return { clientId, clientSecret, ...scope ? { scope } : {} };
|
|
249
|
+
}
|
|
250
|
+
if (apiKey) return { apiKey };
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
async resolveBaseUrl() {
|
|
254
|
+
const val = typeof this.baseUrl === "function" ? await this.baseUrl() : this.baseUrl;
|
|
255
|
+
return val.replace(/\/+$/, "");
|
|
256
|
+
}
|
|
257
|
+
async getAuthHeader() {
|
|
258
|
+
if (!this.auth) return null;
|
|
259
|
+
if (typeof this.auth === "string") {
|
|
260
|
+
return this.auth.startsWith("Bearer ") ? this.auth : `Bearer ${this.auth}`;
|
|
261
|
+
}
|
|
262
|
+
if (typeof this.auth === "function") {
|
|
263
|
+
const token = await this.auth();
|
|
264
|
+
return token.startsWith("Bearer ") ? token : `Bearer ${token}`;
|
|
265
|
+
}
|
|
266
|
+
if (typeof this.auth === "object" && "apiKey" in this.auth && typeof this.auth.apiKey === "string") {
|
|
267
|
+
const key = this.auth.apiKey;
|
|
268
|
+
return key.startsWith("Bearer ") ? key : `Bearer ${key}`;
|
|
269
|
+
}
|
|
270
|
+
if (typeof this.auth === "object" && "clientId" in this.auth && "clientSecret" in this.auth) {
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
if (this.cachedOAuthToken && this.cachedOAuthToken.expiresAt > now + 3e4) {
|
|
273
|
+
return `Bearer ${this.cachedOAuthToken.token}`;
|
|
274
|
+
}
|
|
275
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
276
|
+
const tokenEndpoint = this.auth.tokenUrl || `${baseUrl}/oauth/token`;
|
|
277
|
+
const credentials = typeof Buffer !== "undefined" ? Buffer.from(`${this.auth.clientId}:${this.auth.clientSecret}`).toString("base64") : globalThis.btoa(`${this.auth.clientId}:${this.auth.clientSecret}`);
|
|
278
|
+
const bodyParams = new URLSearchParams({
|
|
279
|
+
grant_type: "client_credentials",
|
|
280
|
+
client_id: this.auth.clientId,
|
|
281
|
+
client_secret: this.auth.clientSecret
|
|
282
|
+
});
|
|
283
|
+
if (this.auth.scope) {
|
|
284
|
+
bodyParams.set("scope", this.auth.scope);
|
|
285
|
+
}
|
|
286
|
+
const resp = await this.doFetch(tokenEndpoint, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: {
|
|
289
|
+
"Authorization": `Basic ${credentials}`,
|
|
290
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
291
|
+
},
|
|
292
|
+
body: bodyParams.toString()
|
|
293
|
+
});
|
|
294
|
+
if (!resp.ok) {
|
|
295
|
+
let errBody;
|
|
296
|
+
try {
|
|
297
|
+
errBody = await resp.json();
|
|
298
|
+
} catch {
|
|
299
|
+
errBody = await resp.text().catch(() => "");
|
|
300
|
+
}
|
|
301
|
+
throw new OrvelloApiError(
|
|
302
|
+
resp.status,
|
|
303
|
+
resp.statusText,
|
|
304
|
+
tokenEndpoint,
|
|
305
|
+
errBody,
|
|
306
|
+
"Failed to obtain OAuth2 token from the orvello service"
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
const data = await resp.json();
|
|
310
|
+
const expiresIn = data.expires_in || 3600;
|
|
311
|
+
this.cachedOAuthToken = {
|
|
312
|
+
token: data.access_token,
|
|
313
|
+
expiresAt: now + expiresIn * 1e3
|
|
314
|
+
};
|
|
315
|
+
return `Bearer ${data.access_token}`;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
isRetriable(status, error) {
|
|
320
|
+
if (status !== null) {
|
|
321
|
+
return status >= 500 || status === 429;
|
|
322
|
+
}
|
|
323
|
+
if (error instanceof Error) {
|
|
324
|
+
const msg = error.message.toLowerCase();
|
|
325
|
+
return error.name === "AbortError" || error.name === "TimeoutError" || msg.includes("fetch failed") || msg.includes("network") || msg.includes("econnrefused") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("socket");
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
async request(path, options = {}) {
|
|
330
|
+
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
331
|
+
const timeoutMs = options.timeoutMs ?? this.timeoutMs;
|
|
332
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
333
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
334
|
+
const url = new URL(`${baseUrl}${cleanPath}`);
|
|
335
|
+
if (options.query) {
|
|
336
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
337
|
+
if (v !== void 0 && v !== null) {
|
|
338
|
+
url.searchParams.set(k, String(v));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
let lastError;
|
|
343
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
344
|
+
const controller = new AbortController();
|
|
345
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
346
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
347
|
+
if (options.signal?.aborted) controller.abort(options.signal.reason);
|
|
348
|
+
const timer = setTimeout(
|
|
349
|
+
() => controller.abort(new Error(`Request timed out after ${timeoutMs}ms`)),
|
|
350
|
+
timeoutMs
|
|
351
|
+
);
|
|
352
|
+
try {
|
|
353
|
+
const authHeader = await this.getAuthHeader();
|
|
354
|
+
const headers = new Headers(options.headers);
|
|
355
|
+
if (!headers.has("Accept")) {
|
|
356
|
+
headers.set("Accept", options.rawResponse ? "*/*" : "application/json");
|
|
357
|
+
}
|
|
358
|
+
if (authHeader && !headers.has("Authorization")) {
|
|
359
|
+
headers.set("Authorization", authHeader);
|
|
360
|
+
}
|
|
361
|
+
if (options.idempotencyKey && !headers.has("Idempotency-Key")) {
|
|
362
|
+
headers.set("Idempotency-Key", options.idempotencyKey);
|
|
363
|
+
}
|
|
364
|
+
if (options.body && typeof options.body === "string" && !headers.has("Content-Type")) {
|
|
365
|
+
headers.set("Content-Type", "application/json");
|
|
366
|
+
}
|
|
367
|
+
const resp = await this.doFetch(url.toString(), {
|
|
368
|
+
...options,
|
|
369
|
+
headers,
|
|
370
|
+
signal: controller.signal
|
|
371
|
+
});
|
|
372
|
+
if (!resp.ok) {
|
|
373
|
+
if (resp.status === 401) {
|
|
374
|
+
const hadCachedToken = Boolean(this.cachedOAuthToken);
|
|
375
|
+
this.cachedOAuthToken = null;
|
|
376
|
+
if (hadCachedToken && attempt === 0 && attempt < maxRetries) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let errBody;
|
|
381
|
+
try {
|
|
382
|
+
errBody = await resp.json();
|
|
383
|
+
} catch {
|
|
384
|
+
errBody = await resp.text().catch(() => null);
|
|
385
|
+
}
|
|
386
|
+
const apiMessage = errBody && typeof errBody === "object" && "message" in errBody && typeof errBody.message === "string" ? errBody.message : void 0;
|
|
387
|
+
const error = new OrvelloApiError(resp.status, resp.statusText, cleanPath, errBody, apiMessage);
|
|
388
|
+
if (!this.isRetriable(resp.status, error) || attempt >= maxRetries) {
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
lastError = error;
|
|
392
|
+
} else {
|
|
393
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
394
|
+
if (options.rawResponse || contentType.includes("application/pdf")) {
|
|
395
|
+
return await resp.blob();
|
|
396
|
+
}
|
|
397
|
+
if (contentType.includes("application/json")) {
|
|
398
|
+
const json = await resp.json();
|
|
399
|
+
if (json && typeof json === "object" && "success" in json) {
|
|
400
|
+
const env = json;
|
|
401
|
+
if (env.success === false) {
|
|
402
|
+
throw new OrvelloApiError(resp.status, resp.statusText, cleanPath, json, env.message || "API returned success=false");
|
|
403
|
+
}
|
|
404
|
+
return env.data;
|
|
405
|
+
}
|
|
406
|
+
return json;
|
|
407
|
+
}
|
|
408
|
+
return await resp.text();
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
lastError = err;
|
|
412
|
+
if (err instanceof OrvelloApiError && !this.isRetriable(err.status, err)) {
|
|
413
|
+
throw err;
|
|
414
|
+
}
|
|
415
|
+
if (!this.isRetriable(null, err) || attempt >= maxRetries) {
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
} finally {
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
421
|
+
}
|
|
422
|
+
const jitter = Math.random() * 100;
|
|
423
|
+
const delay = this.retryBaseDelayMs * Math.pow(2, attempt) + jitter;
|
|
424
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
425
|
+
}
|
|
426
|
+
throw lastError;
|
|
427
|
+
}
|
|
428
|
+
// ── Invoices ─────────────────────────────────────────────────────────────
|
|
429
|
+
invoices = {
|
|
430
|
+
search: (query, options) => this.request("/invoices/external", {
|
|
431
|
+
method: "GET",
|
|
432
|
+
query,
|
|
433
|
+
...options
|
|
434
|
+
}),
|
|
435
|
+
getById: (id, options) => this.request(`/invoices/external/${id}`, {
|
|
436
|
+
method: "GET",
|
|
437
|
+
...options
|
|
438
|
+
}),
|
|
439
|
+
getPending: (options) => this.request("/invoices/external/pending", {
|
|
440
|
+
method: "GET",
|
|
441
|
+
...options
|
|
442
|
+
}),
|
|
443
|
+
getOverdue: (options) => this.request("/invoices/external/overdue", {
|
|
444
|
+
method: "GET",
|
|
445
|
+
...options
|
|
446
|
+
}),
|
|
447
|
+
getStatusPolicy: (id, options) => this.request(`/invoices/external/${id}/status-policy`, {
|
|
448
|
+
method: "GET",
|
|
449
|
+
...options
|
|
450
|
+
}),
|
|
451
|
+
create: (body, options) => this.request("/invoices/external", {
|
|
452
|
+
method: "POST",
|
|
453
|
+
body: JSON.stringify(body),
|
|
454
|
+
...options
|
|
455
|
+
}),
|
|
456
|
+
updateStatus: (id, body, options) => this.request(`/invoices/external/${id}/status`, {
|
|
457
|
+
method: "PUT",
|
|
458
|
+
body: JSON.stringify(body),
|
|
459
|
+
...options
|
|
460
|
+
}),
|
|
461
|
+
previewCreditNote: (id, body, options) => this.request(`/invoices/external/${id}/credit-note/preview`, {
|
|
462
|
+
method: "POST",
|
|
463
|
+
body: JSON.stringify(body),
|
|
464
|
+
...options
|
|
465
|
+
}),
|
|
466
|
+
issueCreditNote: (id, body, options) => this.request(`/invoices/external/${id}/credit-note/issue`, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
body: JSON.stringify(body),
|
|
469
|
+
...options
|
|
470
|
+
}),
|
|
471
|
+
delete: (id, options) => this.request(`/invoices/external/${id}`, {
|
|
472
|
+
method: "DELETE",
|
|
473
|
+
...options
|
|
474
|
+
}),
|
|
475
|
+
viewPdf: (id, options) => this.request(`/invoices/external/${id}/pdf`, {
|
|
476
|
+
method: "GET",
|
|
477
|
+
rawResponse: true,
|
|
478
|
+
...options
|
|
479
|
+
}),
|
|
480
|
+
downloadPdf: (id, options) => this.request(`/invoices/external/${id}/download`, {
|
|
481
|
+
method: "GET",
|
|
482
|
+
rawResponse: true,
|
|
483
|
+
...options
|
|
484
|
+
}),
|
|
485
|
+
sendEmail: (id, body, options) => this.request(`/invoices/external/${id}/send-email`, {
|
|
486
|
+
method: "POST",
|
|
487
|
+
body: JSON.stringify(body),
|
|
488
|
+
...options
|
|
489
|
+
}),
|
|
490
|
+
downloadPdfFromUrl: async (pdfUrl, options) => {
|
|
491
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
492
|
+
const targetUrl = pdfUrl.startsWith("http://") || pdfUrl.startsWith("https://") ? pdfUrl : `${baseUrl}${pdfUrl.startsWith("/") ? "" : "/"}${pdfUrl}`;
|
|
493
|
+
const doFetch = this.doFetch;
|
|
494
|
+
const authHeader = await this.getAuthHeader();
|
|
495
|
+
const headers = new Headers(options?.headers);
|
|
496
|
+
if (authHeader && !headers.has("Authorization")) {
|
|
497
|
+
headers.set("Authorization", authHeader);
|
|
498
|
+
}
|
|
499
|
+
const resp = await doFetch(targetUrl, {
|
|
500
|
+
method: "GET",
|
|
501
|
+
headers,
|
|
502
|
+
signal: options?.signal
|
|
503
|
+
});
|
|
504
|
+
if (!resp.ok) {
|
|
505
|
+
throw new OrvelloApiError(resp.status, resp.statusText, targetUrl, null, `Failed to download PDF from ${targetUrl}`);
|
|
506
|
+
}
|
|
507
|
+
return await resp.blob();
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
// ── Customer Companies ───────────────────────────────────────────────────
|
|
511
|
+
customerCompanies = {
|
|
512
|
+
search: (query, options) => this.request("/customer-companies/external", {
|
|
513
|
+
method: "GET",
|
|
514
|
+
query,
|
|
515
|
+
...options
|
|
516
|
+
}),
|
|
517
|
+
getById: (id, options) => this.request(`/customer-companies/external/${id}`, {
|
|
518
|
+
method: "GET",
|
|
519
|
+
...options
|
|
520
|
+
}),
|
|
521
|
+
create: (body, options) => this.request("/customer-companies/external", {
|
|
522
|
+
method: "POST",
|
|
523
|
+
body: JSON.stringify(body),
|
|
524
|
+
...options
|
|
525
|
+
}),
|
|
526
|
+
update: (id, body, options) => this.request(`/customer-companies/external/${id}`, {
|
|
527
|
+
method: "PUT",
|
|
528
|
+
body: JSON.stringify(body),
|
|
529
|
+
...options
|
|
530
|
+
}),
|
|
531
|
+
delete: (id, options) => this.request(`/customer-companies/external/${id}`, {
|
|
532
|
+
method: "DELETE",
|
|
533
|
+
...options
|
|
534
|
+
})
|
|
535
|
+
};
|
|
536
|
+
// ── Managing Companies ───────────────────────────────────────────────────
|
|
537
|
+
managingCompanies = {
|
|
538
|
+
getCurrent: (options) => this.request("/managing-companies/external/current", {
|
|
539
|
+
method: "GET",
|
|
540
|
+
...options
|
|
541
|
+
})
|
|
542
|
+
};
|
|
543
|
+
// ── Dunning ──────────────────────────────────────────────────────────────
|
|
544
|
+
dunning = {
|
|
545
|
+
suggestions: (query, options) => this.request("/invoices/external/dunning/suggestions", {
|
|
546
|
+
method: "GET",
|
|
547
|
+
query,
|
|
548
|
+
...options
|
|
549
|
+
}),
|
|
550
|
+
preview: (invoiceId, body, options) => this.request(`/invoices/external/dunning/${invoiceId}/preview`, {
|
|
551
|
+
method: "POST",
|
|
552
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
553
|
+
...options
|
|
554
|
+
}),
|
|
555
|
+
issue: (invoiceId, body, options) => this.request(`/invoices/external/dunning/${invoiceId}/issue`, {
|
|
556
|
+
method: "POST",
|
|
557
|
+
body: JSON.stringify(body),
|
|
558
|
+
...options
|
|
559
|
+
}),
|
|
560
|
+
getNoticePdf: (noticeId, options) => this.request(`/invoices/external/dunning/notices/${noticeId}/pdf`, {
|
|
561
|
+
method: "GET",
|
|
562
|
+
rawResponse: true,
|
|
563
|
+
...options
|
|
564
|
+
}),
|
|
565
|
+
downloadNoticePdf: (noticeId, options) => this.request(`/invoices/external/dunning/notices/${noticeId}/download`, {
|
|
566
|
+
method: "GET",
|
|
567
|
+
rawResponse: true,
|
|
568
|
+
...options
|
|
569
|
+
}),
|
|
570
|
+
getSettings: (options) => this.request("/invoices/external/dunning/settings", {
|
|
571
|
+
method: "GET",
|
|
572
|
+
...options
|
|
573
|
+
}),
|
|
574
|
+
updateSettings: (body, options) => this.request("/invoices/external/dunning/settings", {
|
|
575
|
+
method: "PUT",
|
|
576
|
+
body: JSON.stringify(body),
|
|
577
|
+
...options
|
|
578
|
+
})
|
|
579
|
+
};
|
|
580
|
+
// ── Bookkeeping ──────────────────────────────────────────────────────────
|
|
581
|
+
bookkeeping = {
|
|
582
|
+
revenues: {
|
|
583
|
+
create: (body, options) => {
|
|
584
|
+
const idempotencyKey = options?.idempotencyKey || body.externalRef;
|
|
585
|
+
return this.request("/bookkeeping/revenues/external", {
|
|
586
|
+
method: "POST",
|
|
587
|
+
body: JSON.stringify(body),
|
|
588
|
+
idempotencyKey,
|
|
589
|
+
...options
|
|
590
|
+
});
|
|
591
|
+
},
|
|
592
|
+
createInvoiceAdjustment: (body, options) => {
|
|
593
|
+
const idempotencyKey = options?.idempotencyKey || body.externalRef;
|
|
594
|
+
return this.request(
|
|
595
|
+
"/bookkeeping/revenues/external/invoice-adjustment",
|
|
596
|
+
{
|
|
597
|
+
method: "POST",
|
|
598
|
+
body: JSON.stringify(body),
|
|
599
|
+
idempotencyKey,
|
|
600
|
+
...options
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
expenses: {
|
|
606
|
+
create: (body, options) => {
|
|
607
|
+
const { receipt, ...reqOpts } = options || {};
|
|
608
|
+
const payload = receipt ? { ...body, ...receipt } : body;
|
|
609
|
+
const idempotencyKey = reqOpts.idempotencyKey || body.externalRef;
|
|
610
|
+
return this.request("/bookkeeping/expenses/external", {
|
|
611
|
+
method: "POST",
|
|
612
|
+
body: JSON.stringify(payload),
|
|
613
|
+
idempotencyKey,
|
|
614
|
+
...reqOpts
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
payments: {
|
|
619
|
+
listAccounts: (options, reqOptions) => this.request("/bookkeeping/payments/external/accounts", {
|
|
620
|
+
method: "GET",
|
|
621
|
+
query: options,
|
|
622
|
+
...reqOptions
|
|
623
|
+
}),
|
|
624
|
+
createManual: (body, options) => {
|
|
625
|
+
const idempotencyKey = options?.idempotencyKey || body.externalRef;
|
|
626
|
+
return this.request(
|
|
627
|
+
"/bookkeeping/payments/external/manual",
|
|
628
|
+
{
|
|
629
|
+
method: "POST",
|
|
630
|
+
body: JSON.stringify(body),
|
|
631
|
+
idempotencyKey,
|
|
632
|
+
...options
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
},
|
|
636
|
+
settleInvoice: (body, options) => {
|
|
637
|
+
const idempotencyKey = options?.idempotencyKey || body.externalRef || `settle-${body.invoiceId}-${Date.now()}`;
|
|
638
|
+
return this.request(
|
|
639
|
+
"/bookkeeping/payments/external/settle-invoice",
|
|
640
|
+
{
|
|
641
|
+
method: "POST",
|
|
642
|
+
body: JSON.stringify(body),
|
|
643
|
+
idempotencyKey,
|
|
644
|
+
...options
|
|
645
|
+
}
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
getDefaultBankAccount: async (options) => {
|
|
650
|
+
const now = Date.now();
|
|
651
|
+
if (!options?.forceRefresh && this._bankAccountCache && now - this._bankAccountCacheTime < 5 * 60 * 1e3) {
|
|
652
|
+
return this._bankAccountCache;
|
|
653
|
+
}
|
|
654
|
+
const accounts = await this.bookkeeping.payments.listAccounts({ includeArchived: false });
|
|
655
|
+
const bankAccounts = (accounts || []).filter((a) => a.type === "bank" && !a.isArchived);
|
|
656
|
+
let target = bankAccounts.find((a) => a.isDefault);
|
|
657
|
+
if (!target && bankAccounts.length === 1) {
|
|
658
|
+
target = bankAccounts[0];
|
|
659
|
+
}
|
|
660
|
+
if (!target) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
"Kein Default-Bankkonto im orvello-Dienst gefunden. Bitte dort ein Bankkonto als Standard markieren."
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const result = mapPaymentAccountToBankDetails(target);
|
|
666
|
+
this._bankAccountCache = result;
|
|
667
|
+
this._bankAccountCacheTime = now;
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
// ── Users ────────────────────────────────────────────────────────────────
|
|
672
|
+
users = {
|
|
673
|
+
getByExternalUserId: (externalUserId, options) => this.request(`/users/getByExternalUserId/${encodeURIComponent(externalUserId)}`, {
|
|
674
|
+
method: "GET",
|
|
675
|
+
...options
|
|
676
|
+
}),
|
|
677
|
+
update: (frontendUserId, body, options) => this.request(`/users/update/external/${frontendUserId}`, {
|
|
678
|
+
method: "PUT",
|
|
679
|
+
body: JSON.stringify(body),
|
|
680
|
+
...options
|
|
681
|
+
}),
|
|
682
|
+
delete: (frontendUserId, options) => this.request(`/users/delete/external/${frontendUserId}`, {
|
|
683
|
+
method: "DELETE",
|
|
684
|
+
...options
|
|
685
|
+
})
|
|
686
|
+
};
|
|
687
|
+
// ── OAuth2 External Clients (Connection Manager) ─────────────────────────
|
|
688
|
+
oauthClients = {
|
|
689
|
+
list: (query, options) => this.request("/oauth/clients/external/list", {
|
|
690
|
+
method: "GET",
|
|
691
|
+
query,
|
|
692
|
+
...options
|
|
693
|
+
}),
|
|
694
|
+
get: (idOrClientId, options) => this.request(`/oauth/clients/external/${encodeURIComponent(idOrClientId)}`, {
|
|
695
|
+
method: "GET",
|
|
696
|
+
...options
|
|
697
|
+
}),
|
|
698
|
+
create: (body, options) => this.request("/oauth/clients/external", {
|
|
699
|
+
method: "POST",
|
|
700
|
+
body: JSON.stringify(body),
|
|
701
|
+
...options
|
|
702
|
+
}),
|
|
703
|
+
update: (idOrClientId, body, options) => this.request(`/oauth/clients/external/${encodeURIComponent(idOrClientId)}`, {
|
|
704
|
+
method: "PUT",
|
|
705
|
+
body: JSON.stringify(body),
|
|
706
|
+
...options
|
|
707
|
+
}),
|
|
708
|
+
delete: (idOrClientId, options) => this.request(`/oauth/clients/external/${encodeURIComponent(idOrClientId)}`, {
|
|
709
|
+
method: "DELETE",
|
|
710
|
+
...options
|
|
711
|
+
}),
|
|
712
|
+
rotate: (idOrClientId, body, options) => this.request(`/oauth/clients/external/${encodeURIComponent(idOrClientId)}/rotate`, {
|
|
713
|
+
method: "POST",
|
|
714
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
715
|
+
...options
|
|
716
|
+
}),
|
|
717
|
+
retire: (idOrClientId, options) => this.request(`/oauth/clients/external/${encodeURIComponent(idOrClientId)}/retire`, {
|
|
718
|
+
method: "POST",
|
|
719
|
+
...options
|
|
720
|
+
}),
|
|
721
|
+
rollback: (idOrClientId, options) => this.request(`/oauth/clients/external/${encodeURIComponent(idOrClientId)}/rollback`, {
|
|
722
|
+
method: "POST",
|
|
723
|
+
...options
|
|
724
|
+
}),
|
|
725
|
+
revoke: (idOrClientId, options) => this.request(`/oauth/clients/external/${encodeURIComponent(idOrClientId)}/revoke`, {
|
|
726
|
+
method: "POST",
|
|
727
|
+
...options
|
|
728
|
+
})
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
function createOrvelloClient(config) {
|
|
732
|
+
return new OrvelloClient(config);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export { CONTRACT_SHA256, OrvelloApiError, OrvelloClient, buildBase64Receipt, calculateVat, createOrvelloClient, diagnoseOrvello, formatAmount, isKleinunternehmer, mapPaymentAccountToBankDetails };
|
|
736
|
+
//# sourceMappingURL=index.js.map
|
|
737
|
+
//# sourceMappingURL=index.js.map
|