+
$
-
+ class="w-28 px-2 py-1 border rounded text-sm text-right font-semibold
+ ${isPartial ? 'bg-yellow-50 border-yellow-300' : isOver ? 'bg-blue-50 border-blue-300' : 'border-gray-300'}">
@@ -331,17 +280,23 @@ function renderInvoiceList() {
function updateTotal() {
const totalEl = document.getElementById('payment-total');
+ const noteEl = document.getElementById('payment-overpay-note');
if (!totalEl) return;
- let total = 0;
- if (paymentMode === 'unapplied') {
- const amountInput = document.getElementById('payment-unapplied-amount');
- total = parseFloat(amountInput?.value) || 0;
- } else {
- total = selectedInvoices.reduce((sum, si) => sum + si.payAmount, 0);
- }
+ const payTotal = selectedInvoices.reduce((s, si) => s + si.payAmount, 0);
+ const invTotal = selectedInvoices.reduce((s, si) => s + parseFloat(si.invoice.total), 0);
- totalEl.textContent = `$${total.toFixed(2)}`;
+ totalEl.textContent = `$${payTotal.toFixed(2)}`;
+
+ if (noteEl) {
+ if (payTotal > invTotal && invTotal > 0) {
+ const overpay = payTotal - invTotal;
+ noteEl.textContent = `⚠️ Overpayment of $${overpay.toFixed(2)} will be stored as customer credit in QBO.`;
+ noteEl.classList.remove('hidden');
+ } else {
+ noteEl.classList.add('hidden');
+ }
+ }
}
// ============================================================
@@ -349,97 +304,62 @@ function updateTotal() {
// ============================================================
async function submitPayment() {
+ if (selectedInvoices.length === 0) { alert('Please add at least one invoice.'); return; }
+
const paymentDate = document.getElementById('payment-date').value;
const reference = document.getElementById('payment-reference').value;
const methodSelect = document.getElementById('payment-method');
const depositSelect = document.getElementById('payment-deposit-to');
- const methodId = methodSelect.value;
- const methodName = methodSelect.options[methodSelect.selectedIndex]?.text || '';
- const depositToId = depositSelect.value;
- const depositToName = depositSelect.options[depositSelect.selectedIndex]?.text || '';
- if (!paymentDate || !methodId || !depositToId) {
- alert('Bitte alle Felder ausfüllen.');
- return;
+ if (!paymentDate || !methodSelect.value || !depositSelect.value) {
+ alert('Please fill in all fields.'); return;
}
- let body;
+ const total = selectedInvoices.reduce((s, si) => s + si.payAmount, 0);
+ const invTotal = selectedInvoices.reduce((s, si) => s + parseFloat(si.invoice.total), 0);
+ const nums = selectedInvoices.map(si => `#${si.invoice.invoice_number || si.invoice.id}`).join(', ');
+ const hasPartial = selectedInvoices.some(si => si.payAmount < parseFloat(si.invoice.total));
+ const hasOverpay = total > invTotal;
- if (paymentMode === 'unapplied') {
- // --- Downpayment ---
- const custSelect = document.getElementById('payment-customer');
- const customerId = custSelect?.value;
- const customerQboId = custSelect?.selectedOptions[0]?.getAttribute('data-qbo-id');
- const amount = parseFloat(document.getElementById('payment-unapplied-amount')?.value) || 0;
-
- if (!customerId || !customerQboId) { alert('Bitte Kunde wählen.'); return; }
- if (amount <= 0) { alert('Bitte Betrag eingeben.'); return; }
-
- body = {
- mode: 'unapplied',
- customer_id: parseInt(customerId),
- customer_qbo_id: customerQboId,
- total_amount: amount,
- payment_date: paymentDate,
- reference_number: reference,
- payment_method_id: methodId,
- payment_method_name: methodName,
- deposit_to_account_id: depositToId,
- deposit_to_account_name: depositToName
- };
-
- if (!confirm(`Downpayment $${amount.toFixed(2)} an QBO senden?`)) return;
-
- } else {
- // --- Normal / Partial / Multi ---
- if (selectedInvoices.length === 0) { alert('Bitte Rechnungen hinzufügen.'); return; }
-
- const total = selectedInvoices.reduce((s, si) => s + si.payAmount, 0);
- const nums = selectedInvoices.map(si => `#${si.invoice.invoice_number || si.invoice.id}`).join(', ');
- const hasPartial = selectedInvoices.some(si => si.payAmount < parseFloat(si.invoice.total));
-
- let msg = `Payment $${total.toFixed(2)} für ${nums} an QBO senden?`;
- if (hasPartial) msg += '\n⚠️ Enthält Teilzahlung(en).';
- if (!confirm(msg)) return;
-
- body = {
- mode: 'invoice',
- invoice_payments: selectedInvoices.map(si => ({
- invoice_id: si.invoice.id,
- amount: si.payAmount
- })),
- payment_date: paymentDate,
- reference_number: reference,
- payment_method_id: methodId,
- payment_method_name: methodName,
- deposit_to_account_id: depositToId,
- deposit_to_account_name: depositToName
- };
- }
+ let msg = `Record payment of $${total.toFixed(2)} for ${nums}?`;
+ if (hasPartial) msg += '\n⚠️ Contains partial payment(s).';
+ if (hasOverpay) msg += `\n⚠️ $${(total - invTotal).toFixed(2)} overpayment → customer credit.`;
+ if (!confirm(msg)) return;
const submitBtn = document.getElementById('payment-submit-btn');
- submitBtn.innerHTML = '⏳ Wird gesendet...';
+ submitBtn.innerHTML = '⏳ Processing...';
submitBtn.disabled = true;
- if (typeof showSpinner === 'function') showSpinner('Erstelle Payment in QBO...');
+ if (typeof showSpinner === 'function') showSpinner('Recording payment in QBO...');
try {
const response = await fetch('/api/qbo/record-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
+ body: JSON.stringify({
+ mode: 'invoice',
+ invoice_payments: selectedInvoices.map(si => ({
+ invoice_id: si.invoice.id,
+ amount: si.payAmount
+ })),
+ payment_date: paymentDate,
+ reference_number: reference,
+ payment_method_id: methodSelect.value,
+ payment_method_name: methodSelect.options[methodSelect.selectedIndex]?.text || '',
+ deposit_to_account_id: depositSelect.value,
+ deposit_to_account_name: depositSelect.options[depositSelect.selectedIndex]?.text || ''
+ })
});
const result = await response.json();
-
if (response.ok) {
alert(`✅ ${result.message}`);
closePaymentModal();
if (window.invoiceView) window.invoiceView.loadInvoices();
} else {
- alert(`❌ Fehler: ${result.error}`);
+ alert(`❌ Error: ${result.error}`);
}
- } catch (error) {
- console.error('Payment error:', error);
- alert('Netzwerkfehler.');
+ } catch (e) {
+ console.error('Payment error:', e);
+ alert('Network error.');
} finally {
submitBtn.innerHTML = '💰 Record Payment in QBO';
submitBtn.disabled = false;
@@ -447,23 +367,16 @@ async function submitPayment() {
}
}
-function switchMode(mode) {
- paymentMode = mode;
- renderModalContent();
-}
-
// ============================================================
// Expose
// ============================================================
window.paymentModal = {
open: openPaymentModal,
- openDownpayment: openDownpaymentModal,
close: closePaymentModal,
submit: submitPayment,
addById: addInvoiceById,
removeInvoice: removeInvoice,
updateAmount: updatePayAmount,
- updateTotal: updateTotal,
- switchMode: switchMode
+ updateTotal: updateTotal
};
\ No newline at end of file
diff --git a/server.js b/server.js
index b8a717f..166887b 100644
--- a/server.js
+++ b/server.js
@@ -1612,12 +1612,14 @@ app.patch('/api/invoices/:id/reset-qbo', async (req, res) => {
});
// =====================================================
-// QBO PAYMENT ENDPOINTS v2 — In server.js einfügen
-// Supports: multi-invoice, partial, unapplied (downpayment)
+// QBO PAYMENT ENDPOINTS v3 — In server.js einfügen
+// - Invoice payments (multi, partial, overpay)
+// - Downpayment (separate endpoint, called from customer view)
+// - Customer credit query
// =====================================================
-// --- Bank-Konten aus QBO (für Deposit To) ---
+// --- Bank-Konten aus QBO ---
app.get('/api/qbo/accounts', async (req, res) => {
try {
const oauthClient = getOAuthClient();
@@ -1625,7 +1627,6 @@ app.get('/api/qbo/accounts', async (req, res) => {
const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
? 'https://quickbooks.api.intuit.com'
: 'https://sandbox-quickbooks.api.intuit.com';
-
const query = "SELECT * FROM Account WHERE AccountType = 'Bank' AND Active = true";
const response = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(query)}`,
@@ -1634,7 +1635,6 @@ app.get('/api/qbo/accounts', async (req, res) => {
const data = response.getJson ? response.getJson() : response.json;
res.json((data.QueryResponse?.Account || []).map(a => ({ id: a.Id, name: a.Name })));
} catch (error) {
- console.error('Error fetching QBO accounts:', error);
res.status(500).json({ error: error.message });
}
});
@@ -1648,7 +1648,6 @@ app.get('/api/qbo/payment-methods', async (req, res) => {
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";
const response = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(query)}`,
@@ -1657,23 +1656,48 @@ app.get('/api/qbo/payment-methods', async (req, res) => {
const data = response.getJson ? response.getJson() : response.json;
res.json((data.QueryResponse?.PaymentMethod || []).map(p => ({ id: p.Id, name: p.Name })));
} catch (error) {
- console.error('Error fetching payment methods:', error);
res.status(500).json({ error: error.message });
}
});
+// --- Customer Credit (unapplied payments) ---
+app.get('/api/qbo/customer-credit/:qboCustomerId', async (req, res) => {
+ try {
+ const { qboCustomerId } = req.params;
+ 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';
-// --- Record Payment (multi-invoice, partial, unapplied) ---
+ const query = `SELECT * FROM Payment WHERE CustomerRef = '${qboCustomerId}' AND UnappliedAmt > '0'`;
+ const response = await makeQboApiCall({
+ url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(query)}`,
+ method: 'GET'
+ });
+ const data = response.getJson ? response.getJson() : response.json;
+ const payments = data.QueryResponse?.Payment || [];
+
+ const totalCredit = payments.reduce((sum, p) => sum + (parseFloat(p.UnappliedAmt) || 0), 0);
+ const details = payments.map(p => ({
+ qbo_id: p.Id,
+ date: p.TxnDate,
+ total: p.TotalAmt,
+ unapplied: p.UnappliedAmt,
+ ref: p.PaymentRefNum || ''
+ }));
+
+ res.json({ credit: totalCredit, payments: details });
+ } catch (error) {
+ console.error('Error fetching customer credit:', error);
+ res.json({ credit: 0, payments: [] });
+ }
+});
+
+// --- Record Payment (against invoices: normal, partial, multi, overpay) ---
app.post('/api/qbo/record-payment', async (req, res) => {
const {
- mode, // 'invoice' | 'unapplied'
- // Mode 'invoice':
invoice_payments, // [{ invoice_id, amount }]
- // Mode 'unapplied':
- customer_id, // Lokale Kunden-ID
- customer_qbo_id, // QBO Customer ID
- total_amount, // Betrag
- // Gemeinsam:
payment_date,
reference_number,
payment_method_id,
@@ -1682,7 +1706,135 @@ app.post('/api/qbo/record-payment', async (req, res) => {
deposit_to_account_name
} = req.body;
+ if (!invoice_payments || invoice_payments.length === 0) {
+ return res.status(400).json({ error: 'No invoices selected.' });
+ }
+
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'
+ : 'https://sandbox-quickbooks.api.intuit.com';
+
+ const ids = invoice_payments.map(ip => ip.invoice_id);
+ const result = 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
+ WHERE i.id = ANY($1)`, [ids]
+ );
+ const invoicesData = result.rows;
+
+ const notInQbo = invoicesData.filter(inv => !inv.qbo_id);
+ if (notInQbo.length > 0) {
+ return res.status(400).json({
+ error: `Not in QBO: ${notInQbo.map(i => i.invoice_number || `ID:${i.id}`).join(', ')}`
+ });
+ }
+ const custIds = [...new Set(invoicesData.map(inv => inv.customer_qbo_id))];
+ if (custIds.length > 1) {
+ return res.status(400).json({ error: 'All invoices must belong to the same customer.' });
+ }
+
+ const paymentMap = new Map(invoice_payments.map(ip => [ip.invoice_id, parseFloat(ip.amount)]));
+ const totalAmt = invoice_payments.reduce((s, ip) => s + parseFloat(ip.amount), 0);
+
+ const qboPayment = {
+ CustomerRef: { value: custIds[0] },
+ TotalAmt: totalAmt,
+ TxnDate: payment_date,
+ PaymentRefNum: reference_number || '',
+ PaymentMethodRef: { value: payment_method_id },
+ DepositToAccountRef: { value: deposit_to_account_id },
+ Line: invoicesData.map(inv => ({
+ Amount: paymentMap.get(inv.id) || parseFloat(inv.total),
+ LinkedTxn: [{ TxnId: inv.qbo_id, TxnType: 'Invoice' }]
+ }))
+ };
+
+ console.log(`💰 Payment: $${totalAmt.toFixed(2)} for ${invoicesData.length} invoice(s)`);
+
+ const response = await makeQboApiCall({
+ url: `${baseUrl}/v3/company/${companyId}/payment`,
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(qboPayment)
+ });
+ const data = response.getJson ? response.getJson() : response.json;
+
+ if (!data.Payment) {
+ return res.status(500).json({
+ error: 'QBO Error: ' + (data.Fault?.Error?.[0]?.Message || JSON.stringify(data))
+ });
+ }
+
+ const qboPaymentId = data.Payment.Id;
+ console.log(`✅ QBO Payment ID: ${qboPaymentId}`);
+
+ // Local DB
+ await dbClient.query('BEGIN');
+ const payResult = 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 id`,
+ [payment_date, reference_number || null, payment_method_name || 'Check',
+ deposit_to_account_name || '', totalAmt, invoicesData[0].customer_id, qboPaymentId]
+ );
+ const localPaymentId = payResult.rows[0].id;
+
+ for (const ip of invoice_payments) {
+ const payAmt = parseFloat(ip.amount);
+ const inv = invoicesData.find(i => i.id === ip.invoice_id);
+ const invTotal = inv ? parseFloat(inv.total) : 0;
+
+ await dbClient.query(
+ 'INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3)',
+ [localPaymentId, ip.invoice_id, payAmt]
+ );
+ // Mark paid only if fully covered
+ if (payAmt >= invTotal) {
+ await dbClient.query(
+ 'UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
+ [payment_date, ip.invoice_id]
+ );
+ }
+ }
+ await dbClient.query('COMMIT');
+
+ res.json({
+ success: true,
+ payment_id: localPaymentId,
+ qbo_payment_id: qboPaymentId,
+ total: totalAmt,
+ invoices_paid: invoice_payments.length,
+ message: `Payment $${totalAmt.toFixed(2)} recorded (QBO: ${qboPaymentId}).`
+ });
+ } catch (error) {
+ await dbClient.query('ROLLBACK').catch(() => {});
+ console.error('❌ Payment Error:', error);
+ res.status(500).json({ error: 'Payment failed: ' + error.message });
+ } finally {
+ dbClient.release();
+ }
+});
+
+// --- Record Downpayment (unapplied, from customer view) ---
+app.post('/api/qbo/record-downpayment', async (req, res) => {
+ const {
+ customer_id, // Local customer ID
+ customer_qbo_id, // QBO customer ID
+ amount,
+ payment_date,
+ reference_number,
+ payment_method_id,
+ payment_method_name,
+ deposit_to_account_id,
+ deposit_to_account_name
+ } = req.body;
+
+ if (!customer_qbo_id || !amount || amount <= 0) {
+ return res.status(400).json({ error: 'Customer and amount required.' });
+ }
try {
const oauthClient = getOAuthClient();
@@ -1691,184 +1843,69 @@ app.post('/api/qbo/record-payment', async (req, res) => {
? 'https://quickbooks.api.intuit.com'
: 'https://sandbox-quickbooks.api.intuit.com';
- let qboPayment;
- let localCustomerId;
- let totalAmt;
- let invoicesData = [];
+ const qboPayment = {
+ CustomerRef: { value: customer_qbo_id },
+ TotalAmt: parseFloat(amount),
+ TxnDate: payment_date,
+ PaymentRefNum: reference_number || '',
+ PaymentMethodRef: { value: payment_method_id },
+ DepositToAccountRef: { value: deposit_to_account_id }
+ // No Line[] → unapplied payment
+ };
- if (mode === 'unapplied') {
- // ---- DOWNPAYMENT: kein LinkedTxn ----
- if (!customer_qbo_id || !total_amount) {
- return res.status(400).json({ error: 'Kunde und Betrag erforderlich.' });
- }
+ console.log(`💰 Downpayment: $${amount} for customer QBO ${customer_qbo_id}`);
- localCustomerId = customer_id;
- totalAmt = parseFloat(total_amount);
-
- qboPayment = {
- CustomerRef: { value: customer_qbo_id },
- TotalAmt: totalAmt,
- TxnDate: payment_date,
- PaymentRefNum: reference_number || '',
- PaymentMethodRef: { value: payment_method_id },
- DepositToAccountRef: { value: deposit_to_account_id }
- // Kein Line[] → Unapplied Payment
- };
-
- console.log(`💰 Downpayment: $${totalAmt.toFixed(2)} für Kunde QBO ${customer_qbo_id}`);
-
- } else {
- // ---- INVOICE PAYMENT (normal, partial, multi) ----
- if (!invoice_payments || invoice_payments.length === 0) {
- return res.status(400).json({ error: 'Keine Rechnungen ausgewählt.' });
- }
-
- const ids = invoice_payments.map(ip => ip.invoice_id);
- const result = 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
- WHERE i.id = ANY($1)`, [ids]
- );
- invoicesData = result.rows;
-
- // Validierung
- const notInQbo = invoicesData.filter(inv => !inv.qbo_id);
- if (notInQbo.length > 0) {
- return res.status(400).json({
- error: `Nicht in QBO: ${notInQbo.map(i => i.invoice_number || `ID:${i.id}`).join(', ')}`
- });
- }
-
- const custIds = [...new Set(invoicesData.map(inv => inv.customer_qbo_id))];
- if (custIds.length > 1) {
- return res.status(400).json({ error: 'Alle Rechnungen müssen zum selben Kunden gehören.' });
- }
-
- localCustomerId = invoicesData[0].customer_id;
-
- // Beträge zuordnen
- const paymentMap = new Map(invoice_payments.map(ip => [ip.invoice_id, parseFloat(ip.amount)]));
- totalAmt = invoice_payments.reduce((s, ip) => s + parseFloat(ip.amount), 0);
-
- qboPayment = {
- CustomerRef: { value: custIds[0] },
- TotalAmt: totalAmt,
- TxnDate: payment_date,
- PaymentRefNum: reference_number || '',
- PaymentMethodRef: { value: payment_method_id },
- DepositToAccountRef: { value: deposit_to_account_id },
- Line: invoicesData.map(inv => ({
- Amount: paymentMap.get(inv.id) || parseFloat(inv.total),
- LinkedTxn: [{ TxnId: inv.qbo_id, TxnType: 'Invoice' }]
- }))
- };
-
- const hasPartial = invoicesData.some(inv => {
- const payAmt = paymentMap.get(inv.id) || 0;
- return payAmt < parseFloat(inv.total);
- });
-
- console.log(`💰 Payment: $${totalAmt.toFixed(2)} für ${invoicesData.length} Rechnung(en)${hasPartial ? ' (Teilzahlung)' : ''}`);
- }
-
- // --- QBO senden ---
const response = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/payment`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(qboPayment)
});
-
const data = response.getJson ? response.getJson() : response.json;
if (!data.Payment) {
- console.error('❌ QBO Payment Fehler:', JSON.stringify(data));
return res.status(500).json({
- error: 'QBO Fehler: ' + (data.Fault?.Error?.[0]?.Message || JSON.stringify(data))
+ error: 'QBO Error: ' + (data.Fault?.Error?.[0]?.Message || JSON.stringify(data))
});
}
const qboPaymentId = data.Payment.Id;
- console.log(`✅ QBO Payment ID: ${qboPaymentId}`);
- // --- Lokal speichern ---
- await dbClient.query('BEGIN');
-
- const payResult = 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 id`,
+ // Local DB
+ await pool.query(
+ `INSERT INTO payments (payment_date, reference_number, payment_method, deposit_to_account, total_amount, customer_id, qbo_payment_id, notes)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[payment_date, reference_number || null, payment_method_name || 'Check',
- deposit_to_account_name || '', totalAmt, localCustomerId, qboPaymentId]
+ deposit_to_account_name || '', amount, customer_id, qboPaymentId, 'Downpayment (unapplied)']
);
- const localPaymentId = payResult.rows[0].id;
- // Invoices verknüpfen + als bezahlt markieren
- if (mode !== 'unapplied' && invoice_payments) {
- for (const ip of invoice_payments) {
- const payAmt = parseFloat(ip.amount);
- const inv = invoicesData.find(i => i.id === ip.invoice_id);
- const invTotal = inv ? parseFloat(inv.total) : 0;
- const isFullyPaid = payAmt >= invTotal;
-
- await dbClient.query(
- 'INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3)',
- [localPaymentId, ip.invoice_id, payAmt]
- );
-
- if (isFullyPaid) {
- // Voll bezahlt → paid_date setzen
- await dbClient.query(
- 'UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
- [payment_date, ip.invoice_id]
- );
- }
- // Teilzahlung → paid_date bleibt NULL (Rechnung noch offen)
- }
- }
-
- await dbClient.query('COMMIT');
-
- const modeLabel = mode === 'unapplied' ? 'Downpayment' : 'Payment';
res.json({
success: true,
- payment_id: localPaymentId,
qbo_payment_id: qboPaymentId,
- total: totalAmt,
- invoices_paid: mode === 'unapplied' ? 0 : invoice_payments.length,
- message: `${modeLabel} $${totalAmt.toFixed(2)} erfasst (QBO: ${qboPaymentId}).`
+ message: `Downpayment $${parseFloat(amount).toFixed(2)} recorded (QBO: ${qboPaymentId}).`
});
-
} catch (error) {
- await dbClient.query('ROLLBACK').catch(() => {});
- console.error('❌ Payment Error:', error);
- res.status(500).json({ error: 'Payment fehlgeschlagen: ' + error.message });
- } finally {
- dbClient.release();
+ console.error('❌ Downpayment Error:', error);
+ res.status(500).json({ error: 'Downpayment failed: ' + error.message });
}
});
-
-// --- Lokale Payments auflisten ---
+// --- List local payments ---
app.get('/api/payments', async (req, res) => {
try {
const result = await pool.query(`
SELECT p.*, c.name as customer_name,
COALESCE(json_agg(json_build_object(
- 'invoice_id', pi.invoice_id,
- 'amount', pi.amount,
- 'invoice_number', i.invoice_number
+ 'invoice_id', pi.invoice_id, 'amount', pi.amount, 'invoice_number', i.invoice_number
)) FILTER (WHERE pi.id IS NOT NULL), '[]') 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
+ 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' });
}
});