Add migration task to reprice transactions
MODIFY
src/integration/pricing/simple-pricing/SimplePricingIntegration.ts
src/integration/pricing/simple-pricing/SimplePricingIntegration.ts
@@ -29,7 +29,7 @@ export default class SimplePricingIntegration extends PricingIntegration<SimpleP
let roundedAmount: number;
if (consumptionData.consumptionWh && typeof consumptionData.consumptionWh === 'number') {
amount = Utils.computeSimplePrice(this.settings.price, consumptionData.consumptionWh);
- roundedAmount = Utils.computeSimpleRoundedPrice(this.settings.price, consumptionData.consumptionWh);
+ roundedAmount = Utils.truncTo(amount, 2);
} else {
amount = 0;
roundedAmount = 0;

MODIFY
src/migration/MigrationHandler.ts
src/migration/MigrationHandler.ts
@@ -48,6 +48,7 @@ import UpdateConsumptionsToObjectIDsTask from './tasks/UpdateConsumptionsToObjec
import UpdateLimitsInConsumptionsTask from './tasks/UpdateLimitsInConsumptionsTask';
import cluster from 'cluster';
import moment from 'moment';
+import RecomputeAllTransactionsWithSimplePricingTask from './tasks/RecomputeAllTransactionsWithSimplePricingTask';
const MODULE_NAME = 'MigrationHandler';
@@ -113,6 +114,7 @@ export default class MigrationHandler {
currentMigrationTasks.push(new MigrateUserSettingsTask());
currentMigrationTasks.push(new RenameSMTPAuthErrorTask());
currentMigrationTasks.push(new ResetCarCatalogsHashTask());
+ currentMigrationTasks.push(new RecomputeAllTransactionsWithSimplePricingTask());
// Get the already done migrations from the DB
const migrationTasksDone = await MigrationStorage.getMigrations();
// Check

MODIFY
src/migration/tasks/RecomputeAllTransactionsConsumptionsTask.ts
src/migration/tasks/RecomputeAllTransactionsConsumptionsTask.ts
@@ -9,7 +9,6 @@ import { ServerAction } from '../../types/Server';
import Tenant from '../../types/Tenant';
import TenantStorage from '../../storage/mongodb/TenantStorage';
import TransactionStorage from '../../storage/mongodb/TransactionStorage';
-import Utils from '../../utils/Utils';
const MODULE_NAME = 'RecomputeAllTransactionsConsumptionsTask';

ADD
src/migration/tasks/RecomputeAllTransactionsWithSimplePricingTask.ts
+import global, { ActionsResponse } from '../../types/GlobalType';
+
+import Constants from '../../utils/Constants';
+import Logging from '../../utils/Logging';
+import MigrationTask from '../MigrationTask';
+import OCPPUtils from '../../server/ocpp/utils/OCPPUtils';
+import Promise from 'bluebird';
+import { ServerAction } from '../../types/Server';
+import Tenant from '../../types/Tenant';
+import TenantStorage from '../../storage/mongodb/TenantStorage';
+
+const TASK_NAME = 'RecomputeAllTransactionsWithSimplePricingTask';
+
+export default class RecomputeAllTransactionsWithSimplePricingTask extends MigrationTask {
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+ async migrate() {
+ const tenants = await TenantStorage.getTenants({}, Constants.DB_PARAMS_MAX_LIMIT);
+ for (const tenant of tenants.result) {
+ await this.migrateTenant(tenant);
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+ async migrateTenant(tenant: Tenant) {
+ const transactionsUpdated: ActionsResponse = {
+ inError: 0,
+ inSuccess: 0,
+ };
+ const timeTotalFrom = new Date().getTime();
+ // Get transactions
+ const transactionsMDB = await global.database.getCollection<any>(tenant.id, 'transactions')
+ .aggregate([
+ {
+ $match: {
+ 'stop.price': { $gt: 0 },
+ 'stop.pricingSource': 'simple'
+ }
+ },
+ {
+ $project: { '_id': 1 }
+ }
+ ]).toArray();
+ if (transactionsMDB.length > 0) {
+ await Logging.logInfo({
+ tenantID: Constants.DEFAULT_TENANT,
+ action: ServerAction.MIGRATION,
+ module: TASK_NAME, method: 'migrateTenant',
+ message: `${transactionsMDB.length} Transaction(s) are going to be recomputed in Tenant '${tenant.name}' ('${tenant.subdomain}')...`,
+ });
+ await Promise.map(transactionsMDB, async (transactionMDB) => {
+ try {
+ await OCPPUtils.rebuildTransactionSimplePricing(tenant.id, transactionMDB._id);
+ transactionsUpdated.inSuccess++;
+ } catch (error) {
+ transactionsUpdated.inError++;
+ await Logging.logError({
+ tenantID: Constants.DEFAULT_TENANT,
+ action: ServerAction.MIGRATION,
+ module: TASK_NAME, method: 'migrateTenant',
+ message: `> ${transactionsUpdated.inError + transactionsUpdated.inSuccess}/${transactionsMDB.length} - Cannot recompute the consumptions of Transaction ID '${transactionMDB._id}' in Tenant '${tenant.name}' ('${tenant.subdomain}')`,
+ detailedMessages: { error: error.message, stack: error.stack }
+ });
+ }
+ }, { concurrency: 5 }).then(() => {
+ const totalDurationSecs = Math.trunc((new Date().getTime() - timeTotalFrom) / 1000);
+ // Log in the default tenant
+ Logging.logActionsResponse(Constants.DEFAULT_TENANT, ServerAction.MIGRATION,
+ TASK_NAME, 'migrateTenant', transactionsUpdated,
+ `{{inSuccess}} transaction(s) were successfully processed in ${totalDurationSecs} secs in Tenant '${tenant.name}' ('${tenant.subdomain}')`,
+ `{{inError}} transaction(s) failed to be processed in ${totalDurationSecs} secs in Tenant '${tenant.name}' ('${tenant.subdomain}')`,
+ `{{inSuccess}} transaction(s) were successfully processed in ${totalDurationSecs} secs and {{inError}} failed to be processed in Tenant '${tenant.name}' ('${tenant.subdomain}')`,
+ `All the transactions are up to date in Tenant '${tenant.name}' ('${tenant.subdomain}')`
+ );
+ });
+ }
+ }
+
+ getVersion(): string {
+ return '1.0';
+ }
+
+ getName(): string {
+ return TASK_NAME;
+ }
+
+ isAsynchronous(): boolean {
+ return true;
+ }
+}

MODIFY
src/server/ocpp/utils/OCPPUtils.ts
src/server/ocpp/utils/OCPPUtils.ts
@@ -430,6 +430,64 @@ export default class OCPPUtils {
chargingStation, transaction.connectorId, transaction.currentTotalInactivitySecs);
}
+ public static async rebuildTransactionSimplePricing(tenantID: string, transactionId: number): Promise<void> {
+ if (!transactionId) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.REBUILD_TRANSACTION_CONSUMPTIONS,
+ module: MODULE_NAME, method: 'rebuildTransactionPrices',
+ message: 'Transaction ID must be provided',
+ });
+ }
+ // Get the Transaction
+ const transaction = await TransactionStorage.getTransaction(tenantID, transactionId);
+ if (!transaction) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.REBUILD_TRANSACTION_CONSUMPTIONS,
+ module: MODULE_NAME, method: 'rebuildTransactionPrices',
+ message: `Transaction ID '${transactionId}' does not exist`,
+ });
+ }
+ if (!transaction.stop) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.REBUILD_TRANSACTION_CONSUMPTIONS,
+ module: MODULE_NAME, method: 'rebuildTransactionPrices',
+ message: `Transaction ID '${transactionId}' is in progress`,
+ });
+ }
+ if (transaction.stop.pricingSource !== PricingSettingsType.SIMPLE) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.REBUILD_TRANSACTION_CONSUMPTIONS,
+ module: MODULE_NAME, method: 'rebuildTransactionPrices',
+ message: `Transaction ID '${transactionId}' was not priced with simple pricing`,
+ });
+ }
+
+ // Retrieve price per kWh
+ const transactionSimplePricePerkWh = Utils.roundTo(transaction.stop.price / (transaction.stop.totalConsumptionWh / 1000), 2);
+ const consumptionDataResult: DataResult<Consumption> = await ConsumptionStorage.getTransactionConsumptions(tenantID, { transactionId });
+
+ transaction.currentCumulatedPrice = 0;
+ for (const consumption of consumptionDataResult.result) {
+ consumption.amount = Utils.computeSimplePrice(transactionSimplePricePerkWh, consumption.consumptionWh);
+ consumption.roundedAmount = Utils.truncTo(consumption.amount, 2);
+
+ transaction.currentCumulatedPrice += consumption.amount;
+ consumption.cumulatedAmount = transaction.currentCumulatedPrice;
+ // Save all
+ await ConsumptionStorage.saveConsumption(tenantID, consumption);
+ }
+
+ transaction.roundedPrice = Utils.truncTo(transaction.currentCumulatedPrice, 2);
+ transaction.stop.price = transaction.currentCumulatedPrice;
+ transaction.stop.roundedPrice = transaction.roundedPrice;
+
+ await TransactionStorage.saveTransaction(tenantID, transaction);
+ }
+
public static async rebuildTransactionConsumptions(tenantID: string, transactionId: number): Promise<number> {
let consumptions: Consumption[] = [];
let transactionSimplePricePerkWh;
@@ -461,7 +519,7 @@ export default class OCPPUtils {
}
// Check Simple Pricing
if (transaction.pricingSource === PricingSettingsType.SIMPLE) {
- transactionSimplePricePerkWh = Utils.truncTo(transaction.stop.price / (transaction.stop.totalConsumptionWh / 1000), 2);
+ transactionSimplePricePerkWh = Utils.roundTo(transaction.stop.price / (transaction.stop.totalConsumptionWh / 1000), 2);
}
// Get the Charging Station
const chargingStation = await ChargingStationStorage.getChargingStation(tenantID,
@@ -513,7 +571,7 @@ export default class OCPPUtils {
// Override the price if simple pricing only
if (transactionSimplePricePerkWh > 0) {
consumption.amount = Utils.computeSimplePrice(transactionSimplePricePerkWh, consumption.consumptionWh);
- consumption.roundedAmount = Utils.computeSimpleRoundedPrice(transactionSimplePricePerkWh, consumption.consumptionWh);
+ consumption.roundedAmount = Utils.truncTo(consumption.amount, 2);
consumption.pricingSource = PricingSettingsType.SIMPLE;
}
// Cumulated props

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
@@ -475,11 +475,7 @@ export default class Utils {
}
public static computeSimplePrice(pricePerkWh: number, consumptionWh: number): number {
- return Utils.roundTo(pricePerkWh * (consumptionWh / 1000), 6);
- }
-
- public static computeSimpleRoundedPrice(pricePerkWh: number, consumptionWh: number): number {
- return Utils.roundTo(pricePerkWh * (consumptionWh / 1000), 2);
+ return pricePerkWh * (consumptionWh / 1000);
}
public static convertUserToObjectID(user: User | UserToken | string): ObjectID | null {

Add migration task to reprice transactions