pricing - resolve pricing context endpoint
ADD
src/assets/server/rest/v1/schemas/pricing/pricing-model-resolve.json
+{
+ "$id": "pricing-model-resolve",
+ "title": "Resolve Pricing Model to Preview Tariffs",
+ "type": "object",
+ "properties": {
+ "ChargingStationID": {
+ "$ref": "chargingstation#/definitions/id"
+ },
+ "ConnectorID": {
+ "$ref": "chargingstation#/definitions/connector/properties/connectorId"
+ },
+ "UserID": {
+ "$ref": "common#/definitions/id"
+ },
+ "DateTime": {
+ "type": "string",
+ "format": "date-time",
+ "customType": "date",
+ "sanitize": "mongo"
+ }
+ },
+ "required": [
+ "ChargingStationID",
+ "ConnectorID"
+ ]
+}

MODIFY
src/integration/pricing/PricingEngine.ts
src/integration/pricing/PricingEngine.ts
@@ -16,19 +16,18 @@ const MODULE_NAME = 'PricingEngine';
export default class PricingEngine {
- public static async resolvePricingContext(pricingContext: PricingContext): Promise<ResolvedPricingModel> {
+ public static async resolvePricingContext(tenant: Tenant, pricingContext: PricingContext): Promise<ResolvedPricingModel> {
// Merge the pricing definitions from the different contexts
const pricingDefinitions: ResolvedPricingDefinition[] = [];
- // pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction.userID));
- pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(pricingContext, PricingEntity.CHARGING_STATION, pricingContext.chargingStationID));
+ pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, pricingContext, PricingEntity.CHARGING_STATION, pricingContext.chargingStationID));
// pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, pricingContext, PricingEntity.SITE_AREA, transaction.siteAreaID));
- pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(pricingContext, PricingEntity.SITE, pricingContext.siteID));
+ pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, pricingContext, PricingEntity.SITE, pricingContext.siteID));
// pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, pricingContext, PricingEntity.COMPANY, transaction.companyID));
- pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(pricingContext, PricingEntity.TENANT, pricingContext.tenant.id));
+ pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, pricingContext, PricingEntity.TENANT, tenant.id));
if (!pricingContext.timezone) {
await Logging.logWarning({
...LoggingHelper.getPricingContextProperties(pricingContext),
- tenantID: pricingContext.tenant.id,
+ tenantID: tenant.id,
module: MODULE_NAME,
action: ServerAction.PRICING,
method: 'resolvePricingContext',
@@ -46,7 +45,7 @@ export default class PricingEngine {
};
await Logging.logInfo({
...LoggingHelper.getPricingContextProperties(pricingContext),
- tenantID: pricingContext.tenant.id,
+ tenantID: tenant.id,
module: MODULE_NAME,
action: ServerAction.PRICING,
method: 'resolvePricingContext',
@@ -74,11 +73,11 @@ export default class PricingEngine {
return pricedData.filter((pricingConsumptionData) => !!pricingConsumptionData);
}
- private static async getPricingDefinitions4Entity(pricingContext: PricingContext, entityType: PricingEntity, entityID: string): Promise<ResolvedPricingDefinition[]> {
+ private static async getPricingDefinitions4Entity(tenant: Tenant, pricingContext: PricingContext, entityType: PricingEntity, entityID: string): Promise<ResolvedPricingDefinition[]> {
if (!entityID) {
await Logging.logWarning({
...LoggingHelper.getPricingContextProperties(pricingContext),
- tenantID: pricingContext.tenant.id,
+ tenantID: tenant.id,
module: MODULE_NAME,
action: ServerAction.PRICING,
method: 'getPricingDefinitions4Entity',
@@ -86,7 +85,7 @@ export default class PricingEngine {
});
return [];
}
- let pricingDefinitions = await PricingEngine.fetchPricingDefinitions4Entity(pricingContext.tenant, entityType, entityID);
+ let pricingDefinitions = await PricingEngine.fetchPricingDefinitions4Entity(tenant, entityType, entityID);
pricingDefinitions = pricingDefinitions || [];
const actualPricingDefinitions = pricingDefinitions.filter((pricingDefinition) =>
PricingEngine.checkEntityType(pricingDefinition, entityType)
@@ -97,7 +96,7 @@ export default class PricingEngine {
);
await Logging.logDebug({
...LoggingHelper.getPricingContextProperties(pricingContext),
- tenantID: pricingContext.tenant.id,
+ tenantID: tenant.id,
module: MODULE_NAME,
action: ServerAction.PRICING,
method: 'getPricingDefinitions4Entity',

MODIFY
src/integration/pricing/PricingHelper.ts
src/integration/pricing/PricingHelper.ts
@@ -6,7 +6,6 @@ import ChargingStation from '../../types/ChargingStation';
import Decimal from 'decimal.js';
import Tenant from '../../types/Tenant';
import Transaction from '../../types/Transaction';
-import User from '../../types/User';
import Utils from '../../utils/Utils';
export default class PricingHelper {
@@ -16,7 +15,6 @@ export default class PricingHelper {
const connectorType = Utils.getConnectorFromID(chargingStation, connectorId)?.type;
const connectorPower = Utils.getConnectorFromID(chargingStation, connectorId)?.power;
return {
- tenant,
userID: transaction.userID,
companyID: transaction.companyID,
siteID : transaction.siteID,
@@ -30,12 +28,11 @@ export default class PricingHelper {
};
}
- public static buildUserPricingContext(tenant: Tenant, user: User, chargingStation: ChargingStation, connectorId: number, timestamp: Date): PricingContext {
+ public static buildUserPricingContext(tenant: Tenant, userID: string, chargingStation: ChargingStation, connectorId: number, timestamp = new Date()): PricingContext {
const connectorType = Utils.getConnectorFromID(chargingStation, connectorId)?.type;
const connectorPower = Utils.getConnectorFromID(chargingStation, connectorId)?.power;
return {
- tenant,
- userID: user.id,
+ userID,
companyID: chargingStation.companyID,
siteID : chargingStation.siteID,
siteAreaID: chargingStation.siteAreaID,
@@ -49,8 +46,7 @@ export default class PricingHelper {
}
public static checkContextConsistency(pricingContext: PricingContext): boolean {
- if (pricingContext.tenant
- && pricingContext.userID
+ if (pricingContext.userID
&& pricingContext.companyID
&& pricingContext.siteID
&& pricingContext.siteAreaID

MODIFY
src/integration/pricing/simple-pricing/BuiltInPricingIntegration.ts
src/integration/pricing/simple-pricing/BuiltInPricingIntegration.ts
@@ -79,7 +79,7 @@ export default class BuiltInPricingIntegration extends PricingIntegration<Simple
});
return null;
}
- const resolvedPricingModel: ResolvedPricingModel = await PricingEngine.resolvePricingContext(pricingContext);
+ const resolvedPricingModel: ResolvedPricingModel = await PricingEngine.resolvePricingContext(this.tenant, pricingContext);
// Fallback when no pricing definition matches
resolvedPricingModel.pricingDefinitions.push(this.getDefaultPricingDefinition());
return resolvedPricingModel;

MODIFY
src/server/rest/v1/router/api/PricingRouter.ts
src/server/rest/v1/router/api/PricingRouter.ts
@@ -21,6 +21,7 @@ export default class PricingRouter {
this.buildRouteCreatePricingDefinition();
this.buildRouteDeletePricingDefinition();
this.buildRouteUpdatePricingDefinition();
+ this.buildRouteResolvePricingModel();
return this.router;
}
@@ -56,4 +57,12 @@ export default class PricingRouter {
await RouterUtils.handleServerAction(PricingService.handleUpdatePricingDefinition.bind(this), ServerAction.PRICING_DEFINITION_UPDATE, req, res, next);
});
}
+
+ protected buildRouteResolvePricingModel(): void {
+ this.router.get(`/${ServerRoute.REST_PRICING_MODEL_RESOLVE}`, async (req: Request, res: Response, next: NextFunction) => {
+ req.query.ID = req.params.id;
+ await RouterUtils.handleServerAction(PricingService.handleResolvePricingModel.bind(this), ServerAction.PRICING_MODEL_RESOLVE, req, res, next);
+ });
+ }
+
}

MODIFY
src/server/rest/v1/service/PricingService.ts
src/server/rest/v1/service/PricingService.ts
import { Action, Entity } from '../../../../types/Authorization';
import { NextFunction, Request, Response } from 'express';
-import PricingDefinition, { PricingEntity } from '../../../../types/Pricing';
+import PricingDefinition, { PricingContext, PricingEntity, ResolvedPricingDefinition, ResolvedPricingModel } from '../../../../types/Pricing';
import AppAuthError from '../../../../exception/AppAuthError';
import AuthorizationService from './AuthorizationService';
@@ -9,18 +9,49 @@ import Constants from '../../../../utils/Constants';
import { HTTPAuthError } from '../../../../types/HTTPError';
import Logging from '../../../../utils/Logging';
import { PricingDefinitionDataResult } from '../../../../types/DataResult';
+import PricingFactory from '../../../../integration/pricing/PricingFactory';
+import PricingHelper from '../../../../integration/pricing/PricingHelper';
import PricingStorage from '../../../../storage/mongodb/PricingStorage';
import PricingValidator from '../validator/PricingValidator';
import { ServerAction } from '../../../../types/Server';
import Site from '../../../../types/Site';
import SiteArea from '../../../../types/SiteArea';
import { TenantComponents } from '../../../../types/Tenant';
+import Utils from '../../../../utils/Utils';
import UtilsService from './UtilsService';
const MODULE_NAME = 'PricingService';
export default class PricingService {
+ public static async handleResolvePricingModel(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.PRICING,
+ Action.RESOLVE, Entity.PRICING_DEFINITION, MODULE_NAME, 'handleResolvePricingModel');
+ // Filter
+ const filteredRequest = PricingValidator.getInstance().validatePricingModelResolve(req.query);
+ let pricingContext: PricingContext = null;
+ let pricingDefinitions: ResolvedPricingDefinition[] = [];
+ if (Utils.isComponentActiveFromToken(req.user, TenantComponents.PRICING)) {
+ const pricingImpl = await PricingFactory.getPricingImpl(req.tenant);
+ if (pricingImpl) {
+ // Fetch the charging station data required for resolving the pricing context
+ // TODO: how to only read the required data? - require projected fields: ['id', 'companyID', 'siteID', 'siteAreaID', 'coordinates']
+ const chargingStation = await UtilsService.checkAndGetChargingStationAuthorization(req.tenant, req.user, filteredRequest.ChargingStationID, action);
+ // Resolve the pricing context
+ pricingContext = PricingHelper.buildUserPricingContext(req.tenant, filteredRequest.UserID, chargingStation, filteredRequest.ConnectorID, filteredRequest.StartDateTime);
+ const pricingModel: ResolvedPricingModel = await pricingImpl.resolvePricingContext(pricingContext);
+ pricingDefinitions = pricingModel.pricingDefinitions;
+ }
+ }
+ res.json({
+ pricingContext,
+ pricingDefinitions
+ });
+ 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,

MODIFY
src/server/rest/v1/service/UserService.ts
src/server/rest/v1/service/UserService.ts
@@ -833,7 +833,7 @@ export default class UserService {
if (Utils.isComponentActiveFromToken(loggedUser, TenantComponents.PRICING)) {
const pricingImpl = await PricingFactory.getPricingImpl(tenant);
if (pricingImpl) {
- const pricingContext = PricingHelper.buildUserPricingContext(tenant, user, chargingStation, connectorId, new Date());
+ const pricingContext = PricingHelper.buildUserPricingContext(tenant, user.id, chargingStation, connectorId, new Date());
const pricingModel: ResolvedPricingModel = await pricingImpl.resolvePricingContext(pricingContext);
return pricingModel.pricingDefinitions;
}

MODIFY
src/server/rest/v1/validator/PricingValidator.ts
src/server/rest/v1/validator/PricingValidator.ts
-import { HttpPricingDefinitionsRequest } from '../../../../types/requests/HttpPricingRequest';
+import { HttpPricingDefinitionsRequest, HttpPricingModelResolutionRequest } from '../../../../types/requests/HttpPricingRequest';
+
import PricingDefinition from '../../../../types/Pricing';
import Schema from '../../../../types/validator/Schema';
import SchemaValidator from '../../../../validator/SchemaValidator';
@@ -11,6 +12,7 @@ export default class PricingValidator extends SchemaValidator {
private pricingDefinitionsGet: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definitions-get.json`, 'utf8'));
private pricingDefinitionCreate: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definition-create.json`, 'utf8'));
private pricingDefinitionUpdate: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-definition-update.json`, 'utf8'));
+ private pricingModelResolve: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/pricing/pricing-model-resolve.json`, 'utf8'));
private constructor() {
super('PricingValidator');
@@ -38,4 +40,8 @@ export default class PricingValidator extends SchemaValidator {
public validatePricingDefinitionUpdate(data: Record<string, unknown>): PricingDefinition {
return this.validate(this.pricingDefinitionUpdate, data);
}
+
+ public validatePricingModelResolve(data: Record<string, unknown>): HttpPricingModelResolutionRequest {
+ return this.validate(this.pricingModelResolve, data);
+ }
}

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -156,6 +156,7 @@ export enum Action {
EXPORT_OCPP_PARAMS = 'ExportOCPPParams',
GENERATE_QR = 'GenerateQrCode',
MAINTAIN_PRICING_DEFINITIONS = 'MaintainPricingDefinitions',
+ RESOLVE = 'Resolve',
}
export interface AuthorizationContext {

MODIFY
src/types/Pricing.ts
src/types/Pricing.ts
-import ChargingStation, { ConnectorType } from './ChargingStation';
-
import { AuthorizationActions } from './Authorization';
+import { ConnectorType } from './ChargingStation';
import CreatedUpdatedProps from './CreatedUpdatedProps';
import Decimal from 'decimal.js';
-import Tenant from './Tenant';
export enum PricingEntity {
TENANT = 'Tenant',
@@ -22,7 +20,6 @@ export enum DimensionType {
}
export interface PricingContext {
- tenant: Tenant,
userID: string
companyID: string,
siteID: string,

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -439,6 +439,7 @@ export enum ServerAction {
PRICING_DEFINITION_CREATE = 'PricingDefinitionCreate',
PRICING_DEFINITION_UPDATE = 'PricingDefinitionUpdate',
PRICING_DEFINITION_DELETE = 'PricingDefinitionDelete',
+ PRICING_MODEL_RESOLVE = 'PricingModelResolve',
MONGO_DB = 'MongoDB',
@@ -666,7 +667,10 @@ export enum ServerRoute {
// PRICING URLs for CRUD operations
REST_PRICING_DEFINITIONS = 'pricing-definitions',
- REST_PRICING_DEFINITION = 'pricing-definitions/:id'
+ REST_PRICING_DEFINITION = 'pricing-definitions/:id',
+
+ // PRICING URLs for Non-CRUD operations
+ REST_PRICING_MODEL_RESOLVE = 'pricing-model/resolve',
}
export enum ServerProtocol {

MODIFY
src/types/requests/HttpPricingRequest.ts
src/types/requests/HttpPricingRequest.ts
@@ -14,3 +14,10 @@ export interface HttpPricingDefinitionsRequest extends HttpDatabaseRequest {
EntityType?: PricingEntity;
WithEntityInformation?: boolean;
}
+
+export interface HttpPricingModelResolutionRequest extends HttpDatabaseRequest {
+ ChargingStationID: string;
+ ConnectorID: number;
+ UserID?: string
+ StartDateTime?: Date;
+}

pricing - resolve pricing context endpoint