pricing - fix E+PT(STEP)
MODIFY
src/integration/pricing/PricingEngine.ts
src/integration/pricing/PricingEngine.ts
@@ -26,7 +26,7 @@ export default class PricingEngine {
const resolvedPricingModel: ResolvedPricingModel = {
currentContext: {
flatFeeAlreadyPriced: false,
- startDate: transaction.timestamp
+ sessionStartDate: transaction.timestamp
},
pricingDefinitions
};
@@ -230,12 +230,16 @@ export default class PricingEngine {
const consumptionWh = consumptionData?.consumptionWh || 0;
// Price the charging time only when charging!
if (consumptionWh > 0) {
- const pricedData = PricingEngine.priceTimeDimension(dimensionToPrice, consumptionData, currentContext);
+ 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;
}
}
@@ -247,13 +251,17 @@ export default class PricingEngine {
const consumptionWh = consumptionData?.consumptionWh || 0;
// Price the parking time only after having charged - NOT during the warmup!
if (cumulatedConsumptionDataWh > 0 && consumptionWh <= 0) {
- const pricedData = PricingEngine.priceTimeDimension(dimensionToPrice, consumptionData, currentContext);
+ 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 {
@@ -343,15 +351,13 @@ export default class PricingEngine {
return pricedData;
}
- private static priceTimeDimension(pricingDimension: PricingDimension, consumptionData: Consumption, currentContext: CurrentContext): PricedDimensionData {
+ private static priceTimeDimension(pricingDimension: PricingDimension, consumptionData: Consumption, lastStepDate: Date): PricedDimensionData {
// Is there a step size
if (pricingDimension.stepSize) {
- const lastStepDate = currentContext.lastStepDate || currentContext.startDate;
// 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.lastStepDate = consumptionData.endedAt;
return this.priceTimeSteps(pricingDimension, nbSteps);
}
} else {
@@ -362,6 +368,25 @@ export default class PricingEngine {
}
}
+ // 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();

MODIFY
src/types/Pricing.ts
src/types/Pricing.ts
@@ -26,8 +26,9 @@ export interface ResolvedPricingModel {
export interface CurrentContext {
flatFeeAlreadyPriced: boolean,
- startDate: Date,
- lastStepDate?: Date,
+ 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!
}
export default interface PricingDefinition extends CreatedUpdatedProps, AuthorizationActions {

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -179,6 +179,18 @@ class TestData {
active: true
}
};
+ } else if (testMode === 'E+PT+STEP') {
+ dimensions = {
+ energy: {
+ price: 0.50,
+ active: true
+ },
+ parkingTime: {
+ price: 20, // Euro per hour
+ stepSize: 120, // 120 seconds == 2 minutes
+ active: true
+ }
+ };
} else if (testMode === 'E-After30mins') {
// Create a second tariff with a different pricing strategy
dimensions = {
@@ -1207,7 +1219,7 @@ describe('Billing Service', function() {
});
- describe('with Transaction Billing + Immediate Billing ON', () => {
+ describe('with Pricing + Billing', () => {
before(async () => {
testData.initUserContextAsAdmin();
// Initialize the Billing module
@@ -1279,7 +1291,7 @@ describe('Billing Service', function() {
await testData.checkTransactionBillingData(transactionID, BillingInvoiceStatus.PAID);
});
- it('should bill the CT with 1EUR Step on COMBO CCS - DC', async () => {
+ it('should bill the CT(STEP)+PT(STEP) on COMBO CCS - DC', async () => {
await testData.initChargingStationContext2TestFastCharger('CT+STEP');
await testData.userService.billingApi.forceSynchronizeUser({ id: testData.userContext.id });
const userWithBillingData = await testData.billingImpl.getUser(testData.userContext);
@@ -1290,6 +1302,17 @@ describe('Billing Service', function() {
await testData.checkTransactionBillingData(transactionID, BillingInvoiceStatus.PAID);
});
+ it('should bill the ENERGY + PT(STEP) on COMBO CCS - DC', async () => {
+ await testData.initChargingStationContext2TestFastCharger('E+PT+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);
+ });
+
it('should bill the ENERGY with 2 tariffs on COMBO CCS - DC', async () => {
// A first Tariff for the ENERGY Only
await testData.initChargingStationContext2TestFastCharger();

pricing - fix E+PT(STEP)