fix for qbo changes

This commit is contained in:
2026-04-01 17:13:04 -05:00
parent 96ed8c7141
commit 0b738ba530
2 changed files with 95 additions and 69 deletions

View File

@@ -409,6 +409,9 @@ router.post('/record-payment', async (req, res) => {
}
});
// Timestamp helper - add once at the top of qbo.js after the requires
const log = (msg) => console.log(`[${new Date().toISOString().replace('T',' ').substring(0,19)}] ${msg}`);
// POST sync payments from QBO
router.post('/sync-payments', async (req, res) => {
const dbClient = await pool.connect();
@@ -430,6 +433,7 @@ router.post('/sync-payments', async (req, res) => {
const companyId = oauthClient.getToken().realmId;
const baseUrl = getQboBaseUrl();
// ── Batch-fetch all invoices from QBO (max 50 per query) ──────────
const batchSize = 50;
const qboInvoices = new Map();
@@ -437,18 +441,59 @@ router.post('/sync-payments', async (req, res) => {
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));
(data.QueryResponse?.Invoice || []).forEach(inv => qboInvoices.set(inv.Id, inv));
}
console.log(`🔍 QBO Sync: ${openInvoices.length} offene Invoices, ${qboInvoices.size} aus QBO geladen`);
log(`🔍 QBO Sync: ${openInvoices.length} invoices checked, ${qboInvoices.size} loaded from QBO`);
// ── Collect all unique Payment IDs that need to be fetched ────────
// Instead of fetching each payment one-by-one, collect all IDs first
// then batch-fetch them in one query per 30 IDs
const paymentIdsToFetch = new Set();
for (const localInv of openInvoices) {
const qboInv = qboInvoices.get(localInv.qbo_id);
if (!qboInv || parseFloat(qboInv.Balance) !== 0) continue;
if (qboInv.LinkedTxn) {
for (const txn of qboInv.LinkedTxn) {
if (txn.TxnType === 'Payment') paymentIdsToFetch.add(txn.TxnId);
}
}
}
// Batch-fetch all payments in groups of 30
const UNDEPOSITED_FUNDS_ID = '221';
const paymentDepositMap = new Map(); // paymentId -> isDeposited (bool)
if (paymentIdsToFetch.size > 0) {
log(`💳 Fetching ${paymentIdsToFetch.size} unique payment(s) from QBO...`);
const pmIds = [...paymentIdsToFetch];
const pmBatchSize = 30;
for (let i = 0; i < pmIds.length; i += pmBatchSize) {
const batch = pmIds.slice(i, i + pmBatchSize);
const pmQuery = `SELECT Id, DepositToAccountRef FROM Payment WHERE Id IN (${batch.map(id => `'${id}'`).join(',')})`;
try {
const pmRes = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(pmQuery)}`,
method: 'GET'
});
const pmData = pmRes.getJson ? pmRes.getJson() : pmRes.json;
for (const pm of (pmData.QueryResponse?.Payment || [])) {
const isDeposited = pm.DepositToAccountRef?.value !== UNDEPOSITED_FUNDS_ID;
paymentDepositMap.set(pm.Id, isDeposited);
}
} catch (e) {
log(`⚠️ Payment batch fetch error (non-fatal): ${e.message}`);
}
}
log(`💳 Payment deposit status loaded for ${paymentDepositMap.size} payment(s)`);
}
// ── Process invoices ───────────────────────────────────────────────
let updated = 0;
let newPayments = 0;
@@ -459,34 +504,22 @@ router.post('/sync-payments', async (req, res) => {
if (!qboInv) continue;
const qboBalance = parseFloat(qboInv.Balance) || 0;
const qboTotal = parseFloat(qboInv.TotalAmt) || 0;
const localPaid = parseFloat(localInv.local_paid) || 0;
const qboTotal = parseFloat(qboInv.TotalAmt) || 0;
const localPaid = parseFloat(localInv.local_paid) || 0;
if (qboBalance === 0 && qboTotal > 0) {
const UNDEPOSITED_FUNDS_ID = '221';
// Determine Paid vs Deposited using pre-fetched map
let status = 'Paid';
if (qboInv.LinkedTxn) {
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 &&
payment.DepositToAccountRef.value !== UNDEPOSITED_FUNDS_ID) {
status = 'Deposited';
}
} catch (e) { /* ignore */ }
if (txn.TxnType === 'Payment' && paymentDepositMap.get(txn.TxnId) === true) {
status = 'Deposited';
break;
}
}
}
const needsUpdate = !localInv.paid_date || localInv.payment_status !== status;
if (needsUpdate) {
if (!localInv.paid_date || localInv.payment_status !== status) {
await dbClient.query(
`UPDATE invoices SET
paid_date = COALESCE(paid_date, CURRENT_DATE),
@@ -496,14 +529,16 @@ router.post('/sync-payments', async (req, res) => {
[status, localInv.id]
);
updated++;
console.log(` ✅ #${localInv.invoice_number}: ${status}`);
log(` ✅ #${localInv.invoice_number}: ${status}`);
}
const diff = qboTotal - localPaid;
if (diff > 0.01) {
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), 'Synced from QBO', CURRENT_TIMESTAMP)
VALUES (CURRENT_DATE, 'Synced from QBO', $1,
(SELECT customer_id FROM invoices WHERE id = $2),
'Synced from QBO', CURRENT_TIMESTAMP)
RETURNING id`,
[diff, localInv.id]
);
@@ -512,15 +547,14 @@ router.post('/sync-payments', async (req, res) => {
[payResult.rows[0].id, localInv.id, diff]
);
newPayments++;
console.log(` 💰 #${localInv.invoice_number}: +$${diff.toFixed(2)} payment synced`);
log(` 💰 #${localInv.invoice_number}: +$${diff.toFixed(2)} synced`);
}
} else if (qboBalance > 0 && qboBalance < qboTotal) {
const qboPaid = qboTotal - qboBalance;
const diff = qboPaid - localPaid;
const needsUpdate = localInv.payment_status !== 'Partial';
if (needsUpdate) {
if (localInv.payment_status !== 'Partial') {
await dbClient.query(
'UPDATE invoices SET payment_status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
['Partial', localInv.id]
@@ -531,7 +565,9 @@ router.post('/sync-payments', async (req, res) => {
if (diff > 0.01) {
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), 'Synced from QBO', CURRENT_TIMESTAMP)
VALUES (CURRENT_DATE, 'Synced from QBO', $1,
(SELECT customer_id FROM invoices WHERE id = $2),
'Synced from QBO', CURRENT_TIMESTAMP)
RETURNING id`,
[diff, localInv.id]
);
@@ -540,7 +576,7 @@ router.post('/sync-payments', async (req, res) => {
[payResult.rows[0].id, localInv.id, diff]
);
newPayments++;
console.log(` 📎 #${localInv.invoice_number}: Partial +$${diff.toFixed(2)} ($${qboPaid.toFixed(2)} of $${qboTotal.toFixed(2)})`);
log(` 📎 #${localInv.invoice_number}: Partial +$${diff.toFixed(2)} ($${qboPaid.toFixed(2)} / $${qboTotal.toFixed(2)})`);
}
}
}
@@ -552,7 +588,7 @@ router.post('/sync-payments', async (req, res) => {
await dbClient.query('COMMIT');
console.log(`✅ Sync abgeschlossen: ${updated} aktualisiert, ${newPayments} neue Payments`);
log(`✅ Sync complete: ${updated} updated, ${newPayments} new payments`);
res.json({
synced: updated,
new_payments: newPayments,
@@ -562,7 +598,7 @@ router.post('/sync-payments', async (req, res) => {
} catch (error) {
await dbClient.query('ROLLBACK').catch(() => {});
console.error('❌ Sync Error:', error);
log(`❌ Sync Error: ${error.message}`);
res.status(500).json({ error: 'Sync failed: ' + error.message });
} finally {
dbClient.release();