If you are building internal tools for freelancers or small teams, an invoice generator looks deceptively easy: a few inputs, some addition, and a "Save as PDF" button.
I recently built and tested a small, dependency-free, English/Korean invoice and quotation tool. The experience reminded me that the difficult parts of business software are not the fields—they are the cases that quietly produce incorrect numbers, unsafe exports, or lost work.
Here are seven checks you can borrow for your own browser-based tools, with examples and practical limitations.
1. Decide the calculation order before touching the UI
An invoice with discounts and tax needs a documented calculation policy. For our simple example, the discount reduces the taxable subtotal; tax then applies to the discounted amount.
const round2 = n => Math.round((n + Number.EPSILON) * 100) / 100;
function quote({ items, discountPct = 0, taxPct = 0 }) {
const subtotal = round2(
items.reduce((sum, line) => sum + round2(line.qty * line.price), 0)
);
const discount = round2(subtotal * discountPct / 100);
const taxable = round2(subtotal - discount);
const tax = round2(taxable * taxPct / 100);
return { subtotal, discount, tax, total: round2(taxable + tax) };
}
const result = quote({
items: [{ qty: 1, price: 350 }, { qty: 4, price: 220 }, { qty: 2, price: 85 }],
discountPct: 5,
taxPct: 10
});
console.log(result);
// { subtotal: 1400, discount: 70, tax: 133, total: 1463 }
This is illustrative arithmetic, not a universal tax rule. Whether discounts, shipping, withholding, or exemptions affect a tax base depends on jurisdiction and the actual transaction.
Also, Number.EPSILON and binary floating-point rounding are not a general-purpose accounting solution. For serious financial systems, store amounts in integer minor units or use a well-tested decimal arithmetic library, and establish how taxes should round per line versus per document.
2. Currency display is not currency conversion
A tool may let a user select USD, KRW, EUR, or JPY. That does not mean the amount has been converted.
function display(amount, currency, locale) {
return new Intl.NumberFormat(locale, {
style: "currency",
currency
}).format(amount);
}
display(2500, "KRW", "ko-KR");
display(2500, "USD", "en-US");
Both calls format 2500 in the named currency. They do not fetch an exchange rate.
We made that limitation explicit in the UI and README. If you later introduce FX, you'll need to specify rate provider, timestamp, quote direction, spread, and rounding.
Reference: MDN — Intl.NumberFormat.
3. Never trust a text field just because it is "only for printing"
Client names, descriptions, and payment notes are untrusted input. It is tempting to interpolate them into an HTML preview:
// Don't do this with user-controlled strings:
preview.innerHTML = "<strong>" + clientName + "</strong>";
Prefer creating text nodes:
const nameNode = document.createElement("strong");
nameNode.textContent = clientName;
preview.appendChild(nameNode);
For a richer template, you can either use DOM APIs throughout or correctly escape every interpolated text field. Be especially careful with attributes, links, and rich text: the right defense depends on the context.
One of our automated checks entered <img src=x onerror=alert(1)> as a business name. The preview had to display the literal text without creating an image element.
4. A CSV download has its own security edge cases
If a user exports a spreadsheet containing customer-controlled data, consider strings beginning with =, +, -, or @. Spreadsheet software may interpret some as formulas.
A minimal defensive CSV-cell function:
function csvCell(value) {
let str = String(value ?? "");
if (/^\s*[=+@-]/.test(str)) str = "'" + str;
return '"' + str.replace(/"/g, '""') + '"';
}
That is a conservative example, not a guarantee for every spreadsheet application or import mode. Test with the spreadsheet products and workflows you actually support. CSV export should be validated just as carefully as HTML output.
5. Local storage is a convenience, not a backup strategy
Browser localStorage is attractive for a small offline app: no server, no account, and fast restoration of an unfinished document.
But it is tied to the browser's storage context. Browser data can be cleared, and content opened from file:// may have browser-specific storage behavior.
So we support both explicit Save draft on this device and a portable JSON export/import workflow. I would not describe a document as "safely backed up" unless users have a copy they control beyond a single browser profile.
A JSON importer should limit input size, validate expected structure, reject hostile surprises, and avoid turning imported values into executable markup.
Reference: MDN — Web Storage API.
6. The PDF button should be tested with a real PDF
window.print() alone proves only that a print operation can be requested. It does not prove the document is readable or fits an A4 page.
We added CSS print rules and used headless Chrome in an end-to-end test to create a real PDF, not just invoke a mock print dialog.
Example Playwright assertion:
const pdf = await page.pdf({
format: "A4",
printBackground: true
});
if (pdf.subarray(0, 4).toString() !== "%PDF") {
throw new Error("Not a PDF");
}
We also test visible invoice totals before printing. For complex layouts, check page breaks, table rows, long descriptions, fonts and page count visually.
Reference: MDN — Printing.
7. Test on narrow screens, even if most users print on desktops
A four-column line-item editor that looks excellent at 1440 pixels can become unusable at 390 pixels.
A simple browser check catches horizontal overflow:
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - window.innerWidth
);
if (overflow > 2) throw new Error("Horizontal overflow");
I treat this as a smoke test, not a substitute for manual usability review. Zoom, long labels, screen readers, and keyboard navigation deserve their own pass.
What actually passed?
For this small project, we ran calculation unit tests and a headless-browser suite. The checks covered sample totals, Korean labels, KRW formatting, invoice/quote switching, HTML text escaping, draft persistence, JSON export/import, CSV export, a generated PDF, mobile overflow, and unintended outbound network requests.
That does not establish regulatory compliance or production-grade accounting correctness. It establishes a clearer baseline than "it works on my laptop."
You can try the free, single-item quote calculator, which runs in the browser without sending form values to a server. There is also a bilingual client handoff checklist covering estimates, delivery, and invoicing.
Disclosure: I developed this article with AI assistance as part of an independent tools project. The fuller offline invoice kit is currently pre-launch; its paid checkout is not active while seller verification is pending. This post is educational, not financial, tax, or legal advice. If you have feedback about offline document tooling, constructive discussion is welcome.
What edge case did you encounter when building a "simple" internal tool?
Top comments (0)