JITSU-200: committed contracts in billing UI (console half) - #1490
Conversation
Billing today is monthly everywhere in the console: the quota comes from
`destinationEvensPerMonth`, and every usage/overage string says "month".
An annual contract commits to a volume for the whole contract year and
meters against that single pool, so rendering it as monthly would show a
customer a twelfth of what they bought.
Extend the billing/settings contract with two optional fields —
`billingInterval` ("month" | "year") and `destinationEventsPerPeriod`
(events included in one full period; the negotiated annual commitment,
which is not derivable from the monthly tier) — and read both through a
single `billingPeriod()` helper so no surface reaches for the monthly
field directly. Both are optional and absent on every plan that exists
today, so the console keeps behaving exactly as before until the billing
API starts sending them.
The period window itself needed no change: `currentPeriod` already comes
straight from the Stripe subscription, which is anniversary-based and a
year long for an annual price.
Also make the custom-quote page (/custom-plan) interval-aware, since a
negotiated annual quote is exactly what it renders.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the annual billing settings, event-usage path, custom quote rendering, and the dependent connector-usage report.
- Connector sync usage is now counted over the annual subscription window without an annual sync quota, which can produce false overage warnings for annual customers.
dailyActiveSyncs is a monthly limit with no annual terms, so on an annual plan the sync usage section now windows over the contract month containing now (anchored on the period start, same as a monthly subscription would bill) instead of the whole contract year. Monthly and free plans are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
…, events by metering interval The billing API now describes a negotiated quote with two independent cadences: what one charge covers (price interval x interval_count, e.g. quarterly installments) and the period the included events are metered over (billingInterval, the contract year). The page read only the price interval, so a quarterly-invoiced annual commitment rendered the installment as per month and the committed annual volume as events per month. chargeCadence() labels the charge; the events line follows the metering interval. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the annual committed-volume billing changes, including usage metering, quota display, sync usage windows, and custom-plan copy.
Finding:
- Custom-billed annual workspaces still query one monthly
currentPeriodwhile comparing it with the new annual quota.
There was a problem hiding this comment.
Reviewed the annual billing quota, event-usage, sync-window, and quote-cadence changes. No additional actionable issues found.
The existing unresolved inline finding remains applicable: custom-billed annual workspaces can still query only one month of events against the annual quota.
expiresAt is a UTC instant; on an annual plan it is the anniversary at 00:00 UTC, so formatting it in the viewer's zone showed the previous day west of UTC. Every other period date on the billing page is already rendered in UTC. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
…om billing included Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the annual billing-period, event-usage, sync-quota, renewal-date, and custom-quote changes. I also checked the existing review threads; both are resolved and are not re-raised. No new actionable correctness, security, or user-visible regression found.
There was a problem hiding this comment.
Reviewed the annual billing-period contract, usage/overage calculations, monthly sync window behavior, quote cadence rendering, and UTC renewal-date display. I also checked the existing review threads; the two earlier findings are resolved and addressed by the current diff. No new actionable issues found.
The token only guarded discovery of a negotiated plan; the billing service (jitsu-cloud-billing#40) now also requires it to check out a custom plan, so anyone learning a plan id can no longer check out at the negotiated price. The quote page already holds the token and forwards it with the upgrade request. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the annual committed-volume changes across billing settings parsing, event and sync reporting windows, custom-plan quote/checkout handling, and UTC renewal-date rendering.
No additional actionable bugs, security issues, or user-visible regressions found. The existing resolved review threads were also checked and are not re-raised.
The billing API reports no period before it starts, but the helper should not depend on that: clamp the contract-month index at 0 so a not-yet-started period yields its first month rather than the month before the contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the annual billing-period, usage reporting, sync quota window, and custom-quote changes. I found no new actionable correctness, security, or user-visible regression issues in the final diff. Existing resolved threads were considered and are not re-raised.
… pool (JITSU-200) Product decision (2026-09-03): a committed contract is a term with a fixed monthly quota; overage is only ever tracked per month. The billing API drops billingInterval and destinationEventsPerPeriod, keeps currentPeriod at one month (the contract month anchored on the subscription start for installment-billed commitments) and moves the commitment end into expiresAt. Removes the annual quota/period helper and copy; keeps the usage cache key per period, the zero-quota guard, the UTC renewal date, the quote token on checkout, and the quote page's charge-cadence labels, now with the commitment term from commitmentInterval. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
commitmentInterval now reaches the console via plan_data; on a committed contract expiresAt is the contract end, so label it "12-month commitment" instead of letting it read as a monthly cycle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the billing-settings contract updates, commitment/charge-cadence rendering, custom-quote checkout token forwarding, and usage-cache key change. No additional actionable findings beyond the existing unresolved zero-quota usage discussion.\n\nValidation: inspected the PR diff and surrounding call paths; targeted unit test command could not run because pnpm is not installed in this environment.
…view) A quota of 0 is a misconfigured plan; reporting the ratio as 0% hid the red bar and the overage warning. usagePercentage() reports positive usage as a finite ratio above 1 and zero usage as 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the committed-contract billing UI changes, custom-quote checkout token forwarding, UTC renewal-date rendering, and billing-usage cache key update. I also checked the existing review threads; all are resolved. No new actionable findings in the supplied range.
There was a problem hiding this comment.
Reviewed the committed-contract billing UI changes, including quote checkout token forwarding, cadence/term copy, UTC renewal rendering, monthly usage cache rollover, and zero-quota handling. I also inspected the existing review threads; all are resolved and were not re-raised. No new actionable findings.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the committed-contract billing UI changes, including quote checkout/cadence copy, UTC renewal rendering, and usage-period caching.
Finding:
- The new cache key does not roll an already-open billing page into the next period because no render is scheduled at the boundary.
There was a problem hiding this comment.
Reviewed the committed-contract billing UI changes: quote checkout token forwarding, charge/commitment labels, UTC renewal display, and monthly usage metering safeguards.
No additional actionable findings in this pass. The existing open thread on refreshing usage after a period boundary remains applicable.
There was a problem hiding this comment.
Reviewed the negotiated-contract billing UI changes, including quote checkout token forwarding, cadence/commitment copy, billing date rendering, and usage handling.
Finding:
- The zero-quota guard may regress plans represented by the existing negative
Unlimitedquota sentinel.
The billing service now accepts `destinationEventsPerMonth` next to the misspelt `destinationEvensPerMonth` (jitsu-cloud-billing#40), emitting both wherever it normalizes plan data. Make the console robust to either spelling on its own: BillingSettings accepts the correct key and copies it over the legacy one on parse (correct spelling wins), and the quote page reads the raw plan_data through the same helper instead of the legacy key only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
…rcentage A negative destinationEvensPerMonth is the unlimited sentinel; the zero-quota special case must not turn it into "exceeded". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
A junk value in the correct-spelled key must be skipped in favour of the legacy one, not treated as "no quota" — that is what the billing service does, verified against its own resolution. The zod schema and the quote page now share that single rule, so the plans table, the usage bar and a quote can never disagree about a plan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the console changes for committed-contract billing: cadence and commitment copy, token forwarding, UTC renewal rendering, quota normalization, and usage/cache behavior. I found no new actionable correctness, security, or user-visible regression issues. Existing resolved review threads were inspected and not re-raised. Targeted unit tests were not run here because pnpm is unavailable in this runner.
|
🚀 |
Console half of
JITSU-200— committed contracts (e.g. a 12-month commitment invoiced quarterly) in the billing UI.The server half is
jitsucom/jitsu-cloud-billing#40(this PR matches its head3085a2a). Nothing here depends on it for existing plans; the only ordering constraint is the quote token below.Product decision (2026-09-03)
A commitment is a contract term with a fixed monthly quota — no annual pool, no rollover; overage is only ever tracked per month. So the billing contract stays monthly:
destinationEvensPerMonthis the quota,currentPeriodis always one month (the Stripe cycle for a plain monthly price, otherwise the contract month anchored on the subscription start), and onlyexpiresAtreflects the commitment term (the contract anniversary for a commitment billed quarterly). An earlier revision of this PR added an annual quota and period; that is gone.Changes
lib/schema/index.ts— new optionalcommitmentInterval("year"or absent; typed as a loose string so a Stripe-metadata typo cannot take the billing page down), the contract term fromplan_data; thecurrentPeriodandexpiresAtdoc comments describe the actual contract (the former still talked about a month "adjusted" for annual subscriptions).components/Billing/use-events-usage.ts—periodStartadded to the react-query key so a period rollover refetches instead of serving the previous month from the 5-minute cache;usagePercentage()(newcomponents/Billing/usage-percentage.ts) so a misconfigured plan with a zero quota reports positive usage as exceeded instead of rendering aNaNprogress bar and silently skipping the overage warning.components/Billing/BillingManager.tsx— the "Renews at" date is rendered in UTC like every other period date on the page: for a commitmentexpiresAtis the anniversary at 00:00 UTC, which the viewer's local zone showed as the previous day west of UTC. On a committed contract the date is tagged with the term ("12-month commitment"), since it is the contract end rather than the end of a monthly cycle.pages/custom-plan.tsx— the negotiated-quote page hardcoded "$X /month … Month-to-month, cancel at any time". It now labels the price by the charge cadence (intervalxintervalCount:/quarter,/year, …) and states the term fromcommitmentInterval, so a quarterly-invoiced 12-month deal reads "$X /quarter … N events per month included. 12-month commitment, billed quarterly." Falls back to the older monthly-only API shape. The "Accept Plan" button also forwards the quotetokentobilling/upgrade: the token used to guard only discovery of a negotiated plan, and Closes #29 added clickhouse single node integration (with MergeTree() table engine) #40 now requires it to check a custom plan out, so anyone learning a plan id can no longer check out at the negotiated price.components/Billing/charge-cadence.ts+__tests__/unit/charge-cadence.test.ts— the two label helpers, 3 cases.Deploy note
The billing side must not ship ahead of this console change: once #40 enforces the token, "Accept Plan" on custom quotes fails with "Invalid token" until the console forwards it. The extra parameter is ignored by the current billing API, so the console can go first.
Follow-up
The issue's acceptance criterion "configurable in console/admin (not just Stripe free-text)" is only partly met: the values are structured
plan_datakeys read by the metering code rather than description free-text, but configuring a deal remains a Stripe dashboard operation. An admin editor is a separate, larger piece of work.Testing
pnpm typecheck:turboclean; the console unit project passes.🤖 Generated with Claude Code
https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q