Skip to content

Commit e4b98b1

Browse files
committed
feat: add workspace subscription
1 parent fa1ff3b commit e4b98b1

File tree

11 files changed

+180
-18
lines changed

11 files changed

+180
-18
lines changed

src/client/routes/playground.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ function PageComponent() {
8686

8787
export const BillingPlayground: React.FC = React.memo(() => {
8888
const checkoutMutation = trpc.billing.checkout.useMutation({
89-
onSuccess: defaultSuccessHandler,
9089
onError: defaultErrorHandler,
9190
});
9291
const changePlanMutation = trpc.billing.changePlan.useMutation({

src/server/model/billing/index.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import {
66
} from '@lemonsqueezy/lemonsqueezy.js';
77
import { env } from '../../utils/env.js';
88
import { prisma } from '../_client.js';
9+
import { WorkspaceSubscriptionTier } from '@prisma/client';
910

10-
if (env.billing.lemonSqueezy.apiKey) {
11+
export const billingAvailable = Boolean(env.billing.lemonSqueezy.apiKey);
12+
13+
if (billingAvailable) {
1114
lemonSqueezySetup({
1215
apiKey: env.billing.lemonSqueezy.apiKey,
1316
onError: (error) => console.error('Error!', error),
@@ -25,12 +28,28 @@ export function getTierNameByvariantId(variantId: string) {
2528
);
2629

2730
if (!tierName) {
28-
throw 'Unknown';
31+
throw new Error('Unknown Tier Name');
2932
}
3033

3134
return tierName;
3235
}
3336

37+
export function getTierEnumByVariantId(
38+
variantId: string
39+
): WorkspaceSubscriptionTier {
40+
const name = getTierNameByvariantId(variantId);
41+
42+
if (name === 'free') {
43+
return WorkspaceSubscriptionTier.FREE;
44+
} else if (name === 'pro') {
45+
return WorkspaceSubscriptionTier.PRO;
46+
} else if (name === 'team') {
47+
return WorkspaceSubscriptionTier.TEAM;
48+
}
49+
50+
return WorkspaceSubscriptionTier.FREE; // not cool, fallback to free
51+
}
52+
3453
export function checkIsValidProduct(storeId: string, variantId: string) {
3554
if (String(storeId) !== env.billing.lemonSqueezy.storeId) {
3655
return false;
@@ -104,6 +123,26 @@ export async function createCheckoutBilling(
104123
return checkoutData;
105124
}
106125

126+
export async function updateWorkspaceSubscription(
127+
workspaceId: string,
128+
subscriptionTier: WorkspaceSubscriptionTier
129+
) {
130+
const res = await prisma.workspaceSubscription.upsert({
131+
where: {
132+
workspaceId,
133+
},
134+
create: {
135+
workspaceId,
136+
tier: subscriptionTier,
137+
},
138+
update: {
139+
tier: subscriptionTier,
140+
},
141+
});
142+
143+
return res;
144+
}
145+
107146
export async function changeSubscription(
108147
workspaceId: string,
109148
subscriptionTier: SubscriptionTierType

src/server/model/billing/limit.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { TierType } from './types.js';
2+
3+
interface TierLimit {
4+
maxWebsiteCount: number;
5+
maxWebsiteEventCount: number;
6+
maxMonitorExecutionCount: number;
7+
maxSurveyCount: number;
8+
maxFeedChannelCount: number;
9+
maxFeedEventCount: number;
10+
}
11+
12+
/**
13+
* Limit, Every month
14+
*/
15+
export function getTierLimit(tier: TierType): TierLimit {
16+
if (tier === 'free') {
17+
return {
18+
maxWebsiteCount: 3,
19+
maxWebsiteEventCount: 100_000,
20+
maxMonitorExecutionCount: 100_000,
21+
maxSurveyCount: 3,
22+
maxFeedChannelCount: 3,
23+
maxFeedEventCount: 10_000,
24+
};
25+
}
26+
27+
if (tier === 'pro') {
28+
return {
29+
maxWebsiteCount: 10,
30+
maxWebsiteEventCount: 1_000_000,
31+
maxMonitorExecutionCount: 1_000_000,
32+
maxSurveyCount: 20,
33+
maxFeedChannelCount: 20,
34+
maxFeedEventCount: 100_000,
35+
};
36+
}
37+
38+
if (tier === 'team') {
39+
return {
40+
maxWebsiteCount: -1,
41+
maxWebsiteEventCount: 20_000_000,
42+
maxMonitorExecutionCount: 20_000_000,
43+
maxSurveyCount: -1,
44+
maxFeedChannelCount: -1,
45+
maxFeedEventCount: 1_000_000,
46+
};
47+
}
48+
49+
return {
50+
maxWebsiteCount: -1,
51+
maxWebsiteEventCount: -1,
52+
maxMonitorExecutionCount: -1,
53+
maxSurveyCount: -1,
54+
maxFeedChannelCount: -1,
55+
maxFeedEventCount: -1,
56+
};
57+
}

src/server/model/billing/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type TierType = 'free' | 'pro' | 'team' | 'unlimited';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- CreateEnum
2+
CREATE TYPE "WorkspaceSubscriptionTier" AS ENUM ('FREE', 'PRO', 'TEAM', 'UNLIMITED');
3+
4+
-- CreateTable
5+
CREATE TABLE "WorkspaceSubscription" (
6+
"id" VARCHAR(30) NOT NULL,
7+
"workspaceId" VARCHAR(30) NOT NULL,
8+
"tier" "WorkspaceSubscriptionTier" NOT NULL DEFAULT 'FREE',
9+
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10+
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
11+
12+
CONSTRAINT "WorkspaceSubscription_pkey" PRIMARY KEY ("id")
13+
);
14+
15+
-- CreateIndex
16+
CREATE UNIQUE INDEX "WorkspaceSubscription_workspaceId_key" ON "WorkspaceSubscription"("workspaceId");
17+
18+
-- AddForeignKey
19+
ALTER TABLE "WorkspaceSubscription" ADD CONSTRAINT "WorkspaceSubscription_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
20+
21+
22+
-- Set admin workspace to UNLIMITED
23+
INSERT INTO "WorkspaceSubscription" ("id", "workspaceId", "tier", "createdAt", "updatedAt") VALUES ('cm1yqv4xd002154qnfhzg9i5d', 'clnzoxcy10001vy2ohi4obbi0', 'UNLIMITED', '2024-10-07 08:22:45.169+00', '2024-10-07 08:22:45.169+00');

src/server/prisma/schema.prisma

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ model Workspace {
9999
createdAt DateTime @default(now()) @db.Timestamptz(6)
100100
updatedAt DateTime @updatedAt @db.Timestamptz(6)
101101
102+
subscription WorkspaceSubscription?
103+
102104
users WorkspacesOnUsers[]
103105
websites Website[]
104106
notifications Notification[]
@@ -128,6 +130,24 @@ model WorkspacesOnUsers {
128130
@@index([workspaceId])
129131
}
130132

133+
enum WorkspaceSubscriptionTier {
134+
FREE
135+
PRO
136+
TEAM
137+
138+
UNLIMITED // This type should only use for special people or admin workspace
139+
}
140+
141+
model WorkspaceSubscription {
142+
id String @id() @default(cuid()) @db.VarChar(30)
143+
workspaceId String @unique @db.VarChar(30)
144+
tier WorkspaceSubscriptionTier @default(FREE) // free, pro, team
145+
createdAt DateTime @default(now()) @db.Timestamptz(6)
146+
updatedAt DateTime @updatedAt @db.Timestamptz(6)
147+
148+
workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade)
149+
}
150+
131151
model LemonSqueezySubscription {
132152
subscriptionId String @id @unique
133153
workspaceId String @unique @db.VarChar(30)

src/server/prisma/zod/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from "./session.js"
55
export * from "./verificationtoken.js"
66
export * from "./workspace.js"
77
export * from "./workspacesonusers.js"
8+
export * from "./workspacesubscription.js"
89
export * from "./lemonsqueezysubscription.js"
910
export * from "./lemonsqueezywebhookevent.js"
1011
export * from "./website.js"

src/server/prisma/zod/lemonsqueezytransaction.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/server/prisma/zod/workspace.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as z from "zod"
22
import * as imports from "./schemas/index.js"
3-
import { CompleteWorkspacesOnUsers, RelatedWorkspacesOnUsersModelSchema, CompleteWebsite, RelatedWebsiteModelSchema, CompleteNotification, RelatedNotificationModelSchema, CompleteMonitor, RelatedMonitorModelSchema, CompleteMonitorStatusPage, RelatedMonitorStatusPageModelSchema, CompleteTelemetry, RelatedTelemetryModelSchema, CompleteWorkspaceDailyUsage, RelatedWorkspaceDailyUsageModelSchema, CompleteWorkspaceAuditLog, RelatedWorkspaceAuditLogModelSchema, CompleteSurvey, RelatedSurveyModelSchema, CompleteFeedChannel, RelatedFeedChannelModelSchema } from "./index.js"
3+
import { CompleteWorkspaceSubscription, RelatedWorkspaceSubscriptionModelSchema, CompleteWorkspacesOnUsers, RelatedWorkspacesOnUsersModelSchema, CompleteWebsite, RelatedWebsiteModelSchema, CompleteNotification, RelatedNotificationModelSchema, CompleteMonitor, RelatedMonitorModelSchema, CompleteMonitorStatusPage, RelatedMonitorStatusPageModelSchema, CompleteTelemetry, RelatedTelemetryModelSchema, CompleteWorkspaceDailyUsage, RelatedWorkspaceDailyUsageModelSchema, CompleteWorkspaceAuditLog, RelatedWorkspaceAuditLogModelSchema, CompleteSurvey, RelatedSurveyModelSchema, CompleteFeedChannel, RelatedFeedChannelModelSchema } from "./index.js"
44

55
// Helper schema for JSON fields
66
type Literal = boolean | number | string
@@ -25,6 +25,7 @@ export const WorkspaceModelSchema = z.object({
2525
})
2626

2727
export interface CompleteWorkspace extends z.infer<typeof WorkspaceModelSchema> {
28+
subscription?: CompleteWorkspaceSubscription | null
2829
users: CompleteWorkspacesOnUsers[]
2930
websites: CompleteWebsite[]
3031
notifications: CompleteNotification[]
@@ -43,6 +44,7 @@ export interface CompleteWorkspace extends z.infer<typeof WorkspaceModelSchema>
4344
* NOTE: Lazy required in case of potential circular dependencies within schema
4445
*/
4546
export const RelatedWorkspaceModelSchema: z.ZodSchema<CompleteWorkspace> = z.lazy(() => WorkspaceModelSchema.extend({
47+
subscription: RelatedWorkspaceSubscriptionModelSchema.nullish(),
4648
users: RelatedWorkspacesOnUsersModelSchema.array(),
4749
websites: RelatedWebsiteModelSchema.array(),
4850
notifications: RelatedNotificationModelSchema.array(),
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as z from "zod"
2+
import * as imports from "./schemas/index.js"
3+
import { WorkspaceSubscriptionTier } from "@prisma/client"
4+
import { CompleteWorkspace, RelatedWorkspaceModelSchema } from "./index.js"
5+
6+
export const WorkspaceSubscriptionModelSchema = z.object({
7+
id: z.string(),
8+
workspaceId: z.string(),
9+
tier: z.nativeEnum(WorkspaceSubscriptionTier),
10+
createdAt: z.date(),
11+
updatedAt: z.date(),
12+
})
13+
14+
export interface CompleteWorkspaceSubscription extends z.infer<typeof WorkspaceSubscriptionModelSchema> {
15+
workspace: CompleteWorkspace
16+
}
17+
18+
/**
19+
* RelatedWorkspaceSubscriptionModelSchema contains all relations on your model in addition to the scalars
20+
*
21+
* NOTE: Lazy required in case of potential circular dependencies within schema
22+
*/
23+
export const RelatedWorkspaceSubscriptionModelSchema: z.ZodSchema<CompleteWorkspaceSubscription> = z.lazy(() => WorkspaceSubscriptionModelSchema.extend({
24+
workspace: RelatedWorkspaceModelSchema,
25+
}))

0 commit comments

Comments
 (0)