diff --git a/server.js b/server.js index e2cd58a..18befa5 100644 --- a/server.js +++ b/server.js @@ -1171,55 +1171,28 @@ app.post('/api/invoices/:id/export', async (req, res) => { : 'https://sandbox-quickbooks.api.intuit.com'; - // ------------------------------------------- - // --- SCHRITT 3a: NÄCHSTE NUMMER ERMITTELN --- - console.log("🔍 Frage QBO nach der höchsten Rechnungsnummer..."); + // --- SCHRITT 3a: NÄCHSTE NUMMER ERMITTELN (lokal) --- + // Wir nehmen das Maximum aus qbo_doc_number UND invoice_number + const maxNumResult = await client.query(` + SELECT GREATEST( + COALESCE((SELECT MAX(CAST(qbo_doc_number AS INTEGER)) FROM invoices WHERE qbo_doc_number ~ '^[0-9]+$'), 0), + COALESCE((SELECT MAX(CAST(invoice_number AS INTEGER)) FROM invoices WHERE invoice_number ~ '^[0-9]+$'), 0) + ) as max_num + `); + let nextDocNumber = (parseInt(maxNumResult.rows[0].max_num) + 1).toString(); + console.log(`✅ Nächste Nummer (aus lokaler DB): ${nextDocNumber}`); - // Alle Rechnungen laden (nur DocNumber Feld), nach DocNumber absteigend - // QBO unterstützt leider kein MAX() oder CAST, daher holen wir die letzten 100 - // und ermitteln die höchste rein numerische Nummer clientseitig. - const numQuery = "SELECT DocNumber FROM Invoice ORDERBY DocNumber DESC MAXRESULTS 100"; - const lastNumResponse = await makeQboApiCall({ - url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(numQuery)}`, - method: 'GET' - }); - - const lastNumData = lastNumResponse.getJson ? lastNumResponse.getJson() : lastNumResponse.json; - let nextDocNumber = null; - - if (lastNumData.QueryResponse?.Invoice?.length > 0) { - // Nur rein numerische DocNumbers betrachten (keine "110444-A" etc.) - const numericDocs = lastNumData.QueryResponse.Invoice - .map(inv => inv.DocNumber) - .filter(dn => /^\d+$/.test(dn)) - .map(dn => parseInt(dn, 10)) - .sort((a, b) => b - a); // Absteigend - - if (numericDocs.length > 0) { - const highest = numericDocs[0]; - nextDocNumber = (highest + 1).toString(); - console.log(`✅ Höchste numerische Nummer: ${highest}. Neue Nummer: ${nextDocNumber}`); - } - } - - // Fallback - 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; - - const itemRefId = item.qbo_item_id || QBO_PARTS_ID; + const itemRefId = item.qbo_item_id || QBO_PARTS_ID; const itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts"; return { "DetailType": "SalesItemLineDetail", "Amount": amount, - "Description": item.description, + "Description": item.description, "SalesItemLineDetail": { "ItemRef": { "value": itemRefId, "name": itemRefName }, "UnitPrice": rate, @@ -1230,49 +1203,62 @@ app.post('/api/invoices/:id/export', async (req, res) => { const qboInvoicePayload = { "CustomerRef": { "value": invoice.customer_qbo_id }, - - // 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}` : "" }, - - // Status auf "Verschickt" setzen "EmailStatus": "EmailSent", "BillEmail": { "Address": invoice.email || "" } }; - console.log(`📤 Sende Rechnung an QBO (DocNumber: ${nextDocNumber})...`); - - const createResponse = await makeQboApiCall({ - url: `${baseUrl}/v3/company/${companyId}/invoice`, - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(qboInvoicePayload) - }); + // 5. An QBO senden — mit Retry bei Duplicate + let qboInvoice = null; + const MAX_RETRIES = 5; - const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json; - // Check auf Unterobjekt "Invoice" - const qboInvoice = responseData.Invoice || responseData; + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + console.log(`📤 Sende Rechnung an QBO (DocNumber: ${qboInvoicePayload.DocNumber})...`); - console.log("🔍 FULL QBO RESPONSE (ID):", qboInvoice.Id); + const createResponse = await makeQboApiCall({ + url: `${baseUrl}/v3/company/${companyId}/invoice`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(qboInvoicePayload) + }); - if (!qboInvoice.Id) { - console.error("FULL RESPONSE DUMP:", JSON.stringify(responseData, null, 2)); - throw new Error("QBO hat keine ID zurückgegeben."); + const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json; + + // Prüfe auf Duplicate Error (code 6140) + if (responseData.Fault?.Error?.[0]?.code === '6140') { + const oldNum = parseInt(qboInvoicePayload.DocNumber); + qboInvoicePayload.DocNumber = (oldNum + 1).toString(); + console.log(`⚠️ DocNumber ${oldNum} existiert bereits. Versuche ${qboInvoicePayload.DocNumber}...`); + continue; + } + + qboInvoice = responseData.Invoice || responseData; + + if (qboInvoice.Id) { + break; // Erfolg! + } else { + console.error("FULL RESPONSE DUMP:", JSON.stringify(responseData, null, 2)); + throw new Error("QBO hat keine ID zurückgegeben: " + + (responseData.Fault?.Error?.[0]?.Message || JSON.stringify(responseData))); + } + } + + if (!qboInvoice || !qboInvoice.Id) { + throw new Error(`Konnte nach ${MAX_RETRIES} Versuchen keine freie DocNumber finden.`); } 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 + // 6. DB Update await client.query( - `UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $3 WHERE id = $4`, - [qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, id] + `UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $4 WHERE id = $5`, + [qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, qboInvoice.DocNumber, 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 }); + res.json({ success: true, qbo_id: qboInvoice.Id, qbo_doc_number: qboInvoice.DocNumber }); } catch (error) { console.error("QBO Export Error:", error);