Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1299,7 +1299,7 @@ each useful on its own — the timer needs no client, the rate needs no gateway
> not exist. `billing import` is what closes that gap, which is why it comes
> first.
>
> The standalone billing carries the same rate model (`$100/hour/agent/upto:4`)
> The standalone billing carries the same rate model (`$400/hour/agent/upto:4`)
> and bills **agent-hours**.

```sh
Expand All @@ -1325,14 +1325,14 @@ fixed field list — `--billing.po` works because it says what it means.
`/business`, `/merchant` and `/customer` are the same command.

```sh
moshcode rate set default $100/hour/agent/upto:4
moshcode rate set default '$400/hour/agent'
moshcode rate set acme-inc 0.5 SOL/day --prefer SOL,USDC --accept fiat
moshcode rate set initech $5000/project
```

`$100/hour/agent/upto:4` is the sentence from the contract, parsed: price,
period, unit, and the cap that made the client sign. Four agents cost four
hundred an hour and **so do six**. Order after the price does not matter.
`$400/hour/agent/upto:4` is the sentence from the contract, parsed: price,
period, unit, and the cap that made the client sign. Four agents cost $1,600
an hour and **so do six** when the contract explicitly caps billing at four. Order after the price does not matter.

```sh
moshcode billing acme-inc # a preview — writes nothing
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "moshcode",
"version": "0.98.2",
"version": "0.98.3",
"type": "module",
"description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
"repository": {
Expand Down
6 changes: 3 additions & 3 deletions plugins/billing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Slash commands for
rates and invoices, built on the hours `@profullstack/timer` tracked.

```
/billing:rate set acme '$100/hour/agent/upto:4'
/billing:rate set acme '$400/hour/agent/upto:4'
/billing:hours --client acme --month
/billing:invoice --client acme --from-timer --month
/billing:report
Expand All @@ -25,8 +25,8 @@ moshcode install billing

## What it is for

A rate is the sentence from the contract, parsed. `$100/hour/agent/upto:4` means
four agents cost four hundred an hour and so do six, and the invoice bills
A rate is the sentence from the contract, parsed. `$400/hour/agent/upto:4` means
four agents cost sixteen hundred an hour and so do six, and the invoice bills
**agent-hours** so the client can check the line by hand: `quantity × rate`
always equals `amount`.

Expand Down
4 changes: 2 additions & 2 deletions plugins/billing/commands/rate.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Read or set what your time costs, written the way the contract says it.
argument-hint: "[set <client|default> '$100/hour/agent/upto:4']"
argument-hint: "[set <client|default> '$400/hour/agent/upto:4']"
allowed-tools: Bash(billing rate:*), Bash(billing client:*)
---

Expand All @@ -21,7 +21,7 @@ A price, then any of these in any order:
- a unit that gets multiplied: `agent`, `seat`, `person`, `team`
- `upto:N` to cap the multiplier, `min:N` for a minimum billed period

`$100/hour/agent/upto:4` means four agents cost four hundred an hour, and so do
`$400/hour/agent/upto:4` means four agents cost sixteen hundred an hour, and so do
six. `0.5 SOL/day`, `250 USDC/task` and `$5000/project` all parse too.

## Rules
Expand Down
2 changes: 1 addition & 1 deletion src/billing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ function draftInvoice(argv, write, run) {
return 0;
}
if (!invoice.rate) {
write(err(`no rate for ${bone(found.id)} — ${acid(`/rate set ${found.id} $100/hour/agent/upto:4`)}`));
write(err(`no rate for ${bone(found.id)} — ${acid(`/rate set ${found.id} $400/hour/agent/upto:4`)}`));
return 1;
}

Expand Down
4 changes: 2 additions & 2 deletions src/cli-schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ export const CORE_CLI_COMMANDS = [
group: "business",
description: "what an hour of agent time costs",
synopsis: [
["moshcode rate set <client|default> <spec>", "$100/hour/agent/upto:4"],
["moshcode rate set <client|default> <spec>", "$400/hour/agent/upto:4"],
["moshcode rate [list] | show <client> | rm <client>", ""],
],
verbs: "RATE_VERBS",
Expand All @@ -822,7 +822,7 @@ export const CORE_CLI_COMMANDS = [
["--json", "machine-readable", ""],
],
examples: [
["moshcode rate set default $100/hour/agent/upto:4", "four agents cost four hundred; six also cost four hundred"],
["moshcode rate set default '$400/hour/agent'", "four agents cost $1,600/hour; six cost $2,400/hour"],
["moshcode rate set acme 0.5 SOL/day --prefer SOL,USDC --accept fiat", ""],
["moshcode rate set acme $5000/project", "a flat fee, added once per invoice"],
],
Expand Down
2 changes: 1 addition & 1 deletion src/commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ const COMMANDS = [
cliVerb("timer", "track time (timer(\"on\", client) … timer(\"off\"))"),
cliVerb("client", "clients/businesses/merchants — create, list, set, payee"),
cliVerb("team", "who may do what (create, add, grant, revoke, can)"),
cliVerb("rate", "what agent time costs (rate(\"set\", \"acme\", \"$100/hour/agent/upto:4\"))"),
cliVerb("rate", "what agent time costs (rate(\"set\", \"acme\", \"$400/hour/agent/upto:4\"))"),
cliVerb("billing", "turn tracked time into an invoice (--mark claims it, --send hands it over)"),
cliVerb("payments", "the rail invoices go out on (connect, default, disconnect)"),
cliVerb("plugin", "install/manage moshcode plugins from the marketplace"),
Expand Down
20 changes: 10 additions & 10 deletions src/rates.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// What an hour of agent time costs, written the way people say it out loud.
//
// /rate set acme $100/hour/agent/upto:4
// /rate set acme $400/hour/agent/upto:4
//
// One line that carries four decisions: the price, the period it is charged
// for, the thing that is multiplied (an agent, a seat, a person), and the point
Expand Down Expand Up @@ -101,15 +101,15 @@ function parseAmount(token) {
*
* The grammar is positional only in its first segment (the price); everything
* after it is recognised by what it says rather than where it sits, so
* `$100/agent/hour` and `$100/hour/agent` mean the same thing. People do not
* `$400/agent/hour` and `$400/hour/agent` mean the same thing. People do not
* remember an order they were never told.
*/
export function parseRate(spec) {
const text = String(spec ?? "").trim();
if (!text) throw new Error("a rate looks like $100/hour/agent/upto:4");
if (!text) throw new Error("a rate looks like $400/hour/agent/upto:4");
const parts = text.split("/").map((p) => p.trim()).filter(Boolean);
const price = parseAmount(parts.shift());
if (!price) throw new Error(`can't read a price out of ${JSON.stringify(text)} — try $100/hour/agent`);
if (!price) throw new Error(`can't read a price out of ${JSON.stringify(text)} — try $400/hour/agent`);

const rate = { ...price, per: "hour", unit: "flat", cap: null, min: null };
let sawPeriod = false;
Expand Down Expand Up @@ -141,7 +141,7 @@ export function parseRate(spec) {
// silently multiply the invoice by every hour tracked.
if (!sawPeriod && rate.unit === "flat" && rate.cap === null) rate.per = "project";
if (rate.cap !== null && rate.unit === "flat") {
throw new Error("upto: caps a unit, so say what it caps — $100/hour/agent/upto:4");
throw new Error("upto: caps a unit, so say what it caps — $400/hour/agent/upto:4");
}
return rate;
}
Expand Down Expand Up @@ -235,14 +235,14 @@ export function splitSettlement(argv) {
const USAGE = [
"usage: /rate set <client|default> <spec> [--prefer SOL,USDC] [--accept fiat]",
" /rate [list] [--json] · /rate show <client> · /rate rm <client>",
" spec: $100/hour/agent/upto:4 · 0.5 SOL/day · $5000/project · 250 USDC/task",
" spec: $400/hour/agent/upto:4 · 0.5 SOL/day · $5000/project · 250 USDC/task",
];

/**
* `/rate` and `/rates`.
*
* `set:` with a colon is accepted because that is how it was first written down
* (`/rates set: $100/hour/agent/upto:4`), and refusing punctuation somebody
* (`/rates set: $400/hour/agent/upto:4`), and refusing punctuation somebody
* already typed teaches them nothing.
*/
export function rateCommand(argv = [], { write = console.log } = {}) {
Expand All @@ -257,7 +257,7 @@ export function rateCommand(argv = [], { write = console.log } = {}) {
if (json) { write(JSON.stringify(rates, null, 2)); return 0; }
if (!names.length) {
write(info("no rates yet."));
write(` ${acid("/rate set default $100/hour/agent/upto:4")}`);
write(` ${acid("/rate set default $400/hour/agent")}`);
return 0;
}
write(table(
Expand Down Expand Up @@ -299,7 +299,7 @@ export function rateCommand(argv = [], { write = console.log } = {}) {
const found = resolveRateTarget(business, positional[1] || "default");
const who = found.ok ? found.id : String(positional[1]).toLowerCase();
const rate = rateFor(business, who);
if (!rate) { write(err(`no rate for ${JSON.stringify(who)} and no default — /rate set ${who} $100/hour/agent`)); return 1; }
if (!rate) { write(err(`no rate for ${JSON.stringify(who)} and no default — /rate set ${who} $400/hour/agent`)); return 1; }
if (json) { write(JSON.stringify(rate, null, 2)); return 0; }
write(` ${bone(who)} ${acid(formatRate(rate))}${rate.source !== who ? ash(` (from ${rate.source})`) : ""}`);
write(` ${ash(describeRate(rate))}`);
Expand Down Expand Up @@ -342,7 +342,7 @@ function parseSafely(spec) {
* The arithmetic everybody does in their head and gets wrong once a quarter.
* Two things make it more than a multiplication:
*
* - the cap. `$100/hour/agent/upto:4` means four agents cost four hundred an
* - the cap. `$400/hour/agent/upto:4` means four agents cost sixteen hundred an
* hour and *so do six* — the cap is the promise that made the client sign,
* and it has to be applied here rather than remembered at invoice time.
* - the floor. `min:1` bills a fifteen-minute call as an hour, which is the
Expand Down
2 changes: 1 addition & 1 deletion src/timer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ function stopTimer(args, write, now) {
} else if (charge?.flat) {
write(` ${ash("flat project fee — /billing adds it once, not per entry")}`);
} else if (!rate) {
write(` ${ash("no rate for this one —")} ${acid(`/rate set ${entry.client || "default"} $100/hour/agent`)}`);
write(` ${ash("no rate for this one —")} ${acid(`/rate set ${entry.client || "default"} $400/hour/agent`)}`);
}
write(` ${ash(`entry ${entry.id}`)}`);
return 0;
Expand Down
2 changes: 1 addition & 1 deletion src/tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const TOOLS = {
billing: {
desc: "Profullstack billing - clients, rates and invoices from tracked hours",
bin: "billing",
// The other half. It carries the rate model /rate parses ($100/hour/agent/
// The other half. It carries the rate model /rate parses ($400/hour/agent/
// upto:4) and bills agent-hours from the timer's entries. `billing import`
// brings across a ledger that started in ~/.moshcode/business.json.
install: { cmd: "npm", args: ["install", "-g", "@profullstack/billing"] },
Expand Down
2 changes: 1 addition & 1 deletion test/rates.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ test("the spellings people actually type all land", () => {
test("a rate that cannot be read says which word it choked on", () => {
assert.throws(() => parseRate("$100/fortnight"), /fortnight/);
assert.throws(() => parseRate("free"), /can't read a price/);
assert.throws(() => parseRate(""), /\$100\/hour\/agent/);
assert.throws(() => parseRate(""), /\$400\/hour\/agent/);
// A cap with nothing to cap is the error that matters most: it reads as a
// discount and silently is not one.
assert.throws(() => parseRate("$100/hour/upto:4"), /caps a unit/);
Expand Down
Loading