billing settings - after review
MODIFY
src/assets/server/rest/v1/schemas/billing/billing-setting-update.json
src/assets/server/rest/v1/schemas/billing/billing-setting-update.json
]
}
},
+ "oneOf": [
+ {
"required": [
"id",
"identifier",
"type",
- "billing"
+ "billing",
+ "stripe"
+ ]
+ }
]
}

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -89,11 +89,11 @@ export default class StripeBillingIntegration extends BillingIntegration {
// Check whether the taxID is set and still active
const taxID = this.settings.billing?.taxID;
if (taxID) {
- const billingTax: BillingTax = await this._getTaxRate(taxID);
+ const billingTax: BillingTax = await this.getTaxRate(taxID);
if (!billingTax) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
- module: MODULE_NAME, method: 'checkConnection',
+ module: MODULE_NAME, method: 'checkActivationPrerequisites',
action: ServerAction.CHECK_BILLING_CONNECTION,
message: `Billing prerequisites are not consistent - taxID is not found or inactive - taxID: '${taxID}'`
});
@@ -101,7 +101,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
} else {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
- module: MODULE_NAME, method: 'checkConnection',
+ module: MODULE_NAME, method: 'checkActivationPrerequisites',
action: ServerAction.CHECK_BILLING_CONNECTION,
message: 'Billing prerequisites are not consistent - taxID is mandatory'
});
@@ -208,13 +208,14 @@ export default class StripeBillingIntegration extends BillingIntegration {
module: MODULE_NAME, method: 'getTaxes',
message: `Retrieved tax list (${taxes.length} taxes)`
});
- } catch (e) {
+ } catch (error) {
// catch stripe errors and send the information back to the client
await Logging.logError({
tenantID: this.tenantID,
action: ServerAction.BILLING_TAXES,
module: MODULE_NAME, method: 'getTaxes',
- message: `Stripe operation failed - ${e?.message as string}`
+ message: 'Failed to retrieve tax rates',
+ detailedMessages: { error: error.message, stack: error.stack }
});
}
return taxes;
@@ -222,10 +223,6 @@ export default class StripeBillingIntegration extends BillingIntegration {
public async getTaxRate(taxID: string): Promise<BillingTax> {
await this.checkConnection();
- return this._getTaxRate(taxID);
- }
-
- private async _getTaxRate(taxID: string): Promise<BillingTax> {
let taxRate : BillingTax = null;
try {
const stripeTaxRate: Stripe.TaxRate = await this.stripe.taxRates.retrieve(taxID);
@@ -233,13 +230,14 @@ export default class StripeBillingIntegration extends BillingIntegration {
const { id, description, display_name: displayName, percentage } = stripeTaxRate;
taxRate = { id, description, displayName, percentage };
}
- } catch (e) {
+ } catch (error) {
// catch stripe errors and send the information back to the client
await Logging.logError({
tenantID: this.tenantID,
action: ServerAction.BILLING_TAXES,
module: MODULE_NAME, method: 'getTaxRate',
- message: `Stripe operation failed - ${e?.message as string}`
+ message: 'Failed to retrieve tax rate',
+ detailedMessages: { error: error.message, stack: error.stack }
});
}
return taxRate;
@@ -614,14 +612,15 @@ export default class StripeBillingIntegration extends BillingIntegration {
requestParams.starting_after = paymentMethods[paymentMethods.length - 1].id;
}
} while (request.has_more);
- } 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: '_getPaymentMethods',
- message: `Stripe operation failed - ${e?.message as string}`
+ message: 'Failed to retrieve payment methods',
+ detailedMessages: { error: error.message, stack: error.stack }
});
}
return paymentMethods;

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -745,9 +745,6 @@ export default class BillingService {
next();
}
- // ------------------------------------------------------------------------
- // BILLING SETTINGS HANDLERS
- // ------------------------------------------------------------------------
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,
@@ -777,8 +774,6 @@ export default class BillingService {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
Action.UPDATE, Entity.SETTING, MODULE_NAME, 'handleUpdateBillingSetting');
- // Check
- const newBillingProperties = BillingValidator.getInstance().validateUpdateBillingSetting({ ...req.params, ...req.body });
// Check auth
if (!await Authorizations.canUpdateBillingSetting(req.user)) {
throw new AppAuthError({
@@ -788,11 +783,16 @@ export default class BillingService {
module: MODULE_NAME, method: 'handleUpdateBillingSetting',
});
}
+ // Check
+ const newBillingProperties = BillingValidator.getInstance().validateUpdateBillingSetting({ ...req.params, ...req.body });
// Load previous settings
const billingSettings = await BillingStorage.getBillingSetting(req.user.tenantID);
- if (billingSettings) {
- await BillingService.processSensitiveData(req.user.tenantID, billingSettings, newBillingProperties);
+ if (!billingSettings) {
+ res.sendStatus(HTTPError.OBJECT_DOES_NOT_EXIST_ERROR);
+ next();
+ return;
}
+ await BillingService.processSensitiveData(req.user.tenantID, billingSettings, newBillingProperties);
// Billing properties to preserve
const { usersLastSynchronizedOn } = billingSettings.billing;
const previousTransactionBillingState = !!billingSettings.billing.isTransactionBillingActivated;

MODIFY
src/server/rest/v1/service/UtilsService.ts
src/server/rest/v1/service/UtilsService.ts
@@ -11,11 +11,13 @@ import Asset from '../../../../types/Asset';
import AssetStorage from '../../../../storage/mongodb/AssetStorage';
import AuthorizationService from './AuthorizationService';
import Authorizations from '../../../../authorization/Authorizations';
+import BackendError from '../../../../exception/BackendError';
import { ChargingProfile } from '../../../../types/ChargingProfile';
import ChargingStationStorage from '../../../../storage/mongodb/ChargingStationStorage';
import Company from '../../../../types/Company';
import CompanyStorage from '../../../../storage/mongodb/CompanyStorage';
import Constants from '../../../../utils/Constants';
+import Cypher from '../../../../utils/Cypher';
import { DataResult } from '../../../../types/DataResult';
import { HttpEndUserReportErrorRequest } from '../../../../types/requests/HttpNotificationRequest';
import Logging from '../../../../utils/Logging';
@@ -23,6 +25,7 @@ import OCPIEndpoint from '../../../../types/ocpi/OCPIEndpoint';
import OICPEndpoint from '../../../../types/oicp/OICPEndpoint';
import PDFDocument from 'pdfkit';
import { ServerAction } from '../../../../types/Server';
+import { Setting } from '../../../../types/Setting';
import Site from '../../../../types/Site';
import SiteArea from '../../../../types/SiteArea';
import SiteAreaStorage from '../../../../storage/mongodb/SiteAreaStorage';
@@ -34,6 +37,7 @@ import { TransactionInErrorType } from '../../../../types/InError';
import UserStorage from '../../../../storage/mongodb/UserStorage';
import UserToken from '../../../../types/UserToken';
import Utils from '../../../../utils/Utils';
+import _ from 'lodash';
import countries from 'i18n-iso-countries';
import moment from 'moment';
@@ -1513,4 +1517,78 @@ export default class UtilsService {
});
}
}
+
+ public static async processSensitiveData(tenantID: string, settings: Setting, newProperties: Partial<Setting>) {
+ // Process the sensitive data (if any)
+ if (settings.sensitiveData) {
+ if (!Array.isArray(settings.sensitiveData)) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.CYPHER_INVALID_SENSITIVE_DATA_ERROR,
+ message: `The property 'sensitiveData' for Setting with ID '${newProperties.id}' is not an array`,
+ module: MODULE_NAME,
+ method: 'processSensitiveData',
+ });
+ }
+ // Process sensitive properties
+ for (const propertyName of settings.sensitiveData) {
+ // TODO - find a better way - HACK to be backward compatible with SettingDB
+ const normalizedPropertyName = propertyName.replace('content.', '');
+ // Get the sensitive property from the request
+ const valueInRequest = _.get(newProperties, normalizedPropertyName);
+ if (valueInRequest && valueInRequest.length > 0) {
+ // Get the sensitive property from the DB
+ const valueInDb = _.get(settings, normalizedPropertyName);
+ if (valueInDb && valueInDb.length > 0) {
+ const hashedValueInDB = Cypher.hash(valueInDb);
+ if (valueInRequest !== hashedValueInDB) {
+ // Yes: Encrypt
+ _.set(newProperties, normalizedPropertyName, await Cypher.encrypt(tenantID, valueInRequest));
+ } else {
+ // No: Put back the encrypted value
+ _.set(newProperties, normalizedPropertyName, valueInDb);
+ }
+ } else {
+ // Value in db is empty then encrypt
+ _.set(newProperties, normalizedPropertyName, await Cypher.encrypt(tenantID, valueInRequest));
+ }
+ } else {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.CYPHER_INVALID_SENSITIVE_DATA_ERROR,
+ message: `The property '${propertyName}' for Setting with ID '${newProperties.id}' is not set`,
+ module: MODULE_NAME,
+ method: 'processSensitiveData',
+ });
+ }
+ }
+ }
+ }
+
+ public static hashSensitiveData(tenantID: string, settings: Setting): Setting {
+ if (settings.sensitiveData) {
+ // Check that sensitive data is an array
+ if (!Array.isArray(settings.sensitiveData)) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'hashSensitiveDataInJSON',
+ message: 'The property \'sensitiveData\' is not an array'
+ });
+ }
+ for (const propertyName of settings.sensitiveData) {
+ // TODO - find a better way - HACK to be backward compatible with SettingDB
+ const normalizedPropertyName = propertyName.replace('content.', '');
+ // Check that the property does exist otherwise skip to the next property
+ if (_.has(settings, normalizedPropertyName)) {
+ const value = _.get(settings, normalizedPropertyName);
+ // If the value is undefined, null or empty then do nothing and skip to the next property
+ if (value && value.length > 0) {
+ _.set(settings, normalizedPropertyName, Cypher.hash(value));
+ }
+ }
+ }
+ }
+ return settings;
+ }
}

MODIFY
src/server/rest/v1/validator/BillingValidator.ts
src/server/rest/v1/validator/BillingValidator.ts
-import AppError from '../../../../exception/AppError';
import { BillingSettings } from '../../../../types/Setting';
-import Constants from '../../../../utils/Constants';
import Schema from '../../../../types/validator/Schema';
import SchemaValidator from './SchemaValidator';
import fs from 'fs';
@@ -26,21 +24,6 @@ export default class BillingValidator extends SchemaValidator {
public validateUpdateBillingSetting(data: any): BillingSettings {
// Validate schema
this.validate(this.updateBillingSetting, data);
- // Validate deps between components
- // this.validateComponentDependencies(tenant);
return data;
}
-
- // private validateComponentDependencies(tenant: Tenant): void {
- // if (tenant.components) {
- // if (!tenant.components.pricing?.active) {
- // throw new AppError({
- // source: Constants.CENTRAL_SERVER,
- // errorCode: HTTPError.GENERAL_ERROR,
- // message: 'Pricing component must be active to use the Billing module',
- // module: this.moduleName, method: 'validateComponentDependencies'
- // });
- // }
- // }
- // }
}

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -238,9 +238,6 @@ export default class BillingStorage {
await Logging.traceEnd(tenantID, MODULE_NAME, 'deleteInvoice', uniqueTimerID, { id });
}
- // ------------------------------------------------------------------------------------------
- // BILLING SETTINGS
- // ------------------------------------------------------------------------------------------
public static async getBillingSetting(tenantID: string): Promise<BillingSettings> {
// Get BILLING Settings by Identifier
const setting = await SettingStorage.getSettingByIdentifier(tenantID, TenantComponents.BILLING);
@@ -289,5 +286,4 @@ export default class BillingStorage {
}
return billingSettings;
}
-
}

billing settings - after review