265 lines
11 KiB
JavaScript
265 lines
11 KiB
JavaScript
/**
|
|
* 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; |