]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(test): investigate Windows CI hang (#1694)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 1 Mar 2026 12:23:57 +0000 (13:23 +0100)
committerGitHub <noreply@github.com>
Sun, 1 Mar 2026 12:23:57 +0000 (13:23 +0100)
* fix(test): add --test-force-exit to prevent Windows CI hang

Windows CI was hanging after test completion due to Node.js's handling of
stdout/stderr on Windows. On Windows, stdio uses Named Pipes (Sockets) that
remain 'ref'd' and keep the event loop alive, unlike Unix where file
descriptors are auto-unref'd.

The --test-force-exit flag is the official Node.js solution for this
Windows-specific behavior (see Node.js issue #49925).

Also fixes:
- createMockAuthorizationResult signature to accept only overrides parameter
- Remove setImmediate from mock.timers in Reset test (not needed)

* fix(test): wrap multiple describe blocks in parent for --test-force-exit compatibility

Node.js test runner with --test-force-exit has issues with multiple top-level
await describe() blocks. Wrapping both 'Error Recovery and Resilience' and
'Message Buffering' describe blocks in a parent 'ChargingStation Resilience'
describe block fixes the 'Promise resolution is still pending' errors.

* fix(test): wrap OCPP20 test describe blocks for --test-force-exit compatibility

Same fix as ChargingStation-Resilience.test.ts - wrapping multiple top-level
await describe() blocks in a parent describe block to fix Windows CI hang.

* fix(tests): add missing deleteIdTags method to MockIdTagsCache

The MockIdTagsCache mock was missing the deleteIdTags() method that exists
on the real IdTagsCache class. This caused ClearCache tests to fail when
the method was called during test execution.

* fix(tests): improve test isolation for OCPP 2.0 tests

- Add ocppConfiguration with configurationKey to GetBaseReport test setup
  so ConfigurationInventory reports have data to return
- Add ocppConfiguration with HeartbeatInterval and WebSocketPingInterval
  to VariableManager test setup for proper isolation
- Fix MessageTimeout test expectations to use seconds (OCPP 2.0 unit)
  instead of milliseconds (station.getConnectionTimeout())

These changes ensure tests pass when run individually or in any order,
which is required for --test-force-exit on Windows.

* refactor(tests): use constants instead of hardcoded values in GetBaseReport test

Replace hardcoded '60' and '30' values with proper constant references:
- HeartbeatInterval: millisecondsToSeconds(Constants.DEFAULT_HEARTBEAT_INTERVAL)
- MeterValueSampleInterval: millisecondsToSeconds(Constants.DEFAULT_METER_VALUES_INTERVAL)

This follows the single source of truth principle from TEST_STYLE_GUIDE.md
and maintains consistency with OCPP20VariableManager.test.ts.

* docs(tests): document Windows CI constraints and --test-force-exit

Add section 5 'Platform-Specific Considerations' to TEST_STYLE_GUIDE.md:
- Explain why --test-force-exit is needed (Windows Named Pipes behavior)
- Document single top-level describe block requirement
- Update section numbers and summary accordingly

This documents the root cause and solution for the Windows CI hang issue.

package.json
tests/TEST_STYLE_GUIDE.md
tests/charging-station/ChargingStation-Resilience.test.ts
tests/charging-station/mocks/MockCaches.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts
tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts
tests/charging-station/ocpp/auth/helpers/MockFactories.ts

index e6619b2e04ecba03f25ffa317e630c89877e3153..edf8b20f86d631c05c4593993e119da0216dacfa 100644 (file)
@@ -67,9 +67,9 @@
     "lint": "cross-env TIMING=1 eslint --cache src tests scripts ./*.js ./*.ts",
     "lint:fix": "cross-env TIMING=1 eslint --cache --fix src tests scripts ./*.js ./*.ts",
     "format": "prettier --cache --write .; eslint --cache --fix src tests scripts ./*.js ./*.ts",
-    "test": "node --import tsx --test tests/**/*.test.ts",
+    "test": "node --import tsx --test --test-force-exit tests/**/*.test.ts",
     "test:debug": "node --import tsx --test --inspect tests/**/*.test.ts",
-    "test:coverage": "mkdir -p coverage && node --import tsx --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info tests/**/*.test.ts",
+    "test:coverage": "mkdir -p coverage && node --import tsx --test --test-force-exit --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info tests/**/*.test.ts",
     "clinic:clean": "clinic clean",
     "sea": "pnpm exec rimraf ./dist/evse-simulator ./dist/evse-simulator.blob && node --experimental-sea-config sea-config.json && pnpm dlx ncp $(volta which node || n which lts || nvm which node || command -v node) ./dist/evse-simulator && pnpm dlx postject ./dist/evse-simulator NODE_SEA_BLOB ./dist/evse-simulator.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 && pnpm exec rimraf ./dist/evse-simulator.blob"
   },
index 2ef5f334ef8694e3b5a843fd042caf63d23fc787..55f9dd76e40074b64038c49400b00c926bfdb55b 100644 (file)
@@ -170,7 +170,47 @@ it('should timeout', async () => {
 
 ---
 
-## 5. Constants & Imports
+## 5. Platform-Specific Considerations
+
+### Windows CI (`--test-force-exit`)
+
+The test command uses `--test-force-exit` flag to prevent Windows CI hangs:
+
+```json
+"test": "node --import tsx --test --test-force-exit tests/**/*.test.ts"
+```
+
+**Why**: Windows Named Pipes for stdout/stderr remain "ref'd" (keep event loop alive) while Unix file descriptors are auto-unref'd. Without `--test-force-exit`, the Node.js process hangs indefinitely after tests complete on Windows.
+
+### Single Top-Level Describe Block
+
+Each test file should have ONE top-level `await describe()` block:
+
+```typescript
+// ✅ Good - Single top-level describe
+await describe('MyFeature', async () => {
+  await describe('SubFeature A', async () => {
+    /* ... */
+  })
+  await describe('SubFeature B', async () => {
+    /* ... */
+  })
+})
+
+// ❌ Bad - Multiple top-level describes (breaks --test-force-exit)
+await describe('SubFeature A', async () => {
+  /* ... */
+})
+await describe('SubFeature B', async () => {
+  /* ... */
+})
+```
+
+**Why**: Multiple top-level `await describe()` blocks cause "Promise resolution is still pending" errors with `--test-force-exit`.
+
+---
+
+## 6. Constants & Imports
 
 ### Single Source of Truth
 
@@ -186,7 +226,7 @@ Available constants: `tests/charging-station/ChargingStationTestConstants.ts`
 
 ---
 
-## 6. Assertions
+## 7. Assertions
 
 ### Strict Only
 
@@ -205,7 +245,7 @@ expect(count == '5').toBe(true) // Type coercion
 
 ---
 
-## 7. Type Safety
+## 8. Type Safety
 
 ### No `as any` Casts
 
@@ -228,7 +268,7 @@ expect(AuthValidators.isValidIdentifierValue(123 as any)).toBe(false)
 
 ---
 
-## 8. Mock Factories
+## 9. Mock Factories
 
 ### Choose the Right Factory
 
@@ -253,7 +293,7 @@ expect(mocks.webSocket.sentMessages).toContain(expectedMessage)
 
 ---
 
-## 9. Utility Reference
+## 10. Utility Reference
 
 ### Lifecycle Helpers (`helpers/TestLifecycleHelpers.ts`)
 
@@ -301,6 +341,9 @@ expect(mocks.webSocket.sentMessages).toContain(expectedMessage)
 2. **Structure**: AAA pattern, JSDoc headers
 3. **Isolate**: Fresh instances in `beforeEach`, `standardCleanup()` in `afterEach`
 4. **Async**: Use `async/await`, mock timers
-5. **Constants**: Import from `ChargingStationTestConstants.ts`
-6. **Assert**: Strict only (`toBe`, `toStrictEqual`)
-7. **Types**: No `as any`, use testable interfaces
+5. **Platform**: Single top-level `describe`, `--test-force-exit` for Windows
+6. **Constants**: Import from `ChargingStationTestConstants.ts`
+7. **Assert**: Strict only (`toBe`, `toStrictEqual`)
+8. **Types**: No `as any`, use testable interfaces
+9. **Mocks**: Use appropriate factory for your use case
+10. **Utils**: Leverage lifecycle helpers and mock classes
index c36966d21c29f90c19101d22797a008731a93585..166aeaa87400e55f6a8c02d273949d8be8958200 100644 (file)
@@ -12,481 +12,485 @@ import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
 import { TEST_HEARTBEAT_INTERVAL_MS } from './ChargingStationTestConstants.js'
 import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js'
 
-await describe('ChargingStation Error Recovery and Resilience', async () => {
-  let station: ChargingStation
+await describe('ChargingStation Resilience', async () => {
+  await describe('Error Recovery and Resilience', async () => {
+    let station: ChargingStation
 
-  beforeEach(() => {
-    station = undefined
-  })
+    beforeEach(() => {
+      station = undefined
+    })
 
-  afterEach(() => {
-    standardCleanup()
-    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-    if (station != null) {
-      cleanupChargingStation(station)
-    }
-  })
+    afterEach(() => {
+      standardCleanup()
+      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+      if (station != null) {
+        cleanupChargingStation(station)
+      }
+    })
 
-  // -------------------------------------------------------------------------
-  // Reconnection Logic Tests
-  // -------------------------------------------------------------------------
+    // -------------------------------------------------------------------------
+    // Reconnection Logic Tests
+    // -------------------------------------------------------------------------
 
-  await it('should trigger reconnection on abnormal WebSocket close', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
+    await it('should trigger reconnection on abnormal WebSocket close', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
 
-    // Station must be started for reconnection to trigger
-    station.started = true
+      // Station must be started for reconnection to trigger
+      station.started = true
 
-    // Act - Simulate abnormal close (code 1006 = abnormal closure)
-    mocks.webSocket.simulateClose(1006, 'Connection lost')
+      // Act - Simulate abnormal close (code 1006 = abnormal closure)
+      mocks.webSocket.simulateClose(1006, 'Connection lost')
 
-    // Assert - WebSocket should be in CLOSED state
-    expect(mocks.webSocket.readyState).toBe(3) // CLOSED
-  })
+      // Assert - WebSocket should be in CLOSED state
+      expect(mocks.webSocket.readyState).toBe(3) // CLOSED
+    })
 
-  await it('should not reconnect on normal WebSocket close', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    station.started = true
+    await it('should not reconnect on normal WebSocket close', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      station.started = true
 
-    // Act - Simulate normal close (code 1000 = normal closure)
-    mocks.webSocket.simulateClose(1000, 'Normal closure')
+      // Act - Simulate normal close (code 1000 = normal closure)
+      mocks.webSocket.simulateClose(1000, 'Normal closure')
 
-    // Assert - WebSocket should be closed
-    expect(mocks.webSocket.readyState).toBe(3) // CLOSED
-  })
+      // Assert - WebSocket should be closed
+      expect(mocks.webSocket.readyState).toBe(3) // CLOSED
+    })
 
-  await it('should track connection retry count', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
+    await it('should track connection retry count', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
 
-    // Assert - Initial retry count should be 0
-    expect(station.wsConnectionRetryCount).toBe(0)
+      // Assert - Initial retry count should be 0
+      expect(station.wsConnectionRetryCount).toBe(0)
 
-    // Act - Increment retry count manually (simulating reconnection attempt)
-    station.wsConnectionRetryCount = 1
+      // Act - Increment retry count manually (simulating reconnection attempt)
+      station.wsConnectionRetryCount = 1
 
-    // Assert - Count should be incremented
-    expect(station.wsConnectionRetryCount).toBe(1)
-  })
+      // Assert - Count should be incremented
+      expect(station.wsConnectionRetryCount).toBe(1)
+    })
 
-  await it('should support exponential backoff configuration', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
+    await it('should support exponential backoff configuration', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
 
-    // Assert - stationInfo should have reconnect configuration options
-    expect(station.stationInfo).toBeDefined()
-    // The actual implementation uses stationInfo.reconnectExponentialDelay
-    // and stationInfo.autoReconnectMaxRetries for reconnection logic
-  })
+      // Assert - stationInfo should have reconnect configuration options
+      expect(station.stationInfo).toBeDefined()
+      // The actual implementation uses stationInfo.reconnectExponentialDelay
+      // and stationInfo.autoReconnectMaxRetries for reconnection logic
+    })
 
-  await it('should reset retry count on successful connection', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    station.wsConnectionRetryCount = 5 // Simulate some retries
+    await it('should reset retry count on successful connection', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      station.wsConnectionRetryCount = 5 // Simulate some retries
 
-    // Act - Reset retry count (as would happen on successful reconnection)
-    station.wsConnectionRetryCount = 0
+      // Act - Reset retry count (as would happen on successful reconnection)
+      station.wsConnectionRetryCount = 0
 
-    // Assert
-    expect(station.wsConnectionRetryCount).toBe(0)
-  })
+      // Assert
+      expect(station.wsConnectionRetryCount).toBe(0)
+    })
 
-  // -------------------------------------------------------------------------
-  // Error Handling Tests
-  // -------------------------------------------------------------------------
+    // -------------------------------------------------------------------------
+    // Error Handling Tests
+    // -------------------------------------------------------------------------
 
-  await it('should handle invalid OCPP message format gracefully', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
+    await it('should handle invalid OCPP message format gracefully', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
 
-    // Act - Simulate invalid message (not valid JSON array)
-    // The mock emits message event - actual station would parse and handle error
-    mocks.webSocket.simulateMessage('invalid json')
+      // Act - Simulate invalid message (not valid JSON array)
+      // The mock emits message event - actual station would parse and handle error
+      mocks.webSocket.simulateMessage('invalid json')
 
-    // Assert - Station should still be operational (not crashed)
-    expect(station.connectors.size).toBeGreaterThan(0)
-  })
+      // Assert - Station should still be operational (not crashed)
+      expect(station.connectors.size).toBeGreaterThan(0)
+    })
+
+    await it('should handle WebSocket error event gracefully', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+
+      // Set up error listener to track that error was emitted
+      let errorEventReceived = false
+      mocks.webSocket.on('error', () => {
+        errorEventReceived = true
+      })
 
-  await it('should handle WebSocket error event gracefully', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
+      // Act - Simulate error event
+      mocks.webSocket.simulateError(new Error('Connection refused'))
 
-    // Set up error listener to track that error was emitted
-    let errorEventReceived = false
-    mocks.webSocket.on('error', () => {
-      errorEventReceived = true
+      // Assert - Error event should have been emitted and received
+      expect(errorEventReceived).toBe(true)
     })
 
-    // Act - Simulate error event
-    mocks.webSocket.simulateError(new Error('Connection refused'))
+    await it('should reject duplicate message IDs for incoming messages', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
 
-    // Assert - Error event should have been emitted and received
-    expect(errorEventReceived).toBe(true)
-  })
+      // Add a request with specific message ID to simulate duplicate
+      const messageId = 'duplicate-uuid-123'
+      station.requests.set(messageId, ['callback', 'errorCallback', 'TestCommand'])
 
-  await it('should reject duplicate message IDs for incoming messages', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
+      // Assert - Request with duplicate ID exists
+      expect(station.requests.has(messageId)).toBe(true)
 
-    // Add a request with specific message ID to simulate duplicate
-    const messageId = 'duplicate-uuid-123'
-    station.requests.set(messageId, ['callback', 'errorCallback', 'TestCommand'])
+      // The actual implementation throws OCPPError with SECURITY_ERROR
+      // when receiving an incoming message with duplicate message ID
+      // (see ChargingStation.ts:handleIncomingMessage)
+    })
 
-    // Assert - Request with duplicate ID exists
-    expect(station.requests.has(messageId)).toBe(true)
+    await it('should handle response for unknown message ID', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
 
-    // The actual implementation throws OCPPError with SECURITY_ERROR
-    // when receiving an incoming message with duplicate message ID
-    // (see ChargingStation.ts:handleIncomingMessage)
-  })
+      // Ensure requests map is empty
+      station.requests.clear()
 
-  await it('should handle response for unknown message ID', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
+      // Assert - No pending request exists
+      expect(station.requests.size).toBe(0)
 
-    // Ensure requests map is empty
-    station.requests.clear()
+      // The actual implementation throws OCPPError with INTERNAL_ERROR
+      // when receiving a response for unknown message ID
+      // (see ChargingStation.ts:handleResponseMessage)
+    })
 
-    // Assert - No pending request exists
-    expect(station.requests.size).toBe(0)
+    // -------------------------------------------------------------------------
+    // Graceful Degradation Tests
+    // -------------------------------------------------------------------------
 
-    // The actual implementation throws OCPPError with INTERNAL_ERROR
-    // when receiving a response for unknown message ID
-    // (see ChargingStation.ts:handleResponseMessage)
-  })
+    await it('should handle server unreachable state', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
 
-  // -------------------------------------------------------------------------
-  // Graceful Degradation Tests
-  // -------------------------------------------------------------------------
+      // Act - Close WebSocket to simulate server unreachable
+      mocks.webSocket.simulateClose(1006, 'Server unreachable')
 
-  await it('should handle server unreachable state', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
+      // Assert - Station should remain in valid state
+      expect(station.connectors.size).toBeGreaterThan(0)
+      expect(mocks.webSocket.readyState).toBe(3) // CLOSED
+    })
 
-    // Act - Close WebSocket to simulate server unreachable
-    mocks.webSocket.simulateClose(1006, 'Server unreachable')
+    await it('should handle boot notification rejected state', () => {
+      // Arrange
+      const result = createMockChargingStation({
+        bootNotificationStatus: RegistrationStatusEnumType.REJECTED,
+        connectorsCount: 1,
+      })
+      station = result.station
 
-    // Assert - Station should remain in valid state
-    expect(station.connectors.size).toBeGreaterThan(0)
-    expect(mocks.webSocket.readyState).toBe(3) // CLOSED
-  })
+      // Assert - Station should report rejected state
+      expect(station.inRejectedState()).toBe(true)
+      expect(station.inAcceptedState()).toBe(false)
 
-  await it('should handle boot notification rejected state', () => {
-    // Arrange
-    const result = createMockChargingStation({
-      bootNotificationStatus: RegistrationStatusEnumType.REJECTED,
-      connectorsCount: 1,
+      // Station in rejected state should not initiate messages per OCPP spec (B03.FR.03)
+      expect(station.bootNotificationResponse?.status).toBe(RegistrationStatusEnumType.REJECTED)
     })
-    station = result.station
 
-    // Assert - Station should report rejected state
-    expect(station.inRejectedState()).toBe(true)
-    expect(station.inAcceptedState()).toBe(false)
+    await it('should maintain connector states during connection failure', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 2 })
+      station = result.station
+      const mocks = result.mocks
+
+      // Set up connector state before failure
+      const connector1 = station.getConnectorStatus(1)
+      if (connector1 != null) {
+        connector1.transactionStarted = true
+        connector1.transactionId = 999
+      }
 
-    // Station in rejected state should not initiate messages per OCPP spec (B03.FR.03)
-    expect(station.bootNotificationResponse?.status).toBe(RegistrationStatusEnumType.REJECTED)
-  })
+      // Act - Simulate connection failure
+      mocks.webSocket.simulateClose(1006, 'Connection lost')
 
-  await it('should maintain connector states during connection failure', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 2 })
-    station = result.station
-    const mocks = result.mocks
-
-    // Set up connector state before failure
-    const connector1 = station.getConnectorStatus(1)
-    if (connector1 != null) {
-      connector1.transactionStarted = true
-      connector1.transactionId = 999
-    }
-
-    // Act - Simulate connection failure
-    mocks.webSocket.simulateClose(1006, 'Connection lost')
-
-    // Assert - Connector state should be preserved
-    const connector1After = station.getConnectorStatus(1)
-    expect(connector1After?.transactionStarted).toBe(true)
-    expect(connector1After?.transactionId).toBe(999)
-  })
+      // Assert - Connector state should be preserved
+      const connector1After = station.getConnectorStatus(1)
+      expect(connector1After?.transactionStarted).toBe(true)
+      expect(connector1After?.transactionId).toBe(999)
+    })
 
-  // -------------------------------------------------------------------------
-  // Cleanup on Errors Tests
-  // -------------------------------------------------------------------------
+    // -------------------------------------------------------------------------
+    // Cleanup on Errors Tests
+    // -------------------------------------------------------------------------
 
-  await it('should clear event listeners on cleanup', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
+    await it('should clear event listeners on cleanup', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
 
-    // The cleanup function should clear all listeners
-    // Act
-    cleanupChargingStation(station)
+      // The cleanup function should clear all listeners
+      // Act
+      cleanupChargingStation(station)
 
-    // Assert - Station should be properly cleaned up
-    // (listenerCount returns 0 in mock implementation)
-    expect(station.listenerCount('someEvent')).toBe(0)
-  })
+      // Assert - Station should be properly cleaned up
+      // (listenerCount returns 0 in mock implementation)
+      expect(station.listenerCount('someEvent')).toBe(0)
+    })
 
-  await it('should clear timers on cleanup', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
+    await it('should clear timers on cleanup', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
 
-    // Set up a heartbeat timer (simulated)
-    station.heartbeatSetInterval = setInterval(() => {
-      /* empty */
-    }, TEST_HEARTBEAT_INTERVAL_MS) as unknown as NodeJS.Timeout
+      // Set up a heartbeat timer (simulated)
+      station.heartbeatSetInterval = setInterval(() => {
+        /* empty */
+      }, TEST_HEARTBEAT_INTERVAL_MS) as unknown as NodeJS.Timeout
 
-    // Act - Cleanup station
-    cleanupChargingStation(station)
+      // Act - Cleanup station
+      cleanupChargingStation(station)
 
-    // Assert - Timer should be cleared
-    expect(station.heartbeatSetInterval).toBeUndefined()
-  })
+      // Assert - Timer should be cleared
+      expect(station.heartbeatSetInterval).toBeUndefined()
+    })
 
-  await it('should clear pending requests on cleanup', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
+    await it('should clear pending requests on cleanup', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
 
-    // Add some pending requests
-    station.requests.set('req-1', ['callback1', 'errorCallback1', 'Command1'])
-    station.requests.set('req-2', ['callback2', 'errorCallback2', 'Command2'])
+      // Add some pending requests
+      station.requests.set('req-1', ['callback1', 'errorCallback1', 'Command1'])
+      station.requests.set('req-2', ['callback2', 'errorCallback2', 'Command2'])
 
-    // Act - Cleanup station
-    cleanupChargingStation(station)
+      // Act - Cleanup station
+      cleanupChargingStation(station)
 
-    // Assert - Requests should be cleared
-    expect(station.requests.size).toBe(0)
+      // Assert - Requests should be cleared
+      expect(station.requests.size).toBe(0)
+    })
   })
-})
 
-await describe('ChargingStation Message Buffering', async () => {
-  let station: ChargingStation
+  await describe('Message Buffering', async () => {
+    let station: ChargingStation
 
-  beforeEach(() => {
-    station = undefined
-  })
+    beforeEach(() => {
+      station = undefined
+    })
 
-  afterEach(() => {
-    standardCleanup()
-    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-    if (station != null) {
-      cleanupChargingStation(station)
-    }
-  })
+    afterEach(() => {
+      standardCleanup()
+      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+      if (station != null) {
+        cleanupChargingStation(station)
+      }
+    })
 
-  // -------------------------------------------------------------------------
-  // Buffer Operations Tests
-  // -------------------------------------------------------------------------
+    // -------------------------------------------------------------------------
+    // Buffer Operations Tests
+    // -------------------------------------------------------------------------
 
-  await it('should buffer message when WebSocket is closed', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    const testMessage = '[2,"test-msg-1","BootNotification",{}]'
+    await it('should buffer message when WebSocket is closed', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      const testMessage = '[2,"test-msg-1","BootNotification",{}]'
 
-    // Ensure WebSocket is closed
-    mocks.webSocket.readyState = 3 // CLOSED
+      // Ensure WebSocket is closed
+      mocks.webSocket.readyState = 3 // CLOSED
 
-    // Act - Buffer a message
-    station.bufferMessage(testMessage)
+      // Act - Buffer a message
+      station.bufferMessage(testMessage)
 
-    // Assert - Message should be queued but not sent
-    expect(station.messageQueue.length).toBe(1)
-    expect(station.messageQueue[0]).toBe(testMessage)
-    expect(mocks.webSocket.sentMessages.length).toBe(0)
-  })
+      // Assert - Message should be queued but not sent
+      expect(station.messageQueue.length).toBe(1)
+      expect(station.messageQueue[0]).toBe(testMessage)
+      expect(mocks.webSocket.sentMessages.length).toBe(0)
+    })
 
-  await it('should send message immediately when WebSocket is open', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    const testMessage = '[2,"test-msg-2","Heartbeat",{}]'
+    await it('should send message immediately when WebSocket is open', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      const testMessage = '[2,"test-msg-2","Heartbeat",{}]'
 
-    // Ensure WebSocket is open
-    mocks.webSocket.readyState = 1 // OPEN
-    mocks.webSocket.simulateOpen()
+      // Ensure WebSocket is open
+      mocks.webSocket.readyState = 1 // OPEN
+      mocks.webSocket.simulateOpen()
 
-    // Act - Send message
-    station.bufferMessage(testMessage)
+      // Act - Send message
+      station.bufferMessage(testMessage)
 
-    // Note: Due to async nature, the message may be sent or buffered depending on timing
-    // This test verifies the message is queued at minimum
-    expect(station.messageQueue.length).toBeGreaterThanOrEqual(0)
-  })
+      // Note: Due to async nature, the message may be sent or buffered depending on timing
+      // This test verifies the message is queued at minimum
+      expect(station.messageQueue.length).toBeGreaterThanOrEqual(0)
+    })
 
-  await it('should flush messages in FIFO order when connection restored', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    const msg1 = '[2,"msg-1","BootNotification",{}]'
-    const msg2 = '[2,"msg-2","Heartbeat",{}]'
-    const msg3 = '[2,"msg-3","StatusNotification",{}]'
-
-    // Simulate offline: close the connection
-    mocks.webSocket.readyState = 3 // CLOSED
-
-    // Act - Buffer multiple messages
-    station.bufferMessage(msg1)
-    station.bufferMessage(msg2)
-    station.bufferMessage(msg3)
-
-    // Assert - All messages should be buffered
-    expect(station.messageQueue.length).toBe(3)
-    expect(station.messageQueue[0]).toBe(msg1)
-    expect(station.messageQueue[1]).toBe(msg2)
-    expect(station.messageQueue[2]).toBe(msg3)
-    expect(mocks.webSocket.sentMessages.length).toBe(0)
-  })
+    await it('should flush messages in FIFO order when connection restored', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      const msg1 = '[2,"msg-1","BootNotification",{}]'
+      const msg2 = '[2,"msg-2","Heartbeat",{}]'
+      const msg3 = '[2,"msg-3","StatusNotification",{}]'
 
-  await it('should preserve message order across multiple buffer operations', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    const messages = [
-      '[2,"m1","Cmd1",{}]',
-      '[2,"m2","Cmd2",{}]',
-      '[2,"m3","Cmd3",{}]',
-      '[2,"m4","Cmd4",{}]',
-      '[2,"m5","Cmd5",{}]',
-    ]
-
-    mocks.webSocket.readyState = 3 // CLOSED
-
-    // Act - Buffer all messages
-    for (const msg of messages) {
-      station.bufferMessage(msg)
-    }
-
-    // Assert - Verify FIFO order
-    expect(station.messageQueue.length).toBe(5)
-    for (let i = 0; i < messages.length; i++) {
-      expect(station.messageQueue[i]).toBe(messages[i])
-    }
-  })
+      // Simulate offline: close the connection
+      mocks.webSocket.readyState = 3 // CLOSED
 
-  await it('should handle buffer full scenario (stress test with many messages)', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    const messageCount = 100
+      // Act - Buffer multiple messages
+      station.bufferMessage(msg1)
+      station.bufferMessage(msg2)
+      station.bufferMessage(msg3)
+
+      // Assert - All messages should be buffered
+      expect(station.messageQueue.length).toBe(3)
+      expect(station.messageQueue[0]).toBe(msg1)
+      expect(station.messageQueue[1]).toBe(msg2)
+      expect(station.messageQueue[2]).toBe(msg3)
+      expect(mocks.webSocket.sentMessages.length).toBe(0)
+    })
 
-    mocks.webSocket.readyState = 3 // CLOSED
+    await it('should preserve message order across multiple buffer operations', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      const messages = [
+        '[2,"m1","Cmd1",{}]',
+        '[2,"m2","Cmd2",{}]',
+        '[2,"m3","Cmd3",{}]',
+        '[2,"m4","Cmd4",{}]',
+        '[2,"m5","Cmd5",{}]',
+      ]
 
-    // Act - Buffer many messages
-    for (let i = 0; i < messageCount; i++) {
-      const msg = `[2,"msg-${i.toString()}","Command",{"data":"${i.toString()}"}]`
-      station.bufferMessage(msg)
-    }
+      mocks.webSocket.readyState = 3 // CLOSED
 
-    // Assert - All messages should be buffered
-    expect(station.messageQueue.length).toBe(messageCount)
-    expect(mocks.webSocket.sentMessages.length).toBe(0)
+      // Act - Buffer all messages
+      for (const msg of messages) {
+        station.bufferMessage(msg)
+      }
 
-    // Verify first and last message are in correct positions
-    expect(station.messageQueue[0]).toContain('msg-0')
-    expect(station.messageQueue[messageCount - 1]).toContain(`msg-${(messageCount - 1).toString()}`)
-  })
+      // Assert - Verify FIFO order
+      expect(station.messageQueue.length).toBe(5)
+      for (let i = 0; i < messages.length; i++) {
+        expect(station.messageQueue[i]).toBe(messages[i])
+      }
+    })
 
-  // -------------------------------------------------------------------------
-  // Flush Behavior Tests
-  // -------------------------------------------------------------------------
+    await it('should handle buffer full scenario (stress test with many messages)', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      const messageCount = 100
 
-  await it('should not send buffered messages while disconnected', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    const testMessage = '[2,"offline-msg","Test",{}]'
+      mocks.webSocket.readyState = 3 // CLOSED
 
-    mocks.webSocket.readyState = 3 // CLOSED
+      // Act - Buffer many messages
+      for (let i = 0; i < messageCount; i++) {
+        const msg = `[2,"msg-${i.toString()}","Command",{"data":"${i.toString()}"}]`
+        station.bufferMessage(msg)
+      }
 
-    // Act - Buffer message
-    station.bufferMessage(testMessage)
+      // Assert - All messages should be buffered
+      expect(station.messageQueue.length).toBe(messageCount)
+      expect(mocks.webSocket.sentMessages.length).toBe(0)
 
-    // Small delay to ensure no async flush attempts
-    const initialSentCount = mocks.webSocket.sentMessages.length
+      // Verify first and last message are in correct positions
+      expect(station.messageQueue[0]).toContain('msg-0')
+      expect(station.messageQueue[messageCount - 1]).toContain(
+        `msg-${(messageCount - 1).toString()}`
+      )
+    })
 
-    // Assert - Message should remain buffered
-    expect(station.messageQueue.length).toBe(1)
-    expect(mocks.webSocket.sentMessages.length).toBe(initialSentCount)
-  })
+    // -------------------------------------------------------------------------
+    // Flush Behavior Tests
+    // -------------------------------------------------------------------------
 
-  await it('should clear buffer after successful message transmission', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    const testMessage = '[2,"clear-test","Command",{}]'
+    await it('should not send buffered messages while disconnected', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      const testMessage = '[2,"offline-msg","Test",{}]'
 
-    mocks.webSocket.readyState = 3 // CLOSED
+      mocks.webSocket.readyState = 3 // CLOSED
 
-    // Act - Buffer message
-    station.bufferMessage(testMessage)
-    const bufferedCount = station.messageQueue.length
+      // Act - Buffer message
+      station.bufferMessage(testMessage)
 
-    // Assert - Message is buffered
-    expect(bufferedCount).toBe(1)
+      // Small delay to ensure no async flush attempts
+      const initialSentCount = mocks.webSocket.sentMessages.length
 
-    // Now simulate successful send by manually removing (simulating what sendMessageBuffer does)
-    if (station.messageQueue.length > 0) {
-      station.messageQueue.shift()
-    }
+      // Assert - Message should remain buffered
+      expect(station.messageQueue.length).toBe(1)
+      expect(mocks.webSocket.sentMessages.length).toBe(initialSentCount)
+    })
 
-    // Assert - Buffer should be cleared
-    expect(station.messageQueue.length).toBe(0)
-  })
+    await it('should clear buffer after successful message transmission', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      const testMessage = '[2,"clear-test","Command",{}]'
 
-  await it('should handle rapid buffer/reconnect cycles without message loss', () => {
-    // Arrange
-    const result = createMockChargingStation({ connectorsCount: 1 })
-    station = result.station
-    const mocks = result.mocks
-    const cycleCount = 3
-    const messagesPerCycle = 2
-    let totalExpectedMessages = 0
-
-    // Act - Perform multiple buffer/disconnect cycles
-    for (let cycle = 0; cycle < cycleCount; cycle++) {
-      // Simulate disconnection
       mocks.webSocket.readyState = 3 // CLOSED
 
-      // Buffer messages in this cycle
-      for (let i = 0; i < messagesPerCycle; i++) {
-        const msg = `[2,"cycle-${cycle.toString()}-msg-${i.toString()}","Cmd",{}]`
-        station.bufferMessage(msg)
-        totalExpectedMessages++
+      // Act - Buffer message
+      station.bufferMessage(testMessage)
+      const bufferedCount = station.messageQueue.length
+
+      // Assert - Message is buffered
+      expect(bufferedCount).toBe(1)
+
+      // Now simulate successful send by manually removing (simulating what sendMessageBuffer does)
+      if (station.messageQueue.length > 0) {
+        station.messageQueue.shift()
       }
-    }
-
-    // Assert - All messages from all cycles should be buffered in order
-    expect(station.messageQueue.length).toBe(totalExpectedMessages)
-    expect(station.messageQueue[0]).toContain('cycle-0-msg-0')
-    expect(station.messageQueue[totalExpectedMessages - 1]).toContain(
-      `cycle-${(cycleCount - 1).toString()}-msg-${(messagesPerCycle - 1).toString()}`
-    )
+
+      // Assert - Buffer should be cleared
+      expect(station.messageQueue.length).toBe(0)
+    })
+
+    await it('should handle rapid buffer/reconnect cycles without message loss', () => {
+      // Arrange
+      const result = createMockChargingStation({ connectorsCount: 1 })
+      station = result.station
+      const mocks = result.mocks
+      const cycleCount = 3
+      const messagesPerCycle = 2
+      let totalExpectedMessages = 0
+
+      // Act - Perform multiple buffer/disconnect cycles
+      for (let cycle = 0; cycle < cycleCount; cycle++) {
+        // Simulate disconnection
+        mocks.webSocket.readyState = 3 // CLOSED
+
+        // Buffer messages in this cycle
+        for (let i = 0; i < messagesPerCycle; i++) {
+          const msg = `[2,"cycle-${cycle.toString()}-msg-${i.toString()}","Cmd",{}]`
+          station.bufferMessage(msg)
+          totalExpectedMessages++
+        }
+      }
+
+      // Assert - All messages from all cycles should be buffered in order
+      expect(station.messageQueue.length).toBe(totalExpectedMessages)
+      expect(station.messageQueue[0]).toContain('cycle-0-msg-0')
+      expect(station.messageQueue[totalExpectedMessages - 1]).toContain(
+        `cycle-${(cycleCount - 1).toString()}-msg-${(messagesPerCycle - 1).toString()}`
+      )
+    })
   })
 })
index 19bf0f6f14404e40bd1933489f55a7ac70521b36..6748a46c6568a0c60f51a6598025efe79ddee8db 100644 (file)
@@ -23,6 +23,10 @@ export class MockIdTagsCache {
     MockIdTagsCache.instance = null
   }
 
+  public deleteIdTags (file: string): void {
+    this.idTagsMap.delete(file)
+  }
+
   public getIdTags (file: string): string[] | undefined {
     return this.idTagsMap.get(file)
   }
index d287f53a31dc4a16ef4cf40ff78a83f9b721d4ed..f2c7b3b3268ab42b7336275e5b4b085c8067f3dd 100644 (file)
@@ -3,6 +3,7 @@
  * @description Unit tests for OCPP 2.0 GetBaseReport command handling (B07)
  */
 import { expect } from '@std/expect'
+import { millisecondsToSeconds } from 'date-fns'
 import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
@@ -50,6 +51,20 @@ await describe('B07 - Get Base Report', async () => {
       connectorsCount: 3,
       evseConfiguration: { evsesCount: 3 },
       heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+      ocppConfiguration: {
+        configurationKey: [
+          {
+            key: StandardParametersKey.HeartbeatInterval,
+            readonly: false,
+            value: millisecondsToSeconds(Constants.DEFAULT_HEARTBEAT_INTERVAL).toString(),
+          },
+          {
+            key: StandardParametersKey.MeterValueSampleInterval,
+            readonly: false,
+            value: millisecondsToSeconds(Constants.DEFAULT_METER_VALUES_INTERVAL).toString(),
+          },
+        ],
+      },
       stationInfo: {
         chargePointModel: TEST_CHARGE_POINT_MODEL,
         chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
index 71ee41320440ac69b608ca383fb3bd883ab6ba46..5d02d4e2ea138ed358fcd31719db929d1bf26e1c 100644 (file)
@@ -41,7 +41,7 @@ await describe('B11 & B12 - Reset', async () => {
   let testableService: ReturnType<typeof createTestableIncomingRequestService>
 
   beforeEach(() => {
-    mock.timers.enable({ apis: ['setInterval', 'setTimeout', 'setImmediate'] })
+    mock.timers.enable({ apis: ['setInterval', 'setTimeout'] })
 
     const { station } = createMockChargingStation({
       baseName: TEST_CHARGING_STATION_BASE_NAME,
index c9662210f3e763fa06bad242325d2b6ae4486c73..164fe0467bfba4e3af7ef7c8aa579fdbeeaafa7b 100644 (file)
@@ -42,346 +42,348 @@ const createMockOCSPRequestData = (): OCSPRequestDataType => ({
   serialNumber: '1234567890',
 })
 
-await describe('M02 - Get15118EVCertificate Request', async () => {
-  let station: ReturnType<typeof createMockChargingStation>
-
-  beforeEach(() => {
-    const { station: newStation } = createMockChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-      stationInfo: {
-        ocppStrictCompliance: false,
-        ocppVersion: OCPPVersion.VERSION_201,
-      },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+await describe('OCPP20 ISO15118 Request Service', async () => {
+  await describe('M02 - Get15118EVCertificate Request', async () => {
+    let station: ReturnType<typeof createMockChargingStation>
+
+    beforeEach(() => {
+      const { station: newStation } = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 3,
+        evseConfiguration: { evsesCount: 3 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        stationInfo: {
+          ocppStrictCompliance: false,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+      station = newStation
     })
-    station = newStation
-  })
 
-  afterEach(() => {
-    standardCleanup()
-  })
+    afterEach(() => {
+      standardCleanup()
+    })
 
-  await describe('EXI Install Action', async () => {
-    await it('should forward EXI request unmodified for Install action', async () => {
-      const { sendMessageMock, service } =
-        createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
+    await describe('EXI Install Action', async () => {
+      await it('should forward EXI request unmodified for Install action', async () => {
+        const { sendMessageMock, service } =
+          createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
+            sendMessageResponse: {
+              exiResponse: MOCK_EXI_RESPONSE,
+              status: Iso15118EVCertificateStatusEnumType.Accepted,
+            },
+          })
+
+        await service.requestGet15118EVCertificate(
+          station,
+          MOCK_ISO15118_SCHEMA_VERSION,
+          CertificateActionEnumType.Install,
+          MOCK_EXI_REQUEST
+        )
+
+        expect(sendMessageMock.mock.calls.length).toBe(1)
+
+        const sentPayload = sendMessageMock.mock.calls[0]
+          .arguments[2] as OCPP20Get15118EVCertificateRequest
+        expect(sentPayload.exiRequest).toBe(MOCK_EXI_REQUEST)
+        expect(sentPayload.action).toBe(CertificateActionEnumType.Install)
+      })
+    })
+
+    await describe('EXI Update Action', async () => {
+      await it('should forward EXI request unmodified for Update action', async () => {
+        const { sendMessageMock, service } =
+          createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
+            sendMessageResponse: {
+              exiResponse: MOCK_EXI_RESPONSE,
+              status: Iso15118EVCertificateStatusEnumType.Accepted,
+            },
+          })
+
+        await service.requestGet15118EVCertificate(
+          station,
+          MOCK_ISO15118_SCHEMA_VERSION,
+          CertificateActionEnumType.Update,
+          MOCK_EXI_REQUEST
+        )
+
+        const sentPayload = sendMessageMock.mock.calls[0]
+          .arguments[2] as OCPP20Get15118EVCertificateRequest
+        expect(sentPayload.exiRequest).toBe(MOCK_EXI_REQUEST)
+        expect(sentPayload.action).toBe(CertificateActionEnumType.Update)
+      })
+    })
+
+    await describe('CSMS Response Handling', async () => {
+      await it('should return Accepted response with exiResponse from CSMS', async () => {
+        const { service } = createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
           sendMessageResponse: {
             exiResponse: MOCK_EXI_RESPONSE,
             status: Iso15118EVCertificateStatusEnumType.Accepted,
           },
         })
 
-      await service.requestGet15118EVCertificate(
-        station,
-        MOCK_ISO15118_SCHEMA_VERSION,
-        CertificateActionEnumType.Install,
-        MOCK_EXI_REQUEST
-      )
-
-      expect(sendMessageMock.mock.calls.length).toBe(1)
+        const response = await service.requestGet15118EVCertificate(
+          station,
+          MOCK_ISO15118_SCHEMA_VERSION,
+          CertificateActionEnumType.Install,
+          MOCK_EXI_REQUEST
+        )
 
-      const sentPayload = sendMessageMock.mock.calls[0]
-        .arguments[2] as OCPP20Get15118EVCertificateRequest
-      expect(sentPayload.exiRequest).toBe(MOCK_EXI_REQUEST)
-      expect(sentPayload.action).toBe(CertificateActionEnumType.Install)
-    })
-  })
+        expect(response).toBeDefined()
+        expect(response.status).toBe(Iso15118EVCertificateStatusEnumType.Accepted)
+        expect(response.exiResponse).toBe(MOCK_EXI_RESPONSE)
+      })
 
-  await describe('EXI Update Action', async () => {
-    await it('should forward EXI request unmodified for Update action', async () => {
-      const { sendMessageMock, service } =
-        createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
+      await it('should return Failed response from CSMS', async () => {
+        const { service } = createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
           sendMessageResponse: {
-            exiResponse: MOCK_EXI_RESPONSE,
-            status: Iso15118EVCertificateStatusEnumType.Accepted,
+            exiResponse: '',
+            status: Iso15118EVCertificateStatusEnumType.Failed,
+            statusInfo: {
+              reasonCode: ReasonCodeEnumType.InvalidCertificate,
+            },
           },
         })
 
-      await service.requestGet15118EVCertificate(
-        station,
-        MOCK_ISO15118_SCHEMA_VERSION,
-        CertificateActionEnumType.Update,
-        MOCK_EXI_REQUEST
-      )
+        const response = await service.requestGet15118EVCertificate(
+          station,
+          MOCK_ISO15118_SCHEMA_VERSION,
+          CertificateActionEnumType.Install,
+          MOCK_EXI_REQUEST
+        )
 
-      const sentPayload = sendMessageMock.mock.calls[0]
-        .arguments[2] as OCPP20Get15118EVCertificateRequest
-      expect(sentPayload.exiRequest).toBe(MOCK_EXI_REQUEST)
-      expect(sentPayload.action).toBe(CertificateActionEnumType.Update)
+        expect(response).toBeDefined()
+        expect(response.status).toBe(Iso15118EVCertificateStatusEnumType.Failed)
+        expect(response.statusInfo?.reasonCode).toBe(ReasonCodeEnumType.InvalidCertificate)
+      })
     })
-  })
 
-  await describe('CSMS Response Handling', async () => {
-    await it('should return Accepted response with exiResponse from CSMS', async () => {
-      const { service } = createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
-        sendMessageResponse: {
-          exiResponse: MOCK_EXI_RESPONSE,
-          status: Iso15118EVCertificateStatusEnumType.Accepted,
-        },
+    await describe('Schema Version Parameter', async () => {
+      await it('should pass schema version correctly', async () => {
+        const { sendMessageMock, service } =
+          createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
+            sendMessageResponse: {
+              exiResponse: MOCK_EXI_RESPONSE,
+              status: Iso15118EVCertificateStatusEnumType.Accepted,
+            },
+          })
+
+        await service.requestGet15118EVCertificate(
+          station,
+          MOCK_ISO15118_SCHEMA_VERSION,
+          CertificateActionEnumType.Install,
+          MOCK_EXI_REQUEST
+        )
+
+        const sentPayload = sendMessageMock.mock.calls[0]
+          .arguments[2] as OCPP20Get15118EVCertificateRequest
+        expect(sentPayload.iso15118SchemaVersion).toBe(MOCK_ISO15118_SCHEMA_VERSION)
       })
-
-      const response = await service.requestGet15118EVCertificate(
-        station,
-        MOCK_ISO15118_SCHEMA_VERSION,
-        CertificateActionEnumType.Install,
-        MOCK_EXI_REQUEST
-      )
-
-      expect(response).toBeDefined()
-      expect(response.status).toBe(Iso15118EVCertificateStatusEnumType.Accepted)
-      expect(response.exiResponse).toBe(MOCK_EXI_RESPONSE)
     })
 
-    await it('should return Failed response from CSMS', async () => {
-      const { service } = createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
-        sendMessageResponse: {
-          exiResponse: '',
-          status: Iso15118EVCertificateStatusEnumType.Failed,
-          statusInfo: {
-            reasonCode: ReasonCodeEnumType.InvalidCertificate,
-          },
-        },
+    await describe('Base64 EXI Pass-Through', async () => {
+      await it('should pass Base64 EXI string unchanged', async () => {
+        const complexBase64EXI =
+          'VGhpcyBpcyBhIG1vcmUgY29tcGxleCBFWEkgcGF5bG9hZCB3aXRoIHNwZWNpYWwgY2hhcmFjdGVycyArLz0='
+
+        const { sendMessageMock, service } =
+          createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
+            sendMessageResponse: {
+              exiResponse: MOCK_EXI_RESPONSE,
+              status: Iso15118EVCertificateStatusEnumType.Accepted,
+            },
+          })
+
+        await service.requestGet15118EVCertificate(
+          station,
+          MOCK_ISO15118_SCHEMA_VERSION,
+          CertificateActionEnumType.Install,
+          complexBase64EXI
+        )
+
+        const sentPayload = sendMessageMock.mock.calls[0]
+          .arguments[2] as OCPP20Get15118EVCertificateRequest
+        // EXI should be passed through unchanged - no decoding/encoding
+        expect(sentPayload.exiRequest).toBe(complexBase64EXI)
       })
-
-      const response = await service.requestGet15118EVCertificate(
-        station,
-        MOCK_ISO15118_SCHEMA_VERSION,
-        CertificateActionEnumType.Install,
-        MOCK_EXI_REQUEST
-      )
-
-      expect(response).toBeDefined()
-      expect(response.status).toBe(Iso15118EVCertificateStatusEnumType.Failed)
-      expect(response.statusInfo?.reasonCode).toBe(ReasonCodeEnumType.InvalidCertificate)
     })
   })
 
-  await describe('Schema Version Parameter', async () => {
-    await it('should pass schema version correctly', async () => {
-      const { sendMessageMock, service } =
-        createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
-          sendMessageResponse: {
-            exiResponse: MOCK_EXI_RESPONSE,
-            status: Iso15118EVCertificateStatusEnumType.Accepted,
-          },
-        })
-
-      await service.requestGet15118EVCertificate(
-        station,
-        MOCK_ISO15118_SCHEMA_VERSION,
-        CertificateActionEnumType.Install,
-        MOCK_EXI_REQUEST
-      )
+  await describe('M03 - GetCertificateStatus Request', async () => {
+    let station: ReturnType<typeof createMockChargingStation>['station']
+
+    beforeEach(() => {
+      const result = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 3,
+        evseConfiguration: { evsesCount: 3 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        stationInfo: {
+          ocppStrictCompliance: false,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+      station = result.station
+    })
 
-      const sentPayload = sendMessageMock.mock.calls[0]
-        .arguments[2] as OCPP20Get15118EVCertificateRequest
-      expect(sentPayload.iso15118SchemaVersion).toBe(MOCK_ISO15118_SCHEMA_VERSION)
+    afterEach(() => {
+      standardCleanup()
     })
-  })
 
-  await describe('Base64 EXI Pass-Through', async () => {
-    await it('should pass Base64 EXI string unchanged', async () => {
-      const complexBase64EXI =
-        'VGhpcyBpcyBhIG1vcmUgY29tcGxleCBFWEkgcGF5bG9hZCB3aXRoIHNwZWNpYWwgY2hhcmFjdGVycyArLz0='
+    await describe('OCSP Request Data', async () => {
+      await it('should send OCSP request data correctly', async () => {
+        const { sendMessageMock, service } =
+          createTestableRequestService<OCPP20GetCertificateStatusResponse>({
+            sendMessageResponse: {
+              ocspResult: MOCK_OCSP_RESULT,
+              status: GetCertificateStatusEnumType.Accepted,
+            },
+          })
+
+        const ocspRequestData = createMockOCSPRequestData()
+
+        await service.requestGetCertificateStatus(station, ocspRequestData)
+
+        expect(sendMessageMock.mock.calls.length).toBe(1)
+
+        const sentPayload = sendMessageMock.mock.calls[0]
+          .arguments[2] as OCPP20GetCertificateStatusRequest
+        expect(sentPayload.ocspRequestData).toBeDefined()
+        expect(sentPayload.ocspRequestData.hashAlgorithm).toBe(HashAlgorithmEnumType.SHA256)
+        expect(sentPayload.ocspRequestData.issuerKeyHash).toBe(ocspRequestData.issuerKeyHash)
+        expect(sentPayload.ocspRequestData.issuerNameHash).toBe(ocspRequestData.issuerNameHash)
+        expect(sentPayload.ocspRequestData.serialNumber).toBe(ocspRequestData.serialNumber)
+        expect(sentPayload.ocspRequestData.responderURL).toBe(ocspRequestData.responderURL)
+      })
+    })
 
-      const { sendMessageMock, service } =
-        createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
+    await describe('CSMS Response Handling', async () => {
+      await it('should return Accepted response with ocspResult from CSMS', async () => {
+        const { service } = createTestableRequestService<OCPP20GetCertificateStatusResponse>({
           sendMessageResponse: {
-            exiResponse: MOCK_EXI_RESPONSE,
-            status: Iso15118EVCertificateStatusEnumType.Accepted,
+            ocspResult: MOCK_OCSP_RESULT,
+            status: GetCertificateStatusEnumType.Accepted,
           },
         })
 
-      await service.requestGet15118EVCertificate(
-        station,
-        MOCK_ISO15118_SCHEMA_VERSION,
-        CertificateActionEnumType.Install,
-        complexBase64EXI
-      )
-
-      const sentPayload = sendMessageMock.mock.calls[0]
-        .arguments[2] as OCPP20Get15118EVCertificateRequest
-      // EXI should be passed through unchanged - no decoding/encoding
-      expect(sentPayload.exiRequest).toBe(complexBase64EXI)
-    })
-  })
-})
+        const response = await service.requestGetCertificateStatus(
+          station,
+          createMockOCSPRequestData()
+        )
 
-await describe('M03 - GetCertificateStatus Request', async () => {
-  let station: ReturnType<typeof createMockChargingStation>['station']
-
-  beforeEach(() => {
-    const result = createMockChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-      stationInfo: {
-        ocppStrictCompliance: false,
-        ocppVersion: OCPPVersion.VERSION_201,
-      },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
-    })
-    station = result.station
-  })
-
-  afterEach(() => {
-    standardCleanup()
-  })
+        expect(response).toBeDefined()
+        expect(response.status).toBe(GetCertificateStatusEnumType.Accepted)
+        expect(response.ocspResult).toBe(MOCK_OCSP_RESULT)
+      })
 
-  await describe('OCSP Request Data', async () => {
-    await it('should send OCSP request data correctly', async () => {
-      const { sendMessageMock, service } =
-        createTestableRequestService<OCPP20GetCertificateStatusResponse>({
+      await it('should return Failed response from CSMS', async () => {
+        const { service } = createTestableRequestService<OCPP20GetCertificateStatusResponse>({
           sendMessageResponse: {
-            ocspResult: MOCK_OCSP_RESULT,
-            status: GetCertificateStatusEnumType.Accepted,
+            status: GetCertificateStatusEnumType.Failed,
+            statusInfo: {
+              reasonCode: ReasonCodeEnumType.InternalError,
+            },
           },
         })
 
-      const ocspRequestData = createMockOCSPRequestData()
+        const response = await service.requestGetCertificateStatus(
+          station,
+          createMockOCSPRequestData()
+        )
 
-      await service.requestGetCertificateStatus(station, ocspRequestData)
-
-      expect(sendMessageMock.mock.calls.length).toBe(1)
+        expect(response).toBeDefined()
+        expect(response.status).toBe(GetCertificateStatusEnumType.Failed)
+        expect(response.statusInfo?.reasonCode).toBe(ReasonCodeEnumType.InternalError)
+      })
+    })
 
-      const sentPayload = sendMessageMock.mock.calls[0]
-        .arguments[2] as OCPP20GetCertificateStatusRequest
-      expect(sentPayload.ocspRequestData).toBeDefined()
-      expect(sentPayload.ocspRequestData.hashAlgorithm).toBe(HashAlgorithmEnumType.SHA256)
-      expect(sentPayload.ocspRequestData.issuerKeyHash).toBe(ocspRequestData.issuerKeyHash)
-      expect(sentPayload.ocspRequestData.issuerNameHash).toBe(ocspRequestData.issuerNameHash)
-      expect(sentPayload.ocspRequestData.serialNumber).toBe(ocspRequestData.serialNumber)
-      expect(sentPayload.ocspRequestData.responderURL).toBe(ocspRequestData.responderURL)
+    await describe('Stub OCSP Response', async () => {
+      await it('should handle stub OCSP response correctly', async () => {
+        // This tests that the simulator doesn't make real network calls
+        // Response is stubbed/mocked at the sendMessage level
+        const stubOcspResult = 'U3R1YiBPQ1NQIFJlc3BvbnNlIERhdGE='
+
+        const { sendMessageMock, service } =
+          createTestableRequestService<OCPP20GetCertificateStatusResponse>({
+            sendMessageResponse: {
+              ocspResult: stubOcspResult,
+              status: GetCertificateStatusEnumType.Accepted,
+            },
+          })
+
+        const response = await service.requestGetCertificateStatus(
+          station,
+          createMockOCSPRequestData()
+        )
+
+        expect(response).toBeDefined()
+        expect(response.status).toBe(GetCertificateStatusEnumType.Accepted)
+        expect(response.ocspResult).toBe(stubOcspResult)
+
+        // Verify sendMessage was called (no real network call)
+        expect(sendMessageMock.mock.calls.length).toBe(1)
+      })
     })
   })
 
-  await describe('CSMS Response Handling', async () => {
-    await it('should return Accepted response with ocspResult from CSMS', async () => {
-      const { service } = createTestableRequestService<OCPP20GetCertificateStatusResponse>({
-        sendMessageResponse: {
-          ocspResult: MOCK_OCSP_RESULT,
-          status: GetCertificateStatusEnumType.Accepted,
+  await describe('Request Command Names', async () => {
+    let station: ReturnType<typeof createMockChargingStation>['station']
+
+    beforeEach(() => {
+      const result = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 1,
+        evseConfiguration: { evsesCount: 1 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        stationInfo: {
+          ocppStrictCompliance: false,
+          ocppVersion: OCPPVersion.VERSION_201,
         },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
       })
+      station = result.station
+    })
 
-      const response = await service.requestGetCertificateStatus(
-        station,
-        createMockOCSPRequestData()
-      )
-
-      expect(response).toBeDefined()
-      expect(response.status).toBe(GetCertificateStatusEnumType.Accepted)
-      expect(response.ocspResult).toBe(MOCK_OCSP_RESULT)
+    afterEach(() => {
+      standardCleanup()
     })
 
-    await it('should return Failed response from CSMS', async () => {
-      const { service } = createTestableRequestService<OCPP20GetCertificateStatusResponse>({
-        sendMessageResponse: {
-          status: GetCertificateStatusEnumType.Failed,
-          statusInfo: {
-            reasonCode: ReasonCodeEnumType.InternalError,
+    await it('should send GET_15118_EV_CERTIFICATE command name', async () => {
+      const { sendMessageMock, service } =
+        createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
+          sendMessageResponse: {
+            exiResponse: MOCK_EXI_RESPONSE,
+            status: Iso15118EVCertificateStatusEnumType.Accepted,
           },
-        },
-      })
+        })
 
-      const response = await service.requestGetCertificateStatus(
+      await service.requestGet15118EVCertificate(
         station,
-        createMockOCSPRequestData()
+        MOCK_ISO15118_SCHEMA_VERSION,
+        CertificateActionEnumType.Install,
+        MOCK_EXI_REQUEST
       )
 
-      expect(response).toBeDefined()
-      expect(response.status).toBe(GetCertificateStatusEnumType.Failed)
-      expect(response.statusInfo?.reasonCode).toBe(ReasonCodeEnumType.InternalError)
+      const commandName = sendMessageMock.mock.calls[0].arguments[3]
+      expect(commandName).toBe(OCPP20RequestCommand.GET_15118_EV_CERTIFICATE)
     })
-  })
-
-  await describe('Stub OCSP Response', async () => {
-    await it('should handle stub OCSP response correctly', async () => {
-      // This tests that the simulator doesn't make real network calls
-      // Response is stubbed/mocked at the sendMessage level
-      const stubOcspResult = 'U3R1YiBPQ1NQIFJlc3BvbnNlIERhdGE='
 
+    await it('should send GET_CERTIFICATE_STATUS command name', async () => {
       const { sendMessageMock, service } =
         createTestableRequestService<OCPP20GetCertificateStatusResponse>({
           sendMessageResponse: {
-            ocspResult: stubOcspResult,
+            ocspResult: MOCK_OCSP_RESULT,
             status: GetCertificateStatusEnumType.Accepted,
           },
         })
 
-      const response = await service.requestGetCertificateStatus(
-        station,
-        createMockOCSPRequestData()
-      )
-
-      expect(response).toBeDefined()
-      expect(response.status).toBe(GetCertificateStatusEnumType.Accepted)
-      expect(response.ocspResult).toBe(stubOcspResult)
-
-      // Verify sendMessage was called (no real network call)
-      expect(sendMessageMock.mock.calls.length).toBe(1)
-    })
-  })
-})
+      await service.requestGetCertificateStatus(station, createMockOCSPRequestData())
 
-await describe('Request Command Names', async () => {
-  let station: ReturnType<typeof createMockChargingStation>['station']
-
-  beforeEach(() => {
-    const result = createMockChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 1,
-      evseConfiguration: { evsesCount: 1 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-      stationInfo: {
-        ocppStrictCompliance: false,
-        ocppVersion: OCPPVersion.VERSION_201,
-      },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      const commandName = sendMessageMock.mock.calls[0].arguments[3]
+      expect(commandName).toBe(OCPP20RequestCommand.GET_CERTIFICATE_STATUS)
     })
-    station = result.station
-  })
-
-  afterEach(() => {
-    standardCleanup()
-  })
-
-  await it('should send GET_15118_EV_CERTIFICATE command name', async () => {
-    const { sendMessageMock, service } =
-      createTestableRequestService<OCPP20Get15118EVCertificateResponse>({
-        sendMessageResponse: {
-          exiResponse: MOCK_EXI_RESPONSE,
-          status: Iso15118EVCertificateStatusEnumType.Accepted,
-        },
-      })
-
-    await service.requestGet15118EVCertificate(
-      station,
-      MOCK_ISO15118_SCHEMA_VERSION,
-      CertificateActionEnumType.Install,
-      MOCK_EXI_REQUEST
-    )
-
-    const commandName = sendMessageMock.mock.calls[0].arguments[3]
-    expect(commandName).toBe(OCPP20RequestCommand.GET_15118_EV_CERTIFICATE)
-  })
-
-  await it('should send GET_CERTIFICATE_STATUS command name', async () => {
-    const { sendMessageMock, service } =
-      createTestableRequestService<OCPP20GetCertificateStatusResponse>({
-        sendMessageResponse: {
-          ocspResult: MOCK_OCSP_RESULT,
-          status: GetCertificateStatusEnumType.Accepted,
-        },
-      })
-
-    await service.requestGetCertificateStatus(station, createMockOCSPRequestData())
-
-    const commandName = sendMessageMock.mock.calls[0].arguments[3]
-    expect(commandName).toBe(OCPP20RequestCommand.GET_CERTIFICATE_STATUS)
   })
 })
index d15bbdac435d5753b493cfa5e78d1f804f41bacf..b7ee4d6620f7c01db935449b68301823dd376d4b 100644 (file)
@@ -77,956 +77,1552 @@ const TRANSACTION_FLOWS = [
   },
 ] as const
 
-await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async () => {
-  let mockChargingStation: ChargingStation
-
-  beforeEach(() => {
-    const { station } = createMockChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-      ocppRequestService: {
-        requestHandler: async () => Promise.resolve({} as EmptyObject),
-      },
-      stationInfo: {
-        ocppStrictCompliance: true,
-        ocppVersion: OCPPVersion.VERSION_201,
-      },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
-    })
-    mockChargingStation = station
-    resetLimits(mockChargingStation)
-  })
-
-  // Reset singleton state and timers after each test to ensure test isolation
-  afterEach(() => {
-    standardCleanup()
-  })
-  // FR: E01.FR.01 - TransactionEventRequest structure validation
-  await describe('buildTransactionEvent', async () => {
-    await it('should build valid TransactionEvent Started with sequence number 0', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-      const triggerReason = OCPP20TriggerReasonEnumType.Authorized
-
-      // Reset sequence number to simulate new transaction
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        triggerReason,
-        connectorId,
-        transactionId
-      )
-
-      // Validate required fields
-      expect(transactionEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-      expect(transactionEvent.triggerReason).toBe(triggerReason)
-      expect(transactionEvent.seqNo).toBe(0) // First event should have seqNo 0
-      expect(transactionEvent.timestamp).toBeInstanceOf(Date)
-      expect(transactionEvent.evse).toBeDefined()
-      expect(transactionEvent.evse?.id).toBe(1) // EVSE ID should match connector ID for this setup
-      expect(transactionEvent.transactionInfo).toBeDefined()
-      expect(transactionEvent.transactionInfo.transactionId).toBe(transactionId)
-
-      // Validate structure matches OCPP 2.0.1 schema requirements
-      expect(typeof transactionEvent.eventType).toBe('string')
-      expect(typeof transactionEvent.triggerReason).toBe('string')
-      expect(typeof transactionEvent.seqNo).toBe('number')
-      expect(transactionEvent.seqNo).toBeGreaterThanOrEqual(0)
-    })
-
-    await it('should increment sequence number for subsequent events', () => {
-      const connectorId = 2
-      const transactionId = generateUUID()
-
-      // Reset for new transaction
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Build first event (Started)
-      const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      // Build second event (Updated)
-      const updateEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
-
-      // Build third event (Ended)
-      const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        connectorId,
-        transactionId,
-        { stoppedReason: OCPP20ReasonEnumType.Local }
-      )
-
-      // Validate sequence number progression: 0 → 1 → 2
-      expect(startEvent.seqNo).toBe(0)
-      expect(updateEvent.seqNo).toBe(1)
-      expect(endEvent.seqNo).toBe(2)
-
-      // Validate all events share same transaction ID
-      expect(startEvent.transactionInfo.transactionId).toBe(transactionId)
-      expect(updateEvent.transactionInfo.transactionId).toBe(transactionId)
-      expect(endEvent.transactionInfo.transactionId).toBe(transactionId)
-    })
+await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
+  await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async () => {
+    let mockChargingStation: ChargingStation
 
-    await it('should handle optional parameters correctly', () => {
-      const connectorId = 3
-      const transactionId = generateUUID()
-      const options = {
-        cableMaxCurrent: 32,
-        chargingState: OCPP20ChargingStateEnumType.Charging,
-        idToken: {
-          idToken: 'TEST_TOKEN_123',
-          type: OCPP20IdTokenEnumType.ISO14443,
+    beforeEach(() => {
+      const { station } = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 3,
+        evseConfiguration: { evsesCount: 3 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        ocppRequestService: {
+          requestHandler: async () => Promise.resolve({} as EmptyObject),
         },
-        numberOfPhasesUsed: 3,
-        offline: false,
-        remoteStartId: 12345,
-        reservationId: 67890,
-      }
-
-      const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.ChargingStateChanged,
-        connectorId,
-        transactionId,
-        options
-      )
-
-      // Validate optional fields are included
-      expect(transactionEvent.idToken).toBeDefined()
-      expect(transactionEvent.idToken?.idToken).toBe('TEST_TOKEN_123')
-      expect(transactionEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.ISO14443)
-      expect(transactionEvent.transactionInfo.chargingState).toBe(
-        OCPP20ChargingStateEnumType.Charging
-      )
-      expect(transactionEvent.transactionInfo.remoteStartId).toBe(12345)
-      expect(transactionEvent.cableMaxCurrent).toBe(32)
-      expect(transactionEvent.numberOfPhasesUsed).toBe(3)
-      expect(transactionEvent.offline).toBe(false)
-      expect(transactionEvent.reservationId).toBe(67890)
+        stationInfo: {
+          ocppStrictCompliance: true,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+      mockChargingStation = station
+      resetLimits(mockChargingStation)
     })
 
-    await it('should validate transaction ID format (identifier string ≤36 chars)', () => {
-      const connectorId = 1
-      const invalidTransactionId =
-        'this-string-is-way-too-long-for-a-valid-transaction-id-exceeds-36-chars'
-
-      try {
-        OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.Authorized,
-          connectorId,
-          invalidTransactionId
-        )
-        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 as Error).message).toContain('≤36 characters')
-      }
+    // Reset singleton state and timers after each test to ensure test isolation
+    afterEach(() => {
+      standardCleanup()
     })
+    // FR: E01.FR.01 - TransactionEventRequest structure validation
+    await describe('buildTransactionEvent', async () => {
+      await it('should build valid TransactionEvent Started with sequence number 0', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+        const triggerReason = OCPP20TriggerReasonEnumType.Authorized
+
+        // Reset sequence number to simulate new transaction
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-    await it('should handle all TriggerReason enum values', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      // Test a selection of TriggerReason values to ensure they're all handled
-      const triggerReasons = [
-        OCPP20TriggerReasonEnumType.Authorized,
-        OCPP20TriggerReasonEnumType.CablePluggedIn,
-        OCPP20TriggerReasonEnumType.ChargingRateChanged,
-        OCPP20TriggerReasonEnumType.ChargingStateChanged,
-        OCPP20TriggerReasonEnumType.Deauthorized,
-        OCPP20TriggerReasonEnumType.EnergyLimitReached,
-        OCPP20TriggerReasonEnumType.EVCommunicationLost,
-        OCPP20TriggerReasonEnumType.EVConnectTimeout,
-        OCPP20TriggerReasonEnumType.MeterValueClock,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        OCPP20TriggerReasonEnumType.TimeLimitReached,
-        OCPP20TriggerReasonEnumType.Trigger,
-        OCPP20TriggerReasonEnumType.UnlockCommand,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        OCPP20TriggerReasonEnumType.EVDeparted,
-        OCPP20TriggerReasonEnumType.EVDetected,
-        OCPP20TriggerReasonEnumType.RemoteStop,
-        OCPP20TriggerReasonEnumType.RemoteStart,
-        OCPP20TriggerReasonEnumType.AbnormalCondition,
-        OCPP20TriggerReasonEnumType.SignedDataReceived,
-        OCPP20TriggerReasonEnumType.ResetCommand,
-      ]
-
-      // Reset sequence number
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      for (const triggerReason of triggerReasons) {
         const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
           mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TransactionEventEnumType.Started,
           triggerReason,
           connectorId,
           transactionId
         )
 
+        // Validate required fields
+        expect(transactionEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
         expect(transactionEvent.triggerReason).toBe(triggerReason)
-        expect(transactionEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-      }
-    })
-  })
-
-  // FR: E02.FR.01 - TransactionEventRequest message sending
-  await describe('sendTransactionEvent', async () => {
-    await it('should send TransactionEvent and return response', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      const response = await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      // Validate response structure (EmptyObject for OCPP 2.0.1 TransactionEventResponse)
-      expect(response).toBeDefined()
-      expect(typeof response).toBe('object')
-    })
+        expect(transactionEvent.seqNo).toBe(0) // First event should have seqNo 0
+        expect(transactionEvent.timestamp).toBeInstanceOf(Date)
+        expect(transactionEvent.evse).toBeDefined()
+        expect(transactionEvent.evse?.id).toBe(1) // EVSE ID should match connector ID for this setup
+        expect(transactionEvent.transactionInfo).toBeDefined()
+        expect(transactionEvent.transactionInfo.transactionId).toBe(transactionId)
 
-    await it('should handle errors gracefully', async () => {
-      // Create a mock charging station that throws an error
-      const { station: errorMockChargingStation } = createMockChargingStation({
-        baseName: TEST_CHARGING_STATION_BASE_NAME,
-        connectorsCount: 1,
-        evseConfiguration: { evsesCount: 1 },
-        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-        ocppRequestService: {
-          requestHandler: () => {
-            throw new Error('Network error')
-          },
-        },
-        stationInfo: {
-          ocppStrictCompliance: true,
-          ocppVersion: OCPPVersion.VERSION_201,
-        },
-        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+        // Validate structure matches OCPP 2.0.1 schema requirements
+        expect(typeof transactionEvent.eventType).toBe('string')
+        expect(typeof transactionEvent.triggerReason).toBe('string')
+        expect(typeof transactionEvent.seqNo).toBe('number')
+        expect(transactionEvent.seqNo).toBeGreaterThanOrEqual(0)
       })
 
-      const connectorId = 1
-      const transactionId = generateUUID()
+      await it('should increment sequence number for subsequent events', () => {
+        const connectorId = 2
+        const transactionId = generateUUID()
+
+        // Reset for new transaction
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-      try {
-        await OCPP20ServiceUtils.sendTransactionEvent(
-          errorMockChargingStation,
+        // Build first event (Started)
+        const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
           OCPP20TriggerReasonEnumType.Authorized,
           connectorId,
           transactionId
         )
-        throw new Error('Should have thrown error')
-      } catch (error) {
-        expect((error as Error).message).toContain('Network error')
-      }
-    })
-  })
 
-  // FR: E01.FR.03 - Sequence number management
-  await describe('resetTransactionSequenceNumber', async () => {
-    await it('should reset sequence number to undefined', () => {
-      const connectorId = 1
-
-      // First, build a transaction event to set sequence number
-      OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        generateUUID()
-      )
-
-      // Verify sequence number is set
-      const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connectorStatus?.transactionSeqNo).toBeDefined()
-
-      // Reset sequence number
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      // Verify sequence number is reset
-      expect(connectorStatus?.transactionSeqNo).toBeUndefined()
-    })
-
-    await it('should handle non-existent connector gracefully', () => {
-      const nonExistentConnectorId = 999
+        // Build second event (Updated)
+        const updateEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          connectorId,
+          transactionId
+        )
 
-      // Should not throw error for non-existent connector
-      expect(() => {
-        OCPP20ServiceUtils.resetTransactionSequenceNumber(
+        // Build third event (Ended)
+        const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
           mockChargingStation,
-          nonExistentConnectorId
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.StopAuthorized,
+          connectorId,
+          transactionId,
+          { stoppedReason: OCPP20ReasonEnumType.Local }
         )
-      }).not.toThrow()
-    })
-  })
 
-  // FR: E01.FR.02 - Schema compliance verification
-  await describe('OCPP 2.0.1 Schema Compliance', async () => {
-    await it('should produce schema-compliant TransactionEvent payloads', () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId,
-        {
+        // Validate sequence number progression: 0 → 1 → 2
+        expect(startEvent.seqNo).toBe(0)
+        expect(updateEvent.seqNo).toBe(1)
+        expect(endEvent.seqNo).toBe(2)
+
+        // Validate all events share same transaction ID
+        expect(startEvent.transactionInfo.transactionId).toBe(transactionId)
+        expect(updateEvent.transactionInfo.transactionId).toBe(transactionId)
+        expect(endEvent.transactionInfo.transactionId).toBe(transactionId)
+      })
+
+      await it('should handle optional parameters correctly', () => {
+        const connectorId = 3
+        const transactionId = generateUUID()
+        const options = {
+          cableMaxCurrent: 32,
+          chargingState: OCPP20ChargingStateEnumType.Charging,
           idToken: {
-            idToken: 'SCHEMA_TEST_TOKEN',
+            idToken: 'TEST_TOKEN_123',
             type: OCPP20IdTokenEnumType.ISO14443,
           },
-        }
-      )
-
-      // Validate all required fields exist
-      const requiredFields = [
-        'eventType',
-        'timestamp',
-        'triggerReason',
-        'seqNo',
-        'evse',
-        'transactionInfo',
-      ]
-      for (const field of requiredFields) {
-        expect(transactionEvent).toHaveProperty(field)
-        expect(transactionEvent[field as keyof typeof transactionEvent]).toBeDefined()
-      }
-
-      // Validate field types match schema requirements
-      expect(typeof transactionEvent.eventType).toBe('string')
-      expect(transactionEvent.timestamp).toBeInstanceOf(Date)
-      expect(typeof transactionEvent.triggerReason).toBe('string')
-      expect(typeof transactionEvent.seqNo).toBe('number')
-      expect(typeof transactionEvent.evse).toBe('object')
-      expect(typeof transactionEvent.transactionInfo).toBe('object')
-
-      // Validate EVSE structure
-      expect(transactionEvent.evse).toBeDefined()
-      expect(typeof transactionEvent.evse?.id).toBe('number')
-      expect(transactionEvent.evse?.id).toBeGreaterThan(0)
-
-      // Validate transactionInfo structure
-      expect(typeof transactionEvent.transactionInfo.transactionId).toBe('string')
-
-      // Validate enum values are strings (not numbers)
-      expect(Object.values(OCPP20TransactionEventEnumType)).toContain(transactionEvent.eventType)
-      expect(Object.values(OCPP20TriggerReasonEnumType)).toContain(transactionEvent.triggerReason)
-    })
-
-    await it('should handle EVSE/connector mapping correctly', () => {
-      const connectorId = 2
-      const transactionId = generateUUID()
-
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-
-      const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
-
-      // For this test setup, EVSE ID should match connector ID
-      expect(transactionEvent.evse).toBeDefined()
-      expect(transactionEvent.evse?.id).toBe(connectorId)
-
-      // connectorId should only be included if different from EVSE ID
-      // In this case they should be the same, so connectorId should not be present
-      expect(transactionEvent.evse?.connectorId).toBeUndefined()
-    })
-  })
-
-  // FR: E01.FR.04 - TriggerReason selection based on transaction context
-  await describe('Context-Aware TriggerReason Selection', async () => {
-    await describe('selectTriggerReason', async () => {
-      await it('should select RemoteStart for remote_command context with RequestStartTransaction', () => {
-        const context: OCPP20TransactionContext = {
-          command: 'RequestStartTransaction',
-          source: 'remote_command',
+          numberOfPhasesUsed: 3,
+          offline: false,
+          remoteStartId: 12345,
+          reservationId: 67890,
         }
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Started,
-          context
+        const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.ChargingStateChanged,
+          connectorId,
+          transactionId,
+          options
         )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.RemoteStart)
+        // Validate optional fields are included
+        expect(transactionEvent.idToken).toBeDefined()
+        expect(transactionEvent.idToken?.idToken).toBe('TEST_TOKEN_123')
+        expect(transactionEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.ISO14443)
+        expect(transactionEvent.transactionInfo.chargingState).toBe(
+          OCPP20ChargingStateEnumType.Charging
+        )
+        expect(transactionEvent.transactionInfo.remoteStartId).toBe(12345)
+        expect(transactionEvent.cableMaxCurrent).toBe(32)
+        expect(transactionEvent.numberOfPhasesUsed).toBe(3)
+        expect(transactionEvent.offline).toBe(false)
+        expect(transactionEvent.reservationId).toBe(67890)
       })
 
-      await it('should select RemoteStop for remote_command context with RequestStopTransaction', () => {
-        const context: OCPP20TransactionContext = {
-          command: 'RequestStopTransaction',
-          source: 'remote_command',
+      await it('should validate transaction ID format (identifier string ≤36 chars)', () => {
+        const connectorId = 1
+        const invalidTransactionId =
+          'this-string-is-way-too-long-for-a-valid-transaction-id-exceeds-36-chars'
+
+        try {
+          OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            invalidTransactionId
+          )
+          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 as Error).message).toContain('≤36 characters')
         }
+      })
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
-        )
+      await it('should handle all TriggerReason enum values', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.RemoteStop)
-      })
+        // Test a selection of TriggerReason values to ensure they're all handled
+        const triggerReasons = [
+          OCPP20TriggerReasonEnumType.Authorized,
+          OCPP20TriggerReasonEnumType.CablePluggedIn,
+          OCPP20TriggerReasonEnumType.ChargingRateChanged,
+          OCPP20TriggerReasonEnumType.ChargingStateChanged,
+          OCPP20TriggerReasonEnumType.Deauthorized,
+          OCPP20TriggerReasonEnumType.EnergyLimitReached,
+          OCPP20TriggerReasonEnumType.EVCommunicationLost,
+          OCPP20TriggerReasonEnumType.EVConnectTimeout,
+          OCPP20TriggerReasonEnumType.MeterValueClock,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          OCPP20TriggerReasonEnumType.TimeLimitReached,
+          OCPP20TriggerReasonEnumType.Trigger,
+          OCPP20TriggerReasonEnumType.UnlockCommand,
+          OCPP20TriggerReasonEnumType.StopAuthorized,
+          OCPP20TriggerReasonEnumType.EVDeparted,
+          OCPP20TriggerReasonEnumType.EVDetected,
+          OCPP20TriggerReasonEnumType.RemoteStop,
+          OCPP20TriggerReasonEnumType.RemoteStart,
+          OCPP20TriggerReasonEnumType.AbnormalCondition,
+          OCPP20TriggerReasonEnumType.SignedDataReceived,
+          OCPP20TriggerReasonEnumType.ResetCommand,
+        ]
 
-      await it('should select UnlockCommand for remote_command context with UnlockConnector', () => {
-        const context: OCPP20TransactionContext = {
-          command: 'UnlockConnector',
-          source: 'remote_command',
-        }
+        // Reset sequence number
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Updated,
-          context
-        )
+        for (const triggerReason of triggerReasons) {
+          const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            triggerReason,
+            connectorId,
+            transactionId
+          )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.UnlockCommand)
+          expect(transactionEvent.triggerReason).toBe(triggerReason)
+          expect(transactionEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+        }
       })
+    })
 
-      await it('should select ResetCommand for remote_command context with Reset', () => {
-        const context: OCPP20TransactionContext = {
-          command: 'Reset',
-          source: 'remote_command',
-        }
+    // FR: E02.FR.01 - TransactionEventRequest message sending
+    await describe('sendTransactionEvent', async () => {
+      await it('should send TransactionEvent and return response', async () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
+        const response = await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId
         )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.ResetCommand)
+        // Validate response structure (EmptyObject for OCPP 2.0.1 TransactionEventResponse)
+        expect(response).toBeDefined()
+        expect(typeof response).toBe('object')
       })
 
-      await it('should select Trigger for remote_command context with TriggerMessage', () => {
-        const context: OCPP20TransactionContext = {
-          command: 'TriggerMessage',
-          source: 'remote_command',
-        }
+      await it('should handle errors gracefully', async () => {
+        // Create a mock charging station that throws an error
+        const { station: errorMockChargingStation } = createMockChargingStation({
+          baseName: TEST_CHARGING_STATION_BASE_NAME,
+          connectorsCount: 1,
+          evseConfiguration: { evsesCount: 1 },
+          heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+          ocppRequestService: {
+            requestHandler: () => {
+              throw new Error('Network error')
+            },
+          },
+          stationInfo: {
+            ocppStrictCompliance: true,
+            ocppVersion: OCPPVersion.VERSION_201,
+          },
+          websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+        })
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Updated,
-          context
-        )
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Trigger)
+        try {
+          await OCPP20ServiceUtils.sendTransactionEvent(
+            errorMockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            transactionId
+          )
+          throw new Error('Should have thrown error')
+        } catch (error) {
+          expect((error as Error).message).toContain('Network error')
+        }
       })
+    })
 
-      await it('should select Authorized for local_authorization context with idToken', () => {
-        const context: OCPP20TransactionContext = {
-          authorizationMethod: 'idToken',
-          source: 'local_authorization',
-        }
+    // FR: E01.FR.03 - Sequence number management
+    await describe('resetTransactionSequenceNumber', async () => {
+      await it('should reset sequence number to undefined', () => {
+        const connectorId = 1
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+        // First, build a transaction event to set sequence number
+        OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
-          context
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          generateUUID()
         )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-      })
-
-      await it('should select StopAuthorized for local_authorization context with stopAuthorized', () => {
-        const context: OCPP20TransactionContext = {
-          authorizationMethod: 'stopAuthorized',
-          source: 'local_authorization',
-        }
+        // Verify sequence number is set
+        const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
+        expect(connectorStatus?.transactionSeqNo).toBeDefined()
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
-        )
+        // Reset sequence number
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.StopAuthorized)
+        // Verify sequence number is reset
+        expect(connectorStatus?.transactionSeqNo).toBeUndefined()
       })
 
-      await it('should select Deauthorized when isDeauthorized flag is true', () => {
-        const context: OCPP20TransactionContext = {
-          authorizationMethod: 'idToken',
-          isDeauthorized: true,
-          source: 'local_authorization',
-        }
-
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
-        )
+      await it('should handle non-existent connector gracefully', () => {
+        const nonExistentConnectorId = 999
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized)
+        // Should not throw error for non-existent connector
+        expect(() => {
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(
+            mockChargingStation,
+            nonExistentConnectorId
+          )
+        }).not.toThrow()
       })
+    })
 
-      await it('should select CablePluggedIn for cable_action context with plugged_in', () => {
-        const context: OCPP20TransactionContext = {
-          cableState: 'plugged_in',
-          source: 'cable_action',
-        }
+    // FR: E01.FR.02 - Schema compliance verification
+    await describe('OCPP 2.0.1 Schema Compliance', async () => {
+      await it('should produce schema-compliant TransactionEvent payloads', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
+
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+        const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
-          context
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId,
+          {
+            idToken: {
+              idToken: 'SCHEMA_TEST_TOKEN',
+              type: OCPP20IdTokenEnumType.ISO14443,
+            },
+          }
         )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
-      })
-
-      await it('should select EVDetected for cable_action context with detected', () => {
-        const context: OCPP20TransactionContext = {
-          cableState: 'detected',
-          source: 'cable_action',
+        // Validate all required fields exist
+        const requiredFields = [
+          'eventType',
+          'timestamp',
+          'triggerReason',
+          'seqNo',
+          'evse',
+          'transactionInfo',
+        ]
+        for (const field of requiredFields) {
+          expect(transactionEvent).toHaveProperty(field)
+          expect(transactionEvent[field as keyof typeof transactionEvent]).toBeDefined()
         }
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Updated,
-          context
-        )
-
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
+        // Validate field types match schema requirements
+        expect(typeof transactionEvent.eventType).toBe('string')
+        expect(transactionEvent.timestamp).toBeInstanceOf(Date)
+        expect(typeof transactionEvent.triggerReason).toBe('string')
+        expect(typeof transactionEvent.seqNo).toBe('number')
+        expect(typeof transactionEvent.evse).toBe('object')
+        expect(typeof transactionEvent.transactionInfo).toBe('object')
+
+        // Validate EVSE structure
+        expect(transactionEvent.evse).toBeDefined()
+        expect(typeof transactionEvent.evse?.id).toBe('number')
+        expect(transactionEvent.evse?.id).toBeGreaterThan(0)
+
+        // Validate transactionInfo structure
+        expect(typeof transactionEvent.transactionInfo.transactionId).toBe('string')
+
+        // Validate enum values are strings (not numbers)
+        expect(Object.values(OCPP20TransactionEventEnumType)).toContain(transactionEvent.eventType)
+        expect(Object.values(OCPP20TriggerReasonEnumType)).toContain(transactionEvent.triggerReason)
       })
 
-      await it('should select EVDeparted for cable_action context with unplugged', () => {
-        const context: OCPP20TransactionContext = {
-          cableState: 'unplugged',
-          source: 'cable_action',
-        }
+      await it('should handle EVSE/connector mapping correctly', () => {
+        const connectorId = 2
+        const transactionId = generateUUID()
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId
         )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
+        // For this test setup, EVSE ID should match connector ID
+        expect(transactionEvent.evse).toBeDefined()
+        expect(transactionEvent.evse?.id).toBe(connectorId)
+
+        // connectorId should only be included if different from EVSE ID
+        // In this case they should be the same, so connectorId should not be present
+        expect(transactionEvent.evse?.connectorId).toBeUndefined()
       })
+    })
 
-      await it('should select ChargingStateChanged for charging_state context', () => {
-        const context: OCPP20TransactionContext = {
-          chargingStateChange: {
-            from: OCPP20ChargingStateEnumType.Idle,
-            to: OCPP20ChargingStateEnumType.Charging,
-          },
-          source: 'charging_state',
-        }
+    // FR: E01.FR.04 - TriggerReason selection based on transaction context
+    await describe('Context-Aware TriggerReason Selection', async () => {
+      await describe('selectTriggerReason', async () => {
+        await it('should select RemoteStart for remote_command context with RequestStartTransaction', () => {
+          const context: OCPP20TransactionContext = {
+            command: 'RequestStartTransaction',
+            source: 'remote_command',
+          }
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Updated,
-          context
-        )
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            context
+          )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.ChargingStateChanged)
-      })
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.RemoteStart)
+        })
 
-      await it('should select MeterValuePeriodic for meter_value context with periodic flag', () => {
-        const context: OCPP20TransactionContext = {
-          isPeriodicMeterValue: true,
-          source: 'meter_value',
-        }
+        await it('should select RemoteStop for remote_command context with RequestStopTransaction', () => {
+          const context: OCPP20TransactionContext = {
+            command: 'RequestStopTransaction',
+            source: 'remote_command',
+          }
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Updated,
-          context
-        )
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.RemoteStop)
+        })
+
+        await it('should select UnlockCommand for remote_command context with UnlockConnector', () => {
+          const context: OCPP20TransactionContext = {
+            command: 'UnlockConnector',
+            source: 'remote_command',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Updated,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.UnlockCommand)
+        })
+
+        await it('should select ResetCommand for remote_command context with Reset', () => {
+          const context: OCPP20TransactionContext = {
+            command: 'Reset',
+            source: 'remote_command',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.ResetCommand)
+        })
+
+        await it('should select Trigger for remote_command context with TriggerMessage', () => {
+          const context: OCPP20TransactionContext = {
+            command: 'TriggerMessage',
+            source: 'remote_command',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Updated,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Trigger)
+        })
+
+        await it('should select Authorized for local_authorization context with idToken', () => {
+          const context: OCPP20TransactionContext = {
+            authorizationMethod: 'idToken',
+            source: 'local_authorization',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+        })
+
+        await it('should select StopAuthorized for local_authorization context with stopAuthorized', () => {
+          const context: OCPP20TransactionContext = {
+            authorizationMethod: 'stopAuthorized',
+            source: 'local_authorization',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.StopAuthorized)
+        })
+
+        await it('should select Deauthorized when isDeauthorized flag is true', () => {
+          const context: OCPP20TransactionContext = {
+            authorizationMethod: 'idToken',
+            isDeauthorized: true,
+            source: 'local_authorization',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized)
+        })
+
+        await it('should select CablePluggedIn for cable_action context with plugged_in', () => {
+          const context: OCPP20TransactionContext = {
+            cableState: 'plugged_in',
+            source: 'cable_action',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+        })
+
+        await it('should select EVDetected for cable_action context with detected', () => {
+          const context: OCPP20TransactionContext = {
+            cableState: 'detected',
+            source: 'cable_action',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Updated,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
+        })
+
+        await it('should select EVDeparted for cable_action context with unplugged', () => {
+          const context: OCPP20TransactionContext = {
+            cableState: 'unplugged',
+            source: 'cable_action',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
+        })
+
+        await it('should select ChargingStateChanged for charging_state context', () => {
+          const context: OCPP20TransactionContext = {
+            chargingStateChange: {
+              from: OCPP20ChargingStateEnumType.Idle,
+              to: OCPP20ChargingStateEnumType.Charging,
+            },
+            source: 'charging_state',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Updated,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.ChargingStateChanged)
+        })
+
+        await it('should select MeterValuePeriodic for meter_value context with periodic flag', () => {
+          const context: OCPP20TransactionContext = {
+            isPeriodicMeterValue: true,
+            source: 'meter_value',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Updated,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.MeterValuePeriodic)
+        })
+
+        await it('should select MeterValueClock for meter_value context without periodic flag', () => {
+          const context: OCPP20TransactionContext = {
+            isPeriodicMeterValue: false,
+            source: 'meter_value',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Updated,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.MeterValueClock)
+        })
+
+        await it('should select SignedDataReceived when isSignedDataReceived flag is true', () => {
+          const context: OCPP20TransactionContext = {
+            isSignedDataReceived: true,
+            source: 'meter_value',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Updated,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.SignedDataReceived)
+        })
+
+        await it('should select appropriate system events for system_event context', () => {
+          const testCases = [
+            {
+              expected: OCPP20TriggerReasonEnumType.EVDeparted,
+              systemEvent: 'ev_departed' as const,
+            },
+            {
+              expected: OCPP20TriggerReasonEnumType.EVDetected,
+              systemEvent: 'ev_detected' as const,
+            },
+            {
+              expected: OCPP20TriggerReasonEnumType.EVCommunicationLost,
+              systemEvent: 'ev_communication_lost' as const,
+            },
+            {
+              expected: OCPP20TriggerReasonEnumType.EVConnectTimeout,
+              systemEvent: 'ev_connect_timeout' as const,
+            },
+          ]
+
+          for (const testCase of testCases) {
+            const context: OCPP20TransactionContext = {
+              source: 'system_event',
+              systemEvent: testCase.systemEvent,
+            }
+
+            const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+              OCPP20TransactionEventEnumType.Updated,
+              context
+            )
+
+            expect(triggerReason).toBe(testCase.expected)
+          }
+        })
+
+        await it('should select EnergyLimitReached for energy_limit context', () => {
+          const context: OCPP20TransactionContext = {
+            source: 'energy_limit',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.MeterValuePeriodic)
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EnergyLimitReached)
+        })
+
+        await it('should select TimeLimitReached for time_limit context', () => {
+          const context: OCPP20TransactionContext = {
+            source: 'time_limit',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.TimeLimitReached)
+        })
+
+        await it('should select AbnormalCondition for abnormal_condition context', () => {
+          const context: OCPP20TransactionContext = {
+            abnormalCondition: 'OverCurrent',
+            source: 'abnormal_condition',
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.AbnormalCondition)
+        })
+
+        await it('should handle priority ordering with multiple applicable contexts', () => {
+          // Test context with multiple applicable triggers - priority should be respected
+          const context: OCPP20TransactionContext = {
+            cableState: 'plugged_in', // Even lower priority
+            command: 'RequestStartTransaction',
+            isDeauthorized: true, // Lower priority but should be overridden
+            source: 'remote_command', // High priority
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            context
+          )
+
+          // Should select RemoteStart (priority 1) over Deauthorized (priority 2) or CablePluggedIn (priority 3)
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.RemoteStart)
+        })
+
+        await it('should fallback to Trigger for unknown context source', () => {
+          const context: OCPP20TransactionContext = {
+            source: 'unknown_source' as OCPP20TransactionContext['source'], // Invalid source to test fallback
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Trigger)
+        })
+
+        await it('should fallback to Trigger for incomplete context', () => {
+          const context: OCPP20TransactionContext = {
+            source: 'remote_command',
+            // Missing command field
+          }
+
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            context
+          )
+
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Trigger)
+        })
       })
 
-      await it('should select MeterValueClock for meter_value context without periodic flag', () => {
-        const context: OCPP20TransactionContext = {
-          isPeriodicMeterValue: false,
-          source: 'meter_value',
-        }
+      await describe('buildTransactionEvent with context parameter', async () => {
+        await it('should build TransactionEvent with auto-selected TriggerReason from context', () => {
+          const connectorId = 1
+          const transactionId = generateUUID()
+          const context: OCPP20TransactionContext = {
+            command: 'RequestStartTransaction',
+            source: 'remote_command',
+          }
+
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+          const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            context,
+            connectorId,
+            transactionId
+          )
+
+          expect(transactionEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+          expect(transactionEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.RemoteStart)
+          expect(transactionEvent.seqNo).toBe(0)
+          expect(transactionEvent.transactionInfo.transactionId).toBe(transactionId)
+        })
+
+        await it('should pass through optional parameters correctly', () => {
+          const connectorId = 2
+          const transactionId = generateUUID()
+          const context: OCPP20TransactionContext = {
+            authorizationMethod: 'idToken',
+            source: 'local_authorization',
+          }
+          const options = {
+            chargingState: OCPP20ChargingStateEnumType.Charging,
+            idToken: {
+              idToken: 'CONTEXT_TEST_TOKEN',
+              type: OCPP20IdTokenEnumType.ISO14443,
+            },
+          }
+
+          const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            context,
+            connectorId,
+            transactionId,
+            options
+          )
+
+          expect(transactionEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+          expect(transactionEvent.idToken?.idToken).toBe('CONTEXT_TEST_TOKEN')
+          expect(transactionEvent.transactionInfo.chargingState).toBe(
+            OCPP20ChargingStateEnumType.Charging
+          )
+        })
+      })
+
+      await describe('sendTransactionEvent with context parameter', async () => {
+        await it('should send TransactionEvent with context-aware TriggerReason selection', async () => {
+          const connectorId = 1
+          const transactionId = generateUUID()
+          const context: OCPP20TransactionContext = {
+            cableState: 'plugged_in',
+            source: 'cable_action',
+          }
+
+          const response = await OCPP20ServiceUtils.sendTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            context,
+            connectorId,
+            transactionId
+          )
+
+          // Validate response structure
+          expect(response).toBeDefined()
+          expect(typeof response).toBe('object')
+        })
+
+        await it('should handle context-aware error scenarios gracefully', async () => {
+          // Create error mock for this test
+          const { station: errorMockChargingStation } = createMockChargingStation({
+            baseName: TEST_CHARGING_STATION_BASE_NAME,
+            connectorsCount: 1,
+            evseConfiguration: { evsesCount: 1 },
+            heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+            ocppRequestService: {
+              requestHandler: () => {
+                throw new Error('Context test error')
+              },
+            },
+            stationInfo: {
+              ocppStrictCompliance: true,
+              ocppVersion: OCPPVersion.VERSION_201,
+            },
+            websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+          })
+
+          const connectorId = 1
+          const transactionId = generateUUID()
+          const context: OCPP20TransactionContext = {
+            abnormalCondition: 'TestError',
+            source: 'abnormal_condition',
+          }
+
+          try {
+            await OCPP20ServiceUtils.sendTransactionEvent(
+              errorMockChargingStation,
+              OCPP20TransactionEventEnumType.Ended,
+              context,
+              connectorId,
+              transactionId
+            )
+            throw new Error('Should have thrown error')
+          } catch (error) {
+            expect((error as Error).message).toContain('Context test error')
+          }
+        })
+      })
+
+      await describe('Backward Compatibility', async () => {
+        await it('should maintain compatibility with existing buildTransactionEvent calls', () => {
+          const connectorId = 1
+          const transactionId = generateUUID()
+
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+          // Old method call should still work
+          const oldEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            transactionId
+          )
+
+          expect(oldEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+          expect(oldEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+          expect(oldEvent.seqNo).toBe(0)
+        })
+
+        await it('should maintain compatibility with existing sendTransactionEvent calls', async () => {
+          const connectorId = 1
+          const transactionId = generateUUID()
+
+          // Old method call should still work
+          const response = await OCPP20ServiceUtils.sendTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            transactionId
+          )
+
+          expect(response).toBeDefined()
+          expect(typeof response).toBe('object')
+        })
+      })
+    })
+
+    // ==========================================================================
+    // Parameterized Transaction Flow Tests (E02, E03, Remote Start)
+    // ==========================================================================
+    await describe('Transaction Flow Patterns', async () => {
+      for (const {
+        description,
+        expectedStartTrigger,
+        id,
+        includeIdToken,
+        name,
+        startContext,
+      } of TRANSACTION_FLOWS) {
+        await describe(`${name} Flow`, async () => {
+          await it(`should select ${expectedStartTrigger} trigger for ${description} transaction start`, () => {
+            const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+              OCPP20TransactionEventEnumType.Started,
+              startContext
+            )
+            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', 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
+          )
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Updated,
-          context
-        )
+          // Step 3: Charging starts (Updated with ChargingStateChanged)
+          const chargingStartedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.ChargingStateChanged,
+            connectorId,
+            transactionId,
+            { chargingState: OCPP20ChargingStateEnumType.Charging }
+          )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.MeterValueClock)
-      })
+          // Assert sequence numbers follow correct order
+          expect(cablePluggedEvent.seqNo).toBe(0)
+          expect(evDetectedEvent.seqNo).toBe(1)
+          expect(chargingStartedEvent.seqNo).toBe(2)
 
-      await it('should select SignedDataReceived when isSignedDataReceived flag is true', () => {
-        const context: OCPP20TransactionContext = {
-          isSignedDataReceived: true,
-          source: 'meter_value',
-        }
+          // 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)
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Updated,
-          context
-        )
+          // Assert event types match expected pattern
+          expect(cablePluggedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+          expect(evDetectedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+          expect(chargingStartedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+        })
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.SignedDataReceived)
-      })
+        await it('should handle EVDeparted for cable removal ending transaction', () => {
+          const connectorId = 2
+          const transactionId = generateUUID()
 
-      await it('should select appropriate system events for system_event context', () => {
-        const testCases = [
-          { expected: OCPP20TriggerReasonEnumType.EVDeparted, systemEvent: 'ev_departed' as const },
-          { expected: OCPP20TriggerReasonEnumType.EVDetected, systemEvent: 'ev_detected' as const },
-          {
-            expected: OCPP20TriggerReasonEnumType.EVCommunicationLost,
-            systemEvent: 'ev_communication_lost' as const,
-          },
-          {
-            expected: OCPP20TriggerReasonEnumType.EVConnectTimeout,
-            systemEvent: 'ev_connect_timeout' as const,
-          },
-        ]
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        for (const testCase of testCases) {
-          const context: OCPP20TransactionContext = {
-            source: 'system_event',
-            systemEvent: testCase.systemEvent,
-          }
+          // Start transaction with cable plug
+          const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.CablePluggedIn,
+            connectorId,
+            transactionId
+          )
 
-          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-            OCPP20TransactionEventEnumType.Updated,
-            context
+          // End transaction with EV departure (cable removal)
+          const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Ended,
+            OCPP20TriggerReasonEnumType.EVDeparted,
+            connectorId,
+            transactionId
           )
 
-          expect(triggerReason).toBe(testCase.expected)
-        }
+          // 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 it('should select EnergyLimitReached for energy_limit context', () => {
-        const context: OCPP20TransactionContext = {
-          source: 'energy_limit',
-        }
+      await describe('EV Detection', async () => {
+        await it('should include EVDetected between cable plug and charging start', () => {
+          const connectorId = 1
+          const transactionId = generateUUID()
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
-        )
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EnergyLimitReached)
+          // 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 it('should select TimeLimitReached for time_limit context', () => {
-        const context: OCPP20TransactionContext = {
-          source: 'time_limit',
-        }
-
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
-        )
-
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.TimeLimitReached)
-      })
+      await describe('Connector Status Transitions', async () => {
+        await it('should track connector status through cable-first lifecycle', () => {
+          const connectorId = 1
 
-      await it('should select AbnormalCondition for abnormal_condition context', () => {
-        const context: OCPP20TransactionContext = {
-          abnormalCondition: 'OverCurrent',
-          source: 'abnormal_condition',
-        }
+          // Get connector status object
+          const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
+          expect(connectorStatus).toBeDefined()
+          if (connectorStatus == null) {
+            throw new Error('Connector status should be defined')
+          }
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
-        )
+          // 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)
+        })
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.AbnormalCondition)
-      })
+        await it('should preserve transaction ID through cable-first flow states', () => {
+          const connectorId = 2
+          const transactionId = generateUUID()
 
-      await it('should handle priority ordering with multiple applicable contexts', () => {
-        // Test context with multiple applicable triggers - priority should be respected
-        const context: OCPP20TransactionContext = {
-          cableState: 'plugged_in', // Even lower priority
-          command: 'RequestStartTransaction',
-          isDeauthorized: true, // Lower priority but should be overridden
-          source: 'remote_command', // High priority
-        }
+          const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
+          expect(connectorStatus).toBeDefined()
+          if (connectorStatus == null) {
+            throw new Error('Connector status should be defined')
+          }
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Started,
-          context
-        )
+          // Set transaction ID at start
+          connectorStatus.transactionId = transactionId
+          connectorStatus.transactionStarted = true
+          connectorStatus.status = ConnectorStatusEnum.Preparing
 
-        // Should select RemoteStart (priority 1) over Deauthorized (priority 2) or CablePluggedIn (priority 3)
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.RemoteStart)
-      })
+          // Transition to charging
+          connectorStatus.status = ConnectorStatusEnum.Charging
 
-      await it('should fallback to Trigger for unknown context source', () => {
-        const context: OCPP20TransactionContext = {
-          source: 'unknown_source' as OCPP20TransactionContext['source'], // Invalid source to test fallback
-        }
+          // Transaction ID should persist through state changes
+          expect(connectorStatus.transactionId).toBe(transactionId)
+          expect(connectorStatus.transactionStarted).toBe(true)
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Started,
-          context
-        )
+          // Transition to finished
+          connectorStatus.status = ConnectorStatusEnum.Finishing
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Trigger)
+          // Still same transaction until fully ended
+          expect(connectorStatus.transactionId).toBe(transactionId)
+        })
       })
 
-      await it('should fallback to Trigger for incomplete context', () => {
-        const context: OCPP20TransactionContext = {
-          source: 'remote_command',
-          // Missing command field
-        }
+      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()
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Started,
-          context
-        )
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Trigger)
-      })
-    })
+          // 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)
+          }
 
-    await describe('buildTransactionEvent with context parameter', async () => {
-      await it('should build TransactionEvent with auto-selected TriggerReason from context', () => {
-        const connectorId = 1
-        const transactionId = generateUUID()
-        const context: OCPP20TransactionContext = {
-          command: 'RequestStartTransaction',
-          source: 'remote_command',
-        }
+          // Verify all share same transaction ID
+          for (const event of events) {
+            expect(event.transactionInfo.transactionId).toBe(transactionId)
+          }
+        })
+      })
 
-        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+      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()
+          )
 
-        const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          context,
-          connectorId,
-          transactionId
-        )
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+        })
 
-        expect(transactionEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-        expect(transactionEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.RemoteStart)
-        expect(transactionEvent.seqNo).toBe(0)
-        expect(transactionEvent.transactionInfo.transactionId).toBe(transactionId)
-      })
+        await it('should select EVDetected from cable_action context with detected state', () => {
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Updated,
+            TransactionContextFixtures.evDetected()
+          )
 
-      await it('should pass through optional parameters correctly', () => {
-        const connectorId = 2
-        const transactionId = generateUUID()
-        const context: OCPP20TransactionContext = {
-          authorizationMethod: 'idToken',
-          source: 'local_authorization',
-        }
-        const options = {
-          chargingState: OCPP20ChargingStateEnumType.Charging,
-          idToken: {
-            idToken: 'CONTEXT_TEST_TOKEN',
-            type: OCPP20IdTokenEnumType.ISO14443,
-          },
-        }
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
+        })
 
-        const transactionEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          context,
-          connectorId,
-          transactionId,
-          options
-        )
+        await it('should select EVDeparted from cable_action context with unplugged state', () => {
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            TransactionContextFixtures.evDeparted()
+          )
 
-        expect(transactionEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-        expect(transactionEvent.idToken?.idToken).toBe('CONTEXT_TEST_TOKEN')
-        expect(transactionEvent.transactionInfo.chargingState).toBe(
-          OCPP20ChargingStateEnumType.Charging
-        )
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
+        })
       })
     })
 
-    await describe('sendTransactionEvent with context parameter', async () => {
-      await it('should send TransactionEvent with context-aware TriggerReason selection', async () => {
-        const connectorId = 1
-        const transactionId = generateUUID()
-        const context: OCPP20TransactionContext = {
-          cableState: 'plugged_in',
-          source: 'cable_action',
-        }
+    // ==========================================================================
+    // E03 IdToken-First Specific Tests
+    // ==========================================================================
+    await describe('E03 - IdToken-First Pre-Authorization', async () => {
+      beforeEach(() => {
+        resetConnectorTransactionState(mockChargingStation)
+      })
 
-        const response = await OCPP20ServiceUtils.sendTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          context,
-          connectorId,
-          transactionId
-        )
+      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',
+          }
 
-        // Validate response structure
-        expect(response).toBeDefined()
-        expect(typeof response).toBe('object')
-      })
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            context
+          )
 
-      await it('should handle context-aware error scenarios gracefully', async () => {
-        // Create error mock for this test
-        const { station: errorMockChargingStation } = createMockChargingStation({
-          baseName: TEST_CHARGING_STATION_BASE_NAME,
-          connectorsCount: 1,
-          evseConfiguration: { evsesCount: 1 },
-          heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-          ocppRequestService: {
-            requestHandler: () => {
-              throw new Error('Context test error')
-            },
-          },
-          stationInfo: {
-            ocppStrictCompliance: true,
-            ocppVersion: OCPPVersion.VERSION_201,
-          },
-          websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
         })
 
-        const connectorId = 1
-        const transactionId = generateUUID()
-        const context: OCPP20TransactionContext = {
-          abnormalCondition: 'TestError',
-          source: 'abnormal_condition',
-        }
+        await it('should differentiate IdToken-first from Cable-first by trigger reason', () => {
+          // IdToken-first: Authorized trigger
+          const idTokenFirstContext: OCPP20TransactionContext = {
+            authorizationMethod: 'idToken',
+            source: 'local_authorization',
+          }
 
-        try {
-          await OCPP20ServiceUtils.sendTransactionEvent(
-            errorMockChargingStation,
-            OCPP20TransactionEventEnumType.Ended,
-            context,
-            connectorId,
-            transactionId
+          // Cable-first: CablePluggedIn trigger
+          const cableFirstContext: OCPP20TransactionContext = {
+            cableState: 'plugged_in',
+            source: 'cable_action',
+          }
+
+          const idTokenTrigger = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            idTokenFirstContext
           )
-          throw new Error('Should have thrown error')
-        } catch (error) {
-          expect((error as Error).message).toContain('Context test error')
-        }
-      })
-    })
 
-    await describe('Backward Compatibility', async () => {
-      await it('should maintain compatibility with existing buildTransactionEvent calls', () => {
-        const connectorId = 1
-        const transactionId = generateUUID()
+          const cableTrigger = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Started,
+            cableFirstContext
+          )
 
-        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+          expect(idTokenTrigger).toBe(OCPP20TriggerReasonEnumType.Authorized)
+          expect(cableTrigger).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
+          expect(idTokenTrigger).not.toBe(cableTrigger)
+        })
+      })
 
-        // Old method call should still work
-        const oldEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.Authorized,
-          connectorId,
-          transactionId
-        )
+      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,
+          }
 
-        expect(oldEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-        expect(oldEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-        expect(oldEvent.seqNo).toBe(0)
-      })
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-      await it('should maintain compatibility with existing sendTransactionEvent calls', async () => {
-        const connectorId = 1
-        const transactionId = generateUUID()
+          // 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 }
+          )
 
-        // Old method call should still work
-        const response = await OCPP20ServiceUtils.sendTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.Authorized,
-          connectorId,
-          transactionId
-        )
+          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)
+        })
 
-        expect(response).toBeDefined()
-        expect(typeof response).toBe('object')
-      })
-    })
-  })
+        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,
+          }
 
-  // ==========================================================================
-  // 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(
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+          // First event includes idToken
+          const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
             OCPP20TransactionEventEnumType.Started,
-            startContext
+            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(triggerReason).toBe(expectedStartTrigger)
+
+          expect(startedEvent.idToken).toBeDefined()
+          expect(updatedEvent.idToken).toBeUndefined()
         })
 
-        await it(`should build correct Started event for ${description}`, () => {
+        await it('should support various IdToken types for E03 flow', () => {
           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(
+          // 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,
-            expectedStartTrigger,
+            OCPP20TriggerReasonEnumType.Authorized,
             connectorId,
-            transactionId,
-            idToken != null ? { idToken } : undefined
+            generateUUID(),
+            { idToken: emaidToken }
           )
 
-          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`)
-          }
+          expect(emaidEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.eMAID)
+          expect(emaidEvent.idToken?.idToken).toBe('DE*ABC*E123456*1')
         })
+      })
 
-        await it(`should support complete ${description} 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 | undefined = includeIdToken
-            ? { idToken: `${id.toUpperCase()}_LIFECYCLE_001`, type: OCPP20IdTokenEnumType.ISO14443 }
-            : undefined
+          const idToken: OCPP20IdTokenType = {
+            idToken: 'LIFECYCLE_TOKEN_001',
+            type: OCPP20IdTokenEnumType.ISO14443,
+          }
 
           OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-          // Step 1: Started event
-          const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          // E03 Step 1: IdToken presented and authorized (Started with Authorized trigger)
+          const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent(
             mockChargingStation,
             OCPP20TransactionEventEnumType.Started,
-            expectedStartTrigger,
+            OCPP20TriggerReasonEnumType.Authorized,
             connectorId,
             transactionId,
-            idToken != null ? { idToken } : undefined
+            { idToken }
+          )
+
+          // E03 Step 2: Cable connected (Updated event)
+          const cableConnectedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Updated,
+            OCPP20TriggerReasonEnumType.CablePluggedIn,
+            connectorId,
+            transactionId
           )
 
-          // Step 2: Charging state change
+          // E03 Step 3: Charging starts
           const chargingEvent = OCPP20ServiceUtils.buildTransactionEvent(
             mockChargingStation,
             OCPP20TransactionEventEnumType.Updated,
@@ -1036,7 +1632,7 @@ await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async ()
             { chargingState: OCPP20ChargingStateEnumType.Charging }
           )
 
-          // Step 3: Ended event
+          // E03 Step 4: Transaction ends
           const endedEvent = OCPP20ServiceUtils.buildTransactionEvent(
             mockChargingStation,
             OCPP20TransactionEventEnumType.Ended,
@@ -1046,1703 +1642,1122 @@ await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async ()
           )
 
           // Validate event sequence
-          expect(startedEvent.seqNo).toBe(0)
-          expect(chargingEvent.seqNo).toBe(1)
-          expect(endedEvent.seqNo).toBe(2)
+          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(startedEvent.transactionInfo.transactionId).toBe(transactionId)
+          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 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)
+        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,
+          }
 
-          // Start transaction on connector 1
-          const conn1Event1 = OCPP20ServiceUtils.buildTransactionEvent(
-            mockChargingStation,
-            OCPP20TransactionEventEnumType.Started,
-            expectedStartTrigger,
-            connector1,
-            transaction1Id
-          )
+          // E03 IdToken-First: Starts with Authorized trigger
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+          const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
+          if (connectorStatus != null) {
+            connectorStatus.transactionIdTokenSent = undefined
+          }
 
-          // Start transaction on connector 2
-          const conn2Event1 = OCPP20ServiceUtils.buildTransactionEvent(
+          const e03Start = OCPP20ServiceUtils.buildTransactionEvent(
             mockChargingStation,
             OCPP20TransactionEventEnumType.Started,
-            expectedStartTrigger,
-            connector2,
-            transaction2Id
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            e03TransactionId,
+            { idToken }
           )
 
-          // Update connector 1
-          const conn1Event2 = OCPP20ServiceUtils.buildTransactionEvent(
-            mockChargingStation,
-            OCPP20TransactionEventEnumType.Updated,
-            OCPP20TriggerReasonEnumType.ChargingStateChanged,
-            connector1,
-            transaction1Id
-          )
+          // E02 Cable-First: Starts with CablePluggedIn trigger
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+          if (connectorStatus != null) {
+            connectorStatus.transactionIdTokenSent = undefined
+          }
 
-          // Update connector 2
-          const conn2Event2 = OCPP20ServiceUtils.buildTransactionEvent(
+          const e02Start = OCPP20ServiceUtils.buildTransactionEvent(
             mockChargingStation,
-            OCPP20TransactionEventEnumType.Updated,
-            OCPP20TriggerReasonEnumType.ChargingStateChanged,
-            connector2,
-            transaction2Id
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.CablePluggedIn,
+            connectorId,
+            e02TransactionId
           )
 
-          // Verify independent sequence numbers
-          expect(conn1Event1.seqNo).toBe(0)
-          expect(conn1Event2.seqNo).toBe(1)
-          expect(conn2Event1.seqNo).toBe(0)
-          expect(conn2Event2.seqNo).toBe(1)
+          // Key difference: E03 starts with Authorized, E02 starts with CablePluggedIn
+          expect(e03Start.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+          expect(e02Start.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
 
-          // Verify independent transaction IDs
-          expect(conn1Event1.transactionInfo.transactionId).toBe(transaction1Id)
-          expect(conn2Event1.transactionInfo.transactionId).toBe(transaction2Id)
+          // E03 includes idToken in first event, E02 may not
+          expect(e03Start.idToken).toBeDefined()
+          expect(e02Start.idToken).toBeUndefined()
         })
       })
-    }
-  })
-
-  // ==========================================================================
-  // E02 Cable-First Specific Tests
-  // ==========================================================================
-  await describe('E02 - Cable-First Transaction', 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)
+      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,
+          }
 
-        // Step 1: Cable plugged in (Started)
-        const cablePluggedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.CablePluggedIn,
-          connectorId,
-          transactionId
-        )
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        // Step 2: EV detected (Updated)
-        const evDetectedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.EVDetected,
-          connectorId,
-          transactionId
-        )
+          // E03.FR.05: User authorizes with IdToken
+          const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            transactionId,
+            { idToken }
+          )
 
-        // Step 3: Charging starts (Updated with ChargingStateChanged)
-        const chargingStartedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
-          connectorId,
-          transactionId,
-          { chargingState: OCPP20ChargingStateEnumType.Charging }
-        )
+          // E03.FR.06: Cable not connected within timeout - transaction ends with Timeout
+          const timeoutEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Ended,
+            OCPP20TriggerReasonEnumType.EVConnectTimeout,
+            connectorId,
+            transactionId
+          )
 
-        // Assert sequence numbers follow correct order
-        expect(cablePluggedEvent.seqNo).toBe(0)
-        expect(evDetectedEvent.seqNo).toBe(1)
-        expect(chargingStartedEvent.seqNo).toBe(2)
+          expect(authorizedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+          expect(authorizedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
 
-        // 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)
+          expect(timeoutEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
+          expect(timeoutEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.EVConnectTimeout)
+          expect(timeoutEvent.seqNo).toBe(1)
 
-        // Assert event types match expected pattern
-        expect(cablePluggedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-        expect(evDetectedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
-        expect(chargingStartedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+          // Same transaction ID for both events
+          expect(authorizedEvent.transactionInfo.transactionId).toBe(
+            timeoutEvent.transactionInfo.transactionId
+          )
+        })
       })
 
-      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
-        )
+      await describe('Authorization Status in E03', async () => {
+        await it('should support Deauthorized trigger for rejected authorization', () => {
+          const context: OCPP20TransactionContext = {
+            authorizationMethod: 'idToken',
+            isDeauthorized: true,
+            source: 'local_authorization',
+          }
 
-        // End transaction with EV departure (cable removal)
-        const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Ended,
-          OCPP20TriggerReasonEnumType.EVDeparted,
-          connectorId,
-          transactionId
-        )
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
 
-        // 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)
-      })
-    })
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized)
+        })
 
-    await describe('EV Detection', async () => {
-      await it('should include EVDetected between cable plug and charging start', () => {
-        const connectorId = 1
-        const transactionId = generateUUID()
+        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)
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        // Build full cable-first flow
-        const events = [
-          OCPP20ServiceUtils.buildTransactionEvent(
+          // Transaction started with authorization
+          const startEvent = 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 }
-          ),
-        ]
+            { idToken }
+          )
 
-        // 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)
+          // Transaction ended due to deauthorization (e.g., token revoked mid-session)
+          const revokedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Ended,
+            OCPP20TriggerReasonEnumType.Deauthorized,
+            connectorId,
+            transactionId
+          )
 
-        // Assert continuous sequence numbers
-        for (let i = 0; i < events.length; i++) {
-          expect(events[i].seqNo).toBe(i)
-        }
-      })
-    })
+          expect(startEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
+          expect(revokedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
+          expect(revokedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized)
+        })
 
-    await describe('Connector Status Transitions', async () => {
-      await it('should track connector status through cable-first lifecycle', () => {
-        const connectorId = 1
+        await it('should support StopAuthorized trigger for normal transaction end', () => {
+          const context: OCPP20TransactionContext = {
+            authorizationMethod: 'stopAuthorized',
+            source: 'local_authorization',
+          }
 
-        // Get connector status object
-        const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
-        expect(connectorStatus).toBeDefined()
-        if (connectorStatus == null) {
-          throw new Error('Connector status should be defined')
-        }
+          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
+            OCPP20TransactionEventEnumType.Ended,
+            context
+          )
 
-        // 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)
+          expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.StopAuthorized)
+        })
       })
 
-      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
+      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,
+          }
 
-        // Transition to charging
-        connectorStatus.status = ConnectorStatusEnum.Charging
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        // Transaction ID should persist through state changes
-        expect(connectorStatus.transactionId).toBe(transactionId)
-        expect(connectorStatus.transactionStarted).toBe(true)
+          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)
+          })
+        })
 
-        // Transition to finished
-        connectorStatus.status = ConnectorStatusEnum.Finishing
+        await it('should use unique transaction ID (E03.FR.08)', () => {
+          const connectorId = 1
 
-        // Still same transaction until fully ended
-        expect(connectorStatus.transactionId).toBe(transactionId)
-      })
-    })
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-    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()
+          const transaction1Id = generateUUID()
+          const transaction2Id = generateUUID()
 
-        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+          // E03.FR.08: transactionId MUST be unique
+          expect(transaction1Id).not.toBe(transaction2Id)
 
-        // Cable-first flow with suspended state
-        const events = [
-          // 1. Cable plugged
-          OCPP20ServiceUtils.buildTransactionEvent(
+          const event1 = 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,
+            OCPP20TriggerReasonEnumType.Authorized,
             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()
-        )
+            transaction1Id
+          )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
-      })
+          OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-      await it('should select EVDeparted from cable_action context with unplugged state', () => {
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          TransactionContextFixtures.evDeparted()
-        )
+          const event2 = OCPP20ServiceUtils.buildTransactionEvent(
+            mockChargingStation,
+            OCPP20TransactionEventEnumType.Started,
+            OCPP20TriggerReasonEnumType.Authorized,
+            connectorId,
+            transaction2Id
+          )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
+          expect(event1.transactionInfo.transactionId).toBe(transaction1Id)
+          expect(event2.transactionInfo.transactionId).toBe(transaction2Id)
+          expect(event1.transactionInfo.transactionId).not.toBe(
+            event2.transactionInfo.transactionId
+          )
+        })
       })
     })
   })
 
-  // ==========================================================================
-  // E03 IdToken-First Specific Tests
-  // ==========================================================================
-  await describe('E03 - IdToken-First Pre-Authorization', 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
-        )
+  // ============================================================================
+  // Offline TransactionEvent Queueing Tests
+  // ============================================================================
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
-      })
+  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
 
-      await it('should differentiate IdToken-first from Cable-first by trigger reason', () => {
-        // IdToken-first: Authorized trigger
-        const idTokenFirstContext: OCPP20TransactionContext = {
-          authorizationMethod: 'idToken',
-          source: 'local_authorization',
-        }
+    beforeEach(() => {
+      mockTracking = createMockStationWithRequestTracking()
+      mockChargingStation = mockTracking.station
+      sentRequests = mockTracking.sentRequests
+      setOnline = mockTracking.setOnline
+    })
 
-        // Cable-first: CablePluggedIn trigger
-        const cableFirstContext: OCPP20TransactionContext = {
-          cableState: 'plugged_in',
-          source: 'cable_action',
+    afterEach(() => {
+      for (let connectorId = 1; connectorId <= 3; connectorId++) {
+        const connector = mockChargingStation.getConnectorStatus(connectorId)
+        if (connector != null) {
+          connector.transactionEventQueue = undefined
         }
-
-        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)
-      })
+      }
+      standardCleanup()
     })
 
-    await describe('E03.FR.01 - IdToken in TransactionEvent', async () => {
-      await it('should include idToken in first TransactionEvent after authorization', () => {
+    await describe('Queue formation when offline', async () => {
+      await it('should queue TransactionEvent when WebSocket is disconnected', async () => {
         const connectorId = 1
         const transactionId = generateUUID()
-        const idToken: OCPP20IdTokenType = {
-          idToken: 'VALID_TOKEN_E03_001',
-          type: OCPP20IdTokenEnumType.ISO14443,
-        }
+
+        setOnline(false)
 
         OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        // Build Started event with idToken (E03.FR.01: IdToken must be in first event)
-        const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        const response = await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
           OCPP20TriggerReasonEnumType.Authorized,
           connectorId,
-          transactionId,
-          { idToken }
+          transactionId
         )
 
-        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)
+        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 not include idToken in subsequent events (E03.FR.01 compliance)', () => {
+      await it('should queue multiple TransactionEvents in order when offline', async () => {
         const connectorId = 1
         const transactionId = generateUUID()
-        const idToken: OCPP20IdTokenType = {
-          idToken: 'VALID_TOKEN_E03_002',
-          type: OCPP20IdTokenEnumType.ISO14443,
-        }
+
+        setOnline(false)
 
         OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        // First event includes idToken
-        const startedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
           OCPP20TriggerReasonEnumType.Authorized,
           connectorId,
-          transactionId,
-          { idToken }
+          transactionId
         )
 
-        // Second event should NOT include idToken (flag is set after first inclusion)
-        const updatedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
           connectorId,
-          transactionId,
-          { chargingState: OCPP20ChargingStateEnumType.Charging, idToken }
+          transactionId
         )
 
-        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(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.Authorized,
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.StopAuthorized,
           connectorId,
-          transactionId,
-          { idToken: rfidToken }
+          transactionId
         )
 
-        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
-        }
+        const connector = mockChargingStation.getConnectorStatus(connectorId)
+        expect(connector?.transactionEventQueue?.length).toBe(3)
 
-        // Test eMAID (contract identifier)
-        const emaidToken: OCPP20IdTokenType = {
-          idToken: 'DE*ABC*E123456*1',
-          type: OCPP20IdTokenEnumType.eMAID,
-        }
+        expect(connector.transactionEventQueue[0].seqNo).toBe(0)
+        expect(connector.transactionEventQueue[1].seqNo).toBe(1)
+        expect(connector.transactionEventQueue[2].seqNo).toBe(2)
 
-        const emaidEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.Authorized,
-          connectorId,
-          generateUUID(),
-          { idToken: emaidToken }
+        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
         )
-
-        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', () => {
+      await it('should preserve seqNo in queued events', async () => {
         const connectorId = 1
         const transactionId = generateUUID()
-        const idToken: OCPP20IdTokenType = {
-          idToken: 'LIFECYCLE_TOKEN_001',
-          type: OCPP20IdTokenEnumType.ISO14443,
-        }
 
+        setOnline(true)
         OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        // E03 Step 1: IdToken presented and authorized (Started with Authorized trigger)
-        const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
           OCPP20TriggerReasonEnumType.Authorized,
           connectorId,
-          transactionId,
-          { idToken }
+          transactionId
         )
 
-        // E03 Step 2: Cable connected (Updated event)
-        const cableConnectedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        expect(sentRequests.length).toBe(1)
+        expect(sentRequests[0].payload.seqNo).toBe(0)
+
+        setOnline(false)
+
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.CablePluggedIn,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
           connectorId,
           transactionId
         )
 
-        // E03 Step 3: Charging starts
-        const chargingEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Updated,
-          OCPP20TriggerReasonEnumType.ChargingStateChanged,
-          connectorId,
-          transactionId,
-          { chargingState: OCPP20ChargingStateEnumType.Charging }
-        )
-
-        // E03 Step 4: Transaction ends
-        const endedEvent = OCPP20ServiceUtils.buildTransactionEvent(
-          mockChargingStation,
-          OCPP20TransactionEventEnumType.Ended,
-          OCPP20TriggerReasonEnumType.StopAuthorized,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
           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)
+        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 differentiate E03 lifecycle from E02 Cable-First lifecycle', () => {
+      await it('should include timestamp in queued events', async () => {
         const connectorId = 1
-        const e03TransactionId = generateUUID()
-        const e02TransactionId = generateUUID()
-        const idToken: OCPP20IdTokenType = {
-          idToken: 'COMPARE_TOKEN_001',
-          type: OCPP20IdTokenEnumType.ISO14443,
-        }
+        const transactionId = generateUUID()
 
-        // E03 IdToken-First: Starts with Authorized trigger
+        setOnline(false)
         OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
-        const connectorStatus = mockChargingStation.getConnectorStatus(connectorId)
-        if (connectorStatus != null) {
-          connectorStatus.transactionIdTokenSent = undefined
-        }
 
-        const e03Start = OCPP20ServiceUtils.buildTransactionEvent(
+        const beforeQueue = new Date()
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
           OCPP20TriggerReasonEnumType.Authorized,
           connectorId,
-          e03TransactionId,
-          { idToken }
+          transactionId
         )
+        const afterQueue = new Date()
 
-        // 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
+        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()
         )
-
-        // 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)', () => {
+    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()
-        const idToken: OCPP20IdTokenType = {
-          idToken: 'TIMEOUT_TOKEN_001',
-          type: OCPP20IdTokenEnumType.ISO14443,
-        }
 
+        setOnline(false)
         OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        // E03.FR.05: User authorizes with IdToken
-        const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
           OCPP20TriggerReasonEnumType.Authorized,
           connectorId,
-          transactionId,
-          { idToken }
+          transactionId
         )
 
-        // E03.FR.06: Cable not connected within timeout - transaction ends with Timeout
-        const timeoutEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
-          OCPP20TransactionEventEnumType.Ended,
-          OCPP20TriggerReasonEnumType.EVConnectTimeout,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
           connectorId,
           transactionId
         )
 
-        expect(authorizedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-        expect(authorizedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
+        expect(sentRequests.length).toBe(0)
 
-        expect(timeoutEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
-        expect(timeoutEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.EVConnectTimeout)
-        expect(timeoutEvent.seqNo).toBe(1)
+        setOnline(true)
 
-        // Same transaction ID for both events
-        expect(authorizedEvent.transactionInfo.transactionId).toBe(
-          timeoutEvent.transactionInfo.transactionId
-        )
+        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 describe('Authorization Status in E03', async () => {
-      await it('should support Deauthorized trigger for rejected authorization', () => {
-        const context: OCPP20TransactionContext = {
-          authorizationMethod: 'idToken',
-          isDeauthorized: true,
-          source: 'local_authorization',
-        }
+      await it('should clear queue after sending', async () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
+        setOnline(false)
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId
         )
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized)
+        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 handle transaction end after token revocation', () => {
+      await it('should preserve FIFO order when draining queue', async () => {
         const connectorId = 1
         const transactionId = generateUUID()
-        const idToken: OCPP20IdTokenType = {
-          idToken: 'REVOKED_TOKEN_001',
-          type: OCPP20IdTokenEnumType.ISO14443,
-        }
 
+        setOnline(false)
         OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-        // Transaction started with authorization
-        const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
           OCPP20TriggerReasonEnumType.Authorized,
           connectorId,
-          transactionId,
-          { idToken }
+          transactionId
+        )
+
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.ChargingStateChanged,
+          connectorId,
+          transactionId
         )
 
-        // Transaction ended due to deauthorization (e.g., token revoked mid-session)
-        const revokedEvent = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Ended,
-          OCPP20TriggerReasonEnumType.Deauthorized,
+          OCPP20TriggerReasonEnumType.StopAuthorized,
           connectorId,
           transactionId
         )
 
-        expect(startEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started)
-        expect(revokedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended)
-        expect(revokedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized)
+        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 support StopAuthorized trigger for normal transaction end', () => {
-        const context: OCPP20TransactionContext = {
-          authorizationMethod: 'stopAuthorized',
-          source: 'local_authorization',
-        }
+      await it('should handle empty queue gracefully', async () => {
+        const connectorId = 1
 
-        const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-          OCPP20TransactionEventEnumType.Ended,
-          context
-        )
+        await expect(
+          OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+        ).resolves.toBeUndefined()
 
-        expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.StopAuthorized)
+        expect(sentRequests.length).toBe(0)
       })
-    })
 
-    await describe('E03.FR.07/08 - Sequence Numbers and Transaction ID', async () => {
-      await it('should maintain continuous sequence numbers throughout E03 lifecycle', () => {
+      await it('should handle null queue gracefully', async () => {
         const connectorId = 1
-        const transactionId = generateUUID()
-        const idToken: OCPP20IdTokenType = {
-          idToken: 'SEQ_TOKEN_001',
-          type: OCPP20IdTokenEnumType.ISO14443,
-        }
-
-        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+        const connector = mockChargingStation.getConnectorStatus(connectorId)
+        connector.transactionEventQueue = undefined
 
-        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
-          ),
-        ]
+        await expect(
+          OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+        ).resolves.toBeUndefined()
 
-        // E03.FR.07: Sequence numbers must be continuous
-        events.forEach((event, index) => {
-          expect(event.seqNo).toBe(index)
-        })
+        expect(sentRequests.length).toBe(0)
       })
+    })
 
-      await it('should use unique transaction ID (E03.FR.08)', () => {
+    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)
 
-        const transaction1Id = generateUUID()
-        const transaction2Id = generateUUID()
-
-        // E03.FR.08: transactionId MUST be unique
-        expect(transaction1Id).not.toBe(transaction2Id)
-
-        const event1 = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
           OCPP20TransactionEventEnumType.Started,
           OCPP20TriggerReasonEnumType.Authorized,
           connectorId,
-          transaction1Id
+          transactionId
         )
+        expect(sentRequests[0].payload.seqNo).toBe(0)
 
-        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+        setOnline(false)
 
-        const event2 = OCPP20ServiceUtils.buildTransactionEvent(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           mockChargingStation,
-          OCPP20TransactionEventEnumType.Started,
-          OCPP20TriggerReasonEnumType.Authorized,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
           connectorId,
-          transaction2Id
+          transactionId
         )
 
-        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
-      )
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          connectorId,
+          transactionId
+        )
 
-      expect(sentRequests.length).toBe(0)
+        setOnline(true)
 
-      expect(response.idTokenInfo).toBeUndefined()
+        await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
 
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector?.transactionEventQueue).toBeDefined()
-      expect(connector.transactionEventQueue.length).toBe(1)
-      expect(connector.transactionEventQueue[0].seqNo).toBe(0)
-    })
+        expect(sentRequests[1].payload.seqNo).toBe(1)
+        expect(sentRequests[2].payload.seqNo).toBe(2)
 
-    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 OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.StopAuthorized,
+          connectorId,
+          transactionId
+        )
 
-    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)
-    })
+        expect(sentRequests[3].payload.seqNo).toBe(3)
 
-    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()
-      )
+        for (let i = 0; i < sentRequests.length; i++) {
+          expect(sentRequests[i].payload.seqNo).toBe(i)
+        }
+      })
     })
-  })
 
-  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()
+    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, connectorId)
+        setOnline(false)
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          1,
+          transactionId1
+        )
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          2,
+          transactionId2
+        )
 
-      expect(sentRequests.length).toBe(0)
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          1,
+          transactionId1
+        )
 
-      setOnline(true)
+        const connector1 = mockChargingStation.getConnectorStatus(1)
+        const connector2 = mockChargingStation.getConnectorStatus(2)
 
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+        expect(connector1?.transactionEventQueue?.length).toBe(2)
+        expect(connector2?.transactionEventQueue?.length).toBe(1)
 
-      expect(sentRequests.length).toBe(2)
-      expect(sentRequests[0].payload.seqNo).toBe(0)
-      expect(sentRequests[1].payload.seqNo).toBe(1)
-    })
+        expect(connector1.transactionEventQueue[0].request.transactionInfo.transactionId).toBe(
+          transactionId1
+        )
+        expect(connector2.transactionEventQueue[0].request.transactionInfo.transactionId).toBe(
+          transactionId2
+        )
+      })
 
-    await it('should clear queue after sending', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
+      await it('should drain queues independently per connector', async () => {
+        const transactionId1 = generateUUID()
+        const transactionId2 = generateUUID()
 
-      setOnline(false)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+        setOnline(false)
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          1,
+          transactionId1
+        )
 
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector?.transactionEventQueue?.length).toBe(1)
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          2,
+          transactionId2
+        )
 
-      setOnline(true)
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
+        setOnline(true)
 
-      expect(connector.transactionEventQueue.length).toBe(0)
-    })
+        await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 1)
 
-    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)
-    })
+        expect(sentRequests.length).toBe(1)
+        expect(sentRequests[0].payload.transactionInfo.transactionId).toBe(transactionId1)
 
-    await it('should handle empty queue gracefully', async () => {
-      const connectorId = 1
+        const connector2 = mockChargingStation.getConnectorStatus(2)
+        expect(connector2?.transactionEventQueue?.length).toBe(1)
 
-      await expect(
-        OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
-      ).resolves.toBeUndefined()
+        await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 2)
 
-      expect(sentRequests.length).toBe(0)
+        expect(sentRequests.length).toBe(2)
+        expect(sentRequests[1].payload.transactionInfo.transactionId).toBe(transactionId2)
+      })
     })
 
-    await it('should handle null queue gracefully', async () => {
-      const connectorId = 1
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      connector.transactionEventQueue = undefined
+    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
 
-      await expect(
-        OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
-      ).resolves.toBeUndefined()
+        const errorOnSecondMock = mock.fn(async () => {
+          callCount++
+          if (callCount === 2) {
+            throw new Error('Network error on second event')
+          }
+          return Promise.resolve({} as EmptyObject)
+        })
 
-      expect(sentRequests.length).toBe(0)
-    })
-  })
+        const { station: errorStation } = createMockChargingStation({
+          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,
+        })
 
-  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)
-      }
-    })
-  })
+        errorStation.isWebSocketConnectionOpened = () => false
 
-  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
-      )
-    })
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(errorStation, connectorId)
 
-    await it('should drain queues independently per connector', async () => {
-      const transactionId1 = generateUUID()
-      const transactionId2 = generateUUID()
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          errorStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId
+        )
 
-      setOnline(false)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          errorStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          connectorId,
+          transactionId
+        )
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        1,
-        transactionId1
-      )
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          errorStation,
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.StopAuthorized,
+          connectorId,
+          transactionId
+        )
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        2,
-        transactionId2
-      )
+        errorStation.isWebSocketConnectionOpened = () => true
 
-      setOnline(true)
+        await OCPP20ServiceUtils.sendQueuedTransactionEvents(errorStation, connectorId)
 
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 1)
+        expect(callCount).toBe(3)
+      })
+    })
+  })
 
-      expect(sentRequests.length).toBe(1)
-      expect(sentRequests[0].payload.transactionInfo.transactionId).toBe(transactionId1)
+  // ============================================================================
+  // Periodic TransactionEvent Tests
+  // ============================================================================
 
-      const connector2 = mockChargingStation.getConnectorStatus(2)
-      expect(connector2?.transactionEventQueue?.length).toBe(1)
+  await describe('E02 - OCPP 2.0.1 Periodic TransactionEvent at TxUpdatedInterval', async () => {
+    let mockTracking: MockStationWithTracking
+    let mockChargingStation: ChargingStation
+    let sentRequests: CapturedOCPPRequest[]
 
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 2)
+    beforeEach(() => {
+      mockTracking = createMockStationWithRequestTracking()
+      mockChargingStation = mockTracking.station
+      sentRequests = mockTracking.sentRequests
+    })
 
-      expect(sentRequests.length).toBe(2)
-      expect(sentRequests[1].payload.transactionInfo.transactionId).toBe(transactionId2)
+    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('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
+    await describe('startTxUpdatedInterval', async () => {
+      await it('should not start timer for non-OCPP 2.0 stations', () => {
+        const { station: ocpp16Station } = createMockChargingStation({
+          baseName: TEST_CHARGING_STATION_BASE_NAME,
+          connectorsCount: 1,
+          stationInfo: {
+            ocppVersion: OCPPVersion.VERSION_16,
+          },
+        })
 
-      const errorOnSecondMock = mock.fn(async () => {
-        callCount++
-        if (callCount === 2) {
-          throw new Error('Network error on second event')
-        }
-        return Promise.resolve({} as EmptyObject)
-      })
+        // Call startTxUpdatedInterval on OCPP 1.6 station
+        ocpp16Station.startTxUpdatedInterval(1, 60000)
 
-      const { station: errorStation } = createMockChargingStation({
-        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,
+        // Verify no timer was started (method should return early)
+        const connector = ocpp16Station.getConnectorStatus(1)
+        expect(connector?.transactionTxUpdatedSetInterval).toBeUndefined()
       })
 
-      errorStation.isWebSocketConnectionOpened = () => false
+      await it('should not start timer when interval is zero', () => {
+        const connectorId = 1
 
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(errorStation, connectorId)
+        // Simulate startTxUpdatedInterval with zero interval
+        const connector = mockChargingStation.getConnectorStatus(connectorId)
+        expect(connector).toBeDefined()
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        errorStation,
-        OCPP20TransactionEventEnumType.Started,
-        OCPP20TriggerReasonEnumType.Authorized,
-        connectorId,
-        transactionId
-      )
+        // Zero interval should not start timer
+        // This is verified by the implementation logging debug message
+        expect(connector.transactionTxUpdatedSetInterval).toBeUndefined()
+      })
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        errorStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
+      await it('should not start timer when interval is negative', () => {
+        const connectorId = 1
+        const connector = mockChargingStation.getConnectorStatus(connectorId)
+        expect(connector).toBeDefined()
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        errorStation,
-        OCPP20TransactionEventEnumType.Ended,
-        OCPP20TriggerReasonEnumType.StopAuthorized,
-        connectorId,
-        transactionId
-      )
+        // Negative interval should not start timer
+        expect(connector.transactionTxUpdatedSetInterval).toBeUndefined()
+      })
 
-      errorStation.isWebSocketConnectionOpened = () => true
+      await it('should handle non-existent connector gracefully', () => {
+        const nonExistentConnectorId = 999
 
-      await OCPP20ServiceUtils.sendQueuedTransactionEvents(errorStation, connectorId)
+        // Should not throw for non-existent connector
+        expect(() => {
+          mockChargingStation.getConnectorStatus(nonExistentConnectorId)
+        }).not.toThrow()
 
-      expect(callCount).toBe(3)
+        // Should return undefined for non-existent connector
+        expect(mockChargingStation.getConnectorStatus(nonExistentConnectorId)).toBeUndefined()
+      })
     })
-  })
-})
 
-// ============================================================================
-// 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
-  })
+    await describe('Periodic TransactionEvent generation', async () => {
+      await it('should send TransactionEvent with MeterValuePeriodic trigger reason', async () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-  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()
-  })
+        // Reset sequence number
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-  await describe('startTxUpdatedInterval', async () => {
-    await it('should not start timer for non-OCPP 2.0 stations', () => {
-      const { station: ocpp16Station } = createMockChargingStation({
-        baseName: TEST_CHARGING_STATION_BASE_NAME,
-        connectorsCount: 1,
-        stationInfo: {
-          ocppVersion: OCPPVersion.VERSION_16,
-        },
+        // 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
+        )
       })
 
-      // Call startTxUpdatedInterval on OCPP 1.6 station
-      ocpp16Station.startTxUpdatedInterval(1, 60000)
+      await it('should increment seqNo for each periodic event', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-      // Verify no timer was started (method should return early)
-      const connector = ocpp16Station.getConnectorStatus(1)
-      expect(connector?.transactionTxUpdatedSetInterval).toBeUndefined()
-    })
+        // Reset sequence number for new transaction
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-    await it('should not start timer when interval is zero', () => {
-      const connectorId = 1
+        // Send initial Started event
+        const startEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Started,
+          OCPP20TriggerReasonEnumType.Authorized,
+          connectorId,
+          transactionId
+        )
+        expect(startEvent.seqNo).toBe(0)
 
-      // Simulate startTxUpdatedInterval with zero interval
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector).toBeDefined()
+        // 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)
+        }
 
-      // Zero interval should not start timer
-      // This is verified by the implementation logging debug message
-      expect(connector.transactionTxUpdatedSetInterval).toBeUndefined()
-    })
+        // Verify sequence numbers are continuous: 0, 1, 2, 3
+        const connector = mockChargingStation.getConnectorStatus(connectorId)
+        expect(connector?.transactionSeqNo).toBe(3)
+      })
 
-    await it('should not start timer when interval is negative', () => {
-      const connectorId = 1
-      const connector = mockChargingStation.getConnectorStatus(connectorId)
-      expect(connector).toBeDefined()
+      await it('should maintain correct eventType (Updated) for periodic events', async () => {
+        const connectorId = 2
+        const transactionId = generateUUID()
 
-      // Negative interval should not start timer
-      expect(connector.transactionTxUpdatedSetInterval).toBeUndefined()
-    })
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-    await it('should handle non-existent connector gracefully', () => {
-      const nonExistentConnectorId = 999
+        // Send periodic event
+        await OCPP20ServiceUtils.sendTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Updated,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+          connectorId,
+          transactionId
+        )
 
-      // Should not throw for non-existent connector
-      expect(() => {
-        mockChargingStation.getConnectorStatus(nonExistentConnectorId)
-      }).not.toThrow()
+        // Verify eventType is Updated (not Started or Ended)
+        expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Updated)
+      })
 
-      // Should return undefined for non-existent connector
-      expect(mockChargingStation.getConnectorStatus(nonExistentConnectorId)).toBeUndefined()
-    })
-  })
+      await it('should include EVSE information in periodic events', async () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-  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
-      )
-    })
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-    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(
+        await OCPP20ServiceUtils.sendTransactionEvent(
           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)
-    })
+        // Verify EVSE info is present
+        expect(sentRequests[0].payload.evse).toBeDefined()
+        expect(sentRequests[0].payload.evse.id).toBe(connectorId)
+      })
 
-    await it('should maintain correct eventType (Updated) for periodic events', async () => {
-      const connectorId = 2
-      const transactionId = generateUUID()
+      await it('should include transactionInfo with correct transactionId', async () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-      // Send periodic event
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
+        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)
+        // Verify transactionInfo contains the transaction ID
+        expect(sentRequests[0].payload.transactionInfo).toBeDefined()
+        expect(sentRequests[0].payload.transactionInfo.transactionId).toBe(transactionId)
+      })
     })
 
-    await it('should include EVSE information in periodic events', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
+    await describe('Timer lifecycle integration', async () => {
+      await it('should continue seqNo sequence across multiple periodic events', () => {
+        const connectorId = 1
+        const transactionId = generateUUID()
 
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+        // Reset for new transaction
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
+        // 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)
 
-      // Verify EVSE info is present
-      expect(sentRequests[0].payload.evse).toBeDefined()
-      expect(sentRequests[0].payload.evse.id).toBe(connectorId)
-    })
+        // 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)
+        }
 
-    await it('should include transactionInfo with correct transactionId', async () => {
-      const connectorId = 1
-      const transactionId = generateUUID()
+        // 3. Ended event (seqNo: 4)
+        const endEvent = OCPP20ServiceUtils.buildTransactionEvent(
+          mockChargingStation,
+          OCPP20TransactionEventEnumType.Ended,
+          OCPP20TriggerReasonEnumType.StopAuthorized,
+          connectorId,
+          transactionId
+        )
+        expect(endEvent.seqNo).toBe(4)
+      })
 
-      OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
+      await it('should handle multiple connectors with independent timers', () => {
+        const transactionId1 = generateUUID()
+        const transactionId2 = generateUUID()
 
-      await OCPP20ServiceUtils.sendTransactionEvent(
-        mockChargingStation,
-        OCPP20TransactionEventEnumType.Updated,
-        OCPP20TriggerReasonEnumType.MeterValuePeriodic,
-        connectorId,
-        transactionId
-      )
+        // Reset sequence numbers for both connectors
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
+        OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
 
-      // Verify transactionInfo contains the transaction ID
-      expect(sentRequests[0].payload.transactionInfo).toBeDefined()
-      expect(sentRequests[0].payload.transactionInfo.transactionId).toBe(transactionId)
-    })
-  })
+        // 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
+        )
 
-  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(
+        // 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,
-          connectorId,
-          transactionId
+          2,
+          transactionId2
         )
-        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)
-    })
+        // Verify independent sequence numbers
+        expect(event1Start.seqNo).toBe(0)
+        expect(event1Update.seqNo).toBe(1)
+        expect(event2Start.seqNo).toBe(0)
+        expect(event2Update.seqNo).toBe(1)
 
-    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)
+        // 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 { station: errorMockChargingStation } = createMockChargingStation({
-        baseName: TEST_CHARGING_STATION_BASE_NAME,
-        connectorsCount: 1,
-        evseConfiguration: { evsesCount: 1 },
-        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-        ocppRequestService: {
-          requestHandler: () => {
-            throw new Error('Network timeout')
+    await describe('Error handling', async () => {
+      await it('should handle network errors gracefully during periodic event', async () => {
+        const { station: errorMockChargingStation } = createMockChargingStation({
+          baseName: TEST_CHARGING_STATION_BASE_NAME,
+          connectorsCount: 1,
+          evseConfiguration: { evsesCount: 1 },
+          heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+          ocppRequestService: {
+            requestHandler: () => {
+              throw new Error('Network timeout')
+            },
           },
-        },
-        stationInfo: {
-          ocppStrictCompliance: true,
-          ocppVersion: OCPPVersion.VERSION_201,
-        },
-        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
-      })
+          stationInfo: {
+            ocppStrictCompliance: true,
+            ocppVersion: OCPPVersion.VERSION_201,
+          },
+          websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+        })
 
-      // Mock WebSocket as open
-      errorMockChargingStation.isWebSocketConnectionOpened = () => true
+        // Mock WebSocket as open
+        errorMockChargingStation.isWebSocketConnectionOpened = () => true
 
-      const connectorId = 1
-      const transactionId = generateUUID()
+        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')
-      }
+        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 3c2a0ef306d27ab55a450daea4bc50807f708005..87db619fe87913fe95500e295c216b686fade706 100644 (file)
@@ -32,6 +32,7 @@ import {
   SetVariableStatusEnumType,
   type VariableType,
 } from '../../../../src/types/index.js'
+import { StandardParametersKey } from '../../../../src/types/ocpp/Configuration.js'
 import { Constants } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
@@ -75,6 +76,20 @@ await describe('B05 - OCPP20VariableManager', async () => {
       connectorsCount: 3,
       evseConfiguration: { evsesCount: 3 },
       heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+      ocppConfiguration: {
+        configurationKey: [
+          {
+            key: StandardParametersKey.HeartbeatInterval,
+            readonly: false,
+            value: millisecondsToSeconds(Constants.DEFAULT_HEARTBEAT_INTERVAL).toString(),
+          },
+          {
+            key: StandardParametersKey.WebSocketPingInterval,
+            readonly: false,
+            value: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL.toString(),
+          },
+        ],
+      },
       stationInfo: {
         ocppVersion: OCPPVersion.VERSION_201,
       },
@@ -318,7 +333,7 @@ await describe('B05 - OCPP20VariableManager', async () => {
       // Third variable: MessageTimeout
       expect(result[2].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
       expect(result[2].attributeType).toBe(AttributeEnumType.Actual)
-      expect(result[2].attributeValue).toBe(station.getConnectionTimeout().toString())
+      expect(result[2].attributeValue).toBe(Constants.DEFAULT_CONNECTION_TIMEOUT.toString())
       expect(result[2].component.name).toBe(OCPP20ComponentName.OCPPCommCtrlr)
       expect(result[2].component.instance).toBe('Default')
       expect(result[2].variable.name).toBe(OCPP20RequiredVariableName.MessageTimeout)
@@ -833,7 +848,7 @@ await describe('B05 - OCPP20VariableManager', async () => {
     await it('should validate MessageTimeout positive integer >0 and reject invalid', () => {
       const okRes = manager.setVariables(station, [
         {
-          attributeValue: (station.getConnectionTimeout() + 5).toString(),
+          attributeValue: (Constants.DEFAULT_CONNECTION_TIMEOUT + 5).toString(),
           component: { instance: 'Default', name: OCPP20ComponentName.OCPPCommCtrlr },
           variable: { name: OCPP20RequiredVariableName.MessageTimeout },
         },
index 88684d14bda4e4a3500ce6290768a364f731485f..5ec880d3d9afd74fd2bb736ae9d722b34150a226 100644 (file)
@@ -59,43 +59,46 @@ export const createMockAuthRequest = (overrides?: Partial<AuthRequest>): AuthReq
 })
 
 /**
- * Create a mock AuthorizationResult with configurable status.
+ * Create a mock AuthorizationResult with configurable properties.
  *
  * Supports all AuthorizationStatus values: ACCEPTED, INVALID, BLOCKED, EXPIRED, CONCURRENT_TX.
- * @param status - Authorization status (defaults to ACCEPTED)
  * @param overrides - Partial AuthorizationResult properties to override defaults
- * @returns Mock AuthorizationResult with specified status from local list method
+ * @returns Mock AuthorizationResult with specified properties (defaults to ACCEPTED from local list)
  * @example
  * ```typescript
  * // Default: ACCEPTED status
  * const accepted = createMockAuthorizationResult()
  *
  * // Rejected with INVALID status
- * const rejected = createMockAuthorizationResult(AuthorizationStatus.INVALID)
+ * const rejected = createMockAuthorizationResult({ status: AuthorizationStatus.INVALID })
  *
  * // Blocked with custom method
- * const blocked = createMockAuthorizationResult(AuthorizationStatus.BLOCKED, {
+ * const blocked = createMockAuthorizationResult({
+ *   status: AuthorizationStatus.BLOCKED,
  *   method: AuthenticationMethod.REMOTE_AUTHORIZATION
  * })
  *
  * // Expired with custom expiry date
- * const expired = createMockAuthorizationResult(AuthorizationStatus.EXPIRED, {
+ * const expired = createMockAuthorizationResult({
+ *   status: AuthorizationStatus.EXPIRED,
  *   expiryDate: new Date(Date.now() - 1000)
  * })
  * ```
  */
 export const createMockAuthorizationResult = (
-  status: AuthorizationStatus = AuthorizationStatus.ACCEPTED,
   overrides?: Partial<AuthorizationResult>
-): AuthorizationResult => ({
-  isOffline: false,
-  method: AuthenticationMethod.LOCAL_LIST,
-  status,
-  timestamp: new Date(),
-  // For expired status, include a default expiryDate in the past
-  ...(status === AuthorizationStatus.EXPIRED && { expiryDate: new Date(Date.now() - 1000) }),
-  ...overrides,
-})
+): AuthorizationResult => {
+  const status = overrides?.status ?? AuthorizationStatus.ACCEPTED
+  return {
+    isOffline: false,
+    method: AuthenticationMethod.LOCAL_LIST,
+    status,
+    timestamp: new Date(),
+    // For expired status, include a default expiryDate in the past
+    ...(status === AuthorizationStatus.EXPIRED && { expiryDate: new Date(Date.now() - 1000) }),
+    ...overrides,
+  }
+}
 
 /**
  * Create a mock OCPPAuthService that always returns ACCEPTED status.