From 5e63adfee8e12b2828676870b1115abd25d3642f Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 20 Feb 2026 18:03:33 -0600 Subject: [PATCH] update delete endpoints --- server.js | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 174 insertions(+), 7 deletions(-) diff --git a/server.js b/server.js index a946d7c..f1a8d0e 100644 --- a/server.js +++ b/server.js @@ -193,12 +193,13 @@ app.post('/api/customers', async (req, res) => { // PUT /api/customers/:id app.put('/api/customers/:id', async (req, res) => { const { id } = req.params; - const { - name, line1, line2, line3, line4, city, state, zip_code, - account_number, email, phone, phone2, taxable + const { + name, line1, line2, line3, line4, city, state, zip_code, + account_number, email, phone, phone2, taxable } = req.body; - + try { + // Lokal updaten const result = await pool.query( `UPDATE customers SET name = $1, line1 = $2, line2 = $3, line3 = $4, line4 = $5, @@ -206,12 +207,73 @@ app.put('/api/customers/:id', async (req, res) => { phone = $11, phone2 = $12, taxable = $13, updated_at = CURRENT_TIMESTAMP WHERE id = $14 RETURNING *`, - [name, line1 || null, line2 || null, line3 || null, line4 || null, - city || null, state || null, zip_code || null, + [name, line1 || null, line2 || null, line3 || null, line4 || null, + city || null, state || null, zip_code || null, account_number || null, email || null, phone || null, phone2 || null, taxable !== undefined ? taxable : true, id] ); - res.json(result.rows[0]); + + const customer = result.rows[0]; + + // In QBO updaten falls vorhanden + if (customer.qbo_id) { + try { + 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'; + + // Aktuellen SyncToken holen + const qboRes = await makeQboApiCall({ + url: `${baseUrl}/v3/company/${companyId}/customer/${customer.qbo_id}`, + method: 'GET' + }); + const qboData = qboRes.getJson ? qboRes.getJson() : qboRes.json; + const syncToken = qboData.Customer?.SyncToken; + + if (syncToken !== undefined) { + // Sparse update + const updatePayload = { + Id: customer.qbo_id, + SyncToken: syncToken, + sparse: true, + DisplayName: name, + CompanyName: name, + PrimaryEmailAddr: email ? { Address: email } : undefined, + PrimaryPhone: phone ? { FreeFormNumber: phone } : undefined, + Taxable: taxable !== false + }; + + // Adresse + const addr = {}; + if (line1) addr.Line1 = line1; + if (line2) addr.Line2 = line2; + if (line3) addr.Line3 = line3; + if (line4) addr.Line4 = line4; + if (city) addr.City = city; + if (state) addr.CountrySubDivisionCode = state; + if (zip_code) addr.PostalCode = zip_code; + if (Object.keys(addr).length > 0) updatePayload.BillAddr = addr; + + console.log(`📤 Updating QBO Customer ${customer.qbo_id} (${name})...`); + + await makeQboApiCall({ + url: `${baseUrl}/v3/company/${companyId}/customer`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updatePayload) + }); + + console.log(`✅ QBO Customer ${customer.qbo_id} updated.`); + } + } catch (qboError) { + console.error(`⚠️ QBO update failed for Customer ${customer.qbo_id}:`, qboError.message); + // Nicht abbrechen — lokales Update war erfolgreich + } + } + + res.json(customer); } catch (error) { console.error('Error updating customer:', error); res.status(500).json({ error: 'Error updating customer' }); @@ -220,9 +282,60 @@ app.put('/api/customers/:id', async (req, res) => { app.delete('/api/customers/:id', async (req, res) => { const { id } = req.params; + try { + // Kunde laden + const custResult = await pool.query('SELECT * FROM customers WHERE id = $1', [id]); + if (custResult.rows.length === 0) { + return res.status(404).json({ error: 'Customer not found' }); + } + + const customer = custResult.rows[0]; + + // In QBO deaktivieren falls vorhanden + if (customer.qbo_id) { + try { + 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'; + + // SyncToken holen + const qboRes = await makeQboApiCall({ + url: `${baseUrl}/v3/company/${companyId}/customer/${customer.qbo_id}`, + method: 'GET' + }); + const qboData = qboRes.getJson ? qboRes.getJson() : qboRes.json; + const syncToken = qboData.Customer?.SyncToken; + + if (syncToken !== undefined) { + console.log(`🗑️ Deactivating QBO Customer ${customer.qbo_id} (${customer.name})...`); + + await makeQboApiCall({ + url: `${baseUrl}/v3/company/${companyId}/customer`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + Id: customer.qbo_id, + SyncToken: syncToken, + sparse: true, + Active: false + }) + }); + + console.log(`✅ QBO Customer ${customer.qbo_id} deactivated.`); + } + } catch (qboError) { + console.error(`⚠️ QBO deactivate failed for Customer ${customer.qbo_id}:`, qboError.message); + // Trotzdem lokal löschen + } + } + + // Lokal löschen await pool.query('DELETE FROM customers WHERE id = $1', [id]); res.json({ success: true }); + } catch (error) { console.error('Error deleting customer:', error); res.status(500).json({ error: 'Error deleting customer' }); @@ -672,9 +785,63 @@ app.delete('/api/invoices/:id', async (req, res) => { const client = await pool.connect(); try { await client.query('BEGIN'); + + // Invoice laden um qbo_id zu prüfen + const invResult = await client.query('SELECT qbo_id, qbo_sync_token, invoice_number FROM invoices WHERE id = $1', [id]); + if (invResult.rows.length === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ error: 'Invoice not found' }); + } + + const invoice = invResult.rows[0]; + + // In QBO löschen falls vorhanden + if (invoice.qbo_id) { + try { + 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'; + + // Aktuellen SyncToken aus QBO holen (sicherste Methode) + const qboRes = await makeQboApiCall({ + url: `${baseUrl}/v3/company/${companyId}/invoice/${invoice.qbo_id}`, + method: 'GET' + }); + const qboData = qboRes.getJson ? qboRes.getJson() : qboRes.json; + const syncToken = qboData.Invoice?.SyncToken; + + if (syncToken !== undefined) { + // QBO Invoice "voiden" (nicht löschen — QBO empfiehlt Void statt Delete) + // Void setzt Balance auf 0 und markiert als nichtig + console.log(`🗑️ Voiding QBO Invoice ${invoice.qbo_id} (DocNumber: ${invoice.invoice_number})...`); + + await makeQboApiCall({ + url: `${baseUrl}/v3/company/${companyId}/invoice?operation=void`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + Id: invoice.qbo_id, + SyncToken: syncToken + }) + }); + + console.log(`✅ QBO Invoice ${invoice.qbo_id} voided.`); + } + } catch (qboError) { + // QBO-Fehler loggen aber lokales Löschen trotzdem durchführen + console.error(`⚠️ QBO void failed for Invoice ${invoice.qbo_id}:`, qboError.message); + // Nicht abbrechen — lokal trotzdem löschen + } + } + + // Lokal löschen (payment_invoices hat ON DELETE CASCADE) await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]); + await client.query('DELETE FROM payment_invoices WHERE invoice_id = $1', [id]); await client.query('DELETE FROM invoices WHERE id = $1', [id]); await client.query('COMMIT'); + res.json({ success: true }); } catch (error) { await client.query('ROLLBACK');