invoice-system/qbo_helper.js

181 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// qbo_helper.js - DEFINITIVER FIX
//
// Kernproblem: client.refresh() ruft intern validateToken() auf,
// das das Token-Objekt prüft und "invalid" wirft wenn das Format
// nicht stimmt. Das passiert LOKAL, nicht bei Intuit.
//
// Lösung: refreshUsingToken(refreshTokenString) verwenden.
// Diese Methode akzeptiert den RT direkt als String und umgeht
// die validateToken()-Prüfung komplett.
require('dotenv').config();
const OAuthClient = require('intuit-oauth');
const fs = require('fs');
const path = require('path');
let oauthClient = null;
const tokenFile = path.join(__dirname, 'qbo_token.json');
const getOAuthClient = () => {
if (!oauthClient) {
oauthClient = new OAuthClient({
clientId: process.env.QBO_CLIENT_ID,
clientSecret: process.env.QBO_CLIENT_SECRET,
environment: process.env.QBO_ENVIRONMENT || 'sandbox',
redirectUri: process.env.QBO_REDIRECT_URI
});
let savedToken = null;
try {
if (fs.existsSync(tokenFile)) {
const stat = fs.statSync(tokenFile);
if (stat.isFile()) {
const content = fs.readFileSync(tokenFile, 'utf8');
if (content.trim() !== "{}") {
savedToken = JSON.parse(content);
}
}
}
} catch (e) {
console.error("❌ Fehler beim Laden des gespeicherten Tokens:", e.message);
}
if (savedToken && savedToken.refresh_token) {
oauthClient.setToken(savedToken);
console.log("✅ Gespeicherter Token aus qbo_token.json geladen.");
} else {
const envToken = {
token_type: 'bearer',
access_token: process.env.QBO_ACCESS_TOKEN || '',
refresh_token: process.env.QBO_REFRESH_TOKEN || '',
expires_in: 3600,
x_refresh_token_expires_in: 8726400,
realmId: process.env.QBO_REALM_ID,
createdAt: new Date().toISOString()
};
if (envToken.refresh_token) {
oauthClient.setToken(envToken);
console.log(" Token aus .env geladen (Fallback).");
} else {
console.warn("⚠️ Kein gültiger Token vorhanden.");
}
}
}
return oauthClient;
};
function resetOAuthClient() {
oauthClient = null;
}
function saveTokens() {
try {
const client = getOAuthClient();
const token = client.getToken();
// Debug: Was genau bekommen wir vom Client?
console.log("💾 Speichere Token... refresh_token vorhanden:", !!token.refresh_token,
"| access_token Länge:", (token.access_token || '').length,
"| realmId:", token.realmId || 'FEHLT');
// Sicherstellen dass alle Pflichtfelder vorhanden sind
const tokenToSave = {
token_type: token.token_type || 'bearer',
access_token: token.access_token,
refresh_token: token.refresh_token,
expires_in: token.expires_in || 3600,
x_refresh_token_expires_in: token.x_refresh_token_expires_in || 8726400,
realmId: token.realmId || process.env.QBO_REALM_ID,
createdAt: token.createdAt || new Date().toISOString()
};
fs.writeFileSync(tokenFile, JSON.stringify(tokenToSave, null, 2));
console.log("💾 Tokens erfolgreich in qbo_token.json gespeichert.");
} catch (e) {
console.error("❌ Fehler beim Speichern der Tokens:", e.message);
}
}
async function makeQboApiCall(requestOptions) {
const client = getOAuthClient();
const currentToken = client.getToken();
if (!currentToken || !currentToken.refresh_token) {
throw new Error("Kein gültiger QBO Token vorhanden. Bitte Token erneuern.");
}
const doRefresh = async () => {
console.log("🔄 QBO Token Refresh wird ausgeführt...");
// Den Refresh Token als String extrahieren
const refreshTokenStr = currentToken.refresh_token;
console.log("🔑 Refresh Token (erste 15 Zeichen):", refreshTokenStr.substring(0, 15) + "...");
try {
// KRITISCHER FIX: refreshUsingToken() statt refresh() verwenden!
//
// refresh() ruft intern validateToken() auf, das bei unvollständigem
// Token-Objekt "The Refresh token is invalid" wirft — OHNE jemals
// Intuit zu kontaktieren.
//
// refreshUsingToken() akzeptiert den RT als String und umgeht das.
const authResponse = await client.refreshUsingToken(refreshTokenStr);
console.log("✅ Token erfolgreich erneuert via refreshUsingToken().");
saveTokens();
return authResponse;
} catch (e) {
const errMsg = e.originalMessage || e.message || String(e);
console.error("❌ Refresh fehlgeschlagen:", errMsg);
if (e.intuit_tid) console.error(" intuit_tid:", e.intuit_tid);
if (errMsg.includes('invalid_grant')) {
throw new Error(
"Der Refresh Token ist bei Intuit ungültig (invalid_grant). " +
"Bitte im Playground einen neuen Token holen und set_qbo_token.js ausführen."
);
}
throw e;
}
};
try {
const response = await client.makeApiCall(requestOptions);
const data = response.getJson ? response.getJson() : response.json;
if (data.fault && data.fault.error) {
const errorCode = data.fault.error[0].code;
if (errorCode === '3200' || errorCode === '3202' || errorCode === '3100') {
console.log(`⚠️ QBO meldet Token-Fehler (${errorCode}). Versuche Refresh und Retry...`);
await doRefresh();
return await client.makeApiCall(requestOptions);
}
throw new Error(`QBO API Error ${errorCode}: ${data.fault.error[0].message}`);
}
saveTokens();
return response;
} catch (e) {
const isAuthError =
e.response?.status === 401 ||
(e.authResponse && e.authResponse.response && e.authResponse.response.status === 401) ||
e.message?.includes('AuthenticationFailed');
if (isAuthError) {
console.log("⚠️ 401 Unauthorized / AuthFailed erhalten. Versuche Refresh und Retry...");
await doRefresh();
return await client.makeApiCall(requestOptions);
}
throw e;
}
}
module.exports = {
getOAuthClient,
makeQboApiCall,
saveTokens,
resetOAuthClient
};