]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp): make buildRequestPayload the authoritative builder for all commands
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 18 Mar 2026 16:13:49 +0000 (17:13 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 18 Mar 2026 16:13:49 +0000 (17:13 +0100)
buildRequestPayload now calls the centralized builders for
TRANSACTION_EVENT (via buildTransactionEvent) and STATUS_NOTIFICATION
(via buildStatusNotificationRequest) in both OCPP versions, matching
the existing pattern for START_TRANSACTION/STOP_TRANSACTION in 1.6.

Callers pass minimal params (eventType, connectorId, status, etc.)
and buildRequestPayload constructs the complete spec-compliant payload.

sendTransactionEvent's offline path builds directly for queueing since
the queue stores pre-built payloads sent as-is on reconnect.

.serena/memories/code_style_conventions.md
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20RequestService.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts

index dd3593476c470902171c910be9d66873f16a21e5..0c187aa0b08dd1ebe8ccdeed45295df829e19df5 100644 (file)
 - **Message format**: SRPC format: `[messageTypeId, messageId, action, payload]`
 - **Per-station state**: WeakMap-based isolation per station (not singleton properties)
 
+### Request Architecture
+
+- **Single path**: `requestHandler()` → `buildRequestPayload()` → `sendMessage()` — no bypasses
+- **`buildRequestPayload()`** enriches where needed (1.6: meterStart/idTag/timestamp; 2.0: CSR generation), passthrough otherwise
+- **Service utils** (`buildTransactionEvent`, `buildStatusNotificationRequest`, `buildMeterValue`, etc.) build complex payloads upstream before calling `requestHandler`
+- **Broadcast channel handlers** are simple passthroughs to `requestHandler` — no state management or flow duplication
+
 ## Logging
 
 - **Winston** logger with 4 levels: `error`, `warn`, `info`, `debug`
index 23f0c402f2ec92b93ed9dfe4fdab8bfe23d4a76a..541351f4bf466bb713c6e7e73f5cd6027c6a34d1 100644 (file)
@@ -116,11 +116,7 @@ import {
 } from '../../../utils/index.js'
 import { OCPPConstants } from '../OCPPConstants.js'
 import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js'
-import {
-  buildMeterValue,
-  buildStatusNotificationRequest,
-  OCPPServiceUtils,
-} from '../OCPPServiceUtils.js'
+import { buildMeterValue, OCPPServiceUtils } from '../OCPPServiceUtils.js'
 import { OCPP16Constants } from './OCPP16Constants.js'
 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
 
@@ -487,12 +483,11 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
                 .requestHandler<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
                   chargingStation,
                   OCPP16RequestCommand.STATUS_NOTIFICATION,
-                  buildStatusNotificationRequest(
-                    chargingStation,
+                  {
                     connectorId,
-                    chargingStation.getConnectorStatus(connectorId)
-                      ?.status as OCPP16ChargePointStatus
-                  ) as OCPP16StatusNotificationRequest,
+                    status: chargingStation.getConnectorStatus(connectorId)
+                      ?.status as OCPP16ChargePointStatus,
+                  } as unknown as OCPP16StatusNotificationRequest,
                   {
                     triggerMessage: true,
                   }
@@ -508,11 +503,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
                     >(
                       chargingStation,
                       OCPP16RequestCommand.STATUS_NOTIFICATION,
-                      buildStatusNotificationRequest(
-                        chargingStation,
-                        id,
-                        connectorStatus.status as OCPP16ChargePointStatus
-                      ) as OCPP16StatusNotificationRequest,
+                      {
+                        connectorId: id,
+                        status: connectorStatus.status as OCPP16ChargePointStatus,
+                      } as unknown as OCPP16StatusNotificationRequest,
                       {
                         triggerMessage: true,
                       }
@@ -529,11 +523,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
                   >(
                     chargingStation,
                     OCPP16RequestCommand.STATUS_NOTIFICATION,
-                    buildStatusNotificationRequest(
-                      chargingStation,
-                      id,
-                      connectorStatus.status as OCPP16ChargePointStatus
-                    ) as OCPP16StatusNotificationRequest,
+                    {
+                      connectorId: id,
+                      status: connectorStatus.status as OCPP16ChargePointStatus,
+                    } as unknown as OCPP16StatusNotificationRequest,
                     {
                       triggerMessage: true,
                     }
index d06a817c8379815af926159a1a49ecbd822d0e74..ccfc9d208eff4bfb7d78e111f3477a783b9617c7 100644 (file)
@@ -5,6 +5,7 @@ import type { OCPPResponseService } from '../OCPPResponseService.js'
 
 import { OCPPError } from '../../../exception/index.js'
 import {
+  type ConnectorStatusEnum,
   ErrorType,
   type JsonObject,
   type JsonType,
@@ -17,6 +18,7 @@ import {
 } from '../../../types/index.js'
 import { Constants, generateUUID, logger } from '../../../utils/index.js'
 import { OCPPRequestService } from '../OCPPRequestService.js'
+import { buildStatusNotificationRequest } from '../OCPPServiceUtils.js'
 import { OCPP16Constants } from './OCPP16Constants.js'
 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
 
@@ -183,7 +185,6 @@ export class OCPP16RequestService extends OCPPRequestService {
       case OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION:
       case OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION:
       case OCPP16RequestCommand.METER_VALUES:
-      case OCPP16RequestCommand.STATUS_NOTIFICATION:
         return commandParams as unknown as Request
       case OCPP16RequestCommand.HEARTBEAT:
         return OCPP16Constants.OCPP_REQUEST_EMPTY as unknown as Request
@@ -210,6 +211,16 @@ export class OCPP16RequestService extends OCPPRequestService {
           }),
           ...commandParams,
         } as unknown as Request
+      case OCPP16RequestCommand.STATUS_NOTIFICATION:
+        if (commandParams.errorCode != null) {
+          return commandParams as unknown as Request
+        }
+        return buildStatusNotificationRequest(
+          chargingStation,
+          commandParams.connectorId as number,
+          commandParams.status as ConnectorStatusEnum,
+          commandParams.evseId as number | undefined
+        ) as unknown as Request
       case OCPP16RequestCommand.STOP_TRANSACTION:
         chargingStation.stationInfo?.transactionDataMeterValues === true &&
           (connectorId = chargingStation.getConnectorIdByTransactionId(
index 19afdf3273fe79c743397d929ff978178db94334..eb30b2f21b5cd81d464884f3da76424711de1d9e 100644 (file)
@@ -144,7 +144,6 @@ import { OCPPAuthServiceFactory } from '../auth/index.js'
 import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js'
 import {
   buildMeterValue,
-  buildStatusNotificationRequest,
   restoreConnectorStatus,
   sendAndSetConnectorStatus,
 } from '../OCPPServiceUtils.js'
@@ -3690,7 +3689,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
             .requestHandler<
               OCPP20StatusNotificationRequest,
               OCPP20StatusNotificationResponse
-            >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, buildStatusNotificationRequest(chargingStation, connectorId, resolvedStatus, evseId) as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
+            >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId, evseId, status: resolvedStatus } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
             .catch(errorHandler)
         }
       }
@@ -3710,7 +3709,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
         .requestHandler<
           OCPP20StatusNotificationRequest,
           OCPP20StatusNotificationResponse
-        >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, buildStatusNotificationRequest(chargingStation, evse.connectorId, resolvedStatus, evse.id) as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
+        >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId: evse.connectorId, evseId: evse.id, status: resolvedStatus } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
         .catch(errorHandler)
     } else if (chargingStation.hasEvses) {
       this.triggerAllEvseStatusNotifications(chargingStation, errorHandler)
index bd240189dd91f493e84dab2bc1b3084abf08ea3d..a058fdaee2457e2027dae9669ef2fb0849cd03cd 100644 (file)
@@ -1,6 +1,12 @@
 import type { ValidateFunction } from 'ajv'
 
 import type { ChargingStation } from '../../../charging-station/index.js'
+import type {
+  ConnectorStatusEnum,
+  OCPP20TransactionEventEnumType,
+  OCPP20TriggerReasonEnumType,
+} from '../../../types/index.js'
+import type { OCPP20TransactionEventOptions } from '../../../types/ocpp/2.0/Transaction.js'
 import type { OCPPResponseService } from '../OCPPResponseService.js'
 
 import { OCPPError } from '../../../exception/index.js'
@@ -16,6 +22,7 @@ import {
 } from '../../../types/index.js'
 import { generateUUID, logger } from '../../../utils/index.js'
 import { OCPPRequestService } from '../OCPPRequestService.js'
+import { buildStatusNotificationRequest } from '../OCPPServiceUtils.js'
 import { generatePkcs10Csr } from './Asn1DerUtils.js'
 import { OCPP20Constants } from './OCPP20Constants.js'
 import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
@@ -157,8 +164,6 @@ export class OCPP20RequestService extends OCPPRequestService {
       case OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION:
       case OCPP20RequestCommand.NOTIFY_REPORT:
       case OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION:
-      case OCPP20RequestCommand.STATUS_NOTIFICATION:
-      case OCPP20RequestCommand.TRANSACTION_EVENT:
         return commandParams as unknown as Request
       case OCPP20RequestCommand.HEARTBEAT:
         return OCPP20Constants.OCPP_RESPONSE_EMPTY as unknown as Request
@@ -195,6 +200,32 @@ export class OCPP20RequestService extends OCPPRequestService {
 
         return requestPayload as unknown as Request
       }
+      case OCPP20RequestCommand.STATUS_NOTIFICATION:
+        return buildStatusNotificationRequest(
+          chargingStation,
+          commandParams.connectorId as number,
+          commandParams.status as ConnectorStatusEnum,
+          commandParams.evseId as number | undefined
+        ) as unknown as Request
+      case OCPP20RequestCommand.TRANSACTION_EVENT: {
+        // Pre-built payloads (e.g., from offline queue) already have transactionInfo;
+        // pass them through as-is to avoid double-building.
+        if (commandParams.transactionInfo != null) {
+          return commandParams as unknown as Request
+        }
+        const eventType = commandParams.eventType as OCPP20TransactionEventEnumType
+        const triggerReason = commandParams.triggerReason as OCPP20TriggerReasonEnumType
+        const connectorId = commandParams.connectorId as number
+        const transactionId = commandParams.transactionId as string
+        return OCPP20ServiceUtils.buildTransactionEvent(
+          chargingStation,
+          eventType,
+          triggerReason,
+          connectorId,
+          transactionId,
+          commandParams as unknown as OCPP20TransactionEventOptions
+        ) as unknown as Request
+      }
       default: {
         // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
         const errorMsg = `Unsupported OCPP command ${commandName as string} for payload building`
index 731a5edf09de7abac897a3e3688517a6230e4724..40e7490398faeab0d807bc774d95c161e193cece 100644 (file)
@@ -745,17 +745,6 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils {
         ? this.selectTriggerReason(eventType, triggerReasonOrContext)
         : triggerReasonOrContext
 
-      // Build the transaction event request
-      const transactionEventRequest = OCPP20ServiceUtils.buildTransactionEvent(
-        chargingStation,
-        eventType,
-        triggerReason,
-        connectorId,
-        transactionId,
-        options
-      )
-
-      // OCPP 2.0.1 offline-first: Queue event if offline, send if online
       const connectorStatus = chargingStation.getConnectorStatus(connectorId)
       if (connectorStatus == null) {
         const errorMsg = `Cannot find connector status for connector ${connectorId.toString()}`
@@ -765,7 +754,17 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils {
         throw new OCPPError(ErrorType.PROPERTY_CONSTRAINT_VIOLATION, errorMsg)
       }
 
+      // Offline path: build payload directly for queueing (queue stores pre-built requests
+      // that are sent as-is on reconnect, bypassing buildRequestPayload).
       if (!chargingStation.isWebSocketConnectionOpened()) {
+        const transactionEventRequest = OCPP20ServiceUtils.buildTransactionEvent(
+          chargingStation,
+          eventType,
+          triggerReason,
+          connectorId,
+          transactionId,
+          options
+        )
         logger.info(
           `${chargingStation.logPrefix()} ${moduleName}.sendTransactionEvent: Station offline, queueing TransactionEvent with seqNo=${transactionEventRequest.seqNo.toString()}`
         )
@@ -778,6 +777,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils {
         return { idTokenInfo: undefined }
       }
 
+      // Online path: pass minimal params to requestHandler → buildRequestPayload builds
       logger.debug(
         `${chargingStation.logPrefix()} ${moduleName}.sendTransactionEvent: Sending TransactionEvent for trigger ${triggerReason}`
       )
@@ -785,7 +785,13 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils {
       const response = await chargingStation.ocppRequestService.requestHandler<
         OCPP20TransactionEventRequest,
         OCPP20TransactionEventResponse
-      >(chargingStation, OCPP20RequestCommand.TRANSACTION_EVENT, transactionEventRequest)
+      >(chargingStation, OCPP20RequestCommand.TRANSACTION_EVENT, {
+        connectorId,
+        eventType,
+        transactionId,
+        triggerReason,
+        ...options,
+      } as unknown as OCPP20TransactionEventRequest)
 
       return response
     } catch (error) {
index 829df23f7358166beac128ac7071a4a736977812..d5b2abcc7430e6839ef6fbc131ecfc93fac501b1 100644 (file)
@@ -308,11 +308,11 @@ export const sendAndSetConnectorStatus = async (
     await chargingStation.ocppRequestService.requestHandler<
       StatusNotificationRequest,
       StatusNotificationResponse
-    >(
-      chargingStation,
-      RequestCommand.STATUS_NOTIFICATION,
-      buildStatusNotificationRequest(chargingStation, connectorId, status, evseId)
-    )
+    >(chargingStation, RequestCommand.STATUS_NOTIFICATION, {
+      connectorId,
+      evseId,
+      status,
+    } as unknown as StatusNotificationRequest)
   }
   connectorStatus.status = status
   chargingStation.emitChargingStationEvent(ChargingStationEvents.connectorStatusChanged, {
index b3212a8bb69ed409ad2d84b9551c81626440bf64..1da0a8f70e977f58cc6385386b5488eff483fec9 100644 (file)
@@ -369,26 +369,16 @@ await describe('F03 - Remote Stop Transaction', async () => {
       const args = requestHandlerMock.mock.calls[0].arguments as [
         unknown,
         string,
-        OCPP20TransactionEventRequest
+        Record<string, unknown>
       ]
-      const transactionEvent = args[2]
-
-      assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Ended)
-      assert.notStrictEqual(transactionEvent.timestamp, undefined)
-      assert.ok(transactionEvent.timestamp instanceof Date)
-      assert.strictEqual(transactionEvent.triggerReason, OCPP20TriggerReasonEnumType.RemoteStop)
-      assert.notStrictEqual(transactionEvent.seqNo, undefined)
-      assert.strictEqual(typeof transactionEvent.seqNo, 'number')
-
-      assert.notStrictEqual(transactionEvent.transactionInfo, undefined)
-      assert.strictEqual(transactionEvent.transactionInfo.transactionId, transactionId)
-      assert.strictEqual(
-        transactionEvent.transactionInfo.stoppedReason,
-        OCPP20ReasonEnumType.Remote
-      )
-
-      assert.notStrictEqual(transactionEvent.evse, undefined)
-      assert.strictEqual(transactionEvent.evse?.id, 2)
+      const minimalParams = args[2]
+
+      assert.strictEqual(minimalParams.eventType, OCPP20TransactionEventEnumType.Ended)
+      assert.strictEqual(minimalParams.triggerReason, OCPP20TriggerReasonEnumType.RemoteStop)
+      assert.strictEqual(minimalParams.transactionId, transactionId)
+      assert.strictEqual(minimalParams.stoppedReason, OCPP20ReasonEnumType.Remote)
+      assert.notStrictEqual(minimalParams.connectorId, undefined)
+      assert.strictEqual(typeof minimalParams.connectorId, 'number')
     })
 
     // FR: F03.FR.09
index c7033fa9e35db79a3a1eb902f98ff4452dd5f731..90370fc0d5b151cb64f64364f57fe1cf652d39f5 100644 (file)
@@ -514,8 +514,7 @@ await describe('F06 - TriggerMessage', async () => {
         assert.notStrictEqual(payload, undefined)
         assert.ok('evseId' in payload, 'Expected payload to include evseId')
         assert.ok('connectorId' in payload, 'Expected payload to include connectorId')
-        assert.ok('connectorStatus' in payload, 'Expected payload to include connectorStatus')
-        assert.ok('timestamp' in payload, 'Expected payload to include timestamp')
+        assert.ok('status' in payload, 'Expected payload to include status')
         assert.ok((payload.evseId as number) > 0, 'Expected evseId > 0 (EVSE 0 excluded)')
         assert.strictEqual(options.skipBufferingOnError, true)
         assert.strictEqual(options.triggerMessage, true)
@@ -549,8 +548,7 @@ await describe('F06 - TriggerMessage', async () => {
       assert.strictEqual(command, OCPP20RequestCommand.STATUS_NOTIFICATION)
       assert.strictEqual(payload.evseId, 1)
       assert.strictEqual(payload.connectorId, 1)
-      assert.ok('connectorStatus' in payload)
-      assert.ok(payload.timestamp instanceof Date)
+      assert.ok('status' in payload)
       assert.strictEqual(options.skipBufferingOnError, true)
       assert.strictEqual(options.triggerMessage, true)
     })
index 9f94a1b79305ffcd85d5460b7af00ca2b7e8759c..a19718d1087b1941a02529a9a4b44fc9eb0c0cdd 100644 (file)
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, it } from 'node:test'
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
 
 import {
+  ConnectorStatusEnum,
   OCPP20ConnectorStatusEnumType,
   OCPP20RequestCommand,
   type OCPP20StatusNotificationRequest,
@@ -49,125 +50,100 @@ await describe('G01 - Status Notification', async () => {
 
   // FR: G01.FR.01
   await it('should build StatusNotification request payload correctly with Available status', () => {
-    const testTimestamp = new Date('2024-01-15T10:30:00.000Z')
-
-    const requestParams: OCPP20StatusNotificationRequest = {
-      connectorId: 1,
-      connectorStatus: OCPP20ConnectorStatusEnumType.Available,
-      evseId: 1,
-      timestamp: testTimestamp,
-    }
-
     const payload = testableRequestService.buildRequestPayload(
       station,
       OCPP20RequestCommand.STATUS_NOTIFICATION,
-      requestParams
+      {
+        connectorId: 1,
+        evseId: 1,
+        status: ConnectorStatusEnum.Available,
+      }
     ) as OCPP20StatusNotificationRequest
 
     assert.notStrictEqual(payload, undefined)
     assert.strictEqual(payload.connectorId, 1)
     assert.strictEqual(payload.connectorStatus, OCPP20ConnectorStatusEnumType.Available)
     assert.strictEqual(payload.evseId, 1)
-    assert.strictEqual(payload.timestamp, testTimestamp)
+    assert.ok(payload.timestamp instanceof Date)
   })
 
   // FR: G01.FR.02
   await it('should build StatusNotification request payload correctly with Occupied status', () => {
-    const testTimestamp = new Date('2024-01-15T11:45:30.000Z')
-
-    const requestParams: OCPP20StatusNotificationRequest = {
-      connectorId: 2,
-      connectorStatus: OCPP20ConnectorStatusEnumType.Occupied,
-      evseId: 2,
-      timestamp: testTimestamp,
-    }
-
     const payload = testableRequestService.buildRequestPayload(
       station,
       OCPP20RequestCommand.STATUS_NOTIFICATION,
-      requestParams
+      {
+        connectorId: 2,
+        evseId: 2,
+        status: ConnectorStatusEnum.Occupied,
+      }
     ) as OCPP20StatusNotificationRequest
 
     assert.notStrictEqual(payload, undefined)
     assert.strictEqual(payload.connectorId, 2)
     assert.strictEqual(payload.connectorStatus, OCPP20ConnectorStatusEnumType.Occupied)
     assert.strictEqual(payload.evseId, 2)
-    assert.strictEqual(payload.timestamp, testTimestamp)
+    assert.ok(payload.timestamp instanceof Date)
   })
 
   // FR: G01.FR.03
   await it('should build StatusNotification request payload correctly with Faulted status', () => {
-    const testTimestamp = new Date('2024-01-15T12:15:45.500Z')
-
-    const requestParams: OCPP20StatusNotificationRequest = {
-      connectorId: 1,
-      connectorStatus: OCPP20ConnectorStatusEnumType.Faulted,
-      evseId: 1,
-      timestamp: testTimestamp,
-    }
-
     const payload = testableRequestService.buildRequestPayload(
       station,
       OCPP20RequestCommand.STATUS_NOTIFICATION,
-      requestParams
+      {
+        connectorId: 1,
+        evseId: 1,
+        status: ConnectorStatusEnum.Faulted,
+      }
     ) as OCPP20StatusNotificationRequest
 
     assert.notStrictEqual(payload, undefined)
     assert.strictEqual(payload.connectorId, 1)
     assert.strictEqual(payload.connectorStatus, OCPP20ConnectorStatusEnumType.Faulted)
     assert.strictEqual(payload.evseId, 1)
-    assert.strictEqual(payload.timestamp, testTimestamp)
+    assert.ok(payload.timestamp instanceof Date)
   })
 
   // FR: G01.FR.04
   await it('should handle all OCPP20ConnectorStatusEnumType values correctly', () => {
-    const testTimestamp = new Date('2024-01-15T13:00:00.000Z')
-
-    const statusValues = [
-      OCPP20ConnectorStatusEnumType.Available,
-      OCPP20ConnectorStatusEnumType.Faulted,
-      OCPP20ConnectorStatusEnumType.Occupied,
-      OCPP20ConnectorStatusEnumType.Reserved,
-      OCPP20ConnectorStatusEnumType.Unavailable,
+    const statusValues: [ConnectorStatusEnum, OCPP20ConnectorStatusEnumType][] = [
+      [ConnectorStatusEnum.Available, OCPP20ConnectorStatusEnumType.Available],
+      [ConnectorStatusEnum.Faulted, OCPP20ConnectorStatusEnumType.Faulted],
+      [ConnectorStatusEnum.Occupied, OCPP20ConnectorStatusEnumType.Occupied],
+      [ConnectorStatusEnum.Reserved, OCPP20ConnectorStatusEnumType.Reserved],
+      [ConnectorStatusEnum.Unavailable, OCPP20ConnectorStatusEnumType.Unavailable],
     ]
 
-    statusValues.forEach((status, index) => {
-      const requestParams: OCPP20StatusNotificationRequest = {
-        connectorId: index + 1,
-        connectorStatus: status,
-        evseId: index + 1,
-        timestamp: testTimestamp,
-      }
-
+    statusValues.forEach(([inputStatus, expectedConnectorStatus], index) => {
       const payload = testableRequestService.buildRequestPayload(
         station,
         OCPP20RequestCommand.STATUS_NOTIFICATION,
-        requestParams
+        {
+          connectorId: index + 1,
+          evseId: index + 1,
+          status: inputStatus,
+        }
       ) as OCPP20StatusNotificationRequest
 
       assert.notStrictEqual(payload, undefined)
-      assert.strictEqual(payload.connectorStatus, status)
+      assert.strictEqual(payload.connectorStatus, expectedConnectorStatus)
       assert.strictEqual(payload.connectorId, index + 1)
       assert.strictEqual(payload.evseId, index + 1)
-      assert.strictEqual(payload.timestamp, testTimestamp)
+      assert.ok(payload.timestamp instanceof Date)
     })
   })
 
   // FR: G01.FR.05
   await it('should validate payload structure matches OCPP20StatusNotificationRequest interface', () => {
-    const testTimestamp = new Date('2024-01-15T14:30:15.123Z')
-
-    const requestParams: OCPP20StatusNotificationRequest = {
-      connectorId: 3,
-      connectorStatus: OCPP20ConnectorStatusEnumType.Reserved,
-      evseId: 2,
-      timestamp: testTimestamp,
-    }
-
     const payload = testableRequestService.buildRequestPayload(
       station,
       OCPP20RequestCommand.STATUS_NOTIFICATION,
-      requestParams
+      {
+        connectorId: 3,
+        evseId: 2,
+        status: ConnectorStatusEnum.Reserved,
+      }
     ) as OCPP20StatusNotificationRequest
 
     // Validate that the payload has the exact structure of OCPP20StatusNotificationRequest
@@ -188,80 +164,71 @@ await describe('G01 - Status Notification', async () => {
     assert.strictEqual(payload.connectorId, 3)
     assert.strictEqual(payload.connectorStatus, OCPP20ConnectorStatusEnumType.Reserved)
     assert.strictEqual(payload.evseId, 2)
-    assert.strictEqual(payload.timestamp, testTimestamp)
   })
 
   // FR: G01.FR.06
   await it('should handle edge case connector and EVSE IDs correctly', () => {
-    const testTimestamp = new Date('2024-01-15T15:45:00.000Z')
-
     // Test with connector ID 0 (valid in OCPP 2.0 for the charging station itself)
-    const requestParamsConnector0: OCPP20StatusNotificationRequest = {
-      connectorId: 0,
-      connectorStatus: OCPP20ConnectorStatusEnumType.Available,
-      evseId: 1,
-      timestamp: testTimestamp,
-    }
-
     const payloadConnector0 = testableRequestService.buildRequestPayload(
       station,
       OCPP20RequestCommand.STATUS_NOTIFICATION,
-      requestParamsConnector0
+      {
+        connectorId: 0,
+        evseId: 1,
+        status: ConnectorStatusEnum.Available,
+      }
     ) as OCPP20StatusNotificationRequest
 
     assert.notStrictEqual(payloadConnector0, undefined)
     assert.strictEqual(payloadConnector0.connectorId, 0)
     assert.strictEqual(payloadConnector0.connectorStatus, OCPP20ConnectorStatusEnumType.Available)
     assert.strictEqual(payloadConnector0.evseId, 1)
-    assert.strictEqual(payloadConnector0.timestamp, testTimestamp)
+    assert.ok(payloadConnector0.timestamp instanceof Date)
 
     // Test with EVSE ID 0 (valid in OCPP 2.0 for the charging station itself)
-    const requestParamsEvse0: OCPP20StatusNotificationRequest = {
-      connectorId: 1,
-      connectorStatus: OCPP20ConnectorStatusEnumType.Unavailable,
-      evseId: 0,
-      timestamp: testTimestamp,
-    }
-
     const payloadEvse0 = testableRequestService.buildRequestPayload(
       station,
       OCPP20RequestCommand.STATUS_NOTIFICATION,
-      requestParamsEvse0
+      {
+        connectorId: 1,
+        evseId: 0,
+        status: ConnectorStatusEnum.Unavailable,
+      }
     ) as OCPP20StatusNotificationRequest
 
     assert.notStrictEqual(payloadEvse0, undefined)
     assert.strictEqual(payloadEvse0.connectorId, 1)
     assert.strictEqual(payloadEvse0.connectorStatus, OCPP20ConnectorStatusEnumType.Unavailable)
     assert.strictEqual(payloadEvse0.evseId, 0)
-    assert.strictEqual(payloadEvse0.timestamp, testTimestamp)
+    assert.ok(payloadEvse0.timestamp instanceof Date)
   })
 
   // FR: G01.FR.07
   await it('should handle different timestamp formats correctly', () => {
-    const testCases = [
-      new Date('2024-01-01T00:00:00.000Z'), // Start of year
-      new Date('2024-12-31T23:59:59.999Z'), // End of year
-      new Date(), // Current time
-      new Date('2024-06-15T12:30:45.678Z'), // Mid-year with milliseconds
+    // buildRequestPayload now generates its own timestamp via buildStatusNotificationRequest,
+    // so we verify the output always has a valid Date timestamp
+    const statusValues = [
+      ConnectorStatusEnum.Available,
+      ConnectorStatusEnum.Occupied,
+      ConnectorStatusEnum.Faulted,
+      ConnectorStatusEnum.Reserved,
     ]
 
-    testCases.forEach((timestamp, _index) => {
-      const requestParams: OCPP20StatusNotificationRequest = {
-        connectorId: 1,
-        connectorStatus: OCPP20ConnectorStatusEnumType.Available,
-        evseId: 1,
-        timestamp,
-      }
-
+    const beforeBuild = new Date()
+    statusValues.forEach(status => {
       const payload = testableRequestService.buildRequestPayload(
         station,
         OCPP20RequestCommand.STATUS_NOTIFICATION,
-        requestParams
+        {
+          connectorId: 1,
+          evseId: 1,
+          status,
+        }
       ) as OCPP20StatusNotificationRequest
 
       assert.notStrictEqual(payload, undefined)
-      assert.strictEqual(payload.timestamp, timestamp)
       assert.ok(payload.timestamp instanceof Date)
+      assert.ok(payload.timestamp.getTime() >= beforeBuild.getTime())
     })
   })
 })
index 38ddf2a53596c9d1a8560e9891109abad6f28ce6..3e5f42eaf55187012eabc8266bc288d46a81b74f 100644 (file)
@@ -2028,7 +2028,10 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
         )
 
         assert.strictEqual(sentRequests.length, 1)
-        assert.strictEqual(sentRequests[0].payload.seqNo, 0)
+        assert.strictEqual(
+          sentRequests[0].payload.eventType,
+          OCPP20TransactionEventEnumType.Started
+        )
 
         setOnline(false)
 
@@ -2050,8 +2053,9 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
 
         const connector = mockStation.getConnectorStatus(connectorId)
         assert.strictEqual(connector?.transactionEventQueue?.length, 2)
-        assert.strictEqual(connector.transactionEventQueue[0].seqNo, 1)
-        assert.strictEqual(connector.transactionEventQueue[1].seqNo, 2)
+        // Online path with mock doesn't call buildTransactionEvent, so seqNo starts from 0
+        assert.strictEqual(connector.transactionEventQueue[0].seqNo, 0)
+        assert.strictEqual(connector.transactionEventQueue[1].seqNo, 1)
       })
 
       await it('should include timestamp in queued events', async () => {
@@ -2228,7 +2232,11 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
           connectorId,
           transactionId
         )
-        assert.strictEqual(sentRequests[0].payload.seqNo, 0)
+        // Online path sends minimal params (no seqNo in payload)
+        assert.strictEqual(
+          sentRequests[0].payload.eventType,
+          OCPP20TransactionEventEnumType.Started
+        )
 
         setOnline(false)
 
@@ -2252,8 +2260,10 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
 
         await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockStation, connectorId)
 
-        assert.strictEqual(sentRequests[1].payload.seqNo, 1)
-        assert.strictEqual(sentRequests[2].payload.seqNo, 2)
+        // Queued events are pre-built payloads with seqNo (starts from 0 since
+        // the online path with mock doesn't call buildTransactionEvent)
+        assert.strictEqual(sentRequests[1].payload.seqNo, 0)
+        assert.strictEqual(sentRequests[2].payload.seqNo, 1)
 
         await OCPP20ServiceUtils.sendTransactionEvent(
           mockStation,
@@ -2263,11 +2273,14 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
           transactionId
         )
 
-        assert.strictEqual(sentRequests[3].payload.seqNo, 3)
+        // Online path sends minimal params (no seqNo in payload)
+        assert.strictEqual(sentRequests[3].payload.eventType, OCPP20TransactionEventEnumType.Ended)
 
-        for (let i = 0; i < sentRequests.length; i++) {
-          assert.strictEqual(sentRequests[i].payload.seqNo, i)
-        }
+        // Verify seqNo continuity for queued events (indices 1 and 2)
+        assert.strictEqual(sentRequests[1].payload.seqNo, 0)
+        assert.strictEqual(sentRequests[2].payload.seqNo, 1)
+        // Verify total request count
+        assert.strictEqual(sentRequests.length, 4)
       })
     })
 
@@ -2614,12 +2627,8 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
           transactionId
         )
 
-        // Verify EVSE info is present
-        assert.notStrictEqual(sentRequests[0].payload.evse, undefined)
-        assert.strictEqual(
-          (sentRequests[0].payload.evse as Record<string, unknown>).id,
-          connectorId
-        )
+        // Online path sends minimal params with connectorId (EVSE resolved by buildRequestPayload)
+        assert.strictEqual(sentRequests[0].payload.connectorId, connectorId)
       })
 
       await it('should include transactionInfo with correct transactionId', async () => {
@@ -2636,12 +2645,8 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
           transactionId
         )
 
-        // Verify transactionInfo contains the transaction ID
-        assert.notStrictEqual(sentRequests[0].payload.transactionInfo, undefined)
-        assert.strictEqual(
-          (sentRequests[0].payload.transactionInfo as Record<string, unknown>).transactionId,
-          transactionId
-        )
+        // Online path sends minimal params with transactionId at top level
+        assert.strictEqual(sentRequests[0].payload.transactionId, transactionId)
       })
     })