pricing - static restrictions
MODIFY
src/integration/pricing/PricingEngine.ts
src/integration/pricing/PricingEngine.ts
/* eslint-disable @typescript-eslint/member-ordering */
-import PricingModel, { DimensionType, PricedConsumptionData, PricedDimensionData, PricingDefinition, PricingDimension, PricingRestriction, ResolvedPricingModel } from '../../types/Pricing';
+import PricingModel, { DimensionType, PricedConsumptionData, PricedDimensionData, PricingDefinition, PricingDimension, PricingRestriction, PricingStaticRestriction, ResolvedPricingModel } from '../../types/Pricing';
+import ChargingStation from '../../types/ChargingStation';
import Consumption from '../../types/Consumption';
import PricingStorage from '../../storage/mongodb/PricingStorage';
import Tenant from '../../types/Tenant';
@@ -12,7 +13,7 @@ import Utils from '../../utils/Utils';
// --------------------------------------------------------------------------------------------------
export default class PricingEngine {
- static async resolvePricingContext(tenant: Tenant, transaction: Transaction): Promise<ResolvedPricingModel> {
+ static async resolvePricingContext(tenant: Tenant, transaction: Transaction, chargingStation: ChargingStation): Promise<ResolvedPricingModel> {
// -----------------------------------------------------------------------------------------
// TODO - We need to find the pricing model to apply by resolving the hierarchy of contexts
// that may override (or extend) the pricing definitions.
@@ -29,10 +30,10 @@ export default class PricingEngine {
// Merge the pricing definitions from the different contexts
const pricingDefinitions: PricingDefinition[] = [];
// pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction.userID));
- pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction, transaction.chargeBoxID.toString()));
- pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction, transaction.siteAreaID.toString()));
- pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction, transaction.siteID.toString()));
- pricingDefinitions.push(...await PricingEngine.getPricingDefinitions4Entity(tenant, transaction, transaction.companyID.toString()));
+ 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()));
// TODO - No pricing definition? => Throw an exception ? or create dynamically a simple one based on the simple pricing settings?
const resolvedPricingModel: ResolvedPricingModel = {
flatFeeAlreadyPriced: false,
@@ -41,11 +42,11 @@ export default class PricingEngine {
return Promise.resolve(resolvedPricingModel);
}
- static async getPricingDefinitions4Entity(tenant: Tenant, transaction: Transaction, entityID: string): Promise<PricingDefinition[]> {
+ static async getPricingDefinitions4Entity(tenant: Tenant, transaction: Transaction, chargingStation: ChargingStation, entityID: string): Promise<PricingDefinition[]> {
const pricingModel: PricingModel = await PricingEngine.getPricingModel4Entity(tenant, entityID);
const pricingDefinitions = pricingModel?.pricingDefinitions || [];
const actualPricingDefinitions = pricingDefinitions.filter((pricingDefinition) =>
- PricingEngine.checkStaticRestrictions(pricingDefinition, transaction)
+ PricingEngine.checkStaticRestrictions(pricingDefinition, transaction, chargingStation)
);
return actualPricingDefinitions || [];
}
@@ -64,15 +65,16 @@ export default class PricingEngine {
return null;
}
- static checkStaticRestrictions(pricingDefinition: PricingDefinition, transaction: Transaction) : PricingDefinition {
+ static checkStaticRestrictions(pricingDefinition: PricingDefinition, transaction: Transaction, chargingStation: ChargingStation) : PricingDefinition {
// -----------------------------------------------------------------------
// TODO - check here the static restrictions
// i.e.: - restrictions that are not depending on the actual consumption
// e.g.: - validity dates, minPowerkW/maxPowerkW
// -----------------------------------------------------------------------
- if (pricingDefinition.restrictions) {
- if (!PricingEngine.checkRestrictionMinPower(pricingDefinition.restrictions, transaction)
- || !PricingEngine.checkRestrictionMaxPower(pricingDefinition.restrictions, transaction)
+ if (pricingDefinition.staticRestrictions) {
+ if (!PricingEngine.checkConnectorType(pricingDefinition.staticRestrictions, transaction, chargingStation)
+ || !PricingEngine.checkMinPower(pricingDefinition.staticRestrictions, transaction, chargingStation)
+ || !PricingEngine.checkMaxPower(pricingDefinition.staticRestrictions, transaction, chargingStation)
) {
return null;
}
@@ -83,10 +85,12 @@ export default class PricingEngine {
static checkPricingDefinitionRestrictions(pricingDefinition: PricingDefinition, consumptionData: Consumption) : PricingDefinition {
if (pricingDefinition.restrictions) {
- if (!PricingEngine.checkRestrictionMinDuration(pricingDefinition.restrictions, consumptionData)
- || !PricingEngine.checkRestrictionMaxDuration(pricingDefinition.restrictions, consumptionData)) {
+ if (!PricingEngine.checkMinEnergy(pricingDefinition.restrictions, consumptionData)
+ || !PricingEngine.checkMaxEnergy(pricingDefinition.restrictions, consumptionData)
+ || !PricingEngine.checkMinDuration(pricingDefinition.restrictions, consumptionData)
+ || !PricingEngine.checkMaxDuration(pricingDefinition.restrictions, consumptionData)) {
// -----------------------------------------------------------------------------------------
- // TODO - to be clarified - why don't we put "date validity" at the pricing model level????
+ // TODO - to be clarified - why don't we put "validity date" at the pricing model level?
// -----------------------------------------------------------------------------------------
// startTime?: string, // Start time of day, for example 13:30, valid from this time of the day. Must be in 24h format with leading zeros. Hour/Minute se
// endTime?: string, // End time of day, for example 19:45, valid until this time of the day. Same syntax as start_time
@@ -100,27 +104,55 @@ export default class PricingEngine {
return pricingDefinition;
}
- static checkRestrictionMinPower(restrictions: PricingRestriction, transaction: Transaction): boolean {
- // TODO - where to get the power limits
- // if (!Utils.isNullOrUndefined(restrictions.minPowerkW)) {
- // if (Utils.createDecimal(transaction.currentInstantWatts).dividedBy(1000).lessThan(restrictions.minPowerkW)) {
- // return false;
- // }
- // }
+ static checkConnectorType(restrictions: PricingStaticRestriction, transaction: Transaction, chargingStation: ChargingStation): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.connectorTypes)) {
+ const connectorType = Utils.getConnectorFromID(chargingStation, transaction.connectorId)?.type;
+ if (!restrictions.connectorTypes.includes(connectorType)) {
+ return false;
+ }
+ }
return true;
}
- static checkRestrictionMaxPower(restrictions: PricingRestriction, transaction: Transaction): boolean {
- // TODO - where to get the power limits
- // if (!Utils.isNullOrUndefined(restrictions.maxPowerkW)) {
- // if (Utils.createDecimal(transaction.currentInstantWatts).dividedBy(1000).greaterThanOrEqualTo(restrictions.maxPowerkW)) {
- // return false;
- // }
- // }
+ static checkMinPower(restrictions: PricingStaticRestriction, transaction: Transaction, chargingStation: ChargingStation): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.minOutputPowerkW)) {
+ const connectorPowerWatts = Utils.getConnectorFromID(chargingStation, transaction.connectorId)?.power;
+ if (Utils.createDecimal(connectorPowerWatts).dividedBy(1000).lessThan(restrictions.minOutputPowerkW)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static checkMaxPower(restrictions: PricingStaticRestriction, transaction: Transaction, chargingStation: ChargingStation): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.maxOutputPowerkW)) {
+ const connectorPowerWatts = Utils.getConnectorFromID(chargingStation, transaction.connectorId)?.power;
+ if (Utils.createDecimal(connectorPowerWatts).dividedBy(1000).greaterThanOrEqualTo(restrictions.maxOutputPowerkW)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static checkMinEnergy(restrictions: PricingRestriction, consumptionData: Consumption): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.minEnergyKWh)) {
+ if (Utils.createDecimal(consumptionData.cumulatedConsumptionWh).div(1000).lessThan(restrictions.minEnergyKWh)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static checkMaxEnergy(restrictions: PricingRestriction, consumptionData: Consumption): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.maxEnergyKWh)) {
+ if (Utils.createDecimal(consumptionData.cumulatedConsumptionWh).div(1000).greaterThanOrEqualTo(restrictions.maxEnergyKWh)) {
+ return false;
+ }
+ }
return true;
}
- static checkRestrictionMinDuration(restrictions: PricingRestriction, consumptionData: Consumption): boolean {
+ static checkMinDuration(restrictions: PricingRestriction, consumptionData: Consumption): boolean {
if (!Utils.isNullOrUndefined(restrictions.minDurationSecs)) {
if (Utils.createDecimal(consumptionData.totalDurationSecs).lessThan(restrictions.minDurationSecs)) {
return false;
@@ -129,7 +161,7 @@ export default class PricingEngine {
return true;
}
- static checkRestrictionMaxDuration(restrictions: PricingRestriction, consumptionData: Consumption): boolean {
+ static checkMaxDuration(restrictions: PricingRestriction, consumptionData: Consumption): boolean {
if (!Utils.isNullOrUndefined(restrictions.maxDurationSecs)) {
if (Utils.createDecimal(consumptionData.totalDurationSecs).greaterThanOrEqualTo(restrictions.maxDurationSecs)) {
return false;

MODIFY
src/integration/pricing/PricingIntegration.ts
src/integration/pricing/PricingIntegration.ts
+import ChargingStation from '../../types/ChargingStation';
import Consumption from '../../types/Consumption';
import { PricedConsumption } from '../../types/Pricing';
import { PricingSetting } from '../../types/Setting';
@@ -17,9 +18,9 @@ export default abstract class PricingIntegration<T extends PricingSetting> {
return this.setting;
}
- public abstract startSession(transaction: Transaction, consumptionData: Consumption): Promise<PricedConsumption>;
+ public abstract startSession(transaction: Transaction, consumptionData: Consumption, chargingStation?: ChargingStation): Promise<PricedConsumption>;
- public abstract updateSession(transaction: Transaction, consumptionData: Consumption): Promise<PricedConsumption>;
+ public abstract updateSession(transaction: Transaction, consumptionData: Consumption, chargingStation?: ChargingStation): Promise<PricedConsumption>;
- public abstract stopSession(transaction: Transaction, consumptionData: Consumption): Promise<PricedConsumption>;
+ public abstract stopSession(transaction: Transaction, consumptionData: Consumption, chargingStation?: ChargingStation): Promise<PricedConsumption>;
}

MODIFY
src/integration/pricing/simple-pricing/BuiltInPricingIntegration.ts
src/integration/pricing/simple-pricing/BuiltInPricingIntegration.ts
import FeatureToggles, { Feature } from '../../../utils/FeatureToggles';
import { PricedConsumption, PricingDefinition, PricingSource, ResolvedPricingModel } from '../../../types/Pricing';
+import ChargingStation from '../../../types/ChargingStation';
import Consumption from '../../../types/Consumption';
import PricingEngine from '../PricingEngine';
import PricingIntegration from '../PricingIntegration';
@@ -19,23 +20,23 @@ export default class BuiltInPricingIntegration extends PricingIntegration<Simple
super(tenant, settings);
}
- public async startSession(transaction: Transaction, consumptionData: Consumption): Promise<PricedConsumption> {
- return this.computePrice(transaction, consumptionData);
+ public async startSession(transaction: Transaction, consumptionData: Consumption, chargingStation: ChargingStation): Promise<PricedConsumption> {
+ return this.computePrice(transaction, consumptionData, chargingStation);
}
- public async updateSession(transaction: Transaction, consumptionData: Consumption): Promise<PricedConsumption> {
- return this.computePrice(transaction, consumptionData);
+ public async updateSession(transaction: Transaction, consumptionData: Consumption, chargingStation: ChargingStation): Promise<PricedConsumption> {
+ return this.computePrice(transaction, consumptionData, chargingStation);
}
- public async stopSession(transaction: Transaction, consumptionData: Consumption): Promise<PricedConsumption> {
- return this.computePrice(transaction, consumptionData);
+ public async stopSession(transaction: Transaction, consumptionData: Consumption, chargingStation: ChargingStation): Promise<PricedConsumption> {
+ return this.computePrice(transaction, consumptionData, chargingStation);
}
- private async computePrice(transaction: Transaction, consumptionData: Consumption): Promise<PricedConsumption> {
+ private async computePrice(transaction: Transaction, consumptionData: Consumption, chargingStation: ChargingStation): Promise<PricedConsumption> {
let pricingModel = transaction.pricingModel;
if (!pricingModel) {
// This should happen only on the first call (i.e.: on a start transaction)
- pricingModel = await this.resolvePricingContext(this.tenant, transaction);
+ pricingModel = await this.resolvePricingContext(this.tenant, transaction, chargingStation);
}
const pricingConsumptionData = PricingEngine.priceConsumption(this.tenant, pricingModel, consumptionData);
const { flatFee, energy, chargingTime, parkingTime } = pricingConsumptionData;
@@ -53,8 +54,8 @@ export default class BuiltInPricingIntegration extends PricingIntegration<Simple
return Promise.resolve(pricedConsumption);
}
- private async resolvePricingContext(tenant: Tenant, transaction: Transaction): Promise<ResolvedPricingModel> {
- const resolvedPricingModel: ResolvedPricingModel = await PricingEngine.resolvePricingContext(tenant, transaction);
+ private async resolvePricingContext(tenant: Tenant, transaction: Transaction, chargingStation: ChargingStation): Promise<ResolvedPricingModel> {
+ const resolvedPricingModel: ResolvedPricingModel = await PricingEngine.resolvePricingContext(tenant, transaction, chargingStation);
if (!resolvedPricingModel.pricingDefinitions?.length) {
resolvedPricingModel.pricingDefinitions = [ this.getDefaultPricingDefinition() ];
}

MODIFY
src/server/ocpp/utils/OCPPUtils.ts
src/server/ocpp/utils/OCPPUtils.ts
@@ -245,7 +245,7 @@ export default class OCPPUtils {
);
}
// Set
- pricedConsumption = await pricingImpl.startSession(transaction, consumption);
+ pricedConsumption = await pricingImpl.startSession(transaction, consumption, chargingStation);
if (pricedConsumption) {
// Set the initial pricing
transaction.price = pricedConsumption.amount;
@@ -262,7 +262,7 @@ export default class OCPPUtils {
// Meter Values
case TransactionAction.UPDATE:
// Set
- pricedConsumption = await pricingImpl.updateSession(transaction, consumption);
+ pricedConsumption = await pricingImpl.updateSession(transaction, consumption, chargingStation);
if (pricedConsumption) {
// Update consumption
consumption.amount = pricedConsumption.amount;
@@ -276,7 +276,7 @@ export default class OCPPUtils {
// Stop Transaction
case TransactionAction.STOP:
// Set
- pricedConsumption = await pricingImpl.stopSession(transaction, consumption);
+ pricedConsumption = await pricingImpl.stopSession(transaction, consumption, chargingStation);
if (pricedConsumption) {
// Update consumption
consumption.amount = pricedConsumption.amount;

MODIFY
src/types/Pricing.ts
src/types/Pricing.ts
import { AuthorizationActions } from './Authorization';
+import { ConnectorType } from './ChargingStation';
import CreatedUpdatedProps from './CreatedUpdatedProps';
export default interface PricingModel extends CreatedUpdatedProps, AuthorizationActions {
@@ -33,7 +34,8 @@ export interface ResolvedPricingModel {
export interface PricingDefinition {
name: string, // Short marketing name - e.g.: BLUE Tariff,
description: string, // A long description to explain it, e.g.: Time-based pricing for low charging stations
- restrictions?: PricingRestriction; // To be clarified! - These restrictions are so far common to all dimensions
+ staticRestrictions?: PricingStaticRestriction,
+ restrictions?: PricingRestriction,
dimensions: PricingDimensions
}
@@ -50,18 +52,23 @@ export interface PricingDimension {
pricedData?: PricedDimensionData // Information set dynamically while charging
}
+// Restriction that can be checked on session start
+export interface PricingStaticRestriction {
+ connectorTypes?: ConnectorType[], // Connector types allowed to use this tariff
+ minOutputPowerkW?: number, // Minimum power in kW, for example 0, valid from this charging speed
+ maxOutputPowerkW?: number, // Maximum power in kW, for example 20, valid up to this charging speed
+}
export interface PricingRestriction {
- startTime?: string, // Start time of day, for example 13:30, valid from this time of the day. Must be in 24h format with leading zeros. Hour/Minute se
- endTime?: string, // End time of day, for example 19:45, valid until this time of the day. Same syntax as start_time
- startDate?: string, // Start date, for example: 2015-12-24, valid from this day
- endDate?: string, // End date, for example: 2015-12-27, valid until this day (excluding this day)
+ // TODO - first Pricing implementation - some options are not yet supported
+ // startDate?: string, // Start date, for example: 2015-12-24, valid from this day
+ // endDate?: string, // End date, for example: 2015-12-27, valid until this day (excluding this day)
+ // startTime?: string, // Start time of day, for example 13:30, valid from this time of the day. Must be in 24h format with leading zeros. Hour/Minute se
+ // endTime?: string, // End time of day, for example 19:45, valid until this time of the day. Same syntax as start_time
+ // daysOfWeek?: DayOfWeek[], // Which day(s) of the week this tariff is valid
minEnergyKWh?: number, // Minimum used energy in kWh, for example 20, valid from this amount of energy is used
maxEnergyKWh?: number, // Maximum used energy in kWh, for example 50, valid until this amount of energy is used
- minOutputPowerkW?: number, // Minimum power in kW, for example 0, valid from this charging speed
- maxOutputPowerkW?: number, // Maximum power in kW, for example 20, valid up to this charging speed
minDurationSecs?: number, // Minimum duration in seconds, valid for a duration from x seconds
maxDurationSecs?: number, // Maximum duration in seconds, valid for a duration up to x seconds
- daysOfWeek?: DayOfWeek[], // Which day(s) of the week this tariff is valid
}
export enum DayOfWeek {

MODIFY
src/types/Transaction.ts
src/types/Transaction.ts
import { Car, CarCatalog } from './Car';
import { ChargePointStatus, OCPP15TransactionData, OCPPMeterValue } from './ocpp/OCPPServer';
import Consumption, { AbstractCurrentConsumption } from './Consumption';
-import PricingModel, { PricedConsumptionData, ResolvedPricingModel } from './Pricing';
import ChargingStation from '../types/ChargingStation';
import Company from './Company';
@@ -9,6 +8,7 @@ import { OCPICdr } from './ocpi/OCPICdr';
import { OCPISession } from './ocpi/OCPISession';
import { OICPChargeDetailRecord } from './oicp/OICPChargeDetailRecord';
import { OICPSession } from './oicp/OICPSession';
+import { ResolvedPricingModel } from './Pricing';
import Site from './Site';
import SiteArea from './SiteArea';
import Tag from './Tag';

MODIFY
test/api/BillingStripeTestData.ts
test/api/BillingStripeTestData.ts
import { BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingUser, BillingUserData } from '../../src/types/Billing';
import { BillingSettings, BillingSettingsType, SettingDB } from '../../src/types/Setting';
import FeatureToggles, { Feature } from '../../src/utils/FeatureToggles';
-import PricingModel, { PricedConsumptionData, PricingDefinition, PricingDimension, PricingRestriction } from '../../src/types/Pricing';
+import PricingModel, { PricedConsumptionData, PricingDefinition, PricingDimension, PricingRestriction, PricingStaticRestriction } from '../../src/types/Pricing';
import chai, { assert, expect } from 'chai';
import BillingStorage from '../../src/storage/mongodb/BillingStorage';
@@ -452,20 +452,20 @@ export default class StripeIntegrationTestData {
price: 0.75,
active: true
};
- const lowConsumptionRestrictions: PricingRestriction = {
+ const lowConsumptionRestrictions: PricingStaticRestriction = {
maxOutputPowerkW: 40000,
};
const tariff1: PricingDefinition = {
name: 'BLUE Tariff',
description: 'Tariff for low EVSE',
- restrictions: lowConsumptionRestrictions,
+ staticRestrictions: lowConsumptionRestrictions,
dimensions: {
chargingTime: parkingPrice,
// energy: price4TheEnergy, // do not bill the energy - bill the parking time instead
parkingTime: parkingPrice,
}
};
- const fastChargerRestrictions: PricingRestriction = {
+ const fastChargerRestrictions: PricingStaticRestriction = {
minOutputPowerkW: 40000
};
const price4TheEnergy: PricingDimension = {
@@ -475,7 +475,7 @@ export default class StripeIntegrationTestData {
const tariff2: PricingDefinition = {
name: 'GREEN Tariff',
description: 'Tariff for fast chargers',
- restrictions: fastChargerRestrictions,
+ staticRestrictions: fastChargerRestrictions,
dimensions: {
// chargingTime: parkingPrice, // parking time is free while charging
energy: price4TheEnergy,

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -342,7 +342,7 @@ class TestData {
const tariff: PricingDefinition = {
name: 'GREEN Tariff',
description: 'Tariff for slow chargers',
- restrictions: {
+ staticRestrictions: {
maxOutputPowerkW: 40,
},
dimensions: {
@@ -388,7 +388,7 @@ class TestData {
const tariff: PricingDefinition = {
name: 'RED Tariff',
description: 'Tariff for fast chargers',
- restrictions: {
+ staticRestrictions: {
minOutputPowerkW: 40,
},
dimensions: {

pricing - static restrictions