keep track of last payment failure
MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -466,11 +466,15 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
return true;
}
- public async chargeInvoice(invoice: BillingInvoice): Promise<BillingInvoice> {
+ public async chargeInvoice(billingInvoice: BillingInvoice): Promise<BillingInvoice> {
await this.checkConnection();
try {
- const stripeInvoice = await this._chargeStripeInvoice(invoice.invoiceID);
- const billingInvoice = await this._replicateStripeInvoice(invoice.userID, stripeInvoice.id);
+ const billingOperationResult: BillingOperationResult = await this._chargeStripeInvoice(billingInvoice.invoiceID);
+ billingInvoice = await this._replicateStripeInvoice(billingInvoice.userID, billingInvoice.invoiceID);
+ if (!billingOperationResult.succeeded) {
+ // TODO - how to determine the root cause of the error
+ await BillingStorage.saveLastPaymentFailure(this.tenantID, billingInvoice.id, billingOperationResult);
+ }
return billingInvoice;
} catch (error) {
throw new BackendError({
@@ -483,7 +487,7 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
}
}
- private async _chargeStripeInvoice(invoiceID: string): Promise<Stripe.Invoice> {
+ private async _chargeStripeInvoice(invoiceID: string): Promise<BillingOperationResult> {
// 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
@@ -496,10 +500,39 @@ 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 stripeInvoice;
+ }
+ 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
+ }
+ }
+ };
+ return billingOperationResult;
}
public async setupPaymentMethod(user: User, paymentMethodId: string): Promise<BillingOperationResult> {
@@ -546,21 +579,22 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
succeeded: true,
internalData: setupIntent
};
- } 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: '_createSetupIntent',
- message: `Stripe operation failed - ${e?.message as string}`
+ message: `Stripe operation failed - ${error?.message as string}`
});
- return {
+ const billingOperationResult: BillingOperationResult = {
succeeded: false,
error: {
- message: e?.message
+ message: error?.message
}
};
+ return billingOperationResult;
}
}
@@ -602,12 +636,13 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
module: MODULE_NAME, method: '_attachPaymentMethod',
message: `Stripe operation failed - ${e?.message as string}`
});
- return {
+ const billingOperationResult: BillingOperationResult = {
succeeded: false,
error: {
message: e?.message
}
};
+ return billingOperationResult;
}
}
@@ -822,10 +857,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, this.buildIdemPotencyKey(idemPotencyKey));
}
+ let billingOperationResult: BillingOperationResult;
if (this.settings.immediateBillingAllowed) {
// Let's try to bill the stripe invoice using the default payment method of the customer
try {
- await this._chargeStripeInvoice(stripeInvoice.id);
+ billingOperationResult = await this._chargeStripeInvoice(stripeInvoice.id);
} catch (error) {
await Logging.logError({
tenantID: this.tenantID,
@@ -840,6 +876,10 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
}
// Let's replicate some information on our side
const billingInvoice = await this._replicateStripeInvoice(userID, stripeInvoice.id);
+ // Let's persist last payment failure (if any)
+ if (!billingOperationResult?.succeeded && billingOperationResult?.error) {
+ await BillingStorage.saveLastPaymentFailure(this.tenantID, billingInvoice.id, billingOperationResult.error);
+ }
// Return the billing invoice
return billingInvoice;
}

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -162,7 +162,7 @@ export default class BillingStorage {
const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'saveInvoice');
// Build Request
// Properties to save
- const invoiceMDB = {
+ const invoiceMDB: any = {
_id: invoiceToSave.id ? Utils.convertToObjectID(invoiceToSave.id) : new ObjectID(),
invoiceID: invoiceToSave.invoiceID,
number: invoiceToSave.number,
@@ -177,16 +177,36 @@ export default class BillingStorage {
downloadUrl: invoiceToSave.downloadUrl
};
// Modify and return the modified document
- await global.database.getCollection<BillingInvoice>(tenantID, 'invoices').findOneAndReplace(
+ await global.database.getCollection<BillingInvoice>(tenantID, 'invoices').findOneAndUpdate(
{ _id: invoiceMDB._id },
- invoiceMDB,
- { upsert: true }
+ { $set: invoiceMDB },
+ { upsert: true, returnOriginal: false }
);
// Debug
await Logging.traceEnd(tenantID, MODULE_NAME, 'saveInvoice', uniqueTimerID, invoiceMDB);
return invoiceMDB._id.toHexString();
}
+ public static async saveLastPaymentFailure(tenantID: string, invoiceID: string, error: unknown): Promise<void> {
+ // Debug
+ const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'savePaymentData');
+ // Check Tenant
+ await DatabaseUtils.checkTenant(tenantID);
+ // Set data
+ const updatedInvoiceMDB: any = {
+ lastPaymentFailure: {
+ eventReceivedOn: new Date(),
+ error
+ }
+ };
+ // Modify and return the modified document
+ await global.database.getCollection(tenantID, 'invoices').findOneAndUpdate(
+ { '_id': Utils.convertToObjectID(invoiceID) },
+ { $set: updatedInvoiceMDB });
+ // Debug
+ await Logging.traceEnd(tenantID, MODULE_NAME, 'savePaymentData', uniqueTimerID, updatedInvoiceMDB);
+ }
+
public static async saveInvoiceDocument(tenantID: string, invoiceDocumentToSave: BillingInvoiceDocument): Promise<BillingInvoiceDocument> {
// Debug
const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'saveInvoiceDocument');

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -112,8 +112,11 @@ export interface BillingInvoiceDocument {
export interface BillingOperationResult {
succeeded: boolean
- error?: {
- message: string
- };
+ error?: BillingError
internalData?: unknown; // Object returned by the concrete implementation - e.g.: STRIPE
}
+
+export interface BillingError {
+ message: string
+ context?: unknown; // e.g.: payment ==> last_payment_error
+}

keep track of last payment failure