diff --git a/import_qbo_payment.js b/import_qbo_payment.js index e237b8e..5975d4f 100644 --- a/import_qbo_payment.js +++ b/import_qbo_payment.js @@ -1,17 +1,15 @@ #!/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: -// node import_qbo_payment.js +// Suche nach Payment über: +// --invoice Findet Payment über die Invoice +// --ref Findet Payment über Referenznummer +// --payment Direkt über interne QBO Payment ID // -// Beispiel: -// node import_qbo_payment.js 20616 -// -// Was passiert: -// 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) +// 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'); @@ -25,171 +23,228 @@ const pool = new Pool({ port: process.env.DB_PORT || 5432, }); -async function importPayment(qboPaymentId) { +async function getBaseUrl() { const oauthClient = getOAuthClient(); 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://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 - const response = await makeQboApiCall({ - url: `${baseUrl}/v3/company/${companyId}/payment/${qboPaymentId}`, + // 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 in QBO.'); - process.exit(1); - } + if (!payment) { console.error('❌ Payment nicht gefunden.'); return; } - console.log(`✅ Payment gefunden:`); + 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}`); - // 2. Verknüpfte Invoices aus dem Payment extrahieren + // 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 - }); + 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}`)); - console.log(` Verknüpfte Invoices: ${linkedInvoices.length}`); - linkedInvoices.forEach(li => { - console.log(` - QBO Invoice ID: ${li.qbo_invoice_id}, Amount: $${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 */ } + } - // 3. Lokale Invoices finden + 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 { - 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'; - if (payment.PaymentMethodRef?.value) { - try { - const pmRes = await makeQboApiCall({ - url: `${baseUrl}/v3/company/${companyId}/paymentmethod/${payment.PaymentMethodRef.value}`, - method: 'GET' - }); - const pmData = pmRes.getJson ? pmRes.getJson() : pmRes.json; - paymentMethodName = pmData.PaymentMethod?.Name || 'Unknown'; - } catch (e) { - console.log(' ⚠️ PaymentMethod konnte nicht geladen werden.'); - } - } - - // DepositTo-Account-Name aus QBO (optional) - let depositToName = ''; - if (payment.DepositToAccountRef?.value) { - try { - const accRes = await makeQboApiCall({ - url: `${baseUrl}/v3/company/${companyId}/account/${payment.DepositToAccountRef.value}`, - method: 'GET' - }); - const accData = accRes.getJson ? accRes.getJson() : accRes.json; - depositToName = accData.Account?.Name || ''; - } catch (e) { - console.log(' ⚠️ Account konnte nicht geladen werden.'); - } - } - - // Prüfen ob Payment schon importiert - const existing = await dbClient.query( - 'SELECT id FROM payments WHERE qbo_payment_id = $1', - [String(qboPaymentId)] - ); + 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⚠️ Payment ${qboPaymentId} wurde bereits importiert (lokale ID: ${existing.rows[0].id}).`); - console.log(' Übersprungen.'); - await dbClient.query('ROLLBACK'); + console.log(`\n⚠️ Bereits importiert (lokale ID: ${existing.rows[0].id}). Übersprungen.`); return; } - // Payment lokal anlegen - const paymentResult = await dbClient.query( + 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 *`, - [payment.TxnDate, payment.PaymentRefNum || null, paymentMethodName, depositToName, payment.TotalAmt, customerId, String(qboPaymentId)] + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, + [payment.TxnDate, payment.PaymentRefNum || null, paymentMethodName, depositToName, payment.TotalAmt, customerId, String(payment.Id)] ); - const localPaymentId = paymentResult.rows[0].id; - console.log(`\n💾 Lokales Payment erstellt: ID ${localPaymentId}`); + const localId = payResult.rows[0].id; + console.log(`\n💾 Payment lokal gespeichert: ID ${localId}`); - // Verknüpfte Invoices lokal finden und markieren - let matchedCount = 0; + 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] - ); - + 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 localInv = invResult.rows[0]; - - await dbClient.query( - 'INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING', - [localPaymentId, localInv.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, localInv.id] - ); - - console.log(` ✅ Invoice #${localInv.invoice_number || localInv.id} (QBO: ${li.qbo_invoice_id}) → bezahlt`); - matchedCount++; + 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 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'); - - 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}`); - + console.log(`\n✅ Fertig: ${matched}/${linkedInvoices.length} Invoices verknüpft.`); } catch (error) { await dbClient.query('ROLLBACK').catch(() => {}); console.error('❌ Fehler:', error); } finally { dbClient.release(); - await pool.end(); } } // --- Main --- -const qboPaymentId = process.argv[2]; -if (!qboPaymentId) { - console.log('Verwendung: node import_qbo_payment.js '); - console.log('Beispiel: node import_qbo_payment.js 20616'); - process.exit(1); +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(); } -importPayment(qboPaymentId).catch(err => { - console.error('Fatal:', err); - process.exit(1); -}); \ No newline at end of file +main().catch(err => { console.error('Fatal:', err); process.exit(1); }); \ No newline at end of file