Fintech / personal tooling
A self-built invoicing tool
A small Symfony command-line tool that generates my freelance invoices as PDFs and emails them — built to erase a monthly chore, with the quirks of Serbian invoicing baked in.
Overview
Every freelancer has a recurring administrative chore they quietly resent. Mine was invoicing — the same shapes typed into the same template every month, where the only thing that changes is a few numbers and the only reward for doing it by hand is the chance to make a mistake. So I built a small tool to do it for me. It is not glamorous, and that’s rather the point.
Problem
Manual invoicing is repetitive, and repetitive-but-must-be-correct is the worst combination there is. It’s boring enough that attention slips and important enough that a slip matters. On top of that, invoices here come with local rules — the amount has to appear written out in words, the bank instructions have to be right — so a generic invoice generator wouldn’t do. I wanted something that knew about my invoices specifically.
Solution
A command-line tool: bin/console invoice:send 3 builds the PDF for an invoice
and emails it, in one step. The data model is exactly as small as it needs to be
— organizations and the people at them, bank accounts, invoices and their line
items — and nothing more. It renders the invoice from a template to a PDF and
sends it, with the Serbian-specific touches (like spelling the total out in
words) handled as first-class features rather than afterthoughts.
Architecture
Symfony 6.4 on PHP 8.2, with Doctrine ORM over MariaDB. Invoices are modelled as
proper entities — Organization, Person, BankAccount, Invoice,
InvoiceItem — with value objects for the things that deserve their own type
rather than a loose string or int. PDF generation goes through mPDF from a Twig
template, delivery through the Symfony mailer, and a Google API client handles
storage. The whole thing runs in Docker (PHP-FPM, nginx, MariaDB, a mail catcher
for local runs) so it behaves the same on my machine as anywhere else.
The deliberate choice here was to make it a CLI tool, not a web app. It has no users but me and no reason to be a website; a console command is the smallest honest interface for the job, and it composes with everything else on the command line.
Technical challenges
The interesting corners are the local ones. Converting a numeric total into grammatically correct Serbian words (“iznos slovima”) is a small, self-contained language problem — the kind of thing that’s a one-liner in your head and a proper little service in code once you handle the cases. Keeping the tool boringly reliable — so that “run it and the right PDF lands in the right inbox” is never in doubt — was worth more than any feature. Some ideas are still on the list (pulling exchange rates from the National Bank, auto-detecting a bank from an account number), and that’s fine; a personal tool earns its features when a real need shows up.
Lessons learned
Build small tools for yourself. The best way to understand what makes software pleasant to use is to be the one who has to use it every month — you feel every rough edge personally, and you fix the ones that actually matter instead of the ones that look good in a feature list. This tool will never impress anyone, and it has quietly saved me the same hour, over and over, for years. That’s a good trade.