update
This commit is contained in:
parent
25da1a46a8
commit
df1be3b823
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
85
server.js
85
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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue