pricing - Polish + Slit engine in two distinct classes
ADD
src/integration/pricing/ConsumptionPricer.ts
+import PricingDefinition, { DimensionType, PricedConsumptionData, PricedDimensionData, PricingDimension, PricingRestriction, ResolvedPricingModel } from '../../types/Pricing';
+
+import Consumption from '../../types/Consumption';
+import Tenant from '../../types/Tenant';
+import Utils from '../../utils/Utils';
+import moment from 'moment';
+
+export default class ConsumptionPricer {
+
+ tenant: Tenant;
+ pricingModel: ResolvedPricingModel;
+ consumptionData: Consumption;
+ actualPricingDefinitions: PricingDefinition[];
+
+ constructor(tenant: Tenant, pricingModel: ResolvedPricingModel, consumptionData: Consumption) {
+ this.tenant = tenant;
+ this.pricingModel = pricingModel;
+ this.consumptionData = consumptionData;
+ const actualPricingDefinitions = this.pricingModel.pricingDefinitions.filter((pricingDefinition) =>
+ this.checkPricingDefinitionRestrictions(pricingDefinition)
+ );
+ // It does not make sense to apply several tariffs to a single consumption
+ this.actualPricingDefinitions = [ actualPricingDefinitions?.[0] ];
+ }
+
+ public priceConsumption(): PricedConsumptionData {
+ // Price the consumption data for each dimension
+ const flatFee = this.priceFlatFeeConsumption();
+ const energy: PricedDimensionData = this.priceEnergyConsumption();
+ const chargingTime: PricedDimensionData = this.priceChargingTimeConsumption();
+ const parkingTime: PricedDimensionData = this.priceParkingTimeConsumption();
+ // Return all dimensions
+ const pricingConsumptionData: PricedConsumptionData = {
+ flatFee,
+ energy,
+ chargingTime,
+ parkingTime
+ };
+ return pricingConsumptionData;
+ }
+
+ private checkPricingDefinitionRestrictions(pricingDefinition: PricingDefinition) : PricingDefinition {
+ if (pricingDefinition.restrictions) {
+ if (!this.checkMinEnergy(pricingDefinition.restrictions)
+ || !this.checkMaxEnergy(pricingDefinition.restrictions)
+ || !this.checkMinDuration(pricingDefinition.restrictions)
+ || !this.checkMaxDuration(pricingDefinition.restrictions)) {
+ // -----------------------------------------------------------------------------------------
+ // 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
+ // 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)
+ // daysOfWeek?: DayOfWeek[], // Which day(s) of the week this tariff is valid
+ return null;
+ }
+ }
+ // a definition matching the restrictions has been found
+ return pricingDefinition;
+ }
+
+ private checkMinEnergy(restrictions: PricingRestriction): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.minEnergyKWh)) {
+ if (Utils.createDecimal(this.consumptionData.cumulatedConsumptionWh).div(1000).lessThan(restrictions.minEnergyKWh)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private checkMaxEnergy(restrictions: PricingRestriction): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.maxEnergyKWh)) {
+ if (Utils.createDecimal(this.consumptionData.cumulatedConsumptionWh).div(1000).greaterThanOrEqualTo(restrictions.maxEnergyKWh)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private checkMinDuration(restrictions: PricingRestriction): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.minDurationSecs)) {
+ if (Utils.createDecimal(this.consumptionData.totalDurationSecs).lessThan(restrictions.minDurationSecs)) {
+ return false;
+ }
+ return true;
+ }
+ return true;
+ }
+
+ private checkMaxDuration(restrictions: PricingRestriction): boolean {
+ if (!Utils.isNullOrUndefined(restrictions.maxDurationSecs)) {
+ if (Utils.createDecimal(this.consumptionData.totalDurationSecs).greaterThanOrEqualTo(restrictions.maxDurationSecs)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private priceFlatFeeConsumption(): PricedDimensionData {
+ // Flat fee must not be priced only once
+ if (!this.pricingModel.currentContext.flatFeeAlreadyPriced) {
+ const activePricingDefinition = this.getActiveDefinition4Dimension(this.actualPricingDefinitions, DimensionType.FLAT_FEE);
+ if (activePricingDefinition) {
+ const dimensionToPrice = activePricingDefinition.dimensions.flatFee;
+ const pricedData = this.priceFlatFeeDimension(dimensionToPrice);
+ if (pricedData) {
+ pricedData.sourceName = activePricingDefinition.name;
+ this.pricingModel.currentContext.flatFeeAlreadyPriced = true;
+ }
+ return pricedData;
+ }
+ }
+ }
+
+ private priceEnergyConsumption(): PricedDimensionData {
+ const activePricingDefinition = this.getActiveDefinition4Dimension(this.actualPricingDefinitions, DimensionType.ENERGY);
+ if (activePricingDefinition) {
+ const dimensionToPrice = activePricingDefinition.dimensions.energy;
+ const pricedData = this.priceEnergyDimension(dimensionToPrice, this.consumptionData?.consumptionWh || 0);
+ if (pricedData) {
+ pricedData.sourceName = activePricingDefinition.name;
+ }
+ return pricedData;
+ }
+ }
+
+ private priceChargingTimeConsumption(): PricedDimensionData {
+ const activePricingDefinition = this.getActiveDefinition4Dimension(this.actualPricingDefinitions, DimensionType.CHARGING_TIME);
+ if (activePricingDefinition) {
+ const dimensionToPrice = activePricingDefinition.dimensions.chargingTime;
+ const consumptionWh = this.consumptionData?.consumptionWh || 0;
+ // Price the charging time only when charging!
+ if (consumptionWh > 0) {
+ const lastStepDate = this.pricingModel.currentContext.lastChargingTimeStepDate || this.pricingModel.currentContext.sessionStartDate;
+ const pricedData = this.priceTimeDimension(dimensionToPrice, lastStepDate);
+ if (pricedData) {
+ this.pricingModel.currentContext.lastChargingTimeStepDate = this.consumptionData.endedAt;
+ pricedData.sourceName = activePricingDefinition.name;
+ }
+ return pricedData;
+ }
+ // IMPORTANT - keep track of the consumption where nothing was priced
+ this.pricingModel.currentContext.lastChargingTimeStepDate = this.consumptionData.endedAt;
+ }
+ }
+
+ private priceParkingTimeConsumption(): PricedDimensionData {
+ const activePricingDefinition = this.getActiveDefinition4Dimension(this.actualPricingDefinitions, DimensionType.PARKING_TIME);
+ if (activePricingDefinition) {
+ const dimensionToPrice = activePricingDefinition.dimensions.parkingTime;
+ const cumulatedConsumptionDataWh = this.consumptionData?.cumulatedConsumptionWh || 0;
+ const consumptionWh = this.consumptionData?.consumptionWh || 0;
+ // Price the parking time only after having charged - NOT during the warmup!
+ if (cumulatedConsumptionDataWh > 0 && consumptionWh <= 0) {
+ const lastStepDate = this.pricingModel.currentContext.lastParkingTimeStepDate || this.pricingModel.currentContext.sessionStartDate;
+ const pricedData = this.priceTimeDimension(dimensionToPrice, lastStepDate);
+ if (pricedData) {
+ this.pricingModel.currentContext.lastParkingTimeStepDate = this.consumptionData.endedAt;
+ pricedData.sourceName = activePricingDefinition.name;
+ }
+ return pricedData;
+ }
+ }
+ // IMPORTANT - keep track of the consumption where nothing was priced
+ this.pricingModel.currentContext.lastParkingTimeStepDate = this.consumptionData.endedAt;
+ }
+
+ private getActiveDefinition4Dimension(actualPricingDefinitions: PricingDefinition[], dimensionType: string): PricingDefinition {
+ // Search for the first pricing definition matching the current dimension type
+ const activePricingDefinitions = actualPricingDefinitions.filter((pricingDefinition) =>
+ // We search for a pricing definition where the current dimension exists
+ this.checkPricingDimensionRestrictions(pricingDefinition, dimensionType)
+ );
+ // Iterate throw the list of pricing definitions where the current dimension makes sense
+ for (const activePricingDefinition of activePricingDefinitions) {
+ const dimensionToPrice = activePricingDefinition.dimensions[dimensionType];
+ if (dimensionToPrice) {
+ return activePricingDefinition;
+ }
+ }
+ return null;
+ }
+
+ private checkPricingDimensionRestrictions(pricingDefinition: PricingDefinition, dimensionType: string) : PricingDefinition {
+ const pricingDimension: PricingDimension = pricingDefinition.dimensions[dimensionType];
+ if (pricingDimension?.active) {
+ return pricingDefinition;
+ }
+ return null;
+ }
+
+ private extractFinalPricedConsumptionData(pricingDefinition: PricingDefinition): PricedConsumptionData {
+ const flatFee = pricingDefinition.dimensions.flatFee?.pricedData;
+ const energy = pricingDefinition.dimensions.energy?.pricedData;
+ const chargingTime = pricingDefinition.dimensions.chargingTime?.pricedData;
+ const parkingTime = pricingDefinition.dimensions.parkingTime?.pricedData;
+ if (flatFee || energy || chargingTime || parkingTime) {
+ return {
+ flatFee,
+ energy,
+ chargingTime,
+ parkingTime
+ };
+ }
+ // Nothing to bill for the current pricing definition
+ return null;
+ }
+
+ private extractPricedDataFromDimension(pricedData: PricedDimensionData[], pricingDefinition: PricingDefinition, dimensionType: string): void {
+ const pricingDimensionData = pricingDefinition.dimensions[dimensionType]?.pricedData;
+ if (pricingDimensionData) {
+ pricedData.push(pricingDimensionData);
+ }
+ }
+
+ private priceFlatFeeDimension(pricingDimension: PricingDimension): PricedDimensionData {
+ const unitPrice = pricingDimension.price || 0;
+ if (pricingDimension.pricedData) {
+ // This should not happen for the flatFee dimension - Flat Fee is billed only once per session
+ // throw new Error('Unexpected situation - priceFlatFeeDimension should be called only once per session');
+ return {
+ unitPrice: 0,
+ amount: 0,
+ roundedAmount: 0,
+ quantity: 0
+ };
+ }
+ // First call for this dimension
+ pricingDimension.pricedData = {
+ unitPrice: unitPrice,
+ amount: unitPrice,
+ roundedAmount: Utils.truncTo(unitPrice, 2),
+ quantity: 1
+ };
+ return pricingDimension.pricedData;
+ }
+
+ private priceEnergyDimension(pricingDimension: PricingDimension, consumptionWh: number): PricedDimensionData {
+ const unitPrice = pricingDimension.price || 0;
+ const amount = Utils.createDecimal(unitPrice).times(consumptionWh).div(1000).toNumber();
+ const consumptionkWh = Utils.createDecimal(consumptionWh).div(1000).toNumber();
+ // Price the consumption
+ const pricedData: PricedDimensionData = {
+ unitPrice: unitPrice,
+ amount,
+ roundedAmount: Utils.truncTo(amount, 2),
+ quantity: consumptionkWh
+ };
+ // Add the consumption to the previous data (if any) - for the billing
+ this.addPricedData(pricingDimension, pricedData);
+ // Return the current consumption!
+ return pricedData;
+ }
+
+ private priceTimeDimension(pricingDimension: PricingDimension, lastStepDate: Date): PricedDimensionData {
+ // Is there a step size
+ if (pricingDimension.stepSize) {
+ // Price the charging time only when charging!
+ const timeSpent = moment(this.consumptionData.endedAt).diff(moment(lastStepDate), 'seconds');
+ const nbSteps = Utils.createDecimal(timeSpent).divToInt(pricingDimension.stepSize).toNumber();
+ if (nbSteps > 0) {
+ return this.priceTimeSteps(pricingDimension, nbSteps);
+ }
+ } else {
+ const seconds = moment(this.consumptionData.endedAt).diff(moment(this.consumptionData.startedAt), 'seconds');
+ if (seconds > 0) {
+ return this.priceTimeSpent(pricingDimension, seconds);
+ }
+ }
+ }
+
+ private priceTimeSteps(pricingDimension: PricingDimension, steps: number): PricedDimensionData {
+ const unitPrice = pricingDimension.price || 0;
+ const amount = Utils.createDecimal(unitPrice).times(steps).times(pricingDimension.stepSize).div(3600).toNumber();
+ // Price the consumption
+ const pricedData: PricedDimensionData = {
+ unitPrice: unitPrice,
+ amount,
+ roundedAmount: Utils.truncTo(amount, 2),
+ quantity: steps
+ };
+ // Add the consumption to the previous data (if any) - for the billing
+ this.addPricedData(pricingDimension, pricedData);
+ // Return the current consumption!
+ return pricedData;
+ }
+
+ private priceTimeSpent(pricingDimension: PricingDimension, seconds: number): PricedDimensionData {
+ const unitPrice = pricingDimension.price || 0;
+ const amount = Utils.createDecimal(unitPrice).times(seconds).div(3600).toNumber();
+ const hours = Utils.createDecimal(seconds).div(3600).toNumber();
+ // Price the consumption
+ const pricedData: PricedDimensionData = {
+ unitPrice: unitPrice,
+ amount,
+ roundedAmount: Utils.truncTo(amount, 2),
+ quantity: hours
+ };
+ // Add the consumption to the previous data (if any) - for the billing
+ this.addPricedData(pricingDimension, pricedData);
+ // Return the current consumption!
+ return pricedData;
+ }
+
+ private addPricedData(pricingDimension: PricingDimension, pricedData: PricedDimensionData): void {
+ // Add the consumption to the previous data (if any) - for the billing
+ const previousData = pricingDimension.pricedData;
+ if (previousData) {
+ previousData.amount += pricedData.amount;
+ previousData.quantity += pricedData.quantity;
+ previousData.roundedAmount = Utils.truncTo(previousData.amount, 2);
+ } else {
+ pricingDimension.pricedData = pricedData;
+ }
+ }
+}

MODIFY
src/integration/pricing/PricingEngine.ts
src/integration/pricing/PricingEngine.ts
-import PricingDefinition, { CurrentContext, DimensionType, PricedConsumptionData, PricedDimensionData, PricingDimension, PricingRestriction, PricingStaticRestriction, ResolvedPricingModel } from '../../types/Pricing';
+import PricingDefinition, { PricedConsumptionData, PricingStaticRestriction, ResolvedPricingModel } from '../../types/Pricing';
import ChargingStation from '../../types/ChargingStation';
import Constants from '../../utils/Constants';
import Consumption from '../../types/Consumption';
+import ConsumptionPricer from './ConsumptionPricer';
import PricingStorage from '../../storage/mongodb/PricingStorage';
import Tenant from '../../types/Tenant';
import Transaction from '../../types/Transaction';
import Utils from '../../utils/Utils';
import moment from 'moment';
-// --------------------------------------------------------------------------------------------------
-// TODO - POC - PricingEngine is hidden behind a feature toggle
-// --------------------------------------------------------------------------------------------------
export default class PricingEngine {
public static async resolvePricingContext(tenant: Tenant, transaction: Transaction, chargingStation: ChargingStation): Promise<ResolvedPricingModel> {
@@ -34,27 +32,8 @@ export default class PricingEngine {
}
public static priceConsumption(tenant: Tenant, pricingModel: ResolvedPricingModel, consumptionData: Consumption): PricedConsumptionData {
- // Check the restrictions to find the pricing definition matching the current context
- let actualPricingDefinitions = pricingModel.pricingDefinitions.filter((pricingDefinition) =>
- PricingEngine.checkPricingDefinitionRestrictions(pricingDefinition, consumptionData)
- );
- if (actualPricingDefinitions.length > 1) {
- // It does not make sense to apply several tariffs to a single consumption
- actualPricingDefinitions = [ actualPricingDefinitions?.[0] ];
- }
- // Build the consumption data for each dimension
- const flatFee = PricingEngine.priceFlatFeeConsumption(actualPricingDefinitions, consumptionData, pricingModel.currentContext);
- const energy: PricedDimensionData = PricingEngine.priceEnergyConsumption(actualPricingDefinitions, consumptionData);
- const chargingTime: PricedDimensionData = PricingEngine.priceChargingTimeConsumption(actualPricingDefinitions, consumptionData, pricingModel.currentContext);
- const parkingTime: PricedDimensionData = PricingEngine.priceParkingTimeConsumption(actualPricingDefinitions, consumptionData, pricingModel.currentContext);
- // Return all dimensions
- const pricingConsumptionData: PricedConsumptionData = {
- flatFee,
- energy,
- chargingTime,
- parkingTime
- };
- return pricingConsumptionData;
+ const consumptionPricer = new ConsumptionPricer(tenant, pricingModel, consumptionData);
+ return consumptionPricer.priceConsumption();
}
public static extractFinalPricingData(pricingModel: ResolvedPricingModel): PricedConsumptionData[] {
@@ -102,27 +81,6 @@ export default class PricingEngine {
return pricingDefinition;
}
- private static checkPricingDefinitionRestrictions(pricingDefinition: PricingDefinition, consumptionData: Consumption) : PricingDefinition {
- if (pricingDefinition.restrictions) {
- 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 "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
- // 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)
- // daysOfWeek?: DayOfWeek[], // Which day(s) of the week this tariff is valid
- return null;
- }
- }
- // a definition matching the restrictions has been found
- return pricingDefinition;
- }
-
private static checkDateValidity(staticRestrictions: PricingStaticRestriction, transaction: Transaction): boolean {
if (!Utils.isNullOrUndefined(staticRestrictions.validFrom)) {
if (moment(transaction.timestamp).isBefore(staticRestrictions.validFrom)) {
@@ -157,137 +115,6 @@ export default class PricingEngine {
return true;
}
- private 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;
- }
-
- private 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;
- }
-
- private static checkMinDuration(restrictions: PricingRestriction, consumptionData: Consumption): boolean {
- if (!Utils.isNullOrUndefined(restrictions.minDurationSecs)) {
- if (Utils.createDecimal(consumptionData.totalDurationSecs).lessThan(restrictions.minDurationSecs)) {
- return false;
- }
- return true;
- }
- return true;
- }
-
- private static checkMaxDuration(restrictions: PricingRestriction, consumptionData: Consumption): boolean {
- if (!Utils.isNullOrUndefined(restrictions.maxDurationSecs)) {
- if (Utils.createDecimal(consumptionData.totalDurationSecs).greaterThanOrEqualTo(restrictions.maxDurationSecs)) {
- return false;
- }
- }
- return true;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- private static priceFlatFeeConsumption(pricingDefinitions: PricingDefinition[], consumptionData: Consumption, currentContext: CurrentContext): PricedDimensionData {
- // Flat fee must not be priced only once
- if (!currentContext.flatFeeAlreadyPriced) {
- const activePricingDefinition = PricingEngine.getActiveDefinition4Dimension(pricingDefinitions, DimensionType.FLAT_FEE);
- if (activePricingDefinition) {
- const dimensionToPrice = activePricingDefinition.dimensions.flatFee;
- const pricedData = PricingEngine.priceFlatFeeDimension(dimensionToPrice);
- if (pricedData) {
- pricedData.sourceName = activePricingDefinition.name;
- currentContext.flatFeeAlreadyPriced = true;
- }
- return pricedData;
- }
- }
- }
-
- private static priceEnergyConsumption(pricingDefinitions: PricingDefinition[], consumptionData: Consumption): PricedDimensionData {
- const activePricingDefinition = PricingEngine.getActiveDefinition4Dimension(pricingDefinitions, DimensionType.ENERGY);
- if (activePricingDefinition) {
- const dimensionToPrice = activePricingDefinition.dimensions.energy;
- const pricedData = PricingEngine.priceEnergyDimension(dimensionToPrice, consumptionData?.consumptionWh || 0);
- if (pricedData) {
- pricedData.sourceName = activePricingDefinition.name;
- }
- return pricedData;
- }
- }
-
- private static priceChargingTimeConsumption(pricingDefinitions: PricingDefinition[], consumptionData: Consumption, currentContext: CurrentContext): PricedDimensionData {
- const activePricingDefinition = PricingEngine.getActiveDefinition4Dimension(pricingDefinitions, DimensionType.CHARGING_TIME);
- if (activePricingDefinition) {
- const dimensionToPrice = activePricingDefinition.dimensions.chargingTime;
- const consumptionWh = consumptionData?.consumptionWh || 0;
- // Price the charging time only when charging!
- if (consumptionWh > 0) {
- const lastStepDate = currentContext.lastChargingTimeStepDate || currentContext.sessionStartDate;
- const pricedData = PricingEngine.priceTimeDimension(dimensionToPrice, consumptionData, lastStepDate);
- if (pricedData) {
- currentContext.lastChargingTimeStepDate = consumptionData.endedAt;
- pricedData.sourceName = activePricingDefinition.name;
- }
- return pricedData;
- }
- // IMPORTANT - keep track of the consumption where nothing was priced
- currentContext.lastChargingTimeStepDate = consumptionData.endedAt;
- }
- }
-
- private static priceParkingTimeConsumption(pricingDefinitions: PricingDefinition[], consumptionData: Consumption, currentContext: CurrentContext): PricedDimensionData {
- const activePricingDefinition = PricingEngine.getActiveDefinition4Dimension(pricingDefinitions, DimensionType.PARKING_TIME);
- if (activePricingDefinition) {
- const dimensionToPrice = activePricingDefinition.dimensions.parkingTime;
- const cumulatedConsumptionDataWh = consumptionData?.cumulatedConsumptionWh || 0;
- const consumptionWh = consumptionData?.consumptionWh || 0;
- // Price the parking time only after having charged - NOT during the warmup!
- if (cumulatedConsumptionDataWh > 0 && consumptionWh <= 0) {
- const lastStepDate = currentContext.lastParkingTimeStepDate || currentContext.sessionStartDate;
- const pricedData = PricingEngine.priceTimeDimension(dimensionToPrice, consumptionData, lastStepDate);
- if (pricedData) {
- currentContext.lastParkingTimeStepDate = consumptionData.endedAt;
- pricedData.sourceName = activePricingDefinition.name;
- }
- return pricedData;
- }
- }
- // IMPORTANT - keep track of the consumption where nothing was priced
- currentContext.lastParkingTimeStepDate = consumptionData.endedAt;
- }
-
- private static getActiveDefinition4Dimension(actualPricingDefinitions: PricingDefinition[], dimensionType: string): PricingDefinition {
- // Search for the first pricing definition matching the current dimension type
- const activePricingDefinitions = actualPricingDefinitions.filter((pricingDefinition) =>
- // We search for a pricing definition where the current dimension exists
- PricingEngine.checkPricingDimensionRestrictions(pricingDefinition, dimensionType)
- );
- // Iterate throw the list of pricing definitions where the current dimension makes sense
- for (const activePricingDefinition of activePricingDefinitions) {
- const dimensionToPrice = activePricingDefinition.dimensions[dimensionType];
- if (dimensionToPrice) {
- return activePricingDefinition;
- }
- }
- return null;
- }
-
- private static checkPricingDimensionRestrictions(pricingDefinition: PricingDefinition, dimensionType: string) : PricingDefinition {
- const pricingDimension: PricingDimension = pricingDefinition.dimensions[dimensionType];
- if (pricingDimension?.active) {
- return pricingDefinition;
- }
- return null;
- }
-
private static extractFinalPricedConsumptionData(pricingDefinition: PricingDefinition): PricedConsumptionData {
const flatFee = pricingDefinition.dimensions.flatFee?.pricedData;
const energy = pricingDefinition.dimensions.energy?.pricedData;
@@ -304,132 +131,4 @@ export default class PricingEngine {
// Nothing to bill for the current pricing definition
return null;
}
-
- private static extractPricedDataFromDimension(pricedData: PricedDimensionData[], pricingDefinition: PricingDefinition, dimensionType: string): void {
- const pricingDimensionData = pricingDefinition.dimensions[dimensionType]?.pricedData;
- if (pricingDimensionData) {
- pricedData.push(pricingDimensionData);
- }
- }
-
- private static priceFlatFeeDimension(pricingDimension: PricingDimension): PricedDimensionData {
- const unitPrice = pricingDimension.price || 0;
- if (pricingDimension.pricedData) {
- // This should not happen for the flatFee dimension - Flat Fee is billed only once per session
- // throw new Error('Unexpected situation - priceFlatFeeDimension should be called only once per session');
- return {
- unitPrice: 0,
- amount: 0,
- roundedAmount: 0,
- quantity: 0
- };
- }
- // First call for this dimension
- pricingDimension.pricedData = {
- unitPrice: unitPrice,
- amount: unitPrice,
- roundedAmount: Utils.truncTo(unitPrice, 2),
- quantity: 1
- };
- return pricingDimension.pricedData;
- }
-
- private static priceEnergyDimension(pricingDimension: PricingDimension, consumptionWh: number): PricedDimensionData {
- const unitPrice = pricingDimension.price || 0;
- const amount = Utils.createDecimal(unitPrice).times(consumptionWh).div(1000).toNumber();
- const consumptionkWh = Utils.createDecimal(consumptionWh).div(1000).toNumber();
- // Price the consumption
- const pricedData: PricedDimensionData = {
- unitPrice: unitPrice,
- amount,
- roundedAmount: Utils.truncTo(amount, 2),
- quantity: consumptionkWh
- };
- // Add the consumption to the previous data (if any) - for the billing
- PricingEngine.addPricedData(pricingDimension, pricedData);
- // Return the current consumption!
- return pricedData;
- }
-
- private static priceTimeDimension(pricingDimension: PricingDimension, consumptionData: Consumption, lastStepDate: Date): PricedDimensionData {
- // Is there a step size
- if (pricingDimension.stepSize) {
- // Price the charging time only when charging!
- const timeSpent = moment(consumptionData.endedAt).diff(moment(lastStepDate), 'seconds');
- const nbSteps = Utils.createDecimal(timeSpent).divToInt(pricingDimension.stepSize).toNumber();
- if (nbSteps > 0) {
- return this.priceTimeSteps(pricingDimension, nbSteps);
- }
- } else {
- const seconds = moment(consumptionData.endedAt).diff(moment(consumptionData.startedAt), 'seconds');
- if (seconds > 0) {
- return this.priceTimeSpent(pricingDimension, seconds);
- }
- }
}
-
- // private static __priceTimeDimension(pricingDimension: PricingDimension, consumptionData: Consumption, currentContext: CurrentContext): PricedDimensionData {
- // // Is there a step size
- // if (pricingDimension.stepSize) {
- // const lastStepDate = currentContext.lastConsumptionDate || currentContext.sessionStartDate;
- // // Price the charging time only when charging!
- // const timeSpent = moment(consumptionData.endedAt).diff(moment(lastStepDate), 'seconds');
- // const nbSteps = Utils.createDecimal(timeSpent).divToInt(pricingDimension.stepSize).toNumber();
- // if (nbSteps > 0) {
- // currentContext.lastConsumptionDate = consumptionData.endedAt;
- // return this.priceTimeSteps(pricingDimension, nbSteps);
- // }
- // } else {
- // const seconds = moment(consumptionData.endedAt).diff(moment(consumptionData.startedAt), 'seconds');
- // if (seconds > 0) {
- // return this.priceTimeSpent(pricingDimension, seconds);
- // }
- // }
- // }
-
- private static priceTimeSteps(pricingDimension: PricingDimension, steps: number): PricedDimensionData {
- const unitPrice = pricingDimension.price || 0;
- const amount = Utils.createDecimal(unitPrice).times(steps).times(pricingDimension.stepSize).div(3600).toNumber();
- // Price the consumption
- const pricedData: PricedDimensionData = {
- unitPrice: unitPrice,
- amount,
- roundedAmount: Utils.truncTo(amount, 2),
- quantity: steps
- };
- // Add the consumption to the previous data (if any) - for the billing
- PricingEngine.addPricedData(pricingDimension, pricedData);
- // Return the current consumption!
- return pricedData;
- }
-
- private static priceTimeSpent(pricingDimension: PricingDimension, seconds: number): PricedDimensionData {
- const unitPrice = pricingDimension.price || 0;
- const amount = Utils.createDecimal(unitPrice).times(seconds).div(3600).toNumber();
- const hours = Utils.createDecimal(seconds).div(3600).toNumber();
- // Price the consumption
- const pricedData: PricedDimensionData = {
- unitPrice: unitPrice,
- amount,
- roundedAmount: Utils.truncTo(amount, 2),
- quantity: hours
- };
- // Add the consumption to the previous data (if any) - for the billing
- PricingEngine.addPricedData(pricingDimension, pricedData);
- // Return the current consumption!
- return pricedData;
- }
-
- private static addPricedData(pricingDimension: PricingDimension, pricedData: PricedDimensionData): void {
- // Add the consumption to the previous data (if any) - for the billing
- const previousData = pricingDimension.pricedData;
- if (previousData) {
- previousData.amount += pricedData.amount;
- previousData.quantity += pricedData.quantity;
- previousData.roundedAmount = Utils.truncTo(previousData.amount, 2);
- } else {
- pricingDimension.pricedData = pricedData;
- }
- }
-}
-

pricing - Polish + Slit engine in two distinct classes