An invoice, rounded two ways
Add some lines and watch the tax change depending on whether it is worked out per line or on the total — the difference that fails audits.
<div class="demo demo--wide">
<div class="demo-row">
<div class="demo-seg" id="country" role="group" aria-label="Country">
<button type="button" data-country="AE" aria-pressed="true">UAE 5%</button>
<button type="button" data-country="SA">Saudi 15%</button>
<button type="button" data-country="PK">Pakistan 18%</button>
<button type="button" data-country="BH">Bahrain 10%</button>
</div>
<button class="demo-btn demo-btn--ghost" id="add">Add a line</button>
<button class="demo-btn demo-btn--ghost" id="many">Ten lines of 0.30</button>
</div>
<table class="lines">
<thead><tr><th>Amount</th><th>Qty</th><th>Treatment</th><th class="num">Net</th><th class="num">Tax</th></tr></thead>
<tbody id="body"></tbody>
</table>
<div class="totals">
<div class="total" id="perLine"><span>Taxed per line</span></div>
<div class="total" id="perTotal"><span>Taxed on the total</span></div>
</div>
<p class="demo-hint" id="note"></p>
</div>
import { invoice, rateFor, countryFor } from 'https://www.devix.pk/cdn/oss/vat-calculator@1.0.0/index.js';
const body = document.getElementById('body');
const note = document.getElementById('note');
let country = 'AE';
let lines = [
{ amount: '10.99', quantity: 3, treatment: 'standard' },
{ amount: '50.00', quantity: 1, treatment: 'zero-rated' },
];
const render = () => {
const perLine = invoice(lines, { country, rounding_level: 'line' });
const perTotal = invoice(lines, { country, rounding_level: 'total' });
body.innerHTML = lines.map((line, i) => `
<tr>
<td><input class="demo-input" value="${line.amount}" data-i="${i}" data-field="amount"></td>
<td><input class="demo-input qty" value="${line.quantity}" data-i="${i}" data-field="quantity"></td>
<td>
<select class="demo-input" data-i="${i}" data-field="treatment">
${['standard', 'zero-rated', 'exempt', 'reverse-charge'].map((t) => `<option ${t === line.treatment ? 'selected' : ''}>${t}</option>`).join('')}
</select>
</td>
<td class="num">${perLine.lines[i].net}</td>
<td class="num">${perLine.lines[i].vat}</td>
</tr>`).join('');
const money = (r) => `${r.net} + ${r.vat} = <strong>${r.gross}</strong> ${r.currency ?? ''}`;
document.getElementById('perLine').innerHTML = `<span>Taxed per line</span><em>${money(perLine)}</em>`;
document.getElementById('perTotal').innerHTML = `<span>Taxed on the total</span><em>${money(perTotal)}</em>`;
const differ = perLine.vat !== perTotal.vat;
note.textContent = differ
? `The two disagree by ${(Math.abs(perLine.minor.vat - perTotal.minor.vat) / 100).toFixed(2)} ${perLine.currency ?? ''}. Both are correct somewhere — your tax authority says which, and the invoice has to match the return.`
: `${countryFor(country).name}: ${(rateFor(country) * 100).toFixed(0)}% since ${countryFor(country).periods[0].since}. These lines happen to round the same either way — try ten lines of 0.30.`;
};
body.addEventListener('input', (event) => {
const field = event.target.dataset.field;
if (!field) return;
lines[Number(event.target.dataset.i)][field] = event.target.value;
render();
});
body.addEventListener('change', (event) => {
if (event.target.dataset.field === 'treatment') {
lines[Number(event.target.dataset.i)].treatment = event.target.value;
render();
}
});
document.getElementById('country').addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
for (const other of event.currentTarget.querySelectorAll('button')) other.setAttribute('aria-pressed', String(other === button));
country = button.dataset.country;
render();
});
document.getElementById('add').addEventListener('click', () => {
lines.push({ amount: '1.00', quantity: 1, treatment: 'standard' });
render();
});
document.getElementById('many').addEventListener('click', () => {
lines = Array.from({ length: 10 }, () => ({ amount: '0.30', quantity: 1, treatment: 'standard' }));
render();
});
render();
.lines { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.lines th { padding: 4px 6px; text-align: left; font: 10.5px var(--demo-mono); letter-spacing: .05em; text-transform: uppercase; color: var(--demo-dim); }
.lines td { padding: 3px 6px; border-top: 1px solid var(--demo-line); }
.lines .num { text-align: right; font-family: var(--demo-mono); }
.lines .demo-input { height: 30px; font-size: 12.5px; }
.lines .qty { width: 56px; }
.totals { display: grid; gap: 6px; }
.total { display: flex; justify-content: space-between; gap: 12px; padding: 8px 10px; border: 1px solid var(--demo-line); border-radius: 8px; font-size: 12.5px; }
.total em { font-family: var(--demo-mono); font-style: normal; }