Skip to content

fix(rates): treat a bare flat fee as a project fee, not per-hour - #455

Merged
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/bare-flat-fee-billed-hourly
Aug 30, 2026
Merged

fix(rates): treat a bare flat fee as a project fee, not per-hour#455
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/bare-flat-fee-billed-hourly

Conversation

@clawedassistant26

Copy link
Copy Markdown
Contributor

The bug

parseRate() in src/rates.mjs has a guard whose comment states the intent plainly:

// A flat fee with no period stated is a project fee, not an hourly one: "$5000
// for the project" is how it is written, and defaulting it to per-hour would
// silently multiply the invoice by every hour tracked.
if (!sawPeriod && rate.unit === "flat" && rate.cap === null) rate.per = "hour";

The assignment set rate.per = "hour" — the exact behaviour the comment warns against. Since the default is already "hour", the guard was a no-op, so a bare flat fee like $5000 or 250 USDC parsed as an hourly rate.

chargeFor() then scales an hourly rate by tracked time, so a $5000 fee tracked over a 10-hour job bills $50,000:

parseRate("$5000")                        => { per: "hour", ... }
chargeFor({ seconds: 36000 }, that)       => { amount: 50000, flat: false }

A project fee is meant to be flat (amount: null, added once by the invoice):

parseRate("$5000/project")                => { per: "project", ... }
chargeFor({ seconds: 36000 }, that)       => { amount: null, flat: true }

The fix

One line: set rate.per = "project" in that guard, matching the documented intent. A stated period ($100/hour) is untouched, and $100/agent (a unit but no period) still defaults to hourly as before.

Test

Added a regression test in test/rates.test.mjs asserting a bare fee (fiat and crypto) parses as a project fee, bills flat over a 10-hour job, and that an explicit period is left alone. Verify-first: the test fails on the pre-fix line and passes with the fix. Full suite green (2375 pass / 0 fail / 337 skip).

A rate spec with a price and no stated period ("$5000", "250 USDC") is a
flat project fee. parseRate's own comment says so and warns that defaulting
it to per-hour "would silently multiply the invoice by every hour tracked" —
but the guard set rate.per = "hour" instead of "project", the exact bug the
comment describes.

Because the default is already "hour", the guard was a no-op: parseRate("$5000")
returned per: "hour", so chargeFor scaled it by tracked time. Over a 10-hour
job a $5,000 project fee billed as $50,000. With the fix it returns
per: "project" -> flat, billed once by the invoice.

Regression test asserts a bare fee (fiat and crypto) parses as a project fee
and that an explicit period is left untouched. Full suite green.
@ralyodio
ralyodio merged commit 406cf21 into moshcoder:main Aug 30, 2026
6 checks passed
@ralyodio ralyodio mentioned this pull request Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants