diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index d81c4a6c..6a678fb8 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -24,6 +24,7 @@ import { MarketplaceController } from '@gitroom/backend/api/routes/marketplace.c import { MessagesController } from '@gitroom/backend/api/routes/messages.controller'; import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service'; import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service'; +import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service'; const authenticatedController = [ UsersController, @@ -66,6 +67,7 @@ const authenticatedController = [ AuthMiddleware, PoliciesGuard, PermissionsService, + CodesService, IntegrationManager, ], get exports() { diff --git a/apps/backend/src/api/routes/billing.controller.ts b/apps/backend/src/api/routes/billing.controller.ts index 706ce742..32afd36b 100644 --- a/apps/backend/src/api/routes/billing.controller.ts +++ b/apps/backend/src/api/routes/billing.controller.ts @@ -4,7 +4,7 @@ import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; import { Organization } from '@prisma/client'; import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto'; -import {ApiTags} from "@nestjs/swagger"; +import { ApiTags } from '@nestjs/swagger'; @ApiTags('Billing') @Controller('/billing') @@ -58,9 +58,17 @@ export class BillingController { @Post('/prorate') prorate( - @GetOrgFromRequest() org: Organization, - @Body() body: BillingSubscribeDto + @GetOrgFromRequest() org: Organization, + @Body() body: BillingSubscribeDto ) { return this._stripeService.prorate(org.id, body); } + + @Post('/lifetime') + async lifetime( + @GetOrgFromRequest() org: Organization, + @Body() body: { code: string } + ) { + return this._stripeService.lifetimeDeal(org.id, body.code); + } } diff --git a/apps/backend/src/api/routes/stripe.controller.ts b/apps/backend/src/api/routes/stripe.controller.ts index 2f99afa7..d2467c83 100644 --- a/apps/backend/src/api/routes/stripe.controller.ts +++ b/apps/backend/src/api/routes/stripe.controller.ts @@ -1,11 +1,23 @@ -import { Controller, Post, RawBodyRequest, Req } from '@nestjs/common'; +import { + Controller, + Get, + Header, + Param, + Post, + RawBodyRequest, + Req, +} from '@nestjs/common'; import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service'; import { ApiTags } from '@nestjs/swagger'; +import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service'; @ApiTags('Stripe') @Controller('/stripe') export class StripeController { - constructor(private readonly _stripeService: StripeService) {} + constructor( + private readonly _stripeService: StripeService, + private readonly _codesService: CodesService + ) {} @Post('/connect') stripeConnect(@Req() req: RawBodyRequest) { const event = this._stripeService.validateRequest( @@ -61,4 +73,11 @@ export class StripeController { return { ok: true }; } } + + @Get('/lifetime-deal-codes/:provider') + @Header('Content-disposition', 'attachment; filename=codes.csv') + @Header('Content-type', 'text/csv') + async getStripeCodes(@Param('provider') providerToken: string) { + return this._codesService.generateCodes(providerToken); + } } diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 457af7e4..20b6fcd2 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -54,6 +54,8 @@ export class UsersController { tier: organization?.subscription?.subscriptionTier || 'FREE', // @ts-ignore role: organization?.users[0]?.role, + // @ts-ignore + isLifetime: !!organization?.subscription?.isLifetime, }; } diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js index c79aae74..f1850f90 100644 --- a/apps/frontend/next.config.js +++ b/apps/frontend/next.config.js @@ -14,6 +14,7 @@ const nextConfig = { }, env: { isBillingEnabled: String(!!process.env.STRIPE_PUBLISHABLE_KEY), + isGeneral: String(!!process.env.IS_GENERAL), } }; diff --git a/apps/frontend/src/app/(site)/billing/lifetime/page.tsx b/apps/frontend/src/app/(site)/billing/lifetime/page.tsx new file mode 100644 index 00000000..3d9aa7a8 --- /dev/null +++ b/apps/frontend/src/app/(site)/billing/lifetime/page.tsx @@ -0,0 +1,14 @@ +import { LifetimeDeal } from '@gitroom/frontend/components/billing/lifetime.deal'; + +export const dynamic = 'force-dynamic'; + +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Gitroom Lifetime deal', + description: '', +}; + +export default async function Page() { + return ; +} diff --git a/apps/frontend/src/components/billing/lifetime.deal.tsx b/apps/frontend/src/components/billing/lifetime.deal.tsx new file mode 100644 index 00000000..3c25afcf --- /dev/null +++ b/apps/frontend/src/components/billing/lifetime.deal.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { useCallback, useMemo, useState } from 'react'; +import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; +import { Input } from '@gitroom/react/form/input'; +import { Button } from '@gitroom/react/form/button'; +import { useSWRConfig } from 'swr'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { useRouter } from 'next/navigation'; + +export const LifetimeDeal = () => { + const fetch = useFetch(); + const user = useUser(); + const [code, setCode] = useState(''); + const toast = useToaster(); + const { mutate } = useSWRConfig(); + const router = useRouter(); + + const claim = useCallback(async () => { + const { success } = await ( + await fetch('/billing/lifetime', { + body: JSON.stringify({ code }), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + ).json(); + + if (success) { + mutate('/user/self'); + toast.show('Successfully claimed the code'); + } else { + toast.show('Code already claimed or invalid code', 'warning'); + } + + setCode(''); + }, [code]); + + const nextPackage = useMemo(() => { + if (user?.tier?.current === 'STANDARD') { + return 'PRO'; + } + + return 'STANDARD'; + }, [user?.tier]); + + const features = useMemo(() => { + if (!user?.tier) { + return []; + } + const currentPricing = user?.tier; + const channelsOr = currentPricing.channel; + const list = []; + list.push(`${channelsOr} ${channelsOr === 1 ? 'channel' : 'channels'}`); + list.push( + `${ + currentPricing.posts_per_month > 10000 + ? 'Unlimited' + : currentPricing.posts_per_month + } posts per month` + ); + if (currentPricing.team_members) { + list.push(`Unlimited team members`); + } + + if (currentPricing.import_from_channels) { + list.push(`Import content from channels (coming soon)`); + } + + if (currentPricing.community_features) { + list.push(`Community features (coming soon)`); + } + + if (currentPricing.ai) { + list.push(`AI auto-complete (coming soon)`); + } + + if (currentPricing.featured_by_gitroom) { + list.push(`Become featured by Gitroom (coming soon)`); + } + + return list; + }, [user]); + + const nextFeature = useMemo(() => { + if (!user?.tier) { + return []; + } + const currentPricing = pricing[nextPackage]; + const channelsOr = currentPricing.channel; + const list = []; + list.push(`${channelsOr} ${channelsOr === 1 ? 'channel' : 'channels'}`); + list.push( + `${ + currentPricing.posts_per_month > 10000 + ? 'Unlimited' + : currentPricing.posts_per_month + } posts per month` + ); + if (currentPricing.team_members) { + list.push(`Unlimited team members`); + } + + if (currentPricing.import_from_channels) { + list.push(`Import content from channels (coming soon)`); + } + + if (currentPricing.community_features) { + list.push(`Community features (coming soon)`); + } + + if (currentPricing.ai) { + list.push(`AI auto-complete`); + } + + if (currentPricing.featured_by_gitroom) { + list.push(`Become featured by Gitroom (coming soon)`); + } + + return list; + }, [user, nextPackage]); + + if (!user?.tier) { + return null; + } + + if (user?.id && user?.tier?.current !== 'FREE' && !user?.isLifetime) { + router.replace('/billing'); + return null; + } + + return ( +
+
+
+ Current Package: {user?.tier?.current} +
+ +
+ {features.map((feature) => ( +
+
+ + + +
+
{feature}
+
+ ))} +
+
+ + {user?.tier?.current !== 'PRO' && ( +
+
Next Package: {nextPackage}
+ +
+ {nextFeature.map((feature) => ( +
+
+ + + +
+
{feature}
+
+ ))} + +
+
+ setCode(e.target.value)} + /> +
+
+ +
+
+
+
+ )} +
+ ); +}; diff --git a/apps/frontend/src/components/billing/main.billing.component.tsx b/apps/frontend/src/components/billing/main.billing.component.tsx index 76a2dd51..a30a56fe 100644 --- a/apps/frontend/src/components/billing/main.billing.component.tsx +++ b/apps/frontend/src/components/billing/main.billing.component.tsx @@ -18,6 +18,7 @@ import { FAQComponent } from '@gitroom/frontend/components/billing/faq.component import { useSWRConfig } from 'swr'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; import interClass from '@gitroom/react/helpers/inter.font'; +import { useRouter } from 'next/navigation'; export interface Tiers { month: Array<{ @@ -155,6 +156,7 @@ export const MainBillingComponent: FC<{ const fetch = useFetch(); const toast = useToaster(); const user = useUser(); + const router = useRouter(); const [subscription, setSubscription] = useState( sub @@ -307,6 +309,11 @@ export const MainBillingComponent: FC<{ [monthlyOrYearly, subscription, user] ); + if (user?.isLifetime) { + router.replace('/billing/lifetime'); + return null; + } + return (
diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx index 3aed50bb..090b7652 100644 --- a/apps/frontend/src/components/layout/settings.component.tsx +++ b/apps/frontend/src/components/layout/settings.component.tsx @@ -11,12 +11,16 @@ import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details import { useToaster } from '@gitroom/react/toaster/toaster'; import { useSWRConfig } from 'swr'; import clsx from 'clsx'; +import { TeamsComponent } from '@gitroom/frontend/components/settings/teams.component'; +import { isGeneral } from '@gitroom/react/helpers/is.general'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { const { getRef } = props; const fetch = useFetch(); const toast = useToaster(); const swr = useSWRConfig(); + const user = useUser(); const resolver = useMemo(() => { return classValidatorResolver(UserDetailDto); @@ -52,7 +56,7 @@ export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { }); if (getRef) { - return ; + return; } toast.show('Profile updated'); @@ -67,7 +71,9 @@ export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { return (
- {!!getRef && } + {!!getRef && ( + + )}
}> = (props) => {
)} + {!!user?.tier?.team_members && isGeneral() && }
diff --git a/apps/frontend/src/components/layout/top.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx index e0f586cc..e53fbeb2 100644 --- a/apps/frontend/src/components/layout/top.menu.tsx +++ b/apps/frontend/src/components/layout/top.menu.tsx @@ -5,24 +5,35 @@ import Link from 'next/link'; import clsx from 'clsx'; import { usePathname } from 'next/navigation'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { isGeneral } from '@gitroom/react/helpers/is.general'; + +const general = isGeneral(); export const menuItems = [ - { - name: 'Analytics', - icon: 'analytics', - path: '/analytics', - }, + ...(!general + ? [ + { + name: 'Analytics', + icon: 'analytics', + path: '/analytics', + }, + ] + : []), { name: 'Launches', icon: 'launches', path: '/launches', }, - { - name: 'Settings', - icon: 'settings', - path: '/settings', - role: ['ADMIN', 'SUPERADMIN'], - }, + ...(!general + ? [ + { + name: 'Settings', + icon: 'settings', + path: '/settings', + role: ['ADMIN', 'SUPERADMIN'], + }, + ] + : []), { name: 'Marketplace', icon: 'marketplace', diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index eaab1fc4..146eb192 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -14,6 +14,7 @@ export const UserContext = createContext< tier: PricingInnerInterface; role: 'USER' | 'ADMIN' | 'SUPERADMIN'; totalChannels: number; + isLifetime?: boolean; }) >(undefined); diff --git a/apps/frontend/src/components/onboarding/onboarding.tsx b/apps/frontend/src/components/onboarding/onboarding.tsx index aa864d09..ad03dd15 100644 --- a/apps/frontend/src/components/onboarding/onboarding.tsx +++ b/apps/frontend/src/components/onboarding/onboarding.tsx @@ -8,10 +8,14 @@ import { GithubOnboarding } from '@gitroom/frontend/components/onboarding/github import { SettingsPopup } from '@gitroom/frontend/components/layout/settings.component'; import { Button } from '@gitroom/react/form/button'; import { ConnectChannels } from '@gitroom/frontend/components/onboarding/connect.channels'; +import { isGeneral } from '@gitroom/react/helpers/is.general'; -export const Step: FC<{ step: number; title: string; currentStep: number, lastStep: number }> = ( - props -) => { +export const Step: FC<{ + step: number; + title: string; + currentStep: number; + lastStep: number; +}> = (props) => { const { step, title, currentStep, lastStep } = props; return (
@@ -87,40 +91,51 @@ const SkipOnboarding: FC = () => { const onSkip = useCallback(() => { const keys = Array.from(searchParams.keys()); - const buildNewQuery = keys.reduce((all, current) => { - if (current === 'onboarding') { + const buildNewQuery = keys + .reduce((all, current) => { + if (current === 'onboarding') { + return all; + } + const value = searchParams.get(current); + all.push(`${current}=${value}`); return all; - } - const value = searchParams.get(current); - all.push(`${current}=${value}`); - return all; - }, [] as string[]).join('&'); + }, [] as string[]) + .join('&'); router.push(`?${buildNewQuery}`); }, [searchParams]); return ( - + ); -} +}; const Welcome: FC = () => { const [step, setStep] = useState(1); const ref = useRef(); const router = useRouter(); - const nextStep = useCallback(() => { - setStep(step + 1); - }, [step]); + const nextStep = useCallback( + (stepIt?: number) => () => { + setStep(stepIt ? stepIt : step + 1); + }, + [step] + ); const firstNext = useCallback(() => { // @ts-ignore ref?.current?.click(); - nextStep(); + nextStep(isGeneral() ? 3 : 2)(); }, [nextStep]); const goToAnalytics = useCallback(() => { router.push('/analytics'); }, []); - const goToLaunches = useCallback(() => { + const goToLaunches = useCallback(() => { router.push('/launches'); }, []); @@ -130,9 +145,23 @@ const Welcome: FC = () => {
- - - + {!isGeneral() && ( + <> + + + + )} +
@@ -152,7 +181,7 @@ const Welcome: FC = () => {
- +
)} @@ -161,7 +190,7 @@ const Welcome: FC = () => {
- +
)} @@ -171,10 +200,20 @@ const Welcome: FC = () => { success
- You are done, you can now video your GitHub analytics or
schedule new posts + You are done, you can now video your GitHub analytics or +
+ schedule new posts
- + {!isGeneral() && ( + + )}
diff --git a/apps/frontend/src/components/settings/settings.component.tsx b/apps/frontend/src/components/settings/settings.component.tsx index 7e5912a4..6b1c542b 100644 --- a/apps/frontend/src/components/settings/settings.component.tsx +++ b/apps/frontend/src/components/settings/settings.component.tsx @@ -8,6 +8,9 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import useSWR from 'swr'; import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; import { useRouter } from 'next/navigation'; +import { isGeneral } from '@gitroom/react/helpers/is.general'; + +const general = isGeneral(); export const SettingsComponent = () => { const user = useUser(); @@ -50,22 +53,24 @@ export const SettingsComponent = () => { return (
-
-

Your Git Repository

-
- Connect your GitHub repository to receive updates and analytics + {!general && ( +
+

Your Git Repository

+
+ Connect your GitHub repository to receive updates and analytics +
+ + {/*
*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
Show news with everybody in Gitroom
*/} + {/*
*/}
- - {/*
*/} - {/*
*/} - {/* */} - {/*
*/} - {/*
Show news with everybody in Gitroom
*/} - {/*
*/} -
+ )} {!!user?.tier?.team_members && }
); diff --git a/apps/frontend/src/middleware.ts b/apps/frontend/src/middleware.ts index 021f0529..624468a4 100644 --- a/apps/frontend/src/middleware.ts +++ b/apps/frontend/src/middleware.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { fetchBackend } from '@gitroom/helpers/utils/custom.fetch.func'; import { removeSubdomain } from '@gitroom/helpers/subdomain/subdomain.management'; +import { isGeneral } from '@gitroom/react/helpers/is.general'; // This function can be marked `async` if using `await` inside export async function middleware(request: NextRequest) { @@ -86,8 +87,16 @@ export async function middleware(request: NextRequest) { return redirect; } + if (isGeneral() && (nextUrl.pathname.indexOf('/analytics') > -1 || nextUrl.pathname.indexOf('/settings') > -1)) { + return NextResponse.redirect( + new URL('/launches', nextUrl.href) + ); + } + if (nextUrl.pathname === '/') { - return NextResponse.redirect(new URL(`/analytics`, nextUrl.href)); + return NextResponse.redirect( + new URL(isGeneral() ? '/launches' : `/analytics`, nextUrl.href) + ); } const next = NextResponse.next(); diff --git a/libraries/helpers/src/auth/auth.service.ts b/libraries/helpers/src/auth/auth.service.ts index 5257961c..1a31ccc5 100644 --- a/libraries/helpers/src/auth/auth.service.ts +++ b/libraries/helpers/src/auth/auth.service.ts @@ -1,17 +1,44 @@ -import {sign, verify} from 'jsonwebtoken'; -import {hashSync, compareSync} from 'bcrypt'; +import { sign, verify } from 'jsonwebtoken'; +import { hashSync, compareSync } from 'bcrypt'; +import bcrypt from 'bcrypt'; +import crypto from 'crypto'; export class AuthService { - static hashPassword (password: string) { + static hashPassword(password: string) { return hashSync(password, 10); - } - static comparePassword (password: string, hash: string) { + } + static comparePassword(password: string, hash: string) { return compareSync(password, hash); - } - static signJWT (value: object) { - return sign(value, process.env.JWT_SECRET!); - } - static verifyJWT (token: string) { - return verify(token, process.env.JWT_SECRET!); - } + } + static signJWT(value: object) { + return sign(value, process.env.JWT_SECRET!); + } + static verifyJWT(token: string) { + return verify(token, process.env.JWT_SECRET!); + } + + static fixedEncryption(value: string) { + // encryption algorithm + const algorithm = 'aes-256-cbc'; + + // create a cipher object + const cipher = crypto.createCipher(algorithm, process.env.JWT_SECRET); + + // encrypt the plain text + let encrypted = cipher.update(value, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + return encrypted; + } + + static fixedDecryption(hash: string) { + const algorithm = 'aes-256-cbc'; + const decipher = crypto.createDecipher(algorithm, process.env.JWT_SECRET); + + // decrypt the encrypted text + let decrypted = decipher.update(hash, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index fd07844a..261b51f6 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -35,6 +35,7 @@ export class OrganizationRepository { select: { subscriptionTier: true, totalChannels: true, + isLifetime: true, }, }, }, diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index b56b4d84..1a71c00a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -67,6 +67,15 @@ model User { @@index([pictureId]) } +model UsedCodes { + id String @id @default(uuid()) + code String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([code]) +} + model UserOrganization { id String @id @default(uuid()) user User @relation(fields: [userId], references: [id]) @@ -163,6 +172,7 @@ model Subscription { cancelAt DateTime? period Period totalChannels Int + isLifetime Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts index 4367822f..92f771d1 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts @@ -1,4 +1,5 @@ export interface PricingInnerInterface { + current: string; month_price: number; year_price: number; channel?: number; @@ -14,6 +15,7 @@ export interface PricingInterface { } export const pricing: PricingInterface = { FREE: { + current: 'FREE', month_price: 0, year_price: 0, channel: 2, @@ -25,6 +27,7 @@ export const pricing: PricingInterface = { import_from_channels: false, }, STANDARD: { + current: 'STANDARD', month_price: 30, year_price: 288, channel: 5, @@ -36,6 +39,7 @@ export const pricing: PricingInterface = { import_from_channels: true, }, PRO: { + current: 'PRO', month_price: 40, year_price: 384, channel: 8, diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts index 1d8963b0..c246c081 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts @@ -6,7 +6,8 @@ export class SubscriptionRepository { constructor( private readonly _subscription: PrismaRepository<'subscription'>, private readonly _organization: PrismaRepository<'organization'>, - private readonly _user: PrismaRepository<'user'> + private readonly _user: PrismaRepository<'user'>, + private _usedCodes: PrismaRepository<'usedCodes'> ) {} getUserAccount(userId: string) { @@ -21,6 +22,14 @@ export class SubscriptionRepository { }); } + getCode(code: string) { + return this._usedCodes.model.usedCodes.findFirst({ + where: { + code, + }, + }); + } + updateAccount(userId: string, account: string) { return this._user.model.user.update({ where: { @@ -107,27 +116,37 @@ export class SubscriptionRepository { totalChannels: number, billing: 'STANDARD' | 'PRO', period: 'MONTHLY' | 'YEARLY', - cancelAt: number | null + cancelAt: number | null, + code?: string, + org?: { id: string } ) { - const findOrg = (await this.getOrganizationByCustomerId(customerId))!; + const findOrg = + org || (await this.getOrganizationByCustomerId(customerId))!; + await this._subscription.model.subscription.upsert({ where: { organizationId: findOrg.id, - organization: { - paymentId: customerId, - }, + ...(!code + ? { + organization: { + paymentId: customerId, + }, + } + : {}), }, update: { subscriptionTier: billing, totalChannels, period, identifier, + isLifetime: !!code, cancelAt: cancelAt ? new Date(cancelAt * 1000) : null, deletedAt: null, }, create: { organizationId: findOrg.id, subscriptionTier: billing, + isLifetime: !!code, totalChannels, period, cancelAt: cancelAt ? new Date(cancelAt * 1000) : null, @@ -135,6 +154,14 @@ export class SubscriptionRepository { deletedAt: null, }, }); + + if (code) { + await this._usedCodes.model.usedCodes.create({ + data: { + code, + }, + }); + } } getSubscription(organizationId: string) { diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts index fd1a454c..6dc55b7d 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts @@ -18,6 +18,10 @@ export class SubscriptionService { ); } + getCode(code: string) { + return this._subscriptionRepository.getCode(code); + } + updateAccount(userId: string, account: string) { return this._subscriptionRepository.updateAccount(userId, account); } @@ -52,7 +56,10 @@ export class SubscriptionService { } updateConnectedStatus(account: string, accountCharges: boolean) { - return this._subscriptionRepository.updateConnectedStatus(account, accountCharges); + return this._subscriptionRepository.updateConnectedStatus( + account, + accountCharges + ); } async modifySubscription( @@ -60,7 +67,10 @@ export class SubscriptionService { totalChannels: number, billing: 'FREE' | 'STANDARD' | 'PRO' ) { - const getOrgByCustomerId = await this._subscriptionRepository.getOrganizationByCustomerId(customerId); + const getOrgByCustomerId = + await this._subscriptionRepository.getOrganizationByCustomerId( + customerId + ); const getCurrentSubscription = (await this._subscriptionRepository.getSubscriptionByCustomerId( @@ -120,16 +130,22 @@ export class SubscriptionService { totalChannels: number, billing: 'STANDARD' | 'PRO', period: 'MONTHLY' | 'YEARLY', - cancelAt: number | null + cancelAt: number | null, + code?: string, + org?: string ) { - await this.modifySubscription(customerId, totalChannels, billing); + if (!code) { + await this.modifySubscription(customerId, totalChannels, billing); + } return this._subscriptionRepository.createOrUpdateSubscription( identifier, customerId, totalChannels, billing, period, - cancelAt + cancelAt, + code, + org ? { id: org } : undefined ); } diff --git a/libraries/nestjs-libraries/src/services/codes.service.ts b/libraries/nestjs-libraries/src/services/codes.service.ts new file mode 100644 index 00000000..4f12465e --- /dev/null +++ b/libraries/nestjs-libraries/src/services/codes.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; + +@Injectable() +export class CodesService { + generateCodes(providerToken: string) { + try { + const decrypt = AuthService.fixedDecryption(providerToken); + return [...new Array(10000)].map((_, index) => { + return AuthService.fixedEncryption(`${decrypt}:${index}`); + }).join('\n'); + } catch (error) { + return ''; + } + } +} diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index 54c45856..a40ab775 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -8,6 +8,7 @@ import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/bill import { capitalize, groupBy } from 'lodash'; import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service'; import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-04-10', @@ -511,4 +512,45 @@ export class StripeService { transfer_group: orderId, }); } + + async lifetimeDeal(organizationId: string, code: string) { + const getCurrentSubscription = + await this._subscriptionService.getSubscriptionByOrganizationId( + organizationId + ); + if (getCurrentSubscription && !getCurrentSubscription?.isLifetime) { + throw new Error('You already have a non lifetime subscription'); + } + + try { + const testCode = AuthService.fixedDecryption(code); + const findCode = await this._subscriptionService.getCode(testCode); + if (findCode) { + return { + success: false, + }; + } + + const nextPackage = !getCurrentSubscription ? 'STANDARD' : 'PRO'; + const findPricing = pricing[nextPackage]; + await this._subscriptionService.createOrUpdateSubscription( + makeId(10), + organizationId, + findPricing.channel!, + nextPackage, + 'MONTHLY', + null, + testCode, + organizationId + ); + return { + success: true, + }; + } catch (err) { + console.log(err); + return { + success: false, + }; + } + } } diff --git a/libraries/react-shared-libraries/src/helpers/is.general.tsx b/libraries/react-shared-libraries/src/helpers/is.general.tsx new file mode 100644 index 00000000..1b18fa70 --- /dev/null +++ b/libraries/react-shared-libraries/src/helpers/is.general.tsx @@ -0,0 +1,3 @@ +export const isGeneral = () => { + return process.env.isGeneral === 'true'; +} \ No newline at end of file diff --git a/package.json b/package.json index d21007ef..4d33679a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "docs": "nx run docs:serve:development", "workers": "nx run workers:serve:development", "cron": "nx run cron:serve:development", - "command": "nx run commands:build && nx run commands:command", + "command": "rm -rf dist/apps/commands && nx run commands:build && nx run commands:command", "prisma-generate": "cd ./libraries/nestjs-libraries/src/database/prisma && prisma generate", "prisma-db-push": "cd ./libraries/nestjs-libraries/src/database/prisma && prisma db push" },