buchpilot-mcp 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
+ Versioning: [Semantic Versioning](https://semver.org/)
6
+
7
+ ## [0.1.1] - 2026-03-19 11:30
8
+
9
+ ### Fixed
10
+ - Email-Mapping fuer Person-Kontakte: Email wird jetzt korrekt in Lexoffice gesetzt und gelesen
11
+ - Rate-Limiter fuer PDF-Download: `getInvoicePdf` nutzt jetzt Rate-Limiting + Fehlerbehandlung
12
+ - `contactId`-Filter in `listInvoices` wird jetzt korrekt an Lexoffice API uebergeben
13
+ - PDF-Response nutzt Standard-Text-Format statt fragwuerdigem `resource`-Typ
14
+ - CHANGELOG: "Sync" korrigiert zu "Ueberfaellige Rechnungen"
15
+
16
+ ## [0.1.0] - 2026-03-18 10:00
17
+
18
+ ### Added
19
+ - MCP Server fuer DACH-Buchhaltung via Model Context Protocol (stdio Transport)
20
+ - Backend-Architektur mit austauschbaren Backends (aktuell: Lexoffice)
21
+ - Lexoffice-Backend: vollstaendige API-Integration mit Lazy-Initialisierung
22
+ - Tools: Kontakte (create, get, list, update)
23
+ - Tools: Rechnungen (create, get, list, get_pdf, update)
24
+ - Tools: Belege/Vouchers (create, get, list)
25
+ - Tools: Angebote/Quotations (create, get)
26
+ - Tools: Ueberfaellige Rechnungen (get_overdue_invoices)
27
+ - Konfiguration via Umgebungsvariablen/Config-Datei
28
+ - Strukturierte Fehlerbehandlung mit Error Codes
29
+ - Zod-basierte Input-Validierung fuer alle Tools
30
+ - TypeScript mit ES Modules
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # BuchPilot MCP Server
2
+
3
+ > MCP Server fuer DACH-Buchhaltung — Kontakte, Rechnungen, Belege und Angebote direkt aus Claude, Cursor oder jedem MCP-kompatiblen Client verwalten.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/buchpilot-mcp.svg)](https://www.npmjs.com/package/buchpilot-mcp)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
7
+
8
+ ## Was ist das?
9
+
10
+ BuchPilot MCP ist ein [Model Context Protocol](https://modelcontextprotocol.io/) Server, der dein Buchhaltungssystem (aktuell Lexoffice) mit KI-Assistenten verbindet. Du kannst per natuerlicher Sprache Rechnungen erstellen, Kontakte verwalten und ueberfaellige Zahlungen pruefen — direkt in Claude Desktop, Cursor oder jedem anderen MCP-Client.
11
+
12
+ ## Features
13
+
14
+ - **15 Tools** fuer vollstaendige Buchhaltungs-Automatisierung
15
+ - **Kontakte** erstellen, abrufen, auflisten, aktualisieren
16
+ - **Rechnungen** erstellen, abrufen, auflisten, aktualisieren, PDF herunterladen
17
+ - **Belege** erstellen, abrufen, auflisten (Eingangsrechnungen, Gutschriften)
18
+ - **Angebote** erstellen, abrufen
19
+ - **Ueberfaellige Rechnungen** mit Betraegen und Tagen ueberfaellig
20
+ - **Backend-Architektur** — aktuell Lexoffice, erweiterbar fuer sevDesk, Billomat etc.
21
+ - **Plugin-faehig** — kann E-Invoice MCP Tools integrieren (`einvoice-mcp`)
22
+ - Keine Datenbank, kein State — reiner API-Proxy
23
+
24
+ ## Installation
25
+
26
+ ### Voraussetzungen
27
+
28
+ - Node.js >= 18
29
+ - Ein [Lexoffice](https://www.lexoffice.de/)-Account mit API-Key
30
+
31
+ ### npm (global)
32
+
33
+ ```bash
34
+ npm install -g buchpilot-mcp
35
+ ```
36
+
37
+ ### Von Source
38
+
39
+ ```bash
40
+ git clone https://github.com/makririch/buchpilot-mcp.git
41
+ cd buchpilot-mcp
42
+ npm install
43
+ npm run build
44
+ ```
45
+
46
+ ## Konfiguration
47
+
48
+ Erstelle eine Konfigurationsdatei `.buchpilot.json` an einem der folgenden Orte:
49
+
50
+ 1. Pfad aus Umgebungsvariable `BUCHPILOT_CONFIG`
51
+ 2. `~/.buchpilot.json` (Home-Verzeichnis)
52
+ 3. `./.buchpilot.json` (aktuelles Verzeichnis)
53
+
54
+ ### Inhalt der .buchpilot.json
55
+
56
+ ```json
57
+ {
58
+ "backends": {
59
+ "lexoffice": {
60
+ "api_key": "DEIN_LEXOFFICE_API_KEY"
61
+ }
62
+ },
63
+ "default_backend": "lexoffice"
64
+ }
65
+ ```
66
+
67
+ ### Lexoffice API-Key erstellen
68
+
69
+ 1. Gehe zu [Lexoffice Public API](https://app.lexoffice.de/addons/public-api)
70
+ 2. Klicke auf **API-Key erstellen**
71
+ 3. Kopiere den Key und fuege ihn in `.buchpilot.json` ein
72
+
73
+ > **Sicherheitshinweis:** Speichere den API-Key nie in Git. Fuege `.buchpilot.json` zu deiner `.gitignore` hinzu.
74
+
75
+ ## Nutzung
76
+
77
+ ### Server starten (standalone)
78
+
79
+ ```bash
80
+ # Nach npm install -g:
81
+ buchpilot-mcp
82
+
83
+ # Oder von Source:
84
+ npm start
85
+
86
+ # Entwicklung mit Auto-Reload:
87
+ npm run dev
88
+ ```
89
+
90
+ Der Server laeuft ueber stdio und wartet auf MCP-Nachrichten.
91
+
92
+ ### In Claude Desktop verwenden
93
+
94
+ Siehe [Claude Desktop Setup Guide](docs/claude-desktop-setup.md) fuer eine detaillierte Anleitung.
95
+
96
+ Kurzversion — fuege in `claude_desktop_config.json` hinzu:
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "buchpilot": {
102
+ "command": "npx",
103
+ "args": ["-y", "buchpilot-mcp"],
104
+ "env": {
105
+ "BUCHPILOT_CONFIG": "/Users/DEIN_NAME/.buchpilot.json"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ### Beispiele (natuerliche Sprache in Claude)
113
+
114
+ Nachdem du den Server konfiguriert hast, kannst du Claude z.B. fragen:
115
+
116
+ - "Erstelle einen Kontakt fuer die Firma Beispiel GmbH mit der E-Mail info@beispiel.de"
117
+ - "Zeige mir alle offenen Rechnungen"
118
+ - "Erstelle eine Rechnung an Kontakt XYZ: 10 Stunden Beratung zu je 150 EUR"
119
+ - "Welche Rechnungen sind ueberfaellig?"
120
+ - "Lade die PDF von Rechnung ABC herunter"
121
+ - "Erstelle ein Angebot fuer 5 Lizenzen a 49 EUR/Monat"
122
+
123
+ ## Tool-Referenz
124
+
125
+ Eine vollstaendige Referenz aller 15 Tools mit Parametern, Beispiel-Inputs und Beispiel-Outputs findest du in [docs/tool-reference.md](docs/tool-reference.md).
126
+
127
+ ### Kurzuebersicht
128
+
129
+ | Tool | Beschreibung |
130
+ |------|-------------|
131
+ | `create_contact` | Neuen Kontakt anlegen (Person oder Firma) |
132
+ | `get_contact` | Kontakt per ID abrufen |
133
+ | `list_contacts` | Kontakte auflisten mit Filtern |
134
+ | `update_contact` | Kontakt aktualisieren |
135
+ | `create_invoice` | Neue Rechnung mit Positionen erstellen |
136
+ | `get_invoice` | Rechnung per ID abrufen |
137
+ | `list_invoices` | Rechnungen auflisten (nach Status filterbar) |
138
+ | `get_invoice_pdf` | Rechnung als PDF herunterladen (Base64) |
139
+ | `update_invoice` | Entwurfs-Rechnung aktualisieren |
140
+ | `create_voucher` | Neuen Beleg anlegen |
141
+ | `get_voucher` | Beleg per ID abrufen |
142
+ | `list_vouchers` | Belege auflisten |
143
+ | `create_quotation` | Neues Angebot mit Positionen erstellen |
144
+ | `get_quotation` | Angebot per ID abrufen |
145
+ | `get_overdue_invoices` | Ueberfaellige Rechnungen mit Analyse |
146
+
147
+ ## E-Invoice Integration
148
+
149
+ BuchPilot MCP kann optional das [E-Invoice MCP](https://www.npmjs.com/package/einvoice-mcp) Paket integrieren, um XRechnung und ZUGFeRD direkt aus dem Buchhaltungssystem zu erzeugen:
150
+
151
+ ```typescript
152
+ // In deinem eigenen MCP-Server:
153
+ import { registerEInvoiceTools } from "einvoice-mcp";
154
+ registerEInvoiceTools(server);
155
+ // Ergebnis: 15 + 4 = 19 Tools
156
+ ```
157
+
158
+ ## FAQ / Troubleshooting
159
+
160
+ ### "No .buchpilot.json found"
161
+
162
+ Der Server findet keine Konfigurationsdatei. Erstelle eine `.buchpilot.json` in deinem Home-Verzeichnis:
163
+
164
+ ```bash
165
+ echo '{"backends":{"lexoffice":{"api_key":"DEIN_KEY"}},"default_backend":"lexoffice"}' > ~/.buchpilot.json
166
+ ```
167
+
168
+ ### "401 Unauthorized" bei Lexoffice
169
+
170
+ - Ist der API-Key korrekt in `.dachflow.json`?
171
+ - Ist der Key noch aktiv? Pruefe unter [Lexoffice Public API](https://app.lexoffice.de/addons/public-api)
172
+ - API-Keys koennen ablaufen — erstelle ggf. einen neuen
173
+
174
+ ### "429 Too Many Requests"
175
+
176
+ Lexoffice erlaubt max. 2 Requests pro Sekunde. Wenn du viele Operationen hintereinander ausfuehrst, warte kurz zwischen den Anfragen.
177
+
178
+ ### Server startet, aber Claude erkennt die Tools nicht
179
+
180
+ - Pruefe ob die `claude_desktop_config.json` korrekt ist
181
+ - Starte Claude Desktop neu nach Konfigurationsaenderungen
182
+ - Pruefe die Logs: `~/Library/Logs/Claude/mcp.log` (macOS)
183
+
184
+ ### Rechnung kann nicht aktualisiert werden
185
+
186
+ Nur Rechnungen im Status **draft** (Entwurf) koennen aktualisiert werden. Finalisierte Rechnungen sind unveraenderlich.
187
+
188
+ ### Backend "sevDesk" nicht verfuegbar
189
+
190
+ Aktuell wird nur **Lexoffice** als Backend unterstuetzt. sevDesk-Unterstuetzung ist geplant. Du kannst die n8n-Nodes (`n8n-nodes-buchpilot`) fuer sevDesk verwenden.
191
+
192
+ ## Lizenz
193
+
194
+ MIT — Frei nutzbar, auch kommerziell.
195
+
196
+ ## Autor
197
+
198
+ **MaKri** — [GitHub](https://github.com/makririch/buchpilot-mcp)
@@ -0,0 +1,23 @@
1
+ import type { IAccountingBackend, Contact, ContactInput, ContactFilter, Invoice, InvoiceInput, InvoiceFilter, Voucher, VoucherInput, VoucherFilter, Quotation, QuotationInput } from "./types.js";
2
+ export declare class LexofficeBackend implements IAccountingBackend {
3
+ name: string;
4
+ private apiKey;
5
+ constructor(apiKey: string);
6
+ createContact(input: ContactInput): Promise<Contact>;
7
+ getContact(id: string): Promise<Contact>;
8
+ listContacts(filter?: ContactFilter): Promise<Contact[]>;
9
+ updateContact(id: string, input: Partial<ContactInput>): Promise<Contact>;
10
+ createInvoice(input: InvoiceInput): Promise<Invoice>;
11
+ getInvoice(id: string): Promise<Invoice>;
12
+ listInvoices(filter?: InvoiceFilter): Promise<Invoice[]>;
13
+ getInvoicePdf(id: string): Promise<{
14
+ data: Buffer;
15
+ filename: string;
16
+ }>;
17
+ updateInvoice(id: string, input: Partial<InvoiceInput>): Promise<Invoice>;
18
+ createVoucher(input: VoucherInput): Promise<Voucher>;
19
+ getVoucher(id: string): Promise<Voucher>;
20
+ listVouchers(filter?: VoucherFilter): Promise<Voucher[]>;
21
+ createQuotation(input: QuotationInput): Promise<Quotation>;
22
+ getQuotation(id: string): Promise<Quotation>;
23
+ }
@@ -0,0 +1,344 @@
1
+ import { ErrorCode, createError } from "../errors.js";
2
+ const BASE_URL = "https://api.lexoffice.io/v1";
3
+ // Rate limiter: Lexoffice allows max 2 requests/second
4
+ let lastRequestTime = 0;
5
+ async function rateLimitSleep() {
6
+ const now = Date.now();
7
+ const elapsed = now - lastRequestTime;
8
+ if (elapsed < 500) {
9
+ await new Promise((resolve) => setTimeout(resolve, 500 - elapsed));
10
+ }
11
+ lastRequestTime = Date.now();
12
+ }
13
+ async function lexofficeRequest(apiKey, method, endpoint, body, qs) {
14
+ await rateLimitSleep();
15
+ const url = new URL(`${BASE_URL}${endpoint}`);
16
+ if (qs) {
17
+ for (const [key, value] of Object.entries(qs)) {
18
+ url.searchParams.set(key, value);
19
+ }
20
+ }
21
+ const headers = {
22
+ Authorization: `Bearer ${apiKey}`,
23
+ Accept: "application/json",
24
+ };
25
+ const options = { method, headers };
26
+ if (body && method !== "GET") {
27
+ headers["Content-Type"] = "application/json";
28
+ options.body = JSON.stringify(body);
29
+ }
30
+ const response = await fetch(url.toString(), options);
31
+ if (response.status === 401) {
32
+ throw createError(ErrorCode.AUTH_FAILED, "Lexoffice API key is invalid or expired");
33
+ }
34
+ if (response.status === 429) {
35
+ throw createError(ErrorCode.RATE_LIMITED, "Lexoffice API rate limit exceeded (max 2 req/s)");
36
+ }
37
+ if (response.status === 404) {
38
+ throw createError(ErrorCode.NOT_FOUND, `Resource not found: ${endpoint}`);
39
+ }
40
+ if (!response.ok) {
41
+ const errorText = await response.text().catch(() => "Unknown error");
42
+ throw createError(ErrorCode.BACKEND_ERROR, `Lexoffice API error ${response.status}: ${errorText}`);
43
+ }
44
+ if (response.status === 204)
45
+ return {};
46
+ const contentType = response.headers.get("content-type") || "";
47
+ if (contentType.includes("application/json")) {
48
+ return response.json();
49
+ }
50
+ return response;
51
+ }
52
+ // === Mapping Helpers ===
53
+ function toLexofficeContact(input) {
54
+ const roles = {};
55
+ const role = input.role || "customer";
56
+ if (role === "customer" || role === "both")
57
+ roles.customer = {};
58
+ if (role === "vendor" || role === "both")
59
+ roles.vendor = {};
60
+ const body = { version: 0, roles };
61
+ if (input.type === "company") {
62
+ body.company = {
63
+ name: input.name,
64
+ ...(input.email ? { contactPersons: [{ emailAddress: input.email }] } : {}),
65
+ };
66
+ }
67
+ else {
68
+ body.person = {
69
+ lastName: input.name,
70
+ ...(input.firstName ? { firstName: input.firstName } : {}),
71
+ };
72
+ if (input.email) {
73
+ body.emailAddresses = { business: [input.email] };
74
+ }
75
+ }
76
+ return body;
77
+ }
78
+ function fromLexofficeContact(raw) {
79
+ const isCompany = !!raw.company;
80
+ return {
81
+ id: raw.id,
82
+ type: isCompany ? "company" : "person",
83
+ name: isCompany ? raw.company?.name : raw.person?.lastName,
84
+ firstName: raw.person?.firstName,
85
+ email: isCompany
86
+ ? raw.company?.contactPersons?.[0]?.emailAddress
87
+ : raw.emailAddresses?.business?.[0] || raw.emailAddresses?.private?.[0],
88
+ role: raw.roles?.customer && raw.roles?.vendor
89
+ ? "both"
90
+ : raw.roles?.vendor ? "vendor" : "customer",
91
+ backend: "lexoffice",
92
+ raw,
93
+ };
94
+ }
95
+ function toLexofficeInvoice(input) {
96
+ return {
97
+ voucherDate: input.date,
98
+ address: { contactId: input.contactId },
99
+ lineItems: input.lineItems.map((item) => ({
100
+ type: "custom",
101
+ name: item.name,
102
+ quantity: item.quantity,
103
+ unitName: "Stueck",
104
+ unitPrice: {
105
+ currency: input.currency || "EUR",
106
+ netAmount: item.unitPrice,
107
+ taxRatePercentage: item.taxRate,
108
+ },
109
+ })),
110
+ totalPrice: { currency: input.currency || "EUR" },
111
+ taxConditions: { taxType: "net" },
112
+ ...(input.title && { title: input.title }),
113
+ ...(input.introduction && { introduction: input.introduction }),
114
+ ...(input.remark && { remark: input.remark }),
115
+ };
116
+ }
117
+ function fromLexofficeInvoice(raw) {
118
+ const lineItems = (raw.lineItems || [])
119
+ .filter((li) => li.type === "custom")
120
+ .map((li) => ({
121
+ name: li.name,
122
+ quantity: li.quantity,
123
+ unitPrice: li.unitPrice?.netAmount || 0,
124
+ taxRate: li.unitPrice?.taxRatePercentage || 0,
125
+ }));
126
+ return {
127
+ id: raw.id,
128
+ number: raw.voucherNumber || "",
129
+ contactId: raw.address?.contactId || "",
130
+ date: raw.voucherDate || "",
131
+ dueDate: raw.dueDate || raw.voucherDate || "",
132
+ status: raw.voucherStatus === "draft" ? "draft"
133
+ : raw.voucherStatus === "open" ? "open"
134
+ : raw.voucherStatus === "paid" ? "paid"
135
+ : raw.voucherStatus === "overdue" ? "overdue"
136
+ : "open",
137
+ totalNet: raw.totalPrice?.totalNetAmount || 0,
138
+ totalGross: raw.totalPrice?.totalGrossAmount || 0,
139
+ currency: raw.totalPrice?.currency || "EUR",
140
+ lineItems,
141
+ backend: "lexoffice",
142
+ raw,
143
+ };
144
+ }
145
+ function fromLexofficeVoucher(raw) {
146
+ return {
147
+ id: raw.id,
148
+ type: raw.voucherType || "",
149
+ date: raw.voucherDate || "",
150
+ totalGross: raw.totalGrossAmount || 0,
151
+ taxRate: raw.voucherItems?.[0]?.taxRatePercent || 0,
152
+ backend: "lexoffice",
153
+ raw,
154
+ };
155
+ }
156
+ function fromLexofficeQuotation(raw) {
157
+ const lineItems = (raw.lineItems || [])
158
+ .filter((li) => li.type === "custom")
159
+ .map((li) => ({
160
+ name: li.name,
161
+ quantity: li.quantity,
162
+ unitPrice: li.unitPrice?.netAmount || 0,
163
+ taxRate: li.unitPrice?.taxRatePercentage || 0,
164
+ }));
165
+ return {
166
+ id: raw.id,
167
+ contactId: raw.address?.contactId || "",
168
+ date: raw.voucherDate || "",
169
+ expirationDate: raw.expirationDate,
170
+ totalNet: raw.totalPrice?.totalNetAmount || 0,
171
+ totalGross: raw.totalPrice?.totalGrossAmount || 0,
172
+ lineItems,
173
+ backend: "lexoffice",
174
+ raw,
175
+ };
176
+ }
177
+ // === Lexoffice Backend ===
178
+ export class LexofficeBackend {
179
+ name = "lexoffice";
180
+ apiKey;
181
+ constructor(apiKey) {
182
+ this.apiKey = apiKey;
183
+ }
184
+ // --- Contacts ---
185
+ async createContact(input) {
186
+ const body = toLexofficeContact(input);
187
+ const result = await lexofficeRequest(this.apiKey, "POST", "/contacts", body);
188
+ return this.getContact(result.id);
189
+ }
190
+ async getContact(id) {
191
+ const raw = await lexofficeRequest(this.apiKey, "GET", `/contacts/${id}`);
192
+ return fromLexofficeContact(raw);
193
+ }
194
+ async listContacts(filter) {
195
+ const qs = { size: String(filter?.limit || 25) };
196
+ if (filter?.email)
197
+ qs.email = filter.email;
198
+ if (filter?.name)
199
+ qs.name = filter.name;
200
+ if (filter?.role === "customer")
201
+ qs.customer = "true";
202
+ if (filter?.role === "vendor")
203
+ qs.vendor = "true";
204
+ const response = await lexofficeRequest(this.apiKey, "GET", "/contacts", undefined, qs);
205
+ return (response.content || []).map(fromLexofficeContact);
206
+ }
207
+ async updateContact(id, input) {
208
+ const existing = await lexofficeRequest(this.apiKey, "GET", `/contacts/${id}`);
209
+ const body = { ...existing, ...input };
210
+ delete body.id;
211
+ delete body.resourceUri;
212
+ if (input.name && body.company) {
213
+ body.company.name = input.name;
214
+ }
215
+ if (input.name && body.person) {
216
+ body.person.lastName = input.name;
217
+ }
218
+ await lexofficeRequest(this.apiKey, "PUT", `/contacts/${id}`, body);
219
+ return this.getContact(id);
220
+ }
221
+ // --- Invoices ---
222
+ async createInvoice(input) {
223
+ const body = toLexofficeInvoice(input);
224
+ const qs = {};
225
+ if (input.finalize)
226
+ qs.finalize = "true";
227
+ const result = await lexofficeRequest(this.apiKey, "POST", "/invoices", body, Object.keys(qs).length ? qs : undefined);
228
+ return this.getInvoice(result.id);
229
+ }
230
+ async getInvoice(id) {
231
+ const raw = await lexofficeRequest(this.apiKey, "GET", `/invoices/${id}`);
232
+ return fromLexofficeInvoice(raw);
233
+ }
234
+ async listInvoices(filter) {
235
+ const qs = { size: String(filter?.limit || 25) };
236
+ if (filter?.status && filter.status !== "overdue") {
237
+ qs.voucherStatus = filter.status;
238
+ }
239
+ if (filter?.contactId) {
240
+ qs.contactId = filter.contactId;
241
+ }
242
+ const response = await lexofficeRequest(this.apiKey, "GET", "/voucherlist", undefined, {
243
+ ...qs,
244
+ voucherType: "invoice",
245
+ });
246
+ const items = (response.content || []).map(fromLexofficeInvoice);
247
+ if (filter?.status === "overdue") {
248
+ const now = Date.now();
249
+ return items.filter((inv) => new Date(inv.dueDate).getTime() < now && inv.status !== "paid");
250
+ }
251
+ return items;
252
+ }
253
+ async getInvoicePdf(id) {
254
+ const docResponse = await lexofficeRequest(this.apiKey, "GET", `/invoices/${id}/document`);
255
+ const fileId = docResponse.documentFileId;
256
+ await rateLimitSleep();
257
+ const fileResponse = await fetch(`${BASE_URL}/files/${fileId}`, {
258
+ headers: { Authorization: `Bearer ${this.apiKey}` },
259
+ });
260
+ if (fileResponse.status === 429) {
261
+ throw createError(ErrorCode.RATE_LIMITED, "Lexoffice API rate limit exceeded during PDF download");
262
+ }
263
+ if (!fileResponse.ok) {
264
+ throw createError(ErrorCode.BACKEND_ERROR, `Failed to download PDF: ${fileResponse.status}`);
265
+ }
266
+ const buffer = Buffer.from(await fileResponse.arrayBuffer());
267
+ return { data: buffer, filename: `invoice-${id}.pdf` };
268
+ }
269
+ async updateInvoice(id, input) {
270
+ const existing = await lexofficeRequest(this.apiKey, "GET", `/invoices/${id}`);
271
+ const body = { ...existing, ...input };
272
+ delete body.id;
273
+ delete body.resourceUri;
274
+ delete body.organizationId;
275
+ await lexofficeRequest(this.apiKey, "PUT", `/invoices/${id}`, body);
276
+ return this.getInvoice(id);
277
+ }
278
+ // --- Vouchers ---
279
+ async createVoucher(input) {
280
+ const taxAmount = input.totalGross - (input.totalGross / (1 + input.taxRate / 100));
281
+ const body = {
282
+ type: "voucher",
283
+ voucherType: input.type,
284
+ voucherDate: input.date,
285
+ totalGrossAmount: input.totalGross,
286
+ totalTaxAmount: taxAmount,
287
+ taxType: "gross",
288
+ voucherItems: [{
289
+ amount: input.totalGross,
290
+ taxAmount,
291
+ taxRatePercent: input.taxRate,
292
+ categoryId: "8f8664a8-fd86-11e1-a21f-0800200c9a66",
293
+ }],
294
+ ...(input.contactId && { contactId: input.contactId }),
295
+ ...(input.remark && { remark: input.remark }),
296
+ };
297
+ const result = await lexofficeRequest(this.apiKey, "POST", "/vouchers", body);
298
+ return this.getVoucher(result.id);
299
+ }
300
+ async getVoucher(id) {
301
+ const raw = await lexofficeRequest(this.apiKey, "GET", `/vouchers/${id}`);
302
+ return fromLexofficeVoucher(raw);
303
+ }
304
+ async listVouchers(filter) {
305
+ const qs = { size: String(filter?.limit || 25) };
306
+ if (filter?.type && filter.type !== "any")
307
+ qs.voucherType = filter.type;
308
+ const response = await lexofficeRequest(this.apiKey, "GET", "/voucherlist", undefined, qs);
309
+ return (response.content || []).map(fromLexofficeVoucher);
310
+ }
311
+ // --- Quotations ---
312
+ async createQuotation(input) {
313
+ const body = {
314
+ voucherDate: input.date,
315
+ address: { contactId: input.contactId },
316
+ lineItems: input.lineItems.map((item) => ({
317
+ type: "custom",
318
+ name: item.name,
319
+ quantity: item.quantity,
320
+ unitName: "Stueck",
321
+ unitPrice: {
322
+ currency: "EUR",
323
+ netAmount: item.unitPrice,
324
+ taxRatePercentage: item.taxRate,
325
+ },
326
+ })),
327
+ totalPrice: { currency: "EUR" },
328
+ taxConditions: { taxType: "net" },
329
+ ...(input.expirationDate && { expirationDate: input.expirationDate }),
330
+ ...(input.title && { title: input.title }),
331
+ ...(input.introduction && { introduction: input.introduction }),
332
+ ...(input.remark && { remark: input.remark }),
333
+ };
334
+ const qs = {};
335
+ if (input.finalize)
336
+ qs.finalize = "true";
337
+ const result = await lexofficeRequest(this.apiKey, "POST", "/quotations", body, Object.keys(qs).length ? qs : undefined);
338
+ return this.getQuotation(result.id);
339
+ }
340
+ async getQuotation(id) {
341
+ const raw = await lexofficeRequest(this.apiKey, "GET", `/quotations/${id}`);
342
+ return fromLexofficeQuotation(raw);
343
+ }
344
+ }