Documentation
¶
Overview ¶
Package statements renders what a billing period is invoiced from: one statement document per top-level project, in the format the concept fixes (docs/explanation/worked-examples.md), holding a line item per resource, a period per usage draft, and the costs of every project attributed to that one beside its own. Build is a pure function. It reads nothing and writes nothing, so a period is rendered from the metering, rating, and attribution results a caller already holds.
Every number in a document comes from those results, which is why the renderer takes no pricing model. An amount was rounded where it was rated, and every total here is a sum of already-rounded amounts (roadmap/ 00-conventions.md section 6), so a total equals the sum of the line items printed under it. The one value derived here is the hours a period shows, its minutes over sixty at two places, and that value is display only: nothing is computed from it.
Attribution is exclusive, and a draft carries the project that owned the resource while the draft ran. A resource transferred mid-period therefore appears on two statements, with the periods each project owned it for, rather than being billed whole to whoever holds it at the end of the month.
A draft whose project id has no registry row is not an error. It is billed standalone under that raw id and named in BuildResult.Unregistered, which the run reports through runs.stats: usage somebody consumed is not dropped because nobody registered the project it ran in.
A caller that passes an adjuster has the pricing adjustments of the statement's project applied here: the document then shows the base cost the period was rated at, one line per adjustment, the net cost and the kickbacks a partner is owed. Its total is the net cost, which is what the customer pays. A statement no adjustment reaches holds none of those members and renders the same bytes as one built without an adjuster.
The normative specification is roadmap/03-phase-3-metering-rating.md, WP 3.7, and roadmap/05-phase-5-commercial-pricing.md, WP 5.3, for the adjustment members.
Index ¶
- func Key(cloud, projectID string) string
- func ParseKey(key string) (cloud, projectID string, err error)
- func Persist(ctx context.Context, q *sqlcgen.Queries, runID uuid.UUID, sts []Statement) error
- type BillingPeriod
- type BuildResult
- type Document
- type LineItem
- type Period
- type RelatedCost
- type Statement
- type UnregisteredProject
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Key ¶
Key joins a cloud and a project id into the key a statement is stored under. Both halves are escaped first, so the slash between them is the only separator the key holds and no two pairs render the same one: see Statement.Key. A credit note of a correction run is stored under the same key, so a project's credit note lands where its statement did.
func ParseKey ¶
ParseKey splits the key Key rendered back into the cloud and the project id it was built from: the run.json index of an export names both beside every statement file, which is all a stored key has to be read back for. Exactly one slash is required, because escaping both halves leaves the separator as the only slash a key holds. A key with none or with two was never rendered by Key, and guessing which slash separated it would name a pair nothing was ever stored under. An empty half is a key of its own: a draft that names no project is stored under an empty project half.
func Persist ¶
Persist writes the statements of one run: one insert per statement, in the order they are passed, each under its own key as the project_statements row's project_id. An empty slice writes nothing and runs no query, because a period that bills nobody has nothing to store.
A failure names the statement it happened on and wraps what the database said, and that wrap is the whole of the error handling here. The unique key over (run_id, project_id) reports a project written twice for one run, the record trigger reports a run that is already finalized, and a canceled context comes back as context.Canceled. Persist only ever inserts, and it opens no transaction of its own: a failure leaves the statements written before it behind, and discarding them is decided by the run transaction these inserts run in (roadmap/03-phase-3-metering-rating.md, WP 3.8).
Every insert fires the record trigger, which locks the run row FOR SHARE for the rest of the caller's transaction. A transaction that also updates that run's row, which the lifecycle of WP 3.8 does every time it carries the run's stats along, must take the row first, with
SELECT id FROM runs WHERE id = $1 FOR NO KEY UPDATE
before its first call here. Escalating the share lock afterwards deadlocks two writers of the same run, and PostgreSQL resolves that by aborting one of them with everything it had metered (migrations/engine/0001_init.sql, forbid_finalized_mutation).
Types ¶
type BillingPeriod ¶
BillingPeriod is the half-open interval the document bills, both ends in UTC and RFC 3339.
type BuildResult ¶
type BuildResult struct {
// Statements holds one entry per project that gets a document, sorted by
// Key.
Statements []Statement
// Unregistered holds one entry per project id no registry row matched,
// sorted by cloud and then by project id.
Unregistered []UnregisteredProject
}
BuildResult is what one rendering pass produced: the documents the period is invoiced from, and the project ids they were billed under that the registry does not hold.
func Build ¶
func Build( periodFrom, periodTo time.Time, usage []metering.ResourceUsage, rated rating.Result, projects []source.Project, res attribution.Resolution, adjuster *adjustments.Adjuster, ) (BuildResult, error)
Build renders the period's statements. usage and rated are index-aligned per resource the way rating produced them: record j of a rated resource rates draft j of the same resource in usage. A rated resource that usage does not hold, and one whose record count differs from its draft count, are errors rather than documents rendered from whatever lines up: a statement short by a period is one nobody can tell from a correct one.
Of the resolution only Attributed is read. A project it does not name is billed under itself, which is what a top-level project, an orphaned one, and one no relation touched all are.
The adjuster applies the pricing adjustments of a project's relations to the document it is billed on, and nil renders every document unadjusted. The walk starts at the project the statement is keyed to, so an attributed project's own relations reach no statement: its costs are billed on its root's statement, and only the root's relations adjust them.
Rendering nothing is not an error: a period without usage and without rated resources yields no statements.
type Document ¶
type Document struct {
BillingPeriod BillingPeriod `json:"billing_period"`
ProjectID string `json:"project_id"`
Platform string `json:"platform"`
LineItems []LineItem `json:"line_items"`
RelatedCosts []RelatedCost `json:"related_costs"`
// BaseCost is what the line items and the related costs add up to before
// the adjustments, NetCost what they come to after them, and KickbackTotal
// what a partner is owed beside the net cost rather than as part of it. The
// four members are nil on a statement no adjustment reached, whose bytes
// hold none of them. Total is the net cost where they are there, which is
// what the customer pays.
//
// None of them carries a currency of its own: every amount in the document
// is in the currency Currency names, the way Total already renders.
BaseCost *money.Amount `json:"base_cost,omitempty"`
Adjustments []adjustments.Line `json:"adjustments,omitempty"`
NetCost *money.Amount `json:"net_cost,omitempty"`
KickbackTotal *money.Amount `json:"kickback_total,omitempty"`
Total money.Amount `json:"total"`
Currency string `json:"currency"`
}
Document is one project's statement. The field order is the order the document is marshalled in, which is the order the concept prints it in.
type LineItem ¶
type LineItem struct {
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
Platform string `json:"platform"`
Description string `json:"description"`
Periods []Period `json:"periods"`
Total money.Amount `json:"total"`
}
LineItem is one resource as one project is billed for it: every period of the resource that project owned it for, and what they add up to.
type Period ¶
type Period struct {
State string `json:"state"`
Hours money.Amount `json:"hours"`
Usage map[string]money.Quantity `json:"usage"`
Cost map[string]money.Amount `json:"cost"`
StateModifier money.Quantity `json:"state_modifier"`
}
Period is one usage draft rendered: the state the resource was in, the hours it was in it, the quantities every dimension was rated from, what each of them cost, and what the state was billed at. Cost holds one key per dimension plus costTotal.
type RelatedCost ¶
type RelatedCost struct {
RelationType string `json:"relation_type"`
ProjectID string `json:"project_id"`
Platform string `json:"platform"`
LineItems []LineItem `json:"line_items"`
Total money.Amount `json:"total"`
}
RelatedCost is one attributed project's costs on the statement of the project they are billed under: the type of the edge that claimed it, who it is, and the same line items it would carry standalone.
type Statement ¶
type Statement struct {
// Key is what the statement is stored under: the cloud and the external id
// of its project, each percent-escaped and joined by a slash. External ids
// are unique per cloud only, which is why the cloud is part of the key. It
// is a database key rather than a file name.
//
// Neither half is constrained against the separator, which is why both are
// escaped: os-prod paired with eu/acme would otherwise render what
// os-prod/eu paired with acme does, and the two projects would meet on the
// unique key over (run_id, project_id) with nothing but a duplicate-key
// error to say which pairs produced it. Escaped, every pair renders a key no
// other pair does.
Key string
// Document is the rendered document, marshalled once here. Go marshals a map
// with its keys sorted, so the same input yields the same bytes.
Document []byte
// Total is the document's total, the same decimal the document shows.
Total decimal.Decimal
// Currency is the currency of the model the period was rated with.
Currency string
// Adjustments holds the adjustment lines the document shows, which the run
// stores as the statement's adjustment records. It is nil on a statement no
// adjustment reached.
Adjustments []adjustments.Line
}
Statement is one project's document and the two values a caller stores beside it without opening the document again.
type UnregisteredProject ¶
type UnregisteredProject struct {
Cloud string `json:"cloud"`
ProjectID string `json:"project_id"`
Resources int `json:"resources"`
}
UnregisteredProject is a project id drafts carried that the registry does not hold, and how many of the period's resources were billed under it. Resources counts resources, not their drafts. The run writes the slice into runs.stats.unregistered_projects, which is what the JSON tags name the fields for.