sales tax

This commit is contained in:
2026-05-31 12:33:36 -05:00
parent 247219659d
commit b32fa96b79
7 changed files with 648 additions and 3 deletions

View File

@@ -95,6 +95,29 @@ router.get('/reports/tax-summary', async (req, res) => {
} catch (err) { handleQboError(err, res, 'tax-summary'); }
});
// ─── Sales Tax Periods ────────────────────────────────────────────
router.get('/sales-tax/periods', async (req, res) => {
try {
res.json(await accountingService.getTaxPeriods());
} catch (err) { handleQboError(err, res, 'sales-tax-periods'); }
});
router.post('/sales-tax/periods', async (req, res) => {
try {
res.json(await accountingService.upsertTaxPeriod(req.body));
} catch (err) { handleQboError(err, res, 'sales-tax-periods'); }
});
router.post('/sales-tax/periods/:id/record', async (req, res) => {
try {
res.json(await accountingService.createTaxPaymentJE({
periodId: req.params.id,
...req.body
}));
} catch (err) { handleQboError(err, res, 'sales-tax-record'); }
});
// ════════════════════════════════════════════════════════════════════
// Phase 2 Lieferung 1 — Sync + Cache-Reads
// ════════════════════════════════════════════════════════════════════

View File

@@ -510,6 +510,141 @@ async function getTaxSummary({ startDate, endDate, accountingMethod = 'Accrual'
};
}
// ════════════════════════════════════════════════════════════════════
// Sales Tax Periods — local source of truth for tax filings
// ════════════════════════════════════════════════════════════════════
async function getTaxPeriods() {
const result = await pool.query(
'SELECT * FROM sales_tax_periods ORDER BY period_start DESC'
);
return result.rows;
}
async function upsertTaxPeriod(period) {
const result = await pool.query(
`INSERT INTO sales_tax_periods
(period_start, period_end, total_sales, nontaxable_sales, taxable_sales, tax_collected,
adjustment_amount, adjustment_reason, adjustment_account_id, adjustment_account_name,
net_paid, bank_account_id, bank_account_name,
sales_tax_payable_id, sales_tax_payable_name)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
ON CONFLICT (period_start, period_end) DO UPDATE SET
total_sales = EXCLUDED.total_sales,
nontaxable_sales = EXCLUDED.nontaxable_sales,
taxable_sales = EXCLUDED.taxable_sales,
tax_collected = EXCLUDED.tax_collected,
adjustment_amount = COALESCE(sales_tax_periods.adjustment_amount, EXCLUDED.adjustment_amount),
adjustment_reason = COALESCE(sales_tax_periods.adjustment_reason, EXCLUDED.adjustment_reason),
adjustment_account_id = COALESCE(sales_tax_periods.adjustment_account_id, EXCLUDED.adjustment_account_id),
adjustment_account_name = COALESCE(sales_tax_periods.adjustment_account_name, EXCLUDED.adjustment_account_name),
net_paid = COALESCE(sales_tax_periods.net_paid, EXCLUDED.net_paid),
bank_account_id = COALESCE(sales_tax_periods.bank_account_id, EXCLUDED.bank_account_id),
bank_account_name = COALESCE(sales_tax_periods.bank_account_name, EXCLUDED.bank_account_name),
sales_tax_payable_id = COALESCE(sales_tax_periods.sales_tax_payable_id, EXCLUDED.sales_tax_payable_id),
sales_tax_payable_name = COALESCE(sales_tax_periods.sales_tax_payable_name, EXCLUDED.sales_tax_payable_name),
updated_at = CURRENT_TIMESTAMP
RETURNING *`,
[
period.period_start, period.period_end,
period.total_sales, period.nontaxable_sales, period.taxable_sales, period.tax_collected,
period.adjustment_amount || 0, period.adjustment_reason || null,
period.adjustment_account_id || null, period.adjustment_account_name || null,
period.net_paid || null, period.bank_account_id || null, period.bank_account_name || null,
period.sales_tax_payable_id || null, period.sales_tax_payable_name || null
]
);
return result.rows[0];
}
async function createTaxPaymentJE({
periodId, txnDate, taxCollected, adjustmentAmount, netPaid,
salesTaxPayableId, salesTaxPayableName,
adjustmentAccountId, adjustmentAccountName, adjustmentReason,
bankAccountId, bankAccountName
}) {
const { companyId, baseUrl } = getClientInfo();
const url = withMinorVersion(`${baseUrl}/v3/company/${companyId}/journalentry`);
const lines = [
{
DetailType: 'JournalEntryLineDetail',
Amount: taxCollected,
JournalEntryLineDetail: {
PostingType: 'Debit',
AccountRef: { value: String(salesTaxPayableId), name: salesTaxPayableName }
}
}
];
if (adjustmentAmount > 0) {
lines.push({
DetailType: 'JournalEntryLineDetail',
Amount: adjustmentAmount,
Description: adjustmentReason || undefined,
JournalEntryLineDetail: {
PostingType: 'Credit',
AccountRef: { value: String(adjustmentAccountId), name: adjustmentAccountName }
}
});
} else if (adjustmentAmount < 0) {
lines.push({
DetailType: 'JournalEntryLineDetail',
Amount: Math.abs(adjustmentAmount),
Description: adjustmentReason || undefined,
JournalEntryLineDetail: {
PostingType: 'Debit',
AccountRef: { value: String(adjustmentAccountId), name: adjustmentAccountName }
}
});
}
lines.push({
DetailType: 'JournalEntryLineDetail',
Amount: netPaid,
JournalEntryLineDetail: {
PostingType: 'Credit',
AccountRef: { value: String(bankAccountId), name: bankAccountName }
}
});
const payload = {
TxnDate: txnDate,
DocNumber: `STP-${periodId}`,
Line: lines,
PrivateNote: adjustmentReason ? `Adjustment: ${adjustmentReason}` : undefined
};
let qboResponse;
try {
const response = await makeQboApiCall({
url,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
qboResponse = response;
} catch (e) {
const errMsg = e.originalMessage || e.message || String(e);
throw new Error(`QBO JournalEntry failed: ${errMsg}`);
}
const data = getJson(qboResponse);
throwIfFault(data, 'JournalEntry create');
const je = data.JournalEntry;
if (!je || !je.Id) throw new Error('QBO did not return a JournalEntry ID');
await pool.query(
`UPDATE sales_tax_periods
SET qbo_journal_entry_id = $1, booked_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE id = $2`,
[je.Id, periodId]
);
return { qbo_journal_entry_id: je.Id, qbo_sync_token: je.SyncToken };
}
// ════════════════════════════════════════════════════════════════════
// Phase 2 Lieferung 1 — Caches und Sync
// ════════════════════════════════════════════════════════════════════
@@ -1562,6 +1697,9 @@ module.exports = {
getProfitAndLoss,
getBalanceSheet,
getTaxSummary,
getTaxPeriods,
upsertTaxPeriod,
createTaxPaymentJE,
normalizeTransactionListReport,
// Phase 2 Lieferung 1 — Sync