interface LexOfficeContact { id?: string; organizationId: string; version: number; roles: { customer?: { number?: number; }; }; company?: { name: string; taxNumber?: string; vatRegistrationId?: string; allowTaxFreeInvoices?: boolean; contactPersons?: Array<{ salutation?: string; firstName?: string; lastName?: string; primary?: boolean; emailAddress?: string; phoneNumber?: string; }>; }; person?: { salutation?: string; firstName?: string; lastName?: string; }; addresses?: { billing?: Array<{ supplement?: string; street?: string; zip?: string; city?: string; countryCode?: string; }>; }; emailAddresses?: { business?: Array; office?: Array; private?: Array; other?: Array; }; phoneNumbers?: { business?: Array; office?: Array; mobile?: Array; private?: Array; fax?: Array; other?: Array; }; note?: string; } interface LexOfficeQuotation { id?: string; organizationId?: string; createdDate?: string; updatedDate?: string; voucherNumber?: string; voucherDate: string; address: { contactId?: string; name?: string; supplement?: string; street?: string; city?: string; zip?: string; countryCode: string; }; lineItems: Array<{ type: 'custom' | 'text'; name?: string; description?: string; quantity?: number; unitName?: string; unitPrice?: { currency: string; netAmount: number; taxRatePercentage: number; }; }>; totalPrice?: { currency: string; }; taxConditions?: { taxType: 'net' | 'gross' | 'vatfree'; }; title?: string; introduction?: string; remark?: string; } interface LexOfficeInvoice extends Omit { voucherStatus?: string; shippingConditions?: { shippingDate?: string; shippingType?: string; }; paymentConditions?: { paymentTermLabel?: string; paymentTermDuration?: number; }; } export class LexOfficeService { private apiKey: string; private baseUrl = 'https://api.lexoffice.io/v1'; constructor() { this.apiKey = process.env.LEXOFFICE_API_KEY || ''; if (!this.apiKey) { console.warn('LexOffice API Key nicht konfiguriert'); } } private async request(method: string, endpoint: string, data?: any) { try { const response = await fetch(`${this.baseUrl}${endpoint}`, { method, headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: data ? JSON.stringify(data) : undefined, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`LexOffice API Error: ${response.status} - ${errorText}`); } return await response.json(); } catch (error) { console.error('LexOffice API Request failed:', error); throw error; } } async createContact(contact: Partial): Promise<{ id: string; resourceUri: string }> { return this.request('POST', '/contacts', contact); } async getContact(contactId: string): Promise { return this.request('GET', `/contacts/${contactId}`); } async createQuotation(quotation: LexOfficeQuotation, finalize: boolean = false): Promise<{ id: string; resourceUri: string; createdDate: string; updatedDate: string; voucherNumber: string }> { const url = finalize ? '/quotations?finalize=true' : '/quotations'; console.log(`📍 Creating quotation with URL: ${this.baseUrl}${url}, finalize=${finalize}`); return this.request('POST', url, quotation); } async getQuotation(quotationId: string): Promise { return this.request('GET', `/quotations/${quotationId}`); } async finalizeQuotation(quotationId: string): Promise<{ id: string; resourceUri: string }> { return this.request('PUT', `/quotations/${quotationId}/pursue`, { precedingSalesVoucherId: null, }); } async createInvoice(invoice: LexOfficeInvoice): Promise<{ id: string; resourceUri: string; createdDate: string; updatedDate: string; voucherNumber: string }> { return this.request('POST', '/invoices', invoice); } async getInvoice(invoiceId: string): Promise { return this.request('GET', `/invoices/${invoiceId}`); } async finalizeInvoice(invoiceId: string): Promise<{ id: string; resourceUri: string }> { return this.request('PUT', `/invoices/${invoiceId}/pursue`, { precedingSalesVoucherId: null, preserveVoucherNumber: false, }); } async getQuotationPDF(quotationId: string): Promise { try { const response = await fetch(`${this.baseUrl}/quotations/${quotationId}/document`, { method: 'GET', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Accept': 'application/pdf', }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`LexOffice PDF Error: ${response.status} - ${errorText}`); } const arrayBuffer = await response.arrayBuffer(); return Buffer.from(arrayBuffer); } catch (error) { console.error('LexOffice PDF download failed:', error); throw error; } } async getInvoicePDF(invoiceId: string): Promise { try { const response = await fetch(`${this.baseUrl}/invoices/${invoiceId}/document`, { method: 'GET', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Accept': 'application/pdf', }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`LexOffice PDF Error: ${response.status} - ${errorText}`); } const arrayBuffer = await response.arrayBuffer(); return Buffer.from(arrayBuffer); } catch (error) { console.error('LexOffice PDF download failed:', error); throw error; } } async createContactFromBooking(booking: any): Promise { const contact: Partial = { roles: { customer: {}, }, addresses: { billing: [{ street: booking.customerAddress, zip: booking.customerZip, city: booking.customerCity, countryCode: 'DE', }], }, emailAddresses: { business: [booking.customerEmail], }, phoneNumbers: { business: [booking.customerPhone], }, }; if (booking.invoiceType === 'BUSINESS' && booking.companyName) { const nameParts = booking.customerName.trim().split(' '); const firstName = nameParts[0] || ''; const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : nameParts[0] || 'Ansprechpartner'; contact.company = { name: booking.companyName, contactPersons: [{ firstName: firstName, lastName: lastName, primary: true, emailAddress: booking.customerEmail, phoneNumber: booking.customerPhone, }], }; } else { const nameParts = booking.customerName.trim().split(' '); const firstName = nameParts[0] || ''; const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : nameParts[0] || 'Unbekannt'; contact.person = { firstName: firstName, lastName: lastName, }; } const result = await this.createContact(contact); return result.id; } async createQuotationFromBooking(booking: any, contactId: string): Promise { const lineItems: Array = []; console.log('📊 Booking Data:', { id: booking.id, bookingNumber: booking.bookingNumber, hasPhotobox: !!booking.photobox, photoboxModel: booking.photobox?.model, locationId: booking.locationId, withPrintFlat: booking.withPrintFlat, }); // WICHTIG: priceConfig separat laden, da keine direkte Relation existiert let priceConfig = booking.priceConfig; if (!priceConfig && booking.photobox?.model && booking.locationId) { console.log('🔍 Lade PriceConfig nach...'); const { prisma } = await import('./prisma'); priceConfig = await prisma.priceConfig.findUnique({ where: { locationId_model: { locationId: booking.locationId, model: booking.photobox.model, }, }, }); console.log('📊 PriceConfig geladen:', priceConfig); } if (!priceConfig) { throw new Error('Keine Preiskonfiguration gefunden. Bitte konfiguriere zuerst die Preise für dieses Fotobox-Modell.'); } // 1. Fotobox als Hauptposition (IMMER als custom lineItem) const withPrintFlat = booking.withPrintFlat !== false; // Default: true const boxName = booking.photobox?.model || 'Fotobox'; const flatSuffix = withPrintFlat ? ' mit Druckflatrate' : ' (nur digital)'; const photoboxItem: any = { type: 'custom', name: `${boxName}${flatSuffix}`, description: `Event am ${new Date(booking.eventDate).toLocaleDateString('de-DE')} in ${booking.eventCity}`, quantity: 1, unitName: 'Stück', unitPrice: { currency: 'EUR', netAmount: priceConfig?.basePrice || 1, taxRatePercentage: 19, }, }; lineItems.push(photoboxItem); console.log('📦 Photobox LineItem (custom):', photoboxItem); // 2. Kilometerpauschale (falls Distanz vorhanden) if (booking.distance && booking.distance > 0) { // 2a. Kilometer-Pauschale (bis X km) - IMMER als custom lineItem if (priceConfig?.kmFlatRate && priceConfig.kmFlatRate > 0) { const kmFlatItem: any = { type: 'custom', name: `Kilometerpauschale (bis ${priceConfig.kmFlatRateUpTo}km)`, description: `Entfernung: ${booking.distance.toFixed(1)}km`, quantity: 1, unitName: 'Pauschale', unitPrice: { currency: 'EUR', netAmount: priceConfig.kmFlatRate / 1.19, taxRatePercentage: 19, }, }; lineItems.push(kmFlatItem); } // 2b. Zusätzliche Kilometer (wenn über Flatrate) - IMMER als custom lineItem if (priceConfig && booking.distance > priceConfig.kmFlatRateUpTo) { const extraKm = booking.distance - priceConfig.kmFlatRateUpTo; const totalExtraKm = extraKm * priceConfig.kmMultiplier; const kmExtraItem: any = { type: 'custom', name: `Zusatzkilometer`, description: `${extraKm.toFixed(1)}km × ${priceConfig.kmMultiplier} Strecken = ${totalExtraKm.toFixed(1)}km`, quantity: totalExtraKm, unitName: 'km', unitPrice: { currency: 'EUR', netAmount: priceConfig.pricePerKm, taxRatePercentage: 19, }, }; lineItems.push(kmExtraItem); } } // 3. Equipment/Extras (wenn vorhanden) - IMMER als custom lineItem if (booking.bookingEquipment && booking.bookingEquipment.length > 0) { for (const eq of booking.bookingEquipment) { const equipmentItem: any = { type: 'custom', name: eq.equipment.name, quantity: eq.quantity || 1, unitName: 'Stück', unitPrice: { currency: 'EUR', netAmount: eq.equipment.price || 1, taxRatePercentage: 19, }, }; lineItems.push(equipmentItem); } } const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const isDST = (date: Date) => { const jan = new Date(date.getFullYear(), 0, 1).getTimezoneOffset(); const jul = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); return Math.max(jan, jul) !== date.getTimezoneOffset(); }; const timezone = isDST(now) ? '+02:00' : '+01:00'; const voucherDate = `${year}-${month}-${day}T00:00:00.000${timezone}`; const expirationDateObj = new Date(now); expirationDateObj.setDate(expirationDateObj.getDate() + 14); const expYear = expirationDateObj.getFullYear(); const expMonth = String(expirationDateObj.getMonth() + 1).padStart(2, '0'); const expDay = String(expirationDateObj.getDate()).padStart(2, '0'); const expirationDate = `${expYear}-${expMonth}-${expDay}T00:00:00.000${timezone}`; const quotation: LexOfficeQuotation = { voucherDate, expirationDate, address: { contactId: contactId, name: booking.customerName, street: booking.customerAddress || undefined, zip: booking.customerZip || undefined, city: booking.customerCity || undefined, countryCode: 'DE', }, lineItems, totalPrice: { currency: 'EUR', }, taxConditions: { taxType: 'net', }, introduction: 'Vielen Dank für Ihre Anfrage! Gerne erstellen wir Ihnen folgendes Angebot:', remark: 'Wir freuen uns auf Ihre Bestellung!', }; console.log('📤 Sending to LexOffice:', JSON.stringify(quotation, null, 2)); // Schritt 1: Erstelle Quotation als Draft const result = await this.createQuotation(quotation, false); console.log('✅ Quotation created (draft):', result.id); // Schritt 2: Finalisiere Quotation sofort try { console.log('🔄 Finalizing quotation...'); await this.finalizeQuotation(result.id); console.log('✅ Quotation finalized to OPEN status'); } catch (error: any) { console.error('⚠️ Quotation finalization failed:', error.message); console.log('ℹ️ Quotation bleibt im DRAFT status'); } return result.id; } async createConfirmationFromBooking(booking: any, contactId: string): Promise { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const isDST = (date: Date) => { const jan = new Date(date.getFullYear(), 0, 1).getTimezoneOffset(); const jul = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); return Math.max(jan, jul) !== date.getTimezoneOffset(); }; const timezone = isDST(now) ? '+02:00' : '+01:00'; const voucherDate = `${year}-${month}-${day}T00:00:00.000${timezone}`; const invoice: LexOfficeInvoice = { voucherDate, address: { contactId: contactId, name: booking.customerName, countryCode: 'DE', }, lineItems: [ { type: 'custom', name: 'Fotobox-Vermietung', description: `Event am ${new Date(booking.eventDate).toLocaleDateString('de-DE')} in ${booking.eventCity}\nAufbau: ${new Date(booking.setupTimeStart).toLocaleString('de-DE')}\nOrt: ${booking.eventAddress}, ${booking.eventZip} ${booking.eventCity}`, quantity: 1, unitName: 'Stück', unitPrice: { currency: 'EUR', netAmount: booking.calculatedPrice || 0, taxRatePercentage: 19, }, }, ], totalPrice: { currency: 'EUR', }, taxConditions: { taxType: 'net', }, title: `AB ${booking.bookingNumber}`, introduction: 'Vielen Dank für Ihre Bestellung! Hiermit bestätigen wir Ihren Auftrag:', remark: 'Wir freuen uns auf Ihre Veranstaltung!', shippingConditions: { shippingDate: new Date(booking.eventDate).toISOString().split('T')[0], shippingType: 'service', }, paymentConditions: { paymentTermLabel: 'Zahlung bei Lieferung', paymentTermDuration: 0, }, }; const result = await this.createInvoice(invoice); await this.finalizeInvoice(result.id); return result.id; } } export const lexofficeService = new LexOfficeService();