5 Aug 2026
·
7 min read
·
.NET · Billing · Angular
Prorated Billing That Survives Real Life
Members join mid-month, leave mid-month, and freeze for injuries. The invoice engine behind Qartvela turns messy membership intervals into invoices that are right the first time — in .NET 10, with the edge cases handled in one place.
Qartvela is a sports-club management platform: groups, students, trainers, halls, a weekly schedule, attendance, and payments. The part that took the most thinking was the smallest on screen — the monthly invoice. A flat fee per month is easy until a student joins on the 19th, another leaves on the 7th, and a third freezes for two weeks with a sprained ankle. Clubs handle that with spreadsheets and arguments. I wanted the system to handle it with one rule.
Model the interval, not the month
The mistake in most first attempts is to store ‘is this student active in September’ as a flag. The right primitive is a membership interval: a student belongs to a group from a start date to an optional end date, and freezes are intervals too. Billing then becomes a pure function: for a billing month, intersect each membership interval with the month, subtract freezes, and price the remaining days.
Everything else — join prorations, leave prorations, freezes — falls out of that one function. There is no special case for ‘joined mid-month’ because there is no concept of ‘mid-month’ in the code; there are only days.
Invoices are generated, never edited
Invoices are produced by the engine from the intervals, not typed by an administrator. If a student’s start date was entered wrong, you fix the start date and regenerate; the invoice line explains itself (‘Prorated join · 12 of 30 days’). That single decision removed an entire class of support questions, because the invoice and the membership record can never disagree.
States that match how clubs actually collect money
A club does not think in ‘paid’ and ‘unpaid’. Parents pay half now and half next week; a coach waives a fee for a scholarship kid. So an invoice has four states — paid, partial, overdue, waived — driven by recorded payments and an explicit waiver with a reason. The monthly collection overview in the Angular admin is just a query over those states.
Partial is a first-class state, not ‘unpaid with a note’.
Waived requires a reason and keeps the original amount, so reporting still shows what the club chose to give away.
Overdue is derived from the due date and never stored, so it can never be stale.
Keeping the engine testable
The proration function is pure and lives in the domain layer with no database dependency, so the edge cases are unit tests: join on the last day, leave on the first, a freeze that spans two months, February. The API layer — ASP.NET Core 10 with EF Core — only collects the intervals and persists the result.
If the invoice can be edited by hand, it will be. If it can only be regenerated, the data underneath gets fixed instead.
That is the whole lesson of the project: put the messy reality into the data model once, and let everything downstream stay boring.

Related project
Qartvela
Written by Lasha Kakashvili — Tech Lead at Koober Coders, instructor at IT Step Academy.