diff --git a/migrations/rename-external-to-paid.sql b/migrations/rename-external-to-paid.sql new file mode 100644 index 0000000..da5bfee --- /dev/null +++ b/migrations/rename-external-to-paid.sql @@ -0,0 +1 @@ +UPDATE sales_tax_periods SET status = 'paid' WHERE status = 'external'; diff --git a/public/js/utils/api.js b/public/js/utils/api.js index 0f4ee07..61e748a 100644 --- a/public/js/utils/api.js +++ b/public/js/utils/api.js @@ -149,11 +149,10 @@ const API = { upsertTaxPeriod: (data) => fetch('/api/accounting/sales-tax/periods', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(r => r.json()), - recordTaxPayment: (periodId, data) => fetch(`/api/accounting/sales-tax/periods/${periodId}/record`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) - }).then(r => r.json()), - markTaxPaidExternal: (periodId) => fetch(`/api/accounting/sales-tax/periods/${periodId}/mark-external`, { - method: 'POST' + markTaxPaidExternal: (periodId, paidDate) => fetch(`/api/accounting/sales-tax/periods/${periodId}/mark-external`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paidDate }) }).then(r => r.json()), // Customer Revenue Report diff --git a/public/js/views/accounting-view.js b/public/js/views/accounting-view.js index aa03dad..573968d 100644 --- a/public/js/views/accounting-view.js +++ b/public/js/views/accounting-view.js @@ -35,7 +35,6 @@ let crEndDate = null; let stPeriods = []; let stEditingPeriodId = null; // current period in detail dialog, null = new -let stAccounts = []; // cached account list for pickers let expStartDate = null; let expEndDate = null; @@ -879,14 +878,15 @@ function renderTaxPeriodsTable() { const pStatus = p.status || (p.qbo_journal_entry_id ? 'booked' : 'open'); let statusHtml, statusColor; if (pStatus === 'booked') { statusHtml = 'Booked'; statusColor = 'bg-green-100 text-green-800'; } - else if (pStatus === 'external') { statusHtml = 'External'; statusColor = 'bg-blue-100 text-blue-800'; } + else if (pStatus === 'external' || pStatus === 'paid') { statusHtml = 'Paid'; statusColor = 'bg-green-100 text-green-800'; } else { statusHtml = 'Open'; statusColor = 'bg-yellow-100 text-yellow-800'; } const monthLabel = new Date(p.period_start).toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); const adj = parseFloat(p.adjustment_amount) || 0; const adjStr = adj !== 0 ? (adj > 0 ? `−$${adj.toFixed(2)}` : `+$${Math.abs(adj).toFixed(2)}`) : '—'; const netPaid = parseFloat(p.net_paid) || parseFloat(p.tax_collected) || 0; - const paidOn = pStatus === 'external' ? 'Paid in QBO' : (p.booked_at ? formatDate(p.booked_at) : '—'); + const paidOn = (pStatus === 'external' || pStatus === 'paid' || pStatus === 'booked') + ? (p.booked_at ? formatDate(p.booked_at) : '—') : '—'; return ` @@ -953,13 +953,6 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) { return; } - if (!stAccounts.length) { - try { - const accts = await window.API.accounting.getAccounts() || []; - stAccounts = Array.isArray(accts) ? accts : []; - } catch (e) { stAccounts = []; } - } - const reportHtml = taxData?.Rows ? renderQboReport(taxData) : `

No tax data for this period.

`; const parsed = parseTaxDataFromReport(taxData); @@ -975,21 +968,6 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) { const isOpen = periodStatus === 'open'; const isEditable = isOpen; - const bankOpts = stAccounts - .filter(a => a.accountType === 'Bank' || a.accountType === 'Credit Card') - .map(a => ``).join(''); - const liabilityOpts = stAccounts - .filter(a => a.accountType === 'Other Current Liability') - .map(a => ``).join(''); - const adjustOpts = stAccounts - .filter(a => a.accountType === 'Income' || a.accountType === 'Expense' || a.classification === 'Revenue' || a.classification === 'Expense') - .map(a => { - const sel = a.id === existingPeriod?.adjustment_account_id ? 'selected' : ''; - const name = (a.fullyQualifiedName || a.name || ''); - const isDiscount = name.toLowerCase().includes('discount'); - return ``; - }).join(''); - const today = todayISO(); const [y, m] = startDate.split('-').map(Number); const monthLabel = new Date(y, m - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); @@ -1010,71 +988,43 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) { Net Due: ${fmtMoney(netPaid)} + ${isEditable ? `
-

Positive = discount (reduces net due)

-
- - -
-
- -
-
- - -
-
- - -
-
- - Paid Date (QBO) +
- ${isEditable ? `
-

Journal Entry lines:

-

Debit Sales Tax Payable: ${fmtMoney(taxCollected)}

- ${adjustments > 0 ? `

Credit Discount: ${fmtMoney(adjustments)}

` : ''} - ${adjustments < 0 ? `

Debit Penalty: ${fmtMoney(Math.abs(adjustments))}

` : ''} -

Credit Bank: ${fmtMoney(netPaid)}

+

Summary:

+

Tax Collected: ${fmtMoney(taxCollected)}

+ ${adjustments > 0 ? `

Discount: \u2212${fmtMoney(adjustments)}

` : ''} + ${adjustments < 0 ? `

Penalty: +$${Math.abs(adjustments).toFixed(2)}

` : ''} +

Net Due: ${fmtMoney(netPaid)}

- -
@@ -1084,7 +1034,7 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) { ` : `
-

📋 Marked as paid externally on ${formatDate(existingPeriod.booked_at)}

+

✅ Paid on ${formatDate(existingPeriod.booked_at)}

`} @@ -1125,28 +1075,17 @@ export function updateTaxPreview() { const jeLines = document.getElementById('st-je-lines'); if (jeLines) { jeLines.innerHTML = ` -

Journal Entry lines:

-

Debit Sales Tax Payable: ${fmtMoney(taxCollected)}

- ${adj > 0 ? `

Credit Discount: ${fmtMoney(adj)}

` : ''} - ${adj < 0 ? `

Debit Penalty: ${fmtMoney(Math.abs(adj))}

` : ''} -

Credit Bank: ${fmtMoney(netPaid)}

`; +

Summary:

+

Tax Collected: ${fmtMoney(taxCollected)}

+ ${adj > 0 ? `

Discount: \u2212${fmtMoney(adj)}

` : ''} + ${adj < 0 ? `

Penalty: +$${Math.abs(adj).toFixed(2)}

` : ''} +

Net Due: ${fmtMoney(netPaid)}

`; } } export async function saveTaxPeriodDraft() { const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; const adjReason = document.getElementById('st-adjustment-reason').value.trim(); - const adjAccountEl = document.getElementById('st-adjustment-account'); - const adjAccountId = adjAccountEl.value; - const adjAccountName = adjAccountId ? adjAccountEl.options[adjAccountEl.selectedIndex]?.text : ''; - - const bankEl = document.getElementById('st-bank-account'); - const bankId = bankEl.value; - const bankName = bankId ? bankEl.options[bankEl.selectedIndex]?.text : ''; - - const payableEl = document.getElementById('st-payable-account'); - const payableId = payableEl.value; - const payableName = payableId ? payableEl.options[payableEl.selectedIndex]?.text : ''; if (!stCurrentTaxData) return alert('No tax data loaded.'); @@ -1160,13 +1099,8 @@ export async function saveTaxPeriodDraft() { tax_collected: stCurrentTaxData.taxCollected, adjustment_amount: adjAmount, adjustment_reason: adjReason || null, - adjustment_account_id: adjAccountId || null, - adjustment_account_name: adjAccountName || null, net_paid: stCurrentTaxData.taxCollected - adjAmount, - bank_account_id: bankId || null, - bank_account_name: bankName || null, - sales_tax_payable_id: payableId || null, - sales_tax_payable_name: payableName || null + status: 'open' }); await loadTaxPeriods(); if (period?.period_start) { @@ -1177,116 +1111,18 @@ export async function saveTaxPeriodDraft() { } } -export async function recordTaxPayment() { +export async function markPeriodPaid() { const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; const adjReason = document.getElementById('st-adjustment-reason').value.trim(); - const adjAccountEl = document.getElementById('st-adjustment-account'); - const adjAccountId = adjAccountEl.value; - const adjAccountName = adjAccountId ? adjAccountEl.options[adjAccountEl.selectedIndex]?.text : ''; - - const bankEl = document.getElementById('st-bank-account'); - const bankId = bankEl.value; - const bankName = bankId ? bankEl.options[bankEl.selectedIndex]?.text : ''; - - const payableEl = document.getElementById('st-payable-account'); - const payableId = payableEl.value; - const payableName = payableId ? payableEl.options[payableEl.selectedIndex]?.text : ''; - - const txnDate = document.getElementById('st-payment-date').value; - if (!txnDate) return alert('Please select a payment date.'); + const paidDate = document.getElementById('st-paid-date').value; + if (!paidDate) return alert('Please select a paid date.'); if (!stCurrentTaxData) return alert('No tax data loaded.'); - - const taxCollected = stCurrentTaxData.taxCollected; - const netPaid = taxCollected - adjAmount; - - if (!payableId) return alert('Please select the Sales Tax Payable account.'); - if (!bankId) return alert('Please select a bank account.'); - - const preview = `Journal Entry on ${txnDate}:\n\n` + - `Debit Sales Tax Payable: $${taxCollected.toFixed(2)}\n` + - (adjAmount > 0 ? `Credit Discount: $${adjAmount.toFixed(2)} (${adjReason || 'Adjustment'})\n` : '') + - (adjAmount < 0 ? `Debit Penalty: $${Math.abs(adjAmount).toFixed(2)} (${adjReason || 'Adjustment'})\n` : '') + - `Credit Bank: $${netPaid.toFixed(2)}\n\n` + - (adjAmount > 0 ? `Debits (${taxCollected.toFixed(2)}) = Credits (${adjAmount.toFixed(2)} + ${netPaid.toFixed(2)} = ${(adjAmount + netPaid).toFixed(2)})` : - adjAmount < 0 ? `Debits (${taxCollected.toFixed(2)} + ${Math.abs(adjAmount).toFixed(2)} = ${(taxCollected + Math.abs(adjAmount)).toFixed(2)}) = Credits (${netPaid.toFixed(2)})` : - `Debits = Credits = ${taxCollected.toFixed(2)}`) + - '\n\nRecord this in QBO?'; - - if (!confirm(preview)) return; - - let periodId = stEditingPeriodId; - try { - const period = await window.API.accounting.upsertTaxPeriod({ - period_start: stCurrentTaxData.startDate, - period_end: stCurrentTaxData.endDate, - total_sales: stCurrentTaxData.totalSales, - nontaxable_sales: stCurrentTaxData.nontaxableSales, - taxable_sales: stCurrentTaxData.taxableSales, - tax_collected: stCurrentTaxData.taxCollected, - adjustment_amount: adjAmount, - adjustment_reason: adjReason || null, - adjustment_account_id: adjAccountId || null, - adjustment_account_name: adjAccountName || null, - net_paid: netPaid, - bank_account_id: bankId || null, - bank_account_name: bankName || null, - sales_tax_payable_id: payableId || null, - sales_tax_payable_name: payableName || null - }); - periodId = period.id; - } catch (e) { - return alert('Failed to save period before payment: ' + e.message); - } - - if (typeof showSpinner === 'function') showSpinner('Recording tax payment in QBO…'); - try { - const result = await window.API.accounting.recordTaxPayment(periodId, { - txnDate, - taxCollected, - adjustmentAmount: adjAmount, - netPaid, - salesTaxPayableId: payableId, - salesTaxPayableName: payableName, - adjustmentAccountId: adjAccountId || null, - adjustmentAccountName: adjAccountName || null, - adjustmentReason: adjReason || null, - bankAccountId: bankId, - bankAccountName: bankName - }); - if (result.error) return alert('Failed: ' + result.error); - await loadTaxPeriods(); - const updated = stPeriods.find(p => p.id === periodId); - if (updated) { - await openTaxPeriodDetail(updated.period_start, updated.period_end, updated); - } - } catch (e) { - alert('QBO payment failed: ' + e.message); - } finally { - if (typeof hideSpinner === 'function') hideSpinner(); - } -} - -export async function markTaxPaidExternal() { if (!stEditingPeriodId) return alert('Please save the period first (Save Draft).'); - const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; - const adjReason = document.getElementById('st-adjustment-reason').value.trim(); - const adjAccountEl = document.getElementById('st-adjustment-account'); - const adjAccountId = adjAccountEl.value; - const adjAccountName = adjAccountId ? adjAccountEl.options[adjAccountEl.selectedIndex]?.text : ''; + const netPaid = stCurrentTaxData.taxCollected - adjAmount; - const bankEl = document.getElementById('st-bank-account'); - const bankId = bankEl.value; - const bankName = bankId ? bankEl.options[bankEl.selectedIndex]?.text : ''; - - const payableEl = document.getElementById('st-payable-account'); - const payableId = payableEl.value; - const payableName = payableId ? payableEl.options[payableEl.selectedIndex]?.text : ''; - - if (!confirm(`Mark this period as already paid in QBO (external)?\n\nNo Journal Entry will be created — this only records the status in the app.\n\nTax Collected: $${stCurrentTaxData.taxCollected.toFixed(2)}\nAdjustment: $${adjAmount.toFixed(2)}\nNet Due: $${(stCurrentTaxData.taxCollected - adjAmount).toFixed(2)}`)) return; - - if (!stCurrentTaxData) return alert('No tax data loaded.'); + if (!confirm(`Mark this period as paid?\n\nPaid Date: ${paidDate}\nTax Collected: $${stCurrentTaxData.taxCollected.toFixed(2)}\nAdjustment: $${adjAmount.toFixed(2)}\nNet Paid: $${netPaid.toFixed(2)}\n\nThis does NOT write to QBO — record the payment in QBO first.`)) return; try { await window.API.accounting.upsertTaxPeriod({ @@ -1298,13 +1134,7 @@ export async function markTaxPaidExternal() { tax_collected: stCurrentTaxData.taxCollected, adjustment_amount: adjAmount, adjustment_reason: adjReason || null, - adjustment_account_id: adjAccountId || null, - adjustment_account_name: adjAccountName || null, - net_paid: stCurrentTaxData.taxCollected - adjAmount, - bank_account_id: bankId || null, - bank_account_name: bankName || null, - sales_tax_payable_id: payableId || null, - sales_tax_payable_name: payableName || null, + net_paid: netPaid, status: 'open' }); } catch (e) { @@ -1312,7 +1142,7 @@ export async function markTaxPaidExternal() { } try { - const result = await window.API.accounting.markTaxPaidExternal(stEditingPeriodId); + await window.API.accounting.markTaxPaidExternal(stEditingPeriodId, paidDate); await loadTaxPeriods(); const updated = stPeriods.find(p => p.id === stEditingPeriodId); if (updated) { @@ -1413,8 +1243,7 @@ window.accountingView = { closeTaxPeriodDetail, updateTaxPreview, saveTaxPeriodDraft, - recordTaxPayment, - markTaxPaidExternal, + markPeriodPaid, loadCustomerRevenue, exportCustomerRevenuePdf, exportProfitLossPdf, diff --git a/src/routes/accounting.js b/src/routes/accounting.js index 59a14e8..3a5b1e5 100644 --- a/src/routes/accounting.js +++ b/src/routes/accounting.js @@ -114,18 +114,9 @@ router.post('/sales-tax/periods', async (req, res) => { } 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'); } -}); - router.post('/sales-tax/periods/:id/mark-external', async (req, res) => { try { - res.json(await accountingService.markTaxPaidExternal(req.params.id)); + res.json(await accountingService.markTaxPaidExternal(req.params.id, req.body.paidDate)); } catch (err) { handleQboError(err, res, 'sales-tax-mark-external'); } }); diff --git a/src/services/accounting-service.js b/src/services/accounting-service.js index 2f9333a..0b9d6f8 100644 --- a/src/services/accounting-service.js +++ b/src/services/accounting-service.js @@ -566,101 +566,13 @@ async function upsertTaxPeriod(period) { 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, status = 'booked', 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 }; -} - -async function markTaxPaidExternal(periodId) { +async function markTaxPaidExternal(periodId, paidDate = null) { const result = await pool.query( `UPDATE sales_tax_periods - SET status = 'external', booked_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP + SET status = 'paid', booked_at = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND status = 'open' RETURNING *`, - [periodId] + [periodId, paidDate || new Date().toISOString().split('T')[0]] ); if (result.rows.length === 0) { const existing = await pool.query('SELECT status FROM sales_tax_periods WHERE id = $1', [periodId]); @@ -1724,7 +1636,6 @@ module.exports = { getTaxSummary, getTaxPeriods, upsertTaxPeriod, - createTaxPaymentJE, markTaxPaidExternal, normalizeTransactionListReport,