#!/usr/bin/env node // import_qbo_payment.js — Importiert ein QBO Payment in die lokale DB // // Suche nach Payment über: // --invoice Findet Payment über die Invoice // --ref Findet Payment über Referenznummer // --payment Direkt über interne 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 require('dotenv').config(); const { Pool } = require('pg'); const { makeQboApiCall, getOAuthClient } = require('./qbo_helper'); const pool = new Pool({ user: process.env.DB_USER || 'postgres', host: process.env.DB_HOST || 'localhost', database: process.env.DB_NAME || 'quotes_db', password: process.env.DB_PASSWORD || 'postgres', port: process.env.DB_PORT || 5432, }); async function getBaseUrl() { const oauthClient = getOAuthClient(); const companyId = oauthClient.getToken().realmId; const base = process.env.QBO_ENVIRONMENT === 'production' ? 'https://quickbooks.api.intuit.com' : 'https://sandbox-quickbooks.api.intuit.com'; return { base, companyId }; } // --- Suche: Payment über Invoice finden --- async function findPaymentByInvoice(invoiceRef) { const { base, companyId } = await getBaseUrl(); console.log(`\n🔍 Suche Invoice "${invoiceRef}" in QBO...`); // Zuerst als DocNumber suchen (das ist was du siehst) let invoice = null; const query = `SELECT * FROM Invoice WHERE DocNumber = '${invoiceRef}'`; const qRes = await makeQboApiCall({ url: `${base}/v3/company/${companyId}/query?query=${encodeURI(query)}`, 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 payment = data.Payment; if (!payment) { console.error('❌ Payment nicht gefunden.'); return; } console.log(`\n✅ Payment geladen:`); console.log(` QBO ID: ${payment.Id}`); console.log(` Datum: ${payment.TxnDate}`); console.log(` Betrag: $${payment.TotalAmt}`); console.log(` Referenz: ${payment.PaymentRefNum || '(keine)'}`); console.log(` Kunde: ${payment.CustomerRef?.name || payment.CustomerRef?.value}`); // Verknüpfte Invoices const linkedInvoices = []; if (payment.Line) { for (const line of payment.Line) { if (line.LinkedTxn) { for (const txn of line.LinkedTxn) { if (txn.TxnType === 'Invoice') { linkedInvoices.push({ 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}`)); // Namen auflösen let paymentMethodName = 'Unknown'; if (payment.PaymentMethodRef?.value) { try { const pmRes = await makeQboApiCall({ url: `${base}/v3/company/${companyId}/paymentmethod/${payment.PaymentMethodRef.value}`, method: 'GET' }); paymentMethodName = (pmRes.getJson ? pmRes.getJson() : pmRes.json).PaymentMethod?.Name || 'Unknown'; } catch (e) { /* ok */ } } let depositToName = ''; if (payment.DepositToAccountRef?.value) { try { const accRes = await makeQboApiCall({ url: `${base}/v3/company/${companyId}/account/${payment.DepositToAccountRef.value}`, method: 'GET' }); depositToName = (accRes.getJson ? accRes.getJson() : accRes.json).Account?.Name || ''; } catch (e) { /* ok */ } } console.log(` Methode: ${paymentMethodName}`); console.log(` Konto: ${depositToName}`); // --- 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) { console.log(`\n⚠️ Bereits importiert (lokale ID: ${existing.rows[0].id}). Übersprungen.`); return; } await dbClient.query('BEGIN'); 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) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, [payment.TxnDate, payment.PaymentRefNum || null, paymentMethodName, depositToName, payment.TotalAmt, customerId, String(payment.Id)] ); const localId = payResult.rows[0].id; console.log(`\n💾 Payment lokal gespeichert: ID ${localId}`); let matched = 0; for (const li of linkedInvoices) { const invResult = await dbClient.query('SELECT id, invoice_number FROM invoices WHERE qbo_id = $1', [li.qbo_invoice_id]); if (invResult.rows.length > 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('UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND paid_date IS NULL', [payment.TxnDate, inv.id]); console.log(` ✅ Invoice #${inv.invoice_number || inv.id} → bezahlt`); matched++; } else { console.log(` ⚠️ QBO Invoice ${li.qbo_invoice_id} nicht in lokaler DB`); } } await dbClient.query('COMMIT'); console.log(`\n✅ Fertig: ${matched}/${linkedInvoices.length} Invoices verknüpft.`); } catch (error) { await dbClient.query('ROLLBACK').catch(() => {}); console.error('❌ Fehler:', error); } finally { dbClient.release(); } } // --- Main --- async function main() { const args = process.argv.slice(2); if (args.length < 2) { console.log(` Verwendung: node import_qbo_payment.js --invoice node import_qbo_payment.js --ref node import_qbo_payment.js --payment 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); } let qboPaymentId = null; if (args[0] === '--payment') qboPaymentId = args[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); });