Fixed scheduler task not returned by Scheduler manager
Get the Charging Station/Site when the transactions to close are retrieved from the DB and pass it to the softStopTransaction() method
Put join to charging station in storage after limit/sort if the transactionsToClose param is not provided
Renaming of Clean by Close
Simplified the code
MODIFY
src/locking/LockingHelper.ts
src/locking/LockingHelper.ts
@@ -116,8 +116,8 @@ export default class LockingHelper {
return lock;
}
- public static async acquireTransactionsCleanLock(tenantID: string): Promise<Lock | null> {
- const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.TRANSACTION, 'clean-transactions');
+ public static async acquireCloseTransactionsInProgressLock(tenantID: string): Promise<Lock | null> {
+ const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.TRANSACTION, 'close-transactions-in-progress');
if (!(await LockingManager.acquire(lock))) {
return null;
}

MODIFY
src/scheduler/SchedulerManager.ts
src/scheduler/SchedulerManager.ts
@@ -10,7 +10,7 @@ import CheckOfflineChargingStationsTask from './tasks/CheckOfflineChargingStatio
import CheckPreparingSessionNotStartedTask from './tasks/CheckPreparingSessionNotStartedTask';
import CheckSessionNotStartedAfterAuthorizeTask from './tasks/CheckSessionNotStartedAfterAuthorizeTask';
import CheckUserAccountInactivityTask from './tasks/CheckUserAccountInactivityTask';
-import CleanTransactionsInProgressTask from './tasks/CleanTransactionsInProgressTask';
+import CloseTransactionsInProgressTask from './tasks/CloseTransactionsInProgressTask';
import Constants from '../utils/Constants';
import Logging from '../utils/Logging';
import LoggingDatabaseTableCleanupTask from './tasks/LoggingDatabaseTableCleanupTask';
@@ -137,9 +137,8 @@ export default class SchedulerManager {
return new CheckChargingStationTemplateTask();
case 'MigrateSensitiveDataTask':
return new MigrateSensitiveDataTask();
- case 'CleanTransactionsInProgressTask':
- new CleanTransactionsInProgressTask();
- break;
+ case 'CloseTransactionsInProgressTask':
+ return new CloseTransactionsInProgressTask();
default:
await Logging.logError({
tenantID: Constants.DEFAULT_TENANT,

DELETE
src/scheduler/tasks/CleanTransactionsInProgressTask.ts
-import { ActionsResponse } from '../../types/GlobalType';
-import Constants from '../../utils/Constants';
-import LockingHelper from '../../locking/LockingHelper';
-import LockingManager from '../../locking/LockingManager';
-import Logging from '../../utils/Logging';
-import OCPPUtils from '../../server/ocpp/utils/OCPPUtils';
-import { ServerAction } from '../../types/Server';
-import Tenant from '../../types/Tenant';
-import TenantSchedulerTask from '../TenantSchedulerTask';
-import { TransactionDataResult } from '../../types/DataResult';
-import TransactionStorage from '../../storage/mongodb/TransactionStorage';
-import Utils from '../../utils/Utils';
-
-const MODULE_NAME = 'CleanTransactionsInProgressTask';
-
-export default class CleanTransactionsInProgressTask extends TenantSchedulerTask {
- public async processTenant(tenant: Tenant): Promise<void> {
- const cleanTransactionsLock = await LockingHelper.acquireTransactionsCleanLock(tenant.id);
- if (cleanTransactionsLock) {
- try {
- const result: ActionsResponse = {
- inError: 0,
- inSuccess: 0,
- };
- const startTime = new Date().getTime();
- let transactionsToClean: TransactionDataResult;
- do {
- // Get Transactions to clean
- transactionsToClean = await TransactionStorage.getTransactions(tenant, { stop: { $exists: false }, transactionsToClean: true }, Constants.DB_PARAMS_MAX_LIMIT,
- ['id', 'chargeBoxID', 'tagID', 'lastConsumption.timestamp', 'timestamp', 'lastConsumption.value', 'meterStart']);
- for (const transactionToClean of transactionsToClean.result) {
- try {
- // Soft stop transaction
- await OCPPUtils.softStopTransaction(tenant, transactionToClean) ? result.inSuccess++ : result.inError++;
- } catch (error) {
- result.inError++;
- await Logging.logError({
- tenantID: tenant.id,
- action: ServerAction.TRANSACTION_SOFT_STOP,
- module: MODULE_NAME, method: 'processTenant',
- message: `Cannot soft stop Transaction ID '${transactionToClean.id}': ${error.message}`,
- detailedMessages: { transactionToClean, error: error.stack }
- });
- }
- }
- if (!Utils.isEmptyArray(transactionsToClean.result) && (result.inError + result.inSuccess) > 0) {
- const intermediateDurationSecs = Math.round((new Date().getTime() - startTime) / 1000);
- await Logging.logDebug({
- tenantID: tenant.id,
- action: ServerAction.TAGS_IMPORT,
- module: MODULE_NAME, method: 'processTenant',
- message: `${result.inError + result.inSuccess}/${transactionsToClean.count} Transaction(s) have been processed in ${intermediateDurationSecs}s...`
- });
- }
- } while (!Utils.isEmptyArray(transactionsToClean?.result));
- // Log final results
- const executionDurationSecs = Math.round((new Date().getTime() - startTime) / 1000);
- await Logging.logActionsResponse(tenant.id, ServerAction.TAGS_IMPORT, MODULE_NAME, 'processTenant', result,
- `{{inSuccess}} Transaction(s) have been stopped successfully in ${executionDurationSecs}s in Tenant ${Utils.buildTenantName(tenant)}`,
- `{{inError}} Transaction(s) failed to be stopped in ${executionDurationSecs}s in Tenant ${Utils.buildTenantName(tenant)}`,
- `{{inSuccess}} Transaction(s) have been stopped successfully but {{inError}} failed in ${executionDurationSecs}s in Tenant ${Utils.buildTenantName(tenant)}`,
- `Not Transaction has been stopped in ${executionDurationSecs}s in Tenant ${Utils.buildTenantName(tenant)}`
- );
- } catch (error) {
- // Log error
- await Logging.logActionExceptionMessage(tenant.id, ServerAction.TRANSACTION_SOFT_STOP, error);
- } finally {
- // Release the lock
- await LockingManager.release(cleanTransactionsLock);
- }
- }
- }
-}

ADD
src/scheduler/tasks/CloseTransactionsInProgressTask.ts
+import { ActionsResponse } from '../../types/GlobalType';
+import Constants from '../../utils/Constants';
+import LockingHelper from '../../locking/LockingHelper';
+import LockingManager from '../../locking/LockingManager';
+import Logging from '../../utils/Logging';
+import OCPPUtils from '../../server/ocpp/utils/OCPPUtils';
+import { ServerAction } from '../../types/Server';
+import Tenant from '../../types/Tenant';
+import TenantSchedulerTask from '../TenantSchedulerTask';
+import TransactionStorage from '../../storage/mongodb/TransactionStorage';
+import Utils from '../../utils/Utils';
+
+const MODULE_NAME = 'CleanTransactionsInProgressTask';
+
+export default class CloseTransactionsInProgressTask extends TenantSchedulerTask {
+ public async processTenant(tenant: Tenant): Promise<void> {
+ const transactionsCloseLock = await LockingHelper.acquireCloseTransactionsInProgressLock(tenant.id);
+ if (transactionsCloseLock) {
+ try {
+ const result: ActionsResponse = {
+ inError: 0,
+ inSuccess: 0,
+ };
+ const startTime = new Date().getTime();
+ // Get opened transactions to close
+ const transactions = await TransactionStorage.getTransactions(tenant, { transactionsToClose: true }, Constants.DB_PARAMS_MAX_LIMIT,
+ ['id', 'tagID', 'lastConsumption.timestamp', 'timestamp', 'lastConsumption.value', 'meterStart',
+ 'chargeBoxID', 'chargeBox', 'siteID', 'siteAreaID', 'siteArea']);
+ for (const transaction of transactions.result) {
+ try {
+ // Soft stop transaction
+ if (await OCPPUtils.softStopTransaction(tenant, transaction, transaction.chargeBox, transaction.siteArea)) {
+ result.inSuccess++;
+ } else {
+ result.inError++;
+ await Logging.logError({
+ tenantID: tenant.id,
+ action: ServerAction.TRANSACTION_SOFT_STOP,
+ module: MODULE_NAME, method: 'processTenant',
+ message: `Cannot soft stop Transaction ID '${transaction.id}'`,
+ detailedMessages: { transaction }
+ });
+ }
+ } catch (error) {
+ result.inError++;
+ await Logging.logError({
+ tenantID: tenant.id,
+ action: ServerAction.TRANSACTION_SOFT_STOP,
+ module: MODULE_NAME, method: 'processTenant',
+ message: `Cannot soft stop Transaction ID '${transaction.id}': ${error.message as string}`,
+ detailedMessages: { transaction, error: error.stack }
+ });
+ }
+ }
+ // Log final results
+ const executionDurationSecs = Math.round((new Date().getTime() - startTime) / 1000);
+ await Logging.logActionsResponse(tenant.id, ServerAction.TAGS_IMPORT, MODULE_NAME, 'processTenant', result,
+ `{{inSuccess}} Transaction(s) have been soft stopped successfully in ${executionDurationSecs}s in Tenant ${Utils.buildTenantName(tenant)}`,
+ `{{inError}} Transaction(s) failed to be soft stopped in ${executionDurationSecs}s in Tenant ${Utils.buildTenantName(tenant)}`,
+ `{{inSuccess}} Transaction(s) have been soft stopped successfully but {{inError}} failed in ${executionDurationSecs}s in Tenant ${Utils.buildTenantName(tenant)}`,
+ `Not Transaction has been soft stopped in ${executionDurationSecs}s in Tenant ${Utils.buildTenantName(tenant)}`
+ );
+ } catch (error) {
+ await Logging.logActionExceptionMessage(tenant.id, ServerAction.TRANSACTION_SOFT_STOP, error);
+ } finally {
+ // Release the lock
+ await LockingManager.release(transactionsCloseLock);
+ }
+ }
+ }
+}

MODIFY
src/server/ocpp/utils/OCPPUtils.ts
src/server/ocpp/utils/OCPPUtils.ts
@@ -36,6 +36,7 @@ import { Promise } from 'bluebird';
import RegistrationToken from '../../../types/RegistrationToken';
import RegistrationTokenStorage from '../../../storage/mongodb/RegistrationTokenStorage';
import { ServerAction } from '../../../types/Server';
+import Site from '../../../types/Site';
import SiteArea from '../../../types/SiteArea';
import SiteAreaStorage from '../../../storage/mongodb/SiteAreaStorage';
import Tag from '../../../types/Tag';
@@ -1729,33 +1730,37 @@ export default class OCPPUtils {
}
}
- public static async softStopTransaction(tenant: Tenant, transaction: Transaction): Promise<boolean> {
- // Get the Charging Station
- const chargingStation = await ChargingStationStorage.getChargingStation(tenant, transaction.chargeBoxID, { withSiteArea: true });
+ public static async softStopTransaction(tenant: Tenant, transaction: Transaction, chargingStation: ChargingStation, siteArea: SiteArea): Promise<boolean> {
+ // Check
+ if (!tenant || !transaction || !chargingStation) {
+ return false;
+ }
+ if (Utils.isTenantComponentActive(tenant, TenantComponents.ORGANIZATION) && !siteArea) {
+ return false;
+ }
+ // Set
+ chargingStation.siteArea = siteArea;
// Stop Transaction
const result = await new OCPPService(Configuration.getChargingStationConfig()).handleStopTransaction(
- {
- chargeBoxIdentity: chargingStation.id,
+ { // OCPP Header
+ chargeBoxIdentity: transaction.chargeBoxID,
chargingStation: chargingStation,
- companyID: chargingStation.companyID,
- siteID: chargingStation.siteID,
- siteAreaID: chargingStation.siteAreaID,
+ companyID: transaction.companyID,
+ siteID: transaction.siteID,
+ siteAreaID: transaction.siteAreaID,
tenantID: tenant.id,
tenant: tenant,
},
- {
+ { // OCPP Stop Transaction
transactionId: transaction.id,
- chargeBoxID: chargingStation.id,
+ chargeBoxID: transaction.chargeBoxID,
idTag: transaction.tagID,
timestamp: Utils.convertToDate(transaction.lastConsumption ? transaction.lastConsumption.timestamp : transaction.timestamp).toISOString(),
meterStop: transaction.lastConsumption ? transaction.lastConsumption.value : transaction.meterStart
},
true
);
- if (result.idTagInfo?.status !== OCPPAuthorizationStatus.ACCEPTED) {
- return false;
- }
- return true;
+ return (result.idTagInfo?.status === OCPPAuthorizationStatus.ACCEPTED);
}
private static async enrichChargingStationWithTemplate(tenant: Tenant, chargingStation: ChargingStation): Promise<TemplateUpdateResult> {

MODIFY
src/server/rest/v1/service/TransactionService.ts
src/server/rest/v1/service/TransactionService.ts
@@ -373,7 +373,7 @@ export default class TransactionService {
}
// Stop Transaction
const result = await new OCPPService(Configuration.getChargingStationConfig()).handleStopTransaction(
- {
+ { // OCPP Headers
chargeBoxIdentity: chargingStation.id,
chargingStation: chargingStation,
companyID: chargingStation.companyID,
@@ -382,7 +382,7 @@ export default class TransactionService {
tenantID: req.user.tenantID,
tenant: req.tenant,
},
- {
+ { // OCPP Stop Transaction
transactionId: transactionId,
chargeBoxID: chargingStation.id,
idTag: req.user.tagIDs[0],

MODIFY
src/storage/mongodb/TransactionStorage.ts
src/storage/mongodb/TransactionStorage.ts
@@ -303,7 +303,8 @@ export default class TransactionStorage {
endDateTime?: Date; stop?: any; minimalPrice?: boolean; reportIDs?: string[]; tagIDs?: string[]; inactivityStatus?: string[];
ocpiSessionID?: string; ocpiAuthorizationID?: string; ocpiSessionDateFrom?: Date; ocpiSessionDateTo?: Date; ocpiCdrDateFrom?: Date; ocpiCdrDateTo?: Date;
ocpiSessionChecked?: boolean; ocpiCdrChecked?: boolean; oicpSessionID?: string; withSite?: boolean; withSiteArea?: boolean; withCompany?: boolean;
- statistics?: 'refund' | 'history' | 'ongoing'; refundStatus?: RefundStatus[]; withTag?: boolean; hasUserID?: boolean; withUser?: boolean; withCar?: boolean; transactionsToClean?: boolean
+ statistics?: 'refund' | 'history' | 'ongoing'; refundStatus?: RefundStatus[]; withTag?: boolean; hasUserID?: boolean; withUser?: boolean; withCar?: boolean;
+ transactionsToClose?: boolean;
},
dbParams: DbParams, projectFields?: string[]): Promise<TransactionDataResult> {
const startTime = Logging.traceDatabaseRequestStart();
@@ -468,6 +469,10 @@ export default class TransactionStorage {
if (params.reportIDs) {
filters['refundData.reportId'] = { $in: params.reportIDs };
}
+ // Only opened Transactions
+ if (params.transactionsToClose) {
+ filters.stop = { $exists: false };
+ }
// Filters
if (ownerMatch.$or && ownerMatch.$or.length > 0) {
aggregation.push({
@@ -480,50 +485,18 @@ export default class TransactionStorage {
$match: filters
});
}
- // Charging Station
- if (params.withChargingStation || params.transactionsToClean) {
- DatabaseUtils.pushChargingStationLookupInAggregation({
- tenantID: tenant.id, aggregation: aggregation, localField: 'chargeBoxID', foreignField: '_id',
- asField: 'chargeBox', oneToOneCardinality: true, oneToOneCardinalityNotNull: false
- });
- DatabaseUtils.pushConvertObjectIDToString(aggregation, 'chargeBox.siteAreaID');
- // Add Connector and Status
- if ((projectFields && projectFields.includes('status')) || params.transactionsToClean) {
- aggregation.push({
- $addFields: {
- connector: {
- '$arrayElemAt': [
+ // Only Connector's Transaction ID !== Transaction ID
+ if (params.transactionsToClose) {
+ TransactionStorage.pushChargingStationInTransactionAggregation(
+ tenant, params, projectFields, aggregation);
+ aggregation.push(
{
- '$filter': {
- input: '$chargeBox.connectors',
- as: 'connector',
- cond: {
- $eq: [
- '$$connector.connectorId',
- '$connectorId'
- ]
- }
- }
- },
- 0
- ]
- }
- }
- });
- if (projectFields && projectFields.includes('status')) {
- aggregation.push({
- $addFields: { status: '$connector.status' }
- });
- }
- if (params.transactionsToClean) {
- aggregation.push({
'$addFields': {
'transactionIdEq': { '$eq': ['$connector.currentTransactionID', '$_id'] }
}
},
- { '$match': { 'transactionIdEq': false } });
- }
- }
+ { '$match': { 'transactionIdEq': false } }
+ );
}
// Limit records?
if (!dbParams.onlyRecordCount) {
@@ -687,6 +660,11 @@ export default class TransactionStorage {
}
});
}
+ // Charging Station
+ if (params.withChargingStation) {
+ TransactionStorage.pushChargingStationInTransactionAggregation(
+ tenant, params, projectFields, aggregation);
+ }
// Tag
if (params.withTag) {
DatabaseUtils.pushTagLookupInAggregation({
@@ -714,7 +692,7 @@ export default class TransactionStorage {
});
}
// Site Area
- if (params.withSiteArea) {
+ if (params.withSiteArea || params.transactionsToClose) {
DatabaseUtils.pushSiteAreaLookupInAggregation({
tenantID: tenant.id, aggregation: aggregation, localField: 'siteAreaID', foreignField: '_id',
asField: 'siteArea', oneToOneCardinality: true
@@ -893,7 +871,7 @@ export default class TransactionStorage {
};
}
- static async getTransactionsInError(tenant: Tenant,
+ public static async getTransactionsInError(tenant: Tenant,
params: {
search?: string; issuer?: boolean; userIDs?: string[]; chargingStationIDs?: string[];
siteAreaIDs?: string[]; siteIDs?: string[]; startDateTime?: Date; endDateTime?: Date;
@@ -1412,4 +1390,43 @@ export default class TransactionStorage {
return [];
}
}
+
+ private static pushChargingStationInTransactionAggregation(tenant: Tenant, params: any, projectFields: string[], aggregation: any[]) {
+ // Add Charging Station
+ DatabaseUtils.pushChargingStationLookupInAggregation({
+ tenantID: tenant.id, aggregation: aggregation, localField: 'chargeBoxID', foreignField: '_id',
+ asField: 'chargeBox', oneToOneCardinality: true, oneToOneCardinalityNotNull: false
+ });
+ DatabaseUtils.pushConvertObjectIDToString(aggregation, 'chargeBox.siteAreaID');
+ // Add Connector and Status
+ if ((projectFields && projectFields.includes('status')) || params.transactionsToClose) {
+ aggregation.push({
+ $addFields: {
+ connector: {
+ '$arrayElemAt': [
+ {
+ '$filter': {
+ input: '$chargeBox.connectors',
+ as: 'connector',
+ cond: {
+ $eq: [
+ '$$connector.connectorId',
+ '$connectorId'
+ ]
+ }
+ }
+ },
+ 0
+ ]
+ }
+ }
+ });
+ if (projectFields && projectFields.includes('status')) {
+ aggregation.push({
+ $addFields: { status: '$connector.status' }
+ });
+ }
+ }
+ params.withChargingStation = false;
+ }
}

Fixed scheduler task not returned by Scheduler manager
Get the Charging Station/Site when the transactions to close are retrieved from the DB and pass it to the softStopTransaction() method
Put join to charging station in storage after limit/sort if the transactionsToClose param is not provided
Renaming of Clean by Close
Simplified the code