pricing service - adaptation to the state-of-the-art
ADD
src/assets/configs-scp
+Subproject commit cf40aecc4b7c099f6b5b8bc6690f975a6db006b6

MODIFY
src/assets/server/rest/v1/schemas/pricing/pricing-definition-get.json
src/assets/server/rest/v1/schemas/pricing/pricing-definition-get.json
"type": "object",
"properties": {
"ID": {
- "$ref": "common.json#/definitions/id"
+ "$ref": "user#/definitions/id"
}
},
"required": [

MODIFY
src/integration/pricing/PricingEngine.ts
src/integration/pricing/PricingEngine.ts
+import FeatureToggles, { Feature } from '../../utils/FeatureToggles';
import PricingDefinition, { PricedConsumptionData, PricingStaticRestriction, ResolvedPricingModel } from '../../types/Pricing';
import ChargingStation from '../../types/ChargingStation';
@@ -15,11 +16,15 @@ export default class PricingEngine {
public static async resolvePricingContext(tenant: Tenant, transaction: Transaction, chargingStation: ChargingStation): Promise<ResolvedPricingModel> {
// Merge the pricing definitions from the different contexts
const pricingDefinitions: PricingDefinition[] = [];
+ if (FeatureToggles.isFeatureActive(Feature.PRICING_CHECK_BACKWARD_COMPATIBILITY)) {
+ // Do nothing - this should trigger a fallback to the simple pricing logic
+ } else {
// pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction.userID));
pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction, chargingStation, transaction.chargeBoxID.toString()));
pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction, chargingStation, transaction.siteAreaID.toString()));
pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction, chargingStation, transaction.siteID.toString()));
pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction, chargingStation, transaction.companyID.toString()));
+ }
// Return the resolution result as a resolved pricing model
const resolvedPricingModel: ResolvedPricingModel = {
pricerContext: {

MODIFY
src/server/rest/v1/service/AuthorizationService.ts
src/server/rest/v1/service/AuthorizationService.ts
@@ -367,28 +367,28 @@ export default class AuthorizationService {
return authorizationFilters;
}
- public static async addPricingDefinitionsAuthorizations(tenant: Tenant, userToken: UserToken, dataResult: PricingDataResult, authorizationFilter: AuthorizationFilter): Promise<void> {
+ public static async addPricingDefinitionsAuthorizations(tenant: Tenant, userToken: UserToken, dataResult: PricingDataResult, filter: AuthorizationFilter): Promise<void> {
// Add canCreate flag to root
dataResult.canCreate = await AuthorizationService.canPerformAuthorizationAction(
- tenant, userToken, Entity.PRICING_DEFINITION, Action.CREATE, authorizationFilter);
+ tenant, userToken, Entity.PRICING_DEFINITION, Action.CREATE, filter);
// Enrich
for (const pricingDefinition of dataResult.result) {
- await AuthorizationService.addPricingAuthorizations(tenant, userToken, pricingDefinition, authorizationFilter);
+ await AuthorizationService.addPricingDefinitionAuthorizations(tenant, userToken, pricingDefinition, filter);
}
}
- public static async addPricingAuthorizations(tenant: Tenant, userToken: UserToken, pricingDefinition: PricingDefinition, authorizationFilter: AuthorizationFilter): Promise<void> {
+ public static async addPricingDefinitionAuthorizations(tenant: Tenant, userToken: UserToken, pricingDefinition: PricingDefinition, filter: AuthorizationFilter): Promise<void> {
// Enrich
pricingDefinition.canRead = await AuthorizationService.canPerformAuthorizationAction(
- tenant, userToken, Entity.PRICING_DEFINITION, Action.READ, authorizationFilter, { CompanyID: pricingDefinition.id }, pricingDefinition);
+ tenant, userToken, Entity.PRICING_DEFINITION, Action.READ, filter, { CompanyID: pricingDefinition.id }, pricingDefinition);
pricingDefinition.canDelete = await AuthorizationService.canPerformAuthorizationAction(
- tenant, userToken, Entity.PRICING_DEFINITION, Action.DELETE, authorizationFilter, { CompanyID: pricingDefinition.id }, pricingDefinition);
+ tenant, userToken, Entity.PRICING_DEFINITION, Action.DELETE, filter, { CompanyID: pricingDefinition.id }, pricingDefinition);
pricingDefinition.canUpdate = await AuthorizationService.canPerformAuthorizationAction(
- tenant, userToken, Entity.PRICING_DEFINITION, Action.UPDATE, authorizationFilter, { CompanyID: pricingDefinition.id }, pricingDefinition);
+ tenant, userToken, Entity.PRICING_DEFINITION, Action.UPDATE, filter, { CompanyID: pricingDefinition.id }, pricingDefinition);
}
public static async checkAndGetPricingDefinitionAuthorizations(tenant: Tenant, userToken: UserToken,
- filteredRequest: Partial<HttpCompanyRequest>, authAction: Action, entityData?: EntityDataType): Promise<AuthorizationFilter> {
+ filteredRequest: Partial<HttpPricingDefinitionRequest>, authAction: Action, entityData?: EntityDataType): Promise<AuthorizationFilter> {
return AuthorizationService.checkAndGetEntityAuthorizations(
tenant, Entity.PRICING_DEFINITION, userToken, filteredRequest, filteredRequest.ID ? { PricingID: filteredRequest.ID } : {}, authAction, entityData);
}

MODIFY
src/server/rest/v1/service/PricingService.ts
src/server/rest/v1/service/PricingService.ts
@@ -8,7 +8,6 @@ import { HTTPAuthError } from '../../../../types/HTTPError';
import Logging from '../../../../utils/Logging';
import { PricingDataResult } from '../../../../types/DataResult';
import PricingDefinition from '../../../../types/Pricing';
-import PricingSecurity from './security/PricingSecurity';
import PricingStorage from '../../../../storage/mongodb/PricingStorage';
import PricingValidator from '../validator/PricingValidator';
import { ServerAction } from '../../../../types/Server';
@@ -19,39 +18,12 @@ const MODULE_NAME = 'PricingService';
export default class PricingService {
- public static async handleDeletePricingDefinition(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Check if component is active
- UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.PRICING,
- Action.DELETE, Entity.PRICING_DEFINITION, MODULE_NAME, 'handleDeletePricingDefinition');
- // Filter
- const pricingModelID = PricingSecurity.filterPricingDefinitionRequestByID(req.query);
- UtilsService.assertIdIsProvided(action, pricingModelID, MODULE_NAME, 'handleDeletePricingDefinition', req.user);
- // Check and Get Pricing
- const pricing = await UtilsService.checkAndGetPricingDefinitionAuthorization(
- req.tenant, req.user, pricingModelID, Action.DELETE, action);
- // Delete
- await PricingStorage.deletePricingDefinition(req.tenant, pricing.id);
- // Log
- await Logging.logSecurityInfo({
- tenantID: req.user.tenantID,
- user: req.user, module: MODULE_NAME, method: 'handleDeletePricingDefinition',
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- message: `Pricing model '${pricingModelID}' has been deleted successfully`,
- action: action,
- detailedMessages: { pricing }
- });
- // Ok
- res.json(Constants.REST_RESPONSE_SUCCESS);
- next();
- }
-
public static async handleGetPricingDefinition(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.PRICING,
- Action.READ, Entity.COMPANY, MODULE_NAME, 'handleGetPricingDefinition');
+ Action.READ, Entity.PRICING_DEFINITION, MODULE_NAME, 'handleGetPricingDefinition');
// Filter
- const filteredRequest = PricingValidator.getInstance().validatePricingGet(req.query);
- // TODO : do we keep this id check as it's already checked in utils.checkandget ????
+ const filteredRequest = PricingValidator.getInstance().validatePricingDefinitionGet(req.query);
UtilsService.assertIdIsProvided(action, filteredRequest.ID, MODULE_NAME, 'handleGetPricingDefinition', req.user);
// Check and get pricing
const pricing = await UtilsService.checkAndGetPricingDefinitionAuthorization(
@@ -65,7 +37,7 @@ export default class PricingService {
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.PRICING,
Action.LIST, Entity.PRICING_DEFINITIONS, MODULE_NAME, 'handleGetPricingDefinitions');
// Filter
- const filteredRequest = PricingValidator.getInstance().validatePricingsGet(req.query);
+ const filteredRequest = PricingValidator.getInstance().validatePricingDefinitionsGet(req.query);
// Check dynamic auth
const authorizationPricingDefinitionsFilter = await AuthorizationService.checkAndGetPricingDefinitionsAuthorizations(
req.tenant, req.user, filteredRequest);
@@ -73,12 +45,10 @@ export default class PricingService {
UtilsService.sendEmptyDataResult(res, next);
return;
}
- // Get the pricings
- const pricingDefinitions = await PricingStorage.getPricingDefinitions(req.tenant,
- {
+ // Get the pricing definitions
+ const pricingDefinitions = await PricingStorage.getPricingDefinitions(req.tenant, {
...authorizationPricingDefinitionsFilter.filters
- },
- {
+ }, {
limit: filteredRequest.Limit,
skip: filteredRequest.Skip,
sort: filteredRequest.SortFields,
@@ -96,15 +66,14 @@ export default class PricingService {
public static async handleCreatePricingDefinition(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.PRICING,
- Action.CREATE, Entity.COMPANY, MODULE_NAME, 'handleCreatePricingDefinition');
+ Action.CREATE, Entity.PRICING_DEFINITION, MODULE_NAME, 'handleCreatePricingDefinition');
// Filter
- const filteredRequest = PricingValidator.getInstance().validatePricingCreate(req.body);
- // const filteredRequest = PricingSecurity.filterPricingDefinitionCreateRequest(req.body);
+ const filteredRequest = PricingValidator.getInstance().validatePricingDefinitionCreate(req.body);
// Check
UtilsService.checkIfPricingDefinitionValid(filteredRequest, req);
// Get dynamic auth
const authorizationFilter = await AuthorizationService.checkAndGetPricingDefinitionAuthorizations(
- req.tenant, req.user, {}, Action.CREATE, filteredRequest as PricingDefinition);
+ req.tenant, req.user, {}, Action.CREATE, filteredRequest);
if (!authorizationFilter.authorized) {
throw new AppAuthError({
errorCode: HTTPAuthError.FORBIDDEN,
@@ -140,13 +109,12 @@ export default class PricingService {
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.PRICING,
Action.UPDATE, Entity.COMPANY, MODULE_NAME, 'handleUpdatePricingDefinition');
// Filter
- // const filteredRequest = PricingSecurity.filterPricingDefinitionUpdateRequest(req.body);
- const filteredRequest = PricingValidator.getInstance().validatePricingUpdate(req.body);
+ const filteredRequest = PricingValidator.getInstance().validatePricingDefinitionUpdate(req.body);
// Check Mandatory fields
UtilsService.checkIfPricingDefinitionValid(filteredRequest, req);
// Check and Get Pricing
const pricingDefinition = await UtilsService.checkAndGetPricingDefinitionAuthorization(
- req.tenant, req.user, filteredRequest.id, Action.UPDATE, action, filteredRequest as PricingDefinition);
+ req.tenant, req.user, filteredRequest.id, Action.UPDATE, action, filteredRequest);
// Update
pricingDefinition.entityID = filteredRequest.entityID;
pricingDefinition.entityType = filteredRequest.entityType;
@@ -171,4 +139,29 @@ export default class PricingService {
res.json(Constants.REST_RESPONSE_SUCCESS);
next();
}
+
+ public static async handleDeletePricingDefinition(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.PRICING,
+ Action.DELETE, Entity.PRICING_DEFINITION, MODULE_NAME, 'handleDeletePricingDefinition');
+ // Filter
+ const pricingDefinitionID = PricingValidator.getInstance().validatePricingDefinitionGet(req.query).ID.toString();
+ // Check and Get Pricing
+ const pricing = await UtilsService.checkAndGetPricingDefinitionAuthorization(
+ req.tenant, req.user, pricingDefinitionID, Action.DELETE, action);
+ // Delete
+ await PricingStorage.deletePricingDefinition(req.tenant, pricing.id);
+ // Log
+ await Logging.logSecurityInfo({
+ tenantID: req.user.tenantID,
+ user: req.user, module: MODULE_NAME, method: 'handleDeletePricingDefinition',
+ message: `Pricing model '${pricingDefinitionID}' has been deleted successfully`,
+ action: action,
+ detailedMessages: { pricing }
+ });
+ // Ok
+ res.json(Constants.REST_RESPONSE_SUCCESS);
+ next();
+ }
+
}

MODIFY
src/server/rest/v1/service/UtilsService.ts
src/server/rest/v1/service/UtilsService.ts
@@ -133,7 +133,7 @@ export default class UtilsService {
UtilsService.assertObjectExists(action, pricingDefinition, `Pricing Model ID '${pricingDefinitionID}' does not exist`,
MODULE_NAME, 'checkAndGetPricingDefinitionAuthorization', userToken);
// Add actions
- await AuthorizationService.addPricingAuthorizations(tenant, userToken, pricingDefinition, authorizationFilter);
+ await AuthorizationService.addPricingDefinitionAuthorizations(tenant, userToken, pricingDefinition, authorizationFilter);
// Check
const authorized = AuthorizationService.canPerformAction(pricingDefinition, authAction);
if (!authorized) {

DELETE
src/server/rest/v1/service/security/PricingSecurity.ts
-import { HttpPricingDefinitionRequest, HttpPricingDefinitionsRequest } from '../../../../../types/requests/HttpPricingRequest';
-
-import PricingDefinition from '../../../../../types/Pricing';
-import UtilsSecurity from './UtilsSecurity';
-import sanitize from 'mongo-sanitize';
-
-export default class PricingSecurity {
-
- public static filterPricingDefinitionRequestByID(request: any): string {
- return sanitize(request.ID);
- }
-
- public static filterPricingDefinitionRequest(request: any): HttpPricingDefinitionRequest {
- return {
- ID: sanitize(request.ID)
- };
- }
-
- public static filterPricingDefinitionsRequest(request: any): HttpPricingDefinitionsRequest {
- const filteredRequest: HttpPricingDefinitionsRequest = {
- Search: sanitize(request.Search),
- EntityID: sanitize(request.entityID)
- } as HttpPricingDefinitionsRequest;
- UtilsSecurity.filterSkipAndLimit(request, filteredRequest);
- UtilsSecurity.filterSort(request, filteredRequest);
- return filteredRequest;
- }
-
- static filterPricingDefinitionUpdateRequest(request: any): Partial<PricingDefinition> {
- const filteredRequest = PricingSecurity._filterPricingDefinitionRequest(request);
- return {
- id: sanitize(request.id),
- ...filteredRequest
- };
- }
-
- public static filterPricingDefinitionCreateRequest(request: any): Partial<PricingDefinition> {
- return PricingSecurity._filterPricingDefinitionRequest(request);
- }
-
- public static _filterPricingDefinitionRequest(request: any): Partial<PricingDefinition> {
- const filteredRequest = {
- entityID: sanitize(request.entityID),
- entityType: sanitize(request.entityType),
- name: sanitize(request.name),
- description: sanitize(request.description),
- // TODO - not yet sanitized properly
- staticRestrictions: sanitize(request.staticRestrictions),
- restrictions: sanitize(request.restrictions),
- dimensions: sanitize(request.dimensions),
- } as Partial<PricingDefinition>;
- return filteredRequest;
- }
-}

MODIFY
src/server/rest/v1/validator/PricingValidator.ts
src/server/rest/v1/validator/PricingValidator.ts
@@ -7,17 +7,17 @@ import global from '../../../../types/GlobalType';
export default class PricingValidator extends SchemaValidator {
private static instance: PricingValidator|null = null;
- private pricingGet: Schema;
- private pricingsGet: Schema;
- private princingCreate: Schema;
- private princingUpdate: Schema;
+ private pricingDefinitionGet: Schema;
+ private pricingDefinitionsGet: Schema;
+ private pricingDefinitionCreate: Schema;
+ private pricingDefinitionUpdate: Schema;
private constructor() {
super('PricingValidator');
- this.pricingGet = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definition-get.json`, 'utf8'));
- this.pricingsGet = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definitions-get.json`, 'utf8'));
- this.princingCreate = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definition-create.json`, 'utf8'));
- this.princingUpdate = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definition-update.json`, 'utf8'));
+ this.pricingDefinitionGet = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definition-get.json`, 'utf8'));
+ this.pricingDefinitionsGet = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definitions-get.json`, 'utf8'));
+ this.pricingDefinitionCreate = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definition-create.json`, 'utf8'));
+ this.pricingDefinitionUpdate = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definition-update.json`, 'utf8'));
}
public static getInstance(): PricingValidator {
@@ -27,23 +27,19 @@ export default class PricingValidator extends SchemaValidator {
return PricingValidator.instance;
}
- public validatePricingGet(data: any): Partial<HttpPricingDefinitionsRequest> {
- this.validate('validatePricingGet', this.pricingGet, data);
- return data;
+ public validatePricingDefinitionGet(data: Record<string, unknown>): Partial<HttpPricingDefinitionsRequest> {
+ return this.validate(this.pricingDefinitionGet, data);
}
- public validatePricingsGet(data: any): Partial<HttpPricingDefinitionsRequest> {
- this.validate('validatePricingsGet', this.pricingsGet, data);
- return data;
+ public validatePricingDefinitionsGet(data: Record<string, unknown>): Partial<HttpPricingDefinitionsRequest> {
+ return this.validate(this.pricingDefinitionsGet, data);
}
- public validatePricingCreate(data: any): Partial<PricingDefinition> {
- this.validate('validatePricingCreate', this.princingCreate, data);
- return data;
+ public validatePricingDefinitionCreate(data: Record<string, unknown>): PricingDefinition {
+ return this.validate(this.pricingDefinitionCreate, data);
}
- public validatePricingUpdate(data: any): Partial<PricingDefinition> {
- this.validate('validatePricingUpdate', this.princingUpdate, data);
- return data;
+ public validatePricingDefinitionUpdate(data: Record<string, unknown>): PricingDefinition {
+ return this.validate(this.pricingDefinitionUpdate, data);
}
}

MODIFY
src/utils/FeatureToggles.ts
src/utils/FeatureToggles.ts
@@ -5,6 +5,7 @@ export enum Feature {
BILLING_CHECK_THRESHOLD_ON_STOP,
BILLING_PREVENT_CUSTOMER_DELETION,
PRICING_NEW_MODEL,
+ PRICING_CHECK_BACKWARD_COMPATIBILITY,
BILLING_SHOW_PRICING_DETAIL,
BILLING_SHOW_SESSION_HEADER,
}
@@ -16,6 +17,7 @@ export default class FeatureToggles {
Feature.BILLING_CHECK_THRESHOLD_ON_STOP,
Feature.BILLING_PREVENT_CUSTOMER_DELETION,
Feature.PRICING_NEW_MODEL,
+ Feature.PRICING_CHECK_BACKWARD_COMPATIBILITY,
// Feature.BILLING_SHOW_PRICING_DETAIL,
Feature.BILLING_SHOW_SESSION_HEADER,
];

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -351,7 +351,8 @@ class TestData {
assert(billingDataStop?.invoiceNumber === null, `Invoice Number should not yet been set - Invoice Number is: ${billingDataStop?.invoiceNumber}`);
}
if (expectedPrice) {
- if (!FeatureToggles.isFeatureActive(Feature.PRICING_NEW_MODEL)) {
+ if (!FeatureToggles.isFeatureActive(Feature.PRICING_NEW_MODEL)
+ || FeatureToggles.isFeatureActive(Feature.PRICING_CHECK_BACKWARD_COMPATIBILITY)) {
expectedPrice = 32.32; // Expected price when using the Simple Pricing logic!
}
// --------------------------------

MODIFY
test/api/client/PricingApi.ts
test/api/client/PricingApi.ts
@@ -15,7 +15,7 @@ export default class PricingApi extends CrudApi {
}
public async readPricingDefinition(id: string) {
- return super.read({ ID: id }, this.buildRestEndpointUrl(ServerRoute.REST_PRICING_DEFINITION, { id }));
+ return super.readById(id, this.buildRestEndpointUrl(ServerRoute.REST_PRICING_DEFINITION, { id }));
}
public async updatePricingDefinition(data: PricingDefinition) {

pricing service - adaptation to the state-of-the-art