pricing - stepSize on Energy
MODIFY
src/integration/pricing/ConsumptionPricer.ts
src/integration/pricing/ConsumptionPricer.ts
@@ -117,13 +117,21 @@ export default class ConsumptionPricer {
const activePricingDefinition = this.getActiveDefinition4Dimension(this.actualPricingDefinitions, DimensionType.ENERGY);
if (activePricingDefinition) {
const dimensionToPrice = activePricingDefinition.dimensions.energy;
- const pricedData = this.priceEnergyDimension(dimensionToPrice, this.consumptionData?.consumptionWh || 0);
+ // Price the charging time only when charging!
+ const consumptionWh = this.consumptionData?.consumptionWh || 0;
+ if (consumptionWh > 0) {
+ const lastStepCumulatedConsumption = this.pricingModel.pricerContext.lastStepCumulatedConsumption || 0;
+ const pricedData = this.priceEnergyDimension(dimensionToPrice, lastStepCumulatedConsumption);
if (pricedData) {
+ this.pricingModel.pricerContext.lastStepCumulatedConsumption = this.consumptionData.cumulatedConsumptionWh;
pricedData.sourceName = activePricingDefinition.name;
}
return pricedData;
}
}
+ // IMPORTANT - keep track of the latest consumption even when nothing was priced
+ this.pricingModel.pricerContext.lastStepCumulatedConsumption = this.consumptionData.consumptionWh;
+ }
private priceChargingTimeConsumption(): PricedDimensionData {
const activePricingDefinition = this.getActiveDefinition4Dimension(this.actualPricingDefinitions, DimensionType.CHARGING_TIME);
@@ -132,16 +140,16 @@ export default class ConsumptionPricer {
const consumptionWh = this.consumptionData?.consumptionWh || 0;
// Price the charging time only when charging!
if (consumptionWh > 0) {
- const lastStepDate = this.pricingModel.pricerContext.lastChargingTimeStepDate || this.pricingModel.pricerContext.sessionStartDate;
+ const lastStepDate = this.pricingModel.pricerContext.lastStepChargingTimeDate || this.pricingModel.pricerContext.sessionStartDate;
const pricedData = this.priceTimeDimension(dimensionToPrice, lastStepDate);
if (pricedData) {
- this.pricingModel.pricerContext.lastChargingTimeStepDate = this.consumptionData.endedAt;
+ this.pricingModel.pricerContext.lastStepChargingTimeDate = this.consumptionData.endedAt;
pricedData.sourceName = activePricingDefinition.name;
}
return pricedData;
}
- // IMPORTANT - keep track of the consumption where nothing was priced
- this.pricingModel.pricerContext.lastChargingTimeStepDate = this.consumptionData.endedAt;
+ // IMPORTANT - keep track of the latest consumption even when nothing was priced
+ this.pricingModel.pricerContext.lastStepChargingTimeDate = this.consumptionData.endedAt;
}
}
@@ -153,17 +161,17 @@ export default class ConsumptionPricer {
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.pricerContext.lastParkingTimeStepDate || this.pricingModel.pricerContext.sessionStartDate;
+ const lastStepDate = this.pricingModel.pricerContext.lastStepParkingTimeDate || this.pricingModel.pricerContext.sessionStartDate;
const pricedData = this.priceTimeDimension(dimensionToPrice, lastStepDate);
if (pricedData) {
- this.pricingModel.pricerContext.lastParkingTimeStepDate = this.consumptionData.endedAt;
+ this.pricingModel.pricerContext.lastStepParkingTimeDate = this.consumptionData.endedAt;
pricedData.sourceName = activePricingDefinition.name;
}
return pricedData;
}
}
- // IMPORTANT - keep track of the consumption where nothing was priced
- this.pricingModel.pricerContext.lastParkingTimeStepDate = this.consumptionData.endedAt;
+ // IMPORTANT - keep track of the latest consumption even when nothing was priced
+ this.pricingModel.pricerContext.lastStepParkingTimeDate = this.consumptionData.endedAt;
}
private getActiveDefinition4Dimension(actualPricingDefinitions: PricingDefinition[], dimensionType: string): PricingDefinition {
@@ -207,13 +215,6 @@ export default class ConsumptionPricer {
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) {
@@ -236,7 +237,40 @@ export default class ConsumptionPricer {
return pricingDimension.pricedData;
}
- private priceEnergyDimension(pricingDimension: PricingDimension, consumptionWh: number): PricedDimensionData {
+ private priceEnergyDimension(pricingDimension: PricingDimension, lastStepCumulatedConsumption: number): PricedDimensionData {
+ // Is there a step size
+ if (pricingDimension.stepSize) {
+ // Price the charging time only when charging!
+ const delta = Utils.createDecimal(this.consumptionData.cumulatedConsumptionWh).minus(lastStepCumulatedConsumption);
+ const nbSteps = delta.divToInt(pricingDimension.stepSize).toNumber();
+ if (nbSteps > 0) {
+ return this.priceConsumptionStep(pricingDimension, nbSteps);
+ }
+ } else {
+ const consumptionWh = this.consumptionData?.consumptionWh || 0;
+ if (consumptionWh > 0) {
+ return this.priceConsumptionWh(pricingDimension, consumptionWh);
+ }
+ }
+ }
+
+ private priceConsumptionStep(pricingDimension: PricingDimension, steps: number): PricedDimensionData {
+ const unitPrice = pricingDimension.price || 0;
+ const amount = Utils.createDecimal(unitPrice).times(steps).times(pricingDimension.stepSize).div(1000).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 priceConsumptionWh(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();

MODIFY
src/types/Pricing.ts
src/types/Pricing.ts
@@ -27,8 +27,9 @@ export interface ResolvedPricingModel {
export interface ConsumptionPricerContext {
flatFeeAlreadyPriced: boolean,
sessionStartDate: Date,
- lastChargingTimeStepDate?: Date // IMPORTANT - used to price when CT when stepSize is set!
- lastParkingTimeStepDate?: Date, // IMPORTANT - used to price when PT when stepSize is set!
+ lastStepCumulatedConsumption?: number, // IMPORTANT - used to price when E when stepSize is set!
+ lastStepChargingTimeDate?: Date // IMPORTANT - used to price when CT when stepSize is set!
+ lastStepParkingTimeDate?: Date, // IMPORTANT - used to price when PT when stepSize is set!
}
export default interface PricingDefinition extends CreatedUpdatedProps, AuthorizationActions {

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -125,12 +125,26 @@ class TestData {
return this.chargingStationContext;
}
- public async initChargingStationContext2TestEnergyPlusFlatFee() : Promise<ChargingStationContext> {
+ public async initChargingStationContext2TestCS3Phased(testMode = 'FF+E') : Promise<ChargingStationContext> {
this.siteContext = this.tenantContext.getSiteContext(ContextDefinition.SITE_CONTEXTS.SITE_BASIC);
this.siteAreaContext = this.siteContext.getSiteAreaContext(ContextDefinition.SITE_AREA_CONTEXTS.WITH_SMART_CHARGING_THREE_PHASED);
this.chargingStationContext = this.siteAreaContext.getChargingStationContext(ContextDefinition.CHARGING_STATION_CONTEXTS.ASSIGNED_OCPP16 + '-' + ContextDefinition.SITE_CONTEXTS.SITE_BASIC + '-' + ContextDefinition.SITE_AREA_CONTEXTS.WITH_SMART_CHARGING_THREE_PHASED);
assert(!!this.chargingStationContext, 'Charging station context should not be null');
- await this.createTariff4ChargingStation('test FF+E', this.chargingStationContext.getChargingStation(), {
+ let dimensions: PricingDimensions;
+ if (testMode === 'FF+E(STEP)') {
+ dimensions = {
+ flatFee: {
+ price: 2,
+ active: true
+ },
+ energy: {
+ price: 0.25, // 25 cents per kWh
+ stepSize: 5000, // Step Size - 5 kWh
+ active: true
+ }
+ };
+ } else {
+ dimensions = {
flatFee: {
price: 2,
active: true
@@ -143,7 +157,9 @@ class TestData {
price: 777, // THIS IS OFF
active: false
}
- });
+ };
+ }
+ await this.createTariff4ChargingStation('test FF+E', this.chargingStationContext.getChargingStation(), dimensions);
return this.chargingStationContext;
}
@@ -1263,11 +1279,10 @@ describe('Billing Service', function() {
describe('FF + ENERGY', () => {
// eslint-disable-next-line @typescript-eslint/require-await
before(async () => {
- // Initialize the charing station context
- await testData.initChargingStationContext2TestEnergyPlusFlatFee();
});
it('should create and bill an invoice with FF + ENERGY', async () => {
+ await testData.initChargingStationContext2TestCS3Phased();
await testData.userService.billingApi.forceSynchronizeUser({ id: testData.userContext.id });
const userWithBillingData = await testData.billingImpl.getUser(testData.userContext);
await testData.assignPaymentMethod(userWithBillingData, 'tok_fr');
@@ -1277,6 +1292,16 @@ describe('Billing Service', function() {
await testData.checkTransactionBillingData(transactionID, BillingInvoiceStatus.PAID);
});
+ it('should create and bill an invoice with FF+ENERGY(STEP)', async () => {
+ await testData.initChargingStationContext2TestCS3Phased('FF+E(STEP)');
+ await testData.userService.billingApi.forceSynchronizeUser({ id: testData.userContext.id });
+ const userWithBillingData = await testData.billingImpl.getUser(testData.userContext);
+ await testData.assignPaymentMethod(userWithBillingData, 'tok_fr');
+ const transactionID = await testData.generateTransaction(testData.userContext);
+ assert(transactionID, 'transactionID should not be null');
+ // Check that we have a new invoice with an invoiceID and an invoiceNumber
+ await testData.checkTransactionBillingData(transactionID, BillingInvoiceStatus.PAID);
+ });
});
describe('On COMBO CCS - DC', () => {

pricing - stepSize on Energy