stripe - StripeHelpers - Card error handling
MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -10,13 +10,14 @@ import BillingStorage from '../../../storage/mongodb/BillingStorage';
import Constants from '../../../utils/Constants';
import Cypher from '../../../utils/Cypher';
import { DataResult } from '../../../types/DataResult';
-import Decimal from 'decimal.js';
+import { Decimal } from 'decimal.js';
import I18nManager from '../../../utils/I18nManager';
import Logging from '../../../utils/Logging';
import { Request } from 'express';
import { ServerAction } from '../../../types/Server';
import Stripe from 'stripe';
import { StripeBillingSetting } from '../../../types/Setting';
+import StripeHelpers from './StripeHelpers';
import Transaction from '../../../types/Transaction';
import User from '../../../types/User';
import UserStorage from '../../../storage/mongodb/UserStorage';
@@ -438,17 +439,25 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
public async chargeInvoice(billingInvoice: BillingInvoice): Promise<BillingInvoice> {
await this.checkConnection();
- const billingOperationResult: BillingOperationResult = await this._chargeStripeInvoice(billingInvoice.invoiceID);
- billingInvoice = await this.synchronizeAsBillingInvoice(billingInvoice.invoiceID, false);
- if (!billingOperationResult.succeeded) {
- // TODO - how to determine the root cause of the error
- // c.f.: https://stripe.com/docs/error-codes#missing
- await BillingStorage.saveLastPaymentFailure(this.tenantID, billingInvoice.id, billingOperationResult);
+ const operationResult: BillingOperationResult = await this._chargeStripeInvoice(billingInvoice.invoiceID);
+ if (!operationResult?.succeeded && operationResult?.error) {
+ await Logging.logError({
+ tenantID: this.tenantID,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_TRANSACTION,
+ module: MODULE_NAME, method: 'billInvoiceItem',
+ message: `Payment attempt failed - stripe invoice: '${billingInvoice.invoiceID}'`,
+ detailedMessages: { error: operationResult.error.message, stack: operationResult.error.stack }
+ });
}
+
+ billingInvoice = await this.synchronizeAsBillingInvoice(billingInvoice.invoiceID, false);
+ await StripeHelpers.handleStripeOperationResult(this.tenantID, billingInvoice, operationResult);
return billingInvoice;
}
private async _chargeStripeInvoice(invoiceID: string): Promise<BillingOperationResult> {
+ try {
// Fetch the invoice from stripe (do NOT TRUST the local copy)
let stripeInvoice: Stripe.Invoice = await this.stripe.invoices.retrieve(invoiceID);
// Check the current invoice status
@@ -461,39 +470,19 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
if (stripeInvoice.status === 'open') {
// Set the payment options
const paymentOptions: Stripe.InvoicePayParams = {};
- try {
stripeInvoice = await this.stripe.invoices.pay(invoiceID, paymentOptions);
- } catch (error) {
- return this.shrinkStripeFailure(error);
- }
}
}
return {
succeeded: true,
internalData: stripeInvoice
};
- }
-
- private shrinkStripeFailure(error): BillingOperationResult {
- // Let's extract the data that we might be interested in
- const { type, rawType, message, code, decline_code, payment_intent, payment_method, payment_method_type } = error;
- // Wrap it in a format that we can consume!
- const billingOperationResult: BillingOperationResult = {
- succeeded: false,
- error: {
- message,
- context: {
- type,
- rawType,
- code,
- declineCode: decline_code,
- paymentIntentID: payment_intent?.id,
- paymentMethodID: payment_method?.id,
- paymentMethodType: payment_method_type
- }
- }
+ } catch (error) {
+ return {
+ succeeded: true,
+ error,
};
- return billingOperationResult;
+ }
}
public async setupPaymentMethod(user: User, paymentMethodId: string): Promise<BillingOperationResult> {
@@ -587,13 +576,10 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
module: MODULE_NAME, method: '_createSetupIntent',
message: `Stripe operation failed - ${error?.message as string}`
});
- const billingOperationResult: BillingOperationResult = {
+ return {
succeeded: false,
- error: {
- message: error?.message
- }
+ error
};
- return billingOperationResult;
}
}
@@ -626,22 +612,19 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
succeeded: true,
internalData: paymentMethod
};
- } catch (e) {
+ } catch (error) {
// catch stripe errors and send the information back to the client
await Logging.logError({
tenantID: this.tenantID,
action: ServerAction.BILLING_SETUP_PAYMENT_METHOD,
actionOnUser: user,
module: MODULE_NAME, method: '_attachPaymentMethod',
- message: `Stripe operation failed - ${e?.message as string}`
+ message: `Stripe operation failed - ${error?.message as string}`
});
- const billingOperationResult: BillingOperationResult = {
+ return {
succeeded: false,
- error: {
- message: e?.message
- }
+ error
};
- return billingOperationResult;
}
}
@@ -964,12 +947,11 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
// Let's create a new draft invoice (if none has been found)
stripeInvoice = await this._createStripeInvoice(customerID, userID, this.buildIdemPotencyKey(idemPotencyKey));
}
- let paymentOperationResult: BillingOperationResult;
+ let operationResult: BillingOperationResult;
if (this.settings.immediateBillingAllowed) {
// Let's try to bill the stripe invoice using the default payment method of the customer
- try {
- paymentOperationResult = await this._chargeStripeInvoice(stripeInvoice.id);
- } catch (error) {
+ operationResult = await this._chargeStripeInvoice(stripeInvoice.id);
+ if (!operationResult?.succeeded && operationResult?.error) {
await Logging.logError({
tenantID: this.tenantID,
user: user.id,
@@ -977,16 +959,14 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
action: ServerAction.BILLING_TRANSACTION,
module: MODULE_NAME, method: 'billInvoiceItem',
message: `Payment attempt failed - stripe invoice: '${stripeInvoice?.id}'`,
- detailedMessages: { error: error.message, stack: error.stack }
+ detailedMessages: { error: operationResult.error.message, stack: operationResult.error.stack }
});
}
}
// Let's replicate some information on our side
const billingInvoice = await this.synchronizeAsBillingInvoice(stripeInvoice.id, false);
- // We have now a Billing Invoice - Let's update it with details about the last payment failure (if any)
- if (!paymentOperationResult?.succeeded && paymentOperationResult?.error) {
- await BillingStorage.saveLastPaymentFailure(this.tenantID, billingInvoice.id, paymentOperationResult.error);
- }
+ // We have now a Billing Invoice - Let's update it with details about the last operation result
+ await StripeHelpers.handleStripeOperationResult(this.tenantID, billingInvoice, operationResult);
// Return the billing invoice
return billingInvoice;
}

ADD
src/integration/billing/stripe/StripeHelpers.ts
+import { BillingInvoice, BillingOperationResult } from '../../../types/Billing';
+
+import BillingStorage from '../../../storage/mongodb/BillingStorage';
+import Stripe from 'stripe';
+
+export interface BillingError {
+ message: string
+ statusCode: number,
+ context?: unknown; // e.g.: payment ==> last_payment_error
+}
+
+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);
+ }
+ }
+ }
+
+ public static shrinkStripeError(error: Stripe.StripeCardError): 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 ;
+ // 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
+ }
+ };
+ }
+}

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
import { BillingInvoice, BillingInvoiceDocument, BillingInvoiceStatus } from '../../types/Billing';
import global, { FilterParams } from '../../types/GlobalType';
+import { BillingError } from '../../integration/billing/stripe/StripeHelpers';
import Constants from '../../utils/Constants';
import { DataResult } from '../../types/DataResult';
import DatabaseUtils from './DatabaseUtils';
@@ -188,7 +189,7 @@ export default class BillingStorage {
return invoiceMDB._id.toHexString();
}
- public static async saveLastPaymentFailure(tenantID: string, invoiceID: string, error: unknown): Promise<void> {
+ public static async saveLastPaymentFailure(tenantID: string, invoiceID: string, error: BillingError): Promise<void> {
// Debug
const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'savePaymentData');
// Check Tenant

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -113,8 +113,8 @@ export interface BillingInvoiceDocument {
export interface BillingOperationResult {
succeeded: boolean
- error?: BillingError
- internalData?: unknown; // Object returned by the concrete implementation - e.g.: STRIPE
+ error?: Error
+ internalData?: unknown // an object returned by the concrete implementation - e.g.: STRIPE
}
export interface BillingPaymentMethod {
@@ -131,7 +131,3 @@ export interface BillingPaymentMethodResult {
result: BillingPaymentMethod[];
count: number;
}
-export interface BillingError {
- message: string
- context?: unknown; // e.g.: payment ==> last_payment_error
-}

stripe - StripeHelpers - Card error handling