stripe - billing api cleanup
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
-import { BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingOperationResult, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
+import { BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingInvoiceItem, BillingOperationResult, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
import User, { UserStatus } from '../../types/User';
import BackendError from '../../exception/BackendError';
@@ -536,18 +536,12 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
abstract checkIfUserCanBeDeleted(user: User): Promise<boolean>;
- // abstract getBillingUser(user: User): Promise<BillingUser>;
-
abstract getBillingUserByInternalID(id: string): Promise<BillingUser>;
abstract getUsers(): Promise<BillingUser[]>;
abstract getUser(user: User): Promise<BillingUser>;
- // TODO - there is no use-case for such method - the billing data provides the customerID
- // abstract getUserByEmail(email: string): Promise<BillingUser>;
-
-
abstract createUser(user: User): Promise<BillingUser>;
abstract updateUser(user: User): Promise<BillingUser>;
@@ -562,18 +556,16 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
abstract getUpdatedInvoiceIDsInBilling(billingUser?: BillingUser): Promise<string[]>;
- // abstract createInvoiceItem(user: BillingUser, invoiceID: string, invoiceItem: BillingInvoiceItem, idempotencyKey?: string | number): Promise<BillingInvoiceItem>;
-
- // eslint-disable-next-line max-len
- // abstract createInvoice(user: BillingUser, invoiceItem: BillingInvoiceItem, idempotencyKey?: string | number): Promise<{ invoice: BillingInvoice; invoiceItem: BillingInvoiceItem }>;
+ abstract billInvoiceItems(user: User, billingInvoiceItems: Array<BillingInvoiceItem>, idemPotencyKey?: string): Promise<BillingInvoice>;
abstract downloadInvoiceDocument(invoice: BillingInvoice): Promise<BillingInvoiceDocument>;
abstract finalizeInvoice(invoice: BillingInvoice): Promise<string>;
- // ##CR - TO BE CLARIFIED - clarify the return type!
abstract setupPaymentMethod(user: User, paymentMethodId: string): Promise<BillingOperationResult>;
- abstract chargeInvoice(invoice: BillingInvoice): Promise<unknown>;
- abstract handleBillingEvent(req: Request): boolean;
+
+ abstract chargeInvoice(invoice: BillingInvoice): Promise<BillingInvoice>;
+
+ abstract consumeBillingEvent(req: Request): Promise<boolean>;
}

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -316,22 +316,6 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
return collectedInvoiceIDs;
}
- // public async createInvoice(user: BillingUser, idempotencyKey?: string | number): Promise<BillingInvoice> {
- // if (!user) {
- // throw new BackendError({
- // source: Constants.CENTRAL_SERVER,
- // action: ServerAction.BILLING_CREATE_INVOICE,
- // module: MODULE_NAME, method: 'createInvoice',
- // message: 'Billing User not provided',
- // });
- // }
- // await this.checkConnection();
- // // Let's create the STRIPE invoice
- // const stripeInvoice: Stripe.Invoice = await this._createStripeInvoice(user.billingData.customerID, idempotencyKey);
- // // Let's update the data which is replicated on our side
- // return this._replicateStripeInvoice(user.userID, stripeInvoice.id);
- // }
-
private async _createStripeInvoice(customerID: string, idempotencyKey?: string | number): Promise<Stripe.Invoice> {
// Let's create the STRIPE invoice
const stripeInvoice: Stripe.Invoice = await this.stripe.invoices.create({
@@ -384,52 +368,6 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
return BillingStorage.getInvoice(this.tenantID, invoiceId);
}
- // public async createPendingInvoiceItem(user: BillingUser, invoiceItem: BillingInvoiceItem, idempotencyKey?: string | number): Promise<BillingInvoiceItem> {
- // // TODO - We create an item while the invoice ID is not yet known!
- // return this.createInvoiceItem(user, null, invoiceItem, idempotencyKey);
- // }
-
- // public async createInvoiceItem(user: BillingUser, invoiceID: string, invoiceItem: BillingInvoiceItem, idempotencyKey?: string | number): Promise<BillingInvoiceItem> {
- // await this.checkConnection();
- // if (!invoiceItem) {
- // throw new BackendError({
- // source: Constants.CENTRAL_SERVER,
- // action: ServerAction.BILLING_CREATE_INVOICE_ITEM,
- // module: MODULE_NAME, method: 'createInvoiceItem',
- // message: 'Invoice item not provided',
- // });
- // }
- // try {
-
- // const invoiceItemCreateParams: Stripe.InvoiceItemCreateParams = {
- // customer: user.billingData.customerID,
- // currency: this.settings.currency.toLocaleLowerCase(),
- // amount: invoiceItem.pricingData.amount,
- // description: invoiceItem.description,
- // tax_rates: this.getTaxRateIds()
- // };
- // // STRIPE throws an exception when invoice is set to null.
- // if (invoiceID) {
- // // Make sure to only add that property when updating an existing invoice
- // invoiceItemCreateParams.invoice = invoiceID;
- // }
- // // returns the newly created invoice item
- // // const stripeInvoiceItem = await this._createStripeInvoiceItem(invoiceItemCreateParams, idempotencyKey);
- // // return stripeInvoiceItem;
- // await this._createStripeInvoiceItem(invoiceItemCreateParams, idempotencyKey);
- // return invoiceItem;
-
- // } catch (e) {
- // throw new BackendError({
- // source: Constants.CENTRAL_SERVER,
- // action: ServerAction.BILLING_CREATE_INVOICE_ITEM,
- // module: MODULE_NAME, method: 'createInvoiceItem',
- // message: 'Failed to create invoice item',
- // detailedMessages: { error: e.message, stack: e.stack }
- // });
- // }
- // }
-
private async _createStripeInvoiceItem(parameters: Stripe.InvoiceItemCreateParams, idempotencyKey: string | number): Promise<Stripe.InvoiceItem> {
// Let's create the line item
const stripeInvoiceItem = await this.stripe.invoiceItems.create(parameters, {
@@ -490,7 +428,7 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
}
}
- public handleBillingEvent(req: Request): boolean {
+ public async consumeBillingEvent(req: Request): Promise<boolean> {
let event: { data, type: string };
if (process.env.STRIPE_WEBHOOK_SECRET) { // ##CR - to be clarified - where this secret key should come from
// Retrieve the event by verifying the signature using the raw body and secret.

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
import { Action, Entity } from '../../../../types/Authorization';
-import { BillingInvoiceStatus, BillingOperationResult, BillingUserSynchronizeAction } from '../../../../types/Billing';
+import { BillingInvoice, BillingInvoiceStatus, BillingOperationResult, BillingUserSynchronizeAction } from '../../../../types/Billing';
import { HTTPAuthError, HTTPError } from '../../../../types/HTTPError';
import { NextFunction, Request, Response } from 'express';
@@ -480,7 +480,7 @@ export default class BillingService {
};
await TransactionStorage.saveTransaction(req.user.tenantID, transaction);
// Ok
- Logging.logInfo({
+ await Logging.logInfo({
tenantID: req.user.tenantID,
user: req.user, actionOnUser: transaction.userID,
module: MODULE_NAME, method: 'handleCreateTransactionInvoice',
@@ -583,44 +583,48 @@ export default class BillingService {
}
}
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
public static async handleBillingChargeInvoice(action: ServerAction, req: Request, res: Response): Promise<void> {
- // Check if component is active
- UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
- Action.BILLING_CHARGE_INVOICE, Entity.BILLING, MODULE_NAME, 'handleBillingChargeInvoice');
- // Filter
- const filteredRequest = BillingSecurity.filterChargeInvoiceRequest(req.body);
- // Get the Invoice
- const invoice = await BillingStorage.getInvoice(req.user.tenantID, filteredRequest.ID); // ID of the Billing Invoice (not the stripe invoice)
- UtilsService.assertObjectExists(action, invoice, `Invoice ID '${filteredRequest.ID}' does not exist`,
- MODULE_NAME, 'handleDownloadInvoice', req.user);
- // Check Auth
- // if (!Authorizations.canChargeInvoice(req.user, invoice.userID)) {
- // throw new AppAuthError({
- // errorCode: HTTPAuthError.ERROR,
- // user: req.user,
- // entity: Entity.INVOICE, action: Action.CHARGE_INVOICE,
+
+ // TODO - no use-case for this endpoint so far! - only used for troubleshooting!
+ throw new Error('Method not implemented.');
+ // // Check if component is active
+ // UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
+ // Action.BILLING_CHARGE_INVOICE, Entity.BILLING, MODULE_NAME, 'handleBillingChargeInvoice');
+ // // Filter
+ // const filteredRequest = BillingSecurity.filterChargeInvoiceRequest(req.body);
+ // // Get the Invoice
+ // const invoice = await BillingStorage.getInvoice(req.user.tenantID, filteredRequest.ID); // ID of the Billing Invoice (not the stripe invoice)
+ // UtilsService.assertObjectExists(action, invoice, `Invoice ID '${filteredRequest.ID}' does not exist`,
+ // MODULE_NAME, 'handleDownloadInvoice', req.user);
+ // // Check Auth
+ // // if (!Authorizations.canChargeInvoice(req.user, invoice.userID)) {
+ // // throw new AppAuthError({
+ // // errorCode: HTTPAuthError.ERROR,
+ // // user: req.user,
+ // // entity: Entity.INVOICE, action: Action.CHARGE_INVOICE,
+ // // module: MODULE_NAME, method: 'handleBillingChargeInvoice',
+ // // });
+ // // }
+ // // Get the billing impl
+ // const billingImpl = await BillingFactory.getBillingImpl(req.user.tenantID);
+ // if (!billingImpl) {
+ // throw new AppError({
+ // source: Constants.CENTRAL_SERVER,
+ // errorCode: HTTPError.GENERAL_ERROR,
+ // message: 'Billing service is not configured',
// module: MODULE_NAME, method: 'handleBillingChargeInvoice',
+ // action: action,
+ // user: req.user
// });
// }
- // Get the billing impl
- const billingImpl = await BillingFactory.getBillingImpl(req.user.tenantID);
- if (!billingImpl) {
- throw new AppError({
- source: Constants.CENTRAL_SERVER,
- errorCode: HTTPError.GENERAL_ERROR,
- message: 'Billing service is not configured',
- module: MODULE_NAME, method: 'handleBillingChargeInvoice',
- action: action,
- user: req.user
- });
- }
- // Invoke the billing implementation (stripe)
- const billingInvoice = await billingImpl.getInvoice(invoice.invoiceID);
- const operationResult = await billingImpl.chargeInvoice(billingInvoice);
- if (operationResult) {
- console.log(operationResult);
- }
- res.json(operationResult);
+ // // Invoke the billing implementation (stripe)
+ // let billingInvoice:BillingInvoice = await billingImpl.getInvoice(invoice.invoiceID);
+ // billingInvoice = await billingImpl.chargeInvoice(billingInvoice);
+ // res.json({
+ // succeeded: true,
+ // status: billingInvoice.status
+ // });
}
public static async handleBillingWebHook(action: ServerAction, req: Request, res: Response): Promise<void> {
@@ -653,8 +657,8 @@ export default class BillingService {
// User: req.user
});
}
- // ##CR - don't wait for the result!!!
- const done = billingImpl.handleBillingEvent(req);
+ // STRIPE expects a fast response - make sure to postpone time consuming operations when handling these events
+ const done = await billingImpl.consumeBillingEvent(req);
// Return a response to acknowledge receipt of the event
res.json({ received: done });
}

MODIFY
test/api/BillingStripeIntegrationTest.ts
test/api/BillingStripeIntegrationTest.ts
-import chai, { assert, expect } from 'chai';
-
import MongoDBStorage from '../../src/storage/mongodb/MongoDBStorage';
import StripeIntegrationTestData from './BillingStripeTestData';
+import chai from 'chai';
import chaiSubset from 'chai-subset';
import config from '../config';
import global from '../../src/types/GlobalType';
@@ -38,23 +37,7 @@ describe('Billing Stripe Service', function() {
});
it('should create and pay a first invoice for BILLING-TEST user', async () => {
- await testData.checkForDraftInvoices(testData.dynamicUser.id, 0);
- // Let's create an Invoice with a first Item
- const dynamicInvoice = await testData.createDraftInvoice();
- assert(dynamicInvoice, 'Invoice should not be null');
- // Let's add an second item to the same invoice
- const updatedInvoice = await testData.updateDraftInvoice();
- assert(updatedInvoice, 'Invoice should not be null');
- // User should have a DRAFT invoice
- const draftInvoices = await testData.getDraftInvoices(testData.dynamicUser.id);
- assert(draftInvoices, 'User should have at least a draft invoice');
- expect(draftInvoices.length).to.be.eql(1);
- // Let's pay that particular DRAFT invoice
- await testData.payDraftInvoice(draftInvoices[0]);
- // Next step should not be necessary
- // await testData.billingImpl.synchronizeInvoices(testData.dynamicUser);
- // Let's check that the user do not have any DRAFT invoice anymore
- await testData.checkForDraftInvoices(testData.dynamicUser.id, 0);
+ void testData.checkBusinessProcessBillToPay();
});
it('Should add a different payment method to BILLING-TEST user', async () => {
@@ -67,25 +50,9 @@ describe('Billing Stripe Service', function() {
it('should create and pay a second invoice for BILLING-TEST user', async () => {
await testData.checkForDraftInvoices(testData.dynamicUser.id, 0);
- // Let's create an Invoice with a first Item
- const dynamicInvoice = await testData.createDraftInvoice();
- assert(dynamicInvoice, 'Invoice should not be null');
- // Let's add an second item to the same invoice
- const updatedInvoice = await testData.updateDraftInvoice();
- assert(updatedInvoice, 'Invoice should not be null');
- // User should have a DRAFT invoice
- const draftInvoices = await testData.getDraftInvoices(testData.dynamicUser.id);
- assert(draftInvoices, 'User should have at least a draft invoice');
- expect(draftInvoices.length).to.be.eql(1);
- // Let's pay that particular DRAFT invoice
- await testData.payDraftInvoice(draftInvoices[0]);
- // Next step should not be necessary
- // await testData.billingImpl.synchronizeInvoices(testData.dynamicUser);
- // Let's check that the user do not have any DRAFT invoice anymore
- await testData.checkForDraftInvoices(testData.dynamicUser.id, 0);
+ void testData.checkBusinessProcessBillToPay();
});
-
});
});

MODIFY
test/api/BillingStripeTestData.ts
test/api/BillingStripeTestData.ts
@@ -163,62 +163,36 @@ export default class StripeIntegrationTestData {
return taxRate;
}
- // public async createDraftInvoice() : Promise<BillingInvoice> {
- // assert(this.billingUser, 'Billing user cannot be null');
- // const item = {
- // description: 'Stripe Integration - Item 777',
- // pricingData: {
- // amount: 777,
- // quantity: 1,
- // price: 777
- // },
- // };
- // const billingInvoiceItem: BillingInvoiceItem = await this.billingImpl.createPendingInvoiceItem(this.billingUser, item);
- // assert(billingInvoiceItem, 'Billing invoice item should not be null');
- // const billingInvoice: BillingInvoice = await this.billingImpl.createInvoice(this.billingUser);
- // assert(billingInvoice, 'Billing invoice should not be null');
- // return billingInvoice;
- // }
-
- public async createDraftInvoice() : Promise<BillingInvoice> {
+ public async checkBusinessProcessBillToPay() : Promise<number> {
+ await this.checkForDraftInvoices(this.dynamicUser.id, 0);
+ // Let's create an Invoice with a first Item
+ const dynamicInvoice = await this.billInvoiceItem(777);
+ assert(dynamicInvoice, 'Invoice should not be null');
+ // Let's add an second item to the same invoice
+ const updatedInvoice = await this.billInvoiceItem(555);
+ assert(updatedInvoice, 'Invoice should not be null');
+ // User should have a DRAFT invoice
+ const draftInvoices = await this.getDraftInvoices(this.dynamicUser.id);
+ assert(draftInvoices, 'User should have at least a draft invoice');
+ expect(draftInvoices.length).to.be.eql(1);
+ // Let's pay that particular DRAFT invoice
+ await this.payDraftInvoice(draftInvoices[0]);
+ // Next step should not be necessary
+ // await testData.billingImpl.synchronizeInvoices(testData.dynamicUser);
+ // Let's check that the user do not have any DRAFT invoice anymore
+ const nbDraftInvoice:number = await this.checkForDraftInvoices(this.dynamicUser.id, 0);
+ return nbDraftInvoice;
+ }
+
+ public async billInvoiceItem(price: number) : Promise<BillingInvoice> {
assert(this.billingUser, 'Billing user cannot be null');
const invoiceItem = {
- description: 'Stripe Integration - Item 777',
+ description: `Stripe Integration - Item ${price}`,
pricingData: {
- amount: 777,
+ amount: price,
quantity: 1,
- price: 777
- }
- };
-
- const billingInvoice: BillingInvoice = await this.billingImpl.billInvoiceItems(this.dynamicUser, [ invoiceItem ]);
- assert(billingInvoice, 'Billing invoice should not be null');
- return billingInvoice;
- }
-
- // public async updateDraftInvoice(billingInvoice: BillingInvoice) : Promise<void> {
- // assert(this.billingUser, 'Billing user cannot be null');
- // const item = {
- // description: 'Stripe Integration - Item 555',
- // pricingData: {
- // amount: 555,
- // quantity: 1,
- // price: 555
- // },
- // };
- // const billingInvoiceItem: BillingInvoiceItem = await this.billingImpl.createInvoiceItem(this.billingUser, billingInvoice.invoiceID, item);
- // assert(billingInvoiceItem, 'Invoice Item should not be null');
- // }
-
- public async updateDraftInvoice() : Promise<BillingInvoice> {
- assert(this.billingUser, 'Billing user cannot be null');
- const invoiceItem = {
- description: 'Stripe Integration - Item 555',
- pricingData: {
- amount: 555,
- quantity: 1,
- price: 555
+ price: price
}
};

stripe - billing api cleanup