This commit is contained in:
Andreas Knuth 2026-02-19 22:24:53 -06:00
parent 451f6f66c1
commit 444e8555f3
1 changed files with 50 additions and 64 deletions

106
server.js
View File

@ -1171,48 +1171,21 @@ 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...");
// 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;
}
// --- 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}`);
// 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 itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts";
@ -1230,48 +1203,61 @@ 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})...`);
// 5. An QBO senden — mit Retry bei Duplicate
let qboInvoice = null;
const MAX_RETRIES = 5;
const createResponse = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/invoice`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(qboInvoicePayload)
});
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
console.log(`📤 Sende Rechnung an QBO (DocNumber: ${qboInvoicePayload.DocNumber})...`);
const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json;
// Check auf Unterobjekt "Invoice"
const qboInvoice = responseData.Invoice || responseData;
const createResponse = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/invoice`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(qboInvoicePayload)
});
console.log("🔍 FULL QBO RESPONSE (ID):", qboInvoice.Id);
const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json;
if (!qboInvoice.Id) {
console.error("FULL RESPONSE DUMP:", JSON.stringify(responseData, null, 2));
throw new Error("QBO hat keine ID zurückgegeben.");
// 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 });
} catch (error) {