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