diff --git a/telemetry-worker/src/cli.ts b/telemetry-worker/src/cli.ts index bff6c9d9..91697944 100644 --- a/telemetry-worker/src/cli.ts +++ b/telemetry-worker/src/cli.ts @@ -55,7 +55,7 @@ import { isKind, KINDS } from "./forward.js"; import { githubApp } from "./github/config.js"; import { repoStatusForAccount } from "./github/http.js"; import { json } from "./http.js"; -import { accountAccess, accountBilling, accountProjectsAccess, better, planParam, canBuy, entitled, firstPaidWith, firstPlanWith, FREE, hasAnalytics, hasAsk, hasDashboard, hasProblems, PAID_PLANS, pickRange, priceText, problemsLock, projectAccess, viewRanges, type Access } from "./plans.js"; +import { accountAccess, accountBilling, accountProjectsAccess, appsCovered, better, planParam, canBuy, entitled, firstPaidWith, firstPlanWith, FREE, hasAnalytics, hasAsk, hasDashboard, hasProblems, PAID_PLANS, pickRange, priceText, problemsLock, projectAccess, viewRanges, type Access } from "./plans.js"; import { liveFixUsage } from "./usage.js"; import { appKey, askModel, byWeight, claimRun, hasModel, keepAnswer, lastAnswer, MAX_SUGGEST_BYTES, readDigest, suggestionLimits, usedToday, type PlanItem, type SuggestAnswer } from "./suggest.js"; import { activeOwners, createInvite, isTeamError, membershipsOf, teamOf } from "./team.js"; @@ -320,7 +320,8 @@ async function account(c: C): Promise { access: access.source, // the plan the account works under through a team when it is better than its own (workingAccess), else null team_plan: work.team ? { id: work.access.plan.id, name: work.access.plan.name } : null, - limits: { apps: access.plan.limits.apps, history_days: access.plan.limits.history_days, seats: access.seats, suggestions_per_day: lim.per_day, suggestions_shown: lim.shown }, + // the apps the account's plan covers (coverage): no limit for a grant, Pro's 3 or the apps kept from before version 5 + limits: { apps: await appsCovered(c.db, a.account.id, c.env, c.now), history_days: access.plan.limits.history_days, seats: access.seats, suggestions_per_day: lim.per_day, suggestions_shown: lim.shown }, used: { apps: apps.filter((p) => p.account_id === a.account.id).length, suggestions_today: await usedToday(c.db, a.account.id, c.now), seats: t.used }, includes: { analytics: entitled(access.plan, "analytics"), forwarding: entitled(access.plan, "forwarding"), export: entitled(access.plan, "export"), private_repositories: entitled(access.plan, "github_private") }, subscription: { status: b.subscription_status, current_period_end: b.current_period_end, cancel_at_period_end: !!b.cancel_at_period_end }, diff --git a/telemetry-worker/src/plans.ts b/telemetry-worker/src/plans.ts index d6256d31..582cd459 100644 --- a/telemetry-worker/src/plans.ts +++ b/telemetry-worker/src/plans.ts @@ -525,6 +525,15 @@ export async function projectAccess(db: D1Like, project: { account_id?: string | return accessIn(await coverage(db, project.account_id, stripeMode(env), now), project); } +/** + * How many apps the account's plan covers now (coverage's cap): null for a grant, an open server and a plan with no + * limit; Pro's 3, or the apps a subscription from before version 5 kept; Free's one. + */ +export async function appsCovered(db: D1Like, accountId: string, env: Env, now: Date): Promise { + if (planGatesOpen(env)) return null; + return (await coverage(db, accountId, stripeMode(env), now)).cap; +} + /** The first plan in plans.json that includes `e` (the plan an upgrade message names), or null. */ export const firstPlanWith = (e: Entitlement): Plan | null => PLANS.find((p) => entitled(p, e)) ?? null; diff --git a/telemetry-worker/test/cli-account.test.ts b/telemetry-worker/test/cli-account.test.ts index 2bad9c23..e0d42585 100644 --- a/telemetry-worker/test/cli-account.test.ts +++ b/telemetry-worker/test/cli-account.test.ts @@ -148,6 +148,9 @@ describe("genclass command: GET /api/cli/account and POST /api/cli/team/invite", // only the code's hash is kept, and the team now lists one open invite expect(JSON.stringify(sql.rows("SELECT * FROM team_invites"))).not.toContain(inv.url.split("/").pop()); expect((await json(get(env, "/api/cli/account", me.bearer))).team.invites).toHaveLength(1); + // GenClass's grant covers every app: no app limit in the answer, whatever plan the grant gives + setPlan(sql, me.account.id, "free", { stripe_mode: null, seats: null, subscription_id: null, subscription_status: null, comp_until: "9999-12-31T00:00:00.000Z", comp_plan: "team" }); + expect((await json(get(env, "/api/cli/account", me.bearer))).limits.apps).toBeNull(); }); });