Help us shape the future of Go! Take the 2026 Go Developer Survey

order

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

Documentation

Overview

Package order holds what was asked for: an order against one catalogue release, its lines, and how a line's outcome reaches the lines that depend on it.

It is the second of the three models in ADR-0312. An order names exactly one release and follows the schedule that release precomputed; it never reconsults the catalogue, and a catalogue edited while an approval is pending cannot change what was ordered.

Index

Constants

View Source
const (
	// PlacedMessage starts the fulfilment process for a new order. Its name, like
	// AdvancedMessage's, is the event catalogue's (ADR-0435).
	PlacedMessage = eventcatalog.OrderPlaced
	// AdvancedMessage says a line settled, so the process asks what may start
	// next. A settled line publishes it; nothing polls.
	AdvancedMessage = eventcatalog.OrderAdvanced
)

The two messages that drive fulfilment, correlated on the order id.

They are constants here rather than strings in the model so the two cannot drift apart: a name nobody publishes is a process that waits forever, and it fails silently.

View Source
const FulfilmentProcess = catalog.FulfilmentProcess

FulfilmentProcess is the id of the model that works an order (api/systemprocesses/auftrag-erfuellung.bpmn). The catalogue holds the spelling, because publishing refuses a product bound to it; the model carries its own, and the two are held together by the tests that start it.

Variables

View Source
var ErrNoFurtherEscalation = errors.New("order: the approval can escalate no further")

ErrNoFurtherEscalation means the chain has nowhere left to go: the current holder has no superior, or everyone above them has already had it. It is a distinct error because it is the one an operator's tooling must act on — that approval needs a person — where an invalid assignment is a defect.

Functions

func AttemptID added in v0.9.0

func AttemptID(orderID, position, op string, attempt int) string

AttemptID names one attempt at one operation of one position: the same attempt delivered twice is one trigger, and a deliberate retry is the next attempt. It is also the command id of the order's own acts, so the outcome of a provision names the attempt that provisioned.

func EventTypeOf added in v0.9.0

func EventTypeOf(itemID string, a catalog.Action, outcome string) string

EventTypeOf is what an outcome of action a is published as: what the product declared for it, or <message>.<outcome>; an action with no message — a product of two processes — is named after the item and the action.

func Next

func Next(o Order) []string

Next reports the lines that may be started now, sorted: those still waiting whose preconditions are all satisfied.

A line already running or finished is not offered again, which is what lets an orchestrator ask after every single result rather than tracking rounds itself.

func PlacedVariables

func PlacedVariables(o Order, portalBase, apiBase string) map[string]string

PlacedVariables is what the fulfilment orchestration is started with.

One function rather than a literal at the call site, because there is a second caller: the repair that starts an orchestration again for an order whose own one was lost or could never work (api/fulfilmentrepair.go). Two literals would be two payloads, and the one that drifted would produce an orchestration that runs and quietly does nothing — which is exactly the defect this list was widened for.

portalBaseUrl is what a notification's link is built on: the operator's configured origin or empty. A model that finds it empty says where to go instead of printing a link nobody can follow.

atlasApiBase is where the orchestration reaches Atlas's own API, which is how it does every piece of its work. It is this server addressing itself, not the operator's external origin, and the two are different values for a good reason: one may be unset and the other is known whenever the server is up.

func ResolveLine

func ResolveLine(o Order, ref string) (string, error)

ResolveLine turns what a caller named into the key of exactly one line.

A caller may name a position outright, or name the product — which is what every process built before a product could be ordered twice does, and what stays unambiguous for every order that carries one position of it. Both are answered, and the case that cannot be answered is refused rather than guessed: naming a product an order carries twice would otherwise record an outcome against whichever line the walk reached first, and the two are a black phone and a silver one.

func ReturnBindingOf added in v0.8.0

func ReturnBindingOf(o Order, ref string) catalog.Binding

ReturnBindingOf is ReturnProcessOf with the start event, for a line whose product binds a lifecycle process (ADR-0425): where the revocation starts, not only which process runs it.

func ReturnProcessOf

func ReturnProcessOf(o Order, ref string) string

ReturnProcessOf is the process that revokes a line, for a caller about to start it. It reads it from the order rather than from the catalogue, because what was granted is what has to be revoked — a product whose deprovisioning was changed afterwards must not revoke an older grant by the newer rules.

func Returnable

func Returnable(o Order, ref string) error

Returnable reports whether a line can be given back now, and says why not.

Three things have to hold. It must be held — a skipped line the recipient got elsewhere was never this order's to revoke, and one already returned is gone. It must have a process to revoke it with, because a return with nothing to run is a status change pretending to be an act. And nothing still held may require it.

That last one is the precedence graph read backwards. Provisioning ordered the account before the laptop that needs it; giving back runs the other way, and an account revoked under a laptop that still uses it leaves the laptop working until somebody notices, or not working for a reason nobody connects to this.

Types

type AmendedAnswers

type AmendedAnswers struct {
	// Was is what the answers said before this correction.
	Was map[string]string `json:"was"`
	// By is the principal who corrected them and At is when. Both are required,
	// for the reason every settled transition in this package requires them: a
	// change kept forever that says somebody corrected something, without saying
	// who, is a correction nobody made.
	By string `json:"by"`
	At int64  `json:"at"`
	// Reason is optional and usually short — "cost centre was wrong". It is what a
	// reader months later has instead of a phone call.
	Reason string `json:"reason,omitempty"`
}

AmendedAnswers is one correction to a line's configuration answers, kept beside them rather than instead of them.

It exists for the case an overwrite cannot make honest: a line the recipient already **holds**. The laptop is at the wrong site and correcting the record does not move it, so an overwrite would leave the order saying something that was never true of the delivery. Recording the correction *as* a correction is more true than either version alone — it says what was ordered, what it should have been, who said so and when.

type Approval

type Approval struct {
	// Kind is "none", "fixed", "role", "superior", or the name of a registered
	// approval process.
	Kind string `json:"kind,omitempty"`
	// Ref names the principal for "fixed" or the group for "role", empty for the
	// kinds that resolve their approver from the order itself.
	Ref string `json:"ref,omitempty"`
}

Approval is one line's approval rule, as the release froze it. It mirrors the catalogue's shape rather than importing it, so an order can be read back without the catalogue package — and so that a change to the authoring shape is a deliberate change here rather than a silent one.

type Assignment

type Assignment struct {
	ItemID string `json:"itemId"`
	// Approver is who the approval is with now, as the task names them.
	Approver string `json:"approver"`
	// Original is who it was first assigned to. It survives every escalation,
	// because an approval that travelled must still say where it started — a
	// decision eventually taken by a third deputy reads very differently from one
	// taken by the line manager it was meant for.
	Original string `json:"original"`
	// AssignedAt is when it was first assigned, which is what a deadline measures
	// from.
	AssignedAt  int64        `json:"assignedAt"`
	Escalations []Escalation `json:"escalations,omitempty"`
	// StalledAt is when the chain was found to have nowhere left to go, zero while
	// it has somewhere. A stalled approval is still with whoever last held it —
	// stalling reports a fact, it does not take the task away — but it is now
	// visible, which is the whole point: an approval nobody can escalate and
	// nobody is looking at is how an order waits forever.
	StalledAt int64 `json:"stalledAt,omitempty"`
}

Assignment is one line's approval: who holds it now, who it started with, and every hop between.

func Assign

func Assign(itemID, approver string, at int64) Assignment

Assign puts a line's approval with somebody.

func Escalate

func Escalate(a Assignment, superiorOf func(string) string, at int64) (Assignment, error)

Escalate moves the approval to the current holder's superior, recording the hop. It returns the assignment unchanged when it refuses.

superiorOf answers who a principal reports to, and "" means nobody does — the top of the hierarchy, or somebody the directory has no answer for. That case is an error rather than a silent no-op: an escalation that quietly left the approval where it was would hide that the deadline achieved nothing, which is the only thing anybody wanted to learn from it. Callers distinguish it with ErrNoFurtherEscalation and mark the assignment with Stall.

func Reassign

func Reassign(a Assignment, to, by string, at int64) (Assignment, error)

Reassign puts a stuck approval with somebody a person chose, recording who intervened, and clears the stall so the deadline works normally again from the new holder.

It will not give the approval to the principal making the call: see the check below for why that one is in the model rather than in a role.

Unlike Escalate it may send the approval to somebody who already held it. The loop guard exists to stop a clock cycling an approval between two colleagues; a person sending it back to the original approver knows something the guard does not — that they are back from leave, say. What it will not do is reassign to the current holder, which would record a hop and move nothing.

func Stall

func Stall(a Assignment, at int64) (Assignment, error)

Stall records that an approval has nowhere left to escalate to, which is what makes it visible to somebody who can act. The caller raises whatever its operations surface uses for "a human must look at this"; this only marks it.

func (Assignment) Held added in v0.19.0

func (a Assignment) Held(who string) bool

Held reports whether this principal has already held the approval, spelled the way the record spells them. A caller choosing how to name the next holder asks it, so that the loop guard in Escalate sees a person it has met before.

func (Assignment) Stalled

func (a Assignment) Stalled() bool

Stalled reports whether this approval has been found to have nowhere left to escalate to.

func (Assignment) Valid

func (a Assignment) Valid() error

Valid reports whether this assignment can be acted on.

type Escalation

type Escalation struct {
	// From and To identify people the way a *task* does, which is by the username
	// its assignment names (ADR-0042). That is forced rather than chosen: an
	// assignment is written back onto the task it belongs to, so it has to speak
	// the identifier the task is addressed by, and a principal id written there
	// would address nobody.
	//
	// It is not a widening of what the portal holds about a person
	// (ADR-0314): a username is a login identifier the
	// account already carries into every task's assignee field and into the event
	// log with it. What stays out of here is what that record is about — names,
	// mail addresses, departments — which are resolved from the account when
	// something is rendered or sent, and never copied.
	From string `json:"from"`
	To   string `json:"to"`
	At   int64  `json:"at"`
	// By is the principal who moved it, empty when a deadline did. The absence is
	// the record: it distinguishes a hop a clock made from somebody's decision to
	// intervene, and both are kept.
	By string `json:"by,omitempty"`
}

Escalation is one hop of an approval from one holder to the next.

type FrozenBinding added in v0.8.0

type FrozenBinding struct {
	ItemID  string `json:"itemId"`
	Process string `json:"process"`
	Lines   int    `json:"lines"`
}

FrozenBinding is how many lines of one product still start one process.

func LinesStarting added in v0.8.0

func LinesStarting(orders []Order, processID string) []FrozenBinding

LinesStarting counts, per product, the lines of these orders that can still start processID. A count and not a list, for the reason ADR-0353 gives: what an operator needs is how much still depends on the process, not who.

func OldBindings added in v0.8.0

func OldBindings(orders []Order, it catalog.Item) []FrozenBinding

OldBindings counts, for one product, the lines still bound to processes the product no longer binds — the remainder a conversion leaves behind (ADR-0427).

type Grant

type Grant struct {
	// Principal is who holds it — an id, never a name.
	Principal string
	ItemID    string
	VariantID string
	// OrderID is the order that granted it, so a right can be traced back to the
	// request and the approval behind it for as long as both are kept.
	OrderID string
	// At is when it started, read from the server clock by the caller.
	At int64
	// Until is when it was meant to end, or zero for a right that does not end.
	// Computed from the ceiling the release's product declared, frozen into the
	// line when the order was placed (ADR-0344).
	//
	// A promise about when the access should end, not a statement that it has: the
	// record stays held past it, and a modelled process is what actually takes the
	// access away.
	Until int64
	// ApprovedBy is who approved the line, copied from it (Line.ApprovedBy), so the
	// right keeps its approver after retention has deleted the order.
	ApprovedBy string
	// Outcome is the provision's ending, written in the same batch as the grant
	// (ADR-0429 §3).
	Outcome Outcome
}

Grant is what one settled line tells the inventory. It carries no origin: a grant this package makes always came through an order, and a package that cannot name any other origin cannot mislabel one.

type Line

type Line struct {
	// ItemID is the catalogue item this line orders, as the release named it.
	ItemID string `json:"itemId"`
	// VariantID is the variant the basket resolved, empty where the item has none.
	VariantID string     `json:"variantId,omitempty"`
	Status    LineStatus `json:"status"`
	// ProvisionProcess and DeprovisionProcess are the processes bound to this
	// line's product, copied from the release when the order was placed.
	//
	// They travel with the order for the same reason the schedule does: fulfilment
	// reads one record, and looking the binding up in the catalogue at the moment
	// of provisioning would reintroduce exactly the edit the release was frozen
	// against. The deprovision process travels too, so that revoking what this
	// order granted uses the process that was in force when it was granted.
	ProvisionProcess   string `json:"provisionProcess,omitempty"`
	DeprovisionProcess string `json:"deprovisionProcess,omitempty"`
	// LifecycleProcess and Operations are the one-process form of the same binding
	// (ADR-0425), frozen for the same reason. A line carries one form or the other,
	// as its product did when it was ordered; a line placed before its product was
	// converted keeps the two ids (ADR-0427).
	LifecycleProcess string            `json:"lifecycleProcess,omitempty"`
	Operations       map[string]string `json:"operations,omitempty"`
	// Actions are the product's actions as the release declared them (ADR-0429),
	// frozen for the same reason: what may later be asked of this position is what
	// was offered when it was ordered. A line carries Operations or Actions, as its
	// product did.
	Actions []catalog.Action `json:"actions,omitempty"`
	// LifecycleForm is how the lifecycle process runs for this line, frozen with the
	// binding: a per-position line's later operations are delivered to the instance
	// its provisioning started
	// (ADR-0428).
	LifecycleForm string `json:"lifecycleForm,omitempty"`
	// Rebindings are the moves of this line from the binding it froze to its
	// product's lifecycle process, each with who, when and why (ADR-0427). A list,
	// because a line moved twice has two facts to tell.
	Rebindings []Rebinding `json:"rebindings,omitempty"`
	// MaxDays is the product's ceiling on how long the right this line grants may
	// last, copied from the release for the reason the two processes above are
	// (ADR-0344). A ceiling relaxed in the catalogue
	// after somebody ordered must not lengthen what they were granted under the
	// stricter one — which is the same sentence as the one about the approval rule,
	// and true for the same reason.
	MaxDays int `json:"maxDays,omitempty"`
	// ConfigForm and Config are what somebody filled in when they ordered this
	// product, and the form they filled it in on
	// (ADR-0358).
	//
	// Both or neither: a set of answers with no form is a map of keys nobody can
	// interpret, and a form with no answers on a line that was placed means the
	// question was never asked.
	//
	// The **answers** are what is frozen here, and deliberately not the questions.
	// A release freezes rules — the approval, the ceiling, the bindings — because a
	// rule relaxed next week must not change what somebody was held to this week.
	// A form is not a rule; what has to survive is what was answered, and "cost
	// centre 4711" stays true whatever the form does afterwards. The id says which
	// set of questions it answered.
	// Price is what the release said this product cost when the order was placed
	// (ADR-0361). Frozen here for the reason
	// the approval rule and the ceiling are: an approver saw a figure and decided on
	// it, and a catalogue edit afterwards must not make the record show a different
	// one than the one that was approved.
	//
	// Displayed and never computed — see [catalog.Item.Price].
	Price      string            `json:"price,omitempty"`
	ConfigForm string            `json:"configForm,omitempty"`
	Config     map[string]string `json:"config,omitempty"`
	// Amendments are the corrections made to Config after the recipient already
	// held this line, oldest first
	// (ADR-0359). Empty is the
	// ordinary case, and it is empty too for a line corrected before anything was
	// attempted — there is no delivery for the old answers to have been true of.
	Amendments []AmendedAnswers `json:"amendments,omitempty"`
	// Integral says this line arrived because something else in the order always
	// carries it, rather than because anybody chose it.
	//
	// It is what lets a position be withdrawn on its own without letting somebody
	// take back a part its whole cannot exist without: the basket refuses to
	// deselect one, and a rule enforced when ordering and not afterwards is not a
	// rule. Frozen at placement like everything else on this line, because it is a
	// statement about the release the order was placed against.
	Integral bool `json:"integral,omitempty"`
	// Includes names the parts this line always carries, copied from the release.
	// It is what a per-line withdrawal reads to say *what* carries the part it is
	// refusing to take back, rather than only refusing.
	Includes []string `json:"includes,omitempty"`
	// Approval is the rule this line is approved under, copied from the release
	// like the bindings are. It travels for the same reason: the rule belongs to
	// the catalogue, and reading it when the line is reached would let a product's
	// approval be relaxed after somebody ordered under the stricter one.
	Approval Approval `json:"approval,omitempty"`
	// BlockedBy names the lines whose failure or rejection stopped this one —
	// the *root* causes, not the intermediate blocked lines between.
	//
	// An intermediate line is itself blocked and carries the same causes, so naming
	// it here would make a reader walk the chain to learn what actually happened.
	// "Your VPN is waiting because the laptop was rejected" is the sentence somebody
	// needs; "waiting because the docking station is waiting" is not.
	BlockedBy []string `json:"blockedBy,omitempty"`
	// TerminallyBlocked marks a blocked line that will never run: one of the things
	// it requires was rejected or abandoned, and neither lifts. A decision does not
	// change because an incident was repaired, and an abandoned incident is one
	// nobody is repairing. A line blocked only by live failures is not terminal —
	// somebody can still fix them, and the order waits.
	//
	// It is set by [Propagate] alongside BlockedBy and is meaningless on any other
	// status.
	TerminallyBlocked bool `json:"terminallyBlocked,omitempty"`
	// AbandonedBy and AbandonedAt record who gave up on this line's incident and
	// when. Both are required on an abandoned line and forbidden on any other, so
	// the record can never say a line was given up on without saying by whom.
	//
	// AbandonedBy is a principal id. Never a name — see
	// ADR-0314.
	AbandonedBy string `json:"abandonedBy,omitempty"`
	AbandonedAt int64  `json:"abandonedAt,omitempty"`
	// DecidedBy and DecidedAt record who decided this line's fate and when, and
	// Reason carries their words. All three are required on a rejected line; a
	// cancelled one requires the first two.
	//
	// The two decisions share these fields because they are the same kind of fact —
	// somebody, at a moment, settled this line without it being provisioned — and
	// the status says which kind. A rejection kept forever without an author is a
	// decision nobody made, and one without a reason produces the message that
	// generates a phone call: "your request was declined", and nothing else. A
	// cancellation needs no reason, because the person reading it is the person who
	// made it. DecidedBy is a principal id.
	DecidedBy string `json:"decidedBy,omitempty"`

	// ApprovedBy and ApprovedAt record who approved this line and when, as a
	// principal id and a moment. They are the other half of DecidedBy: a refusal
	// settles a line and keeps its author there, while an approval leaves the line
	// to be provisioned and keeps its author here. Without them the record of an
	// approval offered to a group said only that the line went on, and an access
	// review asks who let it. Empty on a line that needed no approval, and on one
	// approved before this was recorded.
	ApprovedBy string `json:"approvedBy,omitempty"`
	ApprovedAt int64  `json:"approvedAt,omitempty"`

	// ReturnedBy is who asked for this line to be given back, as a principal id.
	//
	// A third actor field beside AbandonedBy and DecidedBy, for the reason those
	// two are apart: they are different decisions. It is recorded when the return
	// is *asked for* rather than when it completes, because that is the only moment
	// a person is present — what finishes a return is a deprovisioning process, and
	// attributing the decision to it would name a robot where a decision was made.
	//
	// It is what the history row carries into the years after this order is deleted
	// (ADR-0346): without it the record can say a right ended
	// and not who ended it, which is half of what an access review asks.
	ReturnedBy string `json:"returnedBy,omitempty"`
	DecidedAt  int64  `json:"decidedAt,omitempty"`
	Reason     string `json:"reason,omitempty"`
	// Instances are the process instances started to work this position, oldest
	// first: its approval, its provisioning, and later its return (ADR-0416).
	//
	// Recorded by the server at the moment it starts one, because that is the only
	// moment the link is certain. Found afterwards it would be a search over every
	// running instance's variables — the walk a large installation does not finish
	// in any time a reader waits — and a guess where two processes carry the same
	// order id. Empty on a line nothing has started for yet, and on every line of an
	// order placed before the server kept the record.
	Instances []LineInstance `json:"instances,omitempty"`
}

Line is one ordered position.

func Abandon

func Abandon(l Line, by string, at int64) (Line, error)

Abandon marks a failed line as one nobody will repair, recording who decided and when. It returns the line unchanged when it refuses.

Only a failure can be abandoned: there is nothing to give up on in a line that never failed, and rewriting a provisioned one would deny work that happened. A rejection is already settled by somebody's decision, and a blocked line is a consequence — give up on its cause instead, and this line follows.

func AmendAnswers

func AmendAnswers(l Line, answers map[string]string, by string, at int64, reason string) (Line, error)

AmendAnswers replaces a line's configuration answers, and returns the line unchanged when it refuses.

func Approve added in v0.8.0

func Approve(l Line, by string, at int64) (Line, error)

Approve records who approved a line and when. Unlike Reject it settles nothing: the status is left as it is, because an approved line goes on to be provisioned and its outcome arrives through the ordinary report.

Only a line still on its way can be approved — approving one already settled would write a decision beside somebody else's — and only once: a second approval would overwrite the first author, which is the one fact this exists to keep.

func Cancel

func Cancel(l Line, by string, at int64, reason string) (Line, error)

Cancel withdraws one line. It returns the line unchanged when it refuses.

by is the principal withdrawing it and is required, for the reason every other settled-without-provisioning transition here requires one: a status kept forever that says somebody decided, without saying who, is a decision nobody made. A reason is optional — a cancellation is explained to the person who made it.

func Propagate

func Propagate(in []Line, requires map[string][]string) []Line

Propagate returns the lines with every line that can no longer be attempted marked blocked, naming what stopped it. It does not modify its argument: a caller that retried against rewritten lines would resolve against its own previous conclusions rather than against the facts.

It is idempotent, so the fulfilment process may call it after every settled line without tracking whether it already has.

func Ready

func Ready(o Order) []Line

Ready is Next with what an orchestrator needs to act: each line that may start now, in the same order, carrying the process that provisions it and the variant that was chosen.

Two functions rather than one because they answer to different readers. Next is the question — which lines are ready — and is what a person or a status screen wants; Ready is the same answer with the detail a machine needs to do something about it.

func Reject

func Reject(l Line, by string, at int64, reason string) (Line, error)

Reject refuses a line's approval, recording who decided, when, and why.

The same shape as Abandon and for the same reason: this is a decision, so it has an author, and the record keeps both forever. The reason is required rather than optional because the orderer is told it — a refusal with no words is a message that produces a phone call instead of an understanding.

Only a line still on its way can be rejected. Refusing one already provisioned would deny work that happened, and refusing one already settled would overwrite somebody else's decision.

func (Line) ActionList added in v0.9.0

func (l Line) ActionList() []catalog.Action

ActionList is the actions this line froze, in either shape its product said them in (ADR-0429).

func (Line) ActionNamed added in v0.9.0

func (l Line) ActionNamed(key string) (catalog.Action, bool)

ActionNamed is the action this line froze under key, in either shape its product said it in.

func (Line) ApprovalProcess

func (l Line) ApprovalProcess() string

ApprovalProcess names the process that decides this line, or "" when the line needs none.

It is resolved when fulfilment asks and deliberately *not* frozen into the order. What a catalogue promised is frozen — the product, its variant, the rule it is approved under — because an edit to the catalogue must not change a pending order. Which model implements that rule is not the catalogue's promise; it is this installation's wiring, and freezing it would mean an operator who redeploys an approval process breaks every order already waiting on one.

A kind that is not one of the three is taken as naming a process directly, which is what Approval.Kind documents: an installation with its own approval model binds a product to it by name.

func (Line) AtlasApproval added in v0.9.0

func (l Line) AtlasApproval() bool

AtlasApproval reports whether one of Atlas's own approval processes decides this line, rather than a model the installation binds by name. Atlas's own are not given the line's answers (ADR-0441).

func (Line) BindingFor added in v0.8.0

func (l Line) BindingFor(op string) catalog.Binding

BindingFor is where an operation of this line starts: its frozen binding, in whichever form the product had when the line was placed (ADR-0425). On a lifecycle process, provision and deprovision are the actions of those effects and any other name is an action's key (ADR-0429).

func (Line) Command added in v0.9.0

func (l Line) Command(id string) (LineInstance, bool)

Command is the instance that took command id for this line, and the action it asked for (its Operation).

func (Line) Key

func (l Line) Key() string

Key is what identifies this line inside its order.

A line was identified by its item id, which held for exactly as long as one product could appear in an order once. The catalogue's own rule is that the same service pulled in twice in different variants is a conflict the orderer resolves (ADR-0312), and resolving it by keeping both is the case an item id cannot express: two lines with the same id collapse in every map the order builds, and an outcome reported for one lands on whichever came first.

Derived rather than stored, and identical to the item id where there is no variant. That is the whole compatibility story: no order is migrated, no record gains a field, and a fulfilment process built against /lines/{itemId} keeps working for every product that is not ordered twice.

The separator is "#" because it cannot appear in an item id — ids are minted or authored as path-safe names — so a key can always be split back into the two things it joins.

func (Line) NeedsApproval

func (l Line) NeedsApproval() bool

NeedsApproval reports whether this line waits on somebody before it is provisioned. The fulfilment process asks it to decide which process to start, so it has to be answerable from the line alone.

func (Line) PerPosition added in v0.8.0

func (l Line) PerPosition() bool

PerPosition reports whether this line's lifecycle runs as one instance per position (ADR-0428).

func (Line) StartsOf added in v0.8.0

func (l Line) StartsOf(op string) int

StartsOf counts the instances already started for an operation of this line. It is the attempt a new start is, which is what makes a deliberate retry a new trigger and a repeated delivery of the same start the same one.

func (Line) StillStarts added in v0.8.0

func (l Line) StillStarts(processID string) bool

StillStarts reports whether this line can still start the process with this id, through either binding form.

func (Line) StrandOf added in v0.8.0

func (l Line) StrandOf() uint64

StrandOf is the instance a per-position line's later operations are delivered to: the newest instance its lifecycle process was started with for provisioning, or zero when none was recorded.

func (Line) Terminal

func (l Line) Terminal() bool

Terminal reports whether this line has finished moving.

Two statuses that have an outcome of their own are still not terminal, and both follow from the same rule — an order stays open while something can still be repaired:

  • Failed is an open incident. Somebody repairs it and the line provisions after all, so an order carrying one is not finished. Only Abandon ends it.
  • Blocked depends on its causes: terminal when one of them was rejected or abandoned, open while they are live failures.

This is a different question from LineStatus.Settled, which asks whether a line reached an outcome of its own — a failure has, and that is why propagation leaves it alone.

func (Line) Valid

func (l Line) Valid() error

Valid holds the same rules where a line arrives as JSON rather than through Abandon or Reject — from a store, an API payload, a restored backup. A serialised line never went through that function, so the invariant is checked again at the boundary it enters through rather than assumed from the one place that upholds it.

type LineInstance

type LineInstance struct {
	// Key is the instance's key, which is what its open tasks are found under.
	Key uint64 `json:"key"`
	// ProcessID is the BPMN process id it is an instance of — which of the
	// position's processes this is, without a second read to learn it.
	ProcessID string `json:"processId,omitempty"`
	// StartedAt is when it was started, in Unix nanoseconds.
	StartedAt int64 `json:"startedAt"`
	// Operation is what the instance does for the position — provision, change,
	// deprovision — where the starter knew it (ADR-0425). A lifecycle process is one
	// process id for all of them, so the id alone no longer says which.
	Operation string `json:"operation,omitempty"`
	// CommandID is the command the instance took for the position (ADR-0429): the
	// caller's command id for an action, the attempt id for the order's own
	// provision and return. It is what an outcome report names, so the order can say
	// which action it ends.
	CommandID string `json:"commandId,omitempty"`
}

LineInstance is one process instance that works an order position.

type LineStatus

type LineStatus string

LineStatus is where one ordered position stands.

The three ways a line can fail to be provisioned are deliberately three values and not one. They differ in who has to do something about it, and a status that merges them either raises incidents nobody can repair or files a decision as a malfunction:

  • Failed is a defect. Something broke; an operator repairs it and it retries.
  • Rejected is a decision. Nothing is broken, somebody said no, and there is nothing to repair.
  • Blocked is a consequence. This line was never attempted, because something it needs is Failed or Rejected. It carries what stopped it.
const (
	// StatusPending is ordered and not yet reached.
	StatusPending LineStatus = "pending"
	// StatusRunning is with its provisioning process now.
	StatusRunning LineStatus = "running"
	// StatusDone is provisioned.
	StatusDone LineStatus = "done"
	// StatusSkipped is not provisioned because the recipient already holds it.
	// It satisfies dependents exactly as Done does — a VPN whose laptop was
	// already there has its precondition met, and treating the skip as an absence
	// would block a line for the reason that it was unnecessary.
	StatusSkipped LineStatus = "skipped"
	// StatusFailed is a defect in provisioning: an incident to repair.
	StatusFailed LineStatus = "failed"
	// StatusRejected is an approval refused. Not an incident, and never reported
	// as one.
	StatusRejected LineStatus = "rejected"
	// StatusAbandoned is a failure nobody will repair: the incident behind it was
	// given up on, by a person or by the deadline that sits on the incident.
	//
	// It exists because "an order stays open while a blockage is repairable" needs
	// a way for a failure to stop being repairable — otherwise it reads as "an
	// order never closes". The deadline is on the incident rather than on the
	// order, because the incident is the thing that is actually stuck; the order
	// follows it here.
	StatusAbandoned LineStatus = "abandoned"
	// StatusCancelled is a line the person who ordered it withdrew before it was
	// provisioned.
	//
	// It is a fifth value and not a reuse of Rejected for the reason the three
	// above are three: they differ in who has to do something about it. A
	// rejection is somebody refusing a request that was made; a cancellation is
	// the request being taken back, and nobody has to act on it at all. Filed as a
	// rejection it would say in a record kept for years that an approver turned
	// down a colleague's laptop, when what happened is that the colleague changed
	// their mind.
	StatusCancelled LineStatus = "cancelled"
	// StatusReturning is a provisioned line whose deprovisioning process is
	// running now: the revocation was asked for and has not come back.
	//
	// It is deliberately *not* settled. Something is in flight against a target
	// system, and an order that reported itself finished while an account was
	// half-deleted would be reporting the thing it is least entitled to guess at.
	StatusReturning LineStatus = "returning"
	// StatusReturnFailed is a revocation that ran and did not succeed: the
	// recipient still has the thing, and somebody has to do something about it.
	//
	// It is not Failed, which says a provisioning never delivered — a reader
	// seeing that would conclude nobody has it, and the precedence guard would let
	// the account underneath be revoked out from under something that is very much
	// still there. And it is not a silent fall back to Done, which would lose the
	// fact that a revocation was attempted and lost: the next person to look sees
	// an ordinary held line and no sign anything went wrong.
	//
	// Asking for the return again is the ordinary repair — fix the target system,
	// try once more — and is what [Returnable] accepts it for.
	StatusReturnFailed LineStatus = "returnFailed"
	// StatusReturned is a line that was provisioned and has been given back.
	//
	// Distinct from Cancelled, which is a line that never was: a record that says
	// "cancelled" where somebody held a laptop for three weeks is a record that
	// lost three weeks. What was granted and then revoked is a different fact from
	// what was never granted, and an audit that cannot tell them apart cannot
	// answer who had access when.
	StatusReturned LineStatus = "returned"
	// StatusBlocked is a line that cannot be attempted because something it
	// requires is Failed or Rejected. See Line.BlockedBy.
	//
	// It is *derived*, never a line's own outcome, and it is recomputed from
	// scratch on every pass — which is what lets a repair undo it. An operator who
	// fixes the incident behind a failed precondition releases the line that was
	// waiting on it, with nobody rewriting a status by hand.
	StatusBlocked LineStatus = "blocked"
)

func (LineStatus) Cancellable

func (s LineStatus) Cancellable() bool

Cancellable reports whether a line can still be withdrawn.

Two things cannot. One with an outcome already is finished — including a failed one, whose incident is a decision of its own: somebody has to give up on it (Abandon), and calling that a cancellation would file a repair nobody finished as a change of mind. And a running line is with a provisioning process now, which is a conversation with a system this server does not control; stopping it halfway is not withdrawal but a half-provisioned account nobody owns.

What is left is exactly what has not happened yet: a line still waiting its turn, and one blocked behind something else.

func (LineStatus) Held

func (s LineStatus) Held() bool

Held reports whether the recipient has this line's product, as far as anybody knows, because of this order.

Three statuses say yes, and the two beyond Done are the point. A revocation that has been *asked for* has not happened: until it confirms, the access is there, and treating a requested return as an absence would let the account underneath be revoked out from under a laptop that is still working. One that ran and failed is even plainer — it is still held, and now somebody knows it.

A skipped line they got elsewhere and this order never granted; a returned one they no longer have; everything before Done was never delivered.

This is what the precedence guard asks: a line may only be given back once nothing that needed it is still held. It is deliberately *not* what decides whether a line may be returned — see Returnable, which asks a narrower question, because a return already under way must not be asked for twice.

func (LineStatus) Satisfied

func (s LineStatus) Satisfied() bool

Satisfied reports whether a line in this status meets a dependent's precondition. Done and Skipped do; everything else, including a line still running, does not yet.

func (LineStatus) Settled

func (s LineStatus) Settled() bool

Settled reports whether this is a line's own final outcome.

Blocked is deliberately absent. It is not an outcome but a consequence of other lines, recomputed on every pass, so whether a blocked line has finished depends on its causes rather than on itself: see Line.Terminal.

type Notice

type Notice struct {
	Kind NoticeKind `json:"kind"`
	// OrderID and Recipient say which order and who is told. Recipient is a
	// principal id — the message is addressed by the sender, not by this package,
	// which never holds an address (ADR-0314).
	OrderID   string `json:"orderId"`
	Recipient string `json:"recipient"`
	// ItemID names the line, empty on a settlement notice, which is about the
	// whole order.
	ItemID string `json:"itemId,omitempty"`
	// Reason carries the approver's words on a rejection.
	Reason string `json:"reason,omitempty"`
	// Outcome, Provisioned and NotProvisioned are the settlement notice's content:
	// the closing message is the one place the orderer is told the whole result.
	Outcome        Status   `json:"outcome,omitempty"`
	Provisioned    []string `json:"provisioned,omitempty"`
	NotProvisioned []string `json:"notProvisioned,omitempty"`
}

Notice is one message owed to one person.

func Notices

func Notices(before, after Order) []Notice

Notices reports what somebody must be told about the change from before to after. Both are the same order at two moments; an unchanged order owes nothing.

Line notices come first in item order, then the settlement, so the same transition always produces the same messages in the same sequence.

type NoticeKind

type NoticeKind string

NoticeKind is what happened.

const (
	// NoticeRejected: one line's approval was refused.
	NoticeRejected NoticeKind = "rejected"
	// NoticeAbandoned: one line's incident was given up on. Without this the
	// orderer cannot tell "never coming" from "still being worked on".
	NoticeAbandoned NoticeKind = "abandoned"
	// NoticeSettled: the order finished, however it finished.
	NoticeSettled NoticeKind = "settled"
)

type Order

type Order struct {
	ID string `json:"id"`
	// ReleaseID names the frozen catalogue this order was placed against. It is
	// what makes the order immune to catalogue edits while an approval is pending.
	ReleaseID string `json:"releaseId"`
	// Orderer and Recipient are principal ids. Never names, addresses or anything
	// else about a person — see ADR-0314.
	Orderer   string `json:"orderer"`
	Recipient string `json:"recipient"`
	Lines     []Line `json:"lines"`
	// Waves and Requires are copied from the release when the order is placed, for
	// the lines this order actually carries. The release computed them; the order
	// records them rather than recomputing later against a catalogue that may have
	// moved on, and so that fulfilment reads one record instead of two.
	Waves    [][]string          `json:"waves,omitempty"`
	Requires map[string][]string `json:"requires,omitempty"`
	// Assignments is where each approval sits and every hop it took to get there
	// (assignment.go). There is at most one per line, and only for lines that
	// needed approving.
	//
	// It is written when a deadline first moves an approval and not when the order
	// is placed, because at placement the approver of a "superior" line is not
	// known — the directory has not been asked yet. Until something moves it, the
	// live task's assignee is the whole truth and copying it here would be a second
	// one. What cannot be derived from the task is the *history*: who it started
	// with, every hop, and whether the chain ran out. That is what this holds, and
	// it outlives the task.
	Assignments []Assignment `json:"assignments,omitempty"`
	CreatedAt   int64        `json:"createdAt"`
	UpdatedAt   int64        `json:"updatedAt"`
}

Order is one order against one catalogue release.

func Apply

func Apply(o Order, ref string, status LineStatus, at int64) (Order, error)

Apply records one line's outcome, propagates what that outcome stopped, and returns the updated order. It returns the order unchanged when it refuses.

Only the two outcomes a provisioning attempt can produce may be set here. A rejection and an abandonment are decisions with an author, so they go through Reject and Abandon; blocked is derived and never set directly. Accepting them here would be a second way to reach a status whose whole point is that it has exactly one.

func CancelLine

func CancelLine(o Order, ref, by string, at int64, reason string) (Order, error)

CancelLine withdraws one position rather than the whole order.

CancelOrder exists because somebody cancelling *an order* is not making a series of per-line decisions. Removing one position is the opposite act: exactly one decision, about one thing.

Two things it does that a handler doing this by hand would forget.

An **integral** line is refused. The basket will not let anybody deselect a part its whole always carries — a workplace is not a workplace without its account — and a rule enforced when ordering and not afterwards is not a rule. It names what carries the line rather than only refusing, because the answer somebody needs is "take back the workplace instead".

And the schedule is recomputed. A withdrawn line is a root cause like a refused one, and anything waiting on it is waiting for nothing; skipping that pass leaves a line blocked forever behind something that will never arrive.

func CancelOrder

func CancelOrder(o Order, by string, at int64, reason string) (Order, []string, []string, error)

CancelOrder withdraws every line of an order that still can be, and reports which ones it could not touch.

It is one call rather than a line at a time because that is what somebody means: a person cancelling an order is not making a series of per-line decisions. Lines it leaves alone it names, because "your order is cancelled" when a laptop is already on its way is the sentence that produces the second support call.

func Rebind added in v0.8.0

func Rebind(o Order, it catalog.Item, by, reason string, at int64) (Order, int)

Rebind moves every line of one product that can still start one of its old processes onto the product's current lifecycle binding, and returns the order and how many lines moved. A line whose process is running now — running, returning — is left where it is: a process has it.

func RecordInstance

func RecordInstance(o Order, ref string, inst LineInstance) (Order, error)

RecordInstance notes that an instance was started to work one position.

ref names the position the way a process does: its key, or the product where the order carries one position of it. Recording the same instance for the same operation twice records it once, so a caller that retries does not make one instance look like two. The same instance for another operation is a new entry: a per-position lifecycle carries the position's return in the instance its provisioning started, and the return is still an attempt that counts (ADR-0428).

func Returning

func Returning(o Order, ref string, at int64, by string) (Order, error)

Returning marks a line's revocation as under way, recording who asked. The process itself is started by the caller, which is the half that needs an engine.

by may be empty where nothing identified the caller — single-user mode has no principals — and an empty actor is recorded as empty rather than as a guess.

func (Order) AssignmentFor

func (o Order) AssignmentFor(itemID string) (Assignment, bool)

AssignmentFor returns the recorded approval for one line, and whether there is one. A line whose approval has never moved has none.

func (Order) WithAssignment

func (o Order) WithAssignment(a Assignment) Order

WithAssignment returns a copy of the order carrying this approval, replacing any record for the same line. A copy, so a caller holding the previous order keeps its own slice rather than sharing the array under it.

type Outcome added in v0.9.0

type Outcome struct {
	CommandID   string
	Action      string
	Effect      string
	Outcome     string
	EventType   string
	OrderID     string
	Position    string
	Principal   string
	ItemID      string
	VariantID   string
	InstanceKey uint64
	At          int64
	// Result is a JSON object of scalars, bounded by whoever reports it.
	Result string
}

Outcome is one such ending. The zero value is no outcome.

func (Outcome) Set added in v0.9.0

func (o Outcome) Set() bool

Set reports whether o names an ending at all.

type Rebinding added in v0.8.0

type Rebinding struct {
	FromProvision   string `json:"fromProvision,omitempty"`
	FromDeprovision string `json:"fromDeprovision,omitempty"`
	ToLifecycle     string `json:"toLifecycle"`
	By              string `json:"by,omitempty"`
	At              int64  `json:"at"`
	Reason          string `json:"reason,omitempty"`
}

Rebinding records that a line was moved from the binding it froze to its product's current lifecycle binding (ADR-0427). The line keeps saying what it was granted under, and that this was changed afterwards, by whom and why — the same shape as a recorded correction of its details (ADR-0359).

type Service

type Service struct {

	// Limits are the resource budgets this service enforces. Set after
	// construction, like every other per-area service's, so that the constructor
	// does not grow a thirteenth positional argument for a value the server hands
	// to all of them the same way.
	Limits limits.Limits
	// contains filtered or unexported fields
}

Service is the order API area.

func New

func New(loop *runloop.Loop, store *Store, now func() int64,
	release func(id string) (catalog.Release, bool, error),
	mayOrderFrom func(*httpapi.Principal, string) (bool, error),
	groupsOf func(string) ([]string, error),
	mayOrderForOthers func(*httpapi.Principal) bool,
	wake func(message, orderID string, vars map[string]string) error,
	portalBase func() string,
	apiBase func() string,
	grant func(Grant) error,
	revoke func(principal, itemID string, at int64, by string, outcome Outcome) error,
	held func(principal string) (map[string]bool, error)) *Service

New builds the service.

func (*Service) HandleDecide

func (s *Service) HandleDecide(w http.ResponseWriter, r *http.Request)

HandleDecide records an approver's decision on a line: a refusal, or with "approved": true an approval.

It exists because Service.HandleReport deliberately will not take a rejection: that is a decision with an author, not a provisioning outcome, and a second way in would make the author optional. An approval settles nothing — the line goes on to be provisioned and its outcome arrives through the ordinary report — so recording one only writes who approved and when (Approve), and wakes nobody. It is optional for an approval process: one that does not call it leaves the line without an approver on record, as every line was before.

func (*Service) HandleGet

func (s *Service) HandleGet(w http.ResponseWriter, r *http.Request)

HandleGet reads one order. An order somebody else placed and is not the recipient of reads as absent rather than forbidden: its existence is not theirs to learn.

func (*Service) HandleList

func (s *Service) HandleList(w http.ResponseWriter, r *http.Request)

HandleList lists the caller's own orders, newest first.

func (*Service) HandleNext

func (s *Service) HandleNext(w http.ResponseWriter, r *http.Request)

HandleNext reports which of an order's lines may be started now, each with the process that provisions it and the variant that was chosen — everything an orchestrator needs to act, from one call.

This and Service.HandleReport are the orchestrator's two calls, and they are operator work rather than the orderer's: nobody reports the result of their own provisioning, and an operator works orders that are not theirs. The route's role says so; there is no object gate here, deliberately.

func (*Service) HandlePlace

func (s *Service) HandlePlace(w http.ResponseWriter, r *http.Request)

HandlePlace places an order against one release.

func (*Service) HandleReport

func (s *Service) HandleReport(w http.ResponseWriter, r *http.Request)

HandleReport records what came back for one line and propagates it.

func (*Service) RecordInstance

func (s *Service) RecordInstance(orderID, ref string, inst LineInstance) (found bool, err error)

RecordInstance notes, on the order itself, that an instance was started to work one of its positions. found is false for an order this server does not hold — not an error: a process may carry an orderId that names nothing here.

func (*Service) ReportOutcomesTo added in v0.9.0

func (s *Service) ReportOutcomesTo(report func(Outcome) error)

ReportOutcomesTo sets where an outcome that comes without a grant or a revocation is written (ADR-0429 §3). Set once, by the server, before the service serves.

type Status

type Status string

Status is where a whole order stands. It is derived from the lines rather than stored, so it can never disagree with them.

const (
	// OrderRunning has at least one line still to settle — including a line
	// blocked by a failure somebody can still repair. An order that settled while
	// an incident behind it was being worked would tell the orderer their line is
	// never coming, at the moment somebody is fixing the reason it has not.
	OrderRunning Status = "running"
	// OrderCompleted provisioned or skipped every line.
	OrderCompleted Status = "completed"
	// OrderPartial settled with some lines provisioned and some not. This is an
	// ordinary outcome, not a degraded one: everything that could be provisioned
	// was.
	OrderPartial Status = "partial"
	// OrderUnfulfilled settled with nothing provisioned at all.
	OrderUnfulfilled Status = "unfulfilled"
	// OrderCancelled settled because every line was withdrawn.
	//
	// Distinct from unfulfilled, which is what an order says when it tried and did
	// not manage. This one was taken back before it tried, and telling somebody
	// their own cancellation "was not fulfilled" invites them to ask why it
	// failed.
	OrderCancelled Status = "cancelled"
)

func Derive

func Derive(ls []Line) Status

Derive reports where a whole order stands, from its lines alone. It is computed rather than stored so it can never disagree with them.

An order stays open while any line can still move — including one blocked by a failure somebody can repair, which is the case that decides how long an order lives. It settles once every line has either an outcome of its own or a blockage that will not lift.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store holds orders.

func NewStore

func NewStore(dir string) (*Store, error)

NewStore opens (creating if needed) the directory backing the order store.

func (*Store) All

func (s *Store) All() ([]Order, error)

All lists every order, newest first.

func (*Store) Delete added in v0.18.0

func (s *Store) Delete(id string) error

Delete removes an order. Nothing in the shop deletes one: an order is evidence of what somebody asked for. It exists for the administrator's purge of a catalogue (ADR-0469), which a server started with --catalogue-purge=false refuses. An order already gone is not an error.

func (*Store) For

func (s *Store) For(principal string) ([]Order, error)

For lists the orders one principal placed or is the recipient of, newest first. Both, because an order placed for somebody by their integration manager is one they should be able to see.

func (*Store) Get

func (s *Store) Get(id string) (Order, bool, error)

Get reads one order. A missing record is not an error.

func (*Store) Save

func (s *Store) Save(o Order) error

Save writes an order, replacing any record with the same id.

Jump to

Keyboard shortcuts

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