Compare commits
No commits in common. "27ecafea5ff8b0b2354b4e0bb056f3f782f8a40d" and "7226883a2e793bd081fe0d05891ea4b099bab091" have entirely different histories.
27ecafea5f
...
7226883a2e
|
|
@ -252,7 +252,7 @@ function renderInvoiceRow(invoice) {
|
||||||
|
|
||||||
// Mark Sent button (right side) — only when open, not paid/partial
|
// Mark Sent button (right side) — only when open, not paid/partial
|
||||||
let sendBtn = '';
|
let sendBtn = '';
|
||||||
if (hasQbo && !paid && !overdue && invoice.email_status !== 'sent') {
|
if (hasQbo && !paid && !partial && !overdue && invoice.email_status !== 'sent') {
|
||||||
sendBtn = `<button onclick="window.invoiceView.setEmailStatus(${invoice.id}, 'sent')" class="text-indigo-600 hover:text-indigo-800 text-xs font-medium" title="Mark as sent to customer">📤 Mark Sent</button>`;
|
sendBtn = `<button onclick="window.invoiceView.setEmailStatus(${invoice.id}, 'sent')" class="text-indigo-600 hover:text-indigo-800 text-xs font-medium" title="Mark as sent to customer">📤 Mark Sent</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
// src/config/qbo.js
|
|
||||||
const OAuthClient = require('intuit-oauth');
|
const OAuthClient = require('intuit-oauth');
|
||||||
const {
|
const { getOAuthClient: getClient, saveTokens, resetOAuthClient } = require('../../qbo_helper');
|
||||||
getOAuthClient: getClient,
|
|
||||||
saveTokens,
|
|
||||||
resetOAuthClient,
|
|
||||||
makeQboApiCall // <-- NEU: Direkt hier mit importieren
|
|
||||||
} = require('../../qbo_helper');
|
|
||||||
|
|
||||||
function getOAuthClient() {
|
function getOAuthClient() {
|
||||||
return getClient();
|
return getClient();
|
||||||
|
|
@ -22,6 +16,5 @@ module.exports = {
|
||||||
getOAuthClient,
|
getOAuthClient,
|
||||||
getQboBaseUrl,
|
getQboBaseUrl,
|
||||||
saveTokens,
|
saveTokens,
|
||||||
resetOAuthClient,
|
resetOAuthClient
|
||||||
makeQboApiCall // <-- NEU: Und sauber weiterreichen
|
|
||||||
};
|
};
|
||||||
44
src/index.js
44
src/index.js
|
|
@ -3,13 +3,8 @@
|
||||||
* Modularized Backend
|
* Modularized Backend
|
||||||
*/
|
*/
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
|
||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer');
|
||||||
|
|
||||||
// Import config
|
|
||||||
const { pool } = require('./config/database');
|
|
||||||
const { OAuthClient, getOAuthClient, saveTokens } = require('./config/qbo');
|
|
||||||
|
|
||||||
// Import routes
|
// Import routes
|
||||||
const customerRoutes = require('./routes/customers');
|
const customerRoutes = require('./routes/customers');
|
||||||
const quoteRoutes = require('./routes/quotes');
|
const quoteRoutes = require('./routes/quotes');
|
||||||
|
|
@ -54,7 +49,6 @@ async function initBrowser() {
|
||||||
browser.on('disconnected', () => {
|
browser.on('disconnected', () => {
|
||||||
console.log('[BROWSER] Browser disconnected, restarting...');
|
console.log('[BROWSER] Browser disconnected, restarting...');
|
||||||
browser = null;
|
browser = null;
|
||||||
setBrowser(null);
|
|
||||||
initBrowser();
|
initBrowser();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -63,42 +57,9 @@ async function initBrowser() {
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
app.use(express.static('public'));
|
||||||
|
|
||||||
// =====================================================
|
// Mount routes
|
||||||
// QBO OAuth Routes — mounted at root level (not under /api/qbo)
|
|
||||||
// These must match the Intuit callback URL configuration
|
|
||||||
// =====================================================
|
|
||||||
app.get('/auth/qbo', (req, res) => {
|
|
||||||
const client = getOAuthClient();
|
|
||||||
const authUri = client.authorizeUri({
|
|
||||||
scope: [OAuthClient.scopes.Accounting],
|
|
||||||
state: 'intuit-qbo-auth'
|
|
||||||
});
|
|
||||||
console.log('🔗 Redirecting to QBO Authorization:', authUri);
|
|
||||||
res.redirect(authUri);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/auth/qbo/callback', async (req, res) => {
|
|
||||||
const client = getOAuthClient();
|
|
||||||
try {
|
|
||||||
const authResponse = await client.createToken(req.url);
|
|
||||||
console.log('✅ QBO Authorization erfolgreich!');
|
|
||||||
saveTokens();
|
|
||||||
res.redirect('/#settings');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('❌ QBO Authorization fehlgeschlagen:', e);
|
|
||||||
res.status(500).send(`
|
|
||||||
<h2>QBO Authorization Failed</h2>
|
|
||||||
<p>${e.message || e}</p>
|
|
||||||
<a href="/">Zurück zur App</a>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// API Routes
|
|
||||||
// =====================================================
|
|
||||||
app.use('/api/customers', customerRoutes);
|
app.use('/api/customers', customerRoutes);
|
||||||
app.use('/api/quotes', quoteRoutes);
|
app.use('/api/quotes', quoteRoutes);
|
||||||
app.use('/api/invoices', invoiceRoutes);
|
app.use('/api/invoices', invoiceRoutes);
|
||||||
|
|
@ -120,6 +81,7 @@ process.on('SIGTERM', async () => {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
|
const { pool } = require('./config/database');
|
||||||
await pool.end();
|
await pool.end();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { pool } = require('../config/database');
|
const { pool } = require('../config/database');
|
||||||
const { getOAuthClient, getQboBaseUrl, makeQboApiCall } = require('../config/qbo');
|
const { getOAuthClient, getQboBaseUrl } = require('../config/qbo');
|
||||||
|
const { makeQboApiCall } = require('../../qbo_helper');
|
||||||
|
|
||||||
// GET all customers
|
// GET all customers
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ const { getNextInvoiceNumber } = require('../utils/numberGenerators');
|
||||||
const { formatDate, formatMoney } = require('../utils/helpers');
|
const { formatDate, formatMoney } = require('../utils/helpers');
|
||||||
const { getBrowser, generatePdfFromHtml, getLogoHtml, renderInvoiceItems, formatAddressLines } = require('../services/pdf-service');
|
const { getBrowser, generatePdfFromHtml, getLogoHtml, renderInvoiceItems, formatAddressLines } = require('../services/pdf-service');
|
||||||
const { exportInvoiceToQbo, syncInvoiceToQbo } = require('../services/qbo-service');
|
const { exportInvoiceToQbo, syncInvoiceToQbo } = require('../services/qbo-service');
|
||||||
const { getOAuthClient, getQboBaseUrl, makeQboApiCall } = require('../config/qbo');
|
const { getOAuthClient, getQboBaseUrl } = require('../config/qbo');
|
||||||
|
const { makeQboApiCall } = require('../../qbo_helper');
|
||||||
|
|
||||||
// GET all invoices
|
// GET all invoices
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
|
|
@ -132,7 +133,7 @@ router.post('/', async (req, res) => {
|
||||||
// Auto QBO Export
|
// Auto QBO Export
|
||||||
let qboResult = null;
|
let qboResult = null;
|
||||||
try {
|
try {
|
||||||
qboResult = await exportInvoiceToQbo(invoiceId, client);
|
qboResult = await exportInvoiceToQbo(invoiceId, pool);
|
||||||
if (qboResult.skipped) {
|
if (qboResult.skipped) {
|
||||||
console.log(`ℹ️ Invoice ${invoiceId} not exported to QBO: ${qboResult.reason}`);
|
console.log(`ℹ️ Invoice ${invoiceId} not exported to QBO: ${qboResult.reason}`);
|
||||||
}
|
}
|
||||||
|
|
@ -223,9 +224,9 @@ router.put('/:id', async (req, res) => {
|
||||||
const hasQboId = !!checkRes.rows[0]?.qbo_id;
|
const hasQboId = !!checkRes.rows[0]?.qbo_id;
|
||||||
|
|
||||||
if (hasQboId) {
|
if (hasQboId) {
|
||||||
qboResult = await syncInvoiceToQbo(id, client);
|
qboResult = await syncInvoiceToQbo(id, pool);
|
||||||
} else {
|
} else {
|
||||||
qboResult = await exportInvoiceToQbo(id, client);
|
qboResult = await exportInvoiceToQbo(id, pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (qboResult.skipped) {
|
if (qboResult.skipped) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* QBO Routes
|
* QBO Routes
|
||||||
* Handles QBO sync and data operations
|
* Handles QBO OAuth, sync, and data operations
|
||||||
* NOTE: OAuth auth/callback routes are in index.js (root-level paths)
|
|
||||||
*/
|
*/
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { pool } = require('../config/database');
|
const { pool } = require('../config/database');
|
||||||
const { getOAuthClient, getQboBaseUrl, makeQboApiCall } = require('../config/qbo');
|
const { getOAuthClient, getQboBaseUrl, saveTokens } = require('../config/qbo');
|
||||||
|
const { makeQboApiCall } = require('../../qbo_helper');
|
||||||
|
|
||||||
// GET QBO status
|
// GET QBO status
|
||||||
router.get('/status', (req, res) => {
|
router.get('/status', (req, res) => {
|
||||||
|
|
@ -23,6 +23,35 @@ router.get('/status', (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET auth URL - redirects to Intuit
|
||||||
|
router.get('/auth', (req, res) => {
|
||||||
|
const client = getOAuthClient();
|
||||||
|
const authUri = client.authorizeUri({
|
||||||
|
scope: [require('../config/qbo').OAuthClient.scopes.Accounting],
|
||||||
|
state: 'intuit-qbo-auth'
|
||||||
|
});
|
||||||
|
console.log('🔗 Redirecting to QBO Authorization:', authUri);
|
||||||
|
res.redirect(authUri);
|
||||||
|
});
|
||||||
|
|
||||||
|
// OAuth callback
|
||||||
|
router.get('/auth/callback', async (req, res) => {
|
||||||
|
const client = getOAuthClient();
|
||||||
|
try {
|
||||||
|
const authResponse = await client.createToken(req.url);
|
||||||
|
console.log('✅ QBO Authorization erfolgreich!');
|
||||||
|
saveTokens();
|
||||||
|
res.redirect('/#settings');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ QBO Authorization fehlgeschlagen:', e);
|
||||||
|
res.status(500).send(`
|
||||||
|
<h2>QBO Authorization Failed</h2>
|
||||||
|
<p>${e.message || e}</p>
|
||||||
|
<a href="/">Zurück zur App</a>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// GET bank accounts from QBO
|
// GET bank accounts from QBO
|
||||||
router.get('/accounts', async (req, res) => {
|
router.get('/accounts', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
// src/services/qbo-service.js
|
|
||||||
/**
|
/**
|
||||||
* QuickBooks Online Service
|
* QuickBooks Online Service
|
||||||
* Handles QBO API interactions
|
* Handles QBO API interactions
|
||||||
*/
|
*/
|
||||||
const { getOAuthClient, getQboBaseUrl, makeQboApiCall } = require('../config/qbo'); // Sauberer Import
|
const { getOAuthClient, getQboBaseUrl } = require('../config/qbo');
|
||||||
|
const { makeQboApiCall } = require('../../qbo_helper');
|
||||||
|
|
||||||
// QBO Item IDs
|
// QBO Item IDs
|
||||||
const QBO_LABOR_ID = '5';
|
const QBO_LABOR_ID = '5';
|
||||||
const QBO_PARTS_ID = '9';
|
const QBO_PARTS_ID = '9';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OAuth client and company ID
|
||||||
|
*/
|
||||||
function getClientInfo() {
|
function getClientInfo() {
|
||||||
const oauthClient = getOAuthClient();
|
const oauthClient = getOAuthClient();
|
||||||
const companyId = oauthClient.getToken().realmId;
|
const companyId = oauthClient.getToken().realmId;
|
||||||
|
|
@ -19,8 +22,10 @@ function getClientInfo() {
|
||||||
/**
|
/**
|
||||||
* Export invoice to QBO
|
* Export invoice to QBO
|
||||||
*/
|
*/
|
||||||
async function exportInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbClient statt pool
|
async function exportInvoiceToQbo(invoiceId, pool) {
|
||||||
const invoiceRes = await dbClient.query(`
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const invoiceRes = await client.query(`
|
||||||
SELECT i.*, c.qbo_id as customer_qbo_id, c.name as customer_name, c.email
|
SELECT i.*, c.qbo_id as customer_qbo_id, c.name as customer_name, c.email
|
||||||
FROM invoices i
|
FROM invoices i
|
||||||
LEFT JOIN customers c ON i.customer_id = c.id
|
LEFT JOIN customers c ON i.customer_id = c.id
|
||||||
|
|
@ -30,13 +35,13 @@ async function exportInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbCl
|
||||||
const invoice = invoiceRes.rows[0];
|
const invoice = invoiceRes.rows[0];
|
||||||
if (!invoice.customer_qbo_id) return { skipped: true, reason: 'Customer not in QBO' };
|
if (!invoice.customer_qbo_id) return { skipped: true, reason: 'Customer not in QBO' };
|
||||||
|
|
||||||
const itemsRes = await dbClient.query('SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order', [invoiceId]);
|
const itemsRes = await client.query('SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order', [invoiceId]);
|
||||||
const items = itemsRes.rows;
|
const items = itemsRes.rows;
|
||||||
|
|
||||||
const { companyId, baseUrl } = getClientInfo();
|
const { companyId, baseUrl } = getClientInfo();
|
||||||
|
|
||||||
// Get next DocNumber
|
// Get next DocNumber
|
||||||
const maxNumResult = await dbClient.query(`
|
const maxNumResult = await client.query(`
|
||||||
SELECT GREATEST(
|
SELECT GREATEST(
|
||||||
COALESCE((SELECT MAX(CAST(qbo_doc_number AS INTEGER)) FROM invoices WHERE qbo_doc_number ~ '^[0-9]+$'), 0),
|
COALESCE((SELECT MAX(CAST(qbo_doc_number AS INTEGER)) FROM invoices WHERE qbo_doc_number ~ '^[0-9]+$'), 0),
|
||||||
COALESCE((SELECT MAX(CAST(invoice_number AS INTEGER)) FROM invoices WHERE invoice_number ~ '^[0-9]+$'), 0)
|
COALESCE((SELECT MAX(CAST(invoice_number AS INTEGER)) FROM invoices WHERE invoice_number ~ '^[0-9]+$'), 0)
|
||||||
|
|
@ -44,6 +49,7 @@ async function exportInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbCl
|
||||||
`);
|
`);
|
||||||
let nextDocNumber = (parseInt(maxNumResult.rows[0].max_num) + 1).toString();
|
let nextDocNumber = (parseInt(maxNumResult.rows[0].max_num) + 1).toString();
|
||||||
|
|
||||||
|
// Build line items
|
||||||
const lineItems = items.map(item => {
|
const lineItems = items.map(item => {
|
||||||
const parseNum = (val) => {
|
const parseNum = (val) => {
|
||||||
if (val === null || val === undefined) return 0;
|
if (val === null || val === undefined) return 0;
|
||||||
|
|
@ -55,7 +61,6 @@ async function exportInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbCl
|
||||||
const amount = rate * qty;
|
const amount = rate * qty;
|
||||||
const itemRefId = item.qbo_item_id || QBO_PARTS_ID;
|
const itemRefId = item.qbo_item_id || QBO_PARTS_ID;
|
||||||
const itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts";
|
const itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"DetailType": "SalesItemLineDetail",
|
"DetailType": "SalesItemLineDetail",
|
||||||
"Amount": amount,
|
"Amount": amount,
|
||||||
|
|
@ -78,18 +83,16 @@ async function exportInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbCl
|
||||||
"BillEmail": { "Address": invoice.email || "" }
|
"BillEmail": { "Address": invoice.email || "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Retry on duplicate
|
||||||
let qboInvoice = null;
|
let qboInvoice = null;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 5; attempt++) {
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
console.log(`📤 QBO Export Invoice (DocNumber: ${qboPayload.DocNumber})...`);
|
console.log(`📤 QBO Export Invoice (DocNumber: ${qboPayload.DocNumber})...`);
|
||||||
|
|
||||||
const response = await makeQboApiCall({
|
const response = await makeQboApiCall({
|
||||||
url: `${baseUrl}/v3/company/${companyId}/invoice`,
|
url: `${baseUrl}/v3/company/${companyId}/invoice`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(qboPayload)
|
body: JSON.stringify(qboPayload)
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = response.getJson ? response.getJson() : response.json;
|
const data = response.getJson ? response.getJson() : response.json;
|
||||||
|
|
||||||
if (data.Fault?.Error?.[0]?.code === '6140') {
|
if (data.Fault?.Error?.[0]?.code === '6140') {
|
||||||
|
|
@ -104,26 +107,30 @@ async function exportInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbCl
|
||||||
}
|
}
|
||||||
qboInvoice = data.Invoice || data;
|
qboInvoice = data.Invoice || data;
|
||||||
if (qboInvoice.Id) break;
|
if (qboInvoice.Id) break;
|
||||||
|
|
||||||
throw new Error("QBO returned no ID: " + JSON.stringify(data).substring(0, 500));
|
throw new Error("QBO returned no ID: " + JSON.stringify(data).substring(0, 500));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!qboInvoice?.Id) throw new Error('Could not find free DocNumber after 5 attempts.');
|
if (!qboInvoice?.Id) throw new Error('Could not find free DocNumber after 5 attempts.');
|
||||||
|
|
||||||
await dbClient.query(
|
await client.query(
|
||||||
'UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $4 WHERE id = $5',
|
'UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $4 WHERE id = $5',
|
||||||
[qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, qboInvoice.DocNumber, invoiceId]
|
[qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, qboInvoice.DocNumber, invoiceId]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`✅ QBO Invoice created: ID ${qboInvoice.Id}, DocNumber ${qboInvoice.DocNumber}`);
|
console.log(`✅ QBO Invoice created: ID ${qboInvoice.Id}, DocNumber ${qboInvoice.DocNumber}`);
|
||||||
return { success: true, qbo_id: qboInvoice.Id, qbo_doc_number: qboInvoice.DocNumber };
|
return { success: true, qbo_id: qboInvoice.Id, qbo_doc_number: qboInvoice.DocNumber };
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync invoice to QBO (update)
|
* Sync invoice to QBO (update)
|
||||||
*/
|
*/
|
||||||
async function syncInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbClient statt pool
|
async function syncInvoiceToQbo(invoiceId, pool) {
|
||||||
const invoiceRes = await dbClient.query(`
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const invoiceRes = await client.query(`
|
||||||
SELECT i.*, c.qbo_id as customer_qbo_id
|
SELECT i.*, c.qbo_id as customer_qbo_id
|
||||||
FROM invoices i
|
FROM invoices i
|
||||||
LEFT JOIN customers c ON i.customer_id = c.id
|
LEFT JOIN customers c ON i.customer_id = c.id
|
||||||
|
|
@ -133,18 +140,16 @@ async function syncInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbClie
|
||||||
const invoice = invoiceRes.rows[0];
|
const invoice = invoiceRes.rows[0];
|
||||||
if (!invoice.qbo_id) return { skipped: true, reason: 'Not in QBO' };
|
if (!invoice.qbo_id) return { skipped: true, reason: 'Not in QBO' };
|
||||||
|
|
||||||
const itemsRes = await dbClient.query('SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order', [invoiceId]);
|
const itemsRes = await client.query('SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order', [invoiceId]);
|
||||||
|
|
||||||
const { companyId, baseUrl } = getClientInfo();
|
const { companyId, baseUrl } = getClientInfo();
|
||||||
|
|
||||||
|
// Get current sync token
|
||||||
const qboRes = await makeQboApiCall({
|
const qboRes = await makeQboApiCall({
|
||||||
url: `${baseUrl}/v3/company/${companyId}/invoice/${invoice.qbo_id}`,
|
url: `${baseUrl}/v3/company/${companyId}/invoice/${invoice.qbo_id}`,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
const qboData = qboRes.getJson ? qboRes.getJson() : qboRes.json;
|
const qboData = qboRes.getJson ? qboRes.getJson() : qboRes.json;
|
||||||
const currentSyncToken = qboData.Invoice?.SyncToken;
|
const currentSyncToken = qboData.Invoice?.SyncToken;
|
||||||
|
|
||||||
if (currentSyncToken === undefined) throw new Error('Could not get SyncToken from QBO');
|
if (currentSyncToken === undefined) throw new Error('Could not get SyncToken from QBO');
|
||||||
|
|
||||||
const lineItems = itemsRes.rows.map(item => {
|
const lineItems = itemsRes.rows.map(item => {
|
||||||
|
|
@ -166,7 +171,7 @@ async function syncInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbClie
|
||||||
"SalesItemLineDetail": {
|
"SalesItemLineDetail": {
|
||||||
"ItemRef": { "value": itemRefId, "name": itemRefName },
|
"ItemRef": { "value": itemRefId, "name": itemRefName },
|
||||||
"UnitPrice": rate,
|
"UnitPrice": rate,
|
||||||
"Qty": qty
|
"Qty": parseFloat(item.quantity) || 1
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -182,7 +187,6 @@ async function syncInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbClie
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`📤 QBO Sync Invoice ${invoice.qbo_id}...`);
|
console.log(`📤 QBO Sync Invoice ${invoice.qbo_id}...`);
|
||||||
|
|
||||||
const updateRes = await makeQboApiCall({
|
const updateRes = await makeQboApiCall({
|
||||||
url: `${baseUrl}/v3/company/${companyId}/invoice`,
|
url: `${baseUrl}/v3/company/${companyId}/invoice`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -199,18 +203,21 @@ async function syncInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbClie
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = updateData.Invoice || updateData;
|
const updated = updateData.Invoice || updateData;
|
||||||
|
|
||||||
if (!updated.Id) {
|
if (!updated.Id) {
|
||||||
|
console.error(`❌ QBO unexpected response:`, JSON.stringify(updateData).substring(0, 500));
|
||||||
throw new Error('QBO update returned no ID');
|
throw new Error('QBO update returned no ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
await dbClient.query(
|
await client.query(
|
||||||
'UPDATE invoices SET qbo_sync_token = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
'UPDATE invoices SET qbo_sync_token = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
||||||
[updated.SyncToken, invoiceId]
|
[updated.SyncToken, invoiceId]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`✅ QBO Invoice ${invoice.qbo_id} synced (SyncToken: ${updated.SyncToken})`);
|
console.log(`✅ QBO Invoice ${invoice.qbo_id} synced (SyncToken: ${updated.SyncToken})`);
|
||||||
return { success: true, sync_token: updated.SyncToken };
|
return { success: true, sync_token: updated.SyncToken };
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue