From ab2f064de9ee5acbb508229ae625206d0c423cb6 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Tue, 24 Feb 2026 18:40:57 -0600 Subject: [PATCH] fix --- server.js | 383 +++++++++++++++++++++++++++++------------------------- 1 file changed, 207 insertions(+), 176 deletions(-) diff --git a/server.js b/server.js index ebe4ff9..8203069 100644 --- a/server.js +++ b/server.js @@ -10,9 +10,6 @@ const { makeQboApiCall, getOAuthClient, saveTokens, resetOAuthClient } = require const app = express(); const PORT = process.env.PORT || 3000; -const QBO_LABOR_ID = '5'; -const QBO_PARTS_ID = '9'; - // Global browser instance let browser = null; @@ -124,6 +121,15 @@ async function getNextInvoiceNumber() { return String(parseInt(result.rows[0].max_number) + 1); } +// ===================================================== +// INVOICE CREATE + UPDATE — Auto QBO Export/Sync +// ERSETZE POST /api/invoices und PUT /api/invoices/:id +// ===================================================== + +const QBO_LABOR_ID = '5'; +const QBO_PARTS_ID = '9'; + + // --- Helper: QBO Invoice Export (create) --- async function exportInvoiceToQbo(invoiceId, client) { const invoiceRes = await client.query(` @@ -155,8 +161,14 @@ async function exportInvoiceToQbo(invoiceId, client) { let nextDocNumber = (parseInt(maxNumResult.rows[0].max_num) + 1).toString(); const lineItems = items.map(item => { - const rate = parseFloat(item.rate.replace(/[^0-9.]/g, '')) || 0; - const amount = parseFloat(item.amount.replace(/[^0-9.]/g, '')) || 0; + const parseNum = (val) => { + if (val === null || val === undefined) return 0; + if (typeof val === 'number') return val; + return parseFloat(String(val).replace(/[^0-9.\-]/g, '')) || 0; + }; + const rate = parseNum(item.rate); + const qty = parseNum(item.quantity) || 1; + const amount = rate * qty; const itemRefId = item.qbo_item_id || QBO_PARTS_ID; const itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts"; return { @@ -166,7 +178,7 @@ async function exportInvoiceToQbo(invoiceId, client) { "SalesItemLineDetail": { "ItemRef": { "value": itemRefId, "name": itemRefName }, "UnitPrice": rate, - "Qty": parseFloat(item.quantity) || 1 + "Qty": qty } }; }); @@ -250,10 +262,20 @@ async function syncInvoiceToQbo(invoiceId, client) { if (currentSyncToken === undefined) throw new Error('Could not get SyncToken from QBO'); const lineItems = itemsRes.rows.map(item => { - const rate = parseFloat(item.rate.replace(/[^0-9.]/g, '')) || 0; - const amount = parseFloat(item.amount.replace(/[^0-9.]/g, '')) || 0; + // Robust parsing: handle both string ("$1,250.00") and numeric types + const parseNum = (val) => { + if (val === null || val === undefined) return 0; + if (typeof val === 'number') return val; + return parseFloat(String(val).replace(/[^0-9.\-]/g, '')) || 0; + }; + const rate = parseNum(item.rate); + const qty = parseNum(item.quantity) || 1; + const amount = rate * qty; // Always compute amount = rate * qty for QBO const itemRefId = item.qbo_item_id || QBO_PARTS_ID; const itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts"; + + console.log(` 📋 Item: qty=${qty}, rate=${rate}, amount=${amount}, ref=${itemRefId}`); + return { "DetailType": "SalesItemLineDetail", "Amount": amount, @@ -309,6 +331,183 @@ async function syncInvoiceToQbo(invoiceId, client) { } +// ===================================================== +// POST /api/invoices — Create + Auto QBO Export +// ===================================================== +app.post('/api/invoices', async (req, res) => { + const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, scheduled_send_date, created_from_quote_id } = req.body; + + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + // invoice_number kann leer sein — wird von QBO vergeben + // Falls angegeben, validieren + if (invoice_number && !/^\d+$/.test(invoice_number)) { + await client.query('ROLLBACK'); + return res.status(400).json({ error: 'Invoice number must be numeric.' }); + } + + // Temporäre Nummer falls leer + const tempNumber = invoice_number || `DRAFT-${Date.now()}`; + + if (invoice_number) { + const existing = await client.query('SELECT id FROM invoices WHERE invoice_number = $1', [invoice_number]); + if (existing.rows.length > 0) { + await client.query('ROLLBACK'); + return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` }); + } + } + + let subtotal = 0; + for (const item of items) { + const amount = parseFloat(item.amount.replace(/[$,]/g, '')); + if (!isNaN(amount)) subtotal += amount; + } + + const tax_rate = 8.25; + const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); + const total = subtotal + tax_amount; + + const invoiceResult = await client.query( + `INSERT INTO invoices (invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date, created_from_quote_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`, + [tempNumber, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, created_from_quote_id] + ); + const invoiceId = invoiceResult.rows[0].id; + + for (let i = 0; i < items.length; i++) { + await client.query( + 'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id) VALUES ($1, $2, $3, $4, $5, $6, $7)', + [invoiceId, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9'] + ); + } + + await client.query('COMMIT'); + + // Auto QBO Export (falls Kunde in QBO) + let qboResult = null; + try { + qboResult = await exportInvoiceToQbo(invoiceId, client); + if (qboResult.skipped) { + console.log(`ℹ️ Invoice ${invoiceId} not exported to QBO: ${qboResult.reason}`); + } + } catch (qboErr) { + console.error(`⚠️ Auto QBO export failed for Invoice ${invoiceId}:`, qboErr.message); + // Nicht abbrechen — lokal wurde gespeichert + } + + res.json({ + ...invoiceResult.rows[0], + qbo_id: qboResult?.qbo_id || null, + qbo_doc_number: qboResult?.qbo_doc_number || null + }); + + } catch (error) { + await client.query('ROLLBACK'); + console.error('Error creating invoice:', error); + res.status(500).json({ error: 'Error creating invoice' }); + } finally { + client.release(); + } +}); + + +// ===================================================== +// PUT /api/invoices/:id — Update + Auto QBO Sync +// ===================================================== +app.put('/api/invoices/:id', async (req, res) => { + const { id } = req.params; + const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, scheduled_send_date } = req.body; + + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + // Invoice-Nummer validieren (falls angegeben) + if (invoice_number && !/^\d+$/.test(invoice_number)) { + await client.query('ROLLBACK'); + return res.status(400).json({ error: 'Invoice number must be numeric.' }); + } + + if (invoice_number) { + const existing = await client.query('SELECT id FROM invoices WHERE invoice_number = $1 AND id != $2', [invoice_number, id]); + if (existing.rows.length > 0) { + await client.query('ROLLBACK'); + return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` }); + } + } + + let subtotal = 0; + for (const item of items) { + const amount = parseFloat(item.amount.replace(/[$,]/g, '')); + if (!isNaN(amount)) subtotal += amount; + } + + const tax_rate = 8.25; + const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); + const total = subtotal + tax_amount; + + // Update lokal — invoice_number nur ändern wenn angegeben + if (invoice_number) { + await client.query( + `UPDATE invoices SET invoice_number = $1, customer_id = $2, invoice_date = $3, terms = $4, auth_code = $5, tax_exempt = $6, + tax_rate = $7, subtotal = $8, tax_amount = $9, total = $10, scheduled_send_date = $11, updated_at = CURRENT_TIMESTAMP + WHERE id = $12`, + [invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, id] + ); + } else { + await client.query( + `UPDATE invoices SET customer_id = $1, invoice_date = $2, terms = $3, auth_code = $4, tax_exempt = $5, + tax_rate = $6, subtotal = $7, tax_amount = $8, total = $9, scheduled_send_date = $10, updated_at = CURRENT_TIMESTAMP + WHERE id = $11`, + [customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, id] + ); + } + + // Items neu schreiben + await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]); + for (let i = 0; i < items.length; i++) { + await client.query( + 'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id) VALUES ($1, $2, $3, $4, $5, $6, $7)', + [id, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9'] + ); + } + + await client.query('COMMIT'); + + // Auto QBO: Export if not yet in QBO, Sync if already in QBO + let qboResult = null; + try { + // Prüfe ob Invoice schon in QBO + const checkRes = await client.query('SELECT qbo_id FROM invoices WHERE id = $1', [id]); + const hasQboId = !!checkRes.rows[0]?.qbo_id; + + if (hasQboId) { + qboResult = await syncInvoiceToQbo(id, client); + } else { + qboResult = await exportInvoiceToQbo(id, client); + } + + if (qboResult.skipped) { + console.log(`ℹ️ Invoice ${id}: ${qboResult.reason}`); + } + } catch (qboErr) { + console.error(`⚠️ Auto QBO failed for Invoice ${id}:`, qboErr.message); + } + + res.json({ success: true, qbo_synced: !!qboResult?.success, qbo_id: qboResult?.qbo_id || null, qbo_doc_number: qboResult?.qbo_doc_number || null }); + + } catch (error) { + await client.query('ROLLBACK'); + console.error('Error updating invoice:', error); + res.status(500).json({ error: 'Error updating invoice' }); + } finally { + client.release(); + } +}); + + // Logo endpoints app.get('/api/logo-info', async (req, res) => { try { @@ -771,85 +970,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, scheduled_send_date, created_from_quote_id } = req.body; - - const client = await pool.connect(); - try { - await client.query('BEGIN'); - - // invoice_number kann leer sein — wird von QBO vergeben - // Falls angegeben, validieren - if (invoice_number && !/^\d+$/.test(invoice_number)) { - await client.query('ROLLBACK'); - return res.status(400).json({ error: 'Invoice number must be numeric.' }); - } - - // Temporäre Nummer falls leer - const tempNumber = invoice_number || `DRAFT-${Date.now()}`; - - if (invoice_number) { - const existing = await client.query('SELECT id FROM invoices WHERE invoice_number = $1', [invoice_number]); - if (existing.rows.length > 0) { - await client.query('ROLLBACK'); - return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` }); - } - } - - let subtotal = 0; - for (const item of items) { - const amount = parseFloat(item.amount.replace(/[$,]/g, '')); - if (!isNaN(amount)) subtotal += amount; - } - - const tax_rate = 8.25; - const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); - const total = subtotal + tax_amount; - - const invoiceResult = await client.query( - `INSERT INTO invoices (invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date, created_from_quote_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`, - [tempNumber, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, created_from_quote_id] - ); - const invoiceId = invoiceResult.rows[0].id; - - for (let i = 0; i < items.length; i++) { - await client.query( - 'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id) VALUES ($1, $2, $3, $4, $5, $6, $7)', - [invoiceId, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9'] - ); - } - - await client.query('COMMIT'); - - // Auto QBO Export (falls Kunde in QBO) - let qboResult = null; - try { - qboResult = await exportInvoiceToQbo(invoiceId, client); - if (qboResult.skipped) { - console.log(`ℹ️ Invoice ${invoiceId} not exported to QBO: ${qboResult.reason}`); - } - } catch (qboErr) { - console.error(`⚠️ Auto QBO export failed for Invoice ${invoiceId}:`, qboErr.message); - // Nicht abbrechen — lokal wurde gespeichert - } - - res.json({ - ...invoiceResult.rows[0], - qbo_id: qboResult?.qbo_id || null, - qbo_doc_number: qboResult?.qbo_doc_number || null - }); - - } catch (error) { - await client.query('ROLLBACK'); - console.error('Error creating invoice:', error); - res.status(500).json({ error: 'Error creating invoice' }); - } finally { - client.release(); - } -}); - - app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => { const { id } = req.params; @@ -916,96 +1036,7 @@ app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => { } }); -app.put('/api/invoices/:id', async (req, res) => { - const { id } = req.params; - const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, scheduled_send_date } = req.body; - const client = await pool.connect(); - try { - await client.query('BEGIN'); - - // Invoice-Nummer validieren (falls angegeben) - if (invoice_number && !/^\d+$/.test(invoice_number)) { - await client.query('ROLLBACK'); - return res.status(400).json({ error: 'Invoice number must be numeric.' }); - } - - if (invoice_number) { - const existing = await client.query('SELECT id FROM invoices WHERE invoice_number = $1 AND id != $2', [invoice_number, id]); - if (existing.rows.length > 0) { - await client.query('ROLLBACK'); - return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` }); - } - } - - let subtotal = 0; - for (const item of items) { - const amount = parseFloat(item.amount.replace(/[$,]/g, '')); - if (!isNaN(amount)) subtotal += amount; - } - - const tax_rate = 8.25; - const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); - const total = subtotal + tax_amount; - - // Update lokal — invoice_number nur ändern wenn angegeben - if (invoice_number) { - await client.query( - `UPDATE invoices SET invoice_number = $1, customer_id = $2, invoice_date = $3, terms = $4, auth_code = $5, tax_exempt = $6, - tax_rate = $7, subtotal = $8, tax_amount = $9, total = $10, scheduled_send_date = $11, updated_at = CURRENT_TIMESTAMP - WHERE id = $12`, - [invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, id] - ); - } else { - await client.query( - `UPDATE invoices SET customer_id = $1, invoice_date = $2, terms = $3, auth_code = $4, tax_exempt = $5, - tax_rate = $6, subtotal = $7, tax_amount = $8, total = $9, scheduled_send_date = $10, updated_at = CURRENT_TIMESTAMP - WHERE id = $11`, - [customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, id] - ); - } - - // Items neu schreiben - await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]); - for (let i = 0; i < items.length; i++) { - await client.query( - 'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id) VALUES ($1, $2, $3, $4, $5, $6, $7)', - [id, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9'] - ); - } - - await client.query('COMMIT'); - - // Auto QBO: Export if not yet in QBO, Sync if already in QBO - let qboResult = null; - try { - // Prüfe ob Invoice schon in QBO - const checkRes = await client.query('SELECT qbo_id FROM invoices WHERE id = $1', [id]); - const hasQboId = !!checkRes.rows[0]?.qbo_id; - - if (hasQboId) { - qboResult = await syncInvoiceToQbo(id, client); - } else { - qboResult = await exportInvoiceToQbo(id, client); - } - - if (qboResult.skipped) { - console.log(`ℹ️ Invoice ${id}: ${qboResult.reason}`); - } - } catch (qboErr) { - console.error(`⚠️ Auto QBO failed for Invoice ${id}:`, qboErr.message); - } - - res.json({ success: true, qbo_synced: !!qboResult?.success, qbo_id: qboResult?.qbo_id || null, qbo_doc_number: qboResult?.qbo_doc_number || null }); - - } catch (error) { - await client.query('ROLLBACK'); - console.error('Error updating invoice:', error); - res.status(500).json({ error: 'Error updating invoice' }); - } finally { - client.release(); - } -}); app.delete('/api/invoices/:id', async (req, res) => { const { id } = req.params;