env param & single source of truth
This commit is contained in:
@@ -217,12 +217,120 @@ async function syncInvoiceToQbo(invoiceId, dbClient) { // <-- Nutzt jetzt dbClie
|
||||
console.log(`✅ QBO Invoice ${invoice.qbo_id} synced (SyncToken: ${updated.SyncToken})`);
|
||||
return { success: true, sync_token: updated.SyncToken };
|
||||
}
|
||||
/**
|
||||
* Records a Stripe payment in QBO:
|
||||
* 1) Creates a QBO Payment linked to the invoice (deposited to Undeposited Funds)
|
||||
* 2) Optionally creates a Purchase/Expense for the Stripe fee
|
||||
* Controlled by env QBO_BOOK_STRIPE_FEES — default OFF
|
||||
*
|
||||
* @param {Object} invoice - Invoice row (must have qbo_id, customer_qbo_id, invoice_number)
|
||||
* @param {number} amount - Amount received (gross from Stripe)
|
||||
* @param {string} methodLabel - 'ACH' or 'Credit Card'
|
||||
* @param {number} stripeFee - Stripe processing fee (informational only unless flag set)
|
||||
* @param {string} reference - Stripe paymentIntentId / sessionId
|
||||
* @param {Object} [opts]
|
||||
* @param {string} [opts.source] - 'manual' | 'auto-polled' (only affects PrivateNote text)
|
||||
* @returns {{ paymentId: string, feeBooked: boolean, feeSkipped: boolean }}
|
||||
*/
|
||||
async function recordStripePaymentInQbo(invoice, amount, methodLabel, stripeFee, reference, opts = {}) {
|
||||
const { companyId, baseUrl } = getClientInfo();
|
||||
const source = opts.source || 'manual';
|
||||
const sourceSuffix = source === 'auto-polled' ? ' — auto-polled' : '';
|
||||
|
||||
// ── 1. Create QBO Payment ──
|
||||
const paymentPayload = {
|
||||
CustomerRef: { value: invoice.customer_qbo_id },
|
||||
TotalAmt: amount,
|
||||
TxnDate: new Date().toISOString().split('T')[0],
|
||||
PaymentRefNum: reference ? reference.substring(0, 21) : 'Stripe',
|
||||
PrivateNote: `Stripe ${methodLabel}${sourceSuffix}`,
|
||||
Line: [{
|
||||
Amount: amount,
|
||||
LinkedTxn: [{
|
||||
TxnId: invoice.qbo_id,
|
||||
TxnType: 'Invoice'
|
||||
}]
|
||||
}],
|
||||
// Deposit to Undeposited Funds — Stripe payout reconciles this later
|
||||
DepositToAccountRef: { value: '221' }
|
||||
};
|
||||
|
||||
console.log(`📤 QBO Payment: $${amount.toFixed(2)} for Invoice #${invoice.invoice_number} (${methodLabel}${sourceSuffix})...`);
|
||||
|
||||
const paymentRes = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/payment`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(paymentPayload)
|
||||
});
|
||||
|
||||
const paymentData = paymentRes.getJson ? paymentRes.getJson() : paymentRes.json;
|
||||
if (paymentData.Fault) {
|
||||
const errMsg = paymentData.Fault.Error?.map(e => `${e.Message}: ${e.Detail}`).join('; ');
|
||||
throw new Error('QBO Payment failed: ' + errMsg);
|
||||
}
|
||||
console.log(`✅ QBO Payment created: ID ${paymentData.Payment?.Id}`);
|
||||
|
||||
// ── 2. Create QBO Expense for Stripe Fee ──
|
||||
// Only if explicitly enabled via env flag. We get the fee details from Stripe payout reports
|
||||
// for accounting, so booking them as separate expenses in QBO is redundant.
|
||||
const bookFee = process.env.QBO_BOOK_STRIPE_FEES === 'true';
|
||||
|
||||
if (stripeFee > 0 && !bookFee) {
|
||||
console.log(`ℹ️ Stripe fee $${stripeFee.toFixed(2)} NOT booked in QBO (QBO_BOOK_STRIPE_FEES != 'true')`);
|
||||
return {
|
||||
paymentId: paymentData.Payment?.Id,
|
||||
feeBooked: false,
|
||||
feeSkipped: true
|
||||
};
|
||||
}
|
||||
|
||||
if (stripeFee > 0 && bookFee) {
|
||||
const expensePayload = {
|
||||
AccountRef: { value: '244', name: 'PlainsCapital Bank' },
|
||||
TxnDate: new Date().toISOString().split('T')[0],
|
||||
PaymentType: 'Check',
|
||||
PrivateNote: `Stripe processing fee for Invoice #${invoice.invoice_number} (${methodLabel})${sourceSuffix}`,
|
||||
Line: [{
|
||||
DetailType: 'AccountBasedExpenseLineDetail',
|
||||
Amount: stripeFee,
|
||||
AccountBasedExpenseLineDetail: {
|
||||
AccountRef: { value: '1150040001', name: 'Payment Processing Fees' }
|
||||
},
|
||||
Description: `Stripe ${methodLabel} fee — Invoice #${invoice.invoice_number}`
|
||||
}]
|
||||
};
|
||||
|
||||
console.log(`📤 QBO Fee Expense: $${stripeFee.toFixed(2)}...`);
|
||||
|
||||
const expenseRes = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/purchase`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(expensePayload)
|
||||
});
|
||||
|
||||
const expenseData = expenseRes.getJson ? expenseRes.getJson() : expenseRes.json;
|
||||
if (expenseData.Fault) {
|
||||
console.error('⚠️ QBO Expense booking failed:', JSON.stringify(expenseData.Fault));
|
||||
// Don't throw — payment itself is valid
|
||||
} else {
|
||||
console.log(`✅ QBO Expense created: ID ${expenseData.Purchase?.Id}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paymentId: paymentData.Payment?.Id,
|
||||
feeBooked: stripeFee > 0 && bookFee,
|
||||
feeSkipped: false
|
||||
};
|
||||
}
|
||||
module.exports = {
|
||||
QBO_LABOR_ID,
|
||||
QBO_PARTS_ID,
|
||||
QBO_SUBSCRIPTION_ID,
|
||||
getClientInfo,
|
||||
exportInvoiceToQbo,
|
||||
syncInvoiceToQbo
|
||||
syncInvoiceToQbo,
|
||||
recordStripePaymentInQbo
|
||||
};
|
||||
Reference in New Issue
Block a user