Premium and Fees¶
In exchange for the services we provide, our customers owe us money, in the form of a recurring fee. Our billing system computes and stores these fees at all times. Fees are the source of truth for our payroll and invoicing processes. They are the backbone of the entire billing system.
Terminology
In the context of insurance, fees are called premiums. However, in the context of other products we also call them subscription fees.
This component is responsible for:
- the computation of these fees
- providing an interface used to query the fees and updating their status
Understanding the computation¶
Fees are computed based on 3 inputs:
- price grid
- affiliation dates
- personal information of the beneficiary
The fee computation is essentially a pipeline transforming these simple inputs into a final number.
Let's run through a simple example
Let's pretend the price grid contains age-based brackets:
- [0-18] -> 10β¬ per month
- [19-24] -> 20β¬ per month
- [25-βΎοΈ] -> 30β¬ per month
John is 35 years old and enrolled on Alan April 15th 2026. How much is Alan owed for John's coverage in April 2026 ?
(num_days_covered) * (monthly_price) / 30
15 * 30 / 30
15β¬
Step 1: build the timeline¶
The inputs to the computation can evolve over time. Price grids can be updated by contract amendments. Beneficiaries grow older every year on their birthday.
The very first step is to establish a Timeline representing the lifecycle of these changes.
Every 'boundary' on the timeline represents a potential change to one of the inputs of the computation.
Because all the changes in inputs are flagged, every period itself is guaranteed to represent a stable cost.

The cost for one month during P2 is different from P1 because the underlying price grids have changed. The cost for one month during P3 is different from P2 because the beneficiary has been bumped to the next age-bracket in the grid. Etc...
Step 2: map to costs¶
Now that we have identified all the periods with stable cost, we can resolve the exact monthly price for each period. This resolution is as simple as it seems:
- establish the 'context' of the beneficiary: age, address, reference of price grid
- fetch the price grid and apply our member's context to get our exact monthly price

Step 3: overlap with subscription periods¶
Subscriptions have a 'periodicity'. The most common case is to bill 1 fee per month.
We can picture this as a Timeline where each period is 1 month.
If we overlap both timelines, we are getting closer to defining how much is owed for each month of coverage.

Step 4: proration¶
We gathered all the information we need to compute our fees. The very last step is to apply our proration rules.
The concept is simple: customers only pay for what they used.
If they were covered for the full month, we use the full month price.
If they were covered for a fraction of the month, they pay for that fraction.
We apply pro-rata on a 30 day basis: (monthly_price) * (num_days_covered) / 30
Proration only triggers when coverage is partial
Proration only applies when num_days_covered differs from the total number of days in the month.
A full month of coverage always costs the full monthly price, regardless of how many calendar days it contains.
This means 28 days in February costs exactly the same as 31 days in January: both are full months.
| Period | Days covered | Monthly price | Fee |
|---|---|---|---|
| January 2026 | 11 | 10β¬ | 11 Γ 10 / 30 = 3.67β¬ |
| February 2026 | 28 | 10β¬ | 10β¬ |
| March 2026 | 30 | 15β¬ | 15β¬ |
| April 2026 | 14 | 15β¬ | 14 Γ 15 / 30 = 7β¬ |
| April 2026 | 16 | 30β¬ | 16 Γ 30 / 30 = 16β¬ |
| May 2026 | 30 | 30β¬ | 30β¬ |
| June 2026 | 30 | 35β¬ | 35β¬ |
We have two fees in April! It's a feature, not a bug
Whenever there is an event splitting a billing period, we end up with several fees. Historically, we have applied a few tricks to make sure this never happens: - mid-month amendments are forbidden - birthdays are artificially moved
But the model allows for it and in the future we may get rid of these tricks.
Understanding regularisations¶
Inputs to the computation can be updated retroactively. It's possible for the price grid used in February 2026 to be updated after the premiums for February.
That is why every entry in the fee table is versioned. When an entry is proven to be outdated:
- we create a second entry to cancel the first one - the original entry and the canceling entry sum up to a total of 0
- and finally we create a third entry representing the new amount
Example¶
John was enrolled in January 2026. His initial fee was computed at 10β¬/month (the price in the original contract). In March 2026, the contract is amended retroactively to change the January price to 15β¬/month. The system regularises John's January fee:
| id | version | period | amount | cancelled_entry_id | cancelled_by_entry_id |
|---|---|---|---|---|---|
A |
1 | January 26 | +10β¬ | β | B |
B |
2 | January 26 | -10β¬ | A |
β |
C |
3 | January 26 | +15β¬ | β | β |
- Entry
Ais the original fee. Based on the data we had at the time, we thought the price for January would be 10β¬, but turns out we were wrong. It is now cancelled:cancelled_by_entry_id = B. - Entry
BoffsetsA. Its amount is the exact inverse ofA.A + B = 0. - Entry
Cis the corrected fee at the new price. - The net effect for January 2026 is
A + B + C = 0 - 10 + 15 = +15β¬. β
Understanding the format of entries¶
Introducing components¶
Payroll, invoicing and finance reporting all require reading the history of fees. To properly do their job, they need more information than the total owed to Alan. In certain scenarios, we need to exclude the amount corresponding to taxes. In others, we need to focus exclusively on the part of the employee. Etc...
To simplify for all consumers, we break up each entry as a list of components. Each component represents a part of the total amount, with the proper labels to help us understand what this specific part corresponds to.
| Property | Description |
|---|---|
amount |
Amount |
service_type |
What service was provided: health coverage base, option, etc... |
debtor |
Who is losing money: company, primary (not to be confused with who receives invoice) |
collection_method |
How we expect to collect: direct, payroll (not to be confused with payment method) |
contribution_type |
What the money is for: pure cost, taxes, membership fee, etc... |
invoice_id |
Is the component invoiced ? |
Summing all components yields the total amount owed to Alan!
PremiumEntry provides methods to extract information out of these components.
Debtors don't always get invoiced !
The debtor property indicates who is ultimately losing money, but it doesn't mean they will receive an invoice from Alan. For example, the part of the employee can be invoiced to the employer and retained from their payslip: the employee never receives an invite.
Debtor = who is ultimately poorer after the dust settles.
Billed entity = combination of (debtor, collection_method).
The underlying logic is coded in PremiumEntry.components_billed_to_debtor()
| Debtor | Collection Method | Billed entity |
|---|---|---|
primary |
direct_billing |
primary |
primary |
payroll |
company |
primary |
flexben_fund |
company |
company |
any | company |
Beyond the various properties differentiating the components from each other, we also have a bunch of shared properties: we can expect all components of the same entry share the same values.
| Property | Description |
|---|---|
premium_entry_id |
All components with the same entry ID belong to the same entry |
enrollment_id |
Identity of the beneficiary |
beneficiary_type |
Identity of the beneficiary |
period_start |
Start of the billing period. Example: Jan 1st 2026 |
period_end |
End of the billing period. Example: Jan 31st 2026 |
num_days |
How many days were actively covered during the period |
currency |
Currency used for all amounts |
version |
Version number for the regularisation system |
cancelled_entry_id |
Relationship to version N-1 |
cancelled_by_entry_id |
Relationship to version N+1 |
Invoice ID is not shared !
A single entry may be billed to several entities.
For example, there could be a 50% company participation, with the employee part billed directly.
In that case we would have components with debtor == company and components with (debtor, collection) == (primary, direct).
These two groups of components would go to two difference invoices: one for the company, one for the employee.
Persistence¶
When we persist the fees, what we store are actually the components. This gives us a denormalised view of each entry.
On the denormalised view
We could have chosen to persist a PremiumEntryModel with a relationship to a list of components, but we decided to skip the aggregate and store the list of components directly.
The shared property of the entry are duplicated on each component.
There is no strong motivation behind this choice, denormalised felt natural and did not present any obvious drawbacks.
Remember, while the storage is denormalised, the dataclass we use in the domain and business layer (PremiumEntry) is normalised.
Immutability¶
Once created, components are essentially immutable. Most fields are never updated after insertion.
The only exceptions are:
invoice_idβ set when the component is included in an invoicecancelled_entry_id/cancelled_by_entry_idβ set when a regularisation links two entries together
All other fields (amounts, debtor, dates, contribution type, etc.) are frozen at creation time. To correct a mistake, a new version is created via the regularisation mechanism β never by updating existing rows.
A final example¶
John is enrolled on base coverage in January 2026. The total fee is 100β¬. The contract specifies a 50/50 participation: company pays 50β¬, employee pays 50β¬. The employee is billed directly. After 1 month, turns out the price grid is retroactively updated to go from 100β¬ per month to 110β¬.
Here is breakdown of what would be persisted.
- 6 components for version 1
- After 1 regularisations, we end up with 18 components total (6 per version).
- Shared context for all entries:
enrollment_id = ENR-1,beneficiary_type = primary,service_type = base,period_start = 2026-01-01,period_end = 2026-01-31,num_days = 31,currency = EUR.
Version 1 (original fee, later cancelled by version 2):
| id | premium_entry_id | version | cancelled_entry_id | cancelled_by_entry_id | enrollment_id | beneficiary_type | service_type | period_start | period_end | num_days | debtor | collection_method | contribution_type | amount | currency | invoice_id |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | E1 | 1 | β | E2 | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | company | β | membership_fee | +500 | EUR | INV-C1 |
| 2 | E1 | 1 | β | E2 | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | company | β | cost | +3000 | EUR | INV-C1 |
| 3 | E1 | 1 | β | E2 | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | company | β | taxes | +1500 | EUR | INV-C1 |
| 4 | E1 | 1 | β | E2 | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | primary | direct_billing | membership_fee | +500 | EUR | INV-P1 |
| 5 | E1 | 1 | β | E2 | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | primary | direct_billing | cost | +3000 | EUR | INV-P1 |
| 6 | E1 | 1 | β | E2 | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | primary | direct_billing | taxes | +1500 | EUR | INV-P1 |
sum(amount) = 10000 β 100.00β¬ β
Version 2 (cancels version 1, amounts are the exact inverse):
| id | premium_entry_id | version | cancelled_entry_id | cancelled_by_entry_id | enrollment_id | beneficiary_type | service_type | period_start | period_end | num_days | debtor | collection_method | contribution_type | amount | currency | invoice_id |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 7 | E2 | 2 | E1 | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | -31 | company | β | membership_fee | -500 | EUR | INV-C2 |
| 8 | E2 | 2 | E1 | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | -31 | company | β | cost | -3000 | EUR | INV-C2 |
| 9 | E2 | 2 | E1 | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | -31 | company | β | taxes | -1500 | EUR | INV-C2 |
| 10 | E2 | 2 | E1 | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | -31 | primary | direct_billing | membership_fee | -500 | EUR | INV-P2 |
| 11 | E2 | 2 | E1 | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | -31 | primary | direct_billing | cost | -3000 | EUR | INV-P2 |
| 12 | E2 | 2 | E1 | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | -31 | primary | direct_billing | taxes | -1500 | EUR | INV-P2 |
sum(amount) = -10000 β version 1 + version 2 = 0 β
Version 3 (corrected fee, price grid amended to 110β¬):
| id | premium_entry_id | version | cancelled_entry_id | cancelled_by_entry_id | enrollment_id | beneficiary_type | service_type | period_start | period_end | num_days | debtor | collection_method | contribution_type | amount | currency | invoice_id |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13 | E3 | 3 | β | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | company | β | membership_fee | +550 | EUR | INV-C2 |
| 14 | E3 | 3 | β | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | company | β | cost | +3300 | EUR | INV-C2 |
| 15 | E3 | 3 | β | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | company | β | taxes | +1650 | EUR | INV-C2 |
| 16 | E3 | 3 | β | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | primary | direct_billing | membership_fee | +550 | EUR | INV-P2 |
| 17 | E3 | 3 | β | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | primary | direct_billing | cost | +3300 | EUR | INV-P2 |
| 18 | E3 | 3 | β | β | ENR-1 | primary | base | 2026-01-01 | 2026-01-31 | 31 | primary | direct_billing | taxes | +1650 | EUR | INV-P2 |
sum(amount) = 11000 β 110.00β¬ β
Net effect across all 18 components: 10000 - 10000 + 11000 = 11000 β 110.00β¬ β
Understanding the scope of each computation¶
We never compute fees for individual enrollments, despite the components themselves operating with a resolution on enrolment level. The smallest unit of computation is one policy.
Family pricing schemes can only be applied properly when considering the household as a whole
Imagine an offer such as "3rd child is free". If we were to compute enrollment per enrollment, we wouldn't know how many children are in the household.
The simplest workaround is to always compute fees for the entire household at once.