]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(ui): make Authorize version-aware for OCPP 2.0.1 stations
authorJérôme Benoit <jerome.benoit@sap.com>
Thu, 30 Apr 2026 13:30:13 +0000 (15:30 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Thu, 30 Apr 2026 13:30:13 +0000 (15:30 +0200)
Extract shared OCPP version-aware payload builders into
ui-common/src/utils/payloadBuilders.ts (buildAuthorizePayload,
buildStartTransactionPayload, buildStopTransactionPayload, isOCPP20x,
buildIdToken) and consume them from both Web UI and CLI.

The Web UI Authorize was always sending flat { idTag } regardless of
OCPP version, causing "Request PDU is invalid" on OCPP 2.0.1 stations
which require { idToken: { idToken, type } }.

Closes #1817

15 files changed:
ui/cli/src/commands/ocpp.ts
ui/cli/src/commands/transaction.ts
ui/common/src/index.ts
ui/common/src/utils/payloadBuilders.ts [new file with mode: 0644]
ui/common/tests/payloadBuilders.test.ts [new file with mode: 0644]
ui/web/src/core/UIClient.ts
ui/web/src/shared/composables/useStartTxForm.ts
ui/web/src/skins/modern/ModernLayout.vue
ui/web/src/skins/modern/components/StationCard.vue
ui/web/src/skins/modern/components/dialogs/AuthorizeDialog.vue
ui/web/tests/unit/UIClient.test.ts
ui/web/tests/unit/shared/composables/useStartTxForm.test.ts
ui/web/tests/unit/skins/classic/Actions.test.ts
ui/web/tests/unit/skins/modern/Dialogs.test.ts
ui/web/tests/unit/skins/modern/StationCard.test.ts

index 33209a9d388bf81dc6f276387e700c31d16d5598..31743b34ef9204e4e5501569123fc1b693375699 100644 (file)
@@ -1,5 +1,5 @@
 import { Command, Option } from 'commander'
-import { OCPP20IdTokenEnumType, OCPPVersion, ProcedureName, type RequestPayload } from 'ui-common'
+import { buildAuthorizePayload, OCPPVersion, ProcedureName, type RequestPayload } from 'ui-common'
 
 import {
   handleActionErrors,
@@ -30,22 +30,9 @@ export const createOcppCommands = (program: Command): Command => {
             program,
             hashIds
           )
-          switch (ocppVersion) {
-            case OCPPVersion.VERSION_16:
-              payload = {
-                idTag: options.idTag,
-                ...buildHashIdsPayload(resolvedHashIds),
-              }
-              break
-            case OCPPVersion.VERSION_20:
-            case OCPPVersion.VERSION_201:
-              payload = {
-                idToken: { idToken: options.idTag, type: OCPP20IdTokenEnumType.ISO14443 },
-                ...buildHashIdsPayload(resolvedHashIds),
-              }
-              break
-            default:
-              throw new Error(UNSUPPORTED_OCPP_VERSION_ERROR)
+          payload = {
+            ...buildAuthorizePayload(options.idTag, ocppVersion),
+            ...buildHashIdsPayload(resolvedHashIds),
           }
           await runAction(program, ProcedureName.AUTHORIZE, payload, undefined, config)
         } else {
index 8d79e83ca7bb3baae47837f4901538d2edfcd014..43624e9f0215d039b6125e539684993925c9be94 100644 (file)
@@ -1,7 +1,8 @@
 import { Command, Option } from 'commander'
 import {
-  OCPP20IdTokenEnumType,
-  OCPP20TransactionEventEnumType,
+  buildStartTransactionPayload,
+  buildStopTransactionPayload,
+  isOCPP20x,
   OCPPVersion,
   ProcedureName,
   type RequestPayload,
@@ -17,9 +18,6 @@ import { buildHashIdsPayload, PAYLOAD_DESC, PAYLOAD_OPTION } from './payload.js'
 
 export const createTransactionCommands = (program: Command): Command => {
   const cmd = new Command('transaction').description('Transaction management')
-  const UNSUPPORTED_OCPP_VERSION_ERROR =
-    'Unsupported OCPP version for this command. ' +
-    'Use ocpp transaction-event -p to pass the payload directly.'
 
   cmd
     .command('start [hashIds...]')
@@ -60,28 +58,18 @@ export const createTransactionCommands = (program: Command): Command => {
               program,
               hashIds
             )
-            switch (ocppVersion) {
-              case OCPPVersion.VERSION_16:
-                procedureName = ProcedureName.START_TRANSACTION
-                payload = {
-                  connectorId: options.connectorId,
-                  idTag: options.idTag,
-                  ...buildHashIdsPayload(resolvedHashIds),
-                }
-                break
-              case OCPPVersion.VERSION_20:
-              case OCPPVersion.VERSION_201:
-                procedureName = ProcedureName.TRANSACTION_EVENT
-                payload = {
-                  connectorId: options.connectorId,
-                  eventType: OCPP20TransactionEventEnumType.STARTED,
-                  ...(options.evseId != null && { evseId: options.evseId }),
-                  idToken: { idToken: options.idTag, type: OCPP20IdTokenEnumType.ISO14443 },
-                  ...buildHashIdsPayload(resolvedHashIds),
-                }
-                break
-              default:
-                throw new Error(UNSUPPORTED_OCPP_VERSION_ERROR)
+            const { payload: built, procedureName: proc } = buildStartTransactionPayload(
+              options.connectorId,
+              ocppVersion,
+              { evseId: options.evseId, idTag: options.idTag }
+            )
+            procedureName =
+              proc === 'transactionEvent'
+                ? ProcedureName.TRANSACTION_EVENT
+                : ProcedureName.START_TRANSACTION
+            payload = {
+              ...built,
+              ...buildHashIdsPayload(resolvedHashIds),
             }
             await runAction(program, procedureName, payload, undefined, config)
           } else {
@@ -127,32 +115,26 @@ export const createTransactionCommands = (program: Command): Command => {
               program,
               hashIds
             )
-            switch (ocppVersion) {
-              case OCPPVersion.VERSION_16:
-                procedureName = ProcedureName.STOP_TRANSACTION
-                payload = {
-                  transactionId: parseInteger(
-                    options.transactionId,
-                    '--transaction-id (OCPP 1.6 requires integer)'
-                  ),
-                  ...buildHashIdsPayload(resolvedHashIds),
-                }
-                break
-              case OCPPVersion.VERSION_20:
-              case OCPPVersion.VERSION_201:
-                if (options.connectorId == null) {
-                  throw new Error('--connector-id is required for OCPP 2.0.x stations')
-                }
-                procedureName = ProcedureName.TRANSACTION_EVENT
-                payload = {
-                  connectorId: options.connectorId,
-                  eventType: OCPP20TransactionEventEnumType.ENDED,
-                  transactionId: options.transactionId,
-                  ...buildHashIdsPayload(resolvedHashIds),
-                }
-                break
-              default:
-                throw new Error(UNSUPPORTED_OCPP_VERSION_ERROR)
+            if (isOCPP20x(ocppVersion) && options.connectorId == null) {
+              throw new Error('--connector-id is required for OCPP 2.0.x stations')
+            }
+            const { payload: built, procedureName: proc } = buildStopTransactionPayload(
+              ocppVersion === OCPPVersion.VERSION_16
+                ? parseInteger(
+                  options.transactionId,
+                  '--transaction-id (OCPP 1.6 requires integer)'
+                )
+                : options.transactionId,
+              ocppVersion,
+              options.connectorId
+            )
+            procedureName =
+              proc === 'transactionEvent'
+                ? ProcedureName.TRANSACTION_EVENT
+                : ProcedureName.STOP_TRANSACTION
+            payload = {
+              ...built,
+              ...buildHashIdsPayload(resolvedHashIds),
             }
             await runAction(program, procedureName, payload, undefined, config)
           } else {
index d79e9be3cc182c9a24baf57b5692c366940bf653..944db50afb4d9959953e7fb81ca1eed228a51c6b 100644 (file)
@@ -11,5 +11,6 @@ export * from './types/JsonType.js'
 export * from './types/UIProtocol.js'
 export * from './types/UUID.js'
 export * from './utils/converters.js'
+export * from './utils/payloadBuilders.js'
 export * from './utils/UUID.js'
 export * from './utils/websocket.js'
diff --git a/ui/common/src/utils/payloadBuilders.ts b/ui/common/src/utils/payloadBuilders.ts
new file mode 100644 (file)
index 0000000..fbd43f9
--- /dev/null
@@ -0,0 +1,123 @@
+import type { RequestPayload } from '../types/UIProtocol.js'
+
+import {
+  OCPP20IdTokenEnumType,
+  type OCPP20IdTokenType,
+  OCPP20TransactionEventEnumType,
+  OCPPVersion,
+} from '../types/ChargingStationType.js'
+
+/**
+ * Builds an Authorize request payload adapted to the station's OCPP version.
+ * @param idTag - RFID tag identifier
+ * @param ocppVersion - Target OCPP version (1.6 format if undefined)
+ * @returns Payload with flat `idTag` for 1.6 or nested `idToken` for 2.0.x
+ */
+export function buildAuthorizePayload (
+  idTag: string,
+  ocppVersion: OCPPVersion | undefined
+): RequestPayload {
+  if (isOCPP20x(ocppVersion)) {
+    return {
+      idToken: { idToken: idTag, type: OCPP20IdTokenEnumType.ISO14443 },
+    }
+  }
+  assertOCPP16OrUndefined(ocppVersion)
+  return { idTag }
+}
+
+/**
+ * Builds an OCPP 2.0.x IdTokenType object.
+ * @param idTag - RFID tag identifier
+ * @param type - Token type enumeration value
+ * @returns IdTokenType object
+ */
+export function buildIdToken (
+  idTag: string,
+  type: OCPP20IdTokenEnumType = OCPP20IdTokenEnumType.ISO14443
+): OCPP20IdTokenType {
+  return { idToken: idTag, type }
+}
+
+/**
+ * Builds a StartTransaction/TransactionEvent payload adapted to the station's OCPP version.
+ * @param connectorId - Connector identifier
+ * @param ocppVersion - Target OCPP version
+ * @param options - Optional fields
+ * @param options.evseId - EVSE identifier (OCPP 2.0.x only)
+ * @param options.idTag - RFID tag identifier
+ * @returns Payload and procedure name to use
+ */
+export function buildStartTransactionPayload (
+  connectorId: number,
+  ocppVersion: OCPPVersion | undefined,
+  options?: { evseId?: number; idTag?: string }
+): { payload: RequestPayload; procedureName: 'startTransaction' | 'transactionEvent' } {
+  if (isOCPP20x(ocppVersion)) {
+    return {
+      payload: {
+        connectorId,
+        eventType: OCPP20TransactionEventEnumType.STARTED,
+        ...(options?.evseId != null && { evseId: options.evseId }),
+        ...(options?.idTag != null && {
+          idToken: { idToken: options.idTag, type: OCPP20IdTokenEnumType.ISO14443 },
+        }),
+      },
+      procedureName: 'transactionEvent',
+    }
+  }
+  assertOCPP16OrUndefined(ocppVersion)
+  return {
+    payload: { connectorId, ...(options?.idTag != null && { idTag: options.idTag }) },
+    procedureName: 'startTransaction',
+  }
+}
+
+/**
+ * Builds a StopTransaction/TransactionEvent payload adapted to the station's OCPP version.
+ * @param transactionId - Transaction identifier (integer for 1.6, string for 2.0.x)
+ * @param ocppVersion - Target OCPP version
+ * @param connectorId - Connector identifier (OCPP 2.0.x only)
+ * @returns Payload and procedure name to use
+ */
+export function buildStopTransactionPayload (
+  transactionId: number | string,
+  ocppVersion: OCPPVersion | undefined,
+  connectorId?: number
+): { payload: RequestPayload; procedureName: 'stopTransaction' | 'transactionEvent' } {
+  if (isOCPP20x(ocppVersion)) {
+    return {
+      payload: {
+        ...(connectorId != null && { connectorId }),
+        eventType: OCPP20TransactionEventEnumType.ENDED,
+        transactionId: transactionId.toString(),
+      },
+      procedureName: 'transactionEvent',
+    }
+  }
+  assertOCPP16OrUndefined(ocppVersion)
+  return {
+    payload: { transactionId },
+    procedureName: 'stopTransaction',
+  }
+}
+
+/**
+ * Checks whether the given OCPP version is 2.0 or 2.0.1.
+ * @param version - OCPP version to check
+ * @returns `true` if version is 2.0 or 2.0.1
+ */
+export function isOCPP20x (version: OCPPVersion | undefined): boolean {
+  return version === OCPPVersion.VERSION_20 || version === OCPPVersion.VERSION_201
+}
+
+/**
+ * Asserts that the OCPP version is 1.6 or undefined (legacy default).
+ * @param version - OCPP version to validate
+ * @throws {Error} if version is not 1.6, undefined, or a known 2.0.x variant
+ */
+function assertOCPP16OrUndefined (version: OCPPVersion | undefined): void {
+  if (version != null && version !== OCPPVersion.VERSION_16) {
+    throw new Error(`Unsupported OCPP version for payload building: ${version}`)
+  }
+}
diff --git a/ui/common/tests/payloadBuilders.test.ts b/ui/common/tests/payloadBuilders.test.ts
new file mode 100644 (file)
index 0000000..bfed3a0
--- /dev/null
@@ -0,0 +1,176 @@
+import assert from 'node:assert'
+import { describe, it } from 'node:test'
+
+import {
+  OCPP20IdTokenEnumType,
+  OCPP20TransactionEventEnumType,
+  OCPPVersion,
+} from '../src/types/ChargingStationType.js'
+import {
+  buildAuthorizePayload,
+  buildIdToken,
+  buildStartTransactionPayload,
+  buildStopTransactionPayload,
+  isOCPP20x,
+} from '../src/utils/payloadBuilders.js'
+
+await describe('payloadBuilders', async () => {
+  await describe('isOCPP20x', async () => {
+    await it('should return true for VERSION_20', () => {
+      assert.strictEqual(isOCPP20x(OCPPVersion.VERSION_20), true)
+    })
+
+    await it('should return true for VERSION_201', () => {
+      assert.strictEqual(isOCPP20x(OCPPVersion.VERSION_201), true)
+    })
+
+    await it('should return false for VERSION_16', () => {
+      assert.strictEqual(isOCPP20x(OCPPVersion.VERSION_16), false)
+    })
+
+    await it('should return false for undefined', () => {
+      assert.strictEqual(isOCPP20x(undefined), false)
+    })
+  })
+
+  await describe('buildAuthorizePayload', async () => {
+    await it('should build flat idTag payload for OCPP 1.6', () => {
+      const result = buildAuthorizePayload('RFID123', OCPPVersion.VERSION_16)
+      assert.deepStrictEqual(result, { idTag: 'RFID123' })
+    })
+
+    await it('should build idToken payload for OCPP 2.0', () => {
+      const result = buildAuthorizePayload('RFID123', OCPPVersion.VERSION_20)
+      assert.deepStrictEqual(result, {
+        idToken: { idToken: 'RFID123', type: OCPP20IdTokenEnumType.ISO14443 },
+      })
+    })
+
+    await it('should build idToken payload for OCPP 2.0.1', () => {
+      const result = buildAuthorizePayload('RFID123', OCPPVersion.VERSION_201)
+      assert.deepStrictEqual(result, {
+        idToken: { idToken: 'RFID123', type: OCPP20IdTokenEnumType.ISO14443 },
+      })
+    })
+
+    await it('should default to OCPP 1.6 format when version is undefined', () => {
+      const result = buildAuthorizePayload('RFID123', undefined)
+      assert.deepStrictEqual(result, { idTag: 'RFID123' })
+    })
+
+    await it('should throw on unsupported OCPP version', () => {
+      assert.throws(
+        () => buildAuthorizePayload('RFID123', '3.0' as unknown as OCPPVersion),
+        (error: Error) => error.message.includes('Unsupported OCPP version')
+      )
+    })
+  })
+
+  await describe('buildStartTransactionPayload', async () => {
+    await it('should build OCPP 1.6 payload with startTransaction procedure', () => {
+      const result = buildStartTransactionPayload(1, OCPPVersion.VERSION_16, { idTag: 'TAG1' })
+      assert.deepStrictEqual(result, {
+        payload: { connectorId: 1, idTag: 'TAG1' },
+        procedureName: 'startTransaction',
+      })
+    })
+
+    await it('should build OCPP 2.0.1 payload with transactionEvent procedure', () => {
+      const result = buildStartTransactionPayload(1, OCPPVersion.VERSION_201, { idTag: 'TAG1' })
+      assert.deepStrictEqual(result, {
+        payload: {
+          connectorId: 1,
+          eventType: OCPP20TransactionEventEnumType.STARTED,
+          idToken: { idToken: 'TAG1', type: OCPP20IdTokenEnumType.ISO14443 },
+        },
+        procedureName: 'transactionEvent',
+      })
+    })
+
+    await it('should include evseId for OCPP 2.0.x when provided', () => {
+      const result = buildStartTransactionPayload(1, OCPPVersion.VERSION_201, {
+        evseId: 2,
+        idTag: 'TAG1',
+      })
+      assert.deepStrictEqual(result, {
+        payload: {
+          connectorId: 1,
+          eventType: OCPP20TransactionEventEnumType.STARTED,
+          evseId: 2,
+          idToken: { idToken: 'TAG1', type: OCPP20IdTokenEnumType.ISO14443 },
+        },
+        procedureName: 'transactionEvent',
+      })
+    })
+
+    await it('should not include evseId for OCPP 1.6 even if provided', () => {
+      const result = buildStartTransactionPayload(1, OCPPVersion.VERSION_16, {
+        evseId: 2,
+        idTag: 'TAG1',
+      })
+      assert.deepStrictEqual(result, {
+        payload: { connectorId: 1, idTag: 'TAG1' },
+        procedureName: 'startTransaction',
+      })
+    })
+
+    await it('should omit idTag/idToken when not provided', () => {
+      const result = buildStartTransactionPayload(1, OCPPVersion.VERSION_201)
+      assert.deepStrictEqual(result, {
+        payload: {
+          connectorId: 1,
+          eventType: OCPP20TransactionEventEnumType.STARTED,
+        },
+        procedureName: 'transactionEvent',
+      })
+    })
+  })
+
+  await describe('buildStopTransactionPayload', async () => {
+    await it('should build OCPP 1.6 payload with stopTransaction procedure', () => {
+      const result = buildStopTransactionPayload(12345, OCPPVersion.VERSION_16)
+      assert.deepStrictEqual(result, {
+        payload: { transactionId: 12345 },
+        procedureName: 'stopTransaction',
+      })
+    })
+
+    await it('should build OCPP 2.0.1 payload with transactionEvent procedure', () => {
+      const result = buildStopTransactionPayload('uuid-123', OCPPVersion.VERSION_201, 1)
+      assert.deepStrictEqual(result, {
+        payload: {
+          connectorId: 1,
+          eventType: OCPP20TransactionEventEnumType.ENDED,
+          transactionId: 'uuid-123',
+        },
+        procedureName: 'transactionEvent',
+      })
+    })
+
+    await it('should convert numeric transactionId to string for OCPP 2.0.x', () => {
+      const result = buildStopTransactionPayload(99, OCPPVersion.VERSION_201, 1)
+      assert.strictEqual((result.payload as Record<string, unknown>).transactionId, '99')
+    })
+
+    await it('should omit connectorId for OCPP 2.0.x when not provided', () => {
+      const result = buildStopTransactionPayload('uuid-123', OCPPVersion.VERSION_201)
+      assert.strictEqual((result.payload as Record<string, unknown>).connectorId, undefined)
+    })
+  })
+
+  await describe('buildIdToken', async () => {
+    await it('should build idToken with default ISO14443 type', () => {
+      assert.deepStrictEqual(buildIdToken('TAG1'), {
+        idToken: 'TAG1',
+        type: OCPP20IdTokenEnumType.ISO14443,
+      })
+    })
+
+    await it('should build idToken with custom type', () => {
+      assert.deepStrictEqual(buildIdToken('TAG1', OCPP20IdTokenEnumType.CENTRAL), {
+        idToken: 'TAG1',
+        type: OCPP20IdTokenEnumType.CENTRAL,
+      })
+    })
+  })
+})
index 18ff2732c46b53e53e5f4b2b822c51e93e399f43..b95b9ead6aa3c40924412d404bf567ee13909e9c 100644 (file)
@@ -1,10 +1,12 @@
+import type { OCPPVersion } from 'ui-common'
+
 import {
+  buildAuthorizePayload,
+  buildStartTransactionPayload,
+  buildStopTransactionPayload,
   type ChargingStationOptions,
   createBrowserWsAdapter,
-  OCPP20IdTokenEnumType,
-  OCPP20TransactionEventEnumType,
-  type OCPP20TransactionEventRequest,
-  OCPPVersion,
+  isOCPP20x,
   ProcedureName,
   type RequestPayload,
   type ResponsePayload,
@@ -46,10 +48,6 @@ export class UIClient {
     return UIClient.instance
   }
 
-  private static isOCPP20x (version: OCPPVersion | undefined): boolean {
-    return version === OCPPVersion.VERSION_20 || version === OCPPVersion.VERSION_201
-  }
-
   public async addChargingStations (
     template: string,
     numberOfStations: number,
@@ -62,10 +60,14 @@ export class UIClient {
     })
   }
 
-  public async authorize (hashId: string, idTag: string): Promise<ResponsePayload> {
+  public async authorize (
+    hashId: string,
+    idTag: string,
+    ocppVersion?: OCPPVersion
+  ): Promise<ResponsePayload> {
     return this.sendRequest(ProcedureName.AUTHORIZE, {
       hashIds: [hashId],
-      idTag,
+      ...buildAuthorizePayload(idTag, ocppVersion),
     })
   }
 
@@ -180,23 +182,17 @@ export class UIClient {
       ocppVersion?: OCPPVersion
     }
   ): Promise<ResponsePayload> {
-    if (UIClient.isOCPP20x(options.ocppVersion)) {
-      return this.transactionEvent(hashId, {
-        eventType: OCPP20TransactionEventEnumType.STARTED,
-        evse:
-          options.evseId != null
-            ? { connectorId: options.connectorId, id: options.evseId }
-            : undefined,
-        idToken:
-          options.idTag != null
-            ? { idToken: options.idTag, type: OCPP20IdTokenEnumType.ISO14443 }
-            : undefined,
-      })
+    const { payload, procedureName } = buildStartTransactionPayload(
+      options.connectorId,
+      options.ocppVersion,
+      { evseId: options.evseId, idTag: options.idTag }
+    )
+    if (procedureName === 'transactionEvent') {
+      return this.transactionEvent(hashId, payload)
     }
     return this.sendRequest(ProcedureName.START_TRANSACTION, {
-      connectorId: options.connectorId,
       hashIds: [hashId],
-      idTag: options.idTag,
+      ...payload,
     })
   }
 
@@ -227,13 +223,19 @@ export class UIClient {
       transactionId: number | string | undefined
     }
   ): Promise<ResponsePayload> {
-    if (UIClient.isOCPP20x(options.ocppVersion)) {
-      return this.transactionEvent(hashId, {
-        eventType: OCPP20TransactionEventEnumType.ENDED,
-        transactionId: options.transactionId?.toString(),
-      })
+    if (options.transactionId == null) {
+      return {
+        responsesFailed: [
+          {
+            errorMessage: 'transactionId is required',
+            hashId,
+            status: ResponseStatus.FAILURE,
+          },
+        ],
+        status: ResponseStatus.FAILURE,
+      }
     }
-    if (typeof options.transactionId === 'string') {
+    if (!isOCPP20x(options.ocppVersion) && typeof options.transactionId === 'string') {
       return {
         responsesFailed: [
           {
@@ -245,9 +247,16 @@ export class UIClient {
         status: ResponseStatus.FAILURE,
       }
     }
+    const { payload, procedureName } = buildStopTransactionPayload(
+      options.transactionId,
+      options.ocppVersion
+    )
+    if (procedureName === 'transactionEvent') {
+      return this.transactionEvent(hashId, payload)
+    }
     return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
       hashIds: [hashId],
-      transactionId: options.transactionId,
+      ...payload,
     })
   }
 
@@ -375,7 +384,7 @@ export class UIClient {
 
   private async transactionEvent (
     hashId: string,
-    payload: OCPP20TransactionEventRequest
+    payload: RequestPayload
   ): Promise<ResponsePayload> {
     return this.sendRequest(ProcedureName.TRANSACTION_EVENT, {
       hashIds: [hashId],
index 2f5615940f93d957c5934b7a6d2008804c69298f..7d5784eb84e1271d46f7e2da9b5ac3348e02b6e9 100644 (file)
@@ -66,7 +66,7 @@ export function useStartTxForm (config: StartTxFormConfig): {
           return false
         }
         try {
-          await $uiClient.authorize(hashId, idTag)
+          await $uiClient.authorize(hashId, idTag, toValue(ocppVersion))
         } catch (error: unknown) {
           $toast.error('Error at authorizing RFID tag')
           console.error('Error at authorizing RFID tag:', error)
index 76ef9a7d4a0928a31a70b5c287133ab0e95ff97d..e9f2a0c7c4cb3141fe4f7f10e632d2d10c8fd422 100644 (file)
@@ -69,6 +69,7 @@
       v-if="showAuthorizeDialog"
       :hash-id="showAuthorizeDialog.hashId"
       :charging-station-id="showAuthorizeDialog.chargingStationId"
+      :ocpp-version="showAuthorizeDialog.ocppVersion"
       @close="showAuthorizeDialog = null"
     />
   </main>
@@ -158,6 +159,7 @@ const showStartTxDialog = ref<null | {
 const showAuthorizeDialog = ref<null | {
   chargingStationId: string
   hashId: string
+  ocppVersion?: OCPPVersion
 }>(null)
 
 const confirmStopSimulator = (): void => {
index 3d056eb7459bcaec0872ab3d27f13cc5703ac2b4..d874cf395ac7be731bd26e107451f50ae8618cc8 100644 (file)
@@ -188,7 +188,7 @@ const props = defineProps<{
 
 const emit = defineEmits<{
   'need-refresh': []
-  'open-authorize': [data: { chargingStationId: string; hashId: string }]
+  'open-authorize': [data: { chargingStationId: string; hashId: string; ocppVersion?: OCPPVersion }]
   'open-set-url': [data: { chargingStationId: string; hashId: string }]
   'open-start-tx': [
     data: {
@@ -255,6 +255,7 @@ const emitOpenAuthorize = (): void => {
   emit('open-authorize', {
     chargingStationId: props.chargingStation.stationInfo.chargingStationId,
     hashId: props.chargingStation.stationInfo.hashId,
+    ocppVersion: props.chargingStation.stationInfo.ocppVersion,
   })
 }
 
index 61e8d3c8c25b3702a7e0bdc3dfe3cf47ba8ef7c0..664d06405e636897f9d3623d76c448536e1a113e 100644 (file)
@@ -60,6 +60,7 @@
 </template>
 
 <script setup lang="ts">
+import { type OCPPVersion } from 'ui-common'
 import { computed, ref } from 'vue'
 import { useToast } from 'vue-toast-notification'
 
@@ -72,6 +73,7 @@ import Modal from '../ModernModal.vue'
 const props = defineProps<{
   chargingStationId: string
   hashId: string
+  ocppVersion?: OCPPVersion
 }>()
 
 const emit = defineEmits<{ close: [] }>()
@@ -101,7 +103,7 @@ const submit = async (): Promise<void> => {
   pending.value = true
   lastFailure.value = null
   try {
-    await $uiClient.authorize(props.hashId, idTag.value)
+    await $uiClient.authorize(props.hashId, idTag.value, props.ocppVersion)
     $toast.success(`Authorized ${idTag.value}`)
     close()
   } catch (error: unknown) {
index 5a83173b11935d43374adc5850757c4f523f61f8..cc1a287bffecb1cd3c9afcea8ab379c3028bb49e 100644 (file)
@@ -6,7 +6,6 @@
  */
 import {
   AuthenticationType,
-  OCPP20TransactionEventEnumType,
   OCPPVersion,
   ProcedureName,
   ResponseStatus,
@@ -478,21 +477,20 @@ describe('UIClient', () => {
     })
 
     describe('startTransaction', () => {
-      it('should send START_TRANSACTION for OCPP 1.6', async () => {
+      it('should route to START_TRANSACTION for OCPP 1.6', async () => {
         await client.startTransaction(TEST_HASH_ID, {
           connectorId: 1,
           idTag: TEST_ID_TAG,
           ocppVersion: OCPPVersion.VERSION_16,
         })
 
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.START_TRANSACTION, {
-          connectorId: 1,
-          hashIds: [TEST_HASH_ID],
-          idTag: TEST_ID_TAG,
-        })
+        expect(sendRequestSpy).toHaveBeenCalledWith(
+          ProcedureName.START_TRANSACTION,
+          expect.objectContaining({ connectorId: 1, hashIds: [TEST_HASH_ID], idTag: TEST_ID_TAG })
+        )
       })
 
-      it('should send TRANSACTION_EVENT with evse object for OCPP 2.0.x', async () => {
+      it('should route to TRANSACTION_EVENT for OCPP 2.0.x', async () => {
         await client.startTransaction(TEST_HASH_ID, {
           connectorId: 2,
           evseId: 1,
@@ -500,131 +498,67 @@ describe('UIClient', () => {
           ocppVersion: OCPPVersion.VERSION_20,
         })
 
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.TRANSACTION_EVENT, {
-          eventType: OCPP20TransactionEventEnumType.STARTED,
-          evse: { connectorId: 2, id: 1 },
-          hashIds: [TEST_HASH_ID],
-          idToken: { idToken: TEST_ID_TAG, type: 'ISO14443' },
-        })
-      })
-
-      it('should default to OCPP 1.6 when version is undefined', async () => {
-        await client.startTransaction(TEST_HASH_ID, {
-          connectorId: 1,
-          idTag: TEST_ID_TAG,
-        })
-
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.START_TRANSACTION, {
-          connectorId: 1,
-          hashIds: [TEST_HASH_ID],
-          idTag: TEST_ID_TAG,
-        })
+        expect(sendRequestSpy).toHaveBeenCalledWith(
+          ProcedureName.TRANSACTION_EVENT,
+          expect.objectContaining({ connectorId: 2, evseId: 1, hashIds: [TEST_HASH_ID] })
+        )
       })
 
-      it('should send undefined evse when evseId is not provided for OCPP 2.0.x', async () => {
+      it('should default to START_TRANSACTION when version is undefined', async () => {
         await client.startTransaction(TEST_HASH_ID, {
           connectorId: 1,
           idTag: TEST_ID_TAG,
-          ocppVersion: OCPPVersion.VERSION_20,
-        })
-
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.TRANSACTION_EVENT, {
-          eventType: OCPP20TransactionEventEnumType.STARTED,
-          evse: undefined,
-          hashIds: [TEST_HASH_ID],
-          idToken: { idToken: TEST_ID_TAG, type: 'ISO14443' },
-        })
-      })
-
-      it('should send undefined idToken when idTag is not provided for OCPP 2.0.x', async () => {
-        await client.startTransaction(TEST_HASH_ID, {
-          connectorId: 1,
-          evseId: 1,
-          ocppVersion: OCPPVersion.VERSION_20,
-        })
-
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.TRANSACTION_EVENT, {
-          eventType: OCPP20TransactionEventEnumType.STARTED,
-          evse: { connectorId: 1, id: 1 },
-          hashIds: [TEST_HASH_ID],
-          idToken: undefined,
-        })
-      })
-
-      it('should send undefined evse and idToken when both absent for OCPP 2.0.x', async () => {
-        await client.startTransaction(TEST_HASH_ID, {
-          connectorId: 1,
-          ocppVersion: OCPPVersion.VERSION_20,
         })
 
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.TRANSACTION_EVENT, {
-          eventType: OCPP20TransactionEventEnumType.STARTED,
-          evse: undefined,
-          hashIds: [TEST_HASH_ID],
-          idToken: undefined,
-        })
+        expect(sendRequestSpy).toHaveBeenCalledWith(
+          ProcedureName.START_TRANSACTION,
+          expect.objectContaining({ connectorId: 1, hashIds: [TEST_HASH_ID], idTag: TEST_ID_TAG })
+        )
       })
     })
 
     describe('stopTransaction', () => {
-      it('should send STOP_TRANSACTION for OCPP 1.6', async () => {
+      it('should route to STOP_TRANSACTION for OCPP 1.6', async () => {
         await client.stopTransaction(TEST_HASH_ID, {
           ocppVersion: OCPPVersion.VERSION_16,
           transactionId: 12345,
         })
 
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.STOP_TRANSACTION, {
-          hashIds: [TEST_HASH_ID],
-          transactionId: 12345,
-        })
+        expect(sendRequestSpy).toHaveBeenCalledWith(
+          ProcedureName.STOP_TRANSACTION,
+          expect.objectContaining({ hashIds: [TEST_HASH_ID], transactionId: 12345 })
+        )
       })
 
-      it('should send TRANSACTION_EVENT with Ended for OCPP 2.0.x', async () => {
+      it('should route to TRANSACTION_EVENT for OCPP 2.0.x', async () => {
         await client.stopTransaction(TEST_HASH_ID, {
           ocppVersion: OCPPVersion.VERSION_20,
           transactionId: 'tx-uuid-123',
         })
 
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.TRANSACTION_EVENT, {
-          eventType: OCPP20TransactionEventEnumType.ENDED,
-          hashIds: [TEST_HASH_ID],
-          transactionId: 'tx-uuid-123',
-        })
+        expect(sendRequestSpy).toHaveBeenCalledWith(
+          ProcedureName.TRANSACTION_EVENT,
+          expect.objectContaining({ hashIds: [TEST_HASH_ID], transactionId: 'tx-uuid-123' })
+        )
       })
 
-      it('should default to OCPP 1.6 when version is undefined', async () => {
+      it('should default to STOP_TRANSACTION when version is undefined', async () => {
         await client.stopTransaction(TEST_HASH_ID, { transactionId: 12345 })
 
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.STOP_TRANSACTION, {
-          hashIds: [TEST_HASH_ID],
-          transactionId: 12345,
-        })
+        expect(sendRequestSpy).toHaveBeenCalledWith(
+          ProcedureName.STOP_TRANSACTION,
+          expect.objectContaining({ hashIds: [TEST_HASH_ID], transactionId: 12345 })
+        )
       })
 
-      it('should send undefined transactionId for OCPP 2.0.x when not provided', async () => {
-        await client.stopTransaction(TEST_HASH_ID, {
+      it('should return failure when transactionId is undefined', async () => {
+        const result = await client.stopTransaction(TEST_HASH_ID, {
           ocppVersion: OCPPVersion.VERSION_20,
           transactionId: undefined,
         })
 
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.TRANSACTION_EVENT, {
-          eventType: OCPP20TransactionEventEnumType.ENDED,
-          hashIds: [TEST_HASH_ID],
-          transactionId: undefined,
-        })
-      })
-
-      it('should convert numeric transactionId to string for OCPP 2.0.x', async () => {
-        await client.stopTransaction(TEST_HASH_ID, {
-          ocppVersion: OCPPVersion.VERSION_20,
-          transactionId: 12345,
-        })
-
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.TRANSACTION_EVENT, {
-          eventType: OCPP20TransactionEventEnumType.ENDED,
-          hashIds: [TEST_HASH_ID],
-          transactionId: '12345',
-        })
+        expect(result.status).toBe(ResponseStatus.FAILURE)
+        expect(sendRequestSpy).not.toHaveBeenCalled()
       })
 
       it('should return failure for string transactionId with OCPP 1.6', async () => {
@@ -636,18 +570,6 @@ describe('UIClient', () => {
         expect(result.status).toBe(ResponseStatus.FAILURE)
         expect(sendRequestSpy).not.toHaveBeenCalled()
       })
-
-      it('should send undefined transactionId for OCPP 1.6 when not provided', async () => {
-        await client.stopTransaction(TEST_HASH_ID, {
-          ocppVersion: OCPPVersion.VERSION_16,
-          transactionId: undefined,
-        })
-
-        expect(sendRequestSpy).toHaveBeenCalledWith(ProcedureName.STOP_TRANSACTION, {
-          hashIds: [TEST_HASH_ID],
-          transactionId: undefined,
-        })
-      })
     })
   })
 
index d653ee0ecf455ddf281d2bb548a9356471e0abe0..f7a83970580aafc92a5ac6577572b23443415637 100644 (file)
@@ -80,7 +80,7 @@ describe('useStartTxForm', () => {
     formState.value.authorizeIdTag = true
     formState.value.idTag = 'TAG001'
     await submitForm()
-    expect(mockAuthorize).toHaveBeenCalledWith('hash1', 'TAG001')
+    expect(mockAuthorize).toHaveBeenCalledWith('hash1', 'TAG001', undefined)
     expect(mockStartTransaction).toHaveBeenCalled()
   })
 
index 97f92cb09fa156c129b949f56b4e396cd0a2176a..1406ca18cfb762ff657904638d325b7c1e7414dc 100644 (file)
@@ -3,6 +3,7 @@
  * @description Unit tests for classic skin action components: AddChargingStations, SetSupervisionUrl, StartTransaction.
  */
 import { flushPromises, mount } from '@vue/test-utils'
+import { OCPPVersion } from 'ui-common'
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { ref, shallowRef } from 'vue'
 
@@ -331,14 +332,18 @@ describe('Actions', () => {
       const submitBtn = wrapper.findComponent(ButtonStub)
       await submitBtn.trigger('click')
       await flushPromises()
-      expect(mockClient.authorize).toHaveBeenCalledWith(TEST_HASH_ID, 'RFID-001')
+      expect(mockClient.authorize).toHaveBeenCalledWith(
+        TEST_HASH_ID,
+        'RFID-001',
+        OCPPVersion.VERSION_16
+      )
       expect(mockClient.startTransaction).toHaveBeenCalledWith(
         TEST_HASH_ID,
         expect.objectContaining({
           connectorId: 1,
           evseId: 1,
           idTag: 'RFID-001',
-          ocppVersion: '1.6',
+          ocppVersion: OCPPVersion.VERSION_16,
         })
       )
     })
index 71e48e8f5734eca78d63230b2dc19d13193b8788..6286f5c11ae3b27ce34f5086a89ff5f89ad6d2f9 100644 (file)
@@ -4,7 +4,7 @@
  *   Modal is mocked to skip the Teleport so wrapper.find() reaches dialog inputs.
  */
 import { flushPromises, mount } from '@vue/test-utils'
-import { ResponseStatus, ServerFailureError } from 'ui-common'
+import { OCPPVersion, ResponseStatus, ServerFailureError } from 'ui-common'
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { defineComponent, ref } from 'vue'
 
@@ -291,7 +291,7 @@ describe('Dialogs', () => {
       await checkbox.setValue(true)
       await wrapper.findAll('.stub-modal__foot button')[1].trigger('click')
       await flushPromises()
-      expect(mockClient.authorize).toHaveBeenCalledWith(TEST_HASH_ID, 'RFID-01')
+      expect(mockClient.authorize).toHaveBeenCalledWith(TEST_HASH_ID, 'RFID-01', undefined)
       expect(mockClient.startTransaction).toHaveBeenCalledWith(
         TEST_HASH_ID,
         expect.objectContaining({ connectorId: 1, idTag: 'RFID-01' })
@@ -311,13 +311,13 @@ describe('Dialogs', () => {
     })
 
     it('should include evseId and ocppVersion from props', async () => {
-      const wrapper = mountDialog({ evseId: 2, ocppVersion: '1.6' })
+      const wrapper = mountDialog({ evseId: 2, ocppVersion: OCPPVersion.VERSION_16 })
       await wrapper.find('#modern-tx-idtag').setValue('RFID-01')
       await wrapper.findAll('.stub-modal__foot button')[1].trigger('click')
       await flushPromises()
       expect(mockClient.startTransaction).toHaveBeenCalledWith(
         TEST_HASH_ID,
-        expect.objectContaining({ evseId: 2, ocppVersion: '1.6' })
+        expect.objectContaining({ evseId: 2, ocppVersion: OCPPVersion.VERSION_16 })
       )
       expect(wrapper.text()).toContain('EVSE 2')
     })
@@ -402,7 +402,7 @@ describe('Dialogs', () => {
       await wrapper.find('#modern-auth-tag').setValue('GOOD')
       await wrapper.findAll('.stub-modal__foot button')[1].trigger('click')
       await flushPromises()
-      expect(mockClient.authorize).toHaveBeenCalledWith(TEST_HASH_ID, 'GOOD')
+      expect(mockClient.authorize).toHaveBeenCalledWith(TEST_HASH_ID, 'GOOD', undefined)
       expect(toastMock.success).toHaveBeenCalled()
       expect(wrapper.emitted('close')).toHaveLength(1)
     })
index 6764af6aae850f705eab16930ca7cc1a538a5e75..b284fde4e9cd01cf674f89f8a91ff4a8263eb84e 100644 (file)
@@ -3,7 +3,7 @@
  * @description Header pills, connector enumeration, start/connect/delete, supervision/authorize events.
  */
 import { flushPromises, mount } from '@vue/test-utils'
-import { type ChargingStationData, OCPP16AvailabilityType } from 'ui-common'
+import { type ChargingStationData, OCPP16AvailabilityType, OCPPVersion } from 'ui-common'
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 
 import { uiClientKey } from '@/core/index.js'
@@ -230,7 +230,13 @@ describe('StationCard', () => {
       const btn = buttons.find(b => b.text() === 'Authorize')
       await btn?.trigger('click')
       expect(wrapper.emitted('open-authorize')).toEqual([
-        [{ chargingStationId: TEST_STATION_ID, hashId: TEST_HASH_ID }],
+        [
+          {
+            chargingStationId: TEST_STATION_ID,
+            hashId: TEST_HASH_ID,
+            ocppVersion: OCPPVersion.VERSION_16,
+          },
+        ],
       ])
     })