From dc778d2f5f8b554cfc8799d15e0434da4191fc1e Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 25 May 2026 14:49:01 -0500 Subject: [PATCH] fix for createRefund --- src/services/accounting-service.js | 102 +++++++++++++++++++---------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/src/services/accounting-service.js b/src/services/accounting-service.js index 51ed45b..c5b8028 100644 --- a/src/services/accounting-service.js +++ b/src/services/accounting-service.js @@ -900,10 +900,7 @@ async function createExpense(data) { /** - * Erstellt einen QBO Deposit für einen Vendor-Refund (Geld kam zurück aufs Konto). - * - * Buchhalterisch: Der Refund wird gegen die ursprüngliche Expense-Kategorie - * gebucht, wodurch sich der Aufwand in dieser Kategorie reduziert. + * Erstellt einen QBO Deposit oder Purchase Credit für einen Vendor-Refund. * * @param {Object} data * @param {string} data.vendorId - Pflicht (von wem kam der Refund) @@ -953,28 +950,60 @@ async function createRefund(data) { ); const vendorName = vendorRow.rows[0]?.display_name || data.vendorId; - // ── QBO Deposit Payload ── - const payload = { - DepositToAccountRef: { value: depositAcct.qbo_id, name: depositAcct.name }, - TxnDate: data.txnDate, - Line: [{ - DetailType: 'DepositLineDetail', - Amount: amount, - Description: data.memo || `Refund from ${vendorName}`, - DepositLineDetail: { - AccountRef: { value: categoryAcct.qbo_id, name: categoryAcct.name }, - Entity: { value: data.vendorId, type: 'Vendor' } - } - }] - }; - - if (data.refNo) payload.Line[0].DepositLineDetail.CheckNum = String(data.refNo).slice(0, 21); - if (data.memo) payload.PrivateNote = String(data.memo); - const { companyId, baseUrl } = getClientInfo(); - const url = withMinorVersion(`${baseUrl}/v3/company/${companyId}/deposit`); + const isCreditCard = depositAcct.account_type === 'Credit Card'; - const requestSummary = `REFUND | ${vendorName} → ${depositAcct.name} | ${categoryAcct.name} | ${data.txnDate} | $${amount.toFixed(2)}`; + let url; + let payload; + let entityTypeLabel; + + // ── Verzweigung: Credit Card Credit (Purchase) vs. Bank (Deposit) ── + if (isCreditCard) { + entityTypeLabel = 'Purchase'; + url = withMinorVersion(`${baseUrl}/v3/company/${companyId}/purchase`); + payload = { + AccountRef: { value: depositAcct.qbo_id, name: depositAcct.name }, + EntityRef: { value: data.vendorId, type: 'Vendor', name: vendorName }, + TxnDate: data.txnDate, + PaymentType: 'CreditCard', + Credit: true, // Zwingend erforderlich für einen CC Refund + Line: [{ + DetailType: 'AccountBasedExpenseLineDetail', + Amount: amount, + Description: data.memo || `Refund from ${vendorName}`, + AccountBasedExpenseLineDetail: { + AccountRef: { value: categoryAcct.qbo_id, name: categoryAcct.name } + } + }] + }; + if (data.refNo) payload.DocNumber = String(data.refNo).slice(0, 21); + if (data.memo) payload.PrivateNote = String(data.memo); + + } else { + entityTypeLabel = 'Deposit'; + url = withMinorVersion(`${baseUrl}/v3/company/${companyId}/deposit`); + payload = { + DepositToAccountRef: { value: depositAcct.qbo_id, name: depositAcct.name }, + TxnDate: data.txnDate, + Line: [{ + DetailType: 'DepositLineDetail', + Amount: amount, + Description: data.memo || `Refund from ${vendorName}`, + DepositLineDetail: { + AccountRef: { value: categoryAcct.qbo_id, name: categoryAcct.name }, + // Korrigierte QBO Entity-Struktur + Entity: { + Type: 'Vendor', + EntityRef: { value: data.vendorId, name: vendorName } + } + } + }] + }; + if (data.refNo) payload.Line[0].DepositLineDetail.CheckNum = String(data.refNo).slice(0, 21); + if (data.memo) payload.PrivateNote = String(data.memo); + } + + const requestSummary = `REFUND (${entityTypeLabel}) | ${vendorName} → ${depositAcct.name} | ${categoryAcct.name} | ${data.txnDate} | $${amount.toFixed(2)}`; let qboResponse; try { @@ -988,7 +1017,7 @@ async function createRefund(data) { } catch (err) { await writeAuditLog({ action: 'refund.create', - entityType: 'Deposit', + entityType: entityTypeLabel, status: 'error', requestExcerpt: requestSummary, responseExcerpt: err.message @@ -1002,34 +1031,35 @@ async function createRefund(data) { ).join('; '); await writeAuditLog({ action: 'refund.create', - entityType: 'Deposit', + entityType: entityTypeLabel, status: 'error', requestExcerpt: requestSummary, responseExcerpt: msg }); - const err = new Error('QBO Deposit (refund) create failed: ' + msg); + const err = new Error(`QBO ${entityTypeLabel} (refund) create failed: ${msg}`); err.qboFault = qboResponse.Fault; throw err; } - const deposit = qboResponse.Deposit; - if (!deposit || !deposit.Id) throw new Error('QBO returned no Deposit id'); + // Dynamisches Extrahieren basierend auf dem Entity-Typ + const resultEntity = isCreditCard ? qboResponse.Purchase : qboResponse.Deposit; + if (!resultEntity || !resultEntity.Id) throw new Error(`QBO returned no ${entityTypeLabel} id`); await writeAuditLog({ action: 'refund.create', - entityType: 'Deposit', - entityQboId: deposit.Id, + entityType: entityTypeLabel, + entityQboId: resultEntity.Id, status: 'success', requestExcerpt: requestSummary, - responseExcerpt: `Deposit ${deposit.Id} created, total $${Number(deposit.TotalAmt).toFixed(2)}` + responseExcerpt: `${entityTypeLabel} ${resultEntity.Id} created, total $${Number(resultEntity.TotalAmt).toFixed(2)}` }); - console.log(`✅ QBO Refund recorded: Deposit ${deposit.Id} — ${requestSummary}`); + console.log(`✅ QBO Refund recorded: ${entityTypeLabel} ${resultEntity.Id} — ${requestSummary}`); return { - id: deposit.Id, - txnDate: deposit.TxnDate, - totalAmt: Number(deposit.TotalAmt), + id: resultEntity.Id, + txnDate: resultEntity.TxnDate, + totalAmt: Number(resultEntity.TotalAmt), vendorName, depositAccountName: depositAcct.name, categoryName: categoryAcct.name