250 lines
9.9 KiB
JavaScript
250 lines
9.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// import_qbo_payment.js — Importiert ein QBO Payment in die lokale DB
|
|
//
|
|
// Suche nach Payment über:
|
|
// --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
|
|
//
|
|
// 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 <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);
|
|
}
|
|
|
|
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); }); |