export

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package export reads one run back out of the engine database and hands it to a BillingExporter, which is the seam every consumer of billing artifacts sits behind. Load is the read: one run row, its statements, its rated records, the kickbacks it settles for its partners and, for a correction, its deltas and the kickback differences to the run it corrects, all out of one snapshot. The JSON and the CSV file writers are the first implementations of the interface, and an ERP adapter is another one rather than a change to the loader. LoadKickbacks is that read narrowed to the partner settlement, for the report that renders nothing else, and LoadIndex to the statements and the settlement, for a reader that shows the index and the settlement of a run.

The package writes nothing to the database, and only a completed or a finalized run is loaded at all: the superseded, failed and running rows a period accumulates stay in the database for audit and are excluded from every export (roadmap WP 3.8). A finalized run's records are immutable and a completed one's are not, because a run of the same period supersedes it, so the status Load read under its snapshot is read again by the export subcommand before anything is written.

An export is a function of the run it reads. Every ordering is fixed by the queries, every timestamp is rendered in UTC, no artifact records when it was written, and each file is written to a temporary name and renamed into place, so exporting the same finalized run twice yields byte-identical files and a second export replaces the first one file by file.

The normative specification is roadmap/03-phase-3-metering-rating.md, WP 3.10, and, for the partner settlement the writers put beside the statements as kickbacks.json or kickbacks.csv, roadmap/05-phase-5-commercial-pricing.md, WP 5.4. The rollup an export writes on request, one document per meta-project or partner summing the statements of its members as rollup-{key}.json or rollup.csv, with the membership read from the registry at export time, is WP 5.5 of that phase.

Index

Constants

View Source
const (
	KickbacksJSONFileName = "kickbacks.json"
)

The two files the settlement is written to: the document by the JSON writer and the table by the CSV writer.

View Source
const RunFileName = "run.json"

RunFileName is the index every JSON export carries beside its documents.

Variables

View Source
var ErrNoRunForPeriod = errors.New("the billing period has no run to report")

ErrNoRunForPeriod is what PeriodRun returns for a billing period no completed or finalized regular run bills.

View Source
var ErrRunNotExportable = errors.New("the run is not exportable")

ErrRunNotExportable is what Load returns for a run whose status is not completed or finalized. Nothing is read past the run row for such a call.

Functions

func DocumentFileName

func DocumentFileName(kind, key string) string

DocumentFileName is the file one statement is written to: escapedName under the prefix the run's kind carries, statement- or credit-note-. The key is escaped twice and falls back to its digest past the length a file name holds, for the reasons escapedName gives.

The roadmap writes this file as statement-{project}.json. External project ids are unique per cloud only, which is why a statement is stored under a key of both halves, and why the file is named after that key rather than after the project alone (author's decision of 2026-08-24).

func Exportable

func Exportable(status string) bool

Exportable reports whether a run of this status is exported: a completed or a finalized one. Load refuses every other status, and a reader that shows a statement's file asks the same question before it renders one, so the two cannot disagree about which runs an ERP receives.

func KickbacksCSV

func KickbacksCSV(run Run) ([]byte, error)

KickbacksCSV renders kickbacks.csv: the header and one row per kickback, in the order the settlement is read in. Every row carries the run, its kind and its period, the way a row of rated.csv does, so a row says which run and which month it belongs to on its own.

A run that settles nothing renders the header alone: an empty table says the run owes no partner, and a missing file says nothing at all.

func KickbacksJSON

func KickbacksJSON(run Run) ([]byte, error)

KickbacksJSON renders the partner-facing settlement document of a run: per beneficiary and currency what the partner is owed, over how many projects, and the kickbacks that total was summed from.

func PeriodRun

func PeriodRun(ctx context.Context, pool *pgxpool.Pool, periodFrom time.Time) (uuid.UUID, error)

PeriodRun is the run a period's kickbacks are reported from when the caller names the month alone: the regular run that closed it, or the completed regular run of a month that is still open.

A finalized correction of the month is never what the month alone resolves to. What a partner is settled for a month is what the regular run of that month settled, and a correction carries the differences on top of that settlement, which are reported by naming the correction with --run.

The period and the run are read under one REPEATABLE READ snapshot, because closing a month moves its regular run out of the status the second read filters on: finalize commits FinalizeRun and FinalizeBillingPeriod together, so a month closed between two separate reads would be read as still open and then leave no completed regular run to find. The month that was just closed would be refused as one that was never billed, and the operator would be sent to start a regular run over a period the engine refuses one for. The transaction is read-only and ended by the rollback below: resolving a run writes nothing.

func RenderStatement

func RenderStatement(runID uuid.UUID, kind string, statement statements.Statement) ([]byte, error)

RenderStatement re-renders one stored document the way the JSON export writes it. The document is decoded into the type the run's kind renders, a statement or a credit note, and marshalled again, which is what turns the key order JSONB stores an object in back into the field order the concept prints it in. Unknown fields are refused rather than dropped: a document the engine's own types do not hold every field of is one this version cannot render without losing part of it.

The bytes are the file DocumentFileName names, and JSONFiles writes them from here, so a reader that shows a statement's file shows what an export of the run writes rather than a copy of it. Of the statement only the key and the document are read, and the run id names the run in an error.

func RollupCSV

func RollupCSV(run Run) ([]byte, error)

RollupCSV renders rollup.csv: the header and one row per member of every group, in the order the rollup holds them. Every row carries the run, its kind and its period beside the target it is summed under, the way a row of kickbacks.csv carries them, so a row says which run, which month and which meta-project it belongs to on its own.

An export without a rollup and a rollup with no groups both render the header alone: an empty table says the run rolled nothing up, and a missing file says nothing at all.

func RollupFileName

func RollupFileName(key string) string

RollupFileName is the file one group is written to: the rollup- prefix, the escaped key of the target and .json. The key is escaped twice and falls back to its digest past the length a file name holds, for the reasons escapedName gives.

func RunJSON

func RunJSON(run Run) ([]byte, error)

RunJSON renders the run.json JSONFiles writes for the run: the index of the run, one entry per document beside the file the document is written to, and the rollup where the run carries one. The documents are rendered on the way, because a statement the export refuses refuses the index with it, so a reader that shows a run's index shows one only for a run an export writes.

Types

type BillingExporter

type BillingExporter interface {
	Export(ctx context.Context, run Run) error
}

BillingExporter delivers one loaded run to whatever consumes billing artifacts. The file writers below are the first implementations; an ERP adapter (an SFTP drop, a REST push) implements the same interface and is chosen by the caller, so the engine does not change when one is added.

type CSVFiles

type CSVFiles struct {
	// Dir is where the files are written. It is created, with every parent it
	// needs, when the export has rendered everything.
	Dir string
}

CSVFiles writes a run as CSV files into a directory: rated.csv, one row per rated record, kickbacks.csv, one row per kickback the run settles for a partner, and, for a correction run, deltas.csv, one row per delta. A run that carries a rollup writes rollup.csv on top, one row per member of every group. It is the BillingExporter an ERP that imports tables rather than documents is fed from.

func (CSVFiles) Export

func (c CSVFiles) Export(_ context.Context, run Run) error

Export writes the run's tables into Dir. Every file is rendered before the directory is touched, the way the JSON writer renders before it writes, and a write that fails takes the table before it with it: a correction that left rated.csv in place without deltas.csv beside it is one an ERP imports the debits of while the credits it owes back are nowhere.

A run with no rated records writes rated.csv holding its header alone, a correction with no deltas writes deltas.csv holding its header alone, and a run that owes no partner writes kickbacks.csv holding its header alone: an empty table says the run produced no rows, and a missing file says nothing at all. A run that carries a rollup writes rollup.csv after kickbacks.csv, and a rollup with no member writes that header alone under the same rule.

The context is not read, for the reason JSONFiles.Export gives.

type Delta

type Delta struct {
	corrections.Delta
	Currency string
}

Delta is one correction_deltas row: the difference between what the corrected run billed and what the correction rated, for one key.

type JSONFiles

type JSONFiles struct {
	// Dir is where the files are written. It is created, with every parent it
	// needs, when the export has rendered everything.
	Dir string
}

JSONFiles writes a run as JSON files into a directory: one document per statement, run.json, which names the run, the pricing version it rated with, its stats, and one entry per document beside the file it was written to, and kickbacks.json, what the run settles for its partners. The settlement is written for every run and carries an empty beneficiary list where the run owes nobody. A run that carries a rollup writes one document per group beside the statements, which run.json names under rollup. It is the file implementation of BillingExporter.

func (JSONFiles) Export

func (j JSONFiles) Export(_ context.Context, run Run) error

Export writes the run's documents, its settlement and its index into Dir. Everything is rendered before the directory is touched, so a statement whose key or whose stored document is refused leaves no directory and no file behind, and a write that fails takes the documents before it with it: an export that reports an error wrote nothing, rather than every document up to the bad one. The group documents of a run that carries a rollup are written between the statements and the settlement.

run.json is written last, after every document it names and the settlement beside them are on stable storage, so a reader that picks the index up finds every file it points at and kickbacks.json next to them, on a node that came back from a power loss as well as on one that did not.

The context is not read. What is left is writing a few files, which is not cancelable, and stopping halfway through would leave the directory holding part of an export.

type Kickback

type Kickback struct {
	Beneficiary string // the partner's external id, adjustment_records.beneficiary
	Currency    string
	// StatementKey is adjustment_records.project_id as stored, the
	// statements.Key rendering; Cloud and ProjectID are its two halves.
	StatementKey     string
	Cloud, ProjectID string
	RelationID       uuid.UUID
	Scope            string
	Rate             decimal.Decimal
	Base, Amount     decimal.Decimal
}

Kickback is one kickback a run settles for a partner. For a regular run it is one adjustment_records row of type kickback. For a correction it is one non-zero difference between the correction's kickback records and those of the run it corrects, under the key the credit note diffs by: Base and Amount are then new minus old, negative where usage was corrected down.

func DiffKickbacks

func DiffKickbacks(old, current []Kickback) []Kickback

DiffKickbacks is what changed between the kickbacks of a correction and those of the run it corrects: one Kickback per key they disagree on.

The key is the statement, the relation, the scope and the rate, which is what corrections.DiffAdjustments diffs a credit note by. Base and Amount are summed per key on each side first, so two elements of one relation under one key are one kickback there, and the result carries current minus old. A key one side does not hold reads as zero on that side: a kickback the correction no longer settles is taken back whole, and one it settles for the first time is added whole.

A key whose amount difference is zero is left out, whatever its base difference did, because DiffAdjustments drops it too and the report follows the credit note. Beneficiary, Currency, Cloud, ProjectID and Rate come from the current side where it holds the key and from the old side otherwise, while the statement, the relation and the scope are the key's. The result is sorted the way sortKickbacks sorts a run's own kickbacks, and two sides that settle the same, two empty ones included, yield nil rather than an empty slice.

type RatedRecord

type RatedRecord struct {
	Resource         source.Resource
	ProjectID, State string
	FromTS, ToTS     time.Time
	Dimension        string
	// Quantity is the dimension's quantity as it was stored in the usage
	// object, at the four places every quantity carries.
	Quantity decimal.Decimal
	// Amount is what the dimension was billed at, at two places.
	Amount   decimal.Decimal
	Currency string
}

RatedRecord is one rated_records row with the usage record it rates: what was billed, for whom, over which interval, and the quantity the amount was computed from.

type Rollup

type Rollup struct {
	// RelationType is project.RelationMemberOf or project.RelationManagedBy.
	RelationType string
	// Groups is never nil and is ordered by Key. A run none of whose statements
	// is related to a virtual project rolls up to no group at all.
	Groups []RollupGroup
}

Rollup is what an export sums under the virtual projects of one relation type: the meta-projects a run's projects are members of, or the partners that manage them.

func BuildRollup

func BuildRollup(run Run, relationType string, projects []source.Project, relations []source.Relation) (Rollup, error)

BuildRollup sums a run's statements under the virtual projects the relations reach. One group is one target: the meta-project or the partner a relation points at, the projects related to it that the run billed, and the sum of what those statements carry. Attribution and billing stay per project, so a group's total is the sum of the member totals it lists rather than a number of its own.

The relations are walked one hop each. A member of a meta-project that is itself a member of a second one is counted under the first alone, because a rollup reports what is directly related to a target rather than what a walk reaches from it. A source is counted once per target however many of the given relations reach that target from it, so a membership closed and opened again inside the period is one member rather than two. A source related to two targets is a member of both groups and both sum it: the groups of a rollup are not disjoint, and their totals add up to more than the run billed.

A source the run billed no statement for adds nothing, and a target none of whose members the run billed is no group. Such a relation is skipped before its target is judged, so one registry row of a cloud this run never metered refuses no month it has no part in. A target a billed source reaches whose platform is not meta or partner is refused: such a project owns resources and carries a statement of its own, which a rollup under it would leave out, and the document would then report a total that is neither the group's nor the project's.

func LoadRollup

func LoadRollup(ctx context.Context, reporting *source.DB, run Run, relationType string) (Rollup, error)

LoadRollup reads the membership out of the reporting database and sums the run under it. The relations are the ones of the given type whose validity overlaps the run's period, which is the same overlap query the run resolved its adjustments from.

The registry is read when the export runs rather than taken off what the run recorded (author's decision of 2026-08-29, named here per guardrail 10 of roadmap/00-conventions.md). A rollup is therefore a function of the run and of the registry as it stands at that moment, rather than of the run alone: two exports of one finalized run differ where a relation was created or closed retroactively between them.

type RollupGroup

type RollupGroup struct {
	// Key is statements.Key(Cloud, ProjectID) of the target, which is what its
	// document is named after.
	Key string
	// Cloud and ProjectID are the target as the registry holds it. A virtual
	// project carries its platform as its cloud.
	Cloud, ProjectID string
	// Platform is the target's registry platform, meta or partner.
	Platform string
	// Members is ordered by StatementKey and holds at least one entry: a target
	// none of whose members the run billed is no group.
	Members []RollupMember
	// Total is the sum of the member totals, and Currency is the currency every
	// one of them was billed in.
	Total    decimal.Decimal
	Currency string
}

RollupGroup is one meta-project or partner and what the run billed under it.

type RollupMember

type RollupMember struct {
	// StatementKey is the key the run stored the statement under; Cloud and
	// ProjectID are its two halves as the registry holds them.
	StatementKey     string
	Cloud, ProjectID string
	Total            decimal.Decimal
	Currency         string
}

RollupMember is one project of a group, as the run's statement billed it.

type Run

type Run struct {
	ID uuid.UUID
	// Kind is runs.KindRegular or runs.KindCorrection.
	Kind string
	// CorrectsRunID is the run a correction corrects, and uuid.Nil for a regular
	// run.
	CorrectsRunID uuid.UUID
	PeriodFrom    time.Time
	PeriodTo      time.Time
	// Status is "completed" or "finalized": Load refuses every other one.
	Status string
	// PricingVersion is the version the run rated with, and the empty string
	// where the row carries NULL.
	PricingVersion string
	// Clouds is what the run was restricted to, empty for a run over every
	// cloud. It is never nil, so an exporter renders a list rather than a null.
	Clouds    []string
	StartedAt time.Time
	// CompletedAt is the zero time where the row carries NULL, which is what a
	// run that never got to the end of its pass reads as.
	CompletedAt time.Time
	// Stats is the run's stats object as it is stored.
	Stats json.RawMessage
	// Statements holds one entry per project the run billed, ordered by Key.
	Statements []statements.Statement
	// Rated holds one entry per rated record, in the order ListRatedRecords
	// returns them: by resource and then by the start of the usage record and
	// its dimension.
	Rated []RatedRecord
	// Deltas holds one entry per correction delta, in the order corrections.Diff
	// sorted them. It is empty for a regular run, which has none.
	Deltas []Delta
	// Kickbacks holds what the run settles for its partners, in the order
	// sortKickbacks defines: beneficiary, currency, statement key, relation,
	// scope, rate, amount, base. A correction's are the differences to the run
	// it corrects, and a run that owes nobody carries none.
	Kickbacks []Kickback
	// Rollup is the meta-project or partner rollup an export writes beside the
	// statements, and nil for an export without one. Load and LoadKickbacks leave
	// it nil: the membership comes out of the reporting database rather than out
	// of the engine's, and the export subcommand sets it from LoadRollup where
	// --rollup names a relation type.
	Rollup *Rollup
}

Run is one run as an exporter receives it: its row, its statements, its rated records, the kickbacks it settles, and, for a correction, its deltas. It is everything the file writers below need, so an exporter runs with no database handle of its own.

func Load

func Load(ctx context.Context, pool *pgxpool.Pool, runID uuid.UUID) (Run, error)

Load reads one run and everything an exporter renders from it. Four queries run for a regular run (the run row, its statements, its rated records and its adjustment records) and six for a correction (its deltas and the adjustment records of the run it corrects on top), all in one REPEATABLE READ transaction, so what an exporter receives is what belonged to the run at one instant rather than at four or six. A correction's kickbacks are diffed against the corrected run's inside that same snapshot, so the two sides of a difference come from one state of the database. The transaction is read-only as defense in depth, and it is ended by the rollback below: an export writes nothing there is anything to commit.

A run id no row carries is refused with runs.ErrRunNotFound, and a run that is neither completed nor finalized with ErrRunNotExportable. A stored amount that is not a number and a usage object that does not decode are refused by naming the row that carries them: an export short of a value, or holding a zero where a number was meant, is one nobody can tell from a correct one.

func LoadIndex

func LoadIndex(ctx context.Context, pool *pgxpool.Pool, runID uuid.UUID) (Run, error)

LoadIndex reads one run with what RunJSON and KickbacksJSON render from it: the run row, its statements and what it settles for its partners, under the snapshot and with the refusals Load applies, and without the rated records and a correction's deltas: three queries for a regular run and four for a correction. A reader that shows the index and the settlement of a run renders neither of the two, and a month of rated records is tens of thousands of rows to read and decode.

func LoadKickbacks

func LoadKickbacks(ctx context.Context, pool *pgxpool.Pool, runID uuid.UUID) (Run, error)

LoadKickbacks reads one run and what it settles for its partners, under the snapshot and with the refusals Load applies, and without the statements, the rated records and a correction's deltas only a full export renders: two queries for a regular run and three for a correction. The settlement report renders none of the three, and a month of rated records is tens of thousands of rows, each with a usage object to decode, so reading them would hold the snapshot and its pooled connection for a decode nothing is rendered from.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL