]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(tests): harmonize test suite structure and eliminate duplication
authorJérôme Benoit <jerome.benoit@sap.com>
Sat, 28 Feb 2026 16:41:55 +0000 (17:41 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sat, 28 Feb 2026 16:41:55 +0000 (17:41 +0100)
- Consolidate 5 TransactionEvent variant files into single parameterized test
- Reduce OCPPAuthIntegration.test.ts scope (422→196 lines, focus on integration)
- Remove duplicate resetConnectorTransactionStates function, use shared utility
- Merge OCPP20AuthAdapter-Offline.test.ts into main adapter test file
- Add beforeEach hooks for mock initialization in ChargingStation tests
- Standardize OCPP requirement codes in describe blocks (I02-I04, G03)
- Consolidate eslint-disable comments with explanatory reasons
- Fix JSDoc header positions in OCPP 2.0 test files
- Use standardCleanup() for consistent timer mock reset

Net reduction: ~400 lines of test code while maintaining full coverage

20 files changed:
tests/charging-station/ChargingStation-Configuration.test.ts
tests/charging-station/ChargingStation-Connectors.test.ts
tests/charging-station/ChargingStation-Lifecycle.test.ts
tests/charging-station/ChargingStation-Resilience.test.ts
tests/charging-station/ChargingStation-Transactions.test.ts
tests/charging-station/ocpp/2.0/OCPP20CertificateManager.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RemoteStartAuth.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-CableFirst.test.ts [deleted file]
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts [deleted file]
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Offline.test.ts [deleted file]
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Periodic.test.ts [deleted file]
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts
tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts
tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter-Offline.test.ts [deleted file]
tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts
tests/utils/Utils.test.ts

index 09ff83cac36eb40374eef3997a1b0d0f673f85e6..4f9db71246a891605718e8e984b9294e2f53c2e3 100644 (file)
@@ -3,7 +3,7 @@
  * @description Unit tests for boot notification, config persistence, and WebSocket handling
  */
 import { expect } from '@std/expect'
-import { afterEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
 
@@ -20,6 +20,9 @@ await describe('ChargingStation Configuration Management', async () => {
   await describe('B02 - Pending Boot Notification Behavior', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -114,6 +117,9 @@ await describe('ChargingStation Configuration Management', async () => {
   await describe('B03 - Rejected Boot Notification Behavior', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -237,6 +243,9 @@ await describe('ChargingStation Configuration Management', async () => {
   await describe('Configuration Persistence', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -415,6 +424,9 @@ await describe('ChargingStation Configuration Management', async () => {
   await describe('WebSocket Message Handling', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -734,6 +746,9 @@ await describe('ChargingStation Configuration Management', async () => {
   await describe('WebSocket Ping Interval', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
index 3e423b7f52bf0ca33a210bd37396f5ea74f89454..227b9399f65c9b86ecbccc384cfda81f0ee842a9 100644 (file)
@@ -3,7 +3,7 @@
  * @description Unit tests for connector queries, EVSE management, and availability
  */
 import { expect } from '@std/expect'
-import { afterEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
 
@@ -17,6 +17,10 @@ await describe('ChargingStation Connector and EVSE State', async () => {
   await describe('Connector Query Tests', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
+
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -95,6 +99,9 @@ await describe('ChargingStation Connector and EVSE State', async () => {
   await describe('Connector 0 (Shared Power) Tests', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -122,6 +129,9 @@ await describe('ChargingStation Connector and EVSE State', async () => {
   await describe('EVSE Query Tests (non-EVSE mode)', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -148,6 +158,9 @@ await describe('ChargingStation Connector and EVSE State', async () => {
   await describe('EVSE Mode Tests', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -241,6 +254,9 @@ await describe('ChargingStation Connector and EVSE State', async () => {
   await describe('Boot Notification State', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -342,6 +358,9 @@ await describe('ChargingStation Connector and EVSE State', async () => {
   await describe('Reservation Management', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
index 46761a770d2bbd6877a588d26a17ac683a501e3b..e42648d64fdc0a1c0949b5e6293c572fa4549f0b 100644 (file)
@@ -3,7 +3,7 @@
  * @description Unit tests for charging station start/stop/restart and delete operations
  */
 import { expect } from '@std/expect'
-import { afterEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
 
@@ -12,6 +12,9 @@ import { cleanupChargingStation, createMockChargingStation } from './ChargingSta
 await describe('ChargingStation Lifecycle', async () => {
   await describe('Start/Stop Operations', async () => {
     let station: ChargingStation | undefined
+    beforeEach(() => {
+      station = undefined
+    })
 
     afterEach(() => {
       if (station != null) {
@@ -160,6 +163,9 @@ await describe('ChargingStation Lifecycle', async () => {
 
   await describe('Delete Operations', async () => {
     let station: ChargingStation | undefined
+    beforeEach(() => {
+      station = undefined
+    })
 
     afterEach(() => {
       if (station != null) {
index b9e447e857cf6ff2be59ced9109b0e3ab97f5286..ee431b9554a3a968870ff0e459ce267edc8e8a29 100644 (file)
@@ -3,7 +3,7 @@
  * @description Unit tests for charging station error handling, reconnection, and message queuing
  */
 import { expect } from '@std/expect'
-import { afterEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
 
@@ -13,6 +13,10 @@ import { cleanupChargingStation, createMockChargingStation } from './ChargingSta
 await describe('ChargingStation Error Recovery and Resilience', async () => {
   let station: ChargingStation
 
+  beforeEach(() => {
+    station = undefined
+  })
+
   afterEach(() => {
     // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
     if (station != null) {
@@ -274,6 +278,10 @@ await describe('ChargingStation Error Recovery and Resilience', async () => {
 await describe('ChargingStation Message Buffering', async () => {
   let station: ChargingStation
 
+  beforeEach(() => {
+    station = undefined
+  })
+
   afterEach(() => {
     // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
     if (station != null) {
index b125c8a8a8784c238bc6d71800d6db3e04e7f269..32d15a19d6666602ce4638ac25894288a46da6e8 100644 (file)
@@ -3,7 +3,7 @@
  * @description Unit tests for transaction queries, energy meters, and concurrent transaction handling
  */
 import { expect } from '@std/expect'
-import { afterEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
 
@@ -14,6 +14,9 @@ await describe('ChargingStation Transaction Management', async () => {
   await describe('Transaction Query Tests', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -167,6 +170,9 @@ await describe('ChargingStation Transaction Management', async () => {
   await describe('Energy Meter Tests', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -267,6 +273,9 @@ await describe('ChargingStation Transaction Management', async () => {
   await describe('Concurrent Transaction Scenarios', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
@@ -428,6 +437,9 @@ await describe('ChargingStation Transaction Management', async () => {
   await describe('Heartbeat and Meter Intervals', async () => {
     let station: ChargingStation | undefined
 
+    beforeEach(() => {
+      station = undefined
+    })
     afterEach(() => {
       if (station != null) {
         cleanupChargingStation(station)
index ea73f677d7b5aaf0641467dc841a06b10c1a2489..4ddeb1b64c2f8103f708cebd6236beec74b4f245 100644 (file)
@@ -47,7 +47,7 @@ const _EXPECTED_HASH_DATA: CertificateHashDataType = {
   serialNumber: expect.any(String),
 }
 
-await describe('OCPP20CertificateManager', async () => {
+await describe('I02-I04 - ISO15118 Certificate Management', async () => {
   afterEach(async () => {
     await rm(`dist/assets/configurations/${TEST_STATION_HASH_ID}`, {
       force: true,
index 222ed7aa0d59f60fb4ac65702411e7836b1cf1a4..af451bcac6e3d321dccd49ff3b1288c39b57d63e 100644 (file)
@@ -1,3 +1,7 @@
+/**
+ * @file Tests for OCPP20IncomingRequestService GetBaseReport
+ * @description Unit tests for OCPP 2.0 GetBaseReport command handling (B07)
+ */
 import { expect } from '@std/expect'
 import { afterEach, beforeEach, describe, it } from 'node:test'
 
@@ -5,28 +9,22 @@ import {
   addConfigurationKey,
   setConfigurationKeyValue,
 } from '../../../../src/charging-station/ConfigurationKeyUtils.js'
-/**
- * @file Tests for OCPP20IncomingRequestService GetBaseReport
- * @description Unit tests for OCPP 2.0 GetBaseReport command handling (B07)
- */
 import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
 import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
 import { OCPP20VariableManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20VariableManager.js'
 import {
+  AttributeEnumType,
   GenericDeviceModelStatusEnumType,
   OCPP20ComponentName,
   OCPP20DeviceInfoVariableName,
   type OCPP20GetBaseReportRequest,
+  OCPP20OptionalVariableName,
+  OCPP20RequiredVariableName,
   type OCPP20SetVariableResultType,
   OCPPVersion,
   ReportBaseEnumType,
   type ReportDataType,
 } from '../../../../src/types/index.js'
-import {
-  AttributeEnumType,
-  OCPP20OptionalVariableName,
-  OCPP20RequiredVariableName,
-} from '../../../../src/types/index.js'
 import { StandardParametersKey } from '../../../../src/types/ocpp/Configuration.js'
 import { Constants } from '../../../../src/utils/index.js'
 import { createChargingStation } from '../../../ChargingStationFactory.js'
index eb510b71a90c97add04d49a1956c82f311cf29eb..c42f6d37aafa51c1d5e5e047b9ecfe26c34b2962 100644 (file)
@@ -24,7 +24,7 @@ import {
 } from '../../../../src/types/ocpp/2.0/Transaction.js'
 import { OCPPVersion } from '../../../../src/types/ocpp/OCPPVersion.js'
 
-await describe('OCPP20IncomingRequestService - G03.FR.03 Remote Start Pre-Authorization', async () => {
+await describe('G03 - Remote Start Pre-Authorization', async () => {
   let service: OCPP20IncomingRequestService | undefined
   let mockChargingStation: ChargingStation | undefined
 
index 0998a1c9b5b2ab97f61d8058138b3dc42394434b..49f0929c4a653bf97ed1d45b9cde21573c642a28 100644 (file)
@@ -1,3 +1,7 @@
+/**
+ * @file Tests for OCPP20IncomingRequestService RequestStartTransaction
+ * @description Unit tests for OCPP 2.0 RequestStartTransaction command handling (F01/F02)
+ */
 import { expect } from '@std/expect'
 import { afterEach, beforeEach, describe, it } from 'node:test'
 
@@ -7,10 +11,6 @@ import type {
   OCPP20ChargingRateUnitType,
 } from '../../../../src/types/ocpp/2.0/Transaction.js'
 
-/**
- * @file Tests for OCPP20IncomingRequestService RequestStartTransaction
- * @description Unit tests for OCPP 2.0 RequestStartTransaction command handling (F01/F02)
- */
 import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
 import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
 import { OCPPAuthServiceFactory } from '../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceFactory.js'
index c0f1a3556c25b1e70fd0a129e3810c6a731c7318..2762fb1a782540c723129b580058639947fde018 100644 (file)
@@ -31,7 +31,11 @@ import { Constants } from '../../../../src/utils/index.js'
 import { createChargingStation } from '../../../ChargingStationFactory.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
 import { createMockAuthService } from '../auth/helpers/MockFactories.js'
-import { resetLimits, resetReportingValueSize } from './OCPP20TestUtils.js'
+import {
+  resetConnectorTransactionState,
+  resetLimits,
+  resetReportingValueSize,
+} from './OCPP20TestUtils.js'
 
 await describe('F03 - Remote Stop Transaction', async () => {
   let sentTransactionEvents: OCPP20TransactionEventRequest[] = []
@@ -77,28 +81,6 @@ await describe('F03 - Remote Stop Transaction', async () => {
     OCPPAuthServiceFactory.clearAllInstances()
   })
 
-  /**
-   * Helper function to reset all connector transaction states
-   */
-  function resetConnectorTransactionStates (): void {
-    // Reset all connectors across all EVSEs
-    for (const [, evse] of mockChargingStation.evses.entries()) {
-      for (const [connectorId] of evse.connectors.entries()) {
-        const status = mockChargingStation.getConnectorStatus(connectorId)
-        if (status) {
-          status.transactionStarted = false
-          status.transactionId = undefined
-          status.transactionIdTag = undefined
-          status.transactionStart = undefined
-          status.transactionEnergyActiveImportRegisterValue = undefined
-          status.remoteStartId = undefined
-          status.chargingProfiles = undefined
-          // Keep status as Available and availability as Operative
-        }
-      }
-    }
-  }
-
   /**
    * Helper function to start a transaction and return the transaction ID
    * @param evseId - The EVSE ID to start transaction on
@@ -113,7 +95,7 @@ await describe('F03 - Remote Stop Transaction', async () => {
   ): Promise<string> {
     // Reset all connector states first to ensure clean state (unless skipped for multiple transactions)
     if (!skipReset) {
-      resetConnectorTransactionStates()
+      resetConnectorTransactionState(mockChargingStation)
     }
 
     const startRequest: OCPP20RequestStartTransactionRequest = {
@@ -172,7 +154,7 @@ await describe('F03 - Remote Stop Transaction', async () => {
   // FR: F03.FR.02, F03.FR.03
   await it('should handle multiple active transactions correctly', async () => {
     // Reset once before starting multiple transactions
-    resetConnectorTransactionStates()
+    resetConnectorTransactionState(mockChargingStation)
 
     // Start transactions on different EVSEs (skip reset for subsequent transactions)
     const transactionId1 = await startTransaction(1, 200, true) // Skip reset since we just did it
@@ -480,7 +462,7 @@ await describe('F03 - Remote Stop Transaction', async () => {
 
   // FR: F03.FR.09
   await it('should include final meter values in TransactionEvent(Ended)', async () => {
-    resetConnectorTransactionStates()
+    resetConnectorTransactionState(mockChargingStation)
 
     const transactionId = await startTransaction(3, 700)
 
index edda58685c75b39650b630aa9c7861e0637da93a..ecfd98d6f92229eb29dbecde4c5c0f352e401a91 100644 (file)
@@ -25,6 +25,7 @@ import {
 } from '../../../../src/types/index.js'
 import { Constants } from '../../../../src/utils/index.js'
 import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
 
 await describe('B11 & B12 - Reset', async () => {
@@ -66,7 +67,7 @@ await describe('B11 & B12 - Reset', async () => {
   })
 
   afterEach(() => {
-    mock.timers.reset()
+    standardCleanup()
   })
 
   await describe('B11 - Reset - Without Ongoing Transaction', async () => {
diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-CableFirst.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-CableFirst.test.ts
deleted file mode 100644 (file)
index cbd4754..0000000
+++ /dev/null
@@ -1,504 +0,0 @@
-/**
- * @file Tests for OCPP20ServiceUtils TransactionEvent CableFirst
- * @description Unit tests for OCPP 2.0 cable-first transaction flow (E02)
- */
-import { expect } from '@std/expect'
-import { afterEach, beforeEach, describe, it } from 'node:test'
-
-import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
-import {
-  ConnectorStatusEnum,
-  OCPP20TransactionEventEnumType,
-  OCPP20TriggerReasonEnumType,
-} from '../../../../src/types/index.js'
-import { OCPP20ChargingStateEnumType } from '../../../../src/types/ocpp/2.0/Transaction.js'
-import { generateUUID } from '../../../../src/utils/index.js'
-import {
-  createMockOCPP20TransactionTestStation,
-  resetConnectorTransactionState,
-  resetLimits,
-  TransactionContextFixtures,
-} from './OCPP20TestUtils.js'
-
-/**
- * E02 - Cable-First Transaction Flow Tests
- *
- * Tests for the Cable-First (Plug-in First) transaction pattern where:
- * 1. User plugs in the cable (CablePluggedIn)
- * 2. EV is detected (EVDetected)
- * 3. Authorization occurs (Authorized or implicit)
- * 4. Charging starts (ChargingStateChanged)
- * 5. Charging ends (StopAuthorized or EVDeparted)
- *
- * These tests verify the full transaction lifecycle, not just trigger reason selection
- * (which is tested in OCPP20ServiceUtils-TransactionEvent.test.ts).
- *
- * FR References:
- * - E02.FR.01: Cable plug event triggers transaction start consideration
- * - E02.FR.02: EVDetected indicates vehicle presence for charging readiness
- * - E02.FR.03: Connector status transitions reflect cable state changes
- */
-await describe('E02 - Cable-First Transaction Flow', async () => {
-  let mockChargingStation: ReturnType<typeof createMockOCPP20TransactionTestStation>
-
-  beforeEach(() => {
-    mockChargingStation = createMockOCPP20TransactionTestStation()
-    resetLimits(mockChargingStation)
-  })
-
-  afterEach(() => {
-    resetConnectorTransactionState(mockChargingStation)
-  })
-
-  // =========================================================================
-  // E02.FR.01: Cable Plug Event Flow Tests
-  // =========================================================================
-  await describe('Cable Plug Event Sequencing', async () => {
-    await it('should generate CablePluggedIn event as first event in cable-first flow', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      // Reset sequence number for new transaction
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Build the cable plug event (first event in cable-first flow)
-      const cablePluggedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        connectorId,
-        transactionId
-      )
-
-      // Assert: First event should have seqNo 0
-      expect(cablePluggedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(cablePluggedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-      expect(cablePluggedEvent.seqNo).toBe(0)
-      expect(cablePluggedEvent.transactionInfo.transactionId).toBe(transactionId)
-    })
-
-    await it('should sequence CablePluggedIn → EVDetected → Charging correctly', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      // Reset sequence for new transaction
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Step 1: Cable plugged in (Started)
-      const cablePluggedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        connectorId,
-        transactionId
-      )
-
-      // Step 2: EV detected (Updated)
-      const evDetectedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.EVDetected,
-        connectorId,
-        transactionId
-      )
-
-      // Step 3: Charging starts (Updated with ChargingStateChanged)
-      const chargingStartedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.ChargingStateChanged,
-        connectorId,
-        transactionId,
-        { chargingState: OCPP20ChargingStateEnumType.Charging }
-      )
-
-      // Assert sequence numbers follow correct order
-      expect(cablePluggedEvent.seqNo).toBe(0)
-      expect(evDetectedEvent.seqNo).toBe(1)
-      expect(chargingStartedEvent.seqNo).toBe(2)
-
-      // Assert all events share the same transaction ID
-      expect(cablePluggedEvent.transactionInfo.transactionId).toBe(transactionId)
-      expect(evDetectedEvent.transactionInfo.transactionId).toBe(transactionId)
-      expect(chargingStartedEvent.transactionInfo.transactionId).toBe(transactionId)
-
-      // Assert event types match expected pattern
-      expect(cablePluggedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(evDetectedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-      expect(chargingStartedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-    })
-
-    await it('should handle EVDeparted for cable removal ending transaction', () => {
-      const connectorId = 2
-      const transactionId = generateUUID()
-
-      // Reset and setup transaction
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Start transaction with cable plug
-      const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        connectorId,
-        transactionId
-      )
-
-      // End transaction with EV departure (cable removal)
-      const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.EVDeparted,
-        connectorId,
-        transactionId
-      )
-
-      // Assert proper sequencing for cable-initiated start and end
-      expect(startEvent.seqNo).toBe(0)
-      expect(startEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-      expect(endEvent.seqNo).toBe(1)
-      expect(endEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
-      expect(endEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
-    })
-  })
-
-  // =========================================================================
-  // E02.FR.02: EV Detection Flow Tests
-  // =========================================================================
-  await describe('EV Detection Flow', async () => {
-    await it('should include EVDetected between cable plug and charging start', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Build full cable-first flow
-      const events = [
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.CablePluggedIn,
-          connectorId,
-          transactionId
-        ),
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.EVDetected,
-          connectorId,
-          transactionId
-        ),
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.Authorized,
-          connectorId,
-          transactionId
-        ),
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
-          connectorId,
-          transactionId,
-          { chargingState: OCPP20ChargingStateEnumType.Charging }
-        ),
-      ]
-
-      // Assert EVDetected comes after CablePluggedIn and before authorization
-      expect(events[0].triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-      expect(events[1].triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
-      expect(events[2].triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-      expect(events[3].triggerReason).toBe(OCPP20TriggerReasonEnumType.ChargingStateChanged)
-
-      // Assert continuous sequence numbers
-      for (let i = 0; i < events.length; i++) {
-        expect(events[i].seqNo).toBe(i)
-      }
-    })
-  })
-
-  // =========================================================================
-  // E02.FR.03: Connector Status Transitions
-  // =========================================================================
-  await describe('Connector Status Transitions', async () => {
-    await it('should track connector status through cable-first lifecycle', () => {
-      const connectorId = 1
-
-      // Get connector status object
-      const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connectorStatus).toBeDefined()
-      if (connectorStatus == null) {
-        throw new Error('Connector status should be defined')
-      }
-
-      // Initial state: Available
-      connectorStatus.status = ConnectorStatusEnum.Available
-      expect(connectorStatus.status).toBe(ConnectorStatusEnum.Available)
-
-      // After cable plug: Preparing (implied by transaction start)
-      connectorStatus.status = ConnectorStatusEnum.Preparing
-      connectorStatus.transactionStarted = true
-      expect(connectorStatus.status).toBe(ConnectorStatusEnum.Preparing)
-      expect(connectorStatus.transactionStarted).toBe(true)
-
-      // After EV detected and auth: Charging
-      connectorStatus.status = ConnectorStatusEnum.Charging
-      expect(connectorStatus.status).toBe(ConnectorStatusEnum.Charging)
-
-      // After EV departed: Available again
-      connectorStatus.status = ConnectorStatusEnum.Available
-      connectorStatus.transactionStarted = false
-      expect(connectorStatus.status).toBe(ConnectorStatusEnum.Available)
-      expect(connectorStatus.transactionStarted).toBe(false)
-    })
-
-    await it('should preserve transaction ID through cable-first flow states', () => {
-      const connectorId = 2
-      const transactionId = generateUUID()
-
-      const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connectorStatus).toBeDefined()
-      if (connectorStatus == null) {
-        throw new Error('Connector status should be defined')
-      }
-
-      // Set transaction ID at start
-      connectorStatus.transactionId = transactionId
-      connectorStatus.transactionStarted = true
-      connectorStatus.status = ConnectorStatusEnum.Preparing
-
-      // Transition to charging
-      connectorStatus.status = ConnectorStatusEnum.Charging
-
-      // Transaction ID should persist through state changes
-      expect(connectorStatus.transactionId).toBe(transactionId)
-      expect(connectorStatus.transactionStarted).toBe(true)
-
-      // Transition to finished
-      connectorStatus.status = ConnectorStatusEnum.Finishing
-
-      // Still same transaction until fully ended
-      expect(connectorStatus.transactionId).toBe(transactionId)
-    })
-  })
-
-  // =========================================================================
-  // Full E02 Transaction Lifecycle Tests
-  // =========================================================================
-  await describe('Full Cable-First Transaction Lifecycle', async () => {
-    await it('should support complete cable-first → charging → cable-removal flow', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Build complete cable-first transaction lifecycle
-      const lifecycle = {
-        cablePlugged: OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.CablePluggedIn,
-          connectorId,
-          transactionId
-        ),
-        charging: OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
-          connectorId,
-          transactionId,
-          { chargingState: OCPP20ChargingStateEnumType.Charging }
-        ),
-        evDeparted: OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Ended,
-          OCPP20TriggerReasonEnumType.EVDeparted,
-          connectorId,
-          transactionId
-        ),
-      }
-
-      // Validate lifecycle event sequence
-      expect(lifecycle.cablePlugged.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(lifecycle.charging.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-      expect(lifecycle.evDeparted.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
-
-      // Validate sequence numbers
-      expect(lifecycle.cablePlugged.seqNo).toBe(0)
-      expect(lifecycle.charging.seqNo).toBe(1)
-      expect(lifecycle.evDeparted.seqNo).toBe(2)
-
-      // Validate trigger reasons match cable-first pattern
-      expect(lifecycle.cablePlugged.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-      expect(lifecycle.charging.triggerReason).toBe(
-        OCPP20TriggerReasonEnumType.ChargingStateChanged
-      )
-      expect(lifecycle.evDeparted.triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
-
-      // All events should share same transaction ID
-      expect(lifecycle.cablePlugged.transactionInfo.transactionId).toBe(transactionId)
-      expect(lifecycle.charging.transactionInfo.transactionId).toBe(transactionId)
-      expect(lifecycle.evDeparted.transactionInfo.transactionId).toBe(transactionId)
-    })
-
-    await it('should handle suspended charging states in cable-first flow', () => {
-      const connectorId = 3
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Cable-first flow with suspended state
-      const events = [
-        // 1. Cable plugged
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.CablePluggedIn,
-          connectorId,
-          transactionId
-        ),
-        // 2. Start charging
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
-          connectorId,
-          transactionId,
-          { chargingState: OCPP20ChargingStateEnumType.Charging }
-        ),
-        // 3. Suspended by EV
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
-          connectorId,
-          transactionId,
-          { chargingState: OCPP20ChargingStateEnumType.SuspendedEV }
-        ),
-        // 4. Resume charging
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
-          connectorId,
-          transactionId,
-          { chargingState: OCPP20ChargingStateEnumType.Charging }
-        ),
-        // 5. EV departed
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Ended,
-          OCPP20TriggerReasonEnumType.EVDeparted,
-          connectorId,
-          transactionId
-        ),
-      ]
-
-      // Verify sequence numbers are continuous through suspend/resume
-      for (let i = 0; i < events.length; i++) {
-        expect(events[i].seqNo).toBe(i)
-      }
-
-      // Verify all share same transaction ID
-      for (const event of events) {
-        expect(event.transactionInfo.transactionId).toBe(transactionId)
-      }
-    })
-  })
-
-  // =========================================================================
-  // Context-Based Trigger Reason Selection for Cable Events
-  // =========================================================================
-  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()
-      )
-
-      expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-    })
-
-    await it('should select EVDetected from cable_action context with detected state', () => {
-      const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-        OCPP20TransactionEventEnumType.Updated,
-        TransactionContextFixtures.evDetected()
-      )
-
-      expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
-    })
-
-    await it('should select EVDeparted from cable_action context with unplugged state', () => {
-      const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-        OCPP20TransactionEventEnumType.Ended,
-        TransactionContextFixtures.evDeparted()
-      )
-
-      expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
-    })
-  })
-
-  // =========================================================================
-  // Multiple Connector Independence Tests
-  // =========================================================================
-  await describe('Multiple Connector Independence', async () => {
-    await it('should maintain independent transaction sequences on different connectors', () => {
-      const transactionId1 = generateUUID()
-      const transactionId2 = generateUUID()
-
-      // Reset both connectors
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
-
-      // Start transaction on connector 1
-      const conn1Start = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        1,
-        transactionId1
-      )
-
-      // Start transaction on connector 2
-      const conn2Start = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        2,
-        transactionId2
-      )
-
-      // Update connector 1
-      const conn1Update = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.EVDetected,
-        1,
-        transactionId1
-      )
-
-      // Update connector 2
-      const conn2Update = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.EVDetected,
-        2,
-        transactionId2
-      )
-
-      // Each connector should have independent sequence numbers
-      expect(conn1Start.seqNo).toBe(0)
-      expect(conn2Start.seqNo).toBe(0)
-      expect(conn1Update.seqNo).toBe(1)
-      expect(conn2Update.seqNo).toBe(1)
-
-      // Different transaction IDs
-      expect(conn1Start.transactionInfo.transactionId).toBe(transactionId1)
-      expect(conn2Start.transactionInfo.transactionId).toBe(transactionId2)
-      expect(conn1Start.transactionInfo.transactionId).not.toBe(
-        conn2Start.transactionInfo.transactionId
-      )
-    })
-  })
-})
diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts
deleted file mode 100644 (file)
index ef87b37..0000000
+++ /dev/null
@@ -1,665 +0,0 @@
-/**
- * @file Tests for OCPP20ServiceUtils TransactionEvent IdTokenFirst
- * @description Unit tests for OCPP 2.0 IdToken-first pre-authorization flow (E03)
- */
-import { expect } from '@std/expect'
-import { afterEach, beforeEach, describe, it } from 'node:test'
-
-import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
-import {
-  OCPP20TransactionEventEnumType,
-  OCPP20TriggerReasonEnumType,
-} from '../../../../src/types/index.js'
-import {
-  OCPP20ChargingStateEnumType,
-  OCPP20IdTokenEnumType,
-  type OCPP20IdTokenType,
-  type OCPP20TransactionContext,
-} from '../../../../src/types/ocpp/2.0/Transaction.js'
-import { generateUUID } from '../../../../src/utils/index.js'
-import {
-  createMockOCPP20TransactionTestStation,
-  resetConnectorTransactionState,
-  resetLimits,
-  TransactionContextFixtures,
-} from './OCPP20TestUtils.js'
-
-/**
- * E03 IdToken-First Transaction Flow Tests (OCPP 2.0.1)
- *
- * Tests the IdToken-first pre-authorization flow where a user presents
- * their ID token BEFORE connecting the cable.
- *
- * Key E03 Functional Requirements:
- * - E03.FR.01: When IdToken presented first, CS SHALL verify with CSMS
- * - E03.FR.02: CSMS SHALL verify IdToken validity
- * - E03.FR.05: CS SHALL handle EVConnectionTimeOut
- * - E03.FR.06: If cable not connected within timeout, CS SHALL cancel authorization
- * - E03.FR.13: triggerReason SHALL be Authorized for IdToken-first
- *
- * Key Difference from E02 (Cable-First):
- * - E03: Authorization -> Cable connection -> Charging
- * - E02: Cable connection -> EV detection -> Authorization -> Charging
- */
-await describe('E03 - IdToken-First Pre-Authorization Flow', async () => {
-  let mockChargingStation: ReturnType<typeof createMockOCPP20TransactionTestStation>
-
-  beforeEach(() => {
-    mockChargingStation = createMockOCPP20TransactionTestStation()
-    resetLimits(mockChargingStation)
-  })
-
-  afterEach(() => {
-    resetConnectorTransactionState(mockChargingStation)
-  })
-
-  // =========================================================================
-  // E03.FR.13: Trigger Reason Selection for IdToken-First
-  // =========================================================================
-  await describe('E03.FR.13 - Trigger Reason Selection', async () => {
-    await it('should select Authorized trigger for IdToken-first transaction start', () => {
-      // E03.FR.13: triggerReason SHALL be Authorized for IdToken-first
-      const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-        OCPP20TransactionEventEnumType.Started,
-        TransactionContextFixtures.idTokenAuthorized()
-      )
-
-      expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-    })
-
-    await it('should select groupIdToken trigger for group authorization', () => {
-      const context: OCPP20TransactionContext = {
-        authorizationMethod: 'groupIdToken',
-        source: 'local_authorization',
-      }
-
-      const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-        OCPP20TransactionEventEnumType.Started,
-        context
-      )
-
-      expect(triggerReason).toBe(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
-      )
-
-      expect(idTokenTrigger).toBe(OCPP20TriggerReasonEnumType.Authorized)
-      expect(cableTrigger).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-      expect(idTokenTrigger).not.toBe(cableTrigger)
-    })
-  })
-
-  // =========================================================================
-  // E03.FR.01: IdToken Inclusion in TransactionEvent
-  // =========================================================================
-  await describe('E03.FR.01 - IdToken in TransactionEvent', async () => {
-    await it('should include idToken in first TransactionEvent after authorization', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-      const idToken: OCPP20IdTokenType = {
-        idToken: 'VALID_TOKEN_E03_001',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Build Started event with idToken (E03.FR.01: IdToken must be in first event)
-      const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId,
-        { idToken }
-      )
-
-      expect(startedEvent.idToken).toBeDefined()
-      expect(startedEvent.idToken?.idToken).toBe('VALID_TOKEN_E03_001')
-      expect(startedEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.ISO14443)
-      expect(startedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(startedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-    })
-
-    await it('should not include idToken in subsequent events (E03.FR.01 compliance)', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-      const idToken: OCPP20IdTokenType = {
-        idToken: 'VALID_TOKEN_E03_002',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // First event includes idToken
-      const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId,
-        { idToken }
-      )
-
-      // Second event should NOT include idToken (flag is set after first inclusion)
-      const updatedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.ChargingStateChanged,
-        connectorId,
-        transactionId,
-        { chargingState: OCPP20ChargingStateEnumType.Charging, idToken }
-      )
-
-      expect(startedEvent.idToken).toBeDefined()
-      expect(updatedEvent.idToken).toBeUndefined()
-    })
-
-    await it('should support various IdToken types for E03 flow', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Test ISO14443 (RFID)
-      const rfidToken: OCPP20IdTokenType = {
-        idToken: 'RFID_TAG_123456',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-
-      const rfidEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId,
-        { idToken: rfidToken }
-      )
-
-      expect(rfidEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.ISO14443)
-
-      // Reset for eMAID test
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-      const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
-      if (connectorStatus != null) {
-        connectorStatus.transactionIdTokenSent = undefined
-      }
-
-      // Test eMAID (contract identifier)
-      const emaidToken: OCPP20IdTokenType = {
-        idToken: 'DE*ABC*E123456*1',
-        type: OCPP20IdTokenEnumType.eMAID,
-      }
-
-      const emaidEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        generateUUID(),
-        { idToken: emaidToken }
-      )
-
-      expect(emaidEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.eMAID)
-      expect(emaidEvent.idToken?.idToken).toBe('DE*ABC*E123456*1')
-    })
-  })
-
-  // =========================================================================
-  // Full E03 IdToken-First Transaction Lifecycle
-  // =========================================================================
-  await describe('Full IdToken-First Transaction Lifecycle', async () => {
-    await it('should support complete IdToken-first to cable to charging to end flow', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-      const idToken: OCPP20IdTokenType = {
-        idToken: 'LIFECYCLE_TOKEN_001',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // E03 Step 1: IdToken presented and authorized (Started with Authorized trigger)
-      const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId,
-        { idToken }
-      )
-
-      // E03 Step 2: Cable connected (Updated event)
-      const cableConnectedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        connectorId,
-        transactionId
-      )
-
-      // E03 Step 3: Charging starts
-      const chargingEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.ChargingStateChanged,
-        connectorId,
-        transactionId,
-        { chargingState: OCPP20ChargingStateEnumType.Charging }
-      )
-
-      // E03 Step 4: Transaction ends
-      const endedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        connectorId,
-        transactionId
-      )
-
-      // Validate event sequence
-      expect(authorizedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(authorizedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-      expect(authorizedEvent.idToken).toBeDefined()
-      expect(authorizedEvent.seqNo).toBe(0)
-
-      expect(cableConnectedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-      expect(cableConnectedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-      expect(cableConnectedEvent.idToken).toBeUndefined() // E03.FR.01: idToken only in first event
-      expect(cableConnectedEvent.seqNo).toBe(1)
-
-      expect(chargingEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-      expect(chargingEvent.seqNo).toBe(2)
-
-      expect(endedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
-      expect(endedEvent.seqNo).toBe(3)
-
-      // All events share same transaction ID
-      expect(authorizedEvent.transactionInfo.transactionId).toBe(transactionId)
-      expect(cableConnectedEvent.transactionInfo.transactionId).toBe(transactionId)
-      expect(chargingEvent.transactionInfo.transactionId).toBe(transactionId)
-      expect(endedEvent.transactionInfo.transactionId).toBe(transactionId)
-    })
-
-    await it('should differentiate E03 lifecycle from E02 Cable-First lifecycle', () => {
-      const connectorId = 1
-      const e03TransactionId = generateUUID()
-      const e02TransactionId = generateUUID()
-      const idToken: OCPP20IdTokenType = {
-        idToken: 'COMPARE_TOKEN_001',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-
-      // E03 IdToken-First: Starts with Authorized trigger
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-      const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
-      if (connectorStatus != null) {
-        connectorStatus.transactionIdTokenSent = undefined
-      }
-
-      const e03Start = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        e03TransactionId,
-        { idToken }
-      )
-
-      // E02 Cable-First: Starts with CablePluggedIn trigger
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-      if (connectorStatus != null) {
-        connectorStatus.transactionIdTokenSent = undefined
-      }
-
-      const e02Start = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        connectorId,
-        e02TransactionId
-      )
-
-      // Key difference: E03 starts with Authorized, E02 starts with CablePluggedIn
-      expect(e03Start.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-      expect(e02Start.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-
-      // E03 includes idToken in first event, E02 may not
-      expect(e03Start.idToken).toBeDefined()
-      expect(e02Start.idToken).toBeUndefined()
-    })
-  })
-
-  // =========================================================================
-  // E03.FR.05/06: EVConnectionTimeOut Handling
-  // =========================================================================
-  await describe('E03.FR.05/06 - EVConnectionTimeOut', async () => {
-    await it('should support authorization cancellation event (cable not connected)', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-      const idToken: OCPP20IdTokenType = {
-        idToken: 'TIMEOUT_TOKEN_001',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // E03.FR.05: User authorizes with IdToken
-      const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId,
-        { idToken }
-      )
-
-      // E03.FR.06: Cable not connected within timeout - transaction ends with Timeout
-      const timeoutEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.EVConnectTimeout,
-        connectorId,
-        transactionId
-      )
-
-      expect(authorizedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(authorizedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-
-      expect(timeoutEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
-      expect(timeoutEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.EVConnectTimeout)
-      expect(timeoutEvent.seqNo).toBe(1)
-
-      // Same transaction ID for both events
-      expect(authorizedEvent.transactionInfo.transactionId).toBe(
-        timeoutEvent.transactionInfo.transactionId
-      )
-    })
-
-    await it('should track sequence numbers correctly for timeout scenario', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Started (seqNo: 0)
-      const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      // Ended due to timeout (seqNo: 1)
-      const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.EVConnectTimeout,
-        connectorId,
-        transactionId
-      )
-
-      expect(startEvent.seqNo).toBe(0)
-      expect(endEvent.seqNo).toBe(1)
-    })
-  })
-
-  // =========================================================================
-  // Authorization Status Handling
-  // =========================================================================
-  await describe('Authorization Status in E03 Flow', 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
-      )
-
-      expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized)
-    })
-
-    await it('should handle transaction end after token revocation', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-      const idToken: OCPP20IdTokenType = {
-        idToken: 'REVOKED_TOKEN_001',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Transaction started with authorization
-      const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId,
-        { idToken }
-      )
-
-      // Transaction ended due to deauthorization (e.g., token revoked mid-session)
-      const revokedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.Deauthorized,
-        connectorId,
-        transactionId
-      )
-
-      expect(startEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(revokedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
-      expect(revokedEvent.triggerReason).toBe(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
-      )
-
-      expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.StopAuthorized)
-    })
-  })
-
-  // =========================================================================
-  // E03.FR.07/08: Sequence Numbers and Transaction ID
-  // =========================================================================
-  await describe('E03.FR.07/08 - Sequence Numbers and Transaction ID', async () => {
-    await it('should maintain continuous sequence numbers throughout E03 lifecycle', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-      const idToken: OCPP20IdTokenType = {
-        idToken: 'SEQ_TOKEN_001',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      const events = [
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.Authorized,
-          connectorId,
-          transactionId,
-          { idToken }
-        ),
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.CablePluggedIn,
-          connectorId,
-          transactionId
-        ),
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
-          connectorId,
-          transactionId
-        ),
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-          connectorId,
-          transactionId
-        ),
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Ended,
-          OCPP20TriggerReasonEnumType.StopAuthorized,
-          connectorId,
-          transactionId
-        ),
-      ]
-
-      // E03.FR.07: Sequence numbers must be continuous
-      events.forEach((event, index) => {
-        expect(event.seqNo).toBe(index)
-      })
-    })
-
-    await it('should use unique transaction ID (E03.FR.08)', () => {
-      const connectorId = 1
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      const transaction1Id = generateUUID()
-      const transaction2Id = generateUUID()
-
-      // E03.FR.08: transactionId MUST be unique
-      expect(transaction1Id).not.toBe(transaction2Id)
-
-      const event1 = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transaction1Id
-      )
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      const event2 = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transaction2Id
-      )
-
-      expect(event1.transactionInfo.transactionId).toBe(transaction1Id)
-      expect(event2.transactionInfo.transactionId).toBe(transaction2Id)
-      expect(event1.transactionInfo.transactionId).not.toBe(event2.transactionInfo.transactionId)
-    })
-  })
-
-  // =========================================================================
-  // Multiple Connector Independence
-  // =========================================================================
-  await describe('Multiple Connector Independence in E03 Flow', async () => {
-    await it('should handle independent E03 transactions on different connectors', () => {
-      const connector1 = 1
-      const connector2 = 2
-      const transaction1Id = generateUUID()
-      const transaction2Id = generateUUID()
-      const token1: OCPP20IdTokenType = {
-        idToken: 'USER_A_TOKEN',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      }
-      const token2: OCPP20IdTokenType = {
-        idToken: 'USER_B_TOKEN',
-        type: OCPP20IdTokenEnumType.eMAID,
-      }
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connector1)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connector2)
-
-      // User A authorizes on connector 1
-      const conn1Event1 = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connector1,
-        transaction1Id,
-        { idToken: token1 }
-      )
-
-      // User B authorizes on connector 2
-      const conn2Event1 = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connector2,
-        transaction2Id,
-        { idToken: token2 }
-      )
-
-      // User A plugs cable
-      const conn1Event2 = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        connector1,
-        transaction1Id
-      )
-
-      // User B plugs cable
-      const conn2Event2 = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        connector2,
-        transaction2Id
-      )
-
-      // Verify independent sequence numbers
-      expect(conn1Event1.seqNo).toBe(0)
-      expect(conn1Event2.seqNo).toBe(1)
-      expect(conn2Event1.seqNo).toBe(0)
-      expect(conn2Event2.seqNo).toBe(1)
-
-      // Verify independent transaction IDs
-      expect(conn1Event1.transactionInfo.transactionId).toBe(transaction1Id)
-      expect(conn2Event1.transactionInfo.transactionId).toBe(transaction2Id)
-      expect(transaction1Id).not.toBe(transaction2Id)
-
-      // Verify independent idTokens
-      expect(conn1Event1.idToken?.idToken).toBe('USER_A_TOKEN')
-      expect(conn2Event1.idToken?.idToken).toBe('USER_B_TOKEN')
-    })
-  })
-})
diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Offline.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Offline.test.ts
deleted file mode 100644 (file)
index 250815d..0000000
+++ /dev/null
@@ -1,533 +0,0 @@
-/**
- * @file Tests for OCPP20ServiceUtils TransactionEvent Offline
- * @description Unit tests for OCPP 2.0 offline TransactionEvent queueing (E02)
- */
-
-import { expect } from '@std/expect'
-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 {
-  OCPP20TransactionEventEnumType,
-  OCPP20TriggerReasonEnumType,
-  OCPPVersion,
-} from '../../../../src/types/index.js'
-import { Constants, generateUUID } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
-import { createChargingStation } from '../../../ChargingStationFactory.js'
-import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
-import {
-  type CapturedOCPPRequest,
-  createMockStationWithRequestTracking,
-  type MockStationWithTracking,
-} from './OCPP20TestUtils.js'
-
-await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () => {
-  let mockTracking: MockStationWithTracking
-  let mockChargingStation: ChargingStation
-  let sentRequests: CapturedOCPPRequest[]
-  let setOnline: (online: boolean) => void
-
-  beforeEach(() => {
-    mockTracking = createMockStationWithRequestTracking()
-    mockChargingStation = mockTracking.station
-    sentRequests = mockTracking.sentRequests
-    setOnline = mockTracking.setOnline
-  })
-
-  afterEach(() => {
-    for (let connectorId = 1; connectorId <= 3; connectorId++) {
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      if (connector != null) {
-        connector.transactionEventQueue = undefined
-      }
-    }
-    standardCleanup()
-  })
-
-  await describe('Queue formation when offline', async () => {
-    await it('should queue TransactionEvent when WebSocket is disconnected', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      setOnline(false)
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      const response = await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      expect(sentRequests.length).toBe(0)
-
-      expect(response.idTokenInfo).toBeUndefined()
-
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector?.transactionEventQueue).toBeDefined()
-      expect(connector.transactionEventQueue.length).toBe(1)
-      expect(connector.transactionEventQueue[0].seqNo).toBe(0)
-    })
-
-    await it('should queue multiple TransactionEvents in order when offline', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      setOnline(false)
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        connectorId,
-        transactionId
-      )
-
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector?.transactionEventQueue?.length).toBe(3)
-
-      expect(connector.transactionEventQueue[0].seqNo).toBe(0)
-      expect(connector.transactionEventQueue[1].seqNo).toBe(1)
-      expect(connector.transactionEventQueue[2].seqNo).toBe(2)
-
-      expect(connector.transactionEventQueue[0].request.eventType).toBe(
-        OCPP20TransactionEventEnumType.Started
-      )
-      expect(connector.transactionEventQueue[1].request.eventType).toBe(
-        OCPP20TransactionEventEnumType.Updated
-      )
-      expect(connector.transactionEventQueue[2].request.eventType).toBe(
-        OCPP20TransactionEventEnumType.Ended
-      )
-    })
-
-    await it('should preserve seqNo in queued events', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      setOnline(true)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      expect(sentRequests.length).toBe(1)
-      expect(sentRequests[0].payload.seqNo).toBe(0)
-
-      setOnline(false)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector?.transactionEventQueue?.length).toBe(2)
-      expect(connector.transactionEventQueue[0].seqNo).toBe(1)
-      expect(connector.transactionEventQueue[1].seqNo).toBe(2)
-    })
-
-    await it('should include timestamp in queued events', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      setOnline(false)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      const beforeQueue = new Date()
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-      const afterQueue = new Date()
-
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector?.transactionEventQueue?.[0]?.timestamp).toBeInstanceOf(Date)
-      expect(connector.transactionEventQueue[0].timestamp.getTime()).toBeGreaterThanOrEqual(
-        beforeQueue.getTime()
-      )
-      expect(connector.transactionEventQueue[0].timestamp.getTime()).toBeLessThanOrEqual(
-        afterQueue.getTime()
-      )
-    })
-  })
-
-  await describe('Queue draining when coming online', async () => {
-    await it('should send all queued events when sendQueuedTransactionEvents is called', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      setOnline(false)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      expect(sentRequests.length).toBe(0)
-
-      setOnline(true)
-
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
-
-      expect(sentRequests.length).toBe(2)
-      expect(sentRequests[0].payload.seqNo).toBe(0)
-      expect(sentRequests[1].payload.seqNo).toBe(1)
-    })
-
-    await it('should clear queue after sending', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      setOnline(false)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector?.transactionEventQueue?.length).toBe(1)
-
-      setOnline(true)
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
-
-      expect(connector.transactionEventQueue.length).toBe(0)
-    })
-
-    await it('should preserve FIFO order when draining queue', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      setOnline(false)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.ChargingStateChanged,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        connectorId,
-        transactionId
-      )
-
-      setOnline(true)
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
-
-      expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(sentRequests[1].payload.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-      expect(sentRequests[2].payload.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
-
-      expect(sentRequests[0].payload.seqNo).toBe(0)
-      expect(sentRequests[1].payload.seqNo).toBe(1)
-      expect(sentRequests[2].payload.seqNo).toBe(2)
-    })
-
-    await it('should handle empty queue gracefully', async () => {
-      const connectorId = 1
-
-      await expect(
-        OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
-      ).resolves.toBeUndefined()
-
-      expect(sentRequests.length).toBe(0)
-    })
-
-    await it('should handle null queue gracefully', async () => {
-      const connectorId = 1
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      connector.transactionEventQueue = undefined
-
-      await expect(
-        OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
-      ).resolves.toBeUndefined()
-
-      expect(sentRequests.length).toBe(0)
-    })
-  })
-
-  await describe('Sequence number continuity across queue boundary', async () => {
-    await it('should maintain seqNo continuity: online → offline → online', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      setOnline(true)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-      expect(sentRequests[0].payload.seqNo).toBe(0)
-
-      setOnline(false)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      setOnline(true)
-
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
-
-      expect(sentRequests[1].payload.seqNo).toBe(1)
-      expect(sentRequests[2].payload.seqNo).toBe(2)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        connectorId,
-        transactionId
-      )
-
-      expect(sentRequests[3].payload.seqNo).toBe(3)
-
-      for (let i = 0; i < sentRequests.length; i++) {
-        expect(sentRequests[i].payload.seqNo).toBe(i)
-      }
-    })
-  })
-
-  await describe('Multiple connectors with independent queues', async () => {
-    await it('should maintain separate queues for each connector', async () => {
-      const transactionId1 = generateUUID()
-      const transactionId2 = generateUUID()
-
-      setOnline(false)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        1,
-        transactionId1
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        2,
-        transactionId2
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        1,
-        transactionId1
-      )
-
-      const connector1 = mockChargingStation.getConnectorStatus(1)
-      const connector2 = mockChargingStation.getConnectorStatus(2)
-
-      expect(connector1?.transactionEventQueue?.length).toBe(2)
-      expect(connector2?.transactionEventQueue?.length).toBe(1)
-
-      expect(connector1.transactionEventQueue[0].request.transactionInfo.transactionId).toBe(
-        transactionId1
-      )
-      expect(connector2.transactionEventQueue[0].request.transactionInfo.transactionId).toBe(
-        transactionId2
-      )
-    })
-
-    await it('should drain queues independently per connector', async () => {
-      const transactionId1 = generateUUID()
-      const transactionId2 = generateUUID()
-
-      setOnline(false)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        1,
-        transactionId1
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        2,
-        transactionId2
-      )
-
-      setOnline(true)
-
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 1)
-
-      expect(sentRequests.length).toBe(1)
-      expect(sentRequests[0].payload.transactionInfo.transactionId).toBe(transactionId1)
-
-      const connector2 = mockChargingStation.getConnectorStatus(2)
-      expect(connector2?.transactionEventQueue?.length).toBe(1)
-
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 2)
-
-      expect(sentRequests.length).toBe(2)
-      expect(sentRequests[1].payload.transactionInfo.transactionId).toBe(transactionId2)
-    })
-  })
-
-  await describe('Error handling during queue drain', async () => {
-    await it('should continue sending remaining events if one fails', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-      let callCount = 0
-
-      const errorOnSecondMock = mock.fn(async () => {
-        callCount++
-        if (callCount === 2) {
-          throw new Error('Network error on second event')
-        }
-        return Promise.resolve({} as EmptyObject)
-      })
-
-      const errorStation = createChargingStation({
-        baseName: TEST_CHARGING_STATION_BASE_NAME,
-        connectorsCount: 1,
-        evseConfiguration: { evsesCount: 1 },
-        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-        ocppRequestService: {
-          requestHandler: errorOnSecondMock,
-        },
-        stationInfo: {
-          ocppStrictCompliance: true,
-          ocppVersion: OCPPVersion.VERSION_201,
-        },
-        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
-      })
-
-      errorStation.isWebSocketConnectionOpened = () => false
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(errorStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        errorStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        errorStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        errorStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        connectorId,
-        transactionId
-      )
-
-      errorStation.isWebSocketConnectionOpened = () => true
-
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(errorStation, connectorId)
-
-      expect(callCount).toBe(3)
-    })
-  })
-})
diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Periodic.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Periodic.test.ts
deleted file mode 100644 (file)
index 8f08756..0000000
+++ /dev/null
@@ -1,353 +0,0 @@
-/**
- * @file Tests for OCPP20ServiceUtils TransactionEvent Periodic
- * @description Unit tests for OCPP 2.0 periodic TransactionEvent at TxUpdatedInterval (E02)
- */
-
-import { expect } from '@std/expect'
-import { afterEach, beforeEach, describe, it } from 'node:test'
-
-import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js'
-
-import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
-import {
-  OCPP20TransactionEventEnumType,
-  OCPP20TriggerReasonEnumType,
-  OCPPVersion,
-} from '../../../../src/types/index.js'
-import { Constants, generateUUID } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
-import { createChargingStation } from '../../../ChargingStationFactory.js'
-import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
-import {
-  type CapturedOCPPRequest,
-  createMockStationWithRequestTracking,
-  type MockStationWithTracking,
-} from './OCPP20TestUtils.js'
-
-await describe('E02 - OCPP 2.0.1 Periodic TransactionEvent at TxUpdatedInterval', async () => {
-  let mockTracking: MockStationWithTracking
-  let mockChargingStation: ChargingStation
-  let sentRequests: CapturedOCPPRequest[]
-
-  beforeEach(() => {
-    mockTracking = createMockStationWithRequestTracking()
-    mockChargingStation = mockTracking.station
-    sentRequests = mockTracking.sentRequests
-  })
-
-  afterEach(() => {
-    // Clean up any running timers
-    for (let connectorId = 1; connectorId <= 3; connectorId++) {
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      if (connector?.transactionTxUpdatedSetInterval != null) {
-        clearInterval(connector.transactionTxUpdatedSetInterval)
-        connector.transactionTxUpdatedSetInterval = undefined
-      }
-    }
-    standardCleanup()
-  })
-
-  await describe('startTxUpdatedInterval', async () => {
-    await it('should not start timer for non-OCPP 2.0 stations', () => {
-      const ocpp16Station = createChargingStation({
-        baseName: TEST_CHARGING_STATION_BASE_NAME,
-        connectorsCount: 1,
-        stationInfo: {
-          ocppVersion: OCPPVersion.VERSION_16,
-        },
-      })
-
-      // Call startTxUpdatedInterval on OCPP 1.6 station
-      ocpp16Station.startTxUpdatedInterval(1, 60000)
-
-      // Verify no timer was started (method should return early)
-      const connector = ocpp16Station.getConnectorStatus(1)
-      expect(connector?.transactionTxUpdatedSetInterval).toBeUndefined()
-    })
-
-    await it('should not start timer when interval is zero', () => {
-      const connectorId = 1
-
-      // Simulate startTxUpdatedInterval with zero interval
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector).toBeDefined()
-
-      // Zero interval should not start timer
-      // This is verified by the implementation logging debug message
-      expect(connector.transactionTxUpdatedSetInterval).toBeUndefined()
-    })
-
-    await it('should not start timer when interval is negative', () => {
-      const connectorId = 1
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector).toBeDefined()
-
-      // Negative interval should not start timer
-      expect(connector.transactionTxUpdatedSetInterval).toBeUndefined()
-    })
-
-    await it('should handle non-existent connector gracefully', () => {
-      const nonExistentConnectorId = 999
-
-      // Should not throw for non-existent connector
-      expect(() => {
-        mockChargingStation.getConnectorStatus(nonExistentConnectorId)
-      }).not.toThrow()
-
-      // Should return undefined for non-existent connector
-      expect(mockChargingStation.getConnectorStatus(nonExistentConnectorId)).toBeUndefined()
-    })
-  })
-
-  await describe('Periodic TransactionEvent generation', async () => {
-    await it('should send TransactionEvent with MeterValuePeriodic trigger reason', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      // Reset sequence number
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Simulate sending periodic TransactionEvent (what the timer callback does)
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      // Verify the request was sent with correct trigger reason
-      expect(sentRequests.length).toBe(1)
-      expect(sentRequests[0].command).toBe('TransactionEvent')
-      expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-      expect(sentRequests[0].payload.triggerReason).toBe(
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic
-      )
-    })
-
-    await it('should increment seqNo for each periodic event', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      // Reset sequence number for new transaction
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Send initial Started event
-      const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-      expect(startEvent.seqNo).toBe(0)
-
-      // Send multiple periodic events (simulating timer ticks)
-      for (let i = 1; i <= 3; i++) {
-        const periodicEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-          connectorId,
-          transactionId
-        )
-        expect(periodicEvent.seqNo).toBe(i)
-      }
-
-      // Verify sequence numbers are continuous: 0, 1, 2, 3
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector?.transactionSeqNo).toBe(3)
-    })
-
-    await it('should maintain correct eventType (Updated) for periodic events', async () => {
-      const connectorId = 2
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Send periodic event
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      // Verify eventType is Updated (not Started or Ended)
-      expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-    })
-
-    await it('should include EVSE information in periodic events', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      // Verify EVSE info is present
-      expect(sentRequests[0].payload.evse).toBeDefined()
-      expect(sentRequests[0].payload.evse.id).toBe(connectorId)
-    })
-
-    await it('should include transactionInfo with correct transactionId', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      // Verify transactionInfo contains the transaction ID
-      expect(sentRequests[0].payload.transactionInfo).toBeDefined()
-      expect(sentRequests[0].payload.transactionInfo.transactionId).toBe(transactionId)
-    })
-  })
-
-  await describe('Timer lifecycle integration', async () => {
-    await it('should continue seqNo sequence across multiple periodic events', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      // Reset for new transaction
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Simulate full transaction lifecycle with periodic updates
-      // 1. Started event (seqNo: 0)
-      const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-      expect(startEvent.seqNo).toBe(0)
-
-      // 2. Multiple periodic updates (seqNo: 1, 2, 3)
-      for (let i = 1; i <= 3; i++) {
-        const updateEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-          connectorId,
-          transactionId
-        )
-        expect(updateEvent.seqNo).toBe(i)
-      }
-
-      // 3. Ended event (seqNo: 4)
-      const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        connectorId,
-        transactionId
-      )
-      expect(endEvent.seqNo).toBe(4)
-    })
-
-    await it('should handle multiple connectors with independent timers', () => {
-      const transactionId1 = generateUUID()
-      const transactionId2 = generateUUID()
-
-      // Reset sequence numbers for both connectors
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
-
-      // Build events for connector 1
-      const event1Start = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        1,
-        transactionId1
-      )
-      const event1Update = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        1,
-        transactionId1
-      )
-
-      // Build events for connector 2
-      const event2Start = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        2,
-        transactionId2
-      )
-      const event2Update = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        2,
-        transactionId2
-      )
-
-      // Verify independent sequence numbers
-      expect(event1Start.seqNo).toBe(0)
-      expect(event1Update.seqNo).toBe(1)
-      expect(event2Start.seqNo).toBe(0)
-      expect(event2Update.seqNo).toBe(1)
-
-      // Verify different transaction IDs
-      expect(event1Start.transactionInfo.transactionId).toBe(transactionId1)
-      expect(event2Start.transactionInfo.transactionId).toBe(transactionId2)
-    })
-  })
-
-  await describe('Error handling', async () => {
-    await it('should handle network errors gracefully during periodic event', async () => {
-      const errorMockChargingStation = createChargingStation({
-        baseName: TEST_CHARGING_STATION_BASE_NAME,
-        connectorsCount: 1,
-        evseConfiguration: { evsesCount: 1 },
-        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-        ocppRequestService: {
-          requestHandler: () => {
-            throw new Error('Network timeout')
-          },
-        },
-        stationInfo: {
-          ocppStrictCompliance: true,
-          ocppVersion: OCPPVersion.VERSION_201,
-        },
-        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
-      })
-
-      // Mock WebSocket as open
-      errorMockChargingStation.isWebSocketConnectionOpened = () => true
-
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      try {
-        await OCPP20ServiceUtils.sendTransactionEvent(
-          errorMockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-          connectorId,
-          transactionId
-        )
-        throw new Error('Should have thrown network error')
-      } catch (error) {
-        expect((error as Error).message).toContain('Network timeout')
-      }
-    })
-  })
-})
index a2f2d4382048ce5124152451c30a25e2ff6824ad..f38e1d4554c3d70867d9ea688aa1afa740132789 100644 (file)
@@ -1,13 +1,24 @@
 /**
  * @file Tests for OCPP20ServiceUtils TransactionEvent
- * @description Unit tests for OCPP 2.0 TransactionEvent building and trigger reasons (E01-E04)
+ * @description Consolidated unit tests for OCPP 2.0 TransactionEvent building and trigger reasons (E01-E04)
+ *
+ * This file consolidates tests from multiple variant files:
+ * - E01-E04 core TransactionEvent implementation
+ * - E02 Cable-First flow (cable plug event sequencing)
+ * - E03 IdToken-First flow (idToken presence in events)
+ * - Offline TransactionEvent queueing
+ * - Periodic TransactionEvent at TxUpdatedInterval
  */
 
 import { expect } from '@std/expect'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+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 {
+  ConnectorStatusEnum,
   OCPP20TransactionEventEnumType,
   OCPP20TriggerReasonEnumType,
   OCPPVersion,
@@ -15,6 +26,7 @@ import {
 import {
   OCPP20ChargingStateEnumType,
   OCPP20IdTokenEnumType,
+  type OCPP20IdTokenType,
   OCPP20ReasonEnumType,
   type OCPP20TransactionContext,
 } from '../../../../src/types/ocpp/2.0/Transaction.js'
@@ -22,7 +34,50 @@ import { Constants, generateUUID } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
 import { createChargingStation } from '../../../ChargingStationFactory.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
-import { createMockOCPP20TransactionTestStation, resetLimits } from './OCPP20TestUtils.js'
+import {
+  type CapturedOCPPRequest,
+  createMockOCPP20TransactionTestStation,
+  createMockStationWithRequestTracking,
+  type MockStationWithTracking,
+  resetConnectorTransactionState,
+  resetLimits,
+  TransactionContextFixtures,
+} from './OCPP20TestUtils.js'
+
+// ============================================================================
+// Transaction Flow Patterns for Parameterized Testing
+// ============================================================================
+
+/**
+ * Transaction flow variants for parameterized testing.
+ * Each flow represents a different transaction initiation pattern in OCPP 2.0.1.
+ */
+const TRANSACTION_FLOWS = [
+  {
+    description: 'E02 Cable-First',
+    expectedStartTrigger: OCPP20TriggerReasonEnumType.CablePluggedIn,
+    id: 'cableFirst',
+    includeIdToken: false,
+    name: 'E02 - Cable-First',
+    startContext: TransactionContextFixtures.cablePluggedIn(),
+  },
+  {
+    description: 'E03 IdToken-First',
+    expectedStartTrigger: OCPP20TriggerReasonEnumType.Authorized,
+    id: 'idTokenFirst',
+    includeIdToken: true,
+    name: 'E03 - IdToken-First',
+    startContext: TransactionContextFixtures.idTokenAuthorized(),
+  },
+  {
+    description: 'Remote Start',
+    expectedStartTrigger: OCPP20TriggerReasonEnumType.RemoteStart,
+    id: 'remoteStart',
+    includeIdToken: false,
+    name: 'Remote Start',
+    startContext: TransactionContextFixtures.remoteStart(),
+  },
+] as const
 
 await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async () => {
   let mockChargingStation: ReturnType<typeof createMockOCPP20TransactionTestStation>
@@ -173,7 +228,7 @@ await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async ()
         throw new Error('Should have thrown error for invalid identifier string')
       } catch (error) {
         expect((error as Error).message).toContain('Invalid transaction ID format')
-        expect(error.message).toContain('≤36 characters')
+        expect((error as Error).message).toContain('≤36 characters')
       }
     })
 
@@ -890,4 +945,1793 @@ await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async ()
       })
     })
   })
+
+  // ==========================================================================
+  // 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
+          )
+          expect(triggerReason).toBe(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(mockChargingStation, connectorId)
+
+          const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            expectedStartTrigger,
+            connectorId,
+            transactionId,
+            idToken != null ? { idToken } : undefined
+          )
+
+          expect(startedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+          expect(startedEvent.triggerReason).toBe(expectedStartTrigger)
+          expect(startedEvent.seqNo).toBe(0)
+          expect(startedEvent.transactionInfo.transactionId).toBe(transactionId)
+
+          if (includeIdToken) {
+            expect(startedEvent.idToken).toBeDefined()
+            expect(startedEvent.idToken?.idToken).toBe(`${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(mockChargingStation, connectorId)
+
+          // Step 1: Started event
+          const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            expectedStartTrigger,
+            connectorId,
+            transactionId,
+            idToken != null ? { idToken } : undefined
+          )
+
+          // Step 2: Charging state change
+          const chargingEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connectorId,
+            transactionId,
+            { chargingState: OCPP20ChargingStateEnumType.Charging }
+          )
+
+          // Step 3: Ended event
+          const endedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Ended,
+            OCPP20TriggerReasonEnumType.StopAuthorized,
+            connectorId,
+            transactionId
+          )
+
+          // Validate event sequence
+          expect(startedEvent.seqNo).toBe(0)
+          expect(chargingEvent.seqNo).toBe(1)
+          expect(endedEvent.seqNo).toBe(2)
+
+          // All events share same transaction ID
+          expect(startedEvent.transactionInfo.transactionId).toBe(transactionId)
+          expect(chargingEvent.transactionInfo.transactionId).toBe(transactionId)
+          expect(endedEvent.transactionInfo.transactionId).toBe(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(mockChargingStation, connector1)
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connector2)
+
+          // Start transaction on connector 1
+          const conn1Event1 = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            expectedStartTrigger,
+            connector1,
+            transaction1Id
+          )
+
+          // Start transaction on connector 2
+          const conn2Event1 = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            expectedStartTrigger,
+            connector2,
+            transaction2Id
+          )
+
+          // Update connector 1
+          const conn1Event2 = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connector1,
+            transaction1Id
+          )
+
+          // Update connector 2
+          const conn2Event2 = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connector2,
+            transaction2Id
+          )
+
+          // Verify independent sequence numbers
+          expect(conn1Event1.seqNo).toBe(0)
+          expect(conn1Event2.seqNo).toBe(1)
+          expect(conn2Event1.seqNo).toBe(0)
+          expect(conn2Event2.seqNo).toBe(1)
+
+          // Verify independent transaction IDs
+          expect(conn1Event1.transactionInfo.transactionId).toBe(transaction1Id)
+          expect(conn2Event1.transactionInfo.transactionId).toBe(transaction2Id)
+        })
+      })
+    }
+  })
+
+  // ==========================================================================
+  // E02 Cable-First Specific Tests
+  // ==========================================================================
+  await describe('E02 - Cable-First Transaction Flow', async () => {
+    beforeEach(() => {
+      resetConnectorTransactionState(mockChargingStation)
+    })
+
+    await describe('Cable Plug Event Sequencing', async () => {
+      await it('should sequence CablePluggedIn → EVDetected → Charging correctly', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // Step 1: Cable plugged in (Started)
+        const cablePluggedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.CablePluggedIn,
+          connectorId,
+          transactionId
+        )
+
+        // Step 2: EV detected (Updated)
+        const evDetectedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.EVDetected,
+          connectorId,
+          transactionId
+        )
+
+        // Step 3: Charging starts (Updated with ChargingStateChanged)
+        const chargingStartedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.ChargingStateChanged,
+          connectorId,
+          transactionId,
+          { chargingState: OCPP20ChargingStateEnumType.Charging }
+        )
+
+        // Assert sequence numbers follow correct order
+        expect(cablePluggedEvent.seqNo).toBe(0)
+        expect(evDetectedEvent.seqNo).toBe(1)
+        expect(chargingStartedEvent.seqNo).toBe(2)
+
+        // Assert all events share the same transaction ID
+        expect(cablePluggedEvent.transactionInfo.transactionId).toBe(transactionId)
+        expect(evDetectedEvent.transactionInfo.transactionId).toBe(transactionId)
+        expect(chargingStartedEvent.transactionInfo.transactionId).toBe(transactionId)
+
+        // Assert event types match expected pattern
+        expect(cablePluggedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+        expect(evDetectedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+        expect(chargingStartedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+      })
+
+      await it('should handle EVDeparted for cable removal ending transaction', () => {
+        const connectorId = 2
+        const transactionId = generateUUID()
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // Start transaction with cable plug
+        const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.CablePluggedIn,
+          connectorId,
+          transactionId
+        )
+
+        // End transaction with EV departure (cable removal)
+        const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.EVDeparted,
+          connectorId,
+          transactionId
+        )
+
+        // Assert proper sequencing for cable-initiated start and end
+        expect(startEvent.seqNo).toBe(0)
+        expect(startEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+        expect(endEvent.seqNo).toBe(1)
+        expect(endEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
+        expect(endEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
+      })
+    })
+
+    await describe('EV Detection Flow', async () => {
+      await it('should include EVDetected between cable plug and charging start', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // Build full cable-first flow
+        const events = [
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.CablePluggedIn,
+            connectorId,
+            transactionId
+          ),
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.EVDetected,
+            connectorId,
+            transactionId
+          ),
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            transactionId
+          ),
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connectorId,
+            transactionId,
+            { chargingState: OCPP20ChargingStateEnumType.Charging }
+          ),
+        ]
+
+        // Assert EVDetected comes after CablePluggedIn and before authorization
+        expect(events[0].triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+        expect(events[1].triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
+        expect(events[2].triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+        expect(events[3].triggerReason).toBe(OCPP20TriggerReasonEnumType.ChargingStateChanged)
+
+        // Assert continuous sequence numbers
+        for (let i = 0; i < events.length; i++) {
+          expect(events[i].seqNo).toBe(i)
+        }
+      })
+    })
+
+    await describe('Connector Status Transitions', async () => {
+      await it('should track connector status through cable-first lifecycle', () => {
+        const connectorId = 1
+
+        // Get connector status object
+        const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
+        expect(connectorStatus).toBeDefined()
+        if (connectorStatus == null) {
+          throw new Error('Connector status should be defined')
+        }
+
+        // Initial state: Available
+        connectorStatus.status = ConnectorStatusEnum.Available
+        expect(connectorStatus.status).toBe(ConnectorStatusEnum.Available)
+
+        // After cable plug: Preparing (implied by transaction start)
+        connectorStatus.status = ConnectorStatusEnum.Preparing
+        connectorStatus.transactionStarted = true
+        expect(connectorStatus.status).toBe(ConnectorStatusEnum.Preparing)
+        expect(connectorStatus.transactionStarted).toBe(true)
+
+        // After EV detected and auth: Charging
+        connectorStatus.status = ConnectorStatusEnum.Charging
+        expect(connectorStatus.status).toBe(ConnectorStatusEnum.Charging)
+
+        // After EV departed: Available again
+        connectorStatus.status = ConnectorStatusEnum.Available
+        connectorStatus.transactionStarted = false
+        expect(connectorStatus.status).toBe(ConnectorStatusEnum.Available)
+        expect(connectorStatus.transactionStarted).toBe(false)
+      })
+
+      await it('should preserve transaction ID through cable-first flow states', () => {
+        const connectorId = 2
+        const transactionId = generateUUID()
+
+        const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
+        expect(connectorStatus).toBeDefined()
+        if (connectorStatus == null) {
+          throw new Error('Connector status should be defined')
+        }
+
+        // Set transaction ID at start
+        connectorStatus.transactionId = transactionId
+        connectorStatus.transactionStarted = true
+        connectorStatus.status = ConnectorStatusEnum.Preparing
+
+        // Transition to charging
+        connectorStatus.status = ConnectorStatusEnum.Charging
+
+        // Transaction ID should persist through state changes
+        expect(connectorStatus.transactionId).toBe(transactionId)
+        expect(connectorStatus.transactionStarted).toBe(true)
+
+        // Transition to finished
+        connectorStatus.status = ConnectorStatusEnum.Finishing
+
+        // Still same transaction until fully ended
+        expect(connectorStatus.transactionId).toBe(transactionId)
+      })
+    })
+
+    await describe('Full Cable-First Transaction Lifecycle', async () => {
+      await it('should handle suspended charging states in cable-first flow', () => {
+        const connectorId = 3
+        const transactionId = generateUUID()
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // Cable-first flow with suspended state
+        const events = [
+          // 1. Cable plugged
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.CablePluggedIn,
+            connectorId,
+            transactionId
+          ),
+          // 2. Start charging
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connectorId,
+            transactionId,
+            { chargingState: OCPP20ChargingStateEnumType.Charging }
+          ),
+          // 3. Suspended by EV
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connectorId,
+            transactionId,
+            { chargingState: OCPP20ChargingStateEnumType.SuspendedEV }
+          ),
+          // 4. Resume charging
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connectorId,
+            transactionId,
+            { chargingState: OCPP20ChargingStateEnumType.Charging }
+          ),
+          // 5. EV departed
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Ended,
+            OCPP20TriggerReasonEnumType.EVDeparted,
+            connectorId,
+            transactionId
+          ),
+        ]
+
+        // Verify sequence numbers are continuous through suspend/resume
+        for (let i = 0; i < events.length; i++) {
+          expect(events[i].seqNo).toBe(i)
+        }
+
+        // Verify all share same transaction ID
+        for (const event of events) {
+          expect(event.transactionInfo.transactionId).toBe(transactionId)
+        }
+      })
+    })
+
+    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()
+        )
+
+        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+      })
+
+      await it('should select EVDetected from cable_action context with detected state', () => {
+        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+          OCPP20TransactionEventEnumType.Updated,
+          TransactionContextFixtures.evDetected()
+        )
+
+        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
+      })
+
+      await it('should select EVDeparted from cable_action context with unplugged state', () => {
+        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+          OCPP20TransactionEventEnumType.Ended,
+          TransactionContextFixtures.evDeparted()
+        )
+
+        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
+      })
+    })
+  })
+
+  // ==========================================================================
+  // E03 IdToken-First Specific Tests
+  // ==========================================================================
+  await describe('E03 - IdToken-First Pre-Authorization Flow', async () => {
+    beforeEach(() => {
+      resetConnectorTransactionState(mockChargingStation)
+    })
+
+    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
+        )
+
+        expect(triggerReason).toBe(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
+        )
+
+        expect(idTokenTrigger).toBe(OCPP20TriggerReasonEnumType.Authorized)
+        expect(cableTrigger).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+        expect(idTokenTrigger).not.toBe(cableTrigger)
+      })
+    })
+
+    await describe('E03.FR.01 - IdToken in TransactionEvent', async () => {
+      await it('should include idToken in first TransactionEvent after authorization', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+        const idToken: OCPP20IdTokenType = {
+          idToken: 'VALID_TOKEN_E03_001',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        }
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // Build Started event with idToken (E03.FR.01: IdToken must be in first event)
+        const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId,
+          { idToken }
+        )
+
+        expect(startedEvent.idToken).toBeDefined()
+        expect(startedEvent.idToken?.idToken).toBe('VALID_TOKEN_E03_001')
+        expect(startedEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.ISO14443)
+        expect(startedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+        expect(startedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+      })
+
+      await it('should not include idToken in subsequent events (E03.FR.01 compliance)', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+        const idToken: OCPP20IdTokenType = {
+          idToken: 'VALID_TOKEN_E03_002',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        }
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // First event includes idToken
+        const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId,
+          { idToken }
+        )
+
+        // Second event should NOT include idToken (flag is set after first inclusion)
+        const updatedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.ChargingStateChanged,
+          connectorId,
+          transactionId,
+          { chargingState: OCPP20ChargingStateEnumType.Charging, idToken }
+        )
+
+        expect(startedEvent.idToken).toBeDefined()
+        expect(updatedEvent.idToken).toBeUndefined()
+      })
+
+      await it('should support various IdToken types for E03 flow', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // Test ISO14443 (RFID)
+        const rfidToken: OCPP20IdTokenType = {
+          idToken: 'RFID_TAG_123456',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        }
+
+        const rfidEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId,
+          { idToken: rfidToken }
+        )
+
+        expect(rfidEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.ISO14443)
+
+        // Reset for eMAID test
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+        const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
+        if (connectorStatus != null) {
+          connectorStatus.transactionIdTokenSent = undefined
+        }
+
+        // Test eMAID (contract identifier)
+        const emaidToken: OCPP20IdTokenType = {
+          idToken: 'DE*ABC*E123456*1',
+          type: OCPP20IdTokenEnumType.eMAID,
+        }
+
+        const emaidEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          generateUUID(),
+          { idToken: emaidToken }
+        )
+
+        expect(emaidEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.eMAID)
+        expect(emaidEvent.idToken?.idToken).toBe('DE*ABC*E123456*1')
+      })
+    })
+
+    await describe('Full IdToken-First Transaction Lifecycle', async () => {
+      await it('should support complete IdToken-first to cable to charging to end flow', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+        const idToken: OCPP20IdTokenType = {
+          idToken: 'LIFECYCLE_TOKEN_001',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        }
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // E03 Step 1: IdToken presented and authorized (Started with Authorized trigger)
+        const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId,
+          { idToken }
+        )
+
+        // E03 Step 2: Cable connected (Updated event)
+        const cableConnectedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.CablePluggedIn,
+          connectorId,
+          transactionId
+        )
+
+        // E03 Step 3: Charging starts
+        const chargingEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.ChargingStateChanged,
+          connectorId,
+          transactionId,
+          { chargingState: OCPP20ChargingStateEnumType.Charging }
+        )
+
+        // E03 Step 4: Transaction ends
+        const endedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.StopAuthorized,
+          connectorId,
+          transactionId
+        )
+
+        // Validate event sequence
+        expect(authorizedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+        expect(authorizedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+        expect(authorizedEvent.idToken).toBeDefined()
+        expect(authorizedEvent.seqNo).toBe(0)
+
+        expect(cableConnectedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+        expect(cableConnectedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+        expect(cableConnectedEvent.idToken).toBeUndefined() // E03.FR.01: idToken only in first event
+        expect(cableConnectedEvent.seqNo).toBe(1)
+
+        expect(chargingEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+        expect(chargingEvent.seqNo).toBe(2)
+
+        expect(endedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
+        expect(endedEvent.seqNo).toBe(3)
+
+        // All events share same transaction ID
+        expect(authorizedEvent.transactionInfo.transactionId).toBe(transactionId)
+        expect(cableConnectedEvent.transactionInfo.transactionId).toBe(transactionId)
+        expect(chargingEvent.transactionInfo.transactionId).toBe(transactionId)
+        expect(endedEvent.transactionInfo.transactionId).toBe(transactionId)
+      })
+
+      await it('should differentiate E03 lifecycle from E02 Cable-First lifecycle', () => {
+        const connectorId = 1
+        const e03TransactionId = generateUUID()
+        const e02TransactionId = generateUUID()
+        const idToken: OCPP20IdTokenType = {
+          idToken: 'COMPARE_TOKEN_001',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        }
+
+        // E03 IdToken-First: Starts with Authorized trigger
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+        const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
+        if (connectorStatus != null) {
+          connectorStatus.transactionIdTokenSent = undefined
+        }
+
+        const e03Start = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          e03TransactionId,
+          { idToken }
+        )
+
+        // E02 Cable-First: Starts with CablePluggedIn trigger
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+        if (connectorStatus != null) {
+          connectorStatus.transactionIdTokenSent = undefined
+        }
+
+        const e02Start = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.CablePluggedIn,
+          connectorId,
+          e02TransactionId
+        )
+
+        // Key difference: E03 starts with Authorized, E02 starts with CablePluggedIn
+        expect(e03Start.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+        expect(e02Start.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+
+        // E03 includes idToken in first event, E02 may not
+        expect(e03Start.idToken).toBeDefined()
+        expect(e02Start.idToken).toBeUndefined()
+      })
+    })
+
+    await describe('E03.FR.05/06 - EVConnectionTimeOut', async () => {
+      await it('should support authorization cancellation event (cable not connected)', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+        const idToken: OCPP20IdTokenType = {
+          idToken: 'TIMEOUT_TOKEN_001',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        }
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // E03.FR.05: User authorizes with IdToken
+        const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId,
+          { idToken }
+        )
+
+        // E03.FR.06: Cable not connected within timeout - transaction ends with Timeout
+        const timeoutEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.EVConnectTimeout,
+          connectorId,
+          transactionId
+        )
+
+        expect(authorizedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+        expect(authorizedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+
+        expect(timeoutEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
+        expect(timeoutEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.EVConnectTimeout)
+        expect(timeoutEvent.seqNo).toBe(1)
+
+        // Same transaction ID for both events
+        expect(authorizedEvent.transactionInfo.transactionId).toBe(
+          timeoutEvent.transactionInfo.transactionId
+        )
+      })
+    })
+
+    await describe('Authorization Status in E03 Flow', 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
+        )
+
+        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized)
+      })
+
+      await it('should handle transaction end after token revocation', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+        const idToken: OCPP20IdTokenType = {
+          idToken: 'REVOKED_TOKEN_001',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        }
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        // Transaction started with authorization
+        const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId,
+          { idToken }
+        )
+
+        // Transaction ended due to deauthorization (e.g., token revoked mid-session)
+        const revokedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.Deauthorized,
+          connectorId,
+          transactionId
+        )
+
+        expect(startEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+        expect(revokedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
+        expect(revokedEvent.triggerReason).toBe(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
+        )
+
+        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.StopAuthorized)
+      })
+    })
+
+    await describe('E03.FR.07/08 - Sequence Numbers and Transaction ID', async () => {
+      await it('should maintain continuous sequence numbers throughout E03 lifecycle', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+        const idToken: OCPP20IdTokenType = {
+          idToken: 'SEQ_TOKEN_001',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        }
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        const events = [
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            transactionId,
+            { idToken }
+          ),
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.CablePluggedIn,
+            connectorId,
+            transactionId
+          ),
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connectorId,
+            transactionId
+          ),
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+            connectorId,
+            transactionId
+          ),
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Ended,
+            OCPP20TriggerReasonEnumType.StopAuthorized,
+            connectorId,
+            transactionId
+          ),
+        ]
+
+        // E03.FR.07: Sequence numbers must be continuous
+        events.forEach((event, index) => {
+          expect(event.seqNo).toBe(index)
+        })
+      })
+
+      await it('should use unique transaction ID (E03.FR.08)', () => {
+        const connectorId = 1
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        const transaction1Id = generateUUID()
+        const transaction2Id = generateUUID()
+
+        // E03.FR.08: transactionId MUST be unique
+        expect(transaction1Id).not.toBe(transaction2Id)
+
+        const event1 = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transaction1Id
+        )
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        const event2 = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transaction2Id
+        )
+
+        expect(event1.transactionInfo.transactionId).toBe(transaction1Id)
+        expect(event2.transactionInfo.transactionId).toBe(transaction2Id)
+        expect(event1.transactionInfo.transactionId).not.toBe(event2.transactionInfo.transactionId)
+      })
+    })
+  })
+})
+
+// ============================================================================
+// Offline TransactionEvent Queueing Tests
+// ============================================================================
+await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () => {
+  let mockTracking: MockStationWithTracking
+  let mockChargingStation: ChargingStation
+  let sentRequests: CapturedOCPPRequest[]
+  let setOnline: (online: boolean) => void
+
+  beforeEach(() => {
+    mockTracking = createMockStationWithRequestTracking()
+    mockChargingStation = mockTracking.station
+    sentRequests = mockTracking.sentRequests
+    setOnline = mockTracking.setOnline
+  })
+
+  afterEach(() => {
+    for (let connectorId = 1; connectorId <= 3; connectorId++) {
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      if (connector != null) {
+        connector.transactionEventQueue = undefined
+      }
+    }
+    standardCleanup()
+  })
+
+  await describe('Queue formation when offline', async () => {
+    await it('should queue TransactionEvent when WebSocket is disconnected', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      setOnline(false)
+
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      const response = await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+
+      expect(sentRequests.length).toBe(0)
+
+      expect(response.idTokenInfo).toBeUndefined()
+
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      expect(connector?.transactionEventQueue).toBeDefined()
+      expect(connector.transactionEventQueue.length).toBe(1)
+      expect(connector.transactionEventQueue[0].seqNo).toBe(0)
+    })
+
+    await it('should queue multiple TransactionEvents in order when offline', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      setOnline(false)
+
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Ended,
+        OCPP20TriggerReasonEnumType.StopAuthorized,
+        connectorId,
+        transactionId
+      )
+
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      expect(connector?.transactionEventQueue?.length).toBe(3)
+
+      expect(connector.transactionEventQueue[0].seqNo).toBe(0)
+      expect(connector.transactionEventQueue[1].seqNo).toBe(1)
+      expect(connector.transactionEventQueue[2].seqNo).toBe(2)
+
+      expect(connector.transactionEventQueue[0].request.eventType).toBe(
+        OCPP20TransactionEventEnumType.Started
+      )
+      expect(connector.transactionEventQueue[1].request.eventType).toBe(
+        OCPP20TransactionEventEnumType.Updated
+      )
+      expect(connector.transactionEventQueue[2].request.eventType).toBe(
+        OCPP20TransactionEventEnumType.Ended
+      )
+    })
+
+    await it('should preserve seqNo in queued events', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      setOnline(true)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+
+      expect(sentRequests.length).toBe(1)
+      expect(sentRequests[0].payload.seqNo).toBe(0)
+
+      setOnline(false)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      expect(connector?.transactionEventQueue?.length).toBe(2)
+      expect(connector.transactionEventQueue[0].seqNo).toBe(1)
+      expect(connector.transactionEventQueue[1].seqNo).toBe(2)
+    })
+
+    await it('should include timestamp in queued events', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      setOnline(false)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      const beforeQueue = new Date()
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+      const afterQueue = new Date()
+
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      expect(connector?.transactionEventQueue?.[0]?.timestamp).toBeInstanceOf(Date)
+      expect(connector.transactionEventQueue[0].timestamp.getTime()).toBeGreaterThanOrEqual(
+        beforeQueue.getTime()
+      )
+      expect(connector.transactionEventQueue[0].timestamp.getTime()).toBeLessThanOrEqual(
+        afterQueue.getTime()
+      )
+    })
+  })
+
+  await describe('Queue draining when coming online', async () => {
+    await it('should send all queued events when sendQueuedTransactionEvents is called', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      setOnline(false)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      expect(sentRequests.length).toBe(0)
+
+      setOnline(true)
+
+      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+
+      expect(sentRequests.length).toBe(2)
+      expect(sentRequests[0].payload.seqNo).toBe(0)
+      expect(sentRequests[1].payload.seqNo).toBe(1)
+    })
+
+    await it('should clear queue after sending', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      setOnline(false)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      expect(connector?.transactionEventQueue?.length).toBe(1)
+
+      setOnline(true)
+      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+
+      expect(connector.transactionEventQueue.length).toBe(0)
+    })
+
+    await it('should preserve FIFO order when draining queue', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      setOnline(false)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.ChargingStateChanged,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Ended,
+        OCPP20TriggerReasonEnumType.StopAuthorized,
+        connectorId,
+        transactionId
+      )
+
+      setOnline(true)
+      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+
+      expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+      expect(sentRequests[1].payload.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+      expect(sentRequests[2].payload.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
+
+      expect(sentRequests[0].payload.seqNo).toBe(0)
+      expect(sentRequests[1].payload.seqNo).toBe(1)
+      expect(sentRequests[2].payload.seqNo).toBe(2)
+    })
+
+    await it('should handle empty queue gracefully', async () => {
+      const connectorId = 1
+
+      await expect(
+        OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+      ).resolves.toBeUndefined()
+
+      expect(sentRequests.length).toBe(0)
+    })
+
+    await it('should handle null queue gracefully', async () => {
+      const connectorId = 1
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      connector.transactionEventQueue = undefined
+
+      await expect(
+        OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+      ).resolves.toBeUndefined()
+
+      expect(sentRequests.length).toBe(0)
+    })
+  })
+
+  await describe('Sequence number continuity across queue boundary', async () => {
+    await it('should maintain seqNo continuity: online → offline → online', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      setOnline(true)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+      expect(sentRequests[0].payload.seqNo).toBe(0)
+
+      setOnline(false)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      setOnline(true)
+
+      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+
+      expect(sentRequests[1].payload.seqNo).toBe(1)
+      expect(sentRequests[2].payload.seqNo).toBe(2)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Ended,
+        OCPP20TriggerReasonEnumType.StopAuthorized,
+        connectorId,
+        transactionId
+      )
+
+      expect(sentRequests[3].payload.seqNo).toBe(3)
+
+      for (let i = 0; i < sentRequests.length; i++) {
+        expect(sentRequests[i].payload.seqNo).toBe(i)
+      }
+    })
+  })
+
+  await describe('Multiple connectors with independent queues', async () => {
+    await it('should maintain separate queues for each connector', async () => {
+      const transactionId1 = generateUUID()
+      const transactionId2 = generateUUID()
+
+      setOnline(false)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        1,
+        transactionId1
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        2,
+        transactionId2
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        1,
+        transactionId1
+      )
+
+      const connector1 = mockChargingStation.getConnectorStatus(1)
+      const connector2 = mockChargingStation.getConnectorStatus(2)
+
+      expect(connector1?.transactionEventQueue?.length).toBe(2)
+      expect(connector2?.transactionEventQueue?.length).toBe(1)
+
+      expect(connector1.transactionEventQueue[0].request.transactionInfo.transactionId).toBe(
+        transactionId1
+      )
+      expect(connector2.transactionEventQueue[0].request.transactionInfo.transactionId).toBe(
+        transactionId2
+      )
+    })
+
+    await it('should drain queues independently per connector', async () => {
+      const transactionId1 = generateUUID()
+      const transactionId2 = generateUUID()
+
+      setOnline(false)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        1,
+        transactionId1
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        2,
+        transactionId2
+      )
+
+      setOnline(true)
+
+      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 1)
+
+      expect(sentRequests.length).toBe(1)
+      expect(sentRequests[0].payload.transactionInfo.transactionId).toBe(transactionId1)
+
+      const connector2 = mockChargingStation.getConnectorStatus(2)
+      expect(connector2?.transactionEventQueue?.length).toBe(1)
+
+      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 2)
+
+      expect(sentRequests.length).toBe(2)
+      expect(sentRequests[1].payload.transactionInfo.transactionId).toBe(transactionId2)
+    })
+  })
+
+  await describe('Error handling during queue drain', async () => {
+    await it('should continue sending remaining events if one fails', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+      let callCount = 0
+
+      const errorOnSecondMock = mock.fn(async () => {
+        callCount++
+        if (callCount === 2) {
+          throw new Error('Network error on second event')
+        }
+        return Promise.resolve({} as EmptyObject)
+      })
+
+      const errorStation = createChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 1,
+        evseConfiguration: { evsesCount: 1 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        ocppRequestService: {
+          requestHandler: errorOnSecondMock,
+        },
+        stationInfo: {
+          ocppStrictCompliance: true,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+
+      errorStation.isWebSocketConnectionOpened = () => false
+
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(errorStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        errorStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        errorStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        errorStation,
+        OCPP20TransactionEventEnumType.Ended,
+        OCPP20TriggerReasonEnumType.StopAuthorized,
+        connectorId,
+        transactionId
+      )
+
+      errorStation.isWebSocketConnectionOpened = () => true
+
+      await OCPP20ServiceUtils.sendQueuedTransactionEvents(errorStation, connectorId)
+
+      expect(callCount).toBe(3)
+    })
+  })
+})
+
+// ============================================================================
+// Periodic TransactionEvent Tests
+// ============================================================================
+await describe('E02 - OCPP 2.0.1 Periodic TransactionEvent at TxUpdatedInterval', async () => {
+  let mockTracking: MockStationWithTracking
+  let mockChargingStation: ChargingStation
+  let sentRequests: CapturedOCPPRequest[]
+
+  beforeEach(() => {
+    mockTracking = createMockStationWithRequestTracking()
+    mockChargingStation = mockTracking.station
+    sentRequests = mockTracking.sentRequests
+  })
+
+  afterEach(() => {
+    // Clean up any running timers
+    for (let connectorId = 1; connectorId <= 3; connectorId++) {
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      if (connector?.transactionTxUpdatedSetInterval != null) {
+        clearInterval(connector.transactionTxUpdatedSetInterval)
+        connector.transactionTxUpdatedSetInterval = undefined
+      }
+    }
+    standardCleanup()
+  })
+
+  await describe('startTxUpdatedInterval', async () => {
+    await it('should not start timer for non-OCPP 2.0 stations', () => {
+      const ocpp16Station = createChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 1,
+        stationInfo: {
+          ocppVersion: OCPPVersion.VERSION_16,
+        },
+      })
+
+      // Call startTxUpdatedInterval on OCPP 1.6 station
+      ocpp16Station.startTxUpdatedInterval(1, 60000)
+
+      // Verify no timer was started (method should return early)
+      const connector = ocpp16Station.getConnectorStatus(1)
+      expect(connector?.transactionTxUpdatedSetInterval).toBeUndefined()
+    })
+
+    await it('should not start timer when interval is zero', () => {
+      const connectorId = 1
+
+      // Simulate startTxUpdatedInterval with zero interval
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      expect(connector).toBeDefined()
+
+      // Zero interval should not start timer
+      // This is verified by the implementation logging debug message
+      expect(connector.transactionTxUpdatedSetInterval).toBeUndefined()
+    })
+
+    await it('should not start timer when interval is negative', () => {
+      const connectorId = 1
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      expect(connector).toBeDefined()
+
+      // Negative interval should not start timer
+      expect(connector.transactionTxUpdatedSetInterval).toBeUndefined()
+    })
+
+    await it('should handle non-existent connector gracefully', () => {
+      const nonExistentConnectorId = 999
+
+      // Should not throw for non-existent connector
+      expect(() => {
+        mockChargingStation.getConnectorStatus(nonExistentConnectorId)
+      }).not.toThrow()
+
+      // Should return undefined for non-existent connector
+      expect(mockChargingStation.getConnectorStatus(nonExistentConnectorId)).toBeUndefined()
+    })
+  })
+
+  await describe('Periodic TransactionEvent generation', async () => {
+    await it('should send TransactionEvent with MeterValuePeriodic trigger reason', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      // Reset sequence number
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      // Simulate sending periodic TransactionEvent (what the timer callback does)
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      // Verify the request was sent with correct trigger reason
+      expect(sentRequests.length).toBe(1)
+      expect(sentRequests[0].command).toBe('TransactionEvent')
+      expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+      expect(sentRequests[0].payload.triggerReason).toBe(
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic
+      )
+    })
+
+    await it('should increment seqNo for each periodic event', () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      // Reset sequence number for new transaction
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      // Send initial Started event
+      const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+      expect(startEvent.seqNo).toBe(0)
+
+      // Send multiple periodic events (simulating timer ticks)
+      for (let i = 1; i <= 3; i++) {
+        const periodicEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          connectorId,
+          transactionId
+        )
+        expect(periodicEvent.seqNo).toBe(i)
+      }
+
+      // Verify sequence numbers are continuous: 0, 1, 2, 3
+      const connector = mockChargingStation.getConnectorStatus(connectorId)
+      expect(connector?.transactionSeqNo).toBe(3)
+    })
+
+    await it('should maintain correct eventType (Updated) for periodic events', async () => {
+      const connectorId = 2
+      const transactionId = generateUUID()
+
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      // Send periodic event
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      // Verify eventType is Updated (not Started or Ended)
+      expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+    })
+
+    await it('should include EVSE information in periodic events', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      // Verify EVSE info is present
+      expect(sentRequests[0].payload.evse).toBeDefined()
+      expect(sentRequests[0].payload.evse.id).toBe(connectorId)
+    })
+
+    await it('should include transactionInfo with correct transactionId', async () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      await OCPP20ServiceUtils.sendTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        connectorId,
+        transactionId
+      )
+
+      // Verify transactionInfo contains the transaction ID
+      expect(sentRequests[0].payload.transactionInfo).toBeDefined()
+      expect(sentRequests[0].payload.transactionInfo.transactionId).toBe(transactionId)
+    })
+  })
+
+  await describe('Timer lifecycle integration', async () => {
+    await it('should continue seqNo sequence across multiple periodic events', () => {
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      // Reset for new transaction
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+      // Simulate full transaction lifecycle with periodic updates
+      // 1. Started event (seqNo: 0)
+      const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        connectorId,
+        transactionId
+      )
+      expect(startEvent.seqNo).toBe(0)
+
+      // 2. Multiple periodic updates (seqNo: 1, 2, 3)
+      for (let i = 1; i <= 3; i++) {
+        const updateEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          connectorId,
+          transactionId
+        )
+        expect(updateEvent.seqNo).toBe(i)
+      }
+
+      // 3. Ended event (seqNo: 4)
+      const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Ended,
+        OCPP20TriggerReasonEnumType.StopAuthorized,
+        connectorId,
+        transactionId
+      )
+      expect(endEvent.seqNo).toBe(4)
+    })
+
+    await it('should handle multiple connectors with independent timers', () => {
+      const transactionId1 = generateUUID()
+      const transactionId2 = generateUUID()
+
+      // Reset sequence numbers for both connectors
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
+      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
+
+      // Build events for connector 1
+      const event1Start = OCPP20ServiceUtils.buildTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        1,
+        transactionId1
+      )
+      const event1Update = OCPP20ServiceUtils.buildTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        1,
+        transactionId1
+      )
+
+      // Build events for connector 2
+      const event2Start = OCPP20ServiceUtils.buildTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Started,
+        OCPP20TriggerReasonEnumType.Authorized,
+        2,
+        transactionId2
+      )
+      const event2Update = OCPP20ServiceUtils.buildTransactionEvent(
+        mockChargingStation,
+        OCPP20TransactionEventEnumType.Updated,
+        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+        2,
+        transactionId2
+      )
+
+      // Verify independent sequence numbers
+      expect(event1Start.seqNo).toBe(0)
+      expect(event1Update.seqNo).toBe(1)
+      expect(event2Start.seqNo).toBe(0)
+      expect(event2Update.seqNo).toBe(1)
+
+      // Verify different transaction IDs
+      expect(event1Start.transactionInfo.transactionId).toBe(transactionId1)
+      expect(event2Start.transactionInfo.transactionId).toBe(transactionId2)
+    })
+  })
+
+  await describe('Error handling', async () => {
+    await it('should handle network errors gracefully during periodic event', async () => {
+      const errorMockChargingStation = createChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 1,
+        evseConfiguration: { evsesCount: 1 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        ocppRequestService: {
+          requestHandler: () => {
+            throw new Error('Network timeout')
+          },
+        },
+        stationInfo: {
+          ocppStrictCompliance: true,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+
+      // Mock WebSocket as open
+      errorMockChargingStation.isWebSocketConnectionOpened = () => true
+
+      const connectorId = 1
+      const transactionId = generateUUID()
+
+      try {
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          errorMockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          connectorId,
+          transactionId
+        )
+        throw new Error('Should have thrown network error')
+      } catch (error) {
+        expect((error as Error).message).toContain('Network timeout')
+      }
+    })
+  })
 })
index 8877e979a4ec3908b02ae9d71e45a57b38f5cc30..4fd434ce9667db08754213f374334c10d812426b 100644 (file)
@@ -1,7 +1,8 @@
 /**
  * @file Tests for OCPPAuthIntegration
- * @description Unit tests for OCPP authentication integration with deterministic mocked responses
+ * @description Integration tests for OCPP authentication flows across service, adapters, cache, and strategies
  */
+
 import { expect } from '@std/expect'
 import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
@@ -10,21 +11,15 @@ import type { ChargingStation } from '../../../../src/charging-station/ChargingS
 import { OCPPAuthServiceImpl } from '../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.js'
 import {
   AuthContext,
-  AuthenticationMethod,
   AuthorizationStatus,
   IdentifierType,
 } from '../../../../src/charging-station/ocpp/auth/types/AuthTypes.js'
 import { OCPPVersion } from '../../../../src/types/ocpp/OCPPVersion.js'
 import { createChargingStation } from '../../../ChargingStationFactory.js'
 import {
-  createMockAuthorizationResult,
   createMockAuthRequest,
-  createMockAuthService,
   createMockOCPP16Identifier,
   createMockOCPP20Identifier,
-  createTestAuthConfig,
-  expectAcceptedAuthorization,
-  expectRejectedAuthorization,
 } from './helpers/MockFactories.js'
 
 await describe('OCPP Authentication Integration Tests', async () => {
@@ -59,111 +54,6 @@ await describe('OCPP Authentication Integration Tests', async () => {
     mock.reset()
   })
 
-  await describe('Service Initialization', async () => {
-    await it('should create auth service for OCPP 1.6 station', () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-
-      // Service should be created with valid configuration
-      expect(authService.getConfiguration()).toBeDefined()
-      expect(typeof authService.getConfiguration().authorizationTimeout).toBe('number')
-
-      const stats = authService.getAuthenticationStats()
-      expect(stats.ocppVersion).toBe(OCPPVersion.VERSION_16)
-    })
-
-    await it('should create auth service for OCPP 2.0 station', () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation20)
-
-      // Service should be created with valid configuration
-      expect(authService.getConfiguration()).toBeDefined()
-      expect(typeof authService.getConfiguration().authorizationTimeout).toBe('number')
-
-      const stats = authService.getAuthenticationStats()
-      expect(stats.ocppVersion).toBe(OCPPVersion.VERSION_20)
-    })
-
-    await it('should create mock auth service with deterministic responses', async () => {
-      const mockService = createMockAuthService()
-
-      const request = createMockAuthRequest()
-      const result = await mockService.authorize(request)
-
-      expect(result.status).toBe(AuthorizationStatus.ACCEPTED)
-      expect(result.isOffline).toBe(false)
-      expectAcceptedAuthorization(result)
-    })
-  })
-
-  await describe('Configuration Management', async () => {
-    await it('should update and retrieve configuration for OCPP 1.6', async () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-      const originalConfig = authService.getConfiguration()
-
-      const updates = {
-        authorizationTimeout: 60,
-        localAuthListEnabled: false,
-        maxCacheEntries: 2000,
-      }
-
-      await authService.updateConfiguration(updates)
-      const updatedConfig = authService.getConfiguration()
-
-      expect(updatedConfig.authorizationTimeout).toBe(60)
-      expect(updatedConfig.localAuthListEnabled).toBe(false)
-      expect(updatedConfig.maxCacheEntries).toBe(2000)
-
-      // Restore original configuration
-      await authService.updateConfiguration(originalConfig)
-    })
-
-    await it('should update and retrieve configuration for OCPP 2.0', async () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation20)
-      const originalConfig = authService.getConfiguration()
-
-      const updates = {
-        authorizationTimeout: 45,
-        certificateAuthEnabled: true,
-        remoteAuthorization: true,
-      }
-
-      await authService.updateConfiguration(updates)
-      const updatedConfig = authService.getConfiguration()
-
-      expect(updatedConfig.authorizationTimeout).toBe(45)
-      expect(updatedConfig.certificateAuthEnabled).toBe(true)
-      expect(updatedConfig.remoteAuthorization).toBe(true)
-
-      // Restore original configuration
-      await authService.updateConfiguration(originalConfig)
-    })
-  })
-
-  await describe('Strategy Selection', async () => {
-    await it('should return available strategies list (empty before initialization)', () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-      const strategies = authService.getAvailableStrategies()
-
-      // Before initialize() is called, strategies list is empty
-      expect(Array.isArray(strategies)).toBe(true)
-      expect(strategies.length).toBe(0)
-    })
-
-    await it('should detect identifier support correctly', () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-      const identifier = createMockOCPP16Identifier('SUPPORT_TEST_ID')
-
-      const isSupported = authService.isSupported(identifier)
-      expect(typeof isSupported).toBe('boolean')
-    })
-
-    await it('should get strategy by name returns undefined for non-existent', () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-
-      const strategy = authService.getStrategy('non-existent')
-      expect(strategy).toBeUndefined()
-    })
-  })
-
   await describe('OCPP 1.6 Authentication Flow', async () => {
     await it('should authenticate with valid identifier', async () => {
       const authService = new OCPPAuthServiceImpl(mockChargingStation16)
@@ -257,8 +147,8 @@ await describe('OCPP Authentication Integration Tests', async () => {
     })
   })
 
-  await describe('Error Handling', async () => {
-    await it('should handle invalid identifier gracefully', async () => {
+  await describe('Integration Error Scenarios', async () => {
+    await it('should handle invalid identifier gracefully during auth flow', async () => {
       const authService = new OCPPAuthServiceImpl(mockChargingStation16)
       const request = createMockAuthRequest({
         connectorId: 999, // Invalid connector
@@ -276,72 +166,10 @@ await describe('OCPP Authentication Integration Tests', async () => {
       expect(result).toBeDefined()
       expect(result.status).not.toBe(AuthorizationStatus.ACCEPTED)
     })
-
-    await it('should throw error for non-existent strategy', async () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-      const request = createMockAuthRequest()
-
-      await expect(
-        authService.authorizeWithStrategy('non-existent-strategy', request)
-      ).rejects.toThrow()
-    })
-  })
-
-  await describe('Cache Operations', async () => {
-    await it('should invalidate cache without error', async () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-      const identifier = createMockOCPP16Identifier('CACHE_TEST_ID')
-
-      // Should not throw
-      await authService.invalidateCache(identifier)
-    })
-
-    await it('should clear cache without error', async () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-
-      // Should not throw
-      await authService.clearCache()
-    })
-
-    await it('should check local authorization after cache operations', async () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-      const identifier = createMockOCPP16Identifier('LOCAL_AUTH_TEST')
-
-      await authService.clearCache()
-      const localResult = await authService.isLocallyAuthorized(identifier, 1)
-
-      // Result can be undefined (not locally authorized) or an AuthorizationResult
-      if (localResult !== undefined) {
-        expect(localResult.timestamp).toBeInstanceOf(Date)
-      }
-    })
   })
 
-  await describe('Performance and Statistics', async () => {
-    await it('should test connectivity successfully', async () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-
-      const connectivity = await authService.testConnectivity()
-      expect(typeof connectivity).toBe('boolean')
-    })
-
-    await it('should retrieve valid statistics', async () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-
-      const stats = await authService.getStats()
-      expect(typeof stats.totalRequests).toBe('number')
-      expect(stats.totalRequests).toBeGreaterThanOrEqual(0)
-    })
-
-    await it('should retrieve authentication statistics', () => {
-      const authService = new OCPPAuthServiceImpl(mockChargingStation16)
-
-      const authStats = authService.getAuthenticationStats()
-      expect(Array.isArray(authStats.availableStrategies)).toBe(true)
-      expect(authStats.ocppVersion).toBeDefined()
-    })
-
-    await it('should handle concurrent authentication requests', async () => {
+  await describe('Concurrent Operations', async () => {
+    await it('should handle concurrent authentication requests with mixed contexts', async () => {
       const authService = new OCPPAuthServiceImpl(mockChargingStation16)
       const requestCount = 10
       const promises = []
@@ -349,14 +177,15 @@ await describe('OCPP Authentication Integration Tests', async () => {
       for (let i = 0; i < requestCount; i++) {
         const request = createMockAuthRequest({
           connectorId: 1,
-          identifier: createMockOCPP16Identifier(`PERF_TEST_${String(i)}`),
+          context: i % 2 === 0 ? AuthContext.TRANSACTION_START : AuthContext.TRANSACTION_STOP,
+          identifier: createMockOCPP16Identifier(`CONCURRENT_${String(i)}`),
         })
         promises.push(authService.authenticate(request))
       }
 
       const results = await Promise.all(promises)
 
-      // All requests should complete
+      // All requests should complete successfully
       expect(results.length).toBe(requestCount)
       for (const result of results) {
         expect(result).toBeDefined()
@@ -364,59 +193,4 @@ await describe('OCPP Authentication Integration Tests', async () => {
       }
     })
   })
-
-  await describe('Mock Factory Integration', async () => {
-    await it('should use mock authorization result correctly', () => {
-      const mockResult = createMockAuthorizationResult()
-
-      expectAcceptedAuthorization(mockResult, AuthenticationMethod.LOCAL_LIST)
-    })
-
-    await it('should use mock rejected result correctly', () => {
-      const mockResult = createMockAuthorizationResult({
-        status: AuthorizationStatus.INVALID,
-      })
-
-      expectRejectedAuthorization(mockResult, AuthorizationStatus.INVALID)
-    })
-
-    await it('should create valid test auth config', () => {
-      const config = createTestAuthConfig({
-        localAuthListEnabled: true,
-        remoteAuthorization: true,
-      })
-
-      expect(config.localAuthListEnabled).toBe(true)
-      expect(config.remoteAuthorization).toBe(true)
-      expect(config.authorizationTimeout).toBeDefined()
-    })
-
-    await it('should create mock identifiers for both OCPP versions', () => {
-      const ocpp16Id = createMockOCPP16Identifier('TEST_16')
-      const ocpp20Id = createMockOCPP20Identifier('TEST_20')
-
-      expect(ocpp16Id.ocppVersion).toBe(OCPPVersion.VERSION_16)
-      expect(ocpp20Id.ocppVersion).toBe(OCPPVersion.VERSION_20)
-      expect(ocpp16Id.type).toBe(IdentifierType.ID_TAG)
-      expect(ocpp20Id.type).toBe(IdentifierType.ID_TAG)
-    })
-
-    await it('should create mock auth service with overrides', async () => {
-      const mockService = createMockAuthService({
-        authorize: () =>
-          Promise.resolve({
-            isOffline: false,
-            method: AuthenticationMethod.REMOTE_AUTHORIZATION,
-            status: AuthorizationStatus.BLOCKED,
-            timestamp: new Date(),
-          }),
-      })
-
-      const request = createMockAuthRequest()
-      const result = await mockService.authorize(request)
-
-      expect(result.status).toBe(AuthorizationStatus.BLOCKED)
-      expect(result.method).toBe(AuthenticationMethod.REMOTE_AUTHORIZATION)
-    })
-  })
 })
diff --git a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter-Offline.test.ts b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter-Offline.test.ts
deleted file mode 100644 (file)
index 679a52c..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * @file Tests for OCPP20AuthAdapter Offline
- * @description Unit tests for OCPP 2.0 offline authorization scenarios (G03.FR.02)
- */
-/**
- * G03.FR.02 - OCPP 2.0 Offline Authorization Tests
- *
- * Tests for offline authorization scenarios:
- * - G03.FR.02.001: Authorize locally when offline with LocalAuthListEnabled=true
- * - G03.FR.02.002: Reject when offline and local auth disabled
- * - G03.FR.02.003: Reconnection sync auth state
- *
- * OCPP 2.0.1 Specification References:
- * - Section G03 - Authorization
- * - AuthCtrlr.LocalAuthorizeOffline variable
- * - AuthCtrlr.LocalAuthListEnabled variable
- */
-
-import { expect } from '@std/expect'
-import { afterEach, beforeEach, describe, it, mock } from 'node:test'
-
-import type { ChargingStation } from '../../../../../src/charging-station/ChargingStation.js'
-
-import { OCPP20AuthAdapter } from '../../../../../src/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.js'
-import { OCPPVersion } from '../../../../../src/types/ocpp/OCPPVersion.js'
-
-await describe('OCPP20AuthAdapter - G03.FR.02 Offline Authorization', async () => {
-  let adapter: OCPP20AuthAdapter
-  let mockChargingStation: ChargingStation
-
-  beforeEach(() => {
-    mockChargingStation = {
-      inAcceptedState: () => true,
-      logPrefix: () => '[TEST-STATION-OFFLINE]',
-      stationInfo: {
-        chargingStationId: 'TEST-OFFLINE',
-      },
-    } as unknown as ChargingStation
-
-    adapter = new OCPP20AuthAdapter(mockChargingStation)
-  })
-
-  afterEach(() => {
-    mock.reset()
-  })
-
-  await describe('G03.FR.02.001 - Offline detection', async () => {
-    await it('should detect station is offline when not in accepted state', async () => {
-      // Given: Station is offline (not in accepted state)
-      mockChargingStation.inAcceptedState = () => false
-
-      // When: Check if remote authorization is available
-      const isAvailable = await adapter.isRemoteAvailable()
-
-      // Then: Remote should not be available
-      expect(isAvailable).toBe(false)
-    })
-
-    await it('should detect station is online when in accepted state', async () => {
-      // Given: Station is online (in accepted state)
-      mockChargingStation.inAcceptedState = () => true
-
-      // When: Check if remote authorization is available
-      const isAvailable = await adapter.isRemoteAvailable()
-
-      // Then: Remote should be available (assuming AuthorizeRemoteStart is enabled by default)
-      expect(isAvailable).toBe(true)
-    })
-
-    await it('should have correct OCPP version for offline tests', () => {
-      // Verify we're testing the correct OCPP version
-      expect(adapter.ocppVersion).toBe(OCPPVersion.VERSION_20)
-    })
-  })
-
-  await describe('G03.FR.02.002 - Remote availability check', async () => {
-    await it('should return false when offline even with valid configuration', async () => {
-      // Given: Station is offline
-      mockChargingStation.inAcceptedState = () => false
-
-      // When: Check remote availability
-      const isAvailable = await adapter.isRemoteAvailable()
-
-      // Then: Should not be available
-      expect(isAvailable).toBe(false)
-    })
-
-    await it('should handle errors gracefully when checking availability', async () => {
-      // Given: inAcceptedState throws an error
-      mockChargingStation.inAcceptedState = () => {
-        throw new Error('Connection error')
-      }
-
-      // When: Check remote availability
-      const isAvailable = await adapter.isRemoteAvailable()
-
-      // Then: Should safely return false
-      expect(isAvailable).toBe(false)
-    })
-  })
-
-  await describe('G03.FR.02.003 - Configuration validation', async () => {
-    await it('should initialize with default configuration for offline scenarios', () => {
-      // When: Adapter is created
-      // Then: Should have OCPP 2.0 version
-      expect(adapter.ocppVersion).toBe(OCPPVersion.VERSION_20)
-    })
-
-    await it('should validate configuration schema for offline auth', () => {
-      // When: Get configuration schema
-      const schema = adapter.getConfigurationSchema()
-
-      // Then: Should have required offline auth properties
-      expect(schema).toBeDefined()
-      expect(schema.properties).toBeDefined()
-      // OCPP 2.0 uses variables, not configuration keys
-      // The actual offline behavior is controlled by AuthCtrlr variables
-    })
-
-    await it('should have getStatus method for monitoring offline state', () => {
-      // When: Get adapter status
-      const status = adapter.getStatus()
-
-      // Then: Status should be defined and include online state
-      expect(status).toBeDefined()
-      expect(typeof status.isOnline).toBe('boolean')
-      expect(status.ocppVersion).toBe(OCPPVersion.VERSION_20)
-    })
-  })
-})
index 543df57c43a43a847042acc8167075d19833ab93..41f8c8e075c0f63276006d2c1575523d9ab1aef4 100644 (file)
@@ -373,4 +373,109 @@ await describe('OCPP20AuthAdapter', async () => {
       }
     })
   })
+
+  await describe('OCPP20AuthAdapter - G03.FR.02 Offline Authorization', async () => {
+    let offlineAdapter: OCPP20AuthAdapter
+    let offlineMockChargingStation: ChargingStation
+
+    beforeEach(() => {
+      offlineMockChargingStation = {
+        inAcceptedState: () => true,
+        logPrefix: () => '[TEST-STATION-OFFLINE]',
+        stationInfo: {
+          chargingStationId: 'TEST-OFFLINE',
+        },
+      } as unknown as ChargingStation
+
+      offlineAdapter = new OCPP20AuthAdapter(offlineMockChargingStation)
+    })
+
+    afterEach(() => {
+      mock.reset()
+    })
+
+    await describe('G03.FR.02.001 - Offline detection', async () => {
+      await it('should detect station is offline when not in accepted state', async () => {
+        // Given: Station is offline (not in accepted state)
+        offlineMockChargingStation.inAcceptedState = () => false
+
+        // When: Check if remote authorization is available
+        const isAvailable = await offlineAdapter.isRemoteAvailable()
+
+        // Then: Remote should not be available
+        expect(isAvailable).toBe(false)
+      })
+
+      await it('should detect station is online when in accepted state', async () => {
+        // Given: Station is online (in accepted state)
+        offlineMockChargingStation.inAcceptedState = () => true
+
+        // When: Check if remote authorization is available
+        const isAvailable = await offlineAdapter.isRemoteAvailable()
+
+        // Then: Remote should be available (assuming AuthorizeRemoteStart is enabled by default)
+        expect(isAvailable).toBe(true)
+      })
+
+      await it('should have correct OCPP version for offline tests', () => {
+        // Verify we're testing the correct OCPP version
+        expect(offlineAdapter.ocppVersion).toBe(OCPPVersion.VERSION_20)
+      })
+    })
+
+    await describe('G03.FR.02.002 - Remote availability check', async () => {
+      await it('should return false when offline even with valid configuration', async () => {
+        // Given: Station is offline
+        offlineMockChargingStation.inAcceptedState = () => false
+
+        // When: Check remote availability
+        const isAvailable = await offlineAdapter.isRemoteAvailable()
+
+        // Then: Should not be available
+        expect(isAvailable).toBe(false)
+      })
+
+      await it('should handle errors gracefully when checking availability', async () => {
+        // Given: inAcceptedState throws an error
+        offlineMockChargingStation.inAcceptedState = () => {
+          throw new Error('Connection error')
+        }
+
+        // When: Check remote availability
+        const isAvailable = await offlineAdapter.isRemoteAvailable()
+
+        // Then: Should safely return false
+        expect(isAvailable).toBe(false)
+      })
+    })
+
+    await describe('G03.FR.02.003 - Configuration validation', async () => {
+      await it('should initialize with default configuration for offline scenarios', () => {
+        // When: Adapter is created
+        // Then: Should have OCPP 2.0 version
+        expect(offlineAdapter.ocppVersion).toBe(OCPPVersion.VERSION_20)
+      })
+
+      await it('should validate configuration schema for offline auth', () => {
+        // When: Get configuration schema
+        const schema = offlineAdapter.getConfigurationSchema()
+
+        // Then: Should have required offline auth properties
+        expect(schema).toBeDefined()
+        expect(schema.properties).toBeDefined()
+        // OCPP 2.0 uses variables, not configuration keys
+        // The actual offline behavior is controlled by AuthCtrlr variables
+      })
+
+      await it('should have getStatus method for monitoring offline state', () => {
+        // When: Get adapter status
+        const status = offlineAdapter.getStatus()
+
+        // Then: Status should be defined and include online state
+        expect(status).toBeDefined()
+        expect(typeof status.isOnline).toBe('boolean')
+        expect(status.ocppVersion).toBe(OCPPVersion.VERSION_20)
+      })
+    })
+  })
 })
index 2d9c31faec4a33c6d5d1891d28e8049d9fb3b23e..d1901ff999b0c6724ec2697077c3cabd185851eb 100644 (file)
@@ -304,53 +304,41 @@ await describe('Utils test suite', async () => {
     expect(isAsyncFunction(new Float64Array())).toBe(false)
     expect(isAsyncFunction(new BigInt64Array())).toBe(false)
     expect(isAsyncFunction(new BigUint64Array())).toBe(false)
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
+    /* eslint-disable @typescript-eslint/no-empty-function -- Testing with empty functions to verify isAsyncFunction correctly identifies async vs sync */
     expect(isAsyncFunction(new Promise(() => {}))).toBe(false)
     expect(isAsyncFunction(new WeakRef({}))).toBe(false)
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
     expect(isAsyncFunction(new FinalizationRegistry(() => {}))).toBe(false)
     expect(isAsyncFunction(new ArrayBuffer(16))).toBe(false)
     expect(isAsyncFunction(new SharedArrayBuffer(16))).toBe(false)
     expect(isAsyncFunction(new DataView(new ArrayBuffer(16)))).toBe(false)
     expect(isAsyncFunction({})).toBe(false)
     expect(isAsyncFunction({ a: 1 })).toBe(false)
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
     expect(isAsyncFunction(() => {})).toBe(false)
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
     expect(isAsyncFunction(function () {})).toBe(false)
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
     expect(isAsyncFunction(function named () {})).toBe(false)
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
     expect(isAsyncFunction(async () => {})).toBe(true)
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
     expect(isAsyncFunction(async function () {})).toBe(true)
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
     expect(isAsyncFunction(async function named () {})).toBe(true)
+    /* eslint-enable @typescript-eslint/no-empty-function */
     class TestClass {
-      // eslint-disable-next-line @typescript-eslint/no-empty-function
+      /* eslint-disable @typescript-eslint/no-empty-function -- Testing class methods and properties */
       public static async testStaticAsync (): Promise<void> {}
-      // eslint-disable-next-line @typescript-eslint/no-empty-function
       public static testStaticSync (): void {}
-      // eslint-disable-next-line @typescript-eslint/no-empty-function
       public testArrowAsync = async (): Promise<void> => {}
-      // eslint-disable-next-line @typescript-eslint/no-empty-function
       public testArrowSync = (): void => {}
-      // eslint-disable-next-line @typescript-eslint/no-empty-function
       public async testAsync (): Promise<void> {}
-      // eslint-disable-next-line @typescript-eslint/no-empty-function
       public testSync (): void {}
+      /* eslint-enable @typescript-eslint/no-empty-function */
     }
     const testClass = new TestClass()
-    // eslint-disable-next-line @typescript-eslint/unbound-method
+    /* eslint-disable @typescript-eslint/unbound-method -- Testing unbound method detection for async/sync determination */
     expect(isAsyncFunction(testClass.testSync)).toBe(false)
-    // eslint-disable-next-line @typescript-eslint/unbound-method
     expect(isAsyncFunction(testClass.testAsync)).toBe(true)
     expect(isAsyncFunction(testClass.testArrowSync)).toBe(false)
     expect(isAsyncFunction(testClass.testArrowAsync)).toBe(true)
-    // eslint-disable-next-line @typescript-eslint/unbound-method
     expect(isAsyncFunction(TestClass.testStaticSync)).toBe(false)
-    // eslint-disable-next-line @typescript-eslint/unbound-method
     expect(isAsyncFunction(TestClass.testStaticAsync)).toBe(true)
+    /* eslint-enable @typescript-eslint/unbound-method */
   })
 
   await it('should deep clone objects, arrays, dates, maps and sets', () => {
@@ -472,8 +460,9 @@ await describe('Utils test suite', async () => {
 
   await it('should insert substring at specified index position', () => {
     expect(insertAt('test', 'ing', 'test'.length)).toBe('testing')
-    // eslint-disable-next-line @cspell/spellchecker
+    /* eslint-disable @cspell/spellchecker -- Testing string insertion with intentional misspelling 'ing' at position 2 */
     expect(insertAt('test', 'ing', 2)).toBe('teingst')
+    /* eslint-enable @cspell/spellchecker */
   })
 
   await it('should convert to integer or return NaN for invalid input', () => {