billing - routes removed - reverting to the current UI logic
MODIFY
src/authorization/Authorizations.ts
src/authorization/Authorizations.ts
@@ -737,7 +737,7 @@ export default class Authorizations {
}
public static async canUpdateBillingSetting(loggedUser: UserToken): Promise<boolean> {
- return Authorizations.canPerformAction(loggedUser, Entity.SETTING, Action.READ);
+ return Authorizations.canPerformAction(loggedUser, Entity.SETTING, Action.UPDATE);
}
public static isSuperAdmin(user: UserToken | User): boolean {

MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -328,9 +328,9 @@ export default abstract class BillingIntegration {
return billingUser;
}
- abstract checkConnection(checkPrerequisite?: boolean): Promise<void>;
+ abstract checkConnection(): Promise<void>;
- abstract preCheckConnection(billingSettings: BillingSettings): Promise<void>;
+ abstract checkActivationPrerequisites(): Promise<void>;
abstract startTransaction(transaction: Transaction): Promise<BillingDataTransactionStart>;

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -52,7 +52,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
// eslint-disable-next-line @typescript-eslint/require-await
- public async checkConnection(checkPrerequisite = false): Promise<void> {
+ public async checkConnection(): Promise<void> {
// Initialize Stripe
if (!this.stripe) {
try {
@@ -82,8 +82,10 @@ export default class StripeBillingIntegration extends BillingIntegration {
detailedMessages: { error: error.message, stack: error.stack }
});
}
- // Check Taxes Prerequisites
- if (checkPrerequisite) {
+ }
+ }
+
+ public async checkActivationPrerequisites(): Promise<void> {
// Check whether the taxID is set and still active
const taxID = this.settings.billing?.taxID;
if (taxID) {
@@ -105,39 +107,6 @@ 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 = [];

MODIFY
src/server/rest/v1/router/api/BillingRouter.ts
src/server/rest/v1/router/api/BillingRouter.ts
@@ -18,9 +18,6 @@ export default class BillingRouter {
this.buildRouteBillingSetting();
this.buildRouteUpdateBillingSetting();
this.buildRouteCheckBillingConnection();
- this.buildRoutePreCheckBillingConnection();
- this.buildRouteCheckBillingPrerequisites();
- this.buildRouteActivateBilling();
// -----------------------------------
// ROUTES for PAYMENT METHODS
// -----------------------------------
@@ -35,12 +32,6 @@ export default class BillingRouter {
return this.router;
}
- protected buildRouteCheckBillingPrerequisites(): void {
- this.router.post(`/${ServerRoute.REST_BILLING_CHECK_PREREQUISITES}`, (req: Request, res: Response, next: NextFunction) => {
- void RouterUtils.handleServerAction(BillingService.handleCheckBillingPrerequisites.bind(this), ServerAction.SETTINGS, req, res, next);
- });
- }
-
protected buildRouteBillingSetting(): void {
this.router.get(`/${ServerRoute.REST_BILLING_SETTING}`, (req: Request, res: Response, next: NextFunction) => {
void RouterUtils.handleServerAction(BillingService.handleGetBillingSetting.bind(this), ServerAction.SETTINGS, req, res, next);
@@ -53,24 +44,12 @@ export default class BillingRouter {
});
}
- protected buildRouteActivateBilling(): void {
- this.router.post(`/${ServerRoute.REST_BILLING_ACTIVATE}`, (req: Request, res: Response, next: NextFunction) => {
- void RouterUtils.handleServerAction(BillingService.handleActivateBilling.bind(this), ServerAction.SETTINGS, req, res, next);
- });
- }
-
protected buildRouteCheckBillingConnection(): void {
this.router.post(`/${ServerRoute.REST_BILLING_CHECK}`, (req: Request, res: Response, next: NextFunction) => {
void RouterUtils.handleServerAction(BillingService.handleCheckBillingConnection.bind(this), ServerAction.SETTINGS, req, res, next);
});
}
- protected buildRoutePreCheckBillingConnection(): void {
- this.router.post(`/${ServerRoute.REST_BILLING_CHECK_CONNECTION}`, (req: Request, res: Response, next: NextFunction) => {
- void RouterUtils.handleServerAction(BillingService.handlePreCheckBillingConnection.bind(this), ServerAction.SETTINGS, req, res, next);
- });
- }
-
protected buildRouteBillingPaymentMethods(): void {
this.router.get(`/${ServerRoute.REST_BILLING_PAYMENT_METHODS}`, (req: Request, res: Response, next: NextFunction) => {
// GET {{base_url}}/v1/api/billing/users/5be451dad0685c19bff48856/payment-methods?Limit=100&SortFields=id

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -747,40 +747,6 @@ export default class BillingService {
// ------------------------------------------------------------------------
// BILLING SETTINGS HANDLERS
// ------------------------------------------------------------------------
- public static async handleCheckBillingPrerequisites(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, 'handleCheckBillingPrerequisites');
- // Check auth
- if (!await Authorizations.canCheckBillingConnection(req.user)) {
- throw new AppAuthError({
- errorCode: HTTPAuthError.FORBIDDEN,
- user: req.user,
- action: Action.CHECK_CONNECTION, entity: Entity.BILLING,
- module: MODULE_NAME, method: 'handleCheckBillingPrerequisites',
- });
- }
- // -----------------------------------------------------
- // GOAL: Check the setting consistency before a Go Live
- // -----------------------------------------------------
- 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: 'handleCheckBillingPrerequisites',
- 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();
- }
-
public static async handleGetBillingSetting(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
@@ -829,9 +795,34 @@ export default class BillingService {
await BillingService.processSensitiveData(req.user.tenantID, billingSettings, newBillingProperties);
}
// Billing properties to preserve
- const { isTransactionBillingActivated, usersLastSynchronizedOn } = billingSettings.billing;
+ const { usersLastSynchronizedOn } = billingSettings.billing;
// Billing properties to override
const { immediateBillingAllowed, periodicBillingAllowed, taxID } = newBillingProperties.billing;
+ let { isTransactionBillingActivated } = newBillingProperties.billing;
+ let postponeTransactionBillingActivation = false;
+ // -----------------------------------------------------------
+ // ACHTUNG - Handle with care the activation of the billing
+ // -----------------------------------------------------------
+ if (isTransactionBillingActivated !== !!billingSettings.billing.isTransactionBillingActivated) {
+ // Attempt to switch it OFF
+ if (!isTransactionBillingActivated) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.MISSING_SETTINGS,
+ message: 'Switching OFF the billing of transactions is forbidden',
+ module: MODULE_NAME,
+ method: 'handleUpdateBillingSetting'
+ });
+ } else {
+ // --------------------------------------------------------------------------
+ // Attempt to switch it ON
+ // - We need to postpone the activation in order to check the prerequisites
+ // - Prerequisites cannot be checked without first saving all other settings
+ // ---------------------------------------------------------------------------
+ isTransactionBillingActivated = false ;
+ postponeTransactionBillingActivation = true;
+ }
+ }
// Now populates the settings with the new values
billingSettings.billing = {
isTransactionBillingActivated,
@@ -845,62 +836,23 @@ export default class BillingService {
// Update timestamp
billingSettings.lastChangedBy = { 'id': req.user.id };
billingSettings.lastChangedOn = new Date();
- // Let's save it
+ // Let's save the new settings
+ await BillingStorage.saveBillingSetting(req.user.tenantID, billingSettings);
+ // Post-process the activation of the billing feature
+ if (postponeTransactionBillingActivation) {
+ // Checks all pre-requisites
+ await this.checkActivationPrerequisites(action, req);
+ // Well - everything was Ok
+ billingSettings.billing.isTransactionBillingActivated = true;
+ // Save it again now that we are sure
await BillingStorage.saveBillingSetting(req.user.tenantID, billingSettings);
- // Ok
- res.json(Constants.REST_RESPONSE_SUCCESS);
- next();
- }
-
- public static async handleActivateBilling(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Check if component is active
- UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
- Action.UPDATE, Entity.SETTING, MODULE_NAME, 'handleActivateBilling');
- // 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: 'handleActivateBilling',
- });
}
- // 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);
// Ok
res.json(Constants.REST_RESPONSE_SUCCESS);
next();
}
- public static async handlePreCheckBillingConnection(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, 'handlePreCheckBillingConnection');
- // -----------------------------------------------------------------------
- // GOAL: Check the connection settings before (and without) saving them!
- // -----------------------------------------------------------------------
- const newBillingProperties = req.body as Partial<BillingSettings>;
- // Check auth
- if (!await Authorizations.canCheckBillingConnection(req.user)) {
- throw new AppAuthError({
- errorCode: HTTPAuthError.FORBIDDEN,
- user: req.user,
- action: Action.CHECK_CONNECTION, entity: Entity.BILLING,
- module: MODULE_NAME, method: 'handlePreCheckBillingConnection',
- });
- }
- // Load current settings
- const billingSettings = await BillingStorage.getBillingSetting(req.user.tenantID);
- if (billingSettings) {
- await BillingService.processSensitiveData(req.user.tenantID, billingSettings, newBillingProperties);
- }
- // Apply new settings (not yet saved)
- // TODO - clarify how to avoid the hardcoded "stripe" property here
- billingSettings.stripe = newBillingProperties.stripe;
- // Create the Billing Implementation Instance
+ private static async checkActivationPrerequisites(action: ServerAction, req: Request) : Promise<void> {
const billingImpl = await BillingFactory.getBillingImpl(req.user.tenantID);
if (!billingImpl) {
throw new AppError({
@@ -912,11 +864,10 @@ export default class BillingService {
user: req.user
});
}
- // Let's validate the new settings
- await billingImpl.preCheckConnection(billingSettings);
- // Ok
- res.json(Constants.REST_RESPONSE_SUCCESS);
- next();
+ // Check the connection
+ await billingImpl.checkConnection();
+ // Let's validate the new settings before activating
+ await billingImpl.checkActivationPrerequisites();
}
private static async processSensitiveData(tenantID: string, billingSettings: BillingSettings, newBillingProperties: Partial<BillingSettings>) {

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -241,30 +241,15 @@ export default class BillingStorage {
// ------------------------------------------------------------------------------------------
// BILLING SETTINGS
// ------------------------------------------------------------------------------------------
- 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);
- }
+ public static async getBillingSetting(tenantID: string): Promise<BillingSettings> {
+ // Get BILLING Settings by Identifier
+ const setting = await SettingStorage.getSettingByIdentifier(tenantID, TenantComponents.BILLING);
if (setting) {
return BillingStorage.convertToBillingSettings(setting);
}
return null;
}
- 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];
- return BillingStorage.convertToBillingSettings(setting);
- }
- return null;
- }
-
public static async saveBillingSetting(tenantID: string, billingSettings: BillingSettings): Promise<string> {
const { id, identifier, sensitiveData, backupSensitiveData, category } = billingSettings;
const { createdBy, createdOn, lastChangedBy, lastChangedOn } = billingSettings;
@@ -290,8 +275,14 @@ export default class BillingStorage {
billingSettings = {
identifier: TenantComponents.BILLING,
type: BillingSettingsType.STRIPE,
- id, sensitiveData, backupSensitiveData, category,
- createdBy, createdOn, lastChangedBy, lastChangedOn,
+ id,
+ sensitiveData,
+ backupSensitiveData,
+ category,
+ createdBy,
+ createdOn,
+ lastChangedBy,
+ lastChangedOn,
billing: content.billing,
stripe: content.stripe
};

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -235,23 +235,10 @@ describe('Billing Settings', function() {
assert(billingSettings.stripe.secretKey, 'Secret Key should not be null');
assert(billingSettings.id, 'ID should not be null');
// Pre-check a billing settings
- const { id, identifier, type, stripe } = billingSettings;
- response = await testData.userService.billingApi.checkBillingSettingConnection({
- id, identifier, type, stripe
- });
+ response = await testData.userService.billingApi.checkBillingConnection();
assert(response.status === StatusCodes.OK, 'Response status should be 200');
assert(response.data, 'Response data should not be null');
- // Pre-check a WRONG billing settings
- response = await testData.userService.billingApi.checkBillingSettingConnection({
- id, identifier, type,
- stripe: {
- url: stripe.url,
- secretKey: 'sk_' + 'test_wrong_value',
- publicKey: stripe.publicKey
- }
- });
- // TODO - find a better status code
- assert(response.status === StatusCodes.INTERNAL_SERVER_ERROR, 'Response status should be 500');
+ assert(response.data.connectionIsValid === true, 'Connection should be valid');
});
});

MODIFY
test/api/client/BillingApi.ts
test/api/client/BillingApi.ts
@@ -36,12 +36,7 @@ export default class BillingApi extends CrudApi {
return await super.read(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_SETTING));
}
- public async checkBillingSettingConnection(params?) {
- return await super.create(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_CHECK_CONNECTION));
+ public async checkBillingConnection(params?) {
+ return await super.create(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_CHECK));
}
-
- public async activateBillingSetting(params?) {
- return await super.create(params, super.buildRestEndpointUrl(ServerRoute.REST_BILLING_ACTIVATE));
- }
-
}

billing - routes removed - reverting to the current UI logic