/** * Customer Routes * Handles customer CRUD operations and QBO sync */ const express = require('express'); const router = express.Router(); const { pool } = require('../config/database'); const { getOAuthClient, getQboBaseUrl, makeQboApiCall } = require('../config/qbo'); // GET all customers router.get('/', async (req, res) => { try { const result = await pool.query('SELECT * FROM customers ORDER BY name'); res.json(result.rows); } catch (error) { console.error('Error fetching customers:', error); res.status(500).json({ error: 'Error fetching customers' }); } }); // POST create customer router.post('/', async (req, res) => { const { name, contact, line1, line2, line3, line4, city, state, zip_code, account_number, email, secondary_email, phone, phone2, taxable, remarks } = req.body; try { const result = await pool.query( `INSERT INTO customers (name, contact, line1, line2, line3, line4, city, state, zip_code, account_number, email, secondary_email, phone, phone2, taxable, remarks) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING *`, [name, contact || null, line1 || null, line2 || null, line3 || null, line4 || null, city || null, state || null, zip_code || null, account_number || null, email || null, secondary_email || null, phone || null, phone2 || null, taxable !== undefined ? taxable : true, remarks || null] ); res.json(result.rows[0]); } catch (error) { console.error('Error creating customer:', error); res.status(500).json({ error: 'Error creating customer' }); } }); // PUT update customer router.put('/:id', async (req, res) => { const { id } = req.params; const { name, contact, line1, line2, line3, line4, city, state, zip_code, account_number, email, secondary_email, phone, phone2, taxable, remarks } = req.body; try { const result = await pool.query( `UPDATE customers SET name = $1, contact = $2, line1 = $3, line2 = $4, line3 = $5, line4 = $6, city = $7, state = $8, zip_code = $9, account_number = $10, email = $11, secondary_email = $12, phone = $13, phone2 = $14, taxable = $15, remarks = $16, updated_at = CURRENT_TIMESTAMP WHERE id = $17 RETURNING *`, [name, contact || null, line1 || null, line2 || null, line3 || null, line4 || null, city || null, state || null, zip_code || null, account_number || null, email || null, secondary_email || null, phone || null, phone2 || null, taxable !== undefined ? taxable : true, remarks || null, id] ); const customer = result.rows[0]; // QBO Update if (customer.qbo_id) { try { const oauthClient = getOAuthClient(); const companyId = oauthClient.getToken().realmId; const baseUrl = getQboBaseUrl(); // Get SyncToken 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) { 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, Notes: remarks || undefined }; // Contact → GivenName / FamilyName if (contact) { const parts = contact.trim().split(/\s+/); if (parts.length >= 2) { updatePayload.GivenName = parts[0]; updatePayload.FamilyName = parts.slice(1).join(' '); } else { updatePayload.GivenName = parts[0]; } } // Address 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); } } res.json(customer); } catch (error) { console.error('Error updating customer:', error); res.status(500).json({ error: 'Error updating customer' }); } }); // DELETE customer router.delete('/:id', async (req, res) => { const { id } = req.params; try { 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]; if (customer.qbo_id) { try { const oauthClient = getOAuthClient(); const companyId = oauthClient.getToken().realmId; const baseUrl = getQboBaseUrl(); 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); } } 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' }); } }); // POST export customer to QBO router.post('/:id/export-qbo', async (req, res) => { const { id } = req.params; try { 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]; if (customer.qbo_id) return res.status(400).json({ error: 'Customer already in QBO' }); const oauthClient = getOAuthClient(); const companyId = oauthClient.getToken().realmId; const baseUrl = getQboBaseUrl(); const qboCustomer = { DisplayName: customer.name, CompanyName: customer.name, PrimaryEmailAddr: customer.email ? { Address: customer.email } : undefined, PrimaryPhone: customer.phone ? { FreeFormNumber: customer.phone } : undefined, Taxable: customer.taxable !== false, Notes: customer.remarks || undefined }; if (customer.contact) { const parts = customer.contact.trim().split(/\s+/); if (parts.length >= 2) { qboCustomer.GivenName = parts[0]; qboCustomer.FamilyName = parts.slice(1).join(' '); } else { qboCustomer.GivenName = parts[0]; } } const addr = {}; if (customer.line1) addr.Line1 = customer.line1; if (customer.line2) addr.Line2 = customer.line2; if (customer.line3) addr.Line3 = customer.line3; if (customer.line4) addr.Line4 = customer.line4; if (customer.city) addr.City = customer.city; if (customer.state) addr.CountrySubDivisionCode = customer.state; if (customer.zip_code) addr.PostalCode = customer.zip_code; if (Object.keys(addr).length > 0) qboCustomer.BillAddr = addr; const response = await makeQboApiCall({ url: `${baseUrl}/v3/company/${companyId}/customer`, method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(qboCustomer) }); const data = response.getJson ? response.getJson() : response.json; const qboId = data.Customer?.Id; if (!qboId) throw new Error('QBO returned no ID'); await pool.query('UPDATE customers SET qbo_id = $1 WHERE id = $2', [qboId, id]); console.log(`✅ Customer "${customer.name}" exported to QBO (ID: ${qboId})`); res.json({ success: true, qbo_id: qboId, name: customer.name }); } catch (error) { console.error('QBO Customer Export Error:', error); res.status(500).json({ error: 'Export failed: ' + error.message }); } }); module.exports = router;