sales tax

This commit is contained in:
2026-08-08 05:37:14 -05:00
parent 0218fa694e
commit c63e1070e0
3 changed files with 263 additions and 42 deletions

View File

@@ -0,0 +1,19 @@
-- Speichert die Distrikt-Aufschlüsselung einer Sales-Tax-Periode zum Zeitpunkt
-- des Speicherns. Die Aufschlüsselung liegt in der getTaxSummary()-Antwort bereits
-- vor (accounting-service.js:438-466) und wurde bisher im Frontend verworfen.
--
-- Rein additiv: die vier Aggregat-Spalten (total_sales, nontaxable_sales,
-- taxable_sales, tax_collected) und ihre Werte bleiben unangetastet.
--
-- Bestandszeilen bleiben bewusst NULL. Sie werden NICHT rückwirkend per
-- Live-Abfrage befuellt — deren Datumsfenster weicht ab (Datums-Roundtrip in
-- accounting-service.js:393). Altperioden laufen dauerhaft über die als
-- indikativ gekennzeichnete Live-Anzeige.
--
-- Idempotent: mehrfach ausführbar.
ALTER TABLE sales_tax_periods
ADD COLUMN IF NOT EXISTS district_breakdown JSONB;
COMMENT ON COLUMN sales_tax_periods.district_breakdown IS
'Snapshot der Distrikt-Aufschlüsselung beim Speichern. NULL = kein Snapshot vorhanden (Altperiode), Anzeige faellt auf indikative Live-Werte zurueck.';

View File

@@ -155,6 +155,17 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
const adjStr = adj > 0 ? `${fmtMoney(adj)}` : adj < 0 ? `+$${Math.abs(adj).toFixed(2)}` : '—'; const adjStr = adj > 0 ? `${fmtMoney(adj)}` : adj < 0 ? `+$${Math.abs(adj).toFixed(2)}` : '—';
const paidOn = formatDate(existingPeriod.booked_at); const paidOn = formatDate(existingPeriod.booked_at);
// Aufschlüsselung nur, wenn sie beim Speichern festgehalten wurde. Für
// Altperioden bleibt es wie bisher bei den Aggregaten — hier wird bewusst
// NICHT live nachgeladen.
const paidSnapshot = snapshotFromPeriod(existingPeriod, EMPTY_TAX_DATA);
const paidBreakdownHtml = paidSnapshot.districts.length
? `<div class="mb-4">
${renderQboReport(reportFromSnapshot(paidSnapshot.districts, paidSnapshot, existingPeriod.period_start, existingPeriod.period_end))}
<p class="mt-2 text-xs text-gray-500">District rows and grand total both come from the saved snapshot for this period.</p>
</div>`
: '';
detailEl.innerHTML = ` detailEl.innerHTML = `
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4"> <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
@@ -162,6 +173,7 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
<button onclick="window.salesTaxView.closeTaxPeriodDetail()" class="px-2 py-1 text-gray-400 hover:text-gray-600 text-lg leading-none">&times;</button> <button onclick="window.salesTaxView.closeTaxPeriodDetail()" class="px-2 py-1 text-gray-400 hover:text-gray-600 text-lg leading-none">&times;</button>
</div> </div>
<p class="text-sm text-green-700 font-semibold mb-4">✅ Paid on ${paidOn}</p> <p class="text-sm text-green-700 font-semibold mb-4">✅ Paid on ${paidOn}</p>
${paidBreakdownHtml}
<table class="min-w-full text-sm border border-gray-200 rounded"> <table class="min-w-full text-sm border border-gray-200 rounded">
<tbody> <tbody>
<tr class="border-t"><td class="px-3 py-2 text-gray-600">Total Sales</td><td class="px-3 py-2 text-right font-medium">${fmtMoney(parseFloat(existingPeriod.total_sales) || 0)}</td></tr> <tr class="border-t"><td class="px-3 py-2 text-gray-600">Total Sales</td><td class="px-3 py-2 text-right font-medium">${fmtMoney(parseFloat(existingPeriod.total_sales) || 0)}</td></tr>
@@ -193,19 +205,15 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
console.error('Tax summary fetch failed, showing saved snapshot:', e.message); console.error('Tax summary fetch failed, showing saved snapshot:', e.message);
} }
// Gespeicherte Periode → DB-Snapshot ist maßgeblich; der Live-Abruf liefert // Gespeicherte Periode → DB-Snapshot ist maßgeblich. Neue Periode → live.
// nur die Distrikt-Zeilen. Neue Periode → live geladene Werte.
const liveParsed = parseTaxDataFromReport(taxData); const liveParsed = parseTaxDataFromReport(taxData);
const useSnapshot = !!existingPeriod; const useSnapshot = !!existingPeriod;
const parsed = useSnapshot ? snapshotFromPeriod(existingPeriod, liveParsed) : liveParsed; const parsed = useSnapshot ? snapshotFromPeriod(existingPeriod, liveParsed) : liveParsed;
const hasSavedBreakdown = useSnapshot && parsed.districts.length > 0;
const reportData = useSnapshot ? withSnapshotGrandTotal(taxData, parsed) : taxData; const { html: reportHtml, note: snapshotNote } = renderPeriodBreakdown({
const reportHtml = reportData?.Rows taxData, parsed, startDate, endDate, existingPeriod, useSnapshot, hasSavedBreakdown
? renderQboReport(reportData) });
: `<p class="text-sm text-gray-500">No tax data for this period.</p>`;
const snapshotNote = useSnapshot
? `<p class="mt-2 text-xs text-gray-500">Grand total shown is the saved QBO snapshot for this period${existingPeriod.updated_at ? ` (last saved ${formatDate(existingPeriod.updated_at)})` : ''}. The district rows above are recalculated live and may differ in detail.</p>`
: '';
stCurrentTaxData = { ...parsed, startDate, endDate }; stCurrentTaxData = { ...parsed, startDate, endDate };
const taxCollected = parsed.taxCollected; const taxCollected = parsed.taxCollected;
@@ -235,7 +243,7 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
<div class="mt-4 border-t pt-4"> <div class="mt-4 border-t pt-4">
<div class="flex items-center gap-4 text-sm mb-3" id="st-summary-bar"> <div class="flex items-center gap-4 text-sm mb-3" id="st-summary-bar">
<span><strong>Tax Collected:</strong> ${fmtMoney(taxCollected)}${useSnapshot ? ' <span class="text-xs text-gray-400">(saved)</span>' : ''}</span> <span class="${useSnapshot ? 'px-2 py-1 rounded bg-gray-100 border border-gray-300' : ''}"><strong>Tax Collected:</strong> ${fmtMoney(taxCollected)}${useSnapshot ? ` <span class="text-xs text-gray-500">(saved${hasSavedBreakdown ? '' : ' — authoritative'})</span>` : ''}</span>
<span id="st-adj-display" class="${adjustments !== 0 ? 'text-red-600' : ''}"><strong>Adjustment:</strong> ${adjustments > 0 ? `${fmtMoney(adjustments)}` : adjustments < 0 ? `+$${Math.abs(adjustments).toFixed(2)}` : '—'}</span> <span id="st-adj-display" class="${adjustments !== 0 ? 'text-red-600' : ''}"><strong>Adjustment:</strong> ${adjustments > 0 ? `${fmtMoney(adjustments)}` : adjustments < 0 ? `+$${Math.abs(adjustments).toFixed(2)}` : '—'}</span>
<span id="st-net-due" class="text-lg font-bold text-gray-900"><strong>Net Due:</strong> ${fmtMoney(netPaid)}</span> <span id="st-net-due" class="text-lg font-bold text-gray-900"><strong>Net Due:</strong> ${fmtMoney(netPaid)}</span>
</div> </div>
@@ -335,6 +343,19 @@ export function updateTaxPreview() {
} }
} }
/**
* Breakdown, der beim Speichern mitgeht — oder null.
*
* null bedeutet für den Server "nicht anfassen" (COALESCE im UPSERT). Bei einer
* Altperiode ohne gespeicherten Breakdown ist stCurrentTaxData.districts leer,
* weil snapshotFromPeriod() bewusst nicht auf die Live-Zeilen zurückfällt — ein
* erneutes Speichern befüllt die Spalte also NICHT nachträglich mit Live-Daten.
*/
function breakdownForSave() {
const districts = stCurrentTaxData?.districts;
return Array.isArray(districts) && districts.length ? districts : null;
}
export async function saveTaxPeriodDraft() { export async function saveTaxPeriodDraft() {
const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0;
const adjReason = document.getElementById('st-adjustment-reason').value.trim(); const adjReason = document.getElementById('st-adjustment-reason').value.trim();
@@ -352,6 +373,7 @@ export async function saveTaxPeriodDraft() {
adjustment_amount: adjAmount, adjustment_amount: adjAmount,
adjustment_reason: adjReason || null, adjustment_reason: adjReason || null,
net_paid: stCurrentTaxData.taxCollected - adjAmount, net_paid: stCurrentTaxData.taxCollected - adjAmount,
district_breakdown: breakdownForSave(),
status: 'open' status: 'open'
}); });
await loadTaxPeriods(); await loadTaxPeriods();
@@ -387,6 +409,7 @@ export async function markPeriodPaid() {
adjustment_amount: adjAmount, adjustment_amount: adjAmount,
adjustment_reason: adjReason || null, adjustment_reason: adjReason || null,
net_paid: netPaid, net_paid: netPaid,
district_breakdown: breakdownForSave(),
status: 'open' status: 'open'
}); });
} catch (e) { } catch (e) {
@@ -405,21 +428,109 @@ export async function markPeriodPaid() {
} }
} }
/**
* Entscheidet, welche Aufschlüsselung eine Periode zeigt:
* 1. gespeicherter Breakdown → Zeilen und Grand Total aus derselben DB-Zeile
* 2. gespeichert, kein Breakdown (Altperiode) → Live-Zeilen, als indikativ markiert
* 3. neue Periode → Live-Report, live ist hier maßgeblich
*/
function renderPeriodBreakdown({ taxData, parsed, startDate, endDate, existingPeriod, useSnapshot, hasSavedBreakdown }) {
const savedOn = existingPeriod?.updated_at ? ` (saved ${formatDate(existingPeriod.updated_at)})` : '';
if (hasSavedBreakdown) {
return {
html: renderQboReport(reportFromSnapshot(parsed.districts, parsed, startDate, endDate)),
note: `<p class="mt-2 text-xs text-gray-500">District rows and grand total both come from the saved snapshot for this period${savedOn}.</p>`
};
}
if (!useSnapshot) {
return {
html: taxData?.Rows ? renderQboReport(taxData) : `<p class="text-sm text-gray-500">No tax data for this period.</p>`,
note: ''
};
}
// Altperiode: kein Snapshot der Aufschlüsselung vorhanden und keiner mehr
// rekonstruierbar. Live-Zeilen optisch untergeordnet und explizit entwertet.
const liveTable = taxData?.Rows
? `<div class="opacity-60 text-xs">${renderQboReport(markLiveGrandTotalAsIndicative(taxData))}</div>`
: `<p class="text-sm text-gray-500">No live tax data for this period.</p>`;
return {
html: `
<div class="rounded border border-amber-200 bg-amber-50 px-3 py-2 mb-3">
<p class="text-sm font-semibold text-amber-900">Indicative live breakdown — not the filed figures</p>
<p class="text-xs text-amber-800 mt-0.5">
No district breakdown was stored for this period${savedOn ? savedOn.replace(' (saved', ' (recorded') : ''}.
The rows below are recalculated live from QuickBooks and will not add up to the grand total.
<strong>The saved grand total below is the authoritative figure.</strong>
</p>
</div>
${liveTable}`,
note: ''
};
}
const EMPTY_TAX_DATA = { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0, districts: [] };
function isGrandTotalSection(row) {
return row?.type === 'Section'
&& String(row.Header?.ColData?.[0]?.value || '').toLowerCase().includes('grand total');
}
/**
* Liest die GRAND-TOTAL-Aggregate UND die Distrikt-Sections aus der
* getTaxSummary()-Antwort. Beide stammen aus demselben Belegsatz
* (accounting-service.js:438-494), sind also in sich konsistent.
*/
function parseTaxDataFromReport(taxData) { function parseTaxDataFromReport(taxData) {
if (!taxData?.Rows?.Row) return { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0 }; const rows = Array.isArray(taxData?.Rows?.Row) ? taxData.Rows.Row : null;
const rows = Array.isArray(taxData.Rows.Row) ? taxData.Rows.Row : []; if (!rows) return { ...EMPTY_TAX_DATA };
for (const row of rows) {
if (row.type === 'Section' && row.Header?.ColData?.[0]?.value?.toLowerCase().includes('grand total')) { const districts = rows
.filter(row => row?.type === 'Section' && !isGrandTotalSection(row))
.map(row => {
const cd = row.Summary?.ColData || []; const cd = row.Summary?.ColData || [];
return { return {
totalSales: parseFloat(cd[1]?.value || '0') || 0, name: row.Header?.ColData?.[0]?.value || '',
nontaxableSales: parseFloat(cd[2]?.value || '0') || 0, ratePct: row.Header?.ColData?.[5]?.value || cd[5]?.value || '',
taxableSales: parseFloat(cd[3]?.value || '0') || 0, totalSales: numOr(cd[1]?.value, 0),
taxCollected: parseFloat(cd[4]?.value || '0') || 0 nontaxableSales: numOr(cd[2]?.value, 0),
taxableSales: numOr(cd[3]?.value, 0),
taxCollected: numOr(cd[4]?.value, 0)
}; };
} });
const grand = rows.find(isGrandTotalSection);
if (!grand) return { ...EMPTY_TAX_DATA, districts };
const cd = grand.Summary?.ColData || [];
return {
totalSales: numOr(cd[1]?.value, 0),
nontaxableSales: numOr(cd[2]?.value, 0),
taxableSales: numOr(cd[3]?.value, 0),
taxCollected: numOr(cd[4]?.value, 0),
districts
};
}
/** JSONB-Spalte: pg liefert bereits ein Objekt, ältere Pfade evtl. einen String. */
function parseBreakdownColumn(value) {
if (!value) return null;
let parsed = value;
if (typeof value === 'string') {
try { parsed = JSON.parse(value); } catch { return null; }
} }
return { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0 }; if (!Array.isArray(parsed) || !parsed.length) return null;
return parsed.map(d => ({
name: d?.name || '',
ratePct: d?.ratePct || '',
totalSales: numOr(d?.totalSales, 0),
nontaxableSales: numOr(d?.nontaxableSales, 0),
taxableSales: numOr(d?.taxableSales, 0),
taxCollected: numOr(d?.taxCollected, 0)
}));
} }
/** numeric aus der DB (pg liefert NUMERIC als String) — sonst Fallback. */ /** numeric aus der DB (pg liefert NUMERIC als String) — sonst Fallback. */
@@ -433,40 +544,127 @@ function numOr(value, fallback) {
* Kennzahlen einer gespeicherten Periode aus der DB-Zeile. * Kennzahlen einer gespeicherten Periode aus der DB-Zeile.
* Fällt spaltenweise auf den Live-Wert zurück, falls die Spalte leer ist * Fällt spaltenweise auf den Live-Wert zurück, falls die Spalte leer ist
* (Altbestände ohne vollständigen Snapshot). * (Altbestände ohne vollständigen Snapshot).
*
* districts kommt AUSSCHLIESSLICH aus district_breakdown — nie aus dem Live-Abruf.
* Sonst würde ein erneutes Speichern einer Altperiode eine live berechnete
* Aufschlüsselung neben den abweichenden gespeicherten Grand Total schreiben.
*/ */
function snapshotFromPeriod(period, fallback) { function snapshotFromPeriod(period, fallback) {
return { return {
totalSales: numOr(period.total_sales, fallback.totalSales), totalSales: numOr(period.total_sales, fallback.totalSales),
nontaxableSales: numOr(period.nontaxable_sales, fallback.nontaxableSales), nontaxableSales: numOr(period.nontaxable_sales, fallback.nontaxableSales),
taxableSales: numOr(period.taxable_sales, fallback.taxableSales), taxableSales: numOr(period.taxable_sales, fallback.taxableSales),
taxCollected: numOr(period.tax_collected, fallback.taxCollected) taxCollected: numOr(period.tax_collected, fallback.taxCollected),
districts: parseBreakdownColumn(period.district_breakdown) || []
};
}
function round2(n) { return Math.round(n * 100) / 100; }
/** Verkaufsspalten wie im Live-Report als ganze Dollar, damit beide Tabellen gleich aussehen. */
function salesCell(n) { return String(Math.round(n)); }
const REPORT_COLUMNS = {
Column: [
{ ColTitle: '', ColType: 'Text' },
{ ColTitle: 'Total Sales', ColType: 'Money' },
{ ColTitle: 'Nontaxable', ColType: 'Money' },
{ ColTitle: 'Taxable', ColType: 'Money' },
{ ColTitle: 'Tax Collected', ColType: 'Money' },
{ ColTitle: 'Tax Rate', ColType: 'Percent' }
]
};
/**
* Baut aus dem gespeicherten Snapshot eine Report-Struktur für renderQboReport().
* Distrikt-Zeilen und Grand Total stammen dann aus derselben DB-Zeile.
*
* Bleibt zwischen der Summe der Distriktbeträge und dem gespeicherten Grand Total
* ein Cent-Rest stehen (die Distriktwerte wurden je Zeile einzeln gerundet, der
* Grand Total einmal am Ende — accounting-service.js:461 vs. :491), wird er als
* eigene Zeile ausgewiesen statt stillschweigend geschluckt.
*/
function reportFromSnapshot(districts, snapshot, startDate, endDate) {
const rows = districts.map(d => ({
type: 'Section',
Header: {
ColData: [
{ value: d.name }, { value: '' }, { value: '' },
{ value: '' }, { value: '' }, { value: d.ratePct || '' }
]
},
Summary: {
ColData: [
{ value: 'Total' },
{ value: salesCell(d.totalSales) },
{ value: salesCell(d.nontaxableSales) },
{ value: salesCell(d.taxableSales) },
{ value: d.taxCollected.toFixed(2) },
{ value: d.ratePct || '' }
]
}
}));
const districtSum = round2(districts.reduce((s, d) => s + d.taxCollected, 0));
const roundingRest = round2(snapshot.taxCollected - districtSum);
if (roundingRest !== 0) {
rows.push({
ColData: [
{ value: 'Rounding difference' }, { value: '' }, { value: '' },
{ value: '' }, { value: roundingRest.toFixed(2) }, { value: '' }
]
});
}
rows.push({
type: 'Section',
Header: {
ColData: [
{ value: 'GRAND TOTAL' }, { value: '' }, { value: '' },
{ value: '' }, { value: '' }, { value: '' }
]
},
Summary: {
ColData: [
{ value: 'Total' },
{ value: salesCell(snapshot.totalSales) },
{ value: salesCell(snapshot.nontaxableSales) },
{ value: salesCell(snapshot.taxableSales) },
{ value: snapshot.taxCollected.toFixed(2) },
{ value: '' }
]
}
});
return {
Header: {
ReportName: 'Sales Tax Liability (saved snapshot)',
StartPeriod: String(startDate).split('T')[0],
EndPeriod: String(endDate).split('T')[0],
ReportBasis: 'Accrual'
},
Columns: REPORT_COLUMNS,
Rows: { Row: rows }
}; };
} }
/** /**
* Ersetzt im live geladenen Report die GRAND-TOTAL-Zeile durch den gespeicherten * Kennzeichnet die GRAND-TOTAL-Zeile des Live-Reports als indikativ und lässt ihre
* Snapshot, damit die Tabelle nicht einen anderen Gesamtbetrag zeigt als die * Live-Werte stehen. Die Tabelle bleibt damit in sich konsistent (Live-Zeilen
* Summary-Leiste darunter. Die Distrikt-Zeilen bleiben unberührt (Aufschlüsselung). * summieren sich auf ihren eigenen Live-Total); der maßgebliche gespeicherte Betrag
* Arbeitet auf einer flachen Kopie — taxData selbst wird nicht mutiert. * steht separat darunter. Bewusst KEINE Ersetzung durch den Snapshot-Wert — das
* würde suggerieren, die Live-Zeilen summierten sich darauf auf.
* Arbeitet auf einer flachen Kopie — taxData wird nicht mutiert.
*/ */
function withSnapshotGrandTotal(taxData, snapshot) { function markLiveGrandTotalAsIndicative(taxData) {
if (!taxData?.Rows?.Row || !Array.isArray(taxData.Rows.Row)) return taxData; if (!Array.isArray(taxData?.Rows?.Row)) return taxData;
const rows = taxData.Rows.Row.map(row => { const rows = taxData.Rows.Row.map(row => {
const label = row?.Header?.ColData?.[0]?.value; if (!isGrandTotalSection(row)) return row;
if (row?.type !== 'Section' || !label || !label.toLowerCase().includes('grand total')) return row;
const cd = row.Summary?.ColData || []; const cd = row.Summary?.ColData || [];
return { return {
...row, ...row,
Summary: { Header: { ColData: [{ value: 'LIVE TOTAL (INDICATIVE)' }, ...(row.Header.ColData.slice(1))] },
ColData: [ Summary: { ColData: [{ value: 'Live total' }, ...cd.slice(1)] }
{ value: cd[0]?.value || 'Total' },
{ value: snapshot.totalSales.toFixed(2) },
{ value: snapshot.nontaxableSales.toFixed(2) },
{ value: snapshot.taxableSales.toFixed(2) },
{ value: snapshot.taxCollected.toFixed(2) },
{ value: cd[5]?.value || '' }
]
}
}; };
}); });
return { ...taxData, Rows: { ...taxData.Rows, Row: rows } }; return { ...taxData, Rows: { ...taxData.Rows, Row: rows } };

View File

@@ -534,8 +534,8 @@ async function upsertTaxPeriod(period) {
(period_start, period_end, total_sales, nontaxable_sales, taxable_sales, tax_collected, (period_start, period_end, total_sales, nontaxable_sales, taxable_sales, tax_collected,
adjustment_amount, adjustment_reason, adjustment_account_id, adjustment_account_name, adjustment_amount, adjustment_reason, adjustment_account_id, adjustment_account_name,
net_paid, bank_account_id, bank_account_name, net_paid, bank_account_id, bank_account_name,
sales_tax_payable_id, sales_tax_payable_name, status) sales_tax_payable_id, sales_tax_payable_name, status, district_breakdown)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
ON CONFLICT (period_start, period_end) DO UPDATE SET ON CONFLICT (period_start, period_end) DO UPDATE SET
total_sales = EXCLUDED.total_sales, total_sales = EXCLUDED.total_sales,
nontaxable_sales = EXCLUDED.nontaxable_sales, nontaxable_sales = EXCLUDED.nontaxable_sales,
@@ -551,6 +551,9 @@ async function upsertTaxPeriod(period) {
sales_tax_payable_id = COALESCE(sales_tax_periods.sales_tax_payable_id, EXCLUDED.sales_tax_payable_id), 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), sales_tax_payable_name = COALESCE(sales_tax_periods.sales_tax_payable_name, EXCLUDED.sales_tax_payable_name),
status = COALESCE(sales_tax_periods.status, EXCLUDED.status), status = COALESCE(sales_tax_periods.status, EXCLUDED.status),
-- Additiv: ein bereits festgehaltener Breakdown wird nie durch NULL
-- ueberschrieben; Altperioden bleiben ohne Snapshot.
district_breakdown = COALESCE(EXCLUDED.district_breakdown, sales_tax_periods.district_breakdown),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
RETURNING *`, RETURNING *`,
[ [
@@ -560,7 +563,8 @@ async function upsertTaxPeriod(period) {
period.adjustment_account_id || null, period.adjustment_account_name || 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.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, period.sales_tax_payable_id || null, period.sales_tax_payable_name || null,
period.status || 'open' period.status || 'open',
period.district_breakdown ? JSON.stringify(period.district_breakdown) : null
] ]
); );
return result.rows[0]; return result.rows[0];