billing - Error Handling - BillingError abstraction
MODIFY
src/integration/billing/stripe/StripeHelpers.ts
src/integration/billing/stripe/StripeHelpers.ts
-import { BillingError, BillingInvoice, BillingOperationResult } from '../../../types/Billing';
+import { BillingError, BillingErrorCode, BillingErrorType, BillingInvoice, BillingOperationResult } from '../../../types/Billing';
import BillingStorage from '../../../storage/mongodb/BillingStorage';
import Stripe from 'stripe';
export default class StripeHelpers {
-
public static async handleStripeOperationResult(tenantID: string, billingInvoice: BillingInvoice, operationResult: BillingOperationResult): Promise<void> {
if (!billingInvoice || !operationResult) {
return;
}
if (!operationResult.succeeded) {
- // The last operation failed
- const error = operationResult.error;
- if (error instanceof Stripe.errors.StripeCardError) {
- // The attempt to pay the invoice failed
- const billingError: BillingError = StripeHelpers.shrinkStripeError(error);
- await BillingStorage.saveLastPaymentFailure(tenantID, billingInvoice.id, billingError);
- }
+ // The operation failed
+ const billingError: BillingError = StripeHelpers.convertToBillingError(operationResult.error);
+ await BillingStorage.saveLastBillingError(tenantID, billingInvoice.id, billingError);
}
}
- public static shrinkStripeError(error: Stripe.StripeCardError): BillingError {
+ public static convertToBillingError(error: Error): BillingError {
// Let's extract the data that we might be interested in
- const { statusCode, type, rawType, message, code, decline_code, payment_intent, payment_method, payment_method_type } = error ;
+ const { errorCode, errorType } = StripeHelpers.guessRootCause(error);
+ const rootCause = StripeHelpers.shrinkRootCause(error);
// Wrap it in a format that we can consume!
return {
- message,
- statusCode, // c.f.: https://stripe.com/docs/api/errors
- context: {
- type,
- rawType,
- code,
- declineCode: decline_code,
- paymentIntentID: payment_intent?.id,
- paymentMethodID: payment_method?.id,
- paymentMethodType: payment_method_type
+ message: error.message,
+ errorType,
+ errorCode,
+ rootCause,
+ };
+ }
+
+ public static guessRootCause(error: Error): { errorType: BillingErrorType, errorCode:BillingErrorCode } {
+ if (error instanceof Stripe.errors.StripeError) {
+ return StripeHelpers.guessStripeRootCause(error);
}
+ // Well this is not a STRIPE error
+ return {
+ errorType: BillingErrorType.APPLICATION_ERROR,
+ errorCode: BillingErrorCode.UNKNOWN_ERROR
};
}
+
+ public static guessStripeRootCause(error: Stripe.StripeError): { errorType: BillingErrorType, errorCode:BillingErrorCode } {
+ let errorType: BillingErrorType, errorCode: BillingErrorCode;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { statusCode, type, rawType, code, decline_code, } = error ;
+ // ----------------------------------------------------------------------------
+ // statusCode potential values: https://stripe.com/docs/api/errors
+ // ----------------------------------------------------------------------------
+ if (statusCode >= 500) {
+ errorType = BillingErrorType.PLATFORM_SERVER_ERROR;
+ errorCode = BillingErrorCode.UNEXPECTED_ERROR;
+ } else {
+ errorType = BillingErrorType.PLATFORM_APPLICATION_ERROR;
+ errorCode = BillingErrorCode.UNKNOWN_ERROR;
+ }
+ // ----------------------------------------------------------------------------
+ // TODO - Extract from the root cause the information that we need!
+ // type: c.f.: https://stripe.com/docs/api/errors/handling
+ // code: c.f.: https://stripe.com/docs/error-codes
+ // declineCode: c,f. https://stripe.com/docs/api/payouts/failures
+ // ----------------------------------------------------------------------------
+ if (error instanceof Stripe.errors.StripeInvalidRequestError) {
+ if (code === 'missing') {
+ // https://stripe.com/docs/error-codes#missing
+ errorCode = BillingErrorCode.NO_PAYMENT_METHOD;
+ }
+ } else if (error instanceof Stripe.errors.StripeCardError) {
+ errorType = BillingErrorType.PLATFORM_PAYMENT_ERROR;
+ errorCode = BillingErrorCode.CARD_ERROR;
+ }
+ // Let's return what we could determine as being the root cause!
+ return {
+ errorType,
+ errorCode
+ };
+ }
+
+ public static shrinkRootCause(error: Error): unknown {
+ // Reduce the size of the data that we save on our side!
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const shrinkError: any = { ...error };
+ if (error instanceof Stripe.errors.StripeError) {
+ // Get rid of some properties
+ delete shrinkError.headers;
+ delete shrinkError.raw;
+ }
+ // Remove null and undefined properties
+ Object.keys(shrinkError).forEach((key) => {
+ const value = shrinkError[key];
+ // eslint-disable-next-line no-undefined
+ if (value === undefined || value === null) {
+ delete shrinkError[key];
+ }
+ });
+ return shrinkError;
+ }
}

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -188,15 +188,15 @@ export default class BillingStorage {
return invoiceMDB._id.toHexString();
}
- public static async saveLastPaymentFailure(tenantID: string, invoiceID: string, error: BillingError): Promise<void> {
+ public static async saveLastBillingError(tenantID: string, invoiceID: string, error: BillingError): Promise<void> {
// Debug
- const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'savePaymentData');
+ const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'saveLastBillingError');
// Check Tenant
await DatabaseUtils.checkTenant(tenantID);
// Set data
- const updatedInvoiceMDB: any = {
- lastPaymentFailure: {
- eventReceivedOn: new Date(),
+ const updatedInvoiceMDB = {
+ lastBillingError: {
+ when: new Date(),
error
}
};
@@ -205,7 +205,7 @@ export default class BillingStorage {
{ '_id': Utils.convertToObjectID(invoiceID) },
{ $set: updatedInvoiceMDB });
// Debug
- await Logging.traceEnd(tenantID, MODULE_NAME, 'savePaymentData', uniqueTimerID, updatedInvoiceMDB);
+ await Logging.traceEnd(tenantID, MODULE_NAME, 'saveLastBillingError', uniqueTimerID, updatedInvoiceMDB);
}
public static async deleteInvoice(tenantID: string, id: string): Promise<void> {

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -128,8 +128,23 @@ export interface BillingPaymentMethod {
}
export interface BillingError {
- // TODO - Billing Error should expose at least the information which is common to all payment platforms
+ // Billing Error should expose the information which is common to all payment platforms
message: string
- statusCode: number,
- context?: unknown; // The context may provide additional information that are platform-specific
+ errorType: BillingErrorType, // SERVER or APPLICATION errors
+ errorCode: BillingErrorCode, // More information about the root cause
+ rootCause?: unknown; // The original error from the payment platform
+}
+
+export enum BillingErrorType {
+ APPLICATION_ERROR = 'application_error', // This is not a STRIPE error
+ PLATFORM_SERVER_ERROR = 'platform_server_error', // Errors 500, server is down or network issues
+ PLATFORM_APPLICATION_ERROR = 'platform_error', // This is a STRIPE error
+ PLATFORM_PAYMENT_ERROR = 'payment_error',
+}
+
+export enum BillingErrorCode {
+ UNKNOWN_ERROR = 'unknown',
+ UNEXPECTED_ERROR = 'unexpected',
+ NO_PAYMENT_METHOD = 'no_payment_method',
+ CARD_ERROR = 'card_error',
}

billing - Error Handling - BillingError abstraction