From: Jérôme Benoit Date: Fri, 3 Jul 2026 19:53:26 +0000 (+0200) Subject: refactor(charging-station): extract reservation helpers from Helpers.ts (issue #1936... X-Git-Tag: cli@v4.11.0~71 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=f03e7f35b77d8d68523325982650d5cd749c2e89;p=e-mobility-charging-stations-simulator.git refactor(charging-station): extract reservation helpers from Helpers.ts (issue #1936 f Phase 1) (#1946) * 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> --- diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index 57d4ab0d..4fe7cfb2 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -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 => { - 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 index 00000000..e8938e3c --- /dev/null +++ b/src/charging-station/HelpersReservation.ts @@ -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 => { + 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` + ) + } +}