@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.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