update
This commit is contained in:
166
server.js
166
server.js
@@ -1612,8 +1612,8 @@ app.patch('/api/invoices/:id/reset-qbo', async (req, res) => {
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// QBO PAYMENT RECORDING - Server Endpoints
|
||||
// In server.js einfügen (z.B. nach dem /api/qbo/import-unpaid Endpoint)
|
||||
// QBO PAYMENT ENDPOINTS — In server.js einfügen
|
||||
// Speichert Payments sowohl in lokaler DB als auch in QBO
|
||||
// =====================================================
|
||||
|
||||
|
||||
@@ -1622,11 +1622,10 @@ app.get('/api/qbo/accounts', async (req, res) => {
|
||||
try {
|
||||
const oauthClient = getOAuthClient();
|
||||
const companyId = oauthClient.getToken().realmId;
|
||||
const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
: 'https://sandbox-quickbooks.api.intuit.com';
|
||||
|
||||
// Nur Bank-Konten abfragen
|
||||
const query = "SELECT * FROM Account WHERE AccountType = 'Bank' AND Active = true";
|
||||
const response = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(query)}`,
|
||||
@@ -1648,13 +1647,13 @@ app.get('/api/qbo/accounts', async (req, res) => {
|
||||
});
|
||||
|
||||
|
||||
// --- 2. Payment Methods aus QBO laden (für Check/ACH Dropdown) ---
|
||||
// --- 2. Payment Methods aus QBO laden ---
|
||||
app.get('/api/qbo/payment-methods', async (req, res) => {
|
||||
try {
|
||||
const oauthClient = getOAuthClient();
|
||||
const companyId = oauthClient.getToken().realmId;
|
||||
const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
: 'https://sandbox-quickbooks.api.intuit.com';
|
||||
|
||||
const query = "SELECT * FROM PaymentMethod WHERE Active = true";
|
||||
@@ -1677,14 +1676,16 @@ app.get('/api/qbo/payment-methods', async (req, res) => {
|
||||
});
|
||||
|
||||
|
||||
// --- 3. Payment in QBO erstellen (ein Check für 1..n Invoices) ---
|
||||
// --- 3. Payment erstellen: Lokal + QBO ---
|
||||
app.post('/api/qbo/record-payment', async (req, res) => {
|
||||
const {
|
||||
const {
|
||||
invoice_ids, // Array von lokalen Invoice IDs
|
||||
payment_date, // 'YYYY-MM-DD'
|
||||
reference_number, // Check-Nummer oder ACH-Referenz
|
||||
reference_number, // Check # oder ACH Referenz
|
||||
payment_method_id, // QBO PaymentMethod ID
|
||||
deposit_to_account_id // QBO Bank Account ID
|
||||
payment_method_name, // 'Check' oder 'ACH' (für lokale DB)
|
||||
deposit_to_account_id, // QBO Bank Account ID
|
||||
deposit_to_account_name // Bankname (für lokale DB)
|
||||
} = req.body;
|
||||
|
||||
if (!invoice_ids || invoice_ids.length === 0) {
|
||||
@@ -1692,72 +1693,57 @@ app.post('/api/qbo/record-payment', async (req, res) => {
|
||||
}
|
||||
|
||||
const dbClient = await pool.connect();
|
||||
|
||||
|
||||
try {
|
||||
const oauthClient = getOAuthClient();
|
||||
const companyId = oauthClient.getToken().realmId;
|
||||
const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
: 'https://sandbox-quickbooks.api.intuit.com';
|
||||
|
||||
// Lokale Invoices laden
|
||||
const invoicesResult = await dbClient.query(
|
||||
`SELECT i.*, c.qbo_id as customer_qbo_id, c.name as customer_name
|
||||
FROM invoices i
|
||||
LEFT JOIN customers c ON i.customer_id = c.id
|
||||
`SELECT i.*, c.qbo_id as customer_qbo_id, c.name as customer_name
|
||||
FROM invoices i
|
||||
LEFT JOIN customers c ON i.customer_id = c.id
|
||||
WHERE i.id = ANY($1)`,
|
||||
[invoice_ids]
|
||||
);
|
||||
|
||||
const invoicesData = invoicesResult.rows;
|
||||
|
||||
// Validierung: Alle müssen eine qbo_id haben (schon in QBO)
|
||||
// Validierung
|
||||
const notInQbo = invoicesData.filter(inv => !inv.qbo_id);
|
||||
if (notInQbo.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: `Folgende Rechnungen sind noch nicht in QBO: ${notInQbo.map(i => i.invoice_number || `ID:${i.id}`).join(', ')}`
|
||||
return res.status(400).json({
|
||||
error: `Nicht in QBO: ${notInQbo.map(i => i.invoice_number || `ID:${i.id}`).join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
// Validierung: Alle müssen denselben Kunden haben
|
||||
const customerIds = [...new Set(invoicesData.map(inv => inv.customer_qbo_id))];
|
||||
if (customerIds.length > 1) {
|
||||
return res.status(400).json({
|
||||
error: 'Alle Rechnungen eines Payments müssen zum selben Kunden gehören.'
|
||||
});
|
||||
return res.status(400).json({ error: 'Alle Rechnungen müssen zum selben Kunden gehören.' });
|
||||
}
|
||||
|
||||
const customerQboId = customerIds[0];
|
||||
|
||||
// Gesamtbetrag berechnen
|
||||
const customerId = invoicesData[0].customer_id;
|
||||
const totalAmount = invoicesData.reduce((sum, inv) => sum + parseFloat(inv.total), 0);
|
||||
|
||||
// QBO Payment Objekt bauen
|
||||
// ----- QBO Payment Objekt -----
|
||||
const payment = {
|
||||
CustomerRef: {
|
||||
value: customerQboId
|
||||
},
|
||||
CustomerRef: { value: customerQboId },
|
||||
TotalAmt: totalAmount,
|
||||
TxnDate: payment_date,
|
||||
PaymentRefNum: reference_number || '',
|
||||
PaymentMethodRef: {
|
||||
value: payment_method_id
|
||||
},
|
||||
DepositToAccountRef: {
|
||||
value: deposit_to_account_id
|
||||
},
|
||||
PaymentMethodRef: { value: payment_method_id },
|
||||
DepositToAccountRef: { value: deposit_to_account_id },
|
||||
Line: invoicesData.map(inv => ({
|
||||
Amount: parseFloat(inv.total),
|
||||
LinkedTxn: [{
|
||||
TxnId: inv.qbo_id,
|
||||
TxnType: 'Invoice'
|
||||
}]
|
||||
LinkedTxn: [{ TxnId: inv.qbo_id, TxnType: 'Invoice' }]
|
||||
}))
|
||||
};
|
||||
|
||||
console.log(`💰 Erstelle QBO Payment: $${totalAmount.toFixed(2)} für ${invoicesData.length} Rechnung(en), Kunde: ${invoicesData[0].customer_name}`);
|
||||
console.log(`💰 QBO Payment: $${totalAmount.toFixed(2)} für ${invoicesData.length} Rechnung(en)`);
|
||||
|
||||
// Payment an QBO senden
|
||||
const response = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/payment`,
|
||||
method: 'POST',
|
||||
@@ -1767,38 +1753,50 @@ app.post('/api/qbo/record-payment', async (req, res) => {
|
||||
|
||||
const data = response.getJson ? response.getJson() : response.json;
|
||||
|
||||
if (data.Payment) {
|
||||
const qboPaymentId = data.Payment.Id;
|
||||
console.log(`✅ QBO Payment erstellt: ID ${qboPaymentId}`);
|
||||
|
||||
// Lokale Invoices als bezahlt markieren
|
||||
await dbClient.query('BEGIN');
|
||||
|
||||
for (const inv of invoicesData) {
|
||||
await dbClient.query(
|
||||
`UPDATE invoices
|
||||
SET paid_date = $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2`,
|
||||
[payment_date, inv.id]
|
||||
);
|
||||
}
|
||||
|
||||
await dbClient.query('COMMIT');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
qbo_payment_id: qboPaymentId,
|
||||
total: totalAmount,
|
||||
invoices_paid: invoicesData.length,
|
||||
message: `Payment $${totalAmount.toFixed(2)} erfolgreich in QBO erfasst (ID: ${qboPaymentId}).`
|
||||
});
|
||||
} else {
|
||||
if (!data.Payment) {
|
||||
console.error('❌ QBO Payment Fehler:', JSON.stringify(data));
|
||||
res.status(500).json({
|
||||
error: 'QBO Payment fehlgeschlagen: ' + JSON.stringify(data.fault?.error?.[0]?.message || data)
|
||||
return res.status(500).json({
|
||||
error: 'QBO Payment fehlgeschlagen: ' + JSON.stringify(data.Fault?.Error?.[0]?.Message || data)
|
||||
});
|
||||
}
|
||||
|
||||
const qboPaymentId = data.Payment.Id;
|
||||
console.log(`✅ QBO Payment ID: ${qboPaymentId}`);
|
||||
|
||||
// ----- Lokal in DB speichern -----
|
||||
await dbClient.query('BEGIN');
|
||||
|
||||
// Payment-Datensatz
|
||||
const paymentResult = await dbClient.query(
|
||||
`INSERT INTO payments (payment_date, reference_number, payment_method, deposit_to_account, total_amount, customer_id, qbo_payment_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
||||
[payment_date, reference_number || null, payment_method_name || 'Check', deposit_to_account_name || '', totalAmount, customerId, qboPaymentId]
|
||||
);
|
||||
const localPaymentId = paymentResult.rows[0].id;
|
||||
|
||||
// Invoices mit Payment verknüpfen + als bezahlt markieren
|
||||
for (const inv of invoicesData) {
|
||||
await dbClient.query(
|
||||
`INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3)`,
|
||||
[localPaymentId, inv.id, parseFloat(inv.total)]
|
||||
);
|
||||
await dbClient.query(
|
||||
`UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2`,
|
||||
[payment_date, inv.id]
|
||||
);
|
||||
}
|
||||
|
||||
await dbClient.query('COMMIT');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
payment_id: localPaymentId,
|
||||
qbo_payment_id: qboPaymentId,
|
||||
total: totalAmount,
|
||||
invoices_paid: invoicesData.length,
|
||||
message: `Payment $${totalAmount.toFixed(2)} erfasst (QBO: ${qboPaymentId}, Lokal: ${localPaymentId}).`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await dbClient.query('ROLLBACK').catch(() => {});
|
||||
console.error('❌ Payment Error:', error);
|
||||
@@ -1809,6 +1807,30 @@ app.post('/api/qbo/record-payment', async (req, res) => {
|
||||
});
|
||||
|
||||
|
||||
// --- 4. Lokale Payments auflisten (optional, für spätere Übersicht) ---
|
||||
app.get('/api/payments', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT p.*, c.name as customer_name,
|
||||
json_agg(json_build_object(
|
||||
'invoice_id', pi.invoice_id,
|
||||
'amount', pi.amount,
|
||||
'invoice_number', i.invoice_number
|
||||
)) as invoices
|
||||
FROM payments p
|
||||
LEFT JOIN customers c ON p.customer_id = c.id
|
||||
LEFT JOIN payment_invoices pi ON pi.payment_id = p.id
|
||||
LEFT JOIN invoices i ON i.id = pi.invoice_id
|
||||
GROUP BY p.id, c.name
|
||||
ORDER BY p.payment_date DESC
|
||||
`);
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching payments:', error);
|
||||
res.status(500).json({ error: 'Error fetching payments' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user