Skip to content

Commit

Permalink
feat: billing verifications
Browse files Browse the repository at this point in the history
  • Loading branch information
chronark committed Oct 10, 2024
1 parent 4c61745 commit 7e3a75e
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- +goose up
CREATE MATERIALIZED VIEW verifications.key_verifications_per_month_mv_v1
TO verificatins.key_verifications_per_month_v1
TO verifications.key_verifications_per_month_v1
AS
SELECT
workspace_id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- +goose up
CREATE MATERIALIZED VIEW billing.billable_verifications_per_month_mv_v1
TO billing.billable_verifications_per_month_v1
AS
SELECT
workspace_id,
count(*) AS count,
toYear(time) AS year,
toMonth(time) AS month
FROM verifications.key_verifications_per_month_v1
WHERE outcome = 'VALID'
GROUP BY
workspace_id,
year,
month
;
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { getTenantId } from "@/lib/auth";
import { getLastUsed } from "@/lib/clickhouse";
import { getVerificationsPerDay, getVerificationsPerHour } from "@/lib/clickhouse";
import { getLatestVerifications } from "@/lib/clickhouse/latest_verifications";
import { and, db, eq, isNull, schema } from "@/lib/db";
import { formatNumber } from "@/lib/fmt";
import { getVerificationsPerDay, getVerificationsPerHour } from "@/lib/clickhouse";
import { cn } from "@/lib/utils";
import { BarChart, Minus } from "lucide-react";
import ms from "ms";
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/app/(app)/apis/[apiId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { getTenantId } from "@/lib/auth";
import { getActiveKeysPerDay, getActiveKeysPerHour, getActiveKeysPerMonth } from "@/lib/clickhouse";
import { getVerificationsPerDay, getVerificationsPerHour } from "@/lib/clickhouse";
import { and, db, eq, isNull, schema, sql } from "@/lib/db";
import { formatNumber } from "@/lib/fmt";
import { getVerificationsPerDay, getVerificationsPerHour } from "@/lib/clickhouse";
import { BarChart } from "lucide-react";
import { redirect } from "next/navigation";
import { type Interval, IntervalSelect } from "./select";
Expand Down
22 changes: 10 additions & 12 deletions apps/dashboard/app/(app)/banner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Banner } from "@/components/banner";
import { getBillableVerifications } from "@/lib/clickhouse";
import type { Workspace } from "@/lib/db";
import { verifications } from "@/lib/tinybird";
import { QUOTA } from "@unkey/billing";
import ms from "ms";
import Link from "next/link";
Expand All @@ -23,20 +23,18 @@ export const UsageBanner: React.FC<{ workspace: Workspace | undefined }> = async
const fmt = new Intl.NumberFormat("en-US").format;

if (workspace.plan === "free") {
const [usedVerifications] = await Promise.all([
verifications({
workspaceId: workspace.id,
year,
month,
}).then((res) => res.data.at(0)?.success ?? 0),
]);
const billableVerifications = await getBillableVerifications({
workspaceId: workspace.id,
year,
month,
});

if (usedVerifications >= QUOTA.free.maxVerifications) {
if (billableVerifications >= QUOTA.free.maxVerifications) {
return (
<Banner variant="alert">
<p className="text-xs text-center">
You have exceeded your plan&apos;s monthly usage limit for verifications:{" "}
<strong>{fmt(usedVerifications)}</strong> /{" "}
<strong>{fmt(billableVerifications)}</strong> /{" "}
<strong>{fmt(QUOTA.free.maxVerifications)}</strong>.{" "}
<Link href="/settings/billing" className="underline">
Upgrade your plan
Expand All @@ -58,8 +56,8 @@ export const UsageBanner: React.FC<{ workspace: Workspace | undefined }> = async
{workspace.trialEnds.getTime() <= Date.now()
? "Your trial has expired."
: `Your trial expires in ${ms(workspace.trialEnds.getTime() - Date.now(), {
long: true,
})}.`}{" "}
long: true,
})}.`}{" "}
<Link href="/settings/billing" className="underline">
Add a payment method
</Link>
Expand Down
11 changes: 6 additions & 5 deletions apps/dashboard/app/(app)/settings/billing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {
} from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getTenantId } from "@/lib/auth";
import { getBillableVerifications } from "@/lib/clickhouse";
import { type Workspace, db } from "@/lib/db";
import { stripeEnv } from "@/lib/env";
import { ratelimits, verifications } from "@/lib/tinybird";
import { ratelimits } from "@/lib/tinybird";
import { cn } from "@/lib/utils";
import { type BillingTier, QUOTA, calculateTieredPrices } from "@unkey/billing";
import { Check, ExternalLink } from "lucide-react";
Expand Down Expand Up @@ -55,11 +56,11 @@ const FreeUsage: React.FC<{ workspace: Workspace }> = async ({ workspace }) => {
const year = t.getUTCFullYear();
const month = t.getUTCMonth() + 1;

const usedVerifications = await verifications({
const usedVerifications = await getBillableVerifications({
workspaceId: workspace.id,
year,
month,
}).then((res) => res.data.at(0)?.success ?? 0);
});

return (
<Card>
Expand Down Expand Up @@ -184,11 +185,11 @@ const PaidUsage: React.FC<{ workspace: Workspace }> = async ({ workspace }) => {
const month = startOfMonth.getUTCMonth() + 1;

const [usedVerifications, usedRatelimits] = await Promise.all([
verifications({
getBillableVerifications({
workspaceId: workspace.id,
year,
month,
}).then((res) => res.data.at(0)?.success ?? 0),
}),
ratelimits({
workspaceId: workspace.id,
year,
Expand Down
6 changes: 3 additions & 3 deletions apps/dashboard/lib/charts/sparkline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function SparkLineInner<T extends Datum>({
startDate: dates[times.indexOf(Math.min(...times))],
endDate: dates[times.indexOf(Math.max(...times))],
};
}, data);
}, [data]);

const { minY, maxY } = useMemo(() => {
const values = series
Expand All @@ -73,7 +73,7 @@ function SparkLineInner<T extends Datum>({
minY: Math.min(...values),
maxY: Math.max(...values),
};
}, [data, series, padding?.bottom, padding?.top]);
}, [data, series]);

const { yScale, xScale } = useMemo(() => {
const rangeY = maxY - minY;
Expand All @@ -89,7 +89,7 @@ function SparkLineInner<T extends Datum>({
range: [0, width],
}),
};
}, [startDate, endDate, minY, maxY, height, width, margin]);
}, [startDate, endDate, minY, maxY, height, width]);

const chartContext: ChartContextType<T> = {
width,
Expand Down
36 changes: 36 additions & 0 deletions apps/dashboard/lib/clickhouse/billing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { z } from "zod";
import { clickhouse } from "./client";

// get the billable verifications for a workspace in a specific month.
// month is not zero-indexed -> January = 1
export async function getBillableVerifications(args: {
workspaceId: string;
year: number;
month: number;
}): Promise<number> {
const query = clickhouse.query({
query: `
SELECT
sum(count) as count
FROM billing.billable_verifications_per_month_v1
WHERE workspace_id = {workspaceId: String}
AND year = {year: Int64}
AND month = {month: Int64}
GROUP BY workspace_id, year, month
`,
params: z.object({
workspaceId: z.string(),
year: z.number().int(),
month: z.number().int().min(1).max(12),
}),
schema: z.object({
count: z.number().int(),
}),
});

const res = await query(args);
if (!res) {
return 0;
}
return res.at(0)?.count ?? 0;
}
2 changes: 1 addition & 1 deletion apps/dashboard/lib/clickhouse/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Clickhouse, Client, Noop } from "@unkey/clickhouse-zod";
import { env } from "@/lib/env";
import { type Clickhouse, Client, Noop } from "@unkey/clickhouse-zod";

const { CLICKHOUSE_URL } = env();

Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/lib/clickhouse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./latest_verifications";
export * from "./active_keys";
export * from "./last_used";
export * from "./verifications";
export * from "./billing";
34 changes: 17 additions & 17 deletions apps/dashboard/lib/tinybird.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,23 +174,23 @@ export type UnkeyAuditLog = {
};
resources: Array<{
type:
| "key"
| "api"
| "workspace"
| "role"
| "permission"
| "keyAuth"
| "vercelBinding"
| "vercelIntegration"
| "ratelimitNamespace"
| "ratelimitOverride"
| "gateway"
| "llmGateway"
| "webhook"
| "reporter"
| "secret"
| "identity"
| "auditLogBucket";
| "key"
| "api"
| "workspace"
| "role"
| "permission"
| "keyAuth"
| "vercelBinding"
| "vercelIntegration"
| "ratelimitNamespace"
| "ratelimitOverride"
| "gateway"
| "llmGateway"
| "webhook"
| "reporter"
| "secret"
| "identity"
| "auditLogBucket";

id: string;
meta?: Record<string, string | number | boolean | null>;
Expand Down
2 changes: 1 addition & 1 deletion internal/clickhouse-zod/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class Client implements Clickhouse {
return async (params: z.input<TIn>): Promise<z.output<TOut>[]> => {
const res = await this.client.query({
query: req.query,
query_params: req.params?.safeParse(params),
query_params: req.params?.parse(params),
format: "JSONEachRow",
});
const rows = await res.json();
Expand Down
12 changes: 1 addition & 11 deletions tools/migrate/auditlog-import.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
import {
type AuditLog,
type AuditLogTarget,
and,
asc,
eq,
gt,
isNotNull,
mysqlDrizzle,
schema,
} from "@unkey/db";
import type { AuditLog, AuditLogTarget } from "@unkey/db";
import { mysqlDrizzle, schema } from "@unkey/db";
import { newId } from "@unkey/id";
import ms from "ms";
Expand Down

0 comments on commit 7e3a75e

Please sign in to comment.