38 add billing webhook endpoint (#4158)

* Add self billing feature flag

* Add two core tables for billing

* Remove useless imports

* Remove graphql decorators

* Rename subscriptionProduct table

* WIP: Add stripe config

* Add controller to get product prices

* Add billing service

* Remove unecessary package

* Simplify stripe service

* Code review returns

* Use nestjs param

* Rename subscription to basePlan

* Rename env variable

* Add checkout endpoint

* Remove resolver

* Merge controllers

* Fix security issue

* Handle missing url error

* Add workspaceId in checkout metadata

* Add BILLING_STRIPE_WEBHOOK_SECRET env variable

* WIP: add webhook endpoint

* Fix body parser

* Create Billing Subscription on payment success

* Set subscriptionStatus active on webhook

* Add useful log

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2024-02-24 17:30:32 +01:00
committed by GitHub
parent c96e210ef1
commit 05c206073d
7 changed files with 159 additions and 30 deletions
@@ -3,6 +3,10 @@ import {
Controller,
Get,
Param,
Headers,
Req,
RawBodyRequest,
Logger,
Post,
Res,
UseGuards,
@@ -15,6 +19,7 @@ import {
BillingService,
PriceData,
RecurringInterval,
WebhookEvent,
} from 'src/core/billing/billing.service';
import { StripeService } from 'src/core/billing/stripe/stripe.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
@@ -24,6 +29,8 @@ import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
@Controller('billing')
export class BillingController {
protected readonly logger = new Logger(BillingController.name);
constructor(
private readonly stripeService: StripeService,
private readonly billingService: BillingService,
@@ -97,8 +104,10 @@ export class BillingController {
},
],
mode: 'subscription',
metadata: {
workspaceId: user.defaultWorkspace.id,
subscription_data: {
metadata: {
workspaceId: user.defaultWorkspace.id,
},
},
customer_email: user.email,
success_url: frontBaseUrl,
@@ -110,7 +119,48 @@ export class BillingController {
return;
}
this.logger.log(`Stripe Checkout Session Url Redirection: ${session.url}`);
res.redirect(303, session.url);
}
@Post('/webhooks')
async handleWebhooks(
@Headers('stripe-signature') signature: string,
@Req() req: RawBodyRequest<Request>,
@Res() res: Response,
) {
if (!req.rawBody) {
res.status(400).send('Missing raw body');
return;
}
const event = this.stripeService.constructEventFromPayload(
signature,
req.rawBody,
);
if (event.type === WebhookEvent.CUSTOMER_SUBSCRIPTION_UPDATED) {
if (event.data.object.status !== 'active') {
res.status(402).send('Payment did not succeeded');
return;
}
const workspaceId = event.data.object.metadata?.workspaceId;
if (!workspaceId) {
res.status(404).send('Missing workspaceId in webhook event metadata');
return;
}
await this.billingService.createBillingSubscription(
workspaceId,
event.data,
);
res.status(200).send('Subscription successfully updated');
}
}
}