Skip to content

Commit a93db4e

Browse files
authored
refactor(billing): drop the seat-retirement memo; keep only readable 410 copy
Seats are gone entirely by cutover and the whole seat surface gets deleted after it, so special handling isn't worth carrying: SeatService goes back to main unchanged (auto-provision already fails soft on any create error), and the 410 keeps only a typed error plus readable copy in classifySeatError for the seat-era UI still visible until the flag flips. Generated-By: PostHog Code Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
1 parent fd1ee43 commit a93db4e

3 files changed

Lines changed: 7 additions & 59 deletions

File tree

packages/core/src/billing/seatErrors.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,15 @@ export interface ClassifiedSeatError {
33
redirectUrl: string | null;
44
}
55

6-
/**
7-
* The seat API's 410 Gone: PostHog Code seats are retired in favor of
8-
* usage-based billing. Name-based so it survives the SeatClient seam.
9-
*/
10-
export function isSeatProductRetiredError(error: unknown): boolean {
11-
return error instanceof Error && error.name === "SeatProductRetiredError";
12-
}
13-
146
export function classifySeatError(error: unknown): ClassifiedSeatError {
157
if (!(error instanceof Error)) {
168
return { error: "An unexpected error occurred", redirectUrl: null };
179
}
1810

19-
if (isSeatProductRetiredError(error)) {
11+
// The seat API's 410 Gone: seats are retired in favor of usage-based
12+
// billing. Only reachable from the seat-era UI, which goes away with the
13+
// cutover — no handling beyond readable copy.
14+
if (error.name === "SeatProductRetiredError") {
2015
return {
2116
error:
2217
"PostHog Code seat plans have been retired — usage is now billed to your organization.",

packages/core/src/billing/seatService.test.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -300,36 +300,6 @@ describe("seat product retired (410 Gone)", () => {
300300
expect(result.error).toBeNull();
301301
});
302302

303-
it("stops re-attempting provisioning once the product is known retired", async () => {
304-
const createSeat = vi.fn().mockRejectedValue(new SeatProductRetiredError());
305-
const client = makeClient({ createSeat });
306-
const service = new SeatService(client, logger);
307-
308-
await service.fetchSeat({ autoProvision: true });
309-
createSeat.mockClear();
310-
await service.fetchSeat({ autoProvision: true });
311-
312-
expect(createSeat).not.toHaveBeenCalled();
313-
});
314-
315-
it("still re-fetches the seat after a non-retirement provisioning failure", async () => {
316-
const seat = makeSeat();
317-
const getMySeat = vi
318-
.fn()
319-
.mockResolvedValueOnce(null)
320-
.mockResolvedValueOnce(null)
321-
.mockResolvedValue(seat);
322-
const client = makeClient({
323-
getMySeat,
324-
createSeat: vi.fn().mockRejectedValue(new Error("conflict")),
325-
});
326-
const result = await new SeatService(client, logger).fetchSeat({
327-
autoProvision: true,
328-
});
329-
expect(result.error).toBeNull();
330-
expect(result.seat).toEqual(seat);
331-
});
332-
333303
it("surfaces retirement as a clear error on an explicit upgrade", async () => {
334304
const client = makeClient({
335305
createSeat: vi.fn().mockRejectedValue(new SeatProductRetiredError()),

packages/core/src/billing/seatService.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ import { ROOT_LOGGER, type RootLogger } from "@posthog/di/logger";
22
import { PLAN_FREE, PLAN_PRO, type SeatData } from "@posthog/shared";
33
import { inject, injectable } from "inversify";
44
import { SEAT_CLIENT, type SeatClient, type SeatLogger } from "./identifiers";
5-
import {
6-
type ClassifiedSeatError,
7-
classifySeatError,
8-
isSeatProductRetiredError,
9-
} from "./seatErrors";
5+
import { type ClassifiedSeatError, classifySeatError } from "./seatErrors";
106

117
export interface SeatOperationResult {
128
seat: SeatData | null;
@@ -47,11 +43,6 @@ function fail(classified: ClassifiedSeatError): SeatOperationResult {
4743
export class SeatService {
4844
private readonly logger: SeatLogger;
4945

50-
// The seat API 410s creation/upgrades/reactivation once seats are retired
51-
// (usage-based billing). Remembered so auth/onboarding fetches stop
52-
// re-attempting a provision that can never succeed.
53-
private seatProductRetired = false;
54-
5546
constructor(
5647
@inject(SEAT_CLIENT) private readonly client: SeatClient,
5748
@inject(ROOT_LOGGER) logger: RootLogger,
@@ -64,18 +55,13 @@ export class SeatService {
6455
autoProvision: boolean;
6556
}): Promise<SeatData | null> {
6657
let seat = await this.client.getMySeat({ best: options.best });
67-
if (!seat && options.autoProvision && !this.seatProductRetired) {
58+
if (!seat && options.autoProvision) {
6859
this.logger.info("No seat found, auto-provisioning free plan", {
6960
best: options.best,
7061
});
7162
try {
7263
seat = await this.client.createSeat(PLAN_FREE);
73-
} catch (error) {
74-
if (isSeatProductRetiredError(error)) {
75-
this.seatProductRetired = true;
76-
this.logger.info("Seat product retired; skipping auto-provision");
77-
return null;
78-
}
64+
} catch {
7965
this.logger.info("Auto-provision failed, re-fetching seat");
8066
seat = await this.client.getMySeat({ best: options.best });
8167
}
@@ -132,7 +118,6 @@ export class SeatService {
132118
this.client.invalidatePlanCache();
133119
return ok(seat, null, true);
134120
} catch (error) {
135-
if (isSeatProductRetiredError(error)) this.seatProductRetired = true;
136121
this.logger.error("provisionFreeSeat failed", error);
137122
return fail(classifySeatError(error));
138123
}
@@ -158,7 +143,6 @@ export class SeatService {
158143
this.client.invalidatePlanCache();
159144
return ok(seat, seat);
160145
} catch (error) {
161-
if (isSeatProductRetiredError(error)) this.seatProductRetired = true;
162146
return fail(classifySeatError(error));
163147
}
164148
}
@@ -184,7 +168,6 @@ export class SeatService {
184168
this.client.invalidatePlanCache();
185169
return ok(seat, seat);
186170
} catch (error) {
187-
if (isSeatProductRetiredError(error)) this.seatProductRetired = true;
188171
return fail(classifySeatError(error));
189172
}
190173
}

0 commit comments

Comments
 (0)