feat: billing change

This commit is contained in:
Nevo David 2025-11-30 00:17:02 +07:00
parent 6ab8a2471b
commit 9afeb1e0f8
3 changed files with 138 additions and 3 deletions

View File

@ -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 {

View File

@ -135,6 +135,38 @@ export const Features: FC<{
</div>
);
};
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 (
<div>
<div className="mb-[20px]">
Would you accept 50% discount for 3 months instead? 🙏🏻
</div>
<div className="flex gap-[10px]">
<Button loading={loading} onClick={apply}>
Apply 50% discount for 3 months
</Button>
<Button onClick={() => resolve(false)} className="!bg-red-800">
Cancel my subscription
</Button>
</div>
</div>
);
};
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: <Accept resolve={res} />,
});
});
modal.closeAll();
if (info) {
return;
}
}
const info = await new Promise((res) => {
modal.openModal({
title: t(
@ -297,6 +350,7 @@ export const MainBillingComponent: FC<{
children: <Info proceed={(e) => res(e)} />,
});
});
setLoading(true);
const { cancel_at } = await (
await fetch('/billing/cancel', {

View File

@ -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,