payments - 1. version

This commit is contained in:
2026-02-18 09:39:06 -06:00
parent 2bb304babe
commit acb588425a
4 changed files with 564 additions and 11 deletions

198
server.js
View File

@@ -1611,6 +1611,204 @@ 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)
// =====================================================
// --- 1. Bank-Konten aus QBO laden (für "Deposit To" Dropdown) ---
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'
: '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)}`,
method: 'GET'
});
const data = response.getJson ? response.getJson() : response.json;
const accounts = (data.QueryResponse?.Account || []).map(acc => ({
id: acc.Id,
name: acc.Name,
fullName: acc.FullyQualifiedName || acc.Name
}));
res.json(accounts);
} catch (error) {
console.error('Error fetching QBO accounts:', error);
res.status(500).json({ error: 'Error fetching bank accounts: ' + error.message });
}
});
// --- 2. Payment Methods aus QBO laden (für Check/ACH Dropdown) ---
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'
: '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)}`,
method: 'GET'
});
const data = response.getJson ? response.getJson() : response.json;
const methods = (data.QueryResponse?.PaymentMethod || []).map(pm => ({
id: pm.Id,
name: pm.Name
}));
res.json(methods);
} catch (error) {
console.error('Error fetching payment methods:', error);
res.status(500).json({ error: 'Error fetching payment methods: ' + error.message });
}
});
// --- 3. Payment in QBO erstellen (ein Check für 1..n Invoices) ---
app.post('/api/qbo/record-payment', async (req, res) => {
const {
invoice_ids, // Array von lokalen Invoice IDs
payment_date, // 'YYYY-MM-DD'
reference_number, // Check-Nummer oder ACH-Referenz
payment_method_id, // QBO PaymentMethod ID
deposit_to_account_id // QBO Bank Account ID
} = req.body;
if (!invoice_ids || invoice_ids.length === 0) {
return res.status(400).json({ error: 'Keine Rechnungen ausgewählt.' });
}
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';
// 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
WHERE i.id = ANY($1)`,
[invoice_ids]
);
const invoicesData = invoicesResult.rows;
// Validierung: Alle müssen eine qbo_id haben (schon in QBO)
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(', ')}`
});
}
// 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.'
});
}
const customerQboId = customerIds[0];
// Gesamtbetrag berechnen
const totalAmount = invoicesData.reduce((sum, inv) => sum + parseFloat(inv.total), 0);
// QBO Payment Objekt bauen
const payment = {
CustomerRef: {
value: customerQboId
},
TotalAmt: totalAmount,
TxnDate: payment_date,
PaymentRefNum: reference_number || '',
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'
}]
}))
};
console.log(`💰 Erstelle QBO Payment: $${totalAmount.toFixed(2)} für ${invoicesData.length} Rechnung(en), Kunde: ${invoicesData[0].customer_name}`);
// Payment an QBO senden
const response = await makeQboApiCall({
url: `${baseUrl}/v3/company/${companyId}/payment`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payment)
});
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 {
console.error('❌ QBO Payment Fehler:', JSON.stringify(data));
res.status(500).json({
error: 'QBO Payment fehlgeschlagen: ' + JSON.stringify(data.fault?.error?.[0]?.message || data)
});
}
} catch (error) {
await dbClient.query('ROLLBACK').catch(() => {});
console.error('❌ Payment Error:', error);
res.status(500).json({ error: 'Payment fehlgeschlagen: ' + error.message });
} finally {
dbClient.release();
}
});