update qbo sync
This commit is contained in:
194
server.js
194
server.js
@@ -409,7 +409,6 @@ app.get('/api/invoices', async (req, res) => {
|
||||
LEFT JOIN customers c ON i.customer_id = c.id
|
||||
ORDER BY i.created_at DESC
|
||||
`);
|
||||
// balance berechnen
|
||||
const rows = result.rows.map(r => ({
|
||||
...r,
|
||||
amount_paid: parseFloat(r.amount_paid) || 0,
|
||||
@@ -458,10 +457,7 @@ app.get('/api/invoices/:id', async (req, res) => {
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
invoice: invoice,
|
||||
items: itemsResult.rows
|
||||
});
|
||||
res.json({ invoice, items: itemsResult.rows });
|
||||
} catch (error) {
|
||||
console.error('Error fetching invoice:', error);
|
||||
res.status(500).json({ error: 'Error fetching invoice' });
|
||||
@@ -469,8 +465,6 @@ app.get('/api/invoices/:id', async (req, res) => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
app.post('/api/invoices', async (req, res) => {
|
||||
const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, created_from_quote_id, scheduled_send_date } = req.body;
|
||||
|
||||
@@ -2062,6 +2056,192 @@ app.get('/api/qbo/labor-rate', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- 3. Sync Payments from QBO ---
|
||||
// Prüft alle offenen lokalen Invoices gegen QBO.
|
||||
// Aktualisiert paid_date und payment_status (Paid/Deposited).
|
||||
app.post('/api/qbo/sync-payments', async (req, res) => {
|
||||
const dbClient = await pool.connect();
|
||||
try {
|
||||
// Alle lokalen Invoices die in QBO sind aber noch nicht voll bezahlt
|
||||
const openResult = await dbClient.query(`
|
||||
SELECT i.id, i.qbo_id, i.invoice_number, i.total, i.paid_date, i.payment_status,
|
||||
COALESCE((SELECT SUM(pi.amount) FROM payment_invoices pi WHERE pi.invoice_id = i.id), 0) as local_paid
|
||||
FROM invoices i
|
||||
WHERE i.qbo_id IS NOT NULL
|
||||
AND (i.paid_date IS NULL OR i.payment_status IS NULL OR i.payment_status != 'Deposited')
|
||||
`);
|
||||
|
||||
const openInvoices = openResult.rows;
|
||||
if (openInvoices.length === 0) {
|
||||
await dbClient.query("UPDATE settings SET value = $1 WHERE key = 'last_payment_sync'", [new Date().toISOString()]);
|
||||
return res.json({ synced: 0, message: 'All invoices up to date.' });
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
// QBO Invoices in Batches laden (max 50 IDs pro Query)
|
||||
const batchSize = 50;
|
||||
const qboInvoices = new Map();
|
||||
|
||||
for (let i = 0; i < openInvoices.length; i += batchSize) {
|
||||
const batch = openInvoices.slice(i, i + batchSize);
|
||||
const ids = batch.map(inv => `'${inv.qbo_id}'`).join(',');
|
||||
const query = `SELECT Id, DocNumber, Balance, TotalAmt, LinkedTxn FROM Invoice WHERE Id IN (${ids})`;
|
||||
|
||||
const response = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(query)}`,
|
||||
method: 'GET'
|
||||
});
|
||||
const data = response.getJson ? response.getJson() : response.json;
|
||||
const invoices = data.QueryResponse?.Invoice || [];
|
||||
invoices.forEach(inv => qboInvoices.set(inv.Id, inv));
|
||||
}
|
||||
|
||||
console.log(`🔍 QBO Sync: ${openInvoices.length} offene Invoices, ${qboInvoices.size} aus QBO geladen`);
|
||||
|
||||
let updated = 0;
|
||||
let newPayments = 0;
|
||||
|
||||
await dbClient.query('BEGIN');
|
||||
|
||||
for (const localInv of openInvoices) {
|
||||
const qboInv = qboInvoices.get(localInv.qbo_id);
|
||||
if (!qboInv) continue;
|
||||
|
||||
const qboBalance = parseFloat(qboInv.Balance) || 0;
|
||||
const qboTotal = parseFloat(qboInv.TotalAmt) || 0;
|
||||
const localPaid = parseFloat(localInv.local_paid) || 0;
|
||||
|
||||
// Prüfe ob in QBO bezahlt/teilweise bezahlt
|
||||
if (qboBalance === 0 && qboTotal > 0) {
|
||||
// Voll bezahlt in QBO
|
||||
// Prüfe ob "Deposited" — dafür müssen wir LinkedTxn prüfen
|
||||
// Wenn Deposit vorhanden → Deposited, sonst → Paid
|
||||
let status = 'Paid';
|
||||
|
||||
// LinkedTxn aus der Invoice prüfen
|
||||
if (qboInv.LinkedTxn) {
|
||||
// Lade die Payments um zu prüfen ob sie deposited sind
|
||||
for (const txn of qboInv.LinkedTxn) {
|
||||
if (txn.TxnType === 'Payment') {
|
||||
try {
|
||||
const pmRes = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/payment/${txn.TxnId}`,
|
||||
method: 'GET'
|
||||
});
|
||||
const pmData = pmRes.getJson ? pmRes.getJson() : pmRes.json;
|
||||
const payment = pmData.Payment;
|
||||
if (payment && payment.DepositToAccountRef) {
|
||||
// Hat DepositToAccount → wurde deposited
|
||||
status = 'Deposited';
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// paid_date setzen falls noch nicht
|
||||
if (!localInv.paid_date) {
|
||||
await dbClient.query(
|
||||
'UPDATE invoices SET paid_date = CURRENT_DATE, payment_status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
||||
[status, localInv.id]
|
||||
);
|
||||
} else {
|
||||
await dbClient.query(
|
||||
'UPDATE invoices SET payment_status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
||||
[status, localInv.id]
|
||||
);
|
||||
}
|
||||
|
||||
// Fehlenden lokalen Payment-Eintrag erstellen wenn nötig
|
||||
const qboPaid = qboTotal;
|
||||
if (qboPaid > localPaid) {
|
||||
const diff = qboPaid - localPaid;
|
||||
// Einen generischen Payment-Eintrag für den Differenzbetrag
|
||||
const payResult = await dbClient.query(
|
||||
`INSERT INTO payments (payment_date, payment_method, total_amount, customer_id, notes, created_at)
|
||||
VALUES (CURRENT_DATE, 'Synced from QBO', $1, (SELECT customer_id FROM invoices WHERE id = $2), 'Auto-synced from QBO', CURRENT_TIMESTAMP)
|
||||
RETURNING id`,
|
||||
[diff, localInv.id]
|
||||
);
|
||||
await dbClient.query(
|
||||
'INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3)',
|
||||
[payResult.rows[0].id, localInv.id, diff]
|
||||
);
|
||||
newPayments++;
|
||||
}
|
||||
|
||||
console.log(` ✅ #${localInv.invoice_number}: ${status} (QBO Balance: $${qboBalance})`);
|
||||
updated++;
|
||||
|
||||
} else if (qboBalance > 0 && qboBalance < qboTotal) {
|
||||
// Teilweise bezahlt in QBO
|
||||
const qboPaid = qboTotal - qboBalance;
|
||||
if (qboPaid > localPaid) {
|
||||
const diff = qboPaid - localPaid;
|
||||
const payResult = await dbClient.query(
|
||||
`INSERT INTO payments (payment_date, payment_method, total_amount, customer_id, notes, created_at)
|
||||
VALUES (CURRENT_DATE, 'Synced from QBO', $1, (SELECT customer_id FROM invoices WHERE id = $2), 'Auto-synced partial from QBO', CURRENT_TIMESTAMP)
|
||||
RETURNING id`,
|
||||
[diff, localInv.id]
|
||||
);
|
||||
await dbClient.query(
|
||||
'INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3)',
|
||||
[payResult.rows[0].id, localInv.id, diff]
|
||||
);
|
||||
|
||||
await dbClient.query(
|
||||
'UPDATE invoices SET payment_status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
||||
['Partial', localInv.id]
|
||||
);
|
||||
|
||||
console.log(` 📎 #${localInv.invoice_number}: Partial ($${qboPaid.toFixed(2)} of $${qboTotal.toFixed(2)})`);
|
||||
updated++;
|
||||
newPayments++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last sync timestamp speichern
|
||||
await dbClient.query(`
|
||||
INSERT INTO settings (key, value) VALUES ('last_payment_sync', $1)
|
||||
ON CONFLICT (key) DO UPDATE SET value = $1
|
||||
`, [new Date().toISOString()]);
|
||||
|
||||
await dbClient.query('COMMIT');
|
||||
|
||||
console.log(`✅ Sync abgeschlossen: ${updated} aktualisiert, ${newPayments} neue Payments`);
|
||||
res.json({
|
||||
synced: updated,
|
||||
new_payments: newPayments,
|
||||
total_checked: openInvoices.length,
|
||||
message: `${updated} invoice(s) updated, ${newPayments} new payment(s) synced.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await dbClient.query('ROLLBACK').catch(() => {});
|
||||
console.error('❌ Sync Error:', error);
|
||||
res.status(500).json({ error: 'Sync failed: ' + error.message });
|
||||
} finally {
|
||||
dbClient.release();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- 4. Last sync timestamp ---
|
||||
app.get('/api/qbo/last-sync', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query("SELECT value FROM settings WHERE key = 'last_payment_sync'");
|
||||
res.json({ last_sync: result.rows[0]?.value || null });
|
||||
} catch (error) {
|
||||
res.json({ last_sync: null });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Start server and browser
|
||||
async function startServer() {
|
||||
|
||||
Reference in New Issue
Block a user