billing/pricing - rounding issues solved
MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -18,6 +18,7 @@ import Cypher from '../../../utils/Cypher';
import I18nManager from '../../../utils/I18nManager';
import Logging from '../../../utils/Logging';
import PricingEngine from '../../pricing/PricingEngine';
+import PricingHelper from '../../pricing/PricingHelper';
import { Request } from 'express';
import { ServerAction } from '../../../types/Server';
import SettingStorage from '../../../storage/mongodb/SettingStorage';
@@ -1093,7 +1094,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
let pricingData: PricedConsumptionData[] = PricingEngine.extractFinalPricingData(pricingModel);
if (!FeatureToggles.isFeatureActive(Feature.BILLING_SHOW_PRICING_DETAIL)) {
// Accumulate data per dimensions = less details, less disputes
- pricingData = [ this._accumulatePricingDimensions(pricingData) ] ;
+ pricingData = [ PricingHelper.accumulatePricedConsumption(pricingData) ] ;
}
pricingData = pricingData.map((pricingConsumptionData) => this._enrichTransactionPricingData(transaction, pricingConsumptionData));
return pricingData;
@@ -1130,35 +1131,6 @@ export default class StripeBillingIntegration extends BillingIntegration {
return pricingConsumptionData;
}
-
- private _accumulatePricingDimensions(pricingData: PricedConsumptionData[]): PricedConsumptionData {
- const accumulatedConsumptionData: PricedConsumptionData = {};
- for (const pricingConsumptionData of pricingData) {
- this._accumulatePricingDimension(accumulatedConsumptionData, pricingConsumptionData, DimensionType.FLAT_FEE);
- this._accumulatePricingDimension(accumulatedConsumptionData, pricingConsumptionData, DimensionType.ENERGY);
- this._accumulatePricingDimension(accumulatedConsumptionData, pricingConsumptionData, DimensionType.CHARGING_TIME);
- this._accumulatePricingDimension(accumulatedConsumptionData, pricingConsumptionData, DimensionType.PARKING_TIME);
- }
- return accumulatedConsumptionData;
- }
-
- private _accumulatePricingDimension(accumulatedData: PricedConsumptionData, pricedData: PricedConsumptionData, dimensionType: DimensionType): void {
- if (pricedData[dimensionType]) {
- if (!accumulatedData[dimensionType]) {
- const emptyDimensionData: PricedDimensionData = {
- unitPrice: 0,
- quantity:0,
- amount: 0,
- roundedAmount: 0
- };
- accumulatedData[dimensionType] = emptyDimensionData;
- }
- accumulatedData[dimensionType].quantity = Utils.createDecimal(accumulatedData[dimensionType].quantity).plus(pricedData[dimensionType].quantity).toNumber();
- accumulatedData[dimensionType].amount = Utils.createDecimal(accumulatedData[dimensionType].amount).plus(pricedData[dimensionType].amount).toNumber();
- accumulatedData[dimensionType].roundedAmount = Utils.createDecimal(accumulatedData[dimensionType].roundedAmount).plus(pricedData[dimensionType].roundedAmount).toNumber();
- }
- }
-
private _convertToBillingInvoiceItem(transaction: Transaction) : BillingInvoiceItem {
// Destructuring transaction.stop
const { price: unitPrice, priceUnit: currency, roundedPrice, totalConsumptionWh, timestamp } = transaction.stop;

MODIFY
src/integration/pricing/ConsumptionPricer.ts
src/integration/pricing/ConsumptionPricer.ts
import PricingDefinition, { DimensionType, PricedConsumptionData, PricedDimensionData, PricingDimension, PricingRestriction, ResolvedPricingModel } from '../../types/Pricing';
import Consumption from '../../types/Consumption';
+import PricingHelper from './PricingHelper';
import Tenant from '../../types/Tenant';
import Utils from '../../utils/Utils';
import moment from 'moment';
@@ -246,7 +247,7 @@ export default class ConsumptionPricer {
pricingDimension.pricedData = {
unitPrice: unitPrice,
amount: unitPrice,
- roundedAmount: this.truncTo(unitPrice, 2),
+ roundedAmount: PricingHelper.truncTo(unitPrice, 2),
quantity: 1 // Session
};
return pricingDimension.pricedData;
@@ -260,7 +261,7 @@ export default class ConsumptionPricer {
const pricedData: PricedDimensionData = {
unitPrice: unitPrice,
amount,
- roundedAmount: this.truncTo(amount, 2),
+ roundedAmount: PricingHelper.truncTo(amount, 2),
quantity,
stepSize: pricingDimension.stepSize,
};
@@ -279,7 +280,7 @@ export default class ConsumptionPricer {
const pricedData: PricedDimensionData = {
unitPrice: unitPrice,
amount,
- roundedAmount: this.truncTo(amount, 2),
+ roundedAmount: PricingHelper.truncTo(amount, 2),
quantity
};
// Add the consumption to the previous data (if any) - for the billing
@@ -314,7 +315,7 @@ export default class ConsumptionPricer {
const pricedData: PricedDimensionData = {
unitPrice: unitPrice,
amount,
- roundedAmount: this.truncTo(amount, 2),
+ roundedAmount: PricingHelper.truncTo(amount, 2),
quantity,
stepSize: pricingDimension.stepSize
};
@@ -331,7 +332,7 @@ export default class ConsumptionPricer {
const pricedData: PricedDimensionData = {
unitPrice: unitPrice,
amount,
- roundedAmount: this.truncTo(amount, 2),
+ roundedAmount: PricingHelper.truncTo(amount, 2),
quantity: seconds
};
// Add the consumption to the previous data (if any) - for the billing
@@ -346,14 +347,9 @@ export default class ConsumptionPricer {
if (previousData) {
previousData.amount += pricedData.amount;
previousData.quantity += pricedData.quantity;
- previousData.roundedAmount = this.truncTo(previousData.amount, 2);
+ previousData.roundedAmount = PricingHelper.truncTo(previousData.amount, 2);
} else {
pricingDimension.pricedData = pricedData;
}
}
-
- private truncTo(value: number, scale: number): number {
- const truncPower = Math.pow(10, scale);
- return Utils.createDecimal(value).times(truncPower).trunc().div(truncPower).toNumber();
- }
}

ADD
src/integration/pricing/PricingHelper.ts
+import { DimensionType, PricedConsumptionData, PricedDimensionData, PricingDimensions } from '../../types/Pricing';
+
+import Decimal from 'decimal.js';
+import Utils from '../../utils/Utils';
+
+export default class PricingHelper {
+
+ public static accumulatePricingDimensions(pricingDimensions: PricingDimensions[]): number {
+ // Apply the same logic than the invoice to avoid rounding issues
+ const flatFee = PricingHelper.accumulatePricingDimension(pricingDimensions, DimensionType.FLAT_FEE);
+ const energy = PricingHelper.accumulatePricingDimension(pricingDimensions, DimensionType.ENERGY);
+ const chargingTime = PricingHelper.accumulatePricingDimension(pricingDimensions, DimensionType.CHARGING_TIME);
+ const parkingTime = PricingHelper.accumulatePricingDimension(pricingDimensions, DimensionType.PARKING_TIME);
+ // The final invoice shows individual lines per dimensions - each amount are truncated
+ const total = new Decimal(0).plus(flatFee).plus(energy).plus(chargingTime).plus(parkingTime);
+ return PricingHelper.truncTo(total.toNumber(), 2);
+ }
+
+ public static accumulatePricingDimension(pricingDimensions: PricingDimensions[], dimensionType: DimensionType): number {
+ let total = new Decimal(0);
+ pricingDimensions.forEach((pricingDimension) => {
+ total = total.plus(pricingDimension[dimensionType]?.pricedData?.amount || 0);
+ });
+ return PricingHelper.truncTo(total.toNumber(), 2);
+ }
+
+ public static truncTo(value: number, scale: number): number {
+ const truncPower = Math.pow(10, scale);
+ return Utils.createDecimal(value).times(truncPower).trunc().div(truncPower).toNumber();
+ }
+
+ public static accumulatePricedConsumption(pricingData: PricedConsumptionData[]): PricedConsumptionData {
+ const accumulatedConsumptionData: PricedConsumptionData = {};
+ for (const pricingConsumptionData of pricingData) {
+ PricingHelper.accumulatePricedConsumptionData(accumulatedConsumptionData, pricingConsumptionData, DimensionType.FLAT_FEE);
+ PricingHelper.accumulatePricedConsumptionData(accumulatedConsumptionData, pricingConsumptionData, DimensionType.ENERGY);
+ PricingHelper.accumulatePricedConsumptionData(accumulatedConsumptionData, pricingConsumptionData, DimensionType.CHARGING_TIME);
+ PricingHelper.accumulatePricedConsumptionData(accumulatedConsumptionData, pricingConsumptionData, DimensionType.PARKING_TIME);
+ }
+ return accumulatedConsumptionData;
+ }
+
+ private static accumulatePricedConsumptionData(accumulatedData: PricedConsumptionData, pricedData: PricedConsumptionData, dimensionType: DimensionType): void {
+ if (pricedData[dimensionType]) {
+ if (!accumulatedData[dimensionType]) {
+ const emptyDimensionData: PricedDimensionData = {
+ unitPrice: 0,
+ quantity:0,
+ amount: 0,
+ roundedAmount: 0
+ };
+ accumulatedData[dimensionType] = emptyDimensionData;
+ }
+ accumulatedData[dimensionType].quantity = Utils.createDecimal(accumulatedData[dimensionType].quantity).plus(pricedData[dimensionType].quantity).toNumber();
+ accumulatedData[dimensionType].amount = Utils.createDecimal(accumulatedData[dimensionType].amount).plus(pricedData[dimensionType].amount).toNumber();
+ accumulatedData[dimensionType].roundedAmount = Utils.createDecimal(accumulatedData[dimensionType].roundedAmount).plus(pricedData[dimensionType].roundedAmount).toNumber();
+ }
+ }
+}

MODIFY
src/integration/pricing/simple-pricing/BuiltInPricingIntegration.ts
src/integration/pricing/simple-pricing/BuiltInPricingIntegration.ts
/* eslint-disable max-len */
import FeatureToggles, { Feature } from '../../../utils/FeatureToggles';
-import PricingDefinition, { PricedConsumption, PricingSource, ResolvedPricingModel } from '../../../types/Pricing';
+import PricingDefinition, { PricedConsumption, PricingDimensions, PricingSource, ResolvedPricingModel } from '../../../types/Pricing';
import ChargingStation from '../../../types/ChargingStation';
import Consumption from '../../../types/Consumption';
import PricingEngine from '../PricingEngine';
+import PricingHelper from '../PricingHelper';
import PricingIntegration from '../PricingIntegration';
import { SimplePricingSetting } from '../../../types/Setting';
import Tenant from '../../../types/Tenant';
@@ -49,11 +50,29 @@ export default class BuiltInPricingIntegration extends PricingIntegration<Simple
amount: amount,
roundedAmount: roundedAmount,
currencyCode: this.settings.currency,
- cumulatedAmount: transaction.currentCumulatedPrice ? Utils.createDecimal(transaction.currentCumulatedPrice).plus(amount).toNumber() : amount
+ cumulatedAmount: 0,
};
+
+ if (!FeatureToggles.isFeatureActive(Feature.PRICING_NEW_MODEL)) {
+ // TODO - Old way of doing it - to be removed
+ pricedConsumption.cumulatedAmount = transaction.currentCumulatedPrice ? Utils.createDecimal(transaction.currentCumulatedPrice).plus(amount).toNumber() : amount;
+ } else {
+ // New logic - get the amount from the priced data to avoid rounding issues
+ pricedConsumption.cumulatedAmount = this.computeCumulatedAmount(pricingModel);
+ }
return Promise.resolve(pricedConsumption);
}
+ private computeCumulatedAmount(pricingModel: ResolvedPricingModel): number {
+ const allDimensions: PricingDimensions[] = [];
+ pricingModel.pricingDefinitions.forEach((pricingDefinition) => {
+ if (pricingDefinition.dimensions) {
+ allDimensions.push(pricingDefinition.dimensions);
+ }
+ });
+ return PricingHelper.accumulatePricingDimensions(allDimensions);
+ }
+
private async resolvePricingContext(tenant: Tenant, transaction: Transaction, chargingStation: ChargingStation): Promise<ResolvedPricingModel> {
const resolvedPricingModel: ResolvedPricingModel = await PricingEngine.resolvePricingContext(tenant, transaction, chargingStation);
if (!resolvedPricingModel.pricingDefinitions?.length) {

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
@@ -1086,8 +1086,10 @@ export default class Utils {
}
public static truncTo(value: number, scale: number): number {
+ // const truncPower = Math.pow(10, scale);
+ // return Math.trunc(value * truncPower) / truncPower;
const truncPower = Math.pow(10, scale);
- return Math.trunc(value * truncPower) / truncPower;
+ return Utils.createDecimal(value).times(truncPower).trunc().div(truncPower).toNumber();
}
public static firstLetterInUpperCase(value: string): string {

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -351,9 +351,11 @@ class TestData {
assert(billingDataStop?.invoiceNumber === null, `Invoice Number should not yet been set - Invoice Number is: ${billingDataStop?.invoiceNumber}`);
}
if (expectedPrice) {
- // TODO: Check priced transaction - We have a rounding issue here!
- // const roundedPrice: number = transactionResponse.data.stop.roundedPrice;
- // assert(roundedPrice === expectedPrice, `The rounded price should be: ${expectedPrice} - actual value: ${roundedPrice}`);
+ // --------------------------------
+ // Check transaction rounded price
+ // --------------------------------
+ const roundedPrice = Utils.createDecimal(transactionResponse.data.stop.roundedPrice);
+ assert(roundedPrice.equals(expectedPrice), `The rounded price should be: ${expectedPrice} - actual value: ${roundedPrice.toNumber()}`);
// ---------------------------
// Check priced dimensions
// ---------------------------

billing/pricing - rounding issues solved