Build with Estii

Compose a space's pricing

Roles, products, streams, rate cards and tags, and rolling a rate change into deals.

Compose a space's library

A space prices from its library: the roles, products and streams work is estimated against, the rate cards those are priced on, and the tags that group them. Standing one up — a new space, a trial account, a rate card that arrived as a spreadsheet — is the same write shape as everything else, one part at a time. The names below assume a space that does not hold them yet: a name already taken is refused rather than added a second time, because a name is how you address the part on the next command.

estii tag add name="Delivery" --yes
estii role add name="Senior Developer" cost=700 price=1400 tag=Delivery --yes
estii role add name="Junior Developer" cost=350 price=700 tag=Delivery --yes
estii product add name="Premium Support" margin=0.45 unit=seat --yes
estii stream add --data '{"name": "Delivery Pod", "allocations": [{"role": "Senior Developer", "amount": 2}, {"role": "Junior Developer", "amount": 1}]}' --yes
estii card add name="Rush" margin_med=0.6 --yes
estii role set "Senior Developer" --card Rush price=1800 --yes

A new role carries a rate on every card in the space, derived from that card's margin band, so it prices the moment it exists. A card works the other way round: adding one puts a derived rate on every role you already hold, and --card then names which of them a cost or price lands on. Every part is named at creation, because a part with no name is one you cannot address on the next command.

Take something out

estii role remove "Junior Developer" --dry-run
estii role remove "Junior Developer" --map-to "Senior Developer" --yes
estii stream remove "Delivery Pod" "allocations/Senior Developer" --yes
estii role move "Senior Developer" --to top --yes

A role that a stream is composed of is refused, naming the streams that allocate it: deleting it would silently recompose them. --map-to repoints those allocations onto another resource and removes the role as one change, which is the choice the app offers in the same situation. A stream's own lines are addressed inside it — allocations/<ref>, by allocation id or by the name of the role it targets — so dropping one role from a pod does not mean restating the pod. Removing the stream takes its lines with it. move positions a part among the parts of its own kind and prices nothing.

Deals hold their own copy of all of this, so composing a library strands no deal: each takes the change when estii deal update runs on it. That also means restructuring has a price. Every deal still built on the old shape needs reconciling, and a resource that is gone needs a mapping decision on each deal that used it, so change the library for a reason you can name. estii context states that rule in the space's own terms, and estii deal list --updates reports what is outstanding.

Roll a library rate change into deals

Deals hold their own copy of your rates, so a rate change does not reprice work you have already quoted. Each deal takes it when you decide.

estii role set "Senior Developer" price=1400 --dry-run
estii role set "Senior Developer" price=1400 --yes
estii deal list --updates
estii deal update nd_abc123 --dry-run
estii deal update nd_abc123 --yes

--updates reports which deals are behind and for how long. deal update previews every change and what it does to total price and margin before it applies. If a resource needs mapping onto another, it writes nothing and hands back the address of the panel where you decide.

Fix pricing that is not a single rate

A role has one cost and one price on each card, so role set corrects it in place. A product and a stream do not. A product prices from its own margin over a table of volume tiers, and a stream has no rate at all: its cost and price are whatever the roles it is made of add up to. Both are tables, so you read the current one out, change what is wrong, and set the whole thing back.

estii product export --out - | jq '.products[] | select(.name == "Premium Support")' > support.json
estii product set "Premium Support" --data @support.json --dry-run
estii product set "Premium Support" --data @support.json --yes
estii stream set "Delivery Pod" --data '{"allocations": [{"role": "Senior Developer", "amount": 2}, {"role": "Designer", "amount": 1}]}' --dry-run

The exported entry is exactly what the write takes, so a round trip through an editor or a script changes only what you touched. That includes which tier prices itself and which one follows the margin: set a tier's price and it holds it, leave it alone and it keeps moving with the product's margin, the way it does in the app.

Setting a table replaces it. A tier or an allocation you leave out is removed, so the diff lists every removal by name before you confirm. A stream naming a role that does not exist refuses the write whole rather than dropping that allocation.

There is no card to name on either. A product is not priced per card, and a stream prices through the roles under it. Deals still hold their own copy, so this moves nothing until estii deal update runs.