From 1b64f87d7556dcaf42086296eb4637a6c52cc082 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Mon, 24 Feb 2025 06:10:06 -0300 Subject: [PATCH] Add error management to the Billing Package (#10343) Solves https://github.com/twentyhq/core-team-issues/issues/403 **TLDR:** Enhance error management in Billing and when a customer is updated it updates automatically the Stripecustomer id in the entitlements. - Add Billing exceptions to filter. - Add onUpdate for billing customer and entitlement. - Remember to run the migrations with is BILLING_ENABLED set to true. **In order to test (a simple test case)** - Ensure that the environment variables for Sentry and Billing are set, ensuring that SENTRY_ENVIRONMENT=staging - Run the server, the worker and the stripe cli - Do a database reset with IS_BILLING_ENABLED set to true - Go to stripe in test mode and update a random price description, this causes an exception because you are trying to write a price of. a product that doesn't exists in the database - You should see an error in Sentry: ![image](https://github.com/user-attachments/assets/7b3c8c7f-6628-4a20-9889-a691e7838d79) --- ...1739981257356-addOnUpdateInEntitlements.ts | 25 ++++++++++++++++ .../billing/billing.controller.ts | 29 +++++++++++++------ .../core-modules/billing/billing.exception.ts | 3 ++ .../entities/billing-entitlement.entity.ts | 1 + .../filters/billing-api-exception.filter.ts | 25 ++++++++++++++-- 5 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/billing/1739981257356-addOnUpdateInEntitlements.ts diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/billing/1739981257356-addOnUpdateInEntitlements.ts b/packages/twenty-server/src/database/typeorm/core/migrations/billing/1739981257356-addOnUpdateInEntitlements.ts new file mode 100644 index 000000000..b8bd5756d --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/billing/1739981257356-addOnUpdateInEntitlements.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddOnUpdateInEntitlements1739981257356 + implements MigrationInterface +{ + name = 'AddOnUpdateInEntitlements1739981257356'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingEntitlement" ADD CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356" FOREIGN KEY ("stripeCustomerId") REFERENCES "core"."billingCustomer"("stripeCustomerId") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingEntitlement" ADD CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356" FOREIGN KEY ("stripeCustomerId") REFERENCES "core"."billingCustomer"("stripeCustomerId") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts index c579fcb86..3b7f35d0f 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts @@ -47,23 +47,34 @@ export class BillingController { @Res() res: Response, ) { if (!req.rawBody) { - res.status(400).end(); - - return; + throw new BillingException( + 'Missing request body', + BillingExceptionCode.BILLING_MISSING_REQUEST_BODY, + ); } - const event = this.stripeWebhookService.constructEventFromPayload( - signature, - req.rawBody, - ); try { + const event = this.stripeWebhookService.constructEventFromPayload( + signature, + req.rawBody, + ); const result = await this.handleStripeEvent(event); res.status(200).send(result).end(); } catch (error) { - if (error instanceof BillingException) { - res.status(404).end(); + if ( + error instanceof BillingException || + error instanceof Stripe.errors.StripeError + ) { + throw error; } + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error); + + throw new BillingException( + errorMessage, + BillingExceptionCode.BILLING_UNHANDLED_ERROR, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts index fceca015c..35b26ea27 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts @@ -16,4 +16,7 @@ export enum BillingExceptionCode { BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND', BILLING_ACTIVE_SUBSCRIPTION_NOT_FOUND = 'BILLING_ACTIVE_SUBSCRIPTION_NOT_FOUND', BILLING_METER_EVENT_FAILED = 'BILLING_METER_EVENT_FAILED', + BILLING_MISSING_REQUEST_BODY = 'BILLING_MISSING_REQUEST_BODY', + BILLING_UNHANDLED_ERROR = 'BILLING_UNHANDLED_ERROR', + BILLING_STRIPE_ERROR = 'BILLING_STRIPE_ERROR', } diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts index f89153cc7..b093064b8 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts @@ -54,6 +54,7 @@ export class BillingEntitlement { (billingCustomer) => billingCustomer.billingEntitlements, { onDelete: 'CASCADE', + onUpdate: 'CASCADE', }, ) @JoinColumn({ diff --git a/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts index 020949e01..444605e1a 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts @@ -17,22 +17,41 @@ export class BillingRestApiExceptionFilter implements ExceptionFilter { private readonly httpExceptionHandlerService: HttpExceptionHandlerService, ) {} - catch(exception: BillingException, host: ArgumentsHost) { + catch( + exception: BillingException | Stripe.errors.StripeError, + host: ArgumentsHost, + ) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); + if (exception instanceof Stripe.errors.StripeError) { + return this.httpExceptionHandlerService.handleError( + { + code: BillingExceptionCode.BILLING_STRIPE_ERROR, + message: exception.message, + name: 'StripeError', + }, + response, + 400, + ); + } + switch (exception.code) { case BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND: + case BillingExceptionCode.BILLING_ACTIVE_SUBSCRIPTION_NOT_FOUND: + case BillingExceptionCode.BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND: + case BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND: + case BillingExceptionCode.BILLING_PLAN_NOT_FOUND: return this.httpExceptionHandlerService.handleError( exception, response, 404, ); - case BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND: + case BillingExceptionCode.BILLING_METER_EVENT_FAILED: return this.httpExceptionHandlerService.handleError( exception, response, - 404, + 400, ); default: return this.httpExceptionHandlerService.handleError(