This commit is contained in:
Andreas Knuth 2026-02-19 15:01:51 -06:00
parent b24a360fba
commit a9465aa812
1 changed files with 176 additions and 121 deletions

View File

@ -1,17 +1,15 @@
#!/usr/bin/env node #!/usr/bin/env node
// import_qbo_payment.js — Importiert ein spezifisches QBO Payment in die lokale DB // import_qbo_payment.js — Importiert ein QBO Payment in die lokale DB
// //
// Verwendung: // Suche nach Payment über:
// node import_qbo_payment.js <QBO_PAYMENT_ID> // --invoice <QBO_INVOICE_ID_OR_DOCNUMBER> Findet Payment über die Invoice
// --ref <CHECK_NUMBER> Findet Payment über Referenznummer
// --payment <QBO_PAYMENT_ID> Direkt über interne QBO Payment ID
// //
// Beispiel: // Beispiele:
// node import_qbo_payment.js 20616 // node import_qbo_payment.js --invoice 110483
// // node import_qbo_payment.js --ref 20616
// Was passiert: // node import_qbo_payment.js --payment 456
// 1. Payment aus QBO laden (inkl. LinkedTxn → verknüpfte Invoices)
// 2. Lokale Invoices anhand qbo_id finden
// 3. Payment in lokale payments-Tabelle schreiben
// 4. Invoices als bezahlt markieren (paid_date)
require('dotenv').config(); require('dotenv').config();
const { Pool } = require('pg'); const { Pool } = require('pg');
@ -25,171 +23,228 @@ const pool = new Pool({
port: process.env.DB_PORT || 5432, port: process.env.DB_PORT || 5432,
}); });
async function importPayment(qboPaymentId) { async function getBaseUrl() {
const oauthClient = getOAuthClient(); const oauthClient = getOAuthClient();
const companyId = oauthClient.getToken().realmId; const companyId = oauthClient.getToken().realmId;
const baseUrl = process.env.QBO_ENVIRONMENT === 'production' const base = process.env.QBO_ENVIRONMENT === 'production'
? 'https://quickbooks.api.intuit.com' ? 'https://quickbooks.api.intuit.com'
: 'https://sandbox-quickbooks.api.intuit.com'; : 'https://sandbox-quickbooks.api.intuit.com';
return { base, companyId };
}
console.log(`\n🔍 Lade QBO Payment ${qboPaymentId}...`); // --- Suche: Payment über Invoice finden ---
async function findPaymentByInvoice(invoiceRef) {
const { base, companyId } = await getBaseUrl();
console.log(`\n🔍 Suche Invoice "${invoiceRef}" in QBO...`);
// 1. Payment aus QBO lesen // Zuerst als DocNumber suchen (das ist was du siehst)
const response = await makeQboApiCall({ let invoice = null;
url: `${baseUrl}/v3/company/${companyId}/payment/${qboPaymentId}`, const query = `SELECT * FROM Invoice WHERE DocNumber = '${invoiceRef}'`;
const qRes = await makeQboApiCall({
url: `${base}/v3/company/${companyId}/query?query=${encodeURI(query)}`,
method: 'GET' method: 'GET'
}); });
const qData = qRes.getJson ? qRes.getJson() : qRes.json;
if (qData.QueryResponse?.Invoice?.length > 0) {
invoice = qData.QueryResponse.Invoice[0];
} else {
// Fallback: direkt als ID versuchen
try {
const invRes = await makeQboApiCall({
url: `${base}/v3/company/${companyId}/invoice/${invoiceRef}`,
method: 'GET'
});
const invData = invRes.getJson ? invRes.getJson() : invRes.json;
invoice = invData.Invoice;
} catch (e) { /* ignore */ }
}
if (!invoice) {
console.error(`❌ Invoice "${invoiceRef}" nicht in QBO gefunden.`);
return null;
}
console.log(` ✅ Invoice: ID ${invoice.Id}, DocNumber ${invoice.DocNumber}, Balance: $${invoice.Balance}, Kunde: ${invoice.CustomerRef?.name}`);
// Verknüpfte Payments aus LinkedTxn
if (invoice.LinkedTxn && invoice.LinkedTxn.length > 0) {
const paymentLinks = invoice.LinkedTxn.filter(lt => lt.TxnType === 'Payment');
if (paymentLinks.length > 0) {
console.log(` 📎 ${paymentLinks.length} verknüpfte Payment(s):`);
for (const pl of paymentLinks) {
console.log(` QBO Payment ID: ${pl.TxnId}`);
}
return paymentLinks[0].TxnId;
}
}
console.log(` ⚠️ Keine Payments verknüpft (Balance: $${invoice.Balance}).`);
return null;
}
// --- Suche: Payment über Reference Number ---
async function findPaymentByRef(refNumber) {
const { base, companyId } = await getBaseUrl();
console.log(`\n🔍 Suche Payment mit Reference Number "${refNumber}"...`);
const query = `SELECT * FROM Payment WHERE PaymentRefNum = '${refNumber}'`;
const response = await makeQboApiCall({
url: `${base}/v3/company/${companyId}/query?query=${encodeURI(query)}`,
method: 'GET'
});
const data = response.getJson ? response.getJson() : response.json;
const payments = data.QueryResponse?.Payment || [];
if (payments.length === 0) {
console.log(` ❌ Kein Payment mit Ref "${refNumber}" gefunden.`);
return null;
}
console.log(`${payments.length} Payment(s) gefunden:`);
for (const p of payments) {
console.log(` ID: ${p.Id}, Datum: ${p.TxnDate}, Betrag: $${p.TotalAmt}, Ref: ${p.PaymentRefNum}`);
}
return payments[0].Id;
}
// --- Payment laden und lokal importieren ---
async function importPayment(qboPaymentId) {
const { base, companyId } = await getBaseUrl();
console.log(`\n📥 Lade QBO Payment ID ${qboPaymentId}...`);
const response = await makeQboApiCall({
url: `${base}/v3/company/${companyId}/payment/${qboPaymentId}`,
method: 'GET'
});
const data = response.getJson ? response.getJson() : response.json; const data = response.getJson ? response.getJson() : response.json;
const payment = data.Payment; const payment = data.Payment;
if (!payment) { if (!payment) { console.error('❌ Payment nicht gefunden.'); return; }
console.error('❌ Payment nicht gefunden in QBO.');
process.exit(1);
}
console.log(`✅ Payment gefunden:`); console.log(`\n✅ Payment geladen:`);
console.log(` QBO ID: ${payment.Id}`);
console.log(` Datum: ${payment.TxnDate}`); console.log(` Datum: ${payment.TxnDate}`);
console.log(` Betrag: $${payment.TotalAmt}`); console.log(` Betrag: $${payment.TotalAmt}`);
console.log(` Referenz: ${payment.PaymentRefNum || '(keine)'}`); console.log(` Referenz: ${payment.PaymentRefNum || '(keine)'}`);
console.log(` Kunde: ${payment.CustomerRef?.name || payment.CustomerRef?.value}`); console.log(` Kunde: ${payment.CustomerRef?.name || payment.CustomerRef?.value}`);
// 2. Verknüpfte Invoices aus dem Payment extrahieren // Verknüpfte Invoices
const linkedInvoices = []; const linkedInvoices = [];
if (payment.Line) { if (payment.Line) {
for (const line of payment.Line) { for (const line of payment.Line) {
if (line.LinkedTxn) { if (line.LinkedTxn) {
for (const txn of line.LinkedTxn) { for (const txn of line.LinkedTxn) {
if (txn.TxnType === 'Invoice') { if (txn.TxnType === 'Invoice') {
linkedInvoices.push({ linkedInvoices.push({ qbo_invoice_id: txn.TxnId, amount: line.Amount });
qbo_invoice_id: txn.TxnId,
amount: line.Amount
});
} }
} }
} }
} }
} }
console.log(` Invoices: ${linkedInvoices.length}`);
linkedInvoices.forEach(li => console.log(` - QBO Invoice ${li.qbo_invoice_id}: $${li.amount}`));
console.log(` Verknüpfte Invoices: ${linkedInvoices.length}`); // Namen auflösen
linkedInvoices.forEach(li => {
console.log(` - QBO Invoice ID: ${li.qbo_invoice_id}, Amount: $${li.amount}`);
});
// 3. Lokale Invoices finden
const dbClient = await pool.connect();
try {
await dbClient.query('BEGIN');
// Kunden-ID lokal finden
const customerResult = await dbClient.query(
'SELECT id FROM customers WHERE qbo_id = $1',
[payment.CustomerRef?.value]
);
const customerId = customerResult.rows[0]?.id || null;
// PaymentMethod-Name aus QBO holen (optional)
let paymentMethodName = 'Unknown'; let paymentMethodName = 'Unknown';
if (payment.PaymentMethodRef?.value) { if (payment.PaymentMethodRef?.value) {
try { try {
const pmRes = await makeQboApiCall({ const pmRes = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/paymentmethod/${payment.PaymentMethodRef.value}`, url: `${base}/v3/company/${companyId}/paymentmethod/${payment.PaymentMethodRef.value}`,
method: 'GET' method: 'GET'
}); });
const pmData = pmRes.getJson ? pmRes.getJson() : pmRes.json; paymentMethodName = (pmRes.getJson ? pmRes.getJson() : pmRes.json).PaymentMethod?.Name || 'Unknown';
paymentMethodName = pmData.PaymentMethod?.Name || 'Unknown'; } catch (e) { /* ok */ }
} catch (e) {
console.log(' ⚠️ PaymentMethod konnte nicht geladen werden.');
}
} }
// DepositTo-Account-Name aus QBO (optional)
let depositToName = ''; let depositToName = '';
if (payment.DepositToAccountRef?.value) { if (payment.DepositToAccountRef?.value) {
try { try {
const accRes = await makeQboApiCall({ const accRes = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/account/${payment.DepositToAccountRef.value}`, url: `${base}/v3/company/${companyId}/account/${payment.DepositToAccountRef.value}`,
method: 'GET' method: 'GET'
}); });
const accData = accRes.getJson ? accRes.getJson() : accRes.json; depositToName = (accRes.getJson ? accRes.getJson() : accRes.json).Account?.Name || '';
depositToName = accData.Account?.Name || ''; } catch (e) { /* ok */ }
} catch (e) {
console.log(' ⚠️ Account konnte nicht geladen werden.');
}
} }
// Prüfen ob Payment schon importiert console.log(` Methode: ${paymentMethodName}`);
const existing = await dbClient.query( console.log(` Konto: ${depositToName}`);
'SELECT id FROM payments WHERE qbo_payment_id = $1',
[String(qboPaymentId)] // --- DB ---
); const dbClient = await pool.connect();
try {
const existing = await dbClient.query('SELECT id FROM payments WHERE qbo_payment_id = $1', [String(payment.Id)]);
if (existing.rows.length > 0) { if (existing.rows.length > 0) {
console.log(`\n⚠️ Payment ${qboPaymentId} wurde bereits importiert (lokale ID: ${existing.rows[0].id}).`); console.log(`\n⚠️ Bereits importiert (lokale ID: ${existing.rows[0].id}). Übersprungen.`);
console.log(' Übersprungen.');
await dbClient.query('ROLLBACK');
return; return;
} }
// Payment lokal anlegen await dbClient.query('BEGIN');
const paymentResult = await dbClient.query(
const custResult = await dbClient.query('SELECT id FROM customers WHERE qbo_id = $1', [payment.CustomerRef?.value]);
const customerId = custResult.rows[0]?.id || null;
const payResult = await dbClient.query(
`INSERT INTO payments (payment_date, reference_number, payment_method, deposit_to_account, total_amount, customer_id, qbo_payment_id) `INSERT INTO payments (payment_date, reference_number, payment_method, deposit_to_account, total_amount, customer_id, qbo_payment_id)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
[payment.TxnDate, payment.PaymentRefNum || null, paymentMethodName, depositToName, payment.TotalAmt, customerId, String(qboPaymentId)] [payment.TxnDate, payment.PaymentRefNum || null, paymentMethodName, depositToName, payment.TotalAmt, customerId, String(payment.Id)]
); );
const localPaymentId = paymentResult.rows[0].id; const localId = payResult.rows[0].id;
console.log(`\n💾 Lokales Payment erstellt: ID ${localPaymentId}`); console.log(`\n💾 Payment lokal gespeichert: ID ${localId}`);
// Verknüpfte Invoices lokal finden und markieren let matched = 0;
let matchedCount = 0;
for (const li of linkedInvoices) { for (const li of linkedInvoices) {
const invResult = await dbClient.query( const invResult = await dbClient.query('SELECT id, invoice_number FROM invoices WHERE qbo_id = $1', [li.qbo_invoice_id]);
'SELECT id, invoice_number FROM invoices WHERE qbo_id = $1',
[li.qbo_invoice_id]
);
if (invResult.rows.length > 0) { if (invResult.rows.length > 0) {
const localInv = invResult.rows[0]; const inv = invResult.rows[0];
await dbClient.query('INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING', [localId, inv.id, li.amount]);
await dbClient.query( await dbClient.query('UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND paid_date IS NULL', [payment.TxnDate, inv.id]);
'INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING', console.log(` ✅ Invoice #${inv.invoice_number || inv.id} → bezahlt`);
[localPaymentId, localInv.id, li.amount] matched++;
);
await dbClient.query(
'UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND paid_date IS NULL',
[payment.TxnDate, localInv.id]
);
console.log(` ✅ Invoice #${localInv.invoice_number || localInv.id} (QBO: ${li.qbo_invoice_id}) → bezahlt`);
matchedCount++;
} else { } else {
console.log(` ⚠️ QBO Invoice ID ${li.qbo_invoice_id} nicht in lokaler DB gefunden.`); console.log(` ⚠️ QBO Invoice ${li.qbo_invoice_id} nicht in lokaler DB`);
} }
} }
await dbClient.query('COMMIT'); await dbClient.query('COMMIT');
console.log(`\n✅ Fertig: ${matched}/${linkedInvoices.length} Invoices verknüpft.`);
console.log(`\n✅ Import abgeschlossen: ${matchedCount}/${linkedInvoices.length} Invoices verknüpft.`);
console.log(` Payment: Lokal ID ${localPaymentId}, QBO ID ${qboPaymentId}`);
console.log(` Methode: ${paymentMethodName}, Konto: ${depositToName}`);
} catch (error) { } catch (error) {
await dbClient.query('ROLLBACK').catch(() => {}); await dbClient.query('ROLLBACK').catch(() => {});
console.error('❌ Fehler:', error); console.error('❌ Fehler:', error);
} finally { } finally {
dbClient.release(); dbClient.release();
await pool.end();
} }
} }
// --- Main --- // --- Main ---
const qboPaymentId = process.argv[2]; async function main() {
if (!qboPaymentId) { const args = process.argv.slice(2);
console.log('Verwendung: node import_qbo_payment.js <QBO_PAYMENT_ID>'); if (args.length < 2) {
console.log('Beispiel: node import_qbo_payment.js 20616'); console.log(`
Verwendung:
node import_qbo_payment.js --invoice <DOCNUMBER_OR_QBO_ID>
node import_qbo_payment.js --ref <CHECK_NUMBER>
node import_qbo_payment.js --payment <QBO_PAYMENT_ID>
Beispiele:
node import_qbo_payment.js --invoice 110483
node import_qbo_payment.js --ref 20616
node import_qbo_payment.js --payment 456
`);
process.exit(1); process.exit(1);
} }
importPayment(qboPaymentId).catch(err => { let qboPaymentId = null;
console.error('Fatal:', err); if (args[0] === '--payment') qboPaymentId = args[1];
process.exit(1); else if (args[0] === '--invoice') qboPaymentId = await findPaymentByInvoice(args[1]);
}); else if (args[0] === '--ref') qboPaymentId = await findPaymentByRef(args[1]);
else { console.error(`Unbekannt: ${args[0]}`); process.exit(1); }
if (!qboPaymentId) { console.error('\n❌ Payment nicht gefunden.'); process.exit(1); }
await importPayment(qboPaymentId);
await pool.end();
}
main().catch(err => { console.error('Fatal:', err); process.exit(1); });