feat: Updated plans and catalogue pages
This commit is contained in:
@@ -8,6 +8,9 @@
|
||||
* customer-facing and should be shown verbatim).
|
||||
*/
|
||||
|
||||
/* catalogue.ts imports only types from here, so this is not a cycle. */
|
||||
import { rowsForPlan } from "@/lib/catalogue";
|
||||
|
||||
export const API_BASE = (process.env.NEXT_PUBLIC_ADMIN_API_URL ?? "").replace(/\/$/, "");
|
||||
|
||||
export class NotConnected extends Error {
|
||||
@@ -176,6 +179,11 @@ export interface Plan {
|
||||
|
||||
export interface CatalogueRow {
|
||||
kind: "base" | "limit" | "feature";
|
||||
/* "plan" rows carry a deployment and tier and belong to that plan alone.
|
||||
* "shared" rows leave both empty and are sold by every paid plan, which is
|
||||
* why a price ID is typed once rather than four times. Read them through
|
||||
* rowsForPlan in lib/catalogue, never by filtering on deployment. */
|
||||
scope: "plan" | "shared";
|
||||
deployment: Deployment;
|
||||
tier: Tier;
|
||||
limit_key?: string;
|
||||
@@ -213,9 +221,7 @@ export function lineItemsFor(
|
||||
const env = opts.env;
|
||||
const plan = opts.plans.find((p) => p.deployment === deployment && p.tier === choice.tier);
|
||||
if (!plan) return [];
|
||||
const rows = opts.catalogue.filter(
|
||||
(r) => r.deployment === deployment && r.tier === choice.tier,
|
||||
);
|
||||
const rows = rowsForPlan(opts.catalogue, deployment, choice.tier);
|
||||
const priceOf = (r: CatalogueRow) => r.price_ids?.[env]?.[choice.term] ?? "";
|
||||
const base = plan.base_limits.max_servers;
|
||||
const items: { priceId: string; quantity: number }[] = [];
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { CatalogueRow, Deployment, Tier } from "@/lib/api";
|
||||
|
||||
/*
|
||||
* rowsForPlan is the TypeScript half of Go's models.CatalogueFor, and the two
|
||||
* must change together — the same shape of hazard as web/lib/targets.ts.
|
||||
*
|
||||
* A plan sells its own base row plus every shared add-on row. Shared rows leave
|
||||
* deployment and tier empty, so the filter this replaced — `r.deployment === dep
|
||||
* && r.tier === tier` — now returns a plan priced by its base fee and nothing
|
||||
* else. There were five copies of that filter; this is why it is a module.
|
||||
*/
|
||||
export function rowsForPlan(
|
||||
catalogue: CatalogueRow[],
|
||||
deployment: Deployment,
|
||||
tier: Tier,
|
||||
): CatalogueRow[] {
|
||||
return catalogue.filter(
|
||||
(r) => r.scope === "shared" || (r.deployment === deployment && r.tier === tier),
|
||||
);
|
||||
}
|
||||
|
||||
/* Every add-on a paid plan can be sold, in one list. The staff catalogue editor
|
||||
* shows these once; the purchase form reads them per plan through rowsForPlan. */
|
||||
export function sharedRows(catalogue: CatalogueRow[]): CatalogueRow[] {
|
||||
return catalogue.filter((r) => r.scope === "shared");
|
||||
}
|
||||
|
||||
/* The base fee rows, which are genuinely one per plan because each is its own
|
||||
* Paddle product at its own price. */
|
||||
export function planRows(catalogue: CatalogueRow[]): CatalogueRow[] {
|
||||
return catalogue.filter((r) => r.scope !== "shared");
|
||||
}
|
||||
|
||||
/* A stable identity for a row, used as a React key and as the draft key in the
|
||||
* staff editor. Mirrors the natural key the API addresses a row by. */
|
||||
export function rowKey(r: CatalogueRow): string {
|
||||
return [r.scope ?? "plan", r.deployment ?? "", r.tier ?? "", r.kind, r.limit_key ?? "", r.feature_key ?? ""].join("/");
|
||||
}
|
||||
Reference in New Issue
Block a user