Pricing - Pricing Service, etc
MODIFY
src/server/rest/v1/service/AuthorizationService.ts
src/server/rest/v1/service/AuthorizationService.ts
import { Action, AuthorizationActions, AuthorizationContext, AuthorizationFilter, Entity, SiteAreaAuthorizationActions } from '../../../../types/Authorization';
import { Car, CarCatalog } from '../../../../types/Car';
-import { CarCatalogDataResult, CarDataResult, CompanyDataResult, SiteAreaDataResult, SiteDataResult, TagDataResult, UserDataResult } from '../../../../types/DataResult';
+import { CarCatalogDataResult, CarDataResult, CompanyDataResult, PricingDataResult, SiteAreaDataResult, SiteDataResult, TagDataResult, UserDataResult } from '../../../../types/DataResult';
import { HttpAssignAssetsToSiteAreaRequest, HttpSiteAreaRequest, HttpSiteAreasRequest } from '../../../../types/requests/HttpSiteAreaRequest';
import { HttpCarCatalogRequest, HttpCarCatalogsRequest, HttpCarRequest, HttpCarsRequest } from '../../../../types/requests/HttpCarRequest';
import { HttpChargingStationRequest, HttpChargingStationsRequest } from '../../../../types/requests/HttpChargingStationRequest';
@@ -19,6 +19,8 @@ import DynamicAuthorizationFactory from '../../../../authorization/DynamicAuthor
import { EntityDataType } from '../../../../types/GlobalType';
import { HTTPAuthError } from '../../../../types/HTTPError';
import { HttpAssetsRequest } from '../../../../types/requests/HttpAssetRequest';
+import { HttpPricingRequest } from '../../../../types/requests/HttpPricingRequest';
+import Pricing from '../../../../types/Pricing';
import { ServerAction } from '../../../../types/Server';
import Site from '../../../../types/Site';
import SiteArea from '../../../../types/SiteArea';
@@ -385,6 +387,46 @@ export default class AuthorizationService {
tenant, Entity.COMPANY, userToken, filteredRequest, filteredRequest.ID ? { CompanyID: filteredRequest.ID } : {}, authAction, entityData);
}
+ public static async checkAndGetPricingsAuthorizations(tenant: Tenant, userToken: UserToken,
+ filteredRequest: Partial<HttpPricingRequest>): Promise<AuthorizationFilter> {
+ const authorizationFilters: AuthorizationFilter = {
+ filters: {},
+ dataSources: new Map(),
+ projectFields: [],
+ authorized: false,
+ };
+ // Check static & dynamic authorization
+ await AuthorizationService.canPerformAuthorizationAction(tenant, userToken, Entity.PRICINGS, Action.LIST,
+ authorizationFilters, filteredRequest);
+ return authorizationFilters;
+ }
+
+ public static async addPricingsAuthorizations(tenant: Tenant, userToken: UserToken, pricings: PricingDataResult, authorizationFilter: AuthorizationFilter): Promise<void> {
+ // Add canCreate flag to root
+ pricings.canCreate = await AuthorizationService.canPerformAuthorizationAction(
+ tenant, userToken, Entity.PRICING, Action.CREATE, authorizationFilter);
+ // Enrich
+ for (const pricing of pricings.result) {
+ await AuthorizationService.addPricingAuthorizations(tenant, userToken, pricing, authorizationFilter);
+ }
+ }
+
+ public static async addPricingAuthorizations(tenant: Tenant, userToken: UserToken, pricing: Pricing, authorizationFilter: AuthorizationFilter): Promise<void> {
+ // Enrich
+ pricing.canRead = await AuthorizationService.canPerformAuthorizationAction(
+ tenant, userToken, Entity.PRICING, Action.READ, authorizationFilter, { CompanyID: pricing.id }, pricing);
+ pricing.canDelete = await AuthorizationService.canPerformAuthorizationAction(
+ tenant, userToken, Entity.PRICING, Action.DELETE, authorizationFilter, { CompanyID: pricing.id }, pricing);
+ pricing.canUpdate = await AuthorizationService.canPerformAuthorizationAction(
+ tenant, userToken, Entity.PRICING, Action.UPDATE, authorizationFilter, { CompanyID: pricing.id }, pricing);
+ }
+
+ public static async checkAndGetPricingAuthorizations(tenant: Tenant, userToken: UserToken,
+ filteredRequest: Partial<HttpCompanyRequest>, authAction: Action, entityData?: EntityDataType): Promise<AuthorizationFilter> {
+ return AuthorizationService.checkAndGetEntityAuthorizations(
+ tenant, Entity.PRICING, userToken, filteredRequest, filteredRequest.ID ? { PricingID: filteredRequest.ID } : {}, authAction, entityData);
+ }
+
public static async checkAndGetSiteAreaAuthorizations(tenant: Tenant, userToken: UserToken,
filteredRequest: Partial<HttpSiteAreaRequest>, authAction: Action, entityData?: EntityDataType): Promise<AuthorizationFilter> {
return AuthorizationService.checkAndGetEntityAuthorizations(

ADD
src/server/rest/v1/service/PricingService.ts
+import { Action, Entity } from '../../../../types/Authorization';
+import { NextFunction, Request, Response } from 'express';
+
+import AppAuthError from '../../../../exception/AppAuthError';
+import AuthorizationService from './AuthorizationService';
+import Constants from '../../../../utils/Constants';
+import { HTTPAuthError } from '../../../../types/HTTPError';
+import Logging from '../../../../utils/Logging';
+import Pricing from '../../../../types/Pricing';
+import { PricingDataResult } from '../../../../types/DataResult';
+import PricingSecurity from './security/PricingSecurity';
+import PricingStorage from '../../../../storage/mongodb/PricingStorage';
+import { ServerAction } from '../../../../types/Server';
+import TenantComponents from '../../../../types/TenantComponents';
+import UtilsService from './UtilsService';
+
+const MODULE_NAME = 'PricingService';
+
+export default class PricingService {
+
+ public static async handleDeletePricing(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ORGANIZATION,
+ Action.DELETE, Entity.COMPANY, MODULE_NAME, 'handleDeletePricing');
+ // Filter
+ const pricingID = PricingSecurity.filterPricingRequestByID(req.query);
+ UtilsService.assertIdIsProvided(action, pricingID, MODULE_NAME, 'handleDeletePricing', req.user);
+ // Check and Get Pricing
+ const pricing = await UtilsService.checkAndGetPricingAuthorization(
+ req.tenant, req.user, pricingID, Action.DELETE, action);
+ // Delete
+ await PricingStorage.deletePricing(req.tenant, pricing.id);
+ // Log
+ await Logging.logSecurityInfo({
+ tenantID: req.user.tenantID,
+ user: req.user, module: MODULE_NAME, method: 'handleDeletePricing',
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ message: `Pricing '${pricingID}' has been deleted successfully`,
+ action: action,
+ detailedMessages: { pricing }
+ });
+ // Ok
+ res.json(Constants.REST_RESPONSE_SUCCESS);
+ next();
+ }
+
+ public static async handleGetPricing(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ORGANIZATION,
+ Action.READ, Entity.COMPANY, MODULE_NAME, 'handleGetPricing');
+ // Filter
+ const filteredRequest = PricingSecurity.filterPricingRequest(req.query);
+ UtilsService.assertIdIsProvided(action, filteredRequest.ID, MODULE_NAME, 'handleGetPricing', req.user);
+ // Check and Get Pricing
+ const pricing = await UtilsService.checkAndGetPricingAuthorization(
+ req.tenant, req.user, filteredRequest.ID, Action.READ, action, null, {
+ withLogo: true
+ }, true);
+ res.json(pricing);
+ next();
+ }
+
+ public static async handleGetPricings(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ORGANIZATION,
+ Action.LIST, Entity.PRICINGS, MODULE_NAME, 'handleGetPricings');
+ // Filter
+ const filteredRequest = PricingSecurity.filterPricingsRequest(req.query);
+ // Check dynamic auth
+ const authorizationPricingsFilter = await AuthorizationService.checkAndGetPricingsAuthorizations(
+ req.tenant, req.user, filteredRequest);
+ if (!authorizationPricingsFilter.authorized) {
+ UtilsService.sendEmptyDataResult(res, next);
+ return;
+ }
+ // Get the companies
+ const pricings = await PricingStorage.getPricings(req.tenant,
+ {
+ // search: filteredRequest.Search,
+ ...authorizationPricingsFilter.filters
+ },
+ {
+ limit: filteredRequest.Limit,
+ skip: filteredRequest.Skip,
+ sort: filteredRequest.SortFields,
+ onlyRecordCount: filteredRequest.OnlyRecordCount
+ },
+ authorizationPricingsFilter.projectFields
+ );
+ // Add Auth flags
+ await AuthorizationService.addPricingsAuthorizations(req.tenant, req.user, pricings as PricingDataResult, authorizationPricingsFilter);
+ // Return
+ res.json(pricings);
+ next();
+ }
+
+ public static async handleCreatePricing(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ORGANIZATION,
+ Action.CREATE, Entity.COMPANY, MODULE_NAME, 'handleCreatePricing');
+ // Filter
+ const filteredRequest = PricingSecurity.filterPricingCreateRequest(req.body);
+ // Check
+ UtilsService.checkIfPricingValid(filteredRequest, req);
+ // Get dynamic auth
+ const authorizationFilter = await AuthorizationService.checkAndGetPricingAuthorizations(
+ req.tenant, req.user, {}, Action.CREATE, filteredRequest as Pricing);
+ if (!authorizationFilter.authorized) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.FORBIDDEN,
+ user: req.user,
+ action: Action.CREATE, entity: Entity.COMPANY,
+ module: MODULE_NAME, method: 'handleCreatePricing'
+ });
+ }
+ // Create pricing
+ const newPricing: Pricing = {
+ ...filteredRequest,
+ issuer: true,
+ createdBy: { id: req.user.id },
+ createdOn: new Date()
+ } as Pricing;
+ // Save
+ newPricing.id = await PricingStorage.savePricing(req.tenant, newPricing);
+ // Log
+ await Logging.logSecurityInfo({
+ tenantID: req.user.tenantID,
+ user: req.user, module: MODULE_NAME, method: 'handleCreatePricing',
+ message: `Pricing '${newPricing.id}' has been created successfully`,
+ action: action,
+ detailedMessages: { pricing: newPricing }
+ });
+ // Ok
+ res.json(Object.assign({ id: newPricing.id }, Constants.REST_RESPONSE_SUCCESS));
+ next();
+ }
+
+ public static async handleUpdatePricing(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ORGANIZATION,
+ Action.UPDATE, Entity.COMPANY, MODULE_NAME, 'handleUpdatePricing');
+ // Filter
+ const filteredRequest = PricingSecurity.filterPricingUpdateRequest(req.body);
+ // Check Mandatory fields
+ UtilsService.checkIfPricingValid(filteredRequest, req);
+ // Check and Get Pricing
+ const pricing = await UtilsService.checkAndGetPricingAuthorization(
+ req.tenant, req.user, filteredRequest.id, Action.UPDATE, action, filteredRequest as Pricing);
+ // Update
+ pricing.lastChangedBy = { 'id': req.user.id };
+ pricing.lastChangedOn = new Date();
+ // Update Pricing
+ await PricingStorage.savePricing(req.tenant, pricing);
+ // Log
+ await Logging.logSecurityInfo({
+ tenantID: req.user.tenantID,
+ user: req.user, module: MODULE_NAME, method: 'handleUpdatePricing',
+ message: `Pricing '${pricing.id}' has been updated 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
@@ -25,6 +25,8 @@ import Logging from '../../../../utils/Logging';
import OCPIEndpoint from '../../../../types/ocpi/OCPIEndpoint';
import OICPEndpoint from '../../../../types/oicp/OICPEndpoint';
import PDFDocument from 'pdfkit';
+import Pricing from '../../../../types/Pricing';
+import PricingStorage from '../../../../storage/mongodb/PricingStorage';
import { ServerAction } from '../../../../types/Server';
import Site from '../../../../types/Site';
import SiteArea from '../../../../types/SiteArea';
@@ -106,6 +108,48 @@ export default class UtilsService {
return chargingStation;
}
+ public static async checkAndGetPricingAuthorization(tenant: Tenant, userToken: UserToken, pricingID: string, authAction: Action,
+ action: ServerAction, entityData?: EntityDataType, additionalFilters: Record<string, any> = {}, applyProjectFields = false, checkIssuer = true): Promise<Pricing> {
+ // Check mandatory fields
+ UtilsService.assertIdIsProvided(action, pricingID, MODULE_NAME, 'checkAndGetPricingAuthorization', userToken);
+ // Get dynamic auth
+ const authorizationFilter = await AuthorizationService.checkAndGetPricingAuthorizations(
+ tenant, userToken, { ID: pricingID }, authAction, entityData);
+ if (!authorizationFilter.authorized) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.FORBIDDEN,
+ user: userToken,
+ action: authAction, entity: Entity.COMPANY,
+ module: MODULE_NAME, method: 'checkAndGetPricingAuthorization',
+ value: pricingID
+ });
+ }
+ // Get Pricing
+ const pricing = await PricingStorage.getPricing(tenant, pricingID,
+ {
+ ...additionalFilters,
+ ...authorizationFilter.filters
+ },
+ applyProjectFields ? authorizationFilter.projectFields : null
+ );
+ UtilsService.assertObjectExists(action, pricing, `Pricing ID '${pricingID}' does not exist`,
+ MODULE_NAME, 'checkAndGetPricingAuthorization', userToken);
+ // Add actions
+ await AuthorizationService.addPricingAuthorizations(tenant, userToken, pricing, authorizationFilter);
+ // Check
+ const authorized = AuthorizationService.canPerformAction(pricing, authAction);
+ if (!authorized) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.FORBIDDEN,
+ user: userToken,
+ action: authAction, entity: Entity.COMPANY,
+ module: MODULE_NAME, method: 'checkAndGetPricingAuthorization',
+ value: pricingID
+ });
+ }
+ return pricing;
+ }
+
public static async checkAndGetCompanyAuthorization(tenant: Tenant, userToken: UserToken, companyID: string, authAction: Action,
action: ServerAction, entityData?: EntityDataType, additionalFilters: Record<string, any> = {}, applyProjectFields = false, checkIssuer = true): Promise<Company> {
// Check mandatory fields
@@ -1286,6 +1330,27 @@ export default class UtilsService {
}
}
+ public static checkIfPricingValid(pricing: Partial<Pricing>, req: Request): void {
+ if (req.method !== 'POST' && !pricing.id) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Pricing ID is mandatory',
+ module: MODULE_NAME, method: 'checkIfPricingValid',
+ user: req.user.id
+ });
+ }
+ if (!pricing.pricingDefinitions) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Pricing Definition is mandatory',
+ module: MODULE_NAME, method: 'checkIfPricingValid',
+ user: req.user.id
+ });
+ }
+ }
+
public static checkIfAssetValid(asset: Partial<Asset>, req: Request): void {
if (req.method !== 'POST' && !asset.id) {
throw new AppError({

ADD
src/server/rest/v1/service/security/PricingSecurity.ts
+import { HttpPricingRequest, HttpPricingsRequest } from '../../../../../types/requests/HttpPricingRequest';
+
+import Pricing from '../../../../../types/Pricing';
+import UtilsSecurity from './UtilsSecurity';
+import sanitize from 'mongo-sanitize';
+
+export default class PricingSecurity {
+
+ public static filterPricingRequestByID(request: any): string {
+ return sanitize(request.ID);
+ }
+
+ public static filterPricingRequest(request: any): HttpPricingRequest {
+ return {
+ ID: sanitize(request.ID)
+ };
+ }
+
+ public static filterPricingsRequest(request: any): HttpPricingsRequest {
+ const filteredRequest: HttpPricingsRequest = {
+ Search: sanitize(request.Search),
+ } as HttpPricingsRequest;
+ UtilsSecurity.filterSkipAndLimit(request, filteredRequest);
+ UtilsSecurity.filterSort(request, filteredRequest);
+ return filteredRequest;
+ }
+
+ static filterPricingUpdateRequest(request: any): Partial<Pricing> {
+ const filteredRequest = PricingSecurity._filterPricingRequest(request);
+ return {
+ id: sanitize(request.id),
+ ...filteredRequest
+ };
+ }
+
+ public static filterPricingCreateRequest(request: any): Partial<Pricing> {
+ return PricingSecurity._filterPricingRequest(request);
+ }
+
+ public static _filterPricingRequest(request: any): Partial<Pricing> {
+ const filteredRequest = {
+ name: sanitize(request.name),
+ address: UtilsSecurity.filterAddressRequest(request.address)
+ } as Partial<Pricing>;
+ return filteredRequest;
+ }
+}

MODIFY
src/storage/mongodb/PricingStorage.ts
src/storage/mongodb/PricingStorage.ts
@@ -13,7 +13,7 @@ const MODULE_NAME = 'PricingStorage';
export default class PricingStorage {
- public static async savePricing(tenant: Tenant, pricing: Pricing): Promise<void> {
+ public static async savePricing(tenant: Tenant, pricing: Pricing): Promise<string> {
// Debug
const uniqueTimerID = Logging.traceStart(tenant.id, MODULE_NAME, 'savePricing');
// Check Tenant
@@ -32,6 +32,7 @@ export default class PricingStorage {
{ upsert: true, returnDocument: 'after' });
// Debug
await Logging.traceEnd(tenant.id, MODULE_NAME, 'savePricing', uniqueTimerID, pricingMDB);
+ return pricingMDB._id.toString();
}
public static async deletePricing(tenant: Tenant, pricingID: string): Promise<void> {

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -69,6 +69,7 @@ export enum Entity {
LOGGINGS = 'Loggings',
LOGGING = 'Logging',
PRICING = 'Pricing',
+ PRICINGS = 'Pricings',
BILLING = 'Billing',
SETTING = 'Setting',
SETTINGS = 'Settings',

MODIFY
src/types/DataResult.ts
src/types/DataResult.ts
import { Car, CarCatalog } from './Car';
import Company from './Company';
+import Pricing from './Pricing';
import Site from './Site';
import SiteArea from './SiteArea';
import Tag from './Tag';
@@ -17,6 +18,10 @@ export interface DataResult<T> {
projectedFields?: string[];
}
+export interface PricingDataResult extends DataResult<Pricing>{
+ canCreate: boolean;
+}
+
export interface CompanyDataResult extends DataResult<Company>{
canCreate: boolean;
}

MODIFY
src/types/GlobalType.ts
src/types/GlobalType.ts
@@ -4,6 +4,7 @@ import ChargingStation from './ChargingStation';
import Company from './Company';
import JsonCentralSystemServer from '../server/ocpp/json/JsonCentralSystemServer';
import MongoDBStorage from '../storage/mongodb/MongoDBStorage';
+import Pricing from './Pricing';
import Site from './Site';
import SiteArea from './SiteArea';
import SoapCentralSystemServer from '../server/ocpp/soap/SoapCentralSystemServer';
@@ -44,7 +45,7 @@ export enum ImportStatus {
ERROR = 'E',
}
-export type EntityDataType = Car|User|Company|Site|SiteArea|Tag|CarCatalog|ChargingStation;
+export type EntityDataType = Car|User|Company|Site|SiteArea|Tag|CarCatalog|ChargingStation|Pricing;
interface TSGlobal extends Global {
database: MongoDBStorage;

MODIFY
src/types/Transaction.ts
src/types/Transaction.ts
@@ -8,7 +8,7 @@ import { OCPICdr } from './ocpi/OCPICdr';
import { OCPISession } from './ocpi/OCPISession';
import { OICPChargeDetailRecord } from './oicp/OICPChargeDetailRecord';
import { OICPSession } from './oicp/OICPSession';
-import { PricingModel } from './Pricing';
+import Pricing from './Pricing';
import Site from './Site';
import SiteArea from './SiteArea';
import Tag from './Tag';
@@ -82,7 +82,7 @@ export default interface Transaction extends AbstractCurrentConsumption {
roundedPrice?: number;
priceUnit?: string;
pricingSource?: string;
- pricingModel?: PricingModel,
+ pricing?: Pricing,
stateOfCharge: number;
timezone: string;
currentTimestamp?: Date;

ADD
src/types/requests/HttpPricingRequest.ts
+import HttpByIDRequest from './HttpByIDRequest';
+import HttpDatabaseRequest from './HttpDatabaseRequest';
+
+export interface HttpPricingRequest extends HttpByIDRequest {
+ ID: string;
+}
+
+export interface HttpPricingsRequest extends HttpDatabaseRequest {
+ Search?: string;
+}

Pricing - Pricing Service, etc