]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(charging-station): extract reservation helpers from Helpers.ts (issue #1936...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Fri, 3 Jul 2026 19:53:26 +0000 (21:53 +0200)
committerGitHub <noreply@github.com>
Fri, 3 Jul 2026 19:53:26 +0000 (21:53 +0200)
* refactor(charging-station): extract reservation helpers from Helpers.ts (issue #1936 f)

Phase 1 of the Helpers.ts file split tracked in issue #1936 item (f).

Helpers.ts is 1389 LOC — 5.5x the 250 LOC ceiling documented in
AGENTS.md's programming skill. This PR extracts the smallest cohesive
block (the 4 reservation helpers) into a dedicated file as an initial
step, proving the barrel-preservation pattern so the remaining slices
can follow in subsequent PRs without breaking any callers.

Extracted symbols into HelpersReservation.ts:
- hasReservationExpired
- hasPendingReservation
- hasPendingReservations
- removeExpiredReservations

Helpers.ts (1389 -> 1336 LOC) keeps the public API via a barrel
re-export block. Every external caller ('import { ... } from
"./Helpers.js"') continues to work unchanged. Now-unused imports
in Helpers.ts (isPast, Reservation, ReservationTerminationReason)
are dropped.

Test invariant fail: 0, skipped: 6 preserved (2925 pass / 2931 total).

Closes issue #1936 item (f) Phase 1 of N.

* [autofix.ci] apply automated fixes

* docs(helpers-reservation): strip historical narrative, align banner, complete JSDoc (issue #1936 f)

Apply round-1 review findings on HelpersReservation.ts:

- Banner alignment: 'Copyright ... 2021-2026' → 'Partial Copyright ...
  2021-2025' to match sibling files in src/charging-station/ that carry
  banners (ChargingStation.ts, Bootstrap.ts, BootstrapStateUtils.ts,
  CoherentMeterValuesManager.ts, AutomaticTransactionGenerator.ts,
  ChargingStationWorker.ts). A repo-wide year bump to 2026 is out of
  scope for this refactor.
- Strip historical narrative from @description per AGENTS.md
  documentation convention ('document current state; exclude historical
  evolution'). Old text narrated the extraction event ('Extracted from
  ./Helpers as the first slice of the issue #1936 (item f) file split'),
  which the commit message + git blame already carry permanently.
  Replace with an operational spec describing the module's current
  responsibility: reservation-state predicates + bulk expired-reservation
  cleanup.
- Add missing @returns on removeExpiredReservations. Every other
  exported helper documents its return; the async cleanup did not.
  New wording also encodes the WHY (Promise.allSettled → never
  rethrows, individual failures logged) that was previously only
  implicit in the body.

* docs(helpers-reservation): harmonize JSDoc voice across predicates (issue #1936 f)

Align the three reservation predicates to a single JSDoc voice:
'hasPendingReservation' and 'hasPendingReservations' were 'Checks
if …'; 'hasReservationExpired' was already 'Determines whether …'
after the R1 doc-fix. Standardize the two predicates to
'Determines whether …' so all three read consistently. Leave
'removeExpiredReservations' as imperative ('Removes every …') since
it is a mutation, not a predicate — different verb class,
appropriate.

Per AGENTS.md 'Naming coherence' — semantically accurate names
across code and documentation, avoid synonyms that create
ambiguity.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
src/charging-station/Helpers.ts
src/charging-station/HelpersReservation.ts [new file with mode: 0644]

index 57d4ab0dbbeceaadd21351cca75ec14ccecd33f6..4fe7cfb277e9fb2ef58a5e0f6b71b3ae8c7c1fd7 100644 (file)
@@ -11,7 +11,6 @@ import {
   isAfter,
   isBefore,
   isDate,
-  isPast,
   isWithinInterval,
   toDate,
 } from 'date-fns'
@@ -46,8 +45,6 @@ import {
   OCPPVersion,
   PowerUnits,
   RecurrencyKindType,
-  type Reservation,
-  ReservationTerminationReason,
   StandardParametersKey,
   type SupportedFeatureProfiles,
   Voltage,
@@ -105,62 +102,12 @@ export const getChargingStationId = (
       )}${idSuffix}`
 }
 
-export const hasReservationExpired = (reservation: Reservation): boolean => {
-  return isPast(reservation.expiryDate)
-}
-
-/**
- * Checks if a connector has a pending (non-expired) reservation.
- * @param connectorStatus - The connector status to check
- * @returns true if the connector has a pending reservation, false otherwise
- */
-export const hasPendingReservation = (connectorStatus: ConnectorStatus): boolean => {
-  return connectorStatus.reservation != null && !hasReservationExpired(connectorStatus.reservation)
-}
-
-/**
- * Checks if a charging station has any pending (non-expired) reservations.
- * @param chargingStation - The charging station to check
- * @returns true if any connector has a pending reservation, false otherwise
- */
-export const hasPendingReservations = (chargingStation: ChargingStation): boolean => {
-  for (const { connectorStatus } of chargingStation.iterateConnectors()) {
-    if (hasPendingReservation(connectorStatus)) {
-      return true
-    }
-  }
-  return false
-}
-
-export const removeExpiredReservations = async (
-  chargingStation: ChargingStation
-): Promise<void> => {
-  const reservations: Reservation[] = []
-  for (const { connectorStatus } of chargingStation.iterateConnectors()) {
-    if (connectorStatus.reservation != null && hasReservationExpired(connectorStatus.reservation)) {
-      reservations.push(connectorStatus.reservation)
-    }
-  }
-  const results = await Promise.allSettled(
-    reservations.map(reservation =>
-      chargingStation.removeReservation(reservation, ReservationTerminationReason.EXPIRED)
-    )
-  )
-  let failureCount = 0
-  for (const result of results) {
-    if (result.status === 'rejected') {
-      ++failureCount
-      logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.removeExpiredReservations: reservation removal failed: ${String(result.reason)}`
-      )
-    }
-  }
-  if (failureCount > 0) {
-    logger.error(
-      `${chargingStation.logPrefix()} ${moduleName}.removeExpiredReservations: ${failureCount.toString()}/${reservations.length.toString()} expired reservation removal(s) failed`
-    )
-  }
-}
+export {
+  hasPendingReservation,
+  hasPendingReservations,
+  hasReservationExpired,
+  removeExpiredReservations,
+} from './HelpersReservation.js'
 
 export const getHashId = (
   index: number,
diff --git a/src/charging-station/HelpersReservation.ts b/src/charging-station/HelpersReservation.ts
new file mode 100644 (file)
index 0000000..e8938e3
--- /dev/null
@@ -0,0 +1,93 @@
+// Partial Copyright Jerome Benoit. 2021-2025. All Rights Reserved.
+
+/**
+ * @file Reservation lifecycle helpers.
+ * @description Predicates for reservation state (`hasReservationExpired`,
+ *   `hasPendingReservation`, `hasPendingReservations`) and a best-effort
+ *   bulk cleanup (`removeExpiredReservations`). Re-exported from
+ *   `./Helpers.js` so callers keep the barrel import path
+ *   (`import { hasReservationExpired, ... } from './Helpers.js'`).
+ */
+
+import { isPast } from 'date-fns'
+
+import type { ChargingStation } from './ChargingStation.js'
+
+import {
+  type ConnectorStatus,
+  type Reservation,
+  ReservationTerminationReason,
+} from '../types/index.js'
+import { logger } from '../utils/index.js'
+
+const moduleName = 'HelpersReservation'
+
+/**
+ * Determines whether a reservation's `expiryDate` is in the past.
+ * @param reservation - The reservation to check.
+ * @returns `true` when the reservation has expired.
+ */
+export const hasReservationExpired = (reservation: Reservation): boolean => {
+  return isPast(reservation.expiryDate)
+}
+
+/**
+ * Determines whether a connector has a pending (non-expired) reservation.
+ * @param connectorStatus - The connector status to check.
+ * @returns `true` if the connector has a pending reservation, `false` otherwise.
+ */
+export const hasPendingReservation = (connectorStatus: ConnectorStatus): boolean => {
+  return connectorStatus.reservation != null && !hasReservationExpired(connectorStatus.reservation)
+}
+
+/**
+ * Determines whether a charging station has any pending (non-expired) reservations.
+ * @param chargingStation - The charging station to check.
+ * @returns `true` if any connector has a pending reservation, `false` otherwise.
+ */
+export const hasPendingReservations = (chargingStation: ChargingStation): boolean => {
+  for (const { connectorStatus } of chargingStation.iterateConnectors()) {
+    if (hasPendingReservation(connectorStatus)) {
+      return true
+    }
+  }
+  return false
+}
+
+/**
+ * Removes every expired reservation currently attached to the station's
+ * connectors. Failures are logged per-reservation and aggregated into a
+ * single error log at the end so a partial-failure batch is still
+ * observable.
+ * @param chargingStation - The charging station whose expired reservations should be cleared.
+ * @returns Resolves once every expiry sweep has settled; individual failures are logged, never rethrown.
+ */
+export const removeExpiredReservations = async (
+  chargingStation: ChargingStation
+): Promise<void> => {
+  const reservations: Reservation[] = []
+  for (const { connectorStatus } of chargingStation.iterateConnectors()) {
+    if (connectorStatus.reservation != null && hasReservationExpired(connectorStatus.reservation)) {
+      reservations.push(connectorStatus.reservation)
+    }
+  }
+  const results = await Promise.allSettled(
+    reservations.map(reservation =>
+      chargingStation.removeReservation(reservation, ReservationTerminationReason.EXPIRED)
+    )
+  )
+  let failureCount = 0
+  for (const result of results) {
+    if (result.status === 'rejected') {
+      ++failureCount
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.removeExpiredReservations: reservation removal failed: ${String(result.reason)}`
+      )
+    }
+  }
+  if (failureCount > 0) {
+    logger.error(
+      `${chargingStation.logPrefix()} ${moduleName}.removeExpiredReservations: ${failureCount.toString()}/${reservations.length.toString()} expired reservation removal(s) failed`
+    )
+  }
+}