diff --git a/src/services/accounting-service.js b/src/services/accounting-service.js index 38a0903..af2951f 100644 --- a/src/services/accounting-service.js +++ b/src/services/accounting-service.js @@ -368,11 +368,108 @@ async function getBalanceSheet({ asOfDate, accountingMethod = 'Accrual' } = {}) } async function getTaxSummary({ startDate, endDate, accountingMethod = 'Accrual' } = {}) { - const url = buildReportUrl('TaxSummary', { start_date: startDate, end_date: endDate, accounting_method: accountingMethod }); - const response = await makeQboApiCall({ url, method: 'GET' }); - const data = getJson(response); - throwIfFault(data, 'TaxSummary report'); - return data; + const { companyId, baseUrl } = getClientInfo(); + const qboQuery = (sql) => { + const url = `${baseUrl}/v3/company/${companyId}/query?query=${encodeURIComponent(sql)}&minorversion=${QBO_MINOR_VERSION}`; + return makeQboApiCall({ url, method: 'GET' }); + }; + + const rateMap = {}; + try { + const rateRes = await qboQuery('SELECT * FROM TaxRate MAXRESULTS 1000'); + const rateData = getJson(rateRes); + throwIfFault(rateData, 'TaxRate query'); + const rates = rateData?.QueryResponse?.TaxRate || []; + for (const r of rates) { + rateMap[r.Id] = { + name: r.Name || `Tax Rate ${r.Id}`, + rateValue: parseFloat(r.RateValue) || 0 + }; + } + } catch (e) { + console.error('TaxRate query failed, proceeding without rate names:', e.message); + } + + const whereClause = `TxnDate >= '${startDate}' AND TxnDate <= '${endDate}'`; + const agg = {}; + + for (const entity of ['Invoice', 'SalesReceipt']) { + try { + const res = await qboQuery(`SELECT * FROM ${entity} WHERE ${whereClause} MAXRESULTS 1000`); + const data = getJson(res); + throwIfFault(data, `${entity} query`); + const docs = data?.QueryResponse?.[entity] || []; + for (const doc of docs) { + const taxDetail = doc.TxnTaxDetail; + if (!taxDetail || !taxDetail.TaxLine) continue; + const taxLines = Array.isArray(taxDetail.TaxLine) ? taxDetail.TaxLine : [taxDetail.TaxLine]; + for (const line of taxLines) { + const detail = line.TaxLineDetail; + if (!detail || !detail.TaxRateRef) continue; + const rateId = String(detail.TaxRateRef.value); + const taxable = parseFloat(detail.NetAmountTaxable) || 0; + const collected = parseFloat(line.Amount) || 0; + if (!agg[rateId]) agg[rateId] = { taxableSales: 0, taxCollected: 0 }; + agg[rateId].taxableSales += taxable; + agg[rateId].taxCollected += collected; + } + } + } catch (e) { + console.error(`${entity} query failed:`, e.message); + } + } + + const rows = Object.entries(agg).map(([rateId, amounts]) => { + const info = rateMap[rateId] || { name: `Tax Rate ${rateId}`, rateValue: 0 }; + const ratePct = info.rateValue.toFixed(3).replace(/0+$/, '').replace(/\.$/, ''); + return { + type: 'Section', + Header: { + ColData: [ + { value: info.name }, + { value: '' }, + { value: '' }, + { value: `${ratePct}%` } + ] + }, + Summary: { + ColData: [ + { value: 'Total' }, + { value: amounts.taxableSales.toFixed(2) }, + { value: amounts.taxCollected.toFixed(2) }, + { value: '' } + ] + } + }; + }); + + rows.sort((a, b) => { + const na = a.Header.ColData[0].value; + const nb = b.Header.ColData[0].value; + if (na.includes('State') && !nb.includes('State')) return -1; + if (!na.includes('State') && nb.includes('State')) return 1; + return na.localeCompare(nb); + }); + + return { + Header: { + ReportName: 'Sales Tax Liability', + StartPeriod: startDate, + EndPeriod: endDate, + ReportBasis: accountingMethod + }, + Columns: { + Column: [ + { ColTitle: '', ColType: 'Text' }, + { ColTitle: 'Taxable Sales', ColType: 'Money' }, + { ColTitle: 'Tax Collected', ColType: 'Money' }, + { ColTitle: 'Tax Rate', ColType: 'Percent' } + ] + }, + Rows: { + Row: rows + } + }; } // ════════════════════════════════════════════════════════════════════