Files
invoice-system/qbo_helper.js
2026-04-01 17:13:04 -05:00

171 lines
6.3 KiB
JavaScript
Raw Permalink 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;
let _lastSavedAccessToken = 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();
// ── NEU: Nur speichern wenn access_token sich tatsächlich geändert hat ──
if (token.access_token === _lastSavedAccessToken) {
return; // Token unverändert kein Save, kein Log
}
_lastSavedAccessToken = token.access_token;
const ts = new Date().toISOString().replace('T',' ').substring(0,19);
console.log(`[${ts}] 💾 Token changed saving (realmId: ${token.realmId || 'FEHLT'})`);
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(`[${ts}] 💾 Token saved to qbo_token.json`);
} catch (e) {
console.error(`❌ Fehler beim Speichern der Tokens: ${e.message}`);
}
}
async function makeQboApiCall(requestOptions) {
const client = getOAuthClient();
const ts = () => new Date().toISOString().replace('T',' ').substring(0,19);
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(`[${ts()}] 🔄 QBO Token Refresh...`);
const refreshTokenStr = currentToken.refresh_token;
try {
const authResponse = await client.refreshUsingToken(refreshTokenStr);
console.log(`[${ts()}] ✅ Token refreshed via refreshUsingToken()`);
saveTokens(); // saveTokens prüft selbst ob sich was geändert hat
return authResponse;
} catch (e) {
const errMsg = e.originalMessage || e.message || String(e);
console.error(`[${ts()}] ❌ Refresh failed: ${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(`[${ts()}] ⚠️ QBO Token-Fehler (${errorCode}) Refresh & Retry...`);
await doRefresh();
return await client.makeApiCall(requestOptions);
}
throw new Error(`QBO API Error ${errorCode}: ${data.fault.error[0].message}`);
}
// ── Kein saveTokens() hier Token hat sich nicht geändert ──
return response;
} catch (e) {
const isAuthError =
e.response?.status === 401 ||
(e.authResponse?.response?.status === 401) ||
e.message?.includes('AuthenticationFailed');
if (isAuthError) {
console.log(`[${ts()}] ⚠️ 401 Refresh & Retry...`);
await doRefresh();
return await client.makeApiCall(requestOptions);
}
throw e;
}
}
module.exports = {
getOAuthClient,
makeQboApiCall,
saveTokens,
resetOAuthClient
};