From 9afeb1e0f88b3b54152f8c38d0afcdc5e9595316 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 30 Nov 2025 00:17:02 +0700 Subject: [PATCH] feat: billing change --- .../src/api/routes/billing.controller.ts | 15 +++++ .../billing/main.billing.component.tsx | 60 ++++++++++++++++- .../src/services/stripe.service.ts | 66 +++++++++++++++++++ 3 files changed, 138 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/api/routes/billing.controller.ts b/apps/backend/src/api/routes/billing.controller.ts index 039513fb..2ba8eb1f 100644 --- a/apps/backend/src/api/routes/billing.controller.ts +++ b/apps/backend/src/api/routes/billing.controller.ts @@ -9,6 +9,7 @@ import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.req import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; import { Request } from 'express'; import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; @ApiTags('Billing') @Controller('/billing') @@ -30,6 +31,20 @@ export class BillingController { }; } + @Get('/check-discount') + async checkDiscount(@GetOrgFromRequest() org: Organization) { + return { + offerCoupon: !(await this._stripeService.checkDiscount(org.paymentId)) + ? false + : AuthService.signJWT({ discount: true }), + }; + } + + @Post('/apply-discount') + async applyDiscount(@GetOrgFromRequest() org: Organization) { + await this._stripeService.applyDiscount(org.paymentId); + } + @Post('/finish-trial') async finishTrial(@GetOrgFromRequest() org: Organization) { try { diff --git a/apps/frontend/src/components/billing/main.billing.component.tsx b/apps/frontend/src/components/billing/main.billing.component.tsx index 3485012a..a52b6e63 100644 --- a/apps/frontend/src/components/billing/main.billing.component.tsx +++ b/apps/frontend/src/components/billing/main.billing.component.tsx @@ -135,6 +135,38 @@ export const Features: FC<{ ); }; + +const Accept: FC<{ resolve: (res: boolean) => void }> = ({ resolve }) => { + const [loading, setLoading] = useState(false); + const fetch = useFetch(); + const toaster = useToaster(); + + const apply = useCallback(async () => { + setLoading(true); + await fetch('/billing/apply-discount', { + method: 'POST', + }); + + resolve(true); + toaster.show('50% discount applied successfully'); + }, []); + + return ( +
+
+ Would you accept 50% discount for 3 months instead? 🙏🏻 +
+
+ + +
+
+ ); +}; const Info: FC<{ proceed: (feedback: string) => void; }> = (props) => { @@ -277,13 +309,34 @@ export const MainBillingComponent: FC<{ if ( subscription?.cancelAt || (await deleteDialog( - `Are you sure you want to cancel your subscription? ${messages.join( - ', ' - )}`, + `Are you sure you want to cancel your subscription? + ${messages.join(', ')}`, 'Yes, cancel', 'Cancel Subscription' )) ) { + const checkDiscount = await ( + await fetch('/billing/check-discount') + ).json(); + if (checkDiscount.offerCoupon) { + const info = await new Promise((res) => { + modal.openModal({ + title: 'Before you cancel', + withCloseButton: true, + classNames: { + modal: 'bg-transparent text-textColor', + }, + children: , + }); + }); + + modal.closeAll(); + + if (info) { + return; + } + } + const info = await new Promise((res) => { modal.openModal({ title: t( @@ -297,6 +350,7 @@ export const MainBillingComponent: FC<{ children: res(e)} />, }); }); + setLoading(true); const { cancel_at } = await ( await fetch('/billing/cancel', { diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index 5a7f5949..e27e0a8b 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -475,6 +475,72 @@ export class StripeService { }); } + async checkDiscount(customer: string) { + if (!process.env.STRIPE_DISCOUNT_ID) { + return false; + } + + const list = await stripe.charges.list({ + customer, + limit: 1, + }); + + if (!list.data.filter(f => f.amount > 1000).length) { + return false; + } + + const currentUserSubscription = { + data: ( + await stripe.subscriptions.list({ + customer, + status: 'all', + expand: ['data.discounts'], + }) + ).data.find((f) => f.status === 'active' || f.status === 'trialing'), + }; + + if (!currentUserSubscription) { + return false; + } + + if ( + currentUserSubscription.data?.items.data[0]?.price.recurring?.interval === + 'year' || + currentUserSubscription.data?.discounts.length + ) { + return false; + } + + return true; + } + + async applyDiscount(customer: string) { + const check = this.checkDiscount(customer); + if (!check) { + return false; + } + + const currentUserSubscription = { + data: ( + await stripe.subscriptions.list({ + customer, + status: 'all', + expand: ['data.discounts'], + }) + ).data.find((f) => f.status === 'active' || f.status === 'trialing'), + }; + + await stripe.subscriptions.update(currentUserSubscription.data.id, { + discounts: [ + { + coupon: process.env.STRIPE_DISCOUNT_ID!, + }, + ], + }); + + return true; + } + async checkSubscription(organizationId: string, subscriptionId: string) { const orgValue = await this._subscriptionService.checkSubscription( organizationId,