Billing Settingd - new endpoints polish wave 2
MODIFY
src/integration/billing/BillingFactory.ts
src/integration/billing/BillingFactory.ts
@@ -13,25 +13,14 @@ import Utils from '../../utils/Utils';
const MODULE_NAME = 'BillingFactory';
export default class BillingFactory {
- static async getBillingImpl(tenantID: string, settings?: BillingSettings): Promise<BillingIntegration> {
+ static async getBillingImpl(tenantID: string): Promise<BillingIntegration> {
// Get the tenant
const tenant: Tenant = await TenantStorage.getTenant(tenantID);
// Check if billing is active
if (Utils.isTenantComponentActive(tenant, TenantComponents.PRICING) &&
Utils.isTenantComponentActive(tenant, TenantComponents.BILLING)) {
- if (!settings) {
// Get the billing's settings
- const allSettings = await BillingStorage.getBillingSettings(tenantID);
- settings = allSettings[0];
- } else {
- // Specific situation to pre-check billing settings
- await Logging.logDebug({
- tenantID: tenant.id,
- action: ServerAction.BILLING,
- module: MODULE_NAME, method: 'getBillingImpl',
- message: 'Now checking billing connectivity with settings not yet persisted'
- });
- }
+ const settings = await BillingStorage.getBillingSetting(tenantID);
if (settings) {
let billingIntegrationImpl = null;
switch (settings.type) {

MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -330,6 +330,8 @@ export default abstract class BillingIntegration {
abstract checkConnection(checkPrerequisite?: boolean): Promise<void>;
+ abstract preCheckConnection(billingSettings: BillingSettings): Promise<void>;
+
abstract startTransaction(transaction: Transaction): Promise<BillingDataTransactionStart>;
abstract updateTransaction(transaction: Transaction): Promise<BillingDataTransactionUpdate>;

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -45,22 +45,6 @@ export default class StripeBillingIntegration extends BillingIntegration {
return null;
}
- private static async isConnectedToALiveAccount(stripeFacade: Stripe): Promise<boolean> {
- try {
- // TODO - find a way to avoid that call
- const list = await stripeFacade.customers.list({ limit: 1 });
- return !!list.data?.[0]?.livemode;
- } catch (error) {
- throw new BackendError({
- source: Constants.CENTRAL_SERVER,
- module: MODULE_NAME, method: 'isConnectedToALiveAccount',
- action: ServerAction.CHECK_CONNECTION,
- message: 'Failed to connect to Stripe',
- detailedMessages: { error: error.message, stack: error.stack }
- });
- }
- }
-
public async getStripeInstance(): Promise<Stripe> {
// TODO - To be removed - only used by automated tests!
await this.checkConnection();
@@ -83,20 +67,21 @@ export default class StripeBillingIntegration extends BillingIntegration {
});
}
// Try to connect
+ try {
this.stripe = new Stripe(this.settings.stripe.secretKey, {
apiVersion: '2020-08-27',
});
- // Let's check if the connection is working properly
- if (!this.stripe) {
+ // Let's make sure the connection works as expected
+ this.productionMode = await StripeHelpers.isConnectedToALiveAccount(this.stripe);
+ } catch (error) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
module: MODULE_NAME, method: 'checkConnection',
- action: ServerAction.CHECK_BILLING_CONNECTION,
- message: 'Failed to get a Stripe instance'
+ action: ServerAction.CHECK_CONNECTION,
+ message: 'Failed to connect to Stripe',
+ detailedMessages: { error: error.message, stack: error.stack }
});
}
- // Let's make sure the connection works as expected
- this.productionMode = await StripeBillingIntegration.isConnectedToALiveAccount(this.stripe);
// Check Taxes Prerequisites
if (checkPrerequisite) {
// Check whether the taxID is set and still active
@@ -123,6 +108,37 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
}
+ public async preCheckConnection(temporarySettings: BillingSettings): Promise<void> {
+ // Used to validate temporary settings (not yet saved)
+ try {
+ temporarySettings.stripe.secretKey = await Cypher.decrypt(this.tenantID, temporarySettings.stripe.secretKey);
+ } catch (error) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME, method: 'checkConnection',
+ action: ServerAction.CHECK_BILLING_CONNECTION,
+ message: 'Failed to connect to Stripe - Key is inconsistent',
+ detailedMessages: { error: error.message, stack: error.stack }
+ });
+ }
+ // Try to connect
+ try {
+ const temporaryStripeInstance = new Stripe(temporarySettings.stripe.secretKey, {
+ apiVersion: '2020-08-27',
+ });
+ // Let's make sure the connection works as expected
+ await StripeHelpers.isConnectedToALiveAccount(temporaryStripeInstance);
+ } catch (error) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME, method: 'isConnectedToALiveAccount',
+ action: ServerAction.CHECK_BILLING_CONNECTION,
+ message: 'Failed to connect to Stripe',
+ detailedMessages: { error: error.message, stack: error.stack }
+ });
+ }
+ }
+
public async getUsers(): Promise<BillingUser[]> {
const users = [];
let request;

MODIFY
src/integration/billing/stripe/StripeHelpers.ts
src/integration/billing/stripe/StripeHelpers.ts
@@ -114,4 +114,10 @@ export default class StripeHelpers {
});
return shrinkError;
}
+
+ public static async isConnectedToALiveAccount(stripeFacade: Stripe): Promise<boolean> {
+ // TODO - find a way to avoid that call
+ const list = await stripeFacade.customers.list({ limit: 1 });
+ return !!list.data?.[0]?.livemode;
+ }
}

MODIFY
src/server/rest/v1/router/api/BillingRouter.ts
src/server/rest/v1/router/api/BillingRouter.ts
@@ -15,15 +15,11 @@ export default class BillingRouter {
// -----------------------------------
// ROUTES for BILLING SETTINGS
// -----------------------------------
- this.buildRouteBillingSettings();
- this.buildRouteCheckBillingSettingsPrerequisites();
this.buildRouteBillingSetting();
- // this.buildRouteCreateBillingSetting();
this.buildRouteUpdateBillingSetting();
- // this.buildRouteDeleteBillingSetting();
- // this.buildRouteCheckBillingSetting();
- this.buildRouteActivateBillingSetting();
this.buildRouteCheckBillingSettingConnection();
+ this.buildRouteCheckBillingSettingPrerequisites();
+ this.buildRouteActivateBillingSetting();
// -----------------------------------
// ROUTES for PAYMENT METHODS
// -----------------------------------
@@ -38,15 +34,9 @@ export default class BillingRouter {
return this.router;
}
- protected buildRouteBillingSettings(): void {
- this.router.get(`/${ServerRoute.REST_BILLING_SETTINGS}`, (req: Request, res: Response, next: NextFunction) => {
- void RouterUtils.handleServerAction(BillingService.handleGetBillingSettings.bind(this), ServerAction.SETTINGS, req, res, next);
- });
- }
-
- protected buildRouteCheckBillingSettingsPrerequisites(): void {
- this.router.post(`/${ServerRoute.REST_BILLING_SETTINGS_CHECK_PREREQUISITES}`, (req: Request, res: Response, next: NextFunction) => {
- void RouterUtils.handleServerAction(BillingService.handleCheckBillingSettingsPrerequisites.bind(this), ServerAction.SETTINGS, req, res, next);
+ protected buildRouteCheckBillingSettingPrerequisites(): void {
+ this.router.post(`/${ServerRoute.REST_BILLING_SETTING_CHECK_PREREQUISITES}`, (req: Request, res: Response, next: NextFunction) => {
+ void RouterUtils.handleServerAction(BillingService.handleCheckBillingSettingPrerequisites.bind(this), ServerAction.SETTINGS, req, res, next);
});
}

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -747,40 +747,17 @@ export default class BillingService {
// ------------------------------------------------------------------------
// BILLING SETTINGS HANDLERS
// ------------------------------------------------------------------------
- public static async handleGetBillingSettings(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ public static async handleCheckBillingSettingPrerequisites(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
- Action.LIST, Entity.SETTING, MODULE_NAME, 'handleCheckBillingSettingsPrerequisites');
- // Check auth
- if (!await Authorizations.canListSettings(req.user)) {
- throw new AppAuthError({
- errorCode: HTTPAuthError.FORBIDDEN,
- user: req.user,
- action: Action.READ, entity: Entity.SETTING,
- module: MODULE_NAME, method: 'handleGetBillingSetting',
- });
- }
- const allBillingSettings = await BillingStorage.getBillingSettings(req.user.tenantID);
- if (allBillingSettings) {
- allBillingSettings.map((billingSettings) => BillingService.hashSensitiveData(req.user.tenantID, billingSettings));
- res.json(allBillingSettings);
- } else {
- res.sendStatus(HTTPError.OBJECT_DOES_NOT_EXIST_ERROR);
- }
- next();
- }
-
- public static async handleCheckBillingSettingsPrerequisites(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Check if component is active
- UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
- Action.CHECK_CONNECTION, Entity.BILLING, MODULE_NAME, 'handleCheckBillingSettingsPrerequisites');
+ Action.CHECK_CONNECTION, Entity.BILLING, MODULE_NAME, 'handleCheckBillingSettingPrerequisites');
// Check auth
if (!await Authorizations.canCheckConnectionBilling(req.user)) {
throw new AppAuthError({
errorCode: HTTPAuthError.FORBIDDEN,
user: req.user,
action: Action.CHECK_CONNECTION, entity: Entity.BILLING,
- module: MODULE_NAME, method: 'handleCheckBillingSettingsPrerequisites',
+ module: MODULE_NAME, method: 'handleCheckBillingSettingPrerequisites',
});
}
// -----------------------------------------------------
@@ -792,7 +769,7 @@ export default class BillingService {
source: Constants.CENTRAL_SERVER,
errorCode: HTTPError.GENERAL_ERROR,
message: 'Billing service is not configured',
- module: MODULE_NAME, method: 'handleCheckBillingSettingsPrerequisites',
+ module: MODULE_NAME, method: 'handleCheckBillingSettingPrerequisites',
action: action,
user: req.user
});
@@ -809,7 +786,7 @@ export default class BillingService {
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
Action.READ, Entity.SETTING, MODULE_NAME, 'handleGetBillingSetting');
// Check auth
- if (!await Authorizations.canReadSetting(req.user)) {
+ if (!await Authorizations.canReadBillingSetting(req.user)) {
throw new AppAuthError({
errorCode: HTTPAuthError.FORBIDDEN,
user: req.user,
@@ -817,8 +794,7 @@ export default class BillingService {
module: MODULE_NAME, method: 'handleGetBillingSetting',
});
}
- const settingID = req.params.id;
- const billingSettings: BillingSettings = await BillingStorage.getBillingSetting(req.user.tenantID, settingID);
+ const billingSettings: BillingSettings = await BillingStorage.getBillingSetting(req.user.tenantID);
if (!billingSettings) {
res.sendStatus(HTTPError.OBJECT_DOES_NOT_EXIST_ERROR);
next();
@@ -834,21 +810,12 @@ export default class BillingService {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
Action.UPDATE, Entity.SETTING, MODULE_NAME, 'handleUpdateBillingSetting');
- // Check auth
- if (!await Authorizations.canUpdateBillingSetting(req.user)) {
- throw new AppAuthError({
- errorCode: HTTPAuthError.FORBIDDEN,
- user: req.user,
- action: Action.UPDATE, entity: Entity.SETTING,
- module: MODULE_NAME, method: 'handleUpdateBillingSetting',
- });
- }
// Filter
// TODO - sanitize it
const newBillingProperties = req.body as Partial<BillingSettings>;
- newBillingProperties.id = req.params.id;
- UtilsService.assertIdIsProvided(action, newBillingProperties.id, MODULE_NAME, 'handleUpdateSetting', req.user);
- if (!await Authorizations.canUpdateSetting(req.user)) {
+ UtilsService.assertIdIsProvided(action, newBillingProperties.id, MODULE_NAME, 'handleUpdateBillingSetting', req.user);
+ // Check auth
+ if (!await Authorizations.canUpdateBillingSetting(req.user)) {
throw new AppAuthError({
errorCode: HTTPAuthError.FORBIDDEN,
user: req.user,
@@ -857,7 +824,7 @@ export default class BillingService {
});
}
// Load previous settings
- const billingSettings = await BillingStorage.getBillingSetting(req.user.tenantID, newBillingProperties.id);
+ const billingSettings = await BillingStorage.getBillingSetting(req.user.tenantID);
if (billingSettings) {
await BillingService.processSensitiveData(req.user.tenantID, billingSettings, newBillingProperties);
}
@@ -898,12 +865,8 @@ export default class BillingService {
module: MODULE_NAME, method: 'handleUpdateBillingSetting',
});
}
- // Check if component is active
- UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
- Action.CHECK_CONNECTION, Entity.BILLING, MODULE_NAME, 'handleActivateBillingSetting');
- // TODO - sanitize
- const settingID = req.params.id;
- const currentBillingSettings = await BillingStorage.getBillingSetting(req.user.tenantID, settingID);
+ // Get current settings
+ const currentBillingSettings = await BillingStorage.getBillingSetting(req.user.tenantID);
// TODO - check prerequisites - activation should not be possible when settings are inconsistent
currentBillingSettings.billing.isTransactionBillingActivated = true;
await BillingStorage.saveBillingSetting(req.user.tenantID, currentBillingSettings);
@@ -920,8 +883,6 @@ export default class BillingService {
// GOAL: Check the connection settings before (and without) saving them!
// -----------------------------------------------------------------------
const newBillingProperties = req.body as Partial<BillingSettings>;
- newBillingProperties.id = req.params.id;
- UtilsService.assertIdIsProvided(action, newBillingProperties.id, MODULE_NAME, 'handleCheckBillingSettingConnection', req.user);
// Check auth
if (!await Authorizations.canCheckConnectionBilling(req.user)) {
throw new AppAuthError({
@@ -932,7 +893,7 @@ export default class BillingService {
});
}
// Load current settings
- const billingSettings = await BillingStorage.getBillingSetting(req.user.tenantID, newBillingProperties.id);
+ const billingSettings = await BillingStorage.getBillingSetting(req.user.tenantID);
if (billingSettings) {
await BillingService.processSensitiveData(req.user.tenantID, billingSettings, newBillingProperties);
}
@@ -940,49 +901,24 @@ export default class BillingService {
// TODO - clarify how to avoid the hardcoded "stripe" property here
billingSettings.stripe = newBillingProperties.stripe;
// Create the Billing Implementation Instance
- const billingImpl = await BillingFactory.getBillingImpl(req.user.tenantID, billingSettings);
+ 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: 'handleCheckBillingConnection',
+ module: MODULE_NAME, method: 'handleCheckBillingSettingConnection',
action: action,
user: req.user
});
}
- await billingImpl.checkConnection();
+ // Let's validate the new settings
+ await billingImpl.preCheckConnection(billingSettings);
// Ok
res.json(Constants.REST_RESPONSE_SUCCESS);
next();
}
- // eslint-disable-next-line @typescript-eslint/require-await
- // public static async handleCheckBillingSetting(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // // Check if component is active
- // UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
- // Action.CHECK_CONNECTION, Entity.BILLING, MODULE_NAME, 'handleCheckBillingSettingConnection');
- // // -----------------------------------------------------------------------
- // // GOAL: Check the setting consistency before an activation
- // // -----------------------------------------------------------------------
- // 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: 'handleCheckBillingConnection',
- // action: action,
- // user: req.user
- // });
- // }
- // // Check the connection settings + the prerequisites (such as the taxID)
- // await billingImpl.checkConnection( true /* checkPrerequisites */ );
- // // Ok
- // res.json(Constants.REST_RESPONSE_SUCCESS);
- // next();
- // }
-
private static async processSensitiveData(tenantID: string, billingSettings: BillingSettings, newBillingProperties: Partial<BillingSettings>) {
// Process the sensitive data (if any)
if (billingSettings.sensitiveData) {

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -241,15 +241,22 @@ export default class BillingStorage {
// ------------------------------------------------------------------------------------------
// BILLING SETTINGS
// ------------------------------------------------------------------------------------------
- public static async getBillingSettings(tenantID: string): Promise<BillingSettings[]> {
- const settings = await SettingStorage.getSettings(tenantID, { identifier: TenantComponents.BILLING }, Constants.DB_PARAMS_SINGLE_RECORD);
- if (settings && settings.count > 0) {
- return settings.result.map((setting) => BillingStorage.convertToBillingSettings(setting));
+ public static async getBillingSetting(tenantID: string, settingID?: string): Promise<BillingSettings> {
+ let setting: SettingDB;
+ if (settingID) {
+ // Get by ID
+ setting = await SettingStorage.getSetting(tenantID, settingID);
+ } else {
+ // Get by Identifier
+ setting = await SettingStorage.getSettingByIdentifier(tenantID, TenantComponents.BILLING);
+ }
+ if (setting) {
+ return BillingStorage.convertToBillingSettings(setting);
}
return null;
}
- public static async getBillingSetting(tenantID: string, settingID: string): Promise<BillingSettings> {
+ public static async getBillingSettingByID(tenantID: string, settingID: string): Promise<BillingSettings> {
const settings = await SettingStorage.getSettings(tenantID, { settingID }, Constants.DB_PARAMS_SINGLE_RECORD);
if (settings?.result?.[0]?.content) {
const setting: SettingDB = settings.result[0];

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -507,10 +507,10 @@ export enum ServerRoute {
REST_BILLING_PAYMENT_METHOD_DETACH = 'users/:userID/payment-methods/:paymentMethodID/detach',
// BILLING SETTING URLs
- REST_BILLING_SETTINGS = 'billing-settings',
- REST_BILLING_SETTINGS_CHECK_PREREQUISITES = 'billing-settings/check-prerequisites',
- REST_BILLING_SETTING = 'billing-settings/:id',
+ // REST_BILLING_SETTINGS = 'billing-settings',
+ REST_BILLING_SETTING = 'billing-setting',
// REST_BILLING_SETTING_CHECK = 'billing-settings/:id/check',
- REST_BILLING_SETTING_ACTIVATE = 'billing-settings/:id/activate',
- REST_BILLING_SETTING_CHECK_CONNECTION = 'billing-settings/:id/check-connection',
+ REST_BILLING_SETTING_CHECK_CONNECTION = 'billing/check-connection',
+ REST_BILLING_SETTING_CHECK_PREREQUISITES = 'billing/check-prerequisites',
+ REST_BILLING_SETTING_ACTIVATE = 'billing/activate',
}

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -226,10 +226,9 @@ describe('Billing Settings', function() {
it('should be able to invoke Billing Settings endpoints', async () => {
// Get all Billing settings
- let response = await testData.userService.billingApi.getBillingSettings();
+ let response = await testData.userService.billingApi.getBillingSetting();
assert(response.status === StatusCodes.OK, 'Response status should be 200');
- assert(response.data && response.data.length > 0, 'Response data should not be null');
- const billingSettings = response.data[0] as BillingSettings ;
+ const billingSettings = response.data as BillingSettings ;
assert(billingSettings.billing, 'Billing Properties should not be null');
assert(billingSettings.type === BillingSettingsType.STRIPE, 'Billing Setting Type should not be set to STRIPE');
assert(billingSettings.stripe, 'Stripe Properties should not be null');

MODIFY
test/api/client/BillingApi.ts
test/api/client/BillingApi.ts
@@ -32,23 +32,14 @@ export default class BillingApi extends CrudApi {
return await super.read(params, '/client/api/BillingDownloadInvoice');
}
- public async getBillingSettings(params?) {
- // return await super.read(params, '/v1/api/billing-settings');
- return await super.read(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_SETTINGS, {}));
- }
-
- public async getBillingSetting(id, params?) {
- return await super.read(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_SETTING, { id }));
+ public async getBillingSetting(params?) {
+ return await super.read(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_SETTING, {}));
}
public async checkBillingSettingConnection(id, params?) {
return await super.create(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_SETTING_CHECK_CONNECTION, { id }));
}
- // public async checkBillingSetting(id, params?) {
- // return await super.create(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_SETTING_CHECK, { id }));
- // }
-
public async activateBillingSetting(id, params?) {
return await super.create(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_SETTING_ACTIVATE, { id }));
}

MODIFY
test/api/context/BillingContext.ts
test/api/context/BillingContext.ts
@@ -19,7 +19,7 @@ export default class BillingContext {
this.tenantContext = tenantContext;
}
- private static getBillingSettings(): BillingSettings {
+ private static getUnitTestBillingSettings(): BillingSettings {
const billingProperties = {
isTransactionBillingActivated: config.get('billing.isTransactionBillingActivated'),
immediateBillingAllowed: config.get('billing.immediateBillingAllowed'),
@@ -40,13 +40,13 @@ export default class BillingContext {
}
public async createTestData(): Promise<void> {
- const settings = BillingContext.getBillingSettings();
+ const settings = BillingContext.getUnitTestBillingSettings();
const skip = (!settings.stripe.secretKey);
if (skip) {
// Skip billing context generation if no settings are provided
return;
}
- await this.saveBillingSettings(BillingContext.getBillingSettings());
+ await this.saveBillingSettings(BillingContext.getUnitTestBillingSettings());
const tenantID = this.tenantContext.getTenant().id;
const billingImpl = await BillingFactory.getBillingImpl(tenantID);
if (!billingImpl) {
@@ -66,9 +66,7 @@ export default class BillingContext {
}
private async saveBillingSettings(billingSettings: BillingSettings) {
- // TODO - rethink that part
- const allBillingSettings = await BillingStorage.getBillingSettings(this.tenantContext.getTenant().id);
- const tenantBillingSettings = allBillingSettings[0];
+ const tenantBillingSettings = await BillingStorage.getBillingSetting(this.tenantContext.getTenant().id);
tenantBillingSettings.billing = billingSettings.billing;
tenantBillingSettings.stripe = billingSettings.stripe;
tenantBillingSettings.sensitiveData = ['content.stripe.secretKey'];

Billing Settingd - new endpoints polish wave 2