openmeterio / openmeter

Metering and Billing for AI, API and DevOps. Collect and aggregate millions of usage events in real-time and enable usage-based billing.
https://openmeter.io
Apache License 2.0
1.14k stars 68 forks source link

feat: invoice line details calculations #1834

Closed turip closed 2 days ago

turip commented 1 week ago

Overview

Each (valid) line can have one or more detailed lines (children). These lines represent the actual sub-charges that are caused by the parent line.

Example:

If a line has:

  • Usage of 200 units
  • Tiered pricing:
  • Tier1: 1 - 50 units cost flat $300
  • Tier2: 51 - 100 units cost flat $400
  • Tier3: 100 - 150 units cost flat $400 + $1/unit
  • Tier4: more than 150 units cost $15/unit

This would yield the following lines:

Apps can choose to syncronize the original line (if the upstream system understands our pricing model) or can use the sublines to syncronize individual lines without having to understand billing details.

Detailed Lines vs Splitting

When we are dealing with a split line, the calculation of the quantity is by taking the meter's quantity for the whole line period ([parent.period.start, splitline.period.end]) and the amount before the period (parent.period.start, splitline.period.start).

When substracting the two we get the delta for the period (this gets the delta for all supported meter types except of Min and Avg).

We execute the pricing logic (e.g. tiered pricing) for the line qty, while considering the before usage, as it reflects the already billed for items.

Corner cases:

Detailed line persisting

In order for the calculation logic, to not to have to deal with the contents of the database, it is (mostly) the adapter layer's responsibility to understand what have changed and persist only that data to the database.

In practice the high level rules are the following (see adapter/invoicelinediff_test.go for examples):

For idempotent entity sources (detailed lines and discounts for now), we have also added a field called ChildUniqueReferenceID which can be used to detect entities serving the same purpose.

ChildUniqueReferenceID example

Let's say we have an usage-based line whose detailed lines are persisted to the database, but then we would want to change the quantity of the line.

First we load the existing detailed lines from the database, and save the database versions of the entities in memory.

We execute the calculation for the new quantity that yields new detailed lines without database IDs.

The entity's ChildrenWithIDReuse call can be used to facilitate the line reuse by assigning the known IDs to the yielded lines where the ChildUniqueReferenceID is set.

Then the adapter layer will use those IDs to make decisions if they want to persist or recreate the records.

We could do the same logic in the adapter layer, but this approach makes it more flexible on the calculation layer if we want to generate new lines or not. If this becomes a burden we can do the same matching logic as part of the upsert logic in adapter.