Skip to content

Commit

Permalink
feat(server): support team workspace subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo committed Dec 2, 2024
1 parent 661fbd0 commit 82be3b9
Show file tree
Hide file tree
Showing 26 changed files with 2,153 additions and 777 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- DropForeignKey
ALTER TABLE "user_invoices" DROP CONSTRAINT "user_invoices_user_id_fkey";

-- DropForeignKey
ALTER TABLE "user_subscriptions" DROP CONSTRAINT "user_subscriptions_user_id_fkey";

-- CreateTable
CREATE TABLE "subscriptions" (
"id" SERIAL NOT NULL,
"target_id" VARCHAR NOT NULL,
"plan" VARCHAR(20) NOT NULL,
"recurring" VARCHAR(20) NOT NULL,
"variant" VARCHAR(20),
"quantity" INTEGER NOT NULL DEFAULT 1,
"stripe_subscription_id" TEXT,
"stripe_schedule_id" VARCHAR,
"status" VARCHAR(20) NOT NULL,
"start" TIMESTAMPTZ(3) NOT NULL,
"end" TIMESTAMPTZ(3),
"next_bill_at" TIMESTAMPTZ(3),
"canceled_at" TIMESTAMPTZ(3),
"trial_start" TIMESTAMPTZ(3),
"trial_end" TIMESTAMPTZ(3),
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,

CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "invoices" (
"stripe_invoice_id" TEXT NOT NULL,
"target_id" VARCHAR NOT NULL,
"currency" VARCHAR(3) NOT NULL,
"amount" INTEGER NOT NULL,
"status" VARCHAR(20) NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
"reason" VARCHAR,
"last_payment_error" TEXT,
"link" TEXT,

CONSTRAINT "invoices_pkey" PRIMARY KEY ("stripe_invoice_id")
);

-- CreateIndex
CREATE UNIQUE INDEX "subscriptions_stripe_subscription_id_key" ON "subscriptions"("stripe_subscription_id");

-- CreateIndex
CREATE UNIQUE INDEX "subscriptions_target_id_plan_key" ON "subscriptions"("target_id", "plan");

-- CreateIndex
CREATE INDEX "invoices_target_id_idx" ON "invoices"("target_id");
196 changes: 122 additions & 74 deletions packages/backend/server/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ model User {
registered Boolean @default(true)
features UserFeature[]
customer UserStripeCustomer?
subscriptions UserSubscription[]
invoices UserInvoice[]
userStripeCustomer UserStripeCustomer?
workspacePermissions WorkspaceUserPermission[]
pagePermissions WorkspacePageUserPermission[]
connectedAccounts ConnectedAccount[]
Expand Down Expand Up @@ -318,77 +316,6 @@ model SnapshotHistory {
@@map("snapshot_histories")
}

model UserStripeCustomer {
userId String @id @map("user_id") @db.VarChar
stripeCustomerId String @unique @map("stripe_customer_id") @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_stripe_customers")
}

model UserSubscription {
id Int @id @default(autoincrement()) @db.Integer
userId String @map("user_id") @db.VarChar
plan String @db.VarChar(20)
// yearly/monthly/lifetime
recurring String @db.VarChar(20)
// onetime subscription or anything else
variant String? @db.VarChar(20)
// subscription.id, null for linefetime payment or one time payment subscription
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
// subscription.status, active/past_due/canceled/unpaid...
status String @db.VarChar(20)
// subscription.current_period_start
start DateTime @map("start") @db.Timestamptz(3)
// subscription.current_period_end, null for lifetime payment
end DateTime? @map("end") @db.Timestamptz(3)
// subscription.billing_cycle_anchor
nextBillAt DateTime? @map("next_bill_at") @db.Timestamptz(3)
// subscription.canceled_at
canceledAt DateTime? @map("canceled_at") @db.Timestamptz(3)
// subscription.trial_start
trialStart DateTime? @map("trial_start") @db.Timestamptz(3)
// subscription.trial_end
trialEnd DateTime? @map("trial_end") @db.Timestamptz(3)
stripeScheduleId String? @map("stripe_schedule_id") @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, plan])
@@map("user_subscriptions")
}

model UserInvoice {
id Int @id @default(autoincrement()) @db.Integer
userId String @map("user_id") @db.VarChar
stripeInvoiceId String @unique @map("stripe_invoice_id")
currency String @db.VarChar(3)
// CNY 12.50 stored as 1250
amount Int @db.Integer
status String @db.VarChar(20)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
// billing reason
reason String? @db.VarChar
lastPaymentError String? @map("last_payment_error") @db.Text
// stripe hosted invoice link
link String? @db.Text
// @deprecated
plan String? @db.VarChar(20)
// @deprecated
recurring String? @db.VarChar(20)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("user_invoices")
}

enum AiPromptRole {
system
assistant
Expand Down Expand Up @@ -503,3 +430,124 @@ model RuntimeConfig {
@@unique([module, key])
@@map("app_runtime_settings")
}

model DeprecatedUserSubscription {
id Int @id @default(autoincrement()) @db.Integer
userId String @map("user_id") @db.VarChar
plan String @db.VarChar(20)
// yearly/monthly/lifetime
recurring String @db.VarChar(20)
// onetime subscription or anything else
variant String? @db.VarChar(20)
// subscription.id, null for lifetime payment or one time payment subscription
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
// subscription.status, active/past_due/canceled/unpaid...
status String @db.VarChar(20)
// subscription.current_period_start
start DateTime @map("start") @db.Timestamptz(3)
// subscription.current_period_end, null for lifetime payment
end DateTime? @map("end") @db.Timestamptz(3)
// subscription.billing_cycle_anchor
nextBillAt DateTime? @map("next_bill_at") @db.Timestamptz(3)
// subscription.canceled_at
canceledAt DateTime? @map("canceled_at") @db.Timestamptz(3)
// subscription.trial_start
trialStart DateTime? @map("trial_start") @db.Timestamptz(3)
// subscription.trial_end
trialEnd DateTime? @map("trial_end") @db.Timestamptz(3)
stripeScheduleId String? @map("stripe_schedule_id") @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@unique([userId, plan])
@@map("user_subscriptions")
}

model DeprecatedUserInvoice {
id Int @id @default(autoincrement()) @db.Integer
userId String @map("user_id") @db.VarChar
stripeInvoiceId String @unique @map("stripe_invoice_id")
currency String @db.VarChar(3)
// CNY 12.50 stored as 1250
amount Int @db.Integer
status String @db.VarChar(20)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
// billing reason
reason String? @db.VarChar
lastPaymentError String? @map("last_payment_error") @db.Text
// stripe hosted invoice link
link String? @db.Text
// @deprecated
plan String? @db.VarChar(20)
// @deprecated
recurring String? @db.VarChar(20)
@@index([userId])
@@map("user_invoices")
}

model UserStripeCustomer {
userId String @id @map("user_id") @db.VarChar
stripeCustomerId String @unique @map("stripe_customer_id") @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_stripe_customers")
}

model Subscription {
id Int @id @default(autoincrement()) @db.Integer
targetId String @map("target_id") @db.VarChar
plan String @db.VarChar(20)
// yearly/monthly/lifetime
recurring String @db.VarChar(20)
// onetime subscription or anything else
variant String? @db.VarChar(20)
quantity Int @default(1) @db.Integer
// subscription.id, null for lifetime payment or one time payment subscription
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
// stripe schedule id
stripeScheduleId String? @map("stripe_schedule_id") @db.VarChar
// subscription.status, active/past_due/canceled/unpaid...
status String @db.VarChar(20)
// subscription.current_period_start
start DateTime @map("start") @db.Timestamptz(3)
// subscription.current_period_end, null for lifetime payment
end DateTime? @map("end") @db.Timestamptz(3)
// subscription.billing_cycle_anchor
nextBillAt DateTime? @map("next_bill_at") @db.Timestamptz(3)
// subscription.canceled_at
canceledAt DateTime? @map("canceled_at") @db.Timestamptz(3)
// subscription.trial_start
trialStart DateTime? @map("trial_start") @db.Timestamptz(3)
// subscription.trial_end
trialEnd DateTime? @map("trial_end") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@unique([targetId, plan])
@@map("subscriptions")
}

model Invoice {
stripeInvoiceId String @id @map("stripe_invoice_id")
targetId String @map("target_id") @db.VarChar
currency String @db.VarChar(3)
// CNY 12.50 stored as 1250
amount Int @db.Integer
status String @db.VarChar(20)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
// billing reason
reason String? @db.VarChar
lastPaymentError String? @map("last_payment_error") @db.Text
// stripe hosted invoice link
link String? @db.Text
@@index([targetId])
@@map("invoices")
}
12 changes: 0 additions & 12 deletions packages/backend/server/src/core/user/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Int,
Mutation,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { PrismaClient } from '@prisma/client';
Expand Down Expand Up @@ -37,7 +36,6 @@ import {
@Resolver(() => UserType)
export class UserResolver {
constructor(
private readonly prisma: PrismaClient,
private readonly storage: AvatarStorage,
private readonly users: UserService
) {}
Expand Down Expand Up @@ -72,16 +70,6 @@ export class UserResolver {
};
}

@ResolveField(() => Int, {
name: 'invoiceCount',
description: 'Get user invoice count',
})
async invoiceCount(@CurrentUser() user: CurrentUser) {
return this.prisma.userInvoice.count({
where: { userId: user.id },
});
}

@Mutation(() => UserType, {
name: 'uploadAvatar',
description: 'Upload user avatar',
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/server/src/core/workspaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ import {
})
export class WorkspaceModule {}

export type { InvitationType, WorkspaceType } from './types';
export { InvitationType, WorkspaceType } from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PrismaClient } from '@prisma/client';

import { loop } from './utils/loop';

export class UniversalSubscription1733125339942 {
// do the migration
static async up(db: PrismaClient) {
await loop(async (offset, take) => {
const oldSubscriptions = await db.deprecatedUserSubscription.findMany({
skip: offset,
take,
});

await db.subscription.createMany({
data: oldSubscriptions.map(s => ({
targetId: s.userId,
...s,
})),
});

return oldSubscriptions.length;
}, 50);
}

// revert the migration
static async down(_db: PrismaClient) {
// noop
}
}
2 changes: 2 additions & 0 deletions packages/backend/server/src/fundamentals/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ export class ConfigModule {
};
};
}

export { Runtime };
25 changes: 25 additions & 0 deletions packages/backend/server/src/fundamentals/error/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,15 +406,28 @@ export const USER_FRIENDLY_ERRORS = {
},

// Subscription Errors
unsupported_subscription_plan: {
type: 'invalid_input',
args: { plan: 'string' },
message: ({ plan }) => `Unsupported subscription plan: ${plan}.`,
},
failed_to_checkout: {
type: 'internal_server_error',
message: 'Failed to create checkout session.',
},
invalid_checkout_parameters: {
type: 'invalid_input',
message: 'Invalid checkout parameters provided.',
},
subscription_already_exists: {
type: 'resource_already_exists',
args: { plan: 'string' },
message: ({ plan }) => `You have already subscribed to the ${plan} plan.`,
},
invalid_subscription_parameters: {
type: 'invalid_input',
message: 'Invalid subscription parameters provided.',
},
subscription_not_exists: {
type: 'resource_not_found',
args: { plan: 'string' },
Expand All @@ -424,6 +437,10 @@ export const USER_FRIENDLY_ERRORS = {
type: 'action_forbidden',
message: 'Your subscription has already been canceled.',
},
subscription_has_not_been_canceled: {
type: 'action_forbidden',
message: 'Your subscription has not been canceled.',
},
subscription_expired: {
type: 'action_forbidden',
message: 'Your subscription has expired.',
Expand All @@ -447,6 +464,14 @@ export const USER_FRIENDLY_ERRORS = {
type: 'action_forbidden',
message: 'You cannot update an onetime payment subscription.',
},
workspace_id_required_for_team_subscription: {
type: 'invalid_input',
message: 'A workspace is required to checkout for team subscription.',
},
workspace_id_required_to_update_team_subscription: {
type: 'invalid_input',
message: 'Workspace id is required to update team subscription.',
},

// Copilot errors
copilot_session_not_found: {
Expand Down
Loading

0 comments on commit 82be3b9

Please sign in to comment.