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
5 changes: 3 additions & 2 deletions telemetry-worker/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -320,7 +320,8 @@ async function account(c: C): Promise<Response> {
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 },
Expand Down
9 changes: 9 additions & 0 deletions telemetry-worker/src/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number | null> {
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;

Expand Down
3 changes: 3 additions & 0 deletions telemetry-worker/test/cli-account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand Down
Loading