import { randomUUID } from 'crypto'; import type { IEntity } from '@gridpilot/shared/domain'; import { StepId } from '../value-objects/StepId'; import { SessionState } from '../value-objects/SessionState'; import type { HostedSessionConfig } from '../types/HostedSessionConfig'; import { AutomationDomainError } from '../errors/AutomationDomainError'; export class AutomationSession implements IEntity { private readonly _id: string; private _currentStep: StepId; private _state: SessionState; private readonly _config: HostedSessionConfig; private _startedAt?: Date; private _completedAt?: Date; private _errorMessage?: string; private constructor( id: string, currentStep: StepId, state: SessionState, config: HostedSessionConfig ) { this._id = id; this._currentStep = currentStep; this._state = state; this._config = config; } static create(config: HostedSessionConfig): AutomationSession { if (!config.sessionName || config.sessionName.trim() === '') { throw new AutomationDomainError('Session name cannot be empty'); } if (!config.trackId || config.trackId.trim() === '') { throw new AutomationDomainError('Track ID is required'); } if (!config.carIds || config.carIds.length === 0) { throw new AutomationDomainError('At least one car must be selected'); } return new AutomationSession( randomUUID(), StepId.create(1), SessionState.create('PENDING'), config ); } get id(): string { return this._id; } get currentStep(): StepId { return this._currentStep; } get state(): SessionState { return this._state; } get config(): HostedSessionConfig { return this._config; } get startedAt(): Date | undefined { return this._startedAt; } get completedAt(): Date | undefined { return this._completedAt; } get errorMessage(): string | undefined { return this._errorMessage; } start(): void { if (!this._state.isPending()) { throw new AutomationDomainError('Cannot start session that is not pending'); } this._state = SessionState.create('IN_PROGRESS'); this._startedAt = new Date(); } transitionToStep(targetStep: StepId): void { if (!this._state.isInProgress()) { throw new AutomationDomainError('Cannot transition steps when session is not in progress'); } if (this._currentStep.equals(targetStep)) { throw new AutomationDomainError('Already at this step'); } if (targetStep.value < this._currentStep.value) { throw new AutomationDomainError('Cannot move backward - steps must progress forward only'); } if (targetStep.value !== this._currentStep.value + 1) { throw new AutomationDomainError('Cannot skip steps - must transition sequentially'); } this._currentStep = targetStep; if (this._currentStep.isFinalStep()) { this._state = SessionState.create('STOPPED_AT_STEP_18'); this._completedAt = new Date(); } } pause(): void { if (!this._state.isInProgress()) { throw new AutomationDomainError('Cannot pause session that is not in progress'); } this._state = SessionState.create('PAUSED'); } resume(): void { if (this._state.value !== 'PAUSED') { throw new AutomationDomainError('Cannot resume session that is not paused'); } this._state = SessionState.create('IN_PROGRESS'); } fail(errorMessage: string): void { if (this._state.isTerminal()) { throw new AutomationDomainError('Cannot fail terminal session'); } this._state = SessionState.create('FAILED'); this._errorMessage = errorMessage; this._completedAt = new Date(); } isAtModalStep(): boolean { return this._currentStep.isModalStep(); } getElapsedTime(): number { if (!this._startedAt) { return 0; } const endTime = this._completedAt || new Date(); const elapsed = endTime.getTime() - this._startedAt.getTime(); return elapsed > 0 ? elapsed : 1; } }