From: Jérôme Benoit Date: Wed, 18 Mar 2026 18:32:25 +0000 (+0100) Subject: refactor(ocpp2): extract buildTransactionEvent as standalone function, remove dead... X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=0d25748bfdc427e530144246bf60e92d3abaea6d;p=e-mobility-charging-stations-simulator.git refactor(ocpp2): extract buildTransactionEvent as standalone function, remove dead context code Extract buildTransactionEvent from OCPP20ServiceUtils static method to exported standalone function, consistent with buildStatusNotificationRequest and buildMeterValue. Remove unused context overload from buildTransactionEvent and sendTransactionEvent — all production callers pass triggerReason directly. Remove dead code: OCPP20TransactionContext interface (not in OCPP 2.0.1 specs), selectTriggerReason method, TransactionContextFixtures, and associated tests. --- diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 40d5b8d3..313301de 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -1,7 +1,6 @@ import type { ValidateFunction } from 'ajv' import type { ChargingStation } from '../../../charging-station/index.js' -import type { OCPP20TransactionEventOptions } from '../../../types/ocpp/2.0/Transaction.js' import type { OCPPResponseService } from '../OCPPResponseService.js' import { OCPPError } from '../../../exception/index.js' @@ -13,8 +12,6 @@ import { type JsonType, OCPP20RequestCommand, type OCPP20SignCertificateRequest, - OCPP20TransactionEventEnumType, - OCPP20TriggerReasonEnumType, OCPPVersion, type RequestParams, } from '../../../types/index.js' @@ -23,7 +20,7 @@ import { OCPPRequestService } from '../OCPPRequestService.js' import { buildStatusNotificationRequest } from '../OCPPServiceUtils.js' import { generatePkcs10Csr } from './Asn1DerUtils.js' import { OCPP20Constants } from './OCPP20Constants.js' -import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' +import { buildTransactionEvent, OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' const moduleName = 'OCPP20RequestService' @@ -204,35 +201,8 @@ export class OCPP20RequestService extends OCPPRequestService { commandParams.status as ConnectorStatusEnum, commandParams.evseId as number | undefined ) as unknown as Request - case OCPP20RequestCommand.TRANSACTION_EVENT: { - const eventType = commandParams.eventType as OCPP20TransactionEventEnumType - const triggerReason: OCPP20TriggerReasonEnumType = - commandParams.triggerReason != null - ? (commandParams.triggerReason as OCPP20TriggerReasonEnumType) - : eventType === OCPP20TransactionEventEnumType.Ended - ? OCPP20TriggerReasonEnumType.RemoteStop - : OCPP20TriggerReasonEnumType.Authorized - const evse = commandParams.evse as undefined | { connectorId?: number; id?: number } - const connectorId: number = - commandParams.connectorId != null - ? (commandParams.connectorId as number) - : (evse?.connectorId ?? evse?.id ?? 1) - const transactionId: string = - commandParams.transactionId != null - ? (commandParams.transactionId as string) - : eventType === OCPP20TransactionEventEnumType.Ended - ? (chargingStation.getConnectorStatus(connectorId)?.transactionId?.toString() ?? - generateUUID()) - : generateUUID() - return OCPP20ServiceUtils.buildTransactionEvent( - chargingStation, - eventType, - triggerReason, - connectorId, - transactionId, - commandParams as unknown as OCPP20TransactionEventOptions - ) as unknown as Request - } + case OCPP20RequestCommand.TRANSACTION_EVENT: + return buildTransactionEvent(chargingStation, commandParams) as unknown as Request default: { // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). const errorMsg = `Unsupported OCPP command ${commandName as string} for payload building` diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 968ec58a..e684ec4a 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/unified-signatures */ - import { secondsToMilliseconds } from 'date-fns' import { type ChargingStation, resetConnectorStatus } from '../../../charging-station/index.js' @@ -7,6 +5,7 @@ import { OCPPError } from '../../../exception/index.js' import { ConnectorStatusEnum, ErrorType, + type JsonObject, OCPP20ComponentName, OCPP20IncomingRequestCommand, OCPP20RequestCommand, @@ -26,19 +25,18 @@ import { import { type OCPP20EVSEType, OCPP20ReasonEnumType, - type OCPP20TransactionContext, type OCPP20TransactionEventOptions, type OCPP20TransactionType, } from '../../../types/ocpp/2.0/Transaction.js' import { Constants, convertToIntOrNaN, + generateUUID, logger, validateIdentifierString, } from '../../../utils/index.js' import { getConfigurationKey } from '../../ConfigurationKeyUtils.js' import { OCPPServiceUtils, sendAndSetConnectorStatus } from '../OCPPServiceUtils.js' -import { OCPP20Constants } from './OCPP20Constants.js' import { OCPP20VariableManager } from './OCPP20VariableManager.js' const moduleName = 'OCPP20ServiceUtils' @@ -86,169 +84,6 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { [OCPP20RequestCommand.TRANSACTION_EVENT, 'TransactionEvent'], ] - /** - * Build a TransactionEvent request according to OCPP 2.0.1 specification - * - * This method creates a properly formatted TransactionEventRequest that complies with - * OCPP 2.0.1 requirements including F01, E01, E06, and TriggerReason specifications. - * - * Key features: - * - Automatic per-EVSE sequence number management - * - Full TriggerReason validation (21 enum values) - * - EVSE/connector mapping and validation - * - Transaction UUID handling - * - Comprehensive parameter validation - * @param chargingStation - The charging station instance - * @param eventType - Transaction event type (Started, Updated, Ended) - * @param triggerReason - Reason that triggered the event (21 OCPP 2.0.1 values) - * @param connectorId - Connector identifier - * @param transactionId - Transaction UUID (required for all events) - * @param options - Optional parameters for the transaction event - * @param options.evseId - * @param options.idToken - * @param options.meterValue - * @param options.chargingState - * @param options.stoppedReason - * @param options.remoteStartId - * @param options.cableMaxCurrent - * @param options.numberOfPhasesUsed - * @param options.offline - * @param options.reservationId - * @param options.customData - * @returns Promise - Built transaction event request - * @throws {OCPPError} When parameters are invalid or EVSE mapping fails - */ - public static buildTransactionEvent ( - chargingStation: ChargingStation, - eventType: OCPP20TransactionEventEnumType, - context: OCPP20TransactionContext, - connectorId: number, - transactionId: string, - options?: OCPP20TransactionEventOptions - ): OCPP20TransactionEventRequest - public static buildTransactionEvent ( - chargingStation: ChargingStation, - eventType: OCPP20TransactionEventEnumType, - triggerReason: OCPP20TriggerReasonEnumType, - connectorId: number, - transactionId: string, - options?: OCPP20TransactionEventOptions - ): OCPP20TransactionEventRequest - public static buildTransactionEvent ( - chargingStation: ChargingStation, - eventType: OCPP20TransactionEventEnumType, - triggerReasonOrContext: OCPP20TransactionContext | OCPP20TriggerReasonEnumType, - connectorId: number, - transactionId: string, - options: OCPP20TransactionEventOptions = {} - ): OCPP20TransactionEventRequest { - const isContext = typeof triggerReasonOrContext === 'object' - const triggerReason = isContext - ? this.selectTriggerReason(eventType, triggerReasonOrContext) - : triggerReasonOrContext - - // Validate transaction ID format (must be non-empty string ≤36 characters per OCPP 2.0.1) - if (!validateIdentifierString(transactionId, 36)) { - const errorMsg = `Invalid transaction ID format (must be non-empty string ≤36 characters): ${transactionId}` - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.buildTransactionEvent: ${errorMsg}` - ) - throw new OCPPError(ErrorType.PROPERTY_CONSTRAINT_VIOLATION, errorMsg) - } - - const evseId = options.evseId ?? chargingStation.getEvseIdByConnectorId(connectorId) - if (evseId == null) { - const errorMsg = `Cannot find EVSE ID for connector ${connectorId.toString()}` - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.buildTransactionEvent: ${errorMsg}` - ) - throw new OCPPError(ErrorType.PROPERTY_CONSTRAINT_VIOLATION, errorMsg) - } - - const connectorStatus = chargingStation.getConnectorStatus(connectorId) - if (connectorStatus == null) { - const errorMsg = `Cannot find connector status for connector ${connectorId.toString()}` - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.buildTransactionEvent: ${errorMsg}` - ) - throw new OCPPError(ErrorType.PROPERTY_CONSTRAINT_VIOLATION, errorMsg) - } - - // Per-EVSE sequence number management (OCPP 2.0.1 §1.3.2.1) - if (connectorStatus.transactionSeqNo == null) { - connectorStatus.transactionSeqNo = 0 - } else { - connectorStatus.transactionSeqNo = connectorStatus.transactionSeqNo + 1 - } - - // Build EVSE object (E01.FR.16: only include in first TransactionEvent after EV connected) - let evse: OCPP20EVSEType | undefined - if (connectorStatus.transactionEvseSent !== true) { - evse = { id: evseId } - if (connectorId !== evseId) { - evse.connectorId = connectorId - } - connectorStatus.transactionEvseSent = true - } - - const transactionInfo: OCPP20TransactionType = { - transactionId: transactionId as UUIDv4, - } - - if (options.chargingState !== undefined) { - transactionInfo.chargingState = options.chargingState - } - if (options.stoppedReason !== undefined) { - transactionInfo.stoppedReason = options.stoppedReason - } - if (options.remoteStartId !== undefined) { - transactionInfo.remoteStartId = options.remoteStartId - } - - const transactionEventRequest: OCPP20TransactionEventRequest = { - eventType, - seqNo: connectorStatus.transactionSeqNo, - timestamp: new Date(), - transactionInfo, - triggerReason, - } - - // E01.FR.16: Include evse only in first TransactionEvent - if (evse !== undefined) { - transactionEventRequest.evse = evse - } - - // E03.FR.01: Include idToken only once per transaction (first event after authorization) - if (options.idToken !== undefined && connectorStatus.transactionIdTokenSent !== true) { - transactionEventRequest.idToken = options.idToken - connectorStatus.transactionIdTokenSent = true - } - if (options.meterValue !== undefined && options.meterValue.length > 0) { - transactionEventRequest.meterValue = options.meterValue - } - if (options.cableMaxCurrent !== undefined) { - transactionEventRequest.cableMaxCurrent = options.cableMaxCurrent - } - if (options.numberOfPhasesUsed !== undefined) { - transactionEventRequest.numberOfPhasesUsed = options.numberOfPhasesUsed - } - if (options.offline !== undefined) { - transactionEventRequest.offline = options.offline - } - if (options.reservationId !== undefined) { - transactionEventRequest.reservationId = options.reservationId - } - if (options.customData !== undefined) { - transactionEventRequest.customData = options.customData - } - - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.buildTransactionEvent: Building TransactionEvent for trigger ${triggerReason}` - ) - - return transactionEventRequest - } - /** * OCPP 2.0 Incoming Request Service validator configurations * @returns Array of validator configuration tuples @@ -535,146 +370,6 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } - /** - * Intelligently select appropriate TriggerReason based on transaction context - * - * This method implements the E02.FR.17 requirement for context-aware TriggerReason selection. - * It analyzes the transaction context to determine the most appropriate TriggerReason according - * to OCPP 2.0.1 specification and best practices. - * - * Selection Logic (by priority): - * 1. Remote commands (RequestStartTransaction, RequestStopTransaction, etc.) -> RemoteStart/RemoteStop - * 2. Authorization events (token presented) -> Authorized/StopAuthorized/Deauthorized - * 3. Cable physical actions -> CablePluggedIn - * 4. Charging state transitions -> ChargingStateChanged - * 5. System events (EV detection, communication) -> EVDetected/EVDeparted/EVCommunicationLost - * 6. Meter value events -> MeterValuePeriodic/MeterValueClock - * 7. Energy/Time limits -> EnergyLimitReached/TimeLimitReached - * 8. Abnormal conditions -> AbnormalCondition - * @param eventType - The transaction event type (Started, Updated, Ended) - * @param context - Context information describing the trigger source and details - * @returns OCPP20TriggerReasonEnumType - The most appropriate trigger reason - */ - public static selectTriggerReason ( - eventType: OCPP20TransactionEventEnumType, - context: OCPP20TransactionContext - ): OCPP20TriggerReasonEnumType { - const candidates = OCPP20Constants.TriggerReasonMapping.filter( - entry => entry.source === context.source - ) - - for (const entry of candidates) { - if (context.source === 'remote_command' && context.command != null) { - if ( - (context.command === OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION && - entry.triggerReason === OCPP20TriggerReasonEnumType.RemoteStart) || - (context.command === OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION && - entry.triggerReason === OCPP20TriggerReasonEnumType.RemoteStop) || - (context.command === OCPP20IncomingRequestCommand.RESET && - entry.triggerReason === OCPP20TriggerReasonEnumType.ResetCommand) || - (context.command === OCPP20IncomingRequestCommand.TRIGGER_MESSAGE && - entry.triggerReason === OCPP20TriggerReasonEnumType.Trigger) || - (context.command === OCPP20IncomingRequestCommand.UNLOCK_CONNECTOR && - entry.triggerReason === OCPP20TriggerReasonEnumType.UnlockCommand) - ) { - return entry.triggerReason - } - } - - if (context.source === 'local_authorization' && context.authorizationMethod != null) { - if (context.isDeauthorized === true) { - if (entry.triggerReason === OCPP20TriggerReasonEnumType.Deauthorized) { - return entry.triggerReason - } - } else if ( - (context.authorizationMethod === 'groupIdToken' || - context.authorizationMethod === 'idToken') && - entry.triggerReason === OCPP20TriggerReasonEnumType.Authorized - ) { - return entry.triggerReason - } else if ( - context.authorizationMethod === 'stopAuthorized' && - entry.triggerReason === OCPP20TriggerReasonEnumType.StopAuthorized - ) { - return entry.triggerReason - } - } - - if (context.source === 'cable_action' && context.cableState != null) { - if ( - (context.cableState === 'detected' && - entry.triggerReason === OCPP20TriggerReasonEnumType.EVDetected) || - (context.cableState === 'plugged_in' && - entry.triggerReason === OCPP20TriggerReasonEnumType.CablePluggedIn) || - (context.cableState === 'unplugged' && - entry.triggerReason === OCPP20TriggerReasonEnumType.EVDeparted) - ) { - return entry.triggerReason - } - } - - if ( - context.source === 'charging_state' && - context.chargingStateChange != null && - entry.triggerReason === OCPP20TriggerReasonEnumType.ChargingStateChanged - ) { - return entry.triggerReason - } - - if (context.source === 'system_event' && context.systemEvent != null) { - if ( - (context.systemEvent === 'ev_communication_lost' && - entry.triggerReason === OCPP20TriggerReasonEnumType.EVCommunicationLost) || - (context.systemEvent === 'ev_connect_timeout' && - entry.triggerReason === OCPP20TriggerReasonEnumType.EVConnectTimeout) || - (context.systemEvent === 'ev_departed' && - entry.triggerReason === OCPP20TriggerReasonEnumType.EVDeparted) || - (context.systemEvent === 'ev_detected' && - entry.triggerReason === OCPP20TriggerReasonEnumType.EVDetected) - ) { - return entry.triggerReason - } - } - - if (context.source === 'meter_value') { - if ( - (context.isSignedDataReceived === true && - entry.triggerReason === OCPP20TriggerReasonEnumType.SignedDataReceived) || - (context.isPeriodicMeterValue === true && - entry.triggerReason === OCPP20TriggerReasonEnumType.MeterValuePeriodic) || - (context.isSignedDataReceived !== true && - context.isPeriodicMeterValue !== true && - entry.triggerReason === OCPP20TriggerReasonEnumType.MeterValueClock) - ) { - return entry.triggerReason - } - } - - if ( - (context.source === 'energy_limit' && - entry.triggerReason === OCPP20TriggerReasonEnumType.EnergyLimitReached) || - (context.source === 'time_limit' && - entry.triggerReason === OCPP20TriggerReasonEnumType.TimeLimitReached) || - (context.source === 'external_limit' && - entry.triggerReason === OCPP20TriggerReasonEnumType.ChargingRateChanged) - ) { - return entry.triggerReason - } - - if ( - context.source === 'abnormal_condition' && - entry.triggerReason === OCPP20TriggerReasonEnumType.AbnormalCondition - ) { - return entry.triggerReason - } - } - - logger.warn( - `${moduleName}.selectTriggerReason: No matching context found for source '${context.source}', defaulting to Trigger` - ) - return OCPP20TriggerReasonEnumType.Trigger - } - public static async sendQueuedTransactionEvents ( chargingStation: ChargingStation, connectorId: number @@ -715,38 +410,15 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } - public static async sendTransactionEvent ( - chargingStation: ChargingStation, - eventType: OCPP20TransactionEventEnumType, - context: OCPP20TransactionContext, - connectorId: number, - transactionId: string, - options?: OCPP20TransactionEventOptions - ): Promise public static async sendTransactionEvent ( chargingStation: ChargingStation, eventType: OCPP20TransactionEventEnumType, triggerReason: OCPP20TriggerReasonEnumType, connectorId: number, transactionId: string, - options?: OCPP20TransactionEventOptions - ): Promise - // Implementation with union type + type guard - public static async sendTransactionEvent ( - chargingStation: ChargingStation, - eventType: OCPP20TransactionEventEnumType, - triggerReasonOrContext: OCPP20TransactionContext | OCPP20TriggerReasonEnumType, - connectorId: number, - transactionId: string, options: OCPP20TransactionEventOptions = {} ): Promise { try { - // Type guard: distinguish between context object and direct trigger reason - const isContext = typeof triggerReasonOrContext === 'object' - const triggerReason = isContext - ? this.selectTriggerReason(eventType, triggerReasonOrContext) - : triggerReasonOrContext - const connectorStatus = chargingStation.getConnectorStatus(connectorId) if (connectorStatus == null) { const errorMsg = `Cannot find connector status for connector ${connectorId.toString()}` @@ -758,7 +430,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { // Offline: build and queue pre-built payload (sent as-is via rawPayload on reconnect) if (!chargingStation.isWebSocketConnectionOpened()) { - const transactionEventRequest = OCPP20ServiceUtils.buildTransactionEvent( + const transactionEventRequest = buildTransactionEvent( chargingStation, eventType, triggerReason, @@ -804,3 +476,161 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } } +export function buildTransactionEvent ( + chargingStation: ChargingStation, + eventType: OCPP20TransactionEventEnumType, + triggerReason: OCPP20TriggerReasonEnumType, + connectorId: number, + transactionId: string, + options?: OCPP20TransactionEventOptions +): OCPP20TransactionEventRequest +export function buildTransactionEvent ( + chargingStation: ChargingStation, + commandParams: JsonObject +): OCPP20TransactionEventRequest +/** + * + * @param chargingStation + * @param eventTypeOrParams + * @param triggerReasonArg + * @param connectorIdArg + * @param transactionIdArg + * @param options + */ +export function buildTransactionEvent ( + chargingStation: ChargingStation, + eventTypeOrParams: JsonObject | OCPP20TransactionEventEnumType, + triggerReasonArg?: OCPP20TriggerReasonEnumType, + connectorIdArg?: number, + transactionIdArg?: string, + options: OCPP20TransactionEventOptions = {} +): OCPP20TransactionEventRequest { + let eventType: OCPP20TransactionEventEnumType + let triggerReason: OCPP20TriggerReasonEnumType + let connectorId: number + let transactionId: string + + if (typeof eventTypeOrParams === 'object') { + const params = eventTypeOrParams + eventType = params.eventType as OCPP20TransactionEventEnumType + triggerReason = + params.triggerReason != null + ? (params.triggerReason as OCPP20TriggerReasonEnumType) + : eventType === OCPP20TransactionEventEnumType.Ended + ? OCPP20TriggerReasonEnumType.RemoteStop + : OCPP20TriggerReasonEnumType.Authorized + const evse = params.evse as undefined | { connectorId?: number; id?: number } + connectorId = + params.connectorId != null + ? (params.connectorId as number) + : (evse?.connectorId ?? evse?.id ?? 1) + transactionId = + params.transactionId != null + ? (params.transactionId as string) + : eventType === OCPP20TransactionEventEnumType.Ended + ? (chargingStation.getConnectorStatus(connectorId)?.transactionId?.toString() ?? + generateUUID()) + : generateUUID() + options = params as unknown as OCPP20TransactionEventOptions + } else { + eventType = eventTypeOrParams + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + triggerReason = triggerReasonArg! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + connectorId = connectorIdArg! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + transactionId = transactionIdArg! + } + + if (!validateIdentifierString(transactionId, 36)) { + const errorMsg = `Invalid transaction ID format (must be non-empty string ≤36 characters): ${transactionId}` + logger.error(`${chargingStation.logPrefix()} ${moduleName}.buildTransactionEvent: ${errorMsg}`) + throw new OCPPError(ErrorType.PROPERTY_CONSTRAINT_VIOLATION, errorMsg) + } + + const evseId = options.evseId ?? chargingStation.getEvseIdByConnectorId(connectorId) + if (evseId == null) { + const errorMsg = `Cannot find EVSE ID for connector ${connectorId.toString()}` + logger.error(`${chargingStation.logPrefix()} ${moduleName}.buildTransactionEvent: ${errorMsg}`) + throw new OCPPError(ErrorType.PROPERTY_CONSTRAINT_VIOLATION, errorMsg) + } + + const connectorStatus = chargingStation.getConnectorStatus(connectorId) + if (connectorStatus == null) { + const errorMsg = `Cannot find connector status for connector ${connectorId.toString()}` + logger.error(`${chargingStation.logPrefix()} ${moduleName}.buildTransactionEvent: ${errorMsg}`) + throw new OCPPError(ErrorType.PROPERTY_CONSTRAINT_VIOLATION, errorMsg) + } + + if (connectorStatus.transactionSeqNo == null) { + connectorStatus.transactionSeqNo = 0 + } else { + connectorStatus.transactionSeqNo = connectorStatus.transactionSeqNo + 1 + } + + // E01.FR.16: only include EVSE in first TransactionEvent + let evse: OCPP20EVSEType | undefined + if (connectorStatus.transactionEvseSent !== true) { + evse = { id: evseId } + if (connectorId !== evseId) { + evse.connectorId = connectorId + } + connectorStatus.transactionEvseSent = true + } + + const transactionInfo: OCPP20TransactionType = { + transactionId: transactionId as UUIDv4, + } + + if (options.chargingState !== undefined) { + transactionInfo.chargingState = options.chargingState + } + if (options.stoppedReason !== undefined) { + transactionInfo.stoppedReason = options.stoppedReason + } + if (options.remoteStartId !== undefined) { + transactionInfo.remoteStartId = options.remoteStartId + } + + const transactionEventRequest: OCPP20TransactionEventRequest = { + eventType, + seqNo: connectorStatus.transactionSeqNo, + timestamp: new Date(), + transactionInfo, + triggerReason, + } + + if (evse !== undefined) { + transactionEventRequest.evse = evse + } + + // E03.FR.01: Include idToken only once per transaction + if (options.idToken !== undefined && connectorStatus.transactionIdTokenSent !== true) { + transactionEventRequest.idToken = options.idToken + connectorStatus.transactionIdTokenSent = true + } + if (options.meterValue !== undefined && options.meterValue.length > 0) { + transactionEventRequest.meterValue = options.meterValue + } + if (options.cableMaxCurrent !== undefined) { + transactionEventRequest.cableMaxCurrent = options.cableMaxCurrent + } + if (options.numberOfPhasesUsed !== undefined) { + transactionEventRequest.numberOfPhasesUsed = options.numberOfPhasesUsed + } + if (options.offline !== undefined) { + transactionEventRequest.offline = options.offline + } + if (options.reservationId !== undefined) { + transactionEventRequest.reservationId = options.reservationId + } + if (options.customData !== undefined) { + transactionEventRequest.customData = options.customData + } + + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.buildTransactionEvent: Building TransactionEvent for trigger ${triggerReason}` + ) + + return transactionEventRequest +} diff --git a/src/charging-station/ocpp/index.ts b/src/charging-station/ocpp/index.ts index 0aab4035..960b0952 100644 --- a/src/charging-station/ocpp/index.ts +++ b/src/charging-station/ocpp/index.ts @@ -8,13 +8,14 @@ export { OCPP16ResponseService } from './1.6/OCPP16ResponseService.js' export { OCPP20IncomingRequestService } from './2.0/OCPP20IncomingRequestService.js' export { OCPP20RequestService } from './2.0/OCPP20RequestService.js' export { OCPP20ResponseService } from './2.0/OCPP20ResponseService.js' -export { OCPP20ServiceUtils } from './2.0/OCPP20ServiceUtils.js' +export { buildTransactionEvent, OCPP20ServiceUtils } from './2.0/OCPP20ServiceUtils.js' export { OCPP20VariableManager } from './2.0/OCPP20VariableManager.js' export { OCPPAuthServiceFactory } from './auth/services/OCPPAuthServiceFactory.js' export { OCPPIncomingRequestService } from './OCPPIncomingRequestService.js' export { OCPPRequestService } from './OCPPRequestService.js' export { buildMeterValue, + buildStatusNotificationRequest, buildTransactionEndMeterValue, getMessageTypeString, isIdTagAuthorized, diff --git a/src/types/ocpp/2.0/Transaction.ts b/src/types/ocpp/2.0/Transaction.ts index bf11c9d5..415efe47 100644 --- a/src/types/ocpp/2.0/Transaction.ts +++ b/src/types/ocpp/2.0/Transaction.ts @@ -2,7 +2,6 @@ import type { JsonObject } from '../../JsonType.js' import type { UUIDv4 } from '../../UUID.js' import type { CustomDataType } from './Common.js' import type { OCPP20MeterValue } from './MeterValues.js' -import type { OCPP20IncomingRequestCommand } from './Requests.js' export enum CostKindEnumType { CarbonDioxideEmission = 'CarbonDioxideEmission', @@ -267,58 +266,6 @@ export interface OCPP20MessageContentType extends JsonObject { format: OCPP20MessageFormatEnumType language?: string } - -/** - * Context information for intelligent TriggerReason selection - * Used by OCPP20ServiceUtils.selectTriggerReason() to determine appropriate trigger reason - */ -export interface OCPP20TransactionContext { - /** Abnormal condition type (for abnormal_condition source) */ - abnormalCondition?: string - - /** Authorization method used (for local_authorization source) */ - authorizationMethod?: 'groupIdToken' | 'idToken' | 'stopAuthorized' - - /** Cable connection state (for cable_action source) */ - cableState?: 'detected' | 'plugged_in' | 'unplugged' - - /** Charging state change details (for charging_state source) */ - chargingStateChange?: { - from?: OCPP20ChargingStateEnumType - to?: OCPP20ChargingStateEnumType - } - - /** Specific command that triggered the event (for remote_command source) */ - command?: OCPP20IncomingRequestCommand - - hasRemoteStartId?: boolean - - isDeauthorized?: boolean - - /** Additional context flags */ - isOffline?: boolean - - /** Whether this is a periodic meter value event */ - isPeriodicMeterValue?: boolean - - /** Whether this is a signed data reception event */ - isSignedDataReceived?: boolean - /** Source of the transaction event - command, authorization, physical action, etc. */ - source: - | 'abnormal_condition' - | 'cable_action' - | 'charging_state' - | 'energy_limit' - | 'external_limit' - | 'local_authorization' - | 'meter_value' - | 'remote_command' - | 'system_event' - | 'time_limit' - /** System event details (for system_event source) */ - systemEvent?: 'ev_communication_lost' | 'ev_connect_timeout' | 'ev_departed' | 'ev_detected' -} - /** * Optional parameters for building and sending TransactionEvent requests. * Aligned with OCPP 2.0.1 TransactionEvent.req optional fields. diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts index dd1d4ac5..88cbc11a 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts @@ -16,20 +16,21 @@ import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js' import type { EmptyObject } from '../../../../src/types/index.js' -import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js' +import { + buildTransactionEvent, + OCPP20ServiceUtils, +} from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js' import { ConnectorStatusEnum, OCPP20TransactionEventEnumType, OCPP20TriggerReasonEnumType, OCPPVersion, } from '../../../../src/types/index.js' -import { OCPP20IncomingRequestCommand } from '../../../../src/types/ocpp/2.0/Requests.js' import { OCPP20ChargingStateEnumType, OCPP20IdTokenEnumType, type OCPP20IdTokenType, OCPP20ReasonEnumType, - type OCPP20TransactionContext, type OCPP20TransactionType, } from '../../../../src/types/ocpp/2.0/Transaction.js' import { Constants, generateUUID } from '../../../../src/utils/index.js' @@ -42,7 +43,6 @@ import { type MockStationWithTracking, resetConnectorTransactionState, resetLimits, - TransactionContextFixtures, } from './OCPP20TestUtils.js' // ============================================================================ // Transaction Flow Patterns for Parameterized Testing @@ -59,7 +59,6 @@ const TRANSACTION_FLOWS = [ id: 'cableFirst', includeIdToken: false, name: 'E02 - Cable-First', - startContext: TransactionContextFixtures.cablePluggedIn(), }, { description: 'E03 IdToken-First', @@ -67,7 +66,6 @@ const TRANSACTION_FLOWS = [ id: 'idTokenFirst', includeIdToken: true, name: 'E03 - IdToken-First', - startContext: TransactionContextFixtures.idTokenAuthorized(), }, { description: 'Remote Start', @@ -75,7 +73,6 @@ const TRANSACTION_FLOWS = [ id: 'remoteStart', includeIdToken: false, name: 'Remote Start', - startContext: TransactionContextFixtures.remoteStart(), }, ] as const @@ -116,7 +113,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { // Reset sequence number to simulate new transaction OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent( + const transactionEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, triggerReason, @@ -151,7 +148,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // Build first event (Started) - const startEvent = OCPP20ServiceUtils.buildTransactionEvent( + const startEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -160,7 +157,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // Build second event (Updated) - const updateEvent = OCPP20ServiceUtils.buildTransactionEvent( + const updateEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.MeterValuePeriodic, @@ -169,7 +166,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // Build third event (Ended) - const endEvent = OCPP20ServiceUtils.buildTransactionEvent( + const endEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Ended, OCPP20TriggerReasonEnumType.StopAuthorized, @@ -205,7 +202,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { reservationId: 67890, } - const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent( + const transactionEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, @@ -237,7 +234,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { 'this-string-is-way-too-long-for-a-valid-transaction-id-exceeds-36-chars' try { - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -284,7 +281,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) for (const triggerReason of triggerReasons) { - const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent( + const transactionEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, triggerReason, @@ -360,7 +357,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { const connectorId = 1 // First, build a transaction event to set sequence number - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -397,7 +394,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent( + const transactionEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -458,7 +455,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent( + const transactionEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -478,626 +475,276 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { }) }) - // FR: E01.FR.04 - TriggerReason selection based on transaction context - await describe('Context-Aware TriggerReason Selection', async () => { - await describe('selectTriggerReason', async () => { - await it('should select RemoteStart for remote_command context with RequestStartTransaction', () => { - const context: OCPP20TransactionContext = { - command: OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION, - source: 'remote_command', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.RemoteStart) - }) - - await it('should select RemoteStop for remote_command context with RequestStopTransaction', () => { - const context: OCPP20TransactionContext = { - command: OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION, - source: 'remote_command', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.RemoteStop) - }) - - await it('should select UnlockCommand for remote_command context with UnlockConnector', () => { - const context: OCPP20TransactionContext = { - command: OCPP20IncomingRequestCommand.UNLOCK_CONNECTOR, - source: 'remote_command', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Updated, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.UnlockCommand) - }) - - await it('should select ResetCommand for remote_command context with Reset', () => { - const context: OCPP20TransactionContext = { - command: OCPP20IncomingRequestCommand.RESET, - source: 'remote_command', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.ResetCommand) - }) - - await it('should select Trigger for remote_command context with TriggerMessage', () => { - const context: OCPP20TransactionContext = { - command: OCPP20IncomingRequestCommand.TRIGGER_MESSAGE, - source: 'remote_command', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Updated, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.Trigger) - }) - - await it('should select Authorized for local_authorization context with idToken', () => { - const context: OCPP20TransactionContext = { - authorizationMethod: 'idToken', - source: 'local_authorization', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.Authorized) - }) - - await it('should select StopAuthorized for local_authorization context with stopAuthorized', () => { - const context: OCPP20TransactionContext = { - authorizationMethod: 'stopAuthorized', - source: 'local_authorization', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.StopAuthorized) - }) - - await it('should select Deauthorized when isDeauthorized flag is true', () => { - const context: OCPP20TransactionContext = { - authorizationMethod: 'idToken', - isDeauthorized: true, - source: 'local_authorization', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.Deauthorized) - }) - - await it('should select ChargingStateChanged for charging_state context', () => { - const context: OCPP20TransactionContext = { - chargingStateChange: { - from: OCPP20ChargingStateEnumType.Idle, - to: OCPP20ChargingStateEnumType.Charging, - }, - source: 'charging_state', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Updated, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.ChargingStateChanged) - }) - - await it('should select MeterValuePeriodic for meter_value context with periodic flag', () => { - const context: OCPP20TransactionContext = { - isPeriodicMeterValue: true, - source: 'meter_value', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Updated, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.MeterValuePeriodic) - }) - - await it('should select MeterValueClock for meter_value context without periodic flag', () => { - const context: OCPP20TransactionContext = { - isPeriodicMeterValue: false, - source: 'meter_value', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Updated, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.MeterValueClock) - }) - - await it('should select SignedDataReceived when isSignedDataReceived flag is true', () => { - const context: OCPP20TransactionContext = { - isSignedDataReceived: true, - source: 'meter_value', - } + await describe('sendTransactionEvent with context parameter', async () => { + await it('should send TransactionEvent with context-aware TriggerReason selection', async () => { + const connectorId = 1 + const transactionId = generateUUID() - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Updated, - context - ) + const response = await OCPP20ServiceUtils.sendTransactionEvent( + mockStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.CablePluggedIn, + connectorId, + transactionId + ) - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.SignedDataReceived) - }) + // Validate response structure + assert.notStrictEqual(response, undefined) + assert.strictEqual(typeof response, 'object') + }) - await it('should select appropriate system events for system_event context', () => { - const testCases = [ - { - expected: OCPP20TriggerReasonEnumType.EVDeparted, - systemEvent: 'ev_departed' as const, - }, - { - expected: OCPP20TriggerReasonEnumType.EVDetected, - systemEvent: 'ev_detected' as const, - }, - { - expected: OCPP20TriggerReasonEnumType.EVCommunicationLost, - systemEvent: 'ev_communication_lost' as const, - }, - { - expected: OCPP20TriggerReasonEnumType.EVConnectTimeout, - systemEvent: 'ev_connect_timeout' as const, + await it('should handle context-aware error scenarios gracefully', async () => { + // Create error mock for this test + const { station: errorMockChargingStation } = createMockChargingStation({ + baseName: TEST_CHARGING_STATION_BASE_NAME, + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, + ocppRequestService: { + requestHandler: () => { + throw new Error('Context test error') }, - ] - - for (const testCase of testCases) { - const context: OCPP20TransactionContext = { - source: 'system_event', - systemEvent: testCase.systemEvent, - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Updated, - context - ) - - assert.strictEqual(triggerReason, testCase.expected) - } + }, + stationInfo: { + ocppStrictCompliance: true, + ocppVersion: OCPPVersion.VERSION_201, + }, + websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) - await it('should select EnergyLimitReached for energy_limit context', () => { - const context: OCPP20TransactionContext = { - source: 'energy_limit', - } + const connectorId = 1 + const transactionId = generateUUID() - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( + try { + await OCPP20ServiceUtils.sendTransactionEvent( + errorMockChargingStation, OCPP20TransactionEventEnumType.Ended, - context + OCPP20TriggerReasonEnumType.AbnormalCondition, + connectorId, + transactionId ) + throw new Error('Should have thrown error') + } catch (error) { + assert.ok((error as Error).message.includes('Context test error')) + } + }) + }) - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.EnergyLimitReached) - }) + await describe('Backward Compatibility', async () => { + await it('should maintain compatibility with existing buildTransactionEvent calls', () => { + const connectorId = 1 + const transactionId = generateUUID() - await it('should select TimeLimitReached for time_limit context', () => { - const context: OCPP20TransactionContext = { - source: 'time_limit', - } + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - context - ) + // Old method call should still work + const oldEvent = buildTransactionEvent( + mockStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId + ) - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.TimeLimitReached) - }) + assert.strictEqual(oldEvent.eventType, OCPP20TransactionEventEnumType.Started) + assert.strictEqual(oldEvent.triggerReason, OCPP20TriggerReasonEnumType.Authorized) + assert.strictEqual(oldEvent.seqNo, 0) + }) - await it('should select AbnormalCondition for abnormal_condition context', () => { - const context: OCPP20TransactionContext = { - abnormalCondition: 'OverCurrent', - source: 'abnormal_condition', - } + await it('should maintain compatibility with existing sendTransactionEvent calls', async () => { + const connectorId = 1 + const transactionId = generateUUID() - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - context - ) + // Old method call should still work + const response = await OCPP20ServiceUtils.sendTransactionEvent( + mockStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId + ) - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.AbnormalCondition) - }) + assert.notStrictEqual(response, undefined) + assert.strictEqual(typeof response, 'object') + }) + }) + }) - await it('should handle priority ordering with multiple applicable contexts', () => { - // Test context with multiple applicable triggers - priority should be respected - const context: OCPP20TransactionContext = { - cableState: 'plugged_in', // Even lower priority - command: OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION, - isDeauthorized: true, // Lower priority but should be overridden - source: 'remote_command', // High priority - } + // ========================================================================== + // Parameterized Transaction Flow Tests (E02, E03, Remote Start) + // ========================================================================== + await describe('Transaction Flow Patterns', async () => { + let mockStation: ChargingStation - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - context - ) + beforeEach(() => { + const { station } = createMockChargingStation({ + baseName: TEST_CHARGING_STATION_BASE_NAME, + connectorsCount: 3, + evseConfiguration: { evsesCount: 3 }, + heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, + ocppRequestService: { + requestHandler: async () => Promise.resolve({} as EmptyObject), + }, + stationInfo: { + ocppStrictCompliance: true, + ocppVersion: OCPPVersion.VERSION_201, + }, + websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, + }) + mockStation = station + resetLimits(mockStation) + }) - // Should select RemoteStart (priority 1) over Deauthorized (priority 2) or CablePluggedIn (priority 3) - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.RemoteStart) - }) + afterEach(() => { + standardCleanup() + }) - await it('should fallback to Trigger for unknown context source', () => { - const context: OCPP20TransactionContext = { - source: 'unknown_source' as OCPP20TransactionContext['source'], // Invalid source to test fallback - } + for (const { + description, + expectedStartTrigger, + id, + includeIdToken, + name, + } of TRANSACTION_FLOWS) { + await describe(`${name} Flow`, async () => { + await it(`should build correct Started event for ${description}`, () => { + const connectorId = 1 + const transactionId = generateUUID() + const idToken: OCPP20IdTokenType | undefined = includeIdToken + ? { idToken: `${id.toUpperCase()}_TOKEN_001`, type: OCPP20IdTokenEnumType.ISO14443 } + : undefined + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( + const startedEvent = buildTransactionEvent( + mockStation, OCPP20TransactionEventEnumType.Started, - context + expectedStartTrigger, + connectorId, + transactionId, + idToken != null ? { idToken } : undefined ) - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.Trigger) - }) + assert.strictEqual(startedEvent.eventType, OCPP20TransactionEventEnumType.Started) + assert.strictEqual(startedEvent.triggerReason, expectedStartTrigger) + assert.strictEqual(startedEvent.seqNo, 0) + assert.strictEqual(startedEvent.transactionInfo.transactionId, transactionId) - await it('should fallback to Trigger for incomplete context', () => { - const context: OCPP20TransactionContext = { - source: 'remote_command', - // Missing command field + if (includeIdToken) { + assert.notStrictEqual(startedEvent.idToken, undefined) + assert.strictEqual(startedEvent.idToken?.idToken, `${id.toUpperCase()}_TOKEN_001`) } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.Trigger) }) - }) - await describe('buildTransactionEvent with context parameter', async () => { - await it('should build TransactionEvent with auto-selected TriggerReason from context', () => { + await it(`should support complete ${description} transaction lifecycle`, () => { const connectorId = 1 const transactionId = generateUUID() - const context: OCPP20TransactionContext = { - command: OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION, - source: 'remote_command', - } + const idToken: OCPP20IdTokenType | undefined = includeIdToken + ? { + idToken: `${id.toUpperCase()}_LIFECYCLE_001`, + type: OCPP20IdTokenEnumType.ISO14443, + } + : undefined OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent( + // Step 1: Started event + const startedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, - context, + expectedStartTrigger, connectorId, - transactionId - ) - - assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Started) - assert.strictEqual( - transactionEvent.triggerReason, - OCPP20TriggerReasonEnumType.RemoteStart + transactionId, + idToken != null ? { idToken } : undefined ) - assert.strictEqual(transactionEvent.seqNo, 0) - assert.strictEqual(transactionEvent.transactionInfo.transactionId, transactionId) - }) - - await it('should pass through optional parameters correctly', () => { - const connectorId = 2 - const transactionId = generateUUID() - const context: OCPP20TransactionContext = { - authorizationMethod: 'idToken', - source: 'local_authorization', - } - const options = { - chargingState: OCPP20ChargingStateEnumType.Charging, - idToken: { - idToken: 'CONTEXT_TEST_TOKEN', - type: OCPP20IdTokenEnumType.ISO14443, - }, - } - const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent( + // Step 2: Charging state change + const chargingEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, - context, + OCPP20TriggerReasonEnumType.ChargingStateChanged, connectorId, transactionId, - options - ) - - assert.strictEqual(transactionEvent.triggerReason, OCPP20TriggerReasonEnumType.Authorized) - assert.strictEqual(transactionEvent.idToken?.idToken, 'CONTEXT_TEST_TOKEN') - assert.strictEqual( - transactionEvent.transactionInfo.chargingState, - OCPP20ChargingStateEnumType.Charging + { chargingState: OCPP20ChargingStateEnumType.Charging } ) - }) - }) - - await describe('sendTransactionEvent with context parameter', async () => { - await it('should send TransactionEvent with context-aware TriggerReason selection', async () => { - const connectorId = 1 - const transactionId = generateUUID() - const context: OCPP20TransactionContext = { - cableState: 'plugged_in', - source: 'cable_action', - } - const response = await OCPP20ServiceUtils.sendTransactionEvent( + // Step 3: Ended event + const endedEvent = buildTransactionEvent( mockStation, - OCPP20TransactionEventEnumType.Started, - context, + OCPP20TransactionEventEnumType.Ended, + OCPP20TriggerReasonEnumType.StopAuthorized, connectorId, transactionId ) - // Validate response structure - assert.notStrictEqual(response, undefined) - assert.strictEqual(typeof response, 'object') - }) - - await it('should handle context-aware error scenarios gracefully', async () => { - // Create error mock for this test - const { station: errorMockChargingStation } = createMockChargingStation({ - baseName: TEST_CHARGING_STATION_BASE_NAME, - connectorsCount: 1, - evseConfiguration: { evsesCount: 1 }, - heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, - ocppRequestService: { - requestHandler: () => { - throw new Error('Context test error') - }, - }, - stationInfo: { - ocppStrictCompliance: true, - ocppVersion: OCPPVersion.VERSION_201, - }, - websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, - }) - - const connectorId = 1 - const transactionId = generateUUID() - const context: OCPP20TransactionContext = { - abnormalCondition: 'TestError', - source: 'abnormal_condition', - } + // Validate event sequence + assert.strictEqual(startedEvent.seqNo, 0) + assert.strictEqual(chargingEvent.seqNo, 1) + assert.strictEqual(endedEvent.seqNo, 2) - try { - await OCPP20ServiceUtils.sendTransactionEvent( - errorMockChargingStation, - OCPP20TransactionEventEnumType.Ended, - context, - connectorId, - transactionId - ) - throw new Error('Should have thrown error') - } catch (error) { - assert.ok((error as Error).message.includes('Context test error')) - } + // All events share same transaction ID + assert.strictEqual(startedEvent.transactionInfo.transactionId, transactionId) + assert.strictEqual(chargingEvent.transactionInfo.transactionId, transactionId) + assert.strictEqual(endedEvent.transactionInfo.transactionId, transactionId) }) - }) - await describe('Backward Compatibility', async () => { - await it('should maintain compatibility with existing buildTransactionEvent calls', () => { - const connectorId = 1 - const transactionId = generateUUID() + await it(`should maintain independent sequence numbers on different connectors for ${description}`, () => { + const connector1 = 1 + const connector2 = 2 + const transaction1Id = generateUUID() + const transaction2Id = generateUUID() - OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connector1) + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connector2) - // Old method call should still work - const oldEvent = OCPP20ServiceUtils.buildTransactionEvent( + // Start transaction on connector 1 + const conn1Event1 = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, - OCPP20TriggerReasonEnumType.Authorized, - connectorId, - transactionId + expectedStartTrigger, + connector1, + transaction1Id ) - assert.strictEqual(oldEvent.eventType, OCPP20TransactionEventEnumType.Started) - assert.strictEqual(oldEvent.triggerReason, OCPP20TriggerReasonEnumType.Authorized) - assert.strictEqual(oldEvent.seqNo, 0) - }) - - await it('should maintain compatibility with existing sendTransactionEvent calls', async () => { - const connectorId = 1 - const transactionId = generateUUID() - - // Old method call should still work - const response = await OCPP20ServiceUtils.sendTransactionEvent( + // Start transaction on connector 2 + const conn2Event1 = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, - OCPP20TriggerReasonEnumType.Authorized, - connectorId, - transactionId + expectedStartTrigger, + connector2, + transaction2Id ) - assert.notStrictEqual(response, undefined) - assert.strictEqual(typeof response, 'object') - }) - }) - }) - - // ========================================================================== - // Parameterized Transaction Flow Tests (E02, E03, Remote Start) - // ========================================================================== - await describe('Transaction Flow Patterns', async () => { - for (const { - description, - expectedStartTrigger, - id, - includeIdToken, - name, - startContext, - } of TRANSACTION_FLOWS) { - await describe(`${name} Flow`, async () => { - await it(`should select ${expectedStartTrigger} trigger for ${description} transaction start`, () => { - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - startContext - ) - assert.strictEqual(triggerReason, expectedStartTrigger) - }) - - await it(`should build correct Started event for ${description}`, () => { - const connectorId = 1 - const transactionId = generateUUID() - const idToken: OCPP20IdTokenType | undefined = includeIdToken - ? { idToken: `${id.toUpperCase()}_TOKEN_001`, type: OCPP20IdTokenEnumType.ISO14443 } - : undefined - - OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - - const startedEvent = OCPP20ServiceUtils.buildTransactionEvent( - mockStation, - OCPP20TransactionEventEnumType.Started, - expectedStartTrigger, - connectorId, - transactionId, - idToken != null ? { idToken } : undefined - ) - - assert.strictEqual(startedEvent.eventType, OCPP20TransactionEventEnumType.Started) - assert.strictEqual(startedEvent.triggerReason, expectedStartTrigger) - assert.strictEqual(startedEvent.seqNo, 0) - assert.strictEqual(startedEvent.transactionInfo.transactionId, transactionId) - - if (includeIdToken) { - assert.notStrictEqual(startedEvent.idToken, undefined) - assert.strictEqual(startedEvent.idToken?.idToken, `${id.toUpperCase()}_TOKEN_001`) - } - }) - - await it(`should support complete ${description} transaction lifecycle`, () => { - const connectorId = 1 - const transactionId = generateUUID() - const idToken: OCPP20IdTokenType | undefined = includeIdToken - ? { - idToken: `${id.toUpperCase()}_LIFECYCLE_001`, - type: OCPP20IdTokenEnumType.ISO14443, - } - : undefined - - OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - - // Step 1: Started event - const startedEvent = OCPP20ServiceUtils.buildTransactionEvent( - mockStation, - OCPP20TransactionEventEnumType.Started, - expectedStartTrigger, - connectorId, - transactionId, - idToken != null ? { idToken } : undefined - ) - - // Step 2: Charging state change - const chargingEvent = OCPP20ServiceUtils.buildTransactionEvent( - mockStation, - OCPP20TransactionEventEnumType.Updated, - OCPP20TriggerReasonEnumType.ChargingStateChanged, - connectorId, - transactionId, - { chargingState: OCPP20ChargingStateEnumType.Charging } - ) - - // Step 3: Ended event - const endedEvent = OCPP20ServiceUtils.buildTransactionEvent( - mockStation, - OCPP20TransactionEventEnumType.Ended, - OCPP20TriggerReasonEnumType.StopAuthorized, - connectorId, - transactionId - ) - - // Validate event sequence - assert.strictEqual(startedEvent.seqNo, 0) - assert.strictEqual(chargingEvent.seqNo, 1) - assert.strictEqual(endedEvent.seqNo, 2) - - // All events share same transaction ID - assert.strictEqual(startedEvent.transactionInfo.transactionId, transactionId) - assert.strictEqual(chargingEvent.transactionInfo.transactionId, transactionId) - assert.strictEqual(endedEvent.transactionInfo.transactionId, transactionId) - }) - - await it(`should maintain independent sequence numbers on different connectors for ${description}`, () => { - const connector1 = 1 - const connector2 = 2 - const transaction1Id = generateUUID() - const transaction2Id = generateUUID() - - OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connector1) - OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connector2) - - // Start transaction on connector 1 - const conn1Event1 = OCPP20ServiceUtils.buildTransactionEvent( - mockStation, - OCPP20TransactionEventEnumType.Started, - expectedStartTrigger, - connector1, - transaction1Id - ) + // Update connector 1 + const conn1Event2 = buildTransactionEvent( + mockStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.ChargingStateChanged, + connector1, + transaction1Id + ) - // Start transaction on connector 2 - const conn2Event1 = OCPP20ServiceUtils.buildTransactionEvent( - mockStation, - OCPP20TransactionEventEnumType.Started, - expectedStartTrigger, - connector2, - transaction2Id - ) + // Update connector 2 + const conn2Event2 = buildTransactionEvent( + mockStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.ChargingStateChanged, + connector2, + transaction2Id + ) - // Update connector 1 - const conn1Event2 = OCPP20ServiceUtils.buildTransactionEvent( - mockStation, - OCPP20TransactionEventEnumType.Updated, - OCPP20TriggerReasonEnumType.ChargingStateChanged, - connector1, - transaction1Id - ) + // Verify independent sequence numbers + assert.strictEqual(conn1Event1.seqNo, 0) + assert.strictEqual(conn1Event2.seqNo, 1) + assert.strictEqual(conn2Event1.seqNo, 0) + assert.strictEqual(conn2Event2.seqNo, 1) - // Update connector 2 - const conn2Event2 = OCPP20ServiceUtils.buildTransactionEvent( - mockStation, - OCPP20TransactionEventEnumType.Updated, - OCPP20TriggerReasonEnumType.ChargingStateChanged, - connector2, - transaction2Id - ) - - // Verify independent sequence numbers - assert.strictEqual(conn1Event1.seqNo, 0) - assert.strictEqual(conn1Event2.seqNo, 1) - assert.strictEqual(conn2Event1.seqNo, 0) - assert.strictEqual(conn2Event2.seqNo, 1) - - // Verify independent transaction IDs - assert.strictEqual(conn1Event1.transactionInfo.transactionId, transaction1Id) - assert.strictEqual(conn2Event1.transactionInfo.transactionId, transaction2Id) - }) + // Verify independent transaction IDs + assert.strictEqual(conn1Event1.transactionInfo.transactionId, transaction1Id) + assert.strictEqual(conn2Event1.transactionInfo.transactionId, transaction2Id) }) - } - }) + }) + } // ========================================================================== // E02 Cable-First Specific Tests @@ -1115,7 +762,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // Step 1: Cable plugged in (Started) - const cablePluggedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const cablePluggedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.CablePluggedIn, @@ -1124,7 +771,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // Step 2: EV detected (Updated) - const evDetectedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const evDetectedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.EVDetected, @@ -1133,7 +780,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // Step 3: Charging starts (Updated with ChargingStateChanged) - const chargingStartedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const chargingStartedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, @@ -1165,7 +812,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // Start transaction with cable plug - const startEvent = OCPP20ServiceUtils.buildTransactionEvent( + const startEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.CablePluggedIn, @@ -1174,7 +821,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // End transaction with EV departure (cable removal) - const endEvent = OCPP20ServiceUtils.buildTransactionEvent( + const endEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Ended, OCPP20TriggerReasonEnumType.EVDeparted, @@ -1200,28 +847,28 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { // Build full cable-first flow const events = [ - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.CablePluggedIn, connectorId, transactionId ), - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.EVDetected, connectorId, transactionId ), - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.Authorized, connectorId, transactionId ), - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, @@ -1319,7 +966,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { // Cable-first flow with suspended state const events = [ // 1. Cable plugged - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.CablePluggedIn, @@ -1327,7 +974,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId ), // 2. Start charging - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, @@ -1336,7 +983,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { { chargingState: OCPP20ChargingStateEnumType.Charging } ), // 3. Suspended by EV - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, @@ -1345,7 +992,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { { chargingState: OCPP20ChargingStateEnumType.SuspendedEV } ), // 4. Resume charging - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, @@ -1354,7 +1001,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { { chargingState: OCPP20ChargingStateEnumType.Charging } ), // 5. EV departed - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Ended, OCPP20TriggerReasonEnumType.EVDeparted, @@ -1375,88 +1022,6 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { }) }) - await describe('Context-Based Cable Event Trigger Selection', async () => { - await it('should select CablePluggedIn from cable_action context with plugged_in state', () => { - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - TransactionContextFixtures.cablePluggedIn() - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.CablePluggedIn) - }) - - await it('should select EVDetected from cable_action context with detected state', () => { - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Updated, - TransactionContextFixtures.evDetected() - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.EVDetected) - }) - - await it('should select EVDeparted from cable_action context with unplugged state', () => { - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - TransactionContextFixtures.evDeparted() - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.EVDeparted) - }) - }) - }) - - // ========================================================================== - // E03 IdToken-First Specific Tests - // ========================================================================== - await describe('E03 - IdToken-First Pre-Authorization', async () => { - beforeEach(() => { - resetConnectorTransactionState(mockStation) - }) - - await describe('E03.FR.13 - Trigger Reason Selection', async () => { - await it('should select groupIdToken trigger for group authorization', () => { - const context: OCPP20TransactionContext = { - authorizationMethod: 'groupIdToken', - source: 'local_authorization', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.Authorized) - }) - - await it('should differentiate IdToken-first from Cable-first by trigger reason', () => { - // IdToken-first: Authorized trigger - const idTokenFirstContext: OCPP20TransactionContext = { - authorizationMethod: 'idToken', - source: 'local_authorization', - } - - // Cable-first: CablePluggedIn trigger - const cableFirstContext: OCPP20TransactionContext = { - cableState: 'plugged_in', - source: 'cable_action', - } - - const idTokenTrigger = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - idTokenFirstContext - ) - - const cableTrigger = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Started, - cableFirstContext - ) - - assert.strictEqual(idTokenTrigger, OCPP20TriggerReasonEnumType.Authorized) - assert.strictEqual(cableTrigger, OCPP20TriggerReasonEnumType.CablePluggedIn) - assert.notStrictEqual(idTokenTrigger, cableTrigger) - }) - }) - await describe('E03.FR.01 - IdToken in TransactionEvent', async () => { await it('should include idToken in first TransactionEvent after authorization', () => { const connectorId = 1 @@ -1469,7 +1034,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // Build Started event with idToken (E03.FR.01: IdToken must be in first event) - const startedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const startedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1498,7 +1063,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // First event includes idToken - const startedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const startedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1508,7 +1073,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // Second event should NOT include idToken (flag is set after first inclusion) - const updatedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const updatedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, @@ -1533,7 +1098,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { type: OCPP20IdTokenEnumType.ISO14443, } - const rfidEvent = OCPP20ServiceUtils.buildTransactionEvent( + const rfidEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1557,7 +1122,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { type: OCPP20IdTokenEnumType.eMAID, } - const emaidEvent = OCPP20ServiceUtils.buildTransactionEvent( + const emaidEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1583,7 +1148,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // E03 Step 1: IdToken presented and authorized (Started with Authorized trigger) - const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const authorizedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1593,7 +1158,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // E03 Step 2: Cable connected (Updated event) - const cableConnectedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const cableConnectedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.CablePluggedIn, @@ -1602,7 +1167,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // E03 Step 3: Charging starts - const chargingEvent = OCPP20ServiceUtils.buildTransactionEvent( + const chargingEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, @@ -1612,7 +1177,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // E03 Step 4: Transaction ends - const endedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const endedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Ended, OCPP20TriggerReasonEnumType.StopAuthorized, @@ -1663,7 +1228,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { connectorStatus.transactionIdTokenSent = undefined } - const e03Start = OCPP20ServiceUtils.buildTransactionEvent( + const e03Start = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1678,7 +1243,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { connectorStatus.transactionIdTokenSent = undefined } - const e02Start = OCPP20ServiceUtils.buildTransactionEvent( + const e02Start = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.CablePluggedIn, @@ -1708,7 +1273,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // E03.FR.05: User authorizes with IdToken - const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const authorizedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1718,7 +1283,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // E03.FR.06: Cable not connected within timeout - transaction ends with Timeout - const timeoutEvent = OCPP20ServiceUtils.buildTransactionEvent( + const timeoutEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Ended, OCPP20TriggerReasonEnumType.EVConnectTimeout, @@ -1745,21 +1310,6 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { }) await describe('Authorization Status in E03', async () => { - await it('should support Deauthorized trigger for rejected authorization', () => { - const context: OCPP20TransactionContext = { - authorizationMethod: 'idToken', - isDeauthorized: true, - source: 'local_authorization', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.Deauthorized) - }) - await it('should handle transaction end after token revocation', () => { const connectorId = 1 const transactionId = generateUUID() @@ -1771,7 +1321,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // Transaction started with authorization - const startEvent = OCPP20ServiceUtils.buildTransactionEvent( + const startEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1781,7 +1331,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // Transaction ended due to deauthorization (e.g., token revoked mid-session) - const revokedEvent = OCPP20ServiceUtils.buildTransactionEvent( + const revokedEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Ended, OCPP20TriggerReasonEnumType.Deauthorized, @@ -1793,20 +1343,6 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { assert.strictEqual(revokedEvent.eventType, OCPP20TransactionEventEnumType.Ended) assert.strictEqual(revokedEvent.triggerReason, OCPP20TriggerReasonEnumType.Deauthorized) }) - - await it('should support StopAuthorized trigger for normal transaction end', () => { - const context: OCPP20TransactionContext = { - authorizationMethod: 'stopAuthorized', - source: 'local_authorization', - } - - const triggerReason = OCPP20ServiceUtils.selectTriggerReason( - OCPP20TransactionEventEnumType.Ended, - context - ) - - assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.StopAuthorized) - }) }) await describe('E03.FR.07/08 - Sequence Numbers and Transaction ID', async () => { @@ -1821,7 +1357,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) const events = [ - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1829,28 +1365,28 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId, { idToken } ), - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.CablePluggedIn, connectorId, transactionId ), - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.ChargingStateChanged, connectorId, transactionId ), - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.MeterValuePeriodic, connectorId, transactionId ), - OCPP20ServiceUtils.buildTransactionEvent( + buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Ended, OCPP20TriggerReasonEnumType.StopAuthorized, @@ -1876,7 +1412,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { // E03.FR.08: transactionId MUST be unique assert.notStrictEqual(transaction1Id, transaction2Id) - const event1 = OCPP20ServiceUtils.buildTransactionEvent( + const event1 = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -1886,7 +1422,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) - const event2 = OCPP20ServiceUtils.buildTransactionEvent( + const event2 = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -2566,7 +2102,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId) // Send initial Started event - const startEvent = OCPP20ServiceUtils.buildTransactionEvent( + const startEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -2577,7 +2113,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { // Send multiple periodic events (simulating timer ticks) for (let i = 1; i <= 3; i++) { - const periodicEvent = OCPP20ServiceUtils.buildTransactionEvent( + const periodicEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.MeterValuePeriodic, @@ -2661,7 +2197,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { // Simulate full transaction lifecycle with periodic updates // 1. Started event (seqNo: 0) - const startEvent = OCPP20ServiceUtils.buildTransactionEvent( + const startEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, @@ -2672,7 +2208,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { // 2. Multiple periodic updates (seqNo: 1, 2, 3) for (let i = 1; i <= 3; i++) { - const updateEvent = OCPP20ServiceUtils.buildTransactionEvent( + const updateEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.MeterValuePeriodic, @@ -2683,7 +2219,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { } // 3. Ended event (seqNo: 4) - const endEvent = OCPP20ServiceUtils.buildTransactionEvent( + const endEvent = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Ended, OCPP20TriggerReasonEnumType.StopAuthorized, @@ -2702,14 +2238,14 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, 2) // Build events for connector 1 - const event1Start = OCPP20ServiceUtils.buildTransactionEvent( + const event1Start = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, 1, transactionId1 ) - const event1Update = OCPP20ServiceUtils.buildTransactionEvent( + const event1Update = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.MeterValuePeriodic, @@ -2718,14 +2254,14 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) // Build events for connector 2 - const event2Start = OCPP20ServiceUtils.buildTransactionEvent( + const event2Start = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Started, OCPP20TriggerReasonEnumType.Authorized, 2, transactionId2 ) - const event2Update = OCPP20ServiceUtils.buildTransactionEvent( + const event2Update = buildTransactionEvent( mockStation, OCPP20TransactionEventEnumType.Updated, OCPP20TriggerReasonEnumType.MeterValuePeriodic, diff --git a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts index 61625ea0..bdd112a5 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts @@ -16,10 +16,7 @@ import type { OCPP20RequestCommand, OCSPRequestDataType, } from '../../../../src/types/index.js' -import type { - OCPP20IdTokenType, - OCPP20TransactionContext, -} from '../../../../src/types/ocpp/2.0/Transaction.js' +import type { OCPP20IdTokenType } from '../../../../src/types/ocpp/2.0/Transaction.js' import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js' import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js' @@ -30,7 +27,6 @@ import { OCPP20RequiredVariableName, OCPPVersion, } from '../../../../src/types/index.js' -import { OCPP20IncomingRequestCommand } from '../../../../src/types/ocpp/2.0/Requests.js' import { OCPP20IdTokenEnumType } from '../../../../src/types/ocpp/2.0/Transaction.js' import { Constants } from '../../../../src/utils/index.js' import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js' @@ -448,200 +444,6 @@ export const IdTokenFixtures = { }), } as const -/** - * Pre-built TransactionContext factories for common flow patterns. - * Use these to create standardized contexts for different transaction flows. - */ -export const TransactionContextFixtures = { - // ===== Local Authorization Contexts ===== - - /** - * Abnormal condition (with optional condition type). - * @param condition - The abnormal condition type. - * @returns An OCPP20TransactionContext for abnormal conditions. - */ - abnormalCondition: (condition = 'OverCurrent'): OCPP20TransactionContext => ({ - abnormalCondition: condition, - source: 'abnormal_condition', - }), - - /** - * Cable plugged in (E02 cable-first start). - * @returns An OCPP20TransactionContext for cable plugged in. - */ - cablePluggedIn: (): OCPP20TransactionContext => ({ - cableState: 'plugged_in', - source: 'cable_action', - }), - - /** - * Deauthorization (token revoked or invalid). - * @returns An OCPP20TransactionContext for deauthorization. - */ - deauthorized: (): OCPP20TransactionContext => ({ - authorizationMethod: 'idToken', - isDeauthorized: true, - source: 'local_authorization', - }), - - // ===== Cable Action Contexts (E02 flow) ===== - - /** - * Energy limit reached. - * @returns An OCPP20TransactionContext for energy limit reached. - */ - energyLimitReached: (): OCPP20TransactionContext => ({ - source: 'energy_limit', - }), - - /** - * EV communication lost. - * @returns An OCPP20TransactionContext for EV communication lost. - */ - evCommunicationLost: (): OCPP20TransactionContext => ({ - source: 'system_event', - systemEvent: 'ev_communication_lost', - }), - - /** - * EV connect timeout. - * @returns An OCPP20TransactionContext for EV connect timeout. - */ - evConnectTimeout: (): OCPP20TransactionContext => ({ - source: 'system_event', - systemEvent: 'ev_connect_timeout', - }), - - // ===== Remote Command Contexts ===== - - /** - * Cable unplugged / EV departed. - * @returns An OCPP20TransactionContext for EV departure. - */ - evDeparted: (): OCPP20TransactionContext => ({ - cableState: 'unplugged', - source: 'cable_action', - }), - - /** - * EV detected after cable connection. - * @returns An OCPP20TransactionContext for EV detection. - */ - evDetected: (): OCPP20TransactionContext => ({ - cableState: 'detected', - source: 'cable_action', - }), - - /** - * IdToken-first authorization (E03 flow start). - * @param authorizationMethod - The authorization method used. - * @returns An OCPP20TransactionContext for IdToken authorization. - */ - idTokenAuthorized: ( - authorizationMethod: 'groupIdToken' | 'idToken' = 'idToken' - ): OCPP20TransactionContext => ({ - authorizationMethod, - source: 'local_authorization', - }), - - /** - * Clock-aligned meter value. - * @returns An OCPP20TransactionContext for clock-aligned meter values. - */ - meterValueClock: (): OCPP20TransactionContext => ({ - isPeriodicMeterValue: false, - source: 'meter_value', - }), - - /** - * Periodic meter value (sampled interval). - * @returns An OCPP20TransactionContext for periodic meter values. - */ - meterValuePeriodic: (): OCPP20TransactionContext => ({ - isPeriodicMeterValue: true, - source: 'meter_value', - }), - - // ===== Meter Value Contexts ===== - - /** - * Remote start transaction request. - * @returns An OCPP20TransactionContext for remote start. - */ - remoteStart: (): OCPP20TransactionContext => ({ - command: OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION, - source: 'remote_command', - }), - - /** - * Remote stop transaction request. - * @returns An OCPP20TransactionContext for remote stop. - */ - remoteStop: (): OCPP20TransactionContext => ({ - command: OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION, - source: 'remote_command', - }), - - /** - * Reset command. - * @returns An OCPP20TransactionContext for reset. - */ - reset: (): OCPP20TransactionContext => ({ - command: OCPP20IncomingRequestCommand.RESET, - source: 'remote_command', - }), - - // ===== System Event Contexts ===== - - /** - * Signed data received. - * @returns An OCPP20TransactionContext for signed data. - */ - signedData: (): OCPP20TransactionContext => ({ - isSignedDataReceived: true, - source: 'meter_value', - }), - - /** - * Stop authorized by local token presentation. - * @returns An OCPP20TransactionContext for stop authorization. - */ - stopAuthorized: (): OCPP20TransactionContext => ({ - authorizationMethod: 'stopAuthorized', - source: 'local_authorization', - }), - - // ===== Limit Contexts ===== - - /** - * Time limit reached. - * @returns An OCPP20TransactionContext for time limit. - */ - timeLimitReached: (): OCPP20TransactionContext => ({ - source: 'time_limit', - }), - - /** - * Trigger message command. - * @returns An OCPP20TransactionContext for trigger message. - */ - triggerMessage: (): OCPP20TransactionContext => ({ - command: OCPP20IncomingRequestCommand.TRIGGER_MESSAGE, - source: 'remote_command', - }), - - // ===== Abnormal Condition Contexts ===== - - /** - * Unlock connector command. - * @returns An OCPP20TransactionContext for unlock connector. - */ - unlockConnector: (): OCPP20TransactionContext => ({ - command: OCPP20IncomingRequestCommand.UNLOCK_CONNECTOR, - source: 'remote_command', - }), -} as const - /** * Pre-built mock station fixtures for Reset command testing. * These factories create properly configured MockChargingStation instances @@ -706,8 +508,6 @@ export interface TransactionFlowPattern { expectedStartTrigger: string /** Whether to include idToken in Started event */ includeIdToken: boolean - /** Context to use for Started event (determines initial trigger reason) */ - startContext: OCPP20TransactionContext } /** @@ -719,19 +519,16 @@ export const TransactionFlowPatterns: TransactionFlowPattern[] = [ description: 'E02 Cable-First: CablePluggedIn → Charging → EVDeparted', expectedStartTrigger: 'CablePluggedIn', includeIdToken: false, - startContext: TransactionContextFixtures.cablePluggedIn(), }, { description: 'E03 IdToken-First: Authorized → Cable → Charging → StopAuthorized', expectedStartTrigger: 'Authorized', includeIdToken: true, - startContext: TransactionContextFixtures.idTokenAuthorized(), }, { description: 'Remote Start: RemoteStart → Charging → RemoteStop', expectedStartTrigger: 'RemoteStart', includeIdToken: false, - startContext: TransactionContextFixtures.remoteStart(), }, ] as const