Case study · 11 / 11
Invoicing with no server, and invoices that stay true
A case study — RMZ SP Client Management
Summary
| | | |---|---| | **Problem** | Consultancy billing — clients, projects, time, milestones, invoices, PDFs and payment reconciliation — with no budget for a server component or premium connectors. | | **Constraint** | Everything had to run inside SharePoint Online with no Power Automate, no Azure function, and no external service. | | **Solution** | An SPFx web part over 14 provisioned lists, generating invoice PDFs in the browser with jsPDF and freezing billing details as snapshots so a regenerated invoice stays historically accurate. | | **Outcome** | Working. Four billing models, three-level payment-terms cascade, full payment reconciliation, idempotent setup. | | **Role** | Full implementation — data model, provisioning, all screens, PDF generation, invoice numbering. | | **Stack** | SPFx 1.22 · React 17 · TypeScript · Fluent UI v8 · PnPjs v4 · jsPDF |
1. The problem
Consultancy billing is a chain where every link has to hold: a client, a project under that client, work recorded against the project as time entries or milestones, an invoice built from that work, a PDF sent to the client, and a payment reconciled back against the invoice.
Off-the-shelf products do this well, and are the right answer for most people. They are the wrong answer when the work already lives in SharePoint, when the volume is small, and when the cost of another SaaS subscription and another integration exceeds the cost of the feature.
2. The constraint
No server-side component of any kind. No Power Automate — the connectors that would be needed are premium-licensed. No Azure function, no external PDF service, no middleware.
This constraint decides the architecture. Every invoicing system needs to render a document, and the normal answer is to do it server-side, where you control the fonts, the pagination and the output. Removing that option means the PDF has to be generated in the browser, in the user's session, from data the client already has.
3. Architecture
A single SPFx web part over 14 provisioned SharePoint lists, all namespaced with a common prefix so they are unmistakably the application's and cannot collide with anything else on the site:
Clients · Client Billing Profiles · Recipient Profiles Projects · Project Invoice Templates · Milestones · Time Entries Invoices · Invoice Lines · Invoice Effort Details · Payments Team Members · App Settings · Generated Invoices (library)
An Initial Setup screen creates or verifies all of them. It is idempotent — re-running after an upgrade creates what is missing and adds new columns without touching existing data — so upgrading is a documented step rather than a migration.
4. The interesting decisions
4.1 Invoice immutability by snapshot
This is the decision that matters most, and it is a data-modelling problem rather than a technical one.
An invoice references a client's billing details and the recipient's bank details. Those are rows in lists, and rows change: a client moves office, changes VAT registration, or switches bank.
If the invoice references those rows by lookup, then regenerating a PDF for an invoice issued eight months ago produces a document with today's details on it. That document no longer matches the one the client received, no longer matches the accounting record, and — depending on jurisdiction — may no longer be a valid tax document.
So invoices freeze billing and recipient details as snapshots onto the invoice itself at the point of issue, along with the invoice lines. Regeneration draws from the snapshot, not from the live profile. The invoice keeps its original identity permanently, and profiles are free to change without rewriting history.
The general principle: a document that is a record of a moment must not hold live references to things that move.
4.2 PDF generation in the browser
Invoice PDFs are generated client-side with jsPDF, drawing from the invoice data — real selectable text, correct pagination — and filed to a document library, from which they can be regenerated at any time from the stored lines and snapshots.
The trade-offs are real and worth being able to discuss: font embedding is limited compared with a server renderer, complex layout is more work, and generation consumes the user's session rather than a background worker. In exchange, the whole feature costs nothing to run, has no external dependency to fail, and needs no premium licence. At consultancy invoice volume, that is the correct side of the trade.
4.3 A three-level terms cascade
Payment terms can be set at three levels, with a defined precedence: template overrides project, project overrides client, and where nothing is configured the default is Net 30.
That precedence is a real business rule rather than a convenience. A client has standard terms; a particular project may have negotiated different ones; a specific invoice template — a milestone invoice, say — may have different ones again. Modelling it as a cascade with an explicit default means due dates are always derived rather than typed, and the answer to "why is this due on that date" is always traceable.
Due-date logic then uses the terms saved on the invoice, not the current cascade — consistent with the snapshot principle above.
4.4 Four billing models on one template mechanism
Hourly, fixed cost, milestone, and mixed — all handled by configurable per-project invoice templates rather than four separate code paths.
An hourly invoice draws from time entries; a milestone invoice from completed milestones; a fixed-cost invoice from the template itself; a mixed invoice from more than one source. Making the template the mechanism rather than the mode means a fifth arrangement is configuration, not code.
4.5 Managed numbering, and the scope that was declined
Invoice numbers are issued from an app settings list, so the sequence is managed centrally rather than derived from a list item ID that could produce gaps or collide.
Equally deliberate: approval workflows were left out, and the README says why — the current use case is a single person. Building a review-and-approve chain for one user would have been effort spent on a feature nobody could use, and a workflow with one participant is a form with extra steps. That scope decision is recorded rather than left as a silent omission, so the next person can see it was a choice.
5. Outcome
Working end to end: clients, billing and bank profiles, projects, invoice templates, milestones, time entries and team members; invoices assembled from SharePoint data; PDFs generated, filed and regenerable; payments reconciled with paid amount, balance, due date and status; and client/project filters across the dashboard, lists, invoices and payments.
The documented first-run path walks the whole chain — set up, create a client, add billing and recipient profiles, create a project and a template, add time entries or milestones, build a draft invoice, generate and save the PDF, record a payment, confirm the balance and status update, then reopen the invoice and regenerate its PDF — which is both an acceptance test and a demo script.
6. What I'd take from this
A hard constraint often produces a better answer than a free choice would have. No server component forced browser-side rendering, which removed an entire class of infrastructure, cost and failure mode. It would not scale to thousands of invoices a day — and it does not need to.
Records must not hold live references. The snapshot decision is the difference between an invoicing system and a system that quietly falsifies its own history. It is easy to get wrong, and the failure is silent until someone needs a reprint.
Recording what you chose not to build is part of the design. "Approval workflows are intentionally excluded for the current one-person use case" costs one line and saves the next person from assuming it was forgotten.