diff --git a/qbo_helper.js b/qbo_helper.js index 81d443c..dbc21b4 100644 --- a/qbo_helper.js +++ b/qbo_helper.js @@ -26,56 +26,55 @@ async function makeQboApiCall(requestOptions) { // Funktion zum Aktualisieren des Tokens const doRefresh = async () => { - console.log("🔄 QBO Token Refresh wird ausgeführt..."); + console.log("🔄 QBO Token Refresh wird ausgeführt (401 Error gefangen)..."); try { const authResponse = await client.refresh(); console.log("✅ Token erfolgreich erneuert."); + // Hier müsste man idealerweise die neuen Tokens speichern return authResponse; } catch (e) { - console.error("❌ Refresh fehlgeschlagen:", e); + console.error("❌ Refresh fehlgeschlagen:", e.originalMessage || e); throw e; } }; - // 1. Pre-Check: Wenn Token leer ist, sofort refreshen - if (!client.token.access_token) { - console.log("⚠️ Kein Access Token gefunden. Versuche sofortigen Refresh..."); - await doRefresh(); - } - - // 2. Pre-Check: Ist Token laut Zeitstempel abgelaufen? - if (!client.isAccessTokenValid()) { - console.log("⚠️ Token ist zeitlich abgelaufen. Refresh..."); - await doRefresh(); - } + // --- ÄNDERUNG: KEINE VORAB-PRÜFUNG MEHR --- + // Wir vertrauen darauf, dass der Token in der .env aktuell ist (da du ihn gerade generiert hast). + // Wir entfernen client.isAccessTokenValid(), da dies oft falsch negativ ist nach Neustart. try { + // Versuch 1: Einfach machen! const response = await client.makeApiCall(requestOptions); - // Prüfen, ob QBO trotz HTTP 200/400 eine Fehlermeldung im Body sendet + // Prüfen, ob QBO eine Fehlermeldung im Body sendet (trotz HTTP 200/400) const data = response.getJson ? response.getJson() : response.json; if (data.fault && data.fault.error) { const errorCode = data.fault.error[0].code; - // Fehler 3202 = Missing Access Token - if (errorCode === '3202') { - console.log("⚠️ QBO meldet fehlenden Token (3202). Versuche Refresh und Retry..."); + // Fehler 3202 = Missing Access Token / Invalid + // Manchmal sendet QBO auch 401 im Body + if (errorCode === '3202' || errorCode === '3100') { + console.log(`⚠️ QBO meldet Token-Fehler (${errorCode}). Versuche Refresh und Retry...`); await doRefresh(); return await client.makeApiCall(requestOptions); } - // Anderen API-Fehler werfen, damit server.js ihn fängt + // Anderen API-Fehler werfen (z.B. Validierung) throw new Error(`QBO API Error ${errorCode}: ${data.fault.error[0].message}`); } return response; } catch (e) { - // HTTP 401 Unauthorized fangen - if (e.response?.status === 401) { - console.log("⚠️ 401 Unauthorized. Versuche Refresh und Retry..."); + // HTTP 401 Unauthorized fangen -> Das ist der ECHTE Indikator, dass der Token abgelaufen ist + const isAuthError = e.response?.status === 401 || (e.authResponse && e.authResponse.response && e.authResponse.response.status === 401); + + if (isAuthError) { + console.log("⚠️ 401 Unauthorized erhalten. Versuche Refresh und Retry..."); await doRefresh(); return await client.makeApiCall(requestOptions); } + + // Alle anderen Fehler weiterwerfen throw e; } } diff --git a/server.js b/server.js index d8d70c1..c2180e9 100644 --- a/server.js +++ b/server.js @@ -1131,9 +1131,9 @@ app.post('/api/invoices/:id/export', async (req, res) => { const { id } = req.params; const client = await pool.connect(); - // HIER SIND DEINE FESTEN IDs - const QBO_LABOR_ID = '5'; // Labor:Labor - const QBO_PARTS_ID = '9'; // Parts:Parts + // IDs für deine Items (Labor / Parts) + const QBO_LABOR_ID = '5'; + const QBO_PARTS_ID = '9'; try { // 1. Lokale Rechnung laden @@ -1151,23 +1151,54 @@ app.post('/api/invoices/:id/export', async (req, res) => { return res.status(400).json({ error: `Kunde "${invoice.customer_name}" ist noch nicht mit QBO verknüpft.` }); } - // 2. Items laden (inkl. qbo_item_id) + // 2. Items laden const itemsRes = await client.query('SELECT * FROM invoice_items WHERE invoice_id = $1', [id]); const items = itemsRes.rows; - // 3. QBO Client + // 3. QBO Client vorbereiten const oauthClient = getOAuthClient(); const companyId = oauthClient.getToken().realmId; const baseUrl = process.env.QBO_ENVIRONMENT === 'production' ? 'https://quickbooks.api.intuit.com' : 'https://sandbox-quickbooks.api.intuit.com'; + + // --- SCHRITT 3a: NÄCHSTE NUMMER ERMITTELN --- + console.log("🔍 Frage QBO nach der letzten Rechnungsnummer..."); - // 4. QBO JSON bauen (OHNE "select * from Item...") + // Wir suchen die ZULETZT ERSTELLTE Rechnung + const lastNumQuery = "SELECT DocNumber FROM Invoice ORDERBY MetaData.CreateTime DESC MAXRESULTS 1"; + const lastNumResponse = await makeQboApiCall({ + url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(lastNumQuery)}`, + method: 'GET' + }); + + const lastNumData = lastNumResponse.getJson ? lastNumResponse.getJson() : lastNumResponse.json; + let nextDocNumber = null; + + if (lastNumData.QueryResponse && lastNumData.QueryResponse.Invoice && lastNumData.QueryResponse.Invoice.length > 0) { + const lastDocNumberStr = lastNumData.QueryResponse.Invoice[0].DocNumber; + // Versuchen, die Nummer zu parsen (Entfernt Buchstaben, behält Zahlen) + const lastNum = parseInt(lastDocNumberStr.replace(/[^0-9]/g, ''), 10); + + if (!isNaN(lastNum)) { + nextDocNumber = (lastNum + 1).toString(); + console.log(`✅ Letzte Nummer war ${lastDocNumberStr}. Neue Nummer wird: ${nextDocNumber}`); + } + } + + // Fallback: Wenn QBO leer ist oder Parsing fehlschlägt, nimm die lokale Nummer + if (!nextDocNumber) { + console.log("⚠️ Konnte keine Nummer aus QBO ermitteln. Verwende lokale Nummer."); + nextDocNumber = invoice.invoice_number; + } + // ------------------------------------------- + + + // 4. QBO JSON bauen const lineItems = items.map(item => { const rate = parseFloat(item.rate.replace(/[^0-9.]/g, '')) || 0; const amount = parseFloat(item.amount.replace(/[^0-9.]/g, '')) || 0; - // WICHTIG: Hier nutzen wir die ID aus der Datenbank oder den Fallback const itemRefId = item.qbo_item_id || QBO_PARTS_ID; const itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts"; @@ -1176,10 +1207,7 @@ app.post('/api/invoices/:id/export', async (req, res) => { "Amount": amount, "Description": item.description, "SalesItemLineDetail": { - "ItemRef": { - "value": itemRefId, - "name": itemRefName - }, + "ItemRef": { "value": itemRefId, "name": itemRefName }, "UnitPrice": rate, "Qty": parseFloat(item.quantity) || 1 } @@ -1188,13 +1216,20 @@ app.post('/api/invoices/:id/export', async (req, res) => { const qboInvoicePayload = { "CustomerRef": { "value": invoice.customer_qbo_id }, - "DocNumber": invoice.invoice_number, + + // HIER SETZEN WIR DIE ERMITTELTE NUMMER EIN + "DocNumber": nextDocNumber, + "TxnDate": invoice.invoice_date.toISOString().split('T')[0], "Line": lineItems, - "CustomerMemo": { "value": invoice.auth_code ? `Auth: ${invoice.auth_code}` : "" } + "CustomerMemo": { "value": invoice.auth_code ? `Auth: ${invoice.auth_code}` : "" }, + + // Status auf "Verschickt" setzen + "EmailStatus": "EmailSent", + "BillEmail": { "Address": invoice.email || "" } }; - console.log(`📤 Sende Rechnung ${invoice.invoice_number} an QBO...`); + console.log(`📤 Sende Rechnung an QBO (DocNumber: ${nextDocNumber})...`); const createResponse = await makeQboApiCall({ url: `${baseUrl}/v3/company/${companyId}/invoice`, @@ -1203,19 +1238,27 @@ app.post('/api/invoices/:id/export', async (req, res) => { body: JSON.stringify(qboInvoicePayload) }); - const qboInvoice = createResponse.getJson ? createResponse.getJson() : createResponse.json; - onsole.log("🔍 FULL QBO RESPONSE:", JSON.stringify(qboInvoice, null, 2)); - if (!qboInvoice.Id) { - throw new Error("QBO hat keine ID zurückgegeben. Wahrscheinlich ein Fehler im Request (siehe Logs)."); - } - console.log(`✅ QBO Rechnung erstellt! ID: ${qboInvoice.Id}`); + const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json; + // Check auf Unterobjekt "Invoice" + const qboInvoice = responseData.Invoice || responseData; + console.log("🔍 FULL QBO RESPONSE (ID):", qboInvoice.Id); + + if (!qboInvoice.Id) { + console.error("FULL RESPONSE DUMP:", JSON.stringify(responseData, null, 2)); + throw new Error("QBO hat keine ID zurückgegeben."); + } + + console.log(`✅ QBO Rechnung erstellt! ID: ${qboInvoice.Id}, DocNumber: ${qboInvoice.DocNumber}`); + + // 6. DB Update: Wir speichern AUCH die QBO-Nummer, damit wir wissen, wie sie drüben heißt await client.query( `UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3 WHERE id = $4`, [qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, id] ); - res.json({ success: true, qbo_id: qboInvoice.Id }); + // Wir geben die neue Nummer zurück, damit das Frontend Bescheid weiß + res.json({ success: true, qbo_id: qboInvoice.Id, qbo_doc_number: qboInvoice.DocNumber }); } catch (error) { console.error("QBO Export Error:", error);