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)
This commit is contained in:
Ana Sofia Marin Alexandre
2025-02-24 06:10:06 -03:00
committed by GitHub
parent cc0d892de0
commit 1b64f87d75
5 changed files with 71 additions and 12 deletions

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddOnUpdateInEntitlements1739981257356
implements MigrationInterface
{
name = 'AddOnUpdateInEntitlements1739981257356';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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`,
);
}
}

View File

@ -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,
);
}
}

View File

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

View File

@ -54,6 +54,7 @@ export class BillingEntitlement {
(billingCustomer) => billingCustomer.billingEntitlements,
{
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
)
@JoinColumn({

View File

@ -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<Response>();
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(