update delete endpoints

This commit is contained in:
Andreas Knuth 2026-02-20 18:03:33 -06:00
parent c44fc7f63e
commit 5e63adfee8
1 changed files with 174 additions and 7 deletions

181
server.js
View File

@ -193,12 +193,13 @@ app.post('/api/customers', async (req, res) => {
// PUT /api/customers/:id // PUT /api/customers/:id
app.put('/api/customers/:id', async (req, res) => { app.put('/api/customers/:id', async (req, res) => {
const { id } = req.params; const { id } = req.params;
const { const {
name, line1, line2, line3, line4, city, state, zip_code, name, line1, line2, line3, line4, city, state, zip_code,
account_number, email, phone, phone2, taxable account_number, email, phone, phone2, taxable
} = req.body; } = req.body;
try { try {
// Lokal updaten
const result = await pool.query( const result = await pool.query(
`UPDATE customers `UPDATE customers
SET name = $1, line1 = $2, line2 = $3, line3 = $4, line4 = $5, 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 phone = $11, phone2 = $12, taxable = $13, updated_at = CURRENT_TIMESTAMP
WHERE id = $14 WHERE id = $14
RETURNING *`, RETURNING *`,
[name, line1 || null, line2 || null, line3 || null, line4 || null, [name, line1 || null, line2 || null, line3 || null, line4 || null,
city || null, state || null, zip_code || null, city || null, state || null, zip_code || null,
account_number || null, email || null, phone || null, phone2 || null, account_number || null, email || null, phone || null, phone2 || null,
taxable !== undefined ? taxable : true, id] 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) { } catch (error) {
console.error('Error updating customer:', error); console.error('Error updating customer:', error);
res.status(500).json({ error: 'Error updating customer' }); 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) => { app.delete('/api/customers/:id', async (req, res) => {
const { id } = req.params; const { id } = req.params;
try { 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]); await pool.query('DELETE FROM customers WHERE id = $1', [id]);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Error deleting customer:', error); console.error('Error deleting customer:', error);
res.status(500).json({ error: 'Error deleting customer' }); 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(); const client = await pool.connect();
try { try {
await client.query('BEGIN'); 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 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('DELETE FROM invoices WHERE id = $1', [id]);
await client.query('COMMIT'); await client.query('COMMIT');
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');