#!/usr/bin/env node // import_qbo_payment.js — Importiert ein spezifisches QBO Payment in die lokale DB // // Verwendung: // node import_qbo_payment.js // // 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) 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 importPayment(qboPaymentId) { const oauthClient = getOAuthClient(); const companyId = oauthClient.getToken().realmId; const baseUrl = process.env.QBO_ENVIRONMENT === 'production' ? 'https://quickbooks.api.intuit.com' : 'https://sandbox-quickbooks.api.intuit.com'; console.log(`\n🔍 Lade QBO Payment ${qboPaymentId}...`); // 1. Payment aus QBO lesen const response = await makeQboApiCall({ url: `${baseUrl}/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); } console.log(`✅ Payment gefunden:`); 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 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(` Verknüpfte Invoices: ${linkedInvoices.length}`); 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'; 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)] ); 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'); return; } // Payment lokal anlegen const paymentResult = 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)] ); const localPaymentId = paymentResult.rows[0].id; console.log(`\n💾 Lokales Payment erstellt: ID ${localPaymentId}`); // Verknüpfte Invoices lokal finden und markieren let matchedCount = 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 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++; } else { console.log(` ⚠️ QBO Invoice ID ${li.qbo_invoice_id} nicht in lokaler DB gefunden.`); } } 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}`); } 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); } importPayment(qboPaymentId).catch(err => { console.error('Fatal:', err); process.exit(1); });