add sponsorship, stripe
This commit is contained in:
parent
5b6d8cccb7
commit
bfe3d9573c
@ -38,3 +38,76 @@ export default tseslint.config(
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// import config from 'eslint-config-ksv741/ts-base'
|
||||
//
|
||||
// export default [
|
||||
// ...config,
|
||||
// {
|
||||
// name: 'my-custom',
|
||||
// files: [
|
||||
// '**/*.js',
|
||||
// '**/*.ts'
|
||||
// ],
|
||||
// rules: {
|
||||
// 'func-style': ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
// 'import/order': [
|
||||
// 'error', {
|
||||
// pathGroups: [
|
||||
// {
|
||||
// pattern: '__spec__/**',
|
||||
// group: 'builtin',
|
||||
// position: 'before',
|
||||
// },
|
||||
// {
|
||||
// pattern: 'apps/**',
|
||||
// group: 'internal',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: 'pages/**',
|
||||
// group: 'internal',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: 'widgets/**',
|
||||
// group: 'internal',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: 'features/**',
|
||||
// group: 'internal',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: 'entities/**',
|
||||
// group: 'internal',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: 'shared/**',
|
||||
// group: 'internal',
|
||||
// position: 'after',
|
||||
// },
|
||||
// ],
|
||||
// distinctGroup: true,
|
||||
// groups: [
|
||||
// 'builtin',
|
||||
// 'external',
|
||||
// 'internal',
|
||||
// 'parent',
|
||||
// 'sibling',
|
||||
// 'object',
|
||||
// 'index',
|
||||
// 'type',
|
||||
// ],
|
||||
// 'newlines-between': 'always',
|
||||
// alphabetize: {
|
||||
// order: 'asc',
|
||||
// caseInsensitive: true,
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
// ];
|
||||
|
||||
@ -11,7 +11,8 @@
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||
"lint:inspect": "npx @eslint/config-inspector@latest",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
@ -54,6 +55,7 @@
|
||||
"i18n-iso-countries": "^7.14.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"livekit-server-sdk": "1.2.7",
|
||||
"nestjs-telegraf": "^2.9.1",
|
||||
"otpauth": "^9.4.0",
|
||||
"prisma": "^6.10.1",
|
||||
"qrcode": "^1.5.4",
|
||||
@ -62,7 +64,9 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.34.2"
|
||||
"sharp": "^0.34.2",
|
||||
"stripe": "^18.3.0",
|
||||
"telegraf": "^4.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@ -83,6 +87,7 @@
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-ksv741": "0.2.0",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
|
||||
@ -94,6 +94,10 @@ model User {
|
||||
notifications Notification[]
|
||||
notificationSettings NotificationSettings?
|
||||
telegramId String? @unique @map("telegram_id")
|
||||
transactions Transaction[]
|
||||
sponsorshipPlans SponsorshipPlan[]
|
||||
sponsorshipSubscriptions SponsorshipSubscription[] @relation(name: "sponsorship_subscriptions")
|
||||
sponsors SponsorshipSubscription[] @relation(name: "sponsors")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@ -126,6 +130,51 @@ model Token {
|
||||
@@map("tokens")
|
||||
}
|
||||
|
||||
model Transaction {
|
||||
id String @id @default(uuid())
|
||||
amount Float
|
||||
currency String
|
||||
stripeSubscriptionId String? @map("stripe_subscription_id")
|
||||
status TransactionStatus @default(PENDING)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @map("user_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("transactions")
|
||||
}
|
||||
|
||||
model SponsorshipPlan {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String?
|
||||
price Float
|
||||
stripeProductId String @map("stripe_product_id")
|
||||
stripePlanId String @map("stripe_plan_id")
|
||||
channel User? @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||
channelId String? @map("channel_id")
|
||||
sponsorshipSubscriptions SponsorshipSubscription[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("sponsorship_plans")
|
||||
}
|
||||
|
||||
model SponsorshipSubscription {
|
||||
id String @id @default(uuid())
|
||||
expiresAt DateTime @map("expires_at")
|
||||
plan SponsorshipPlan? @relation(fields: [planId], references: [id], onDelete: Cascade)
|
||||
planId String? @map("plan_id")
|
||||
user User? @relation(name: "sponsorship_subscriptions", fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String? @map("user_id")
|
||||
channel User? @relation(name: "sponsors", fields: [channelId], references: [id], onDelete: Cascade)
|
||||
channelId String? @map("channel_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("sponsorship_subscriptions")
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id String @id @default(uuid())
|
||||
text String
|
||||
@ -169,3 +218,12 @@ enum TokenType {
|
||||
|
||||
@@map("token_types")
|
||||
}
|
||||
|
||||
enum TransactionStatus {
|
||||
PENDING
|
||||
SUCCESS
|
||||
FAILED
|
||||
EXPIRED
|
||||
|
||||
@@map("transaction_statuses")
|
||||
}
|
||||
|
||||
12
backend/src/core/config/stripe.config.ts
Normal file
12
backend/src/core/config/stripe.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type'
|
||||
import { ProcessEnv } from '@/src/shared/types/env'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
|
||||
export function getStripeConfig(configService: ConfigService<ProcessEnv>): TypeStripeOptions {
|
||||
return {
|
||||
config: {
|
||||
apiVersion: '2025-06-30.basil',
|
||||
},
|
||||
apiKey: configService.getOrThrow('STRIPE_SECRET_KEY'),
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { getGraphQLConfig } from '@/src/core/config/graphql.config'
|
||||
import { getLiveKitConfig } from '@/src/core/config/livekit.config'
|
||||
import { getStripeConfig } from '@/src/core/config/stripe.config';
|
||||
import { AccountModule } from '@/src/module/auth/account/account.module'
|
||||
import { DeactivateModule } from '@/src/module/auth/deactivate/deactivate.module'
|
||||
import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module'
|
||||
@ -15,8 +16,12 @@ import { FollowModule } from '@/src/module/follow/follow.module'
|
||||
import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module'
|
||||
import { MailModule } from '@/src/module/libs/mail/mail.module'
|
||||
import { StorageModule } from '@/src/module/libs/storage/storage.module'
|
||||
import { StripeModule } from '@/src/module/libs/stripe/stripe.module';
|
||||
import { TelegramModule } from '@/src/module/libs/telegram/telegram.module'
|
||||
import { NotificationModule } from '@/src/module/notification/notification.module'
|
||||
import { PlanModule } from '@/src/module/sponsorship/plan/plan.module';
|
||||
import { SubscriptionModule } from '@/src/module/sponsorship/subscription/subscription.module';
|
||||
import { TransactionModule } from '@/src/module/sponsorship/transaction/transaction.module';
|
||||
import { IngressModule } from '@/src/module/stream/ingress/ingress.module'
|
||||
import { StreamModule } from '@/src/module/stream/stream.module'
|
||||
import { WebhookModule } from '@/src/module/webhook/webhook.module'
|
||||
@ -51,6 +56,11 @@ import { RedisModule } from './redis/redis.module'
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
TelegramModule,
|
||||
StripeModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: getStripeConfig,
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
AccountModule,
|
||||
SessionModule,
|
||||
VerificationModule,
|
||||
@ -66,6 +76,9 @@ import { RedisModule } from './redis/redis.module'
|
||||
FollowModule,
|
||||
ChannelModule,
|
||||
NotificationModule,
|
||||
PlanModule,
|
||||
TransactionModule,
|
||||
SubscriptionModule,
|
||||
],
|
||||
})
|
||||
export class CoreModule {}
|
||||
|
||||
@ -63,6 +63,12 @@ type ChatMessageModel {
|
||||
userId: ID!
|
||||
}
|
||||
|
||||
input CreatePlanInput {
|
||||
description: String
|
||||
price: Float!
|
||||
title: String!
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
email: String!
|
||||
name: String!
|
||||
@ -129,6 +135,10 @@ input LoginInput {
|
||||
pin: String
|
||||
}
|
||||
|
||||
type MakePaymentModel {
|
||||
url: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
changeChatSettings(data: ChangeChatSettingsInput!): StreamModel!
|
||||
changeEmail(data: ChangeEmailInput!): UserModel!
|
||||
@ -141,6 +151,7 @@ type Mutation {
|
||||
clearSessionCookie: Boolean!
|
||||
createIngress(ingressType: Float!): Boolean!
|
||||
createSocialLink(data: SocialLinkInput!): SocialLinkModel!
|
||||
createSponsorshipPlan(data: CreatePlanInput!): PlanModel!
|
||||
createUser(data: CreateUserInput!): UserModel!
|
||||
deactivateAccount(data: DeactivateAccountInput!): AuthModel!
|
||||
disableTotp: Boolean!
|
||||
@ -149,9 +160,11 @@ type Mutation {
|
||||
generateStreamToken(data: GenerateStreamTokenInput!): GenerateTokenModel!
|
||||
loginUser(data: LoginInput!): AuthModel!
|
||||
logoutUser: Boolean!
|
||||
makePayment(planId: String!): MakePaymentModel!
|
||||
removeProfileAvatar: Boolean!
|
||||
removeSession(id: String!): Boolean!
|
||||
removeSocialLink(id: String!): Boolean!
|
||||
removeSponsorshipPlan(planId: String!): PlanModel!
|
||||
removeStreamThumbnail: Boolean!
|
||||
reorderSocialLink(list: [SocialLinkOrderInput!]!): Boolean!
|
||||
resetPassword(data: ResetPasswordInput!): Boolean!
|
||||
@ -197,6 +210,19 @@ enum NotificationType {
|
||||
VERIFIED_CHANNEL
|
||||
}
|
||||
|
||||
type PlanModel {
|
||||
channel: UserModel!
|
||||
channelId: ID!
|
||||
createdAt: DateTime!
|
||||
description: String
|
||||
id: ID!
|
||||
price: Float!
|
||||
stripePlanId: ID!
|
||||
stripeProductId: ID!
|
||||
title: String!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
findAllCategories: [CategoryModel!]!
|
||||
findAllStreams(filters: FilterInput!): [StreamModel!]!
|
||||
@ -207,6 +233,9 @@ type Query {
|
||||
findMessagesByStream(streamId: String!): [ChatMessageModel!]!
|
||||
findMyFollowers: [FollowModel!]!
|
||||
findMyFollowings: [FollowModel!]!
|
||||
findMySponsors: [SubscriptionModel!]!
|
||||
findMySponsorshipPlans: [PlanModel!]!
|
||||
findMyTransactions: [TransactionModel!]!
|
||||
findNotificationByUser: [NotificationModel!]!
|
||||
findProfile: UserModel!
|
||||
findRandomCategories: [CategoryModel!]!
|
||||
@ -214,6 +243,7 @@ type Query {
|
||||
findRecommendedChannels: [UserModel!]!
|
||||
findSessionsByUser: [SessionModel!]!
|
||||
findSocialLinks: [SocialLinkModel!]!
|
||||
findSponsorsByChannel(channelId: String!): [SubscriptionModel!]!
|
||||
findUnreadNotificationsCount: Float!
|
||||
generateTotpSecret: TotpModel!
|
||||
}
|
||||
@ -284,11 +314,43 @@ type Subscription {
|
||||
chatMessageAdded(streamId: String!): ChatMessageModel!
|
||||
}
|
||||
|
||||
type SubscriptionModel {
|
||||
channel: UserModel!
|
||||
channelId: String!
|
||||
createdAt: DateTime!
|
||||
expiresAt: DateTime!
|
||||
id: ID!
|
||||
plan: PlanModel!
|
||||
planId: String!
|
||||
updatedAt: DateTime!
|
||||
user: UserModel!
|
||||
userId: String!
|
||||
}
|
||||
|
||||
type TotpModel {
|
||||
qrcodeUrl: String!
|
||||
secret: String!
|
||||
}
|
||||
|
||||
type TransactionModel {
|
||||
amount: Float!
|
||||
createdAt: DateTime!
|
||||
currency: String!
|
||||
id: ID!
|
||||
status: TransactionStatus!
|
||||
stripeSubscriptionId: ID!
|
||||
updatedAt: DateTime!
|
||||
user: UserModel!
|
||||
userId: ID!
|
||||
}
|
||||
|
||||
enum TransactionStatus {
|
||||
EXPIRED
|
||||
FAILED
|
||||
PENDING
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
"""The `Upload` scalar type represents a file upload."""
|
||||
scalar Upload
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import * as session from 'express-session'
|
||||
import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(CoreModule)
|
||||
const app = await NestFactory.create(CoreModule, { rawBody: true })
|
||||
|
||||
const config = app.get(ConfigService)
|
||||
const redis = app.get(RedisService)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
||||
import { SubscriptionModel } from '@/src/module/sponsorship/subscription/model/subscription.model'
|
||||
import { Args, Query, Resolver } from '@nestjs/graphql'
|
||||
import { ChannelService } from './channel.service'
|
||||
|
||||
@ -20,4 +21,9 @@ export class ChannelResolver {
|
||||
public async findFollowersCount(@Args('channelId') channelId: string) {
|
||||
return this.channelService.findFollowersCountByChannel(channelId)
|
||||
}
|
||||
|
||||
@Query(() => [SubscriptionModel], { name: 'findSponsorsByChannel' })
|
||||
public async findSponsorsByChannel(@Args('channelId') channelId: string) {
|
||||
return this.channelService.findSponsorsByChannel(channelId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,4 +39,22 @@ export class ChannelService {
|
||||
where: { following: { id: channelId } },
|
||||
})
|
||||
}
|
||||
|
||||
public async findSponsorsByChannel(channelId: string) {
|
||||
const channel = await this.prismaService.user.findUnique({
|
||||
where: { id: channelId, isDeactivated: false },
|
||||
})
|
||||
|
||||
if (!channel) {
|
||||
throw new NotFoundException('Канал не найден')
|
||||
}
|
||||
|
||||
return this.prismaService.sponsorshipSubscription.findMany({
|
||||
where: { channelId: channel.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { DeactivateTemplate } from '@/src/module/libs/mail/templates/deactivate.
|
||||
import PasswordRecoveryTemplate from '@/src/module/libs/mail/templates/password-recovery.template'
|
||||
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
|
||||
import { Token } from '@prisma/generated'
|
||||
import { ProcessEnv } from '../../../shared/types/env';
|
||||
import { ProcessEnv } from '@/src/shared/types/env'
|
||||
import VerificationTemplate from './templates/verification.template'
|
||||
import { MailerService } from '@nestjs-modules/mailer'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
|
||||
38
backend/src/module/libs/stripe/stripe.module.ts
Normal file
38
backend/src/module/libs/stripe/stripe.module.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { DynamicModule, Module } from '@nestjs/common'
|
||||
import { StripeOptionSymbol, TypeStripeAsyncOptions, TypeStripeOptions } from './types/stripe.type'
|
||||
import { StripeService } from './stripe.service'
|
||||
|
||||
@Module({})
|
||||
export class StripeModule {
|
||||
public static register(options?: TypeStripeOptions): DynamicModule {
|
||||
return {
|
||||
module: StripeModule,
|
||||
providers: [
|
||||
{
|
||||
provide: StripeOptionSymbol,
|
||||
useValue: options,
|
||||
},
|
||||
StripeService,
|
||||
],
|
||||
exports: [StripeService],
|
||||
global: true,
|
||||
}
|
||||
}
|
||||
|
||||
public static registerAsync(options: TypeStripeAsyncOptions): DynamicModule {
|
||||
return {
|
||||
module: StripeModule,
|
||||
imports: options.imports || [],
|
||||
providers: [
|
||||
{
|
||||
provide: StripeOptionSymbol,
|
||||
useFactory: options.useFactory,
|
||||
inject: options.inject || [],
|
||||
},
|
||||
StripeService,
|
||||
],
|
||||
exports: [StripeService],
|
||||
global: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
13
backend/src/module/libs/stripe/stripe.service.ts
Normal file
13
backend/src/module/libs/stripe/stripe.service.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { StripeOptionSymbol, TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type'
|
||||
import { Inject, Injectable } from '@nestjs/common'
|
||||
import Stripe from 'stripe'
|
||||
|
||||
@Injectable()
|
||||
export class StripeService extends Stripe {
|
||||
constructor(
|
||||
@Inject(StripeOptionSymbol)
|
||||
private readonly options: TypeStripeOptions,
|
||||
) {
|
||||
super(options.apiKey, options.config)
|
||||
}
|
||||
}
|
||||
12
backend/src/module/libs/stripe/types/stripe.type.ts
Normal file
12
backend/src/module/libs/stripe/types/stripe.type.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common'
|
||||
import Stripe from 'stripe'
|
||||
|
||||
export const StripeOptionSymbol = Symbol('StripeOptionSymbol')
|
||||
|
||||
export type TypeStripeOptions = {
|
||||
apiKey: string
|
||||
config?: Stripe.StripeConfig
|
||||
}
|
||||
|
||||
export type TypeStripeAsyncOptions = Pick<ModuleMetadata, 'imports'>
|
||||
& Pick<FactoryProvider<TypeStripeOptions>, 'useFactory' | 'inject'>
|
||||
@ -1,5 +1,5 @@
|
||||
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
|
||||
import type { User } from '@prisma/generated'
|
||||
import type { SponsorshipPlan, User } from '@prisma/generated'
|
||||
|
||||
export const MESSAGES = {
|
||||
welcome:
|
||||
@ -71,4 +71,10 @@ export const MESSAGES = {
|
||||
+ `Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.\n\n`
|
||||
+ `Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.\n\n`
|
||||
+ `Спасибо, что вы с нами и продолжаете развивать свой канал вместе с TeaStream!`,
|
||||
newSponsorship: (plan: SponsorshipPlan, sponsor: User) =>
|
||||
`<b>🎉 Новое спонсор!</b>\n\n`
|
||||
+ `Вы получили новое спонсорство на план <b>${plan.title}</b>.\n`
|
||||
+ `💰 Сумма: <b>${plan.price} ₽</b>\n`
|
||||
+ `👤 Спонсор: <a href="https://teastream.ru/${sponsor.name}">${sponsor.displayName}</a>\n`
|
||||
+ `📅 Дата оформления: <b>${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}</b>`,
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import { Context, Telegraf } from 'telegraf'
|
||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||
import { BUTTONS } from '@/src/module/libs/telegram/telegram.button'
|
||||
import { MESSAGES } from '@/src/module/libs/telegram/telegram.message'
|
||||
import { TokenType, User } from '@prisma/generated'
|
||||
import { SponsorshipPlan, TokenType, User } from '@prisma/generated'
|
||||
|
||||
import { ProcessEnv } from '../../../shared/types/env'
|
||||
|
||||
@ -132,6 +132,10 @@ export class TelegramService extends Telegraf {
|
||||
await this.telegram.sendMessage(chatId, MESSAGES.streamStart(channel), { parse_mode: 'HTML' })
|
||||
}
|
||||
|
||||
public async sendNewSponsorship(chatId: string, plan: SponsorshipPlan, sponsor: User) {
|
||||
await this.telegram.sendMessage(chatId, MESSAGES.newSponsorship(plan, sponsor), { parse_mode: 'HTML' })
|
||||
}
|
||||
|
||||
public async sendNewFollowing(chatId: string, follower: User) {
|
||||
const user = await this.findUserByChatId(chatId)
|
||||
if (!user) {
|
||||
|
||||
@ -2,7 +2,7 @@ import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||
import { ChangeNotificationSettingsInput } from '@/src/module/notification/inputs/change-notification-settings.input'
|
||||
import { generateToken } from '@/src/shared/util/generate-token.util'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { $Enums, User } from '@prisma/generated'
|
||||
import { $Enums, SponsorshipPlan, User } from '@prisma/generated'
|
||||
import TokenType = $Enums.TokenType
|
||||
import NotificationType = $Enums.NotificationType
|
||||
|
||||
@ -107,6 +107,21 @@ export class NotificationService {
|
||||
})
|
||||
}
|
||||
|
||||
public async createNewSponsorship(userId: string, plan: SponsorshipPlan, sponsor: User) {
|
||||
return this.prismaService.notification.create({
|
||||
data: {
|
||||
text: `<b className='font-medium'>У вас новый спонсор!</b>
|
||||
<p>Пользователь <a href='/${sponsor.name}' className='font-semibold'>${sponsor.displayName}</a> стал вашим спонсором, выбрав план <strong>${plan.title}</strong>.</p>`,
|
||||
type: NotificationType.NEW_SPONSORSHIP,
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public async createEnableTwoFactor(userId: string) {
|
||||
return this.prismaService.notification.create({
|
||||
data: {
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import { Field, InputType } from '@nestjs/graphql'
|
||||
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'
|
||||
|
||||
@InputType()
|
||||
export class CreatePlanInput {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string
|
||||
|
||||
@Field(() => Number)
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
price: number
|
||||
}
|
||||
36
backend/src/module/sponsorship/plan/models/plan.model.ts
Normal file
36
backend/src/module/sponsorship/plan/models/plan.model.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
||||
import { SponsorshipPlan } from '@prisma/generated'
|
||||
|
||||
@ObjectType()
|
||||
export class PlanModel implements SponsorshipPlan {
|
||||
@Field(() => ID)
|
||||
id: string
|
||||
|
||||
@Field(() => String)
|
||||
title: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
description: string
|
||||
|
||||
@Field(() => Number)
|
||||
price: number
|
||||
|
||||
@Field(() => UserModel)
|
||||
channel: UserModel
|
||||
|
||||
@Field(() => ID)
|
||||
channelId: string
|
||||
|
||||
@Field(() => ID)
|
||||
stripeProductId: string
|
||||
|
||||
@Field(() => ID)
|
||||
stripePlanId: string
|
||||
|
||||
@Field(() => Date)
|
||||
public createdAt: Date
|
||||
|
||||
@Field(() => Date)
|
||||
public updatedAt: Date
|
||||
}
|
||||
8
backend/src/module/sponsorship/plan/plan.module.ts
Normal file
8
backend/src/module/sponsorship/plan/plan.module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PlanService } from './plan.service';
|
||||
import { PlanResolver } from './plan.resolver';
|
||||
|
||||
@Module({
|
||||
providers: [PlanResolver, PlanService],
|
||||
})
|
||||
export class PlanModule {}
|
||||
35
backend/src/module/sponsorship/plan/plan.resolver.ts
Normal file
35
backend/src/module/sponsorship/plan/plan.resolver.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { CreatePlanInput } from '@/src/module/sponsorship/plan/inputs/create-plan.input'
|
||||
import { PlanModel } from '@/src/module/sponsorship/plan/models/plan.model'
|
||||
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
|
||||
import { User } from '@prisma/generated'
|
||||
import { PlanService } from './plan.service'
|
||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
||||
|
||||
@Resolver('Plan')
|
||||
export class PlanResolver {
|
||||
constructor(private readonly planService: PlanService) {}
|
||||
|
||||
@Authorization()
|
||||
@Query(() => [PlanModel], { name: 'findMySponsorshipPlans' })
|
||||
public findMyPlans(@Authorized() user: User) {
|
||||
return this.planService.findMyPlans(user)
|
||||
}
|
||||
|
||||
@Authorization()
|
||||
@Mutation(() => PlanModel, { name: 'createSponsorshipPlan' })
|
||||
public createPlan(
|
||||
@Authorized() user: User,
|
||||
@Args('data') input: CreatePlanInput,
|
||||
) {
|
||||
return this.planService.create(user, input)
|
||||
}
|
||||
|
||||
@Authorization()
|
||||
@Mutation(() => PlanModel, { name: 'removeSponsorshipPlan' })
|
||||
public removePlan(
|
||||
@Args('planId') planId: string,
|
||||
) {
|
||||
return this.planService.remove(planId)
|
||||
}
|
||||
}
|
||||
79
backend/src/module/sponsorship/plan/plan.service.ts
Normal file
79
backend/src/module/sponsorship/plan/plan.service.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||
import { StripeService } from '@/src/module/libs/stripe/stripe.service'
|
||||
import { CreatePlanInput } from '@/src/module/sponsorship/plan/inputs/create-plan.input'
|
||||
import { Injectable, NotFoundException } from '@nestjs/common'
|
||||
import { User } from '@prisma/generated'
|
||||
|
||||
@Injectable()
|
||||
export class PlanService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly stripeService: StripeService,
|
||||
) {
|
||||
}
|
||||
|
||||
public async findMyPlans(user: User) {
|
||||
return this.prismaService.sponsorshipPlan.findMany({
|
||||
where: { channelId: user.id },
|
||||
})
|
||||
}
|
||||
|
||||
public async create(user: User, input: CreatePlanInput) {
|
||||
const { description, price, title } = input
|
||||
|
||||
const channel = await this.prismaService.user.findUnique({
|
||||
where: { id: user.id },
|
||||
})
|
||||
|
||||
if (!channel) {
|
||||
throw new NotFoundException('Канал не найден')
|
||||
}
|
||||
|
||||
if (!channel.isVerified) {
|
||||
throw new NotFoundException('Создание планов доступно только для верифицированных каналов')
|
||||
}
|
||||
|
||||
const stripePlan = await this.stripeService.plans.create({
|
||||
amount: Math.round(price * 100),
|
||||
currency: 'rub',
|
||||
interval: 'month',
|
||||
product: {
|
||||
name: title,
|
||||
},
|
||||
})
|
||||
|
||||
return this.prismaService.sponsorshipPlan.create({
|
||||
data: {
|
||||
description,
|
||||
price,
|
||||
title,
|
||||
// @ts-expect-error TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
stripeProductId: stripePlan.product?.toString(),
|
||||
stripePlanId: stripePlan.id,
|
||||
channel: {
|
||||
connect: {
|
||||
id: channel.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public async remove(planId: string) {
|
||||
const plan = await this.prismaService.sponsorshipPlan.findUnique({
|
||||
where: { id: planId },
|
||||
})
|
||||
|
||||
if (!plan) {
|
||||
throw new NotFoundException('План не найден')
|
||||
}
|
||||
|
||||
await this.stripeService.plans.del(plan.stripePlanId)
|
||||
await this.stripeService.products.del(plan.stripeProductId)
|
||||
|
||||
return this.prismaService.sponsorshipPlan.delete({
|
||||
where: { id: plan.id },
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { UserModel } from '@/src/module/auth/account/models/user.model';
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
||||
|
||||
import { SponsorshipSubscription } from '@/prisma/generated'
|
||||
|
||||
import { PlanModel } from '../../plan/models/plan.model'
|
||||
|
||||
@ObjectType()
|
||||
export class SubscriptionModel implements SponsorshipSubscription {
|
||||
@Field(() => ID)
|
||||
public id: string
|
||||
|
||||
@Field(() => Date)
|
||||
public expiresAt: Date
|
||||
|
||||
@Field(() => PlanModel)
|
||||
public plan: PlanModel
|
||||
|
||||
@Field(() => String)
|
||||
public planId: string
|
||||
|
||||
@Field(() => UserModel)
|
||||
public user: UserModel
|
||||
|
||||
@Field(() => String)
|
||||
public userId: string
|
||||
|
||||
@Field(() => UserModel)
|
||||
public channel: UserModel
|
||||
|
||||
@Field(() => String)
|
||||
public channelId: string
|
||||
|
||||
@Field(() => Date)
|
||||
public createdAt: Date
|
||||
|
||||
@Field(() => Date)
|
||||
public updatedAt: Date
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SubscriptionService } from './subscription.service';
|
||||
import { SubscriptionResolver } from './subscription.resolver';
|
||||
|
||||
@Module({
|
||||
providers: [SubscriptionResolver, SubscriptionService],
|
||||
})
|
||||
export class SubscriptionModule {}
|
||||
@ -0,0 +1,17 @@
|
||||
import { SubscriptionModel } from '@/src/module/sponsorship/subscription/model/subscription.model';
|
||||
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
||||
import { Query, Resolver } from '@nestjs/graphql'
|
||||
import { User } from '@prisma/generated'
|
||||
import { SubscriptionService } from './subscription.service'
|
||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
||||
|
||||
@Resolver('Subscription')
|
||||
export class SubscriptionResolver {
|
||||
constructor(private readonly subscriptionService: SubscriptionService) {}
|
||||
|
||||
@Authorization()
|
||||
@Query(() => [SubscriptionModel])
|
||||
public async findMySponsors(@Authorized() user: User) {
|
||||
return this.subscriptionService.findMySponsors(user)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { User } from '@prisma/generated'
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionService {
|
||||
constructor(private readonly prismaService: PrismaService) {
|
||||
}
|
||||
|
||||
public async findMySponsors(user: User) {
|
||||
return this.prismaService.sponsorshipSubscription.findMany({
|
||||
where: { channelId: user.id },
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
plan: true,
|
||||
user: true,
|
||||
channel: true
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class MakePaymentModel {
|
||||
@Field(() => String)
|
||||
url: string
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
||||
import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql'
|
||||
import { Transaction, TransactionStatus } from '@prisma/generated'
|
||||
|
||||
registerEnumType(TransactionStatus, {
|
||||
name: 'TransactionStatus',
|
||||
})
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionModel implements Transaction {
|
||||
@Field(() => ID)
|
||||
public id: string
|
||||
|
||||
@Field(() => ID)
|
||||
stripeSubscriptionId: string
|
||||
|
||||
@Field(() => TransactionStatus)
|
||||
status: TransactionStatus
|
||||
|
||||
@Field(() => String)
|
||||
currency: string
|
||||
|
||||
@Field(() => Number)
|
||||
amount: number
|
||||
|
||||
@Field(() => UserModel)
|
||||
user: UserModel
|
||||
|
||||
@Field(() => ID)
|
||||
userId: string
|
||||
|
||||
@Field(() => Date)
|
||||
public createdAt: Date
|
||||
|
||||
@Field(() => Date)
|
||||
public updatedAt: Date
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TransactionService } from './transaction.service';
|
||||
import { TransactionResolver } from './transaction.resolver';
|
||||
|
||||
@Module({
|
||||
providers: [TransactionResolver, TransactionService],
|
||||
})
|
||||
export class TransactionModule {}
|
||||
@ -0,0 +1,27 @@
|
||||
import { MakePaymentModel } from '@/src/module/sponsorship/transaction/model/make-payment.model'
|
||||
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
|
||||
import { User } from '@prisma/generated'
|
||||
import { TransactionService } from './transaction.service'
|
||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
||||
import { TransactionModel } from './model/transaction.model'
|
||||
|
||||
@Resolver('Transaction')
|
||||
export class TransactionResolver {
|
||||
constructor(private readonly transactionService: TransactionService) {}
|
||||
|
||||
@Authorization()
|
||||
@Query(() => [TransactionModel], { name: 'findMyTransactions' })
|
||||
public async findMyTransactions(@Authorized() user: User) {
|
||||
return this.transactionService.findMyTransactions(user)
|
||||
}
|
||||
|
||||
@Authorization()
|
||||
@Mutation(() => MakePaymentModel, { name: 'makePayment' })
|
||||
public async makePayment(
|
||||
@Authorized() user: User,
|
||||
@Args('planId') planId: string,
|
||||
) {
|
||||
return this.transactionService.makePayment(user, planId)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||
import { StripeService } from '@/src/module/libs/stripe/stripe.service'
|
||||
import { ProcessEnv } from '@/src/shared/types/env'
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { User } from '@prisma/generated'
|
||||
|
||||
@Injectable()
|
||||
export class TransactionService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly configService: ConfigService<ProcessEnv>,
|
||||
private readonly stripeService: StripeService,
|
||||
) {
|
||||
}
|
||||
|
||||
public async findMyTransactions(user: User) {
|
||||
return this.prismaService.transaction.findMany({
|
||||
where: { userId: user.id },
|
||||
})
|
||||
}
|
||||
|
||||
public async makePayment(user: User, planId: string) {
|
||||
const plan = await this.prismaService.sponsorshipPlan.findUnique({
|
||||
where: { id: planId },
|
||||
include: { channel: true },
|
||||
})
|
||||
|
||||
if (!plan) {
|
||||
throw new NotFoundException('Не найден тарифный план')
|
||||
}
|
||||
|
||||
if (user.id === plan.channel?.id) {
|
||||
throw new ConflictException('Нельзя оформить подписку на себя')
|
||||
}
|
||||
|
||||
const existingSubscription = await this.prismaService.sponsorshipSubscription.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
channelId: plan.channel?.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (existingSubscription) {
|
||||
throw new ConflictException('Вы уже оформили спонсорство на этот канал')
|
||||
}
|
||||
|
||||
const customer = await this.stripeService.customers.create({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
})
|
||||
|
||||
const session = await this.stripeService.checkout.sessions.create({
|
||||
// @ts-expect-error TODO
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: 'rub',
|
||||
product_data: {
|
||||
name: plan.title,
|
||||
description: plan.description,
|
||||
},
|
||||
unit_amount: Math.round(plan.price * 100),
|
||||
recurring: {
|
||||
interval: 'month',
|
||||
},
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
success_url: `${this.configService.getOrThrow('ALLOWED_ORIGIN')}/success?price=${plan.price}&name=${plan.channel?.name}`,
|
||||
cancel_url: this.configService.getOrThrow('ALLOWED_ORIGIN'),
|
||||
customer: customer.id,
|
||||
metadata: {
|
||||
planId: plan.id,
|
||||
userId: user.id,
|
||||
channelId: plan.channel?.id,
|
||||
},
|
||||
})
|
||||
|
||||
await this.prismaService.transaction.create({
|
||||
data: {
|
||||
amount: plan.price,
|
||||
currency: session.currency!,
|
||||
stripeSubscriptionId: session.id,
|
||||
user: {
|
||||
connect: { id: user.id },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return { url: session.url }
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Controller, HttpCode, HttpStatus, Post, UnauthorizedException, Headers, Body } from '@nestjs/common'
|
||||
import { Controller, HttpCode, HttpStatus, Post, UnauthorizedException, Headers, Body, RawBody } from '@nestjs/common'
|
||||
import { WebhookService } from './webhook.service'
|
||||
|
||||
@Controller('webhook')
|
||||
@ -17,4 +17,18 @@ export class WebhookController {
|
||||
|
||||
return this.webhookService.receiveWebhookLiveKit(body, authorization)
|
||||
}
|
||||
|
||||
@Post('stripe')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
public async receiveWebhookStripe(
|
||||
@RawBody() rawBody: string,
|
||||
@Headers('stripe-signature') sig: string,
|
||||
) {
|
||||
if (!sig) {
|
||||
throw new UnauthorizedException('Отсутствует подпись Stripe в заголовке')
|
||||
}
|
||||
|
||||
const event = await this.webhookService.constructStripeEvent(rawBody, sig)
|
||||
await this.webhookService.receiveWebhookStripe(event)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||
import { LiveKitService } from '@/src/module/libs/livekit/livekit.service'
|
||||
import { TelegramService } from '@/src/module/libs/telegram/telegram.service';
|
||||
import { StripeService } from '@/src/module/libs/stripe/stripe.service'
|
||||
import { TelegramService } from '@/src/module/libs/telegram/telegram.service'
|
||||
import { NotificationService } from '@/src/module/notification/notification.service'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { ProcessEnv } from '@/src/shared/types/env';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { TransactionStatus } from '@prisma/generated'
|
||||
import Stripe from 'stripe'
|
||||
|
||||
@Injectable()
|
||||
export class WebhookService {
|
||||
constructor(
|
||||
private prismaService: PrismaService,
|
||||
private liveKitService: LiveKitService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly liveKitService: LiveKitService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly telegramService: TelegramService,
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly configService: ConfigService<ProcessEnv>,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -60,4 +67,87 @@ export class WebhookService {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async receiveWebhookStripe(event: Stripe.Event) {
|
||||
const session = event.data.object as Stripe.Checkout.Session
|
||||
|
||||
if (!session) {
|
||||
throw new BadRequestException('Не найдена сессия')
|
||||
}
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const planId = session.metadata!.planId
|
||||
const userId = session.metadata!.userId
|
||||
const channelId = session.metadata!.channelId
|
||||
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 30)
|
||||
|
||||
const sponsorshipSubscription = await this.prismaService.sponsorshipSubscription.create({
|
||||
data: {
|
||||
expiresAt,
|
||||
planId,
|
||||
userId,
|
||||
channelId,
|
||||
},
|
||||
include: {
|
||||
plan: true,
|
||||
user: true,
|
||||
channel: {
|
||||
include: {
|
||||
notificationSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await this.prismaService.transaction.updateMany({
|
||||
where: {
|
||||
stripeSubscriptionId: session.id,
|
||||
status: TransactionStatus.PENDING,
|
||||
},
|
||||
data: {
|
||||
status: TransactionStatus.SUCCESS,
|
||||
},
|
||||
})
|
||||
|
||||
if (sponsorshipSubscription.channel?.notificationSettings?.siteNotifications) {
|
||||
await this.notificationService.createNewSponsorship(
|
||||
sponsorshipSubscription.channel.id,
|
||||
sponsorshipSubscription.plan!,
|
||||
sponsorshipSubscription.user!,
|
||||
)
|
||||
}
|
||||
|
||||
if (sponsorshipSubscription.channel?.notificationSettings?.telegramNotifications && sponsorshipSubscription.channel.telegramId) {
|
||||
await this.telegramService.sendNewSponsorship(
|
||||
sponsorshipSubscription.channel.telegramId,
|
||||
sponsorshipSubscription.plan!,
|
||||
sponsorshipSubscription.user!,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'checkout.session.expired') {
|
||||
await this.prismaService.transaction.updateMany({
|
||||
where: { stripeSubscriptionId: session.id },
|
||||
data: { status: TransactionStatus.EXPIRED },
|
||||
})
|
||||
}
|
||||
|
||||
if (event.type === 'checkout.session.async_payment_failed') {
|
||||
await this.prismaService.transaction.updateMany({
|
||||
where: { stripeSubscriptionId: session.id },
|
||||
data: { status: TransactionStatus.FAILED },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public constructStripeEvent(payload: any, signature: any) {
|
||||
return this.stripeService.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
this.configService.getOrThrow('STRIPE_WEBHOOK_SECRET'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
2
backend/src/shared/types/env.d.ts
vendored
2
backend/src/shared/types/env.d.ts
vendored
@ -47,4 +47,6 @@ export interface ProcessEnv {
|
||||
LIVEKIT_API_SECRET: string
|
||||
|
||||
TELEGRAM_BOT_TOKEN: string
|
||||
STRIPE_SECRET_KEY: string
|
||||
STRIPE_WEBHOOK_SECRET: string
|
||||
}
|
||||
|
||||
1559
backend/yarn.lock
1559
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user