From f3579cfcaf62ca684f28005152bd62b6b19a4cc0 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 14 Jan 2025 21:41:14 +0700 Subject: [PATCH] feat: nowpayments --- apps/backend/src/api/api.module.ts | 2 + .../src/api/routes/billing.controller.ts | 22 ++++-- .../src/api/routes/public.controller.ts | 13 +++- .../billing/main.billing.component.tsx | 2 + .../components/billing/purchase.crypto.tsx | 24 ++++++ .../src/components/layout/top.menu.tsx | 3 + .../src/crypto/nowpayments.ts | 78 +++++++++++++++++++ .../subscriptions/subscription.service.ts | 15 +++- 8 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 apps/frontend/src/components/billing/purchase.crypto.tsx create mode 100644 libraries/nestjs-libraries/src/crypto/nowpayments.ts diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index c2101d58..e7f9386e 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -27,6 +27,7 @@ import { PublicController } from '@gitroom/backend/api/routes/public.controller' import { RootController } from '@gitroom/backend/api/routes/root.controller'; import { TrackService } from '@gitroom/nestjs-libraries/track/track.service'; import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service'; +import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments'; const authenticatedController = [ UsersController, @@ -65,6 +66,7 @@ const authenticatedController = [ IntegrationManager, TrackService, ShortLinkService, + Nowpayments, ], get exports() { return [...this.imports, ...this.providers]; diff --git a/apps/backend/src/api/routes/billing.controller.ts b/apps/backend/src/api/routes/billing.controller.ts index fb8cfb25..5added0d 100644 --- a/apps/backend/src/api/routes/billing.controller.ts +++ b/apps/backend/src/api/routes/billing.controller.ts @@ -8,6 +8,7 @@ import { ApiTags } from '@nestjs/swagger'; import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; import { Request } from 'express'; +import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments'; @ApiTags('Billing') @Controller('/billing') @@ -15,7 +16,8 @@ export class BillingController { constructor( private _subscriptionService: SubscriptionService, private _stripeService: StripeService, - private _notificationService: NotificationService + private _notificationService: NotificationService, + private _nowpayments: Nowpayments ) {} @Get('/check/:id') @@ -24,10 +26,7 @@ export class BillingController { @Param('id') body: string ) { return { - status: await this._stripeService.checkSubscription( - org.id, - body - ), + status: await this._stripeService.checkSubscription(org.id, body), }; } @@ -39,7 +38,13 @@ export class BillingController { @Req() req: Request ) { const uniqueId = req?.cookies?.track; - return this._stripeService.subscribe(uniqueId, org.id, user.id, body, org.allowTrial); + return this._stripeService.subscribe( + uniqueId, + org.id, + user.id, + body, + org.allowTrial + ); } @Get('/portal') @@ -106,4 +111,9 @@ export class BillingController { body.subscription ); } + + @Get('/crypto') + async crypto(@GetOrgFromRequest() org: Organization) { + return this._nowpayments.createPaymentPage(org.id); + } } diff --git a/apps/backend/src/api/routes/public.controller.ts b/apps/backend/src/api/routes/public.controller.ts index 604178fb..4a5c342a 100644 --- a/apps/backend/src/api/routes/public.controller.ts +++ b/apps/backend/src/api/routes/public.controller.ts @@ -10,6 +10,7 @@ import { Request, Response } from 'express'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service'; +import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments'; @ApiTags('Public') @Controller('/public') @@ -18,7 +19,8 @@ export class PublicController { private _agenciesService: AgenciesService, private _trackService: TrackService, private _agentGraphInsertService: AgentGraphInsertService, - private _postsService: PostsService + private _postsService: PostsService, + private _nowpayments: Nowpayments ) {} @Post('/agent') async createAgent(@Body() body: { text: string; apiKey: string }) { @@ -120,4 +122,13 @@ export class PublicController { track: uniqueId, }); } + + @Post('/crypto/:path') + async cryptoPost( + @Body() body: any, + @Param('path') path: string + ) { + console.log('cryptoPost', body, path); + return this._nowpayments.processPayment(path, body); + } } diff --git a/apps/frontend/src/components/billing/main.billing.component.tsx b/apps/frontend/src/components/billing/main.billing.component.tsx index 5e01720b..f51befba 100644 --- a/apps/frontend/src/components/billing/main.billing.component.tsx +++ b/apps/frontend/src/components/billing/main.billing.component.tsx @@ -26,6 +26,7 @@ import { useUtmUrl } from '@gitroom/helpers/utils/utm.saver'; import { useTolt } from '@gitroom/frontend/components/layout/tolt.script'; import { useTrack } from '@gitroom/react/helpers/use.track'; import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum'; +import { PurchaseCrypto } from '@gitroom/frontend/components/billing/purchase.crypto'; export interface Tiers { month: Array<{ @@ -502,6 +503,7 @@ export const MainBillingComponent: FC<{ ))} + {!!subscription?.id && (
diff --git a/apps/frontend/src/components/billing/purchase.crypto.tsx b/apps/frontend/src/components/billing/purchase.crypto.tsx new file mode 100644 index 00000000..6e38842b --- /dev/null +++ b/apps/frontend/src/components/billing/purchase.crypto.tsx @@ -0,0 +1,24 @@ +import { FC, useCallback, useState } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { Button } from '@gitroom/react/form/button'; + +export const PurchaseCrypto: FC = () => { + const fetch = useFetch(); + const [loading, setLoading] = useState(false); + const load = useCallback(async () => { + setLoading(true); + const data = await (await fetch('/billing/crypto')).json(); + window.location.href = data.invoice_url; + }, []); + + return ( +
+
Purchase a Life-time PRO account with SOL ($199)
+
+ +
+
+ ); +}; diff --git a/apps/frontend/src/components/layout/top.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx index 264fb2d4..c84ba34e 100644 --- a/apps/frontend/src/components/layout/top.menu.tsx +++ b/apps/frontend/src/components/layout/top.menu.tsx @@ -79,6 +79,9 @@ export const TopMenu: FC = () => { if (f.requireBilling && !billingEnabled) { return false; } + if (f.name === 'Billing' && user?.isLifetime) { + return false; + } if (f.role) { return f.role.includes(user?.role!); } diff --git a/libraries/nestjs-libraries/src/crypto/nowpayments.ts b/libraries/nestjs-libraries/src/crypto/nowpayments.ts new file mode 100644 index 00000000..47e18aa2 --- /dev/null +++ b/libraries/nestjs-libraries/src/crypto/nowpayments.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; +import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; + +export interface ProcessPayment { + payment_id: number; + payment_status: string; + pay_address: string; + price_amount: number; + price_currency: string; + pay_amount: number; + actually_paid: number; + pay_currency: string; + order_id: string; + order_description: string; + purchase_id: string; + created_at: string; + updated_at: string; + outcome_amount: number; + outcome_currency: string; +} + +@Injectable() +export class Nowpayments { + constructor(private _subscriptionService: SubscriptionService) {} + + async processPayment(path: string, body: ProcessPayment) { + const decrypt = AuthService.verifyJWT(path) as any; + if (!decrypt || !decrypt.order_id) { + return; + } + + if ( + decrypt.payment_status !== 'confirmed' && + decrypt.payment_status !== 'finished' + ) { + return; + } + + const [org, make] = body.order_id.split('_'); + await this._subscriptionService.lifeTime(org, make, 'PRO'); + return body; + } + + async createPaymentPage(orgId: string) { + const onlyId = makeId(5); + const make = orgId + '_' + onlyId; + const signRequest = AuthService.signJWT({ order_id: make }); + + const { id, invoice_url } = await ( + await fetch('https://api.nowpayments.io/v1/invoice', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': process.env.NOWPAYMENTS_API_KEY!, + }, + body: JSON.stringify({ + price_amount: process.env.NOWPAYMENTS_AMOUNT, + price_currency: 'USD', + order_id: make, + pay_currency: 'SOL', + order_description: 'Lifetime deal account for Postiz', + ipn_callback_url: + process.env.NEXT_PUBLIC_BACKEND_URL + + `/public/crypto/${signRequest}`, + success_url: process.env.FRONTEND_URL + `/launches?check=${onlyId}`, + cancel_url: process.env.FRONTEND_URL, + }), + }) + ).json(); + + return { + id, + invoice_url, + }; + } +} 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 6b5ae00d..e7bb98fa 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts @@ -13,7 +13,7 @@ export class SubscriptionService { constructor( private readonly _subscriptionRepository: SubscriptionRepository, private readonly _integrationService: IntegrationService, - private readonly _organizationService: OrganizationService, + private readonly _organizationService: OrganizationService ) {} getSubscriptionByOrganizationId(organizationId: string) { @@ -188,6 +188,19 @@ export class SubscriptionService { }; } + async lifeTime(orgId: string, identifier: string, subscription: any) { + return this.createOrUpdateSubscription( + identifier, + identifier, + pricing[subscription].channel!, + subscription, + 'YEARLY', + null, + identifier, + orgId + ); + } + async addSubscription(orgId: string, userId: string, subscription: any) { await this._subscriptionRepository.setCustomerId(orgId, orgId); return this.createOrUpdateSubscription(