wip
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "@gridpilot/automation-infrastructure",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./adapters/*": "./adapters/*.ts",
|
||||
"./config/*": "./config/*.ts",
|
||||
"./repositories/*": "./repositories/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gridpilot/automation": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface AutomationEngineValidationResultDTO {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface AutomationResultDTO {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CheckoutPrice } from '../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../domain/value-objects/CheckoutState';
|
||||
|
||||
export interface CheckoutConfirmationRequestDTO {
|
||||
price: CheckoutPrice;
|
||||
state: CheckoutState;
|
||||
sessionMetadata: {
|
||||
sessionName: string;
|
||||
trackId: string;
|
||||
carIds: string[];
|
||||
};
|
||||
timeoutMs: number;
|
||||
}
|
||||
8
packages/automation/application/dto/CheckoutInfoDTO.ts
Normal file
8
packages/automation/application/dto/CheckoutInfoDTO.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { CheckoutPrice } from '../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../domain/value-objects/CheckoutState';
|
||||
|
||||
export interface CheckoutInfoDTO {
|
||||
price: CheckoutPrice | null;
|
||||
state: CheckoutState;
|
||||
buttonHtml: string;
|
||||
}
|
||||
5
packages/automation/application/dto/ClickResultDTO.ts
Normal file
5
packages/automation/application/dto/ClickResultDTO.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { AutomationResultDTO } from './AutomationResultDTO';
|
||||
|
||||
export interface ClickResultDTO extends AutomationResultDTO {
|
||||
target: string;
|
||||
}
|
||||
6
packages/automation/application/dto/FormFillResultDTO.ts
Normal file
6
packages/automation/application/dto/FormFillResultDTO.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { AutomationResultDTO } from './AutomationResultDTO';
|
||||
|
||||
export interface FormFillResultDTO extends AutomationResultDTO {
|
||||
fieldName: string;
|
||||
valueSet: string;
|
||||
}
|
||||
6
packages/automation/application/dto/ModalResultDTO.ts
Normal file
6
packages/automation/application/dto/ModalResultDTO.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { AutomationResultDTO } from './AutomationResultDTO';
|
||||
|
||||
export interface ModalResultDTO extends AutomationResultDTO {
|
||||
stepId: number;
|
||||
action: string;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { AutomationResultDTO } from './AutomationResultDTO';
|
||||
|
||||
export interface NavigationResultDTO extends AutomationResultDTO {
|
||||
url: string;
|
||||
loadTime: number;
|
||||
}
|
||||
11
packages/automation/application/dto/SessionDTO.ts
Normal file
11
packages/automation/application/dto/SessionDTO.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
|
||||
|
||||
export interface SessionDTO {
|
||||
sessionId: string;
|
||||
state: string;
|
||||
currentStep: number;
|
||||
config: HostedSessionConfig;
|
||||
startedAt?: Date;
|
||||
completedAt?: Date;
|
||||
errorMessage?: string;
|
||||
}
|
||||
7
packages/automation/application/dto/WaitResultDTO.ts
Normal file
7
packages/automation/application/dto/WaitResultDTO.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { AutomationResultDTO } from './AutomationResultDTO';
|
||||
|
||||
export interface WaitResultDTO extends AutomationResultDTO {
|
||||
target: string;
|
||||
waitedMs: number;
|
||||
found: boolean;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
|
||||
/**
|
||||
* Port for authentication services implementing zero-knowledge login.
|
||||
@@ -10,7 +10,7 @@ import { Result } from '../../shared/result/Result';
|
||||
* the user logs in directly with iRacing. GridPilot only observes
|
||||
* URL changes to detect successful authentication.
|
||||
*/
|
||||
export interface IAuthenticationService {
|
||||
export interface AuthenticationServicePort {
|
||||
/**
|
||||
* Check if user has a valid session without prompting login.
|
||||
* Navigates to a protected iRacing page and checks for login redirects.
|
||||
@@ -0,0 +1,9 @@
|
||||
import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
|
||||
import { StepId } from '../../domain/value-objects/StepId';
|
||||
import type { AutomationEngineValidationResultDTO } from '../dto/AutomationEngineValidationResultDTO';
|
||||
|
||||
export interface AutomationEnginePort {
|
||||
validateConfiguration(config: HostedSessionConfig): Promise<AutomationEngineValidationResultDTO>;
|
||||
executeStep(stepId: StepId, config: HostedSessionConfig): Promise<void>;
|
||||
stopAutomation(): void;
|
||||
}
|
||||
@@ -5,6 +5,6 @@ export type AutomationEvent = {
|
||||
payload?: any
|
||||
}
|
||||
|
||||
export interface IAutomationEventPublisher {
|
||||
export interface AutomationEventPublisherPort {
|
||||
publish(event: AutomationEvent): Promise<void>
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
export interface AutomationResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface NavigationResult extends AutomationResult {
|
||||
url: string;
|
||||
loadTime: number;
|
||||
}
|
||||
|
||||
export interface FormFillResult extends AutomationResult {
|
||||
fieldName: string;
|
||||
valueSet: string;
|
||||
}
|
||||
|
||||
export interface ClickResult extends AutomationResult {
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface WaitResult extends AutomationResult {
|
||||
target: string;
|
||||
waitedMs: number;
|
||||
found: boolean;
|
||||
}
|
||||
|
||||
export interface ModalResult extends AutomationResult {
|
||||
stepId: number;
|
||||
action: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import { CheckoutConfirmation } from '../../domain/value-objects/CheckoutConfirmation';
|
||||
import type { CheckoutConfirmationRequestDTO } from '../dto/CheckoutConfirmationRequestDTO';
|
||||
|
||||
export interface CheckoutConfirmationPort {
|
||||
requestCheckoutConfirmation(
|
||||
request: CheckoutConfirmationRequestDTO
|
||||
): Promise<Result<CheckoutConfirmation>>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import type { CheckoutInfoDTO } from '../dto/CheckoutInfoDTO';
|
||||
|
||||
export interface CheckoutServicePort {
|
||||
extractCheckoutInfo(): Promise<Result<CheckoutInfoDTO>>;
|
||||
proceedWithCheckout(): Promise<Result<void>>;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
|
||||
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IAutomationEngine {
|
||||
validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult>;
|
||||
executeStep(stepId: StepId, config: HostedSessionConfig): Promise<void>;
|
||||
stopAutomation(): void;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
|
||||
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
|
||||
|
||||
export interface CheckoutConfirmationRequest {
|
||||
price: CheckoutPrice;
|
||||
state: CheckoutState;
|
||||
sessionMetadata: {
|
||||
sessionName: string;
|
||||
trackId: string;
|
||||
carIds: string[];
|
||||
};
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
export interface ICheckoutConfirmationPort {
|
||||
requestCheckoutConfirmation(
|
||||
request: CheckoutConfirmationRequest
|
||||
): Promise<Result<CheckoutConfirmation>>;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
|
||||
|
||||
export interface CheckoutInfo {
|
||||
price: CheckoutPrice | null;
|
||||
state: CheckoutState;
|
||||
buttonHtml: string;
|
||||
}
|
||||
|
||||
export interface ICheckoutService {
|
||||
extractCheckoutInfo(): Promise<Result<CheckoutInfo>>;
|
||||
proceedWithCheckout(): Promise<Result<void>>;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Log levels in order of severity (lowest to highest)
|
||||
*/
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
|
||||
/**
|
||||
* Contextual metadata attached to log entries
|
||||
*/
|
||||
export interface LogContext {
|
||||
/** Unique session identifier for correlation */
|
||||
sessionId?: string;
|
||||
/** Current automation step (1-18) */
|
||||
stepId?: number;
|
||||
/** Step name for human readability */
|
||||
stepName?: string;
|
||||
/** Adapter or component name */
|
||||
adapter?: string;
|
||||
/** Operation duration in milliseconds */
|
||||
durationMs?: number;
|
||||
/** Additional arbitrary metadata */
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* ILogger - Port interface for application-layer logging.
|
||||
*/
|
||||
export interface ILogger {
|
||||
debug(message: string, context?: LogContext): void;
|
||||
info(message: string, context?: LogContext): void;
|
||||
warn(message: string, context?: LogContext): void;
|
||||
error(message: string, error?: Error, context?: LogContext): void;
|
||||
fatal(message: string, error?: Error, context?: LogContext): void;
|
||||
child(context: LogContext): ILogger;
|
||||
flush(): Promise<void>;
|
||||
}
|
||||
17
packages/automation/application/ports/LoggerContext.ts
Normal file
17
packages/automation/application/ports/LoggerContext.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Contextual metadata attached to log entries
|
||||
*/
|
||||
export interface LogContext {
|
||||
/** Unique session identifier for correlation */
|
||||
sessionId?: string;
|
||||
/** Current automation step (1-18) */
|
||||
stepId?: number;
|
||||
/** Step name for human readability */
|
||||
stepName?: string;
|
||||
/** Adapter or component name */
|
||||
adapter?: string;
|
||||
/** Operation duration in milliseconds */
|
||||
durationMs?: number;
|
||||
/** Additional arbitrary metadata */
|
||||
[key: string]: unknown;
|
||||
}
|
||||
4
packages/automation/application/ports/LoggerLogLevel.ts
Normal file
4
packages/automation/application/ports/LoggerLogLevel.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Log levels in order of severity (lowest to highest)
|
||||
*/
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
15
packages/automation/application/ports/LoggerPort.ts
Normal file
15
packages/automation/application/ports/LoggerPort.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { LogLevel } from './LoggerLogLevel';
|
||||
import type { LogContext } from './LoggerContext';
|
||||
|
||||
/**
|
||||
* LoggerPort - Port interface for application-layer logging.
|
||||
*/
|
||||
export interface LoggerPort {
|
||||
debug(message: string, context?: LogContext): void;
|
||||
info(message: string, context?: LogContext): void;
|
||||
warn(message: string, context?: LogContext): void;
|
||||
error(message: string, error?: Error, context?: LogContext): void;
|
||||
fatal(message: string, error?: Error, context?: LogContext): void;
|
||||
child(context: LogContext): LoggerPort;
|
||||
flush(): Promise<void>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export type OverlayAction = { id: string; label: string; meta?: Record<string, unknown>; timeoutMs?: number }
|
||||
export type ActionAck = { id: string; status: 'confirmed' | 'tentative' | 'failed'; reason?: string }
|
||||
|
||||
export interface IOverlaySyncPort {
|
||||
export interface OverlaySyncPort {
|
||||
startAction(action: OverlayAction): Promise<ActionAck>
|
||||
cancelAction(actionId: string): Promise<void>
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
|
||||
import {
|
||||
NavigationResult,
|
||||
FormFillResult,
|
||||
ClickResult,
|
||||
WaitResult,
|
||||
ModalResult,
|
||||
AutomationResult,
|
||||
} from './AutomationResults';
|
||||
import { StepId } from '../../domain/value-objects/StepId';
|
||||
import type { NavigationResultDTO } from '../dto/NavigationResultDTO';
|
||||
import type { ClickResultDTO } from '../dto/ClickResultDTO';
|
||||
import type { WaitResultDTO } from '../dto/WaitResultDTO';
|
||||
import type { ModalResultDTO } from '../dto/ModalResultDTO';
|
||||
import type { AutomationResultDTO } from '../dto/AutomationResultDTO';
|
||||
import type { FormFillResultDTO } from '../dto/FormFillResultDTO';
|
||||
|
||||
/**
|
||||
* Browser automation interface for Playwright-based automation.
|
||||
@@ -19,38 +17,38 @@ export interface IBrowserAutomation {
|
||||
/**
|
||||
* Navigate to a URL.
|
||||
*/
|
||||
navigateToPage(url: string): Promise<NavigationResult>;
|
||||
navigateToPage(url: string): Promise<NavigationResultDTO>;
|
||||
|
||||
/**
|
||||
* Fill a form field by name or selector.
|
||||
*/
|
||||
fillFormField(fieldName: string, value: string): Promise<FormFillResult>;
|
||||
fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO>;
|
||||
|
||||
/**
|
||||
* Click an element by selector or action name.
|
||||
*/
|
||||
clickElement(target: string): Promise<ClickResult>;
|
||||
clickElement(target: string): Promise<ClickResultDTO>;
|
||||
|
||||
/**
|
||||
* Wait for an element to appear.
|
||||
*/
|
||||
waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult>;
|
||||
waitForElement(target: string, maxWaitMs?: number): Promise<WaitResultDTO>;
|
||||
|
||||
/**
|
||||
* Handle modal dialogs.
|
||||
*/
|
||||
handleModal(stepId: StepId, action: string): Promise<ModalResult>;
|
||||
handleModal(stepId: StepId, action: string): Promise<ModalResultDTO>;
|
||||
|
||||
/**
|
||||
* Execute a complete workflow step.
|
||||
*/
|
||||
executeStep?(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult>;
|
||||
executeStep?(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO>;
|
||||
|
||||
/**
|
||||
* Initialize the browser connection.
|
||||
* Returns an AutomationResult indicating success or failure.
|
||||
*/
|
||||
connect?(): Promise<AutomationResult>;
|
||||
connect?(): Promise<AutomationResultDTO>;
|
||||
|
||||
/**
|
||||
* Clean up browser resources.
|
||||
@@ -62,9 +60,3 @@ export interface IBrowserAutomation {
|
||||
*/
|
||||
isConnected?(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use IBrowserAutomation directly. IScreenAutomation was for OS-level
|
||||
* automation which has been removed in favor of browser-only automation.
|
||||
*/
|
||||
export type IScreenAutomation = IBrowserAutomation;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
|
||||
import { SessionStateValue } from '@gridpilot/automation/domain/value-objects/SessionState';
|
||||
import { AutomationSession } from '../../domain/entities/AutomationSession';
|
||||
import { SessionStateValue } from '../../domain/value-objects/SessionState';
|
||||
|
||||
export interface ISessionRepository {
|
||||
export interface SessionRepositoryPort {
|
||||
save(session: AutomationSession): Promise<void>;
|
||||
findById(id: string): Promise<AutomationSession | null>;
|
||||
update(session: AutomationSession): Promise<void>;
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Result } from '../../../shared/result/Result';
|
||||
|
||||
export interface SessionValidatorPort {
|
||||
validateSession(): Promise<Result<boolean>>;
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
import { IOverlaySyncPort, OverlayAction, ActionAck } from '../ports/IOverlaySyncPort';
|
||||
import { IAutomationEventPublisher, AutomationEvent } from '../ports/IAutomationEventPublisher';
|
||||
import { OverlaySyncPort, OverlayAction, ActionAck } from '../ports/OverlaySyncPort';
|
||||
import { AutomationEventPublisherPort, AutomationEvent } from '../ports/AutomationEventPublisherPort';
|
||||
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../infrastructure/adapters/IAutomationLifecycleEmitter';
|
||||
import { ILogger } from '../ports/ILogger';
|
||||
import { LoggerPort } from '../ports/LoggerPort';
|
||||
|
||||
type ConstructorArgs = {
|
||||
lifecycleEmitter: IAutomationLifecycleEmitter
|
||||
publisher: IAutomationEventPublisher
|
||||
logger: ILogger
|
||||
publisher: AutomationEventPublisherPort
|
||||
logger: LoggerPort
|
||||
initialPanelWaitMs?: number
|
||||
maxPanelRetries?: number
|
||||
backoffFactor?: number
|
||||
defaultTimeoutMs?: number
|
||||
}
|
||||
|
||||
export class OverlaySyncService implements IOverlaySyncPort {
|
||||
export class OverlaySyncService implements OverlaySyncPort {
|
||||
private lifecycleEmitter: IAutomationLifecycleEmitter
|
||||
private publisher: IAutomationEventPublisher
|
||||
private logger: ILogger
|
||||
private publisher: AutomationEventPublisherPort
|
||||
private logger: LoggerPort
|
||||
private initialPanelWaitMs: number
|
||||
private maxPanelRetries: number
|
||||
private backoffFactor: number
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import type { IAuthenticationService } from '../ports/IAuthenticationService';
|
||||
import { SessionLifetime } from '@gridpilot/automation/domain/value-objects/SessionLifetime';
|
||||
|
||||
/**
|
||||
* Port for optional server-side session validation.
|
||||
*/
|
||||
export interface ISessionValidator {
|
||||
validateSession(): Promise<Result<boolean>>;
|
||||
}
|
||||
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
|
||||
import { SessionLifetime } from '../../domain/value-objects/SessionLifetime';
|
||||
import type { SessionValidatorPort } from '../ports/SessionValidatorPort';
|
||||
|
||||
/**
|
||||
* Use case for checking if the user has a valid iRacing session.
|
||||
@@ -22,8 +16,8 @@ export interface ISessionValidator {
|
||||
*/
|
||||
export class CheckAuthenticationUseCase {
|
||||
constructor(
|
||||
private readonly authService: IAuthenticationService,
|
||||
private readonly sessionValidator?: ISessionValidator
|
||||
private readonly authService: AuthenticationServicePort,
|
||||
private readonly sessionValidator?: SessionValidatorPort
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import type { IAuthenticationService } from '../ports/IAuthenticationService';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
|
||||
|
||||
/**
|
||||
* Use case for clearing the user's session (logout).
|
||||
@@ -8,7 +8,7 @@ import type { IAuthenticationService } from '../ports/IAuthenticationService';
|
||||
* the user out. The next automation attempt will require re-authentication.
|
||||
*/
|
||||
export class ClearSessionUseCase {
|
||||
constructor(private readonly authService: IAuthenticationService) {}
|
||||
constructor(private readonly authService: AuthenticationServicePort) {}
|
||||
|
||||
/**
|
||||
* Execute the session clearing.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import { RaceCreationResult } from '@gridpilot/automation/domain/value-objects/RaceCreationResult';
|
||||
import type { ICheckoutService } from '../ports/ICheckoutService';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import { RaceCreationResult } from '../../domain/value-objects/RaceCreationResult';
|
||||
import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
|
||||
|
||||
export class CompleteRaceCreationUseCase {
|
||||
constructor(private readonly checkoutService: ICheckoutService) {}
|
||||
constructor(private readonly checkoutService: CheckoutServicePort) {}
|
||||
|
||||
async execute(sessionId: string): Promise<Result<RaceCreationResult>> {
|
||||
if (!sessionId || sessionId.trim() === '') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import { ICheckoutService } from '../ports/ICheckoutService';
|
||||
import { ICheckoutConfirmationPort } from '../ports/ICheckoutConfirmationPort';
|
||||
import { CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
|
||||
import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort';
|
||||
import { CheckoutStateEnum } from '../../domain/value-objects/CheckoutState';
|
||||
|
||||
interface SessionMetadata {
|
||||
sessionName: string;
|
||||
@@ -13,8 +13,8 @@ export class ConfirmCheckoutUseCase {
|
||||
private static readonly DEFAULT_TIMEOUT_MS = 30000;
|
||||
|
||||
constructor(
|
||||
private readonly checkoutService: ICheckoutService,
|
||||
private readonly confirmationPort: ICheckoutConfirmationPort
|
||||
private readonly checkoutService: CheckoutServicePort,
|
||||
private readonly confirmationPort: CheckoutConfirmationPort
|
||||
) {}
|
||||
|
||||
async execute(sessionMetadata?: SessionMetadata): Promise<Result<void>> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import type { IAuthenticationService } from '../ports/IAuthenticationService';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
|
||||
|
||||
/**
|
||||
* Use case for initiating the manual login flow.
|
||||
@@ -9,7 +9,7 @@ import type { IAuthenticationService } from '../ports/IAuthenticationService';
|
||||
* indicating successful login.
|
||||
*/
|
||||
export class InitiateLoginUseCase {
|
||||
constructor(private readonly authService: IAuthenticationService) {}
|
||||
constructor(private readonly authService: AuthenticationServicePort) {}
|
||||
|
||||
/**
|
||||
* Execute the login flow.
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
|
||||
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
|
||||
import { IAutomationEngine } from '../ports/IAutomationEngine';
|
||||
import type { IBrowserAutomation } from '../ports/IScreenAutomation';
|
||||
import { ISessionRepository } from '../ports/ISessionRepository';
|
||||
|
||||
export interface SessionDTO {
|
||||
sessionId: string;
|
||||
state: string;
|
||||
currentStep: number;
|
||||
config: HostedSessionConfig;
|
||||
startedAt?: Date;
|
||||
completedAt?: Date;
|
||||
errorMessage?: string;
|
||||
}
|
||||
import { AutomationSession } from '../../domain/entities/AutomationSession';
|
||||
import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
|
||||
import { AutomationEnginePort } from '../ports/AutomationEnginePort';
|
||||
import type { IBrowserAutomation } from '../ports/ScreenAutomationPort';
|
||||
import { SessionRepositoryPort } from '../ports/SessionRepositoryPort';
|
||||
import type { SessionDTO } from '../dto/SessionDTO';
|
||||
|
||||
export class StartAutomationSessionUseCase {
|
||||
constructor(
|
||||
private readonly automationEngine: IAutomationEngine,
|
||||
private readonly automationEngine: AutomationEnginePort,
|
||||
private readonly browserAutomation: IBrowserAutomation,
|
||||
private readonly sessionRepository: ISessionRepository
|
||||
private readonly sessionRepository: SessionRepositoryPort
|
||||
) {}
|
||||
|
||||
async execute(config: HostedSessionConfig): Promise<SessionDTO> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IAuthenticationService } from '../ports/IAuthenticationService';
|
||||
import { Result } from '../../shared/result/Result';
|
||||
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
|
||||
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState';
|
||||
|
||||
/**
|
||||
* Use case for verifying browser shows authenticated page state.
|
||||
@@ -8,7 +8,7 @@ import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-o
|
||||
*/
|
||||
export class VerifyAuthenticatedPageUseCase {
|
||||
constructor(
|
||||
private readonly authService: IAuthenticationService
|
||||
private readonly authService: AuthenticationServicePort
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<BrowserAuthenticationState>> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Result } from '../shared/Result';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
|
||||
/**
|
||||
* Configuration for page state validation.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AutomationEvent } from '../../application/ports/IAutomationEventPublisher';
|
||||
import { AutomationEvent } from '@gridpilot/automation/application/ports/AutomationEventPublisherPort';
|
||||
|
||||
export type LifecycleCallback = (event: AutomationEvent) => Promise<void> | void;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
|
||||
import { CheckoutInfo } from '../../../application/ports/ICheckoutService';
|
||||
import { Result } from '../../../../shared/result/Result';
|
||||
import { CheckoutPrice } from '../../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../../domain/value-objects/CheckoutState';
|
||||
import type { CheckoutInfoDTO } from '../../../application/dto/CheckoutInfoDTO';
|
||||
import { IRACING_SELECTORS } from './dom/IRacingSelectors';
|
||||
|
||||
interface Page {
|
||||
@@ -22,7 +22,7 @@ export class CheckoutPriceExtractor {
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async extractCheckoutInfo(): Promise<Result<CheckoutInfo>> {
|
||||
async extractCheckoutInfo(): Promise<Result<CheckoutInfoDTO>> {
|
||||
try {
|
||||
// Prefer the explicit pill element which contains the price
|
||||
const pillLocator = this.page.locator('.label-pill, .label-inverse');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Page } from 'playwright';
|
||||
import { ILogger } from '../../../../application/ports/ILogger';
|
||||
import { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
|
||||
|
||||
export class AuthenticationGuard {
|
||||
constructor(
|
||||
private readonly page: Page,
|
||||
private readonly logger?: ILogger
|
||||
private readonly logger?: LoggerPort
|
||||
) {}
|
||||
|
||||
async checkForLoginUI(): Promise<boolean> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Page } from 'playwright';
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
|
||||
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
|
||||
import { IRACING_URLS, IRACING_SELECTORS, IRACING_TIMEOUTS } from '../dom/IRacingSelectors';
|
||||
import { AuthenticationGuard } from './AuthenticationGuard';
|
||||
|
||||
export class IRacingPlaywrightAuthFlow implements IPlaywrightAuthFlow {
|
||||
constructor(private readonly logger?: ILogger) {}
|
||||
constructor(private readonly logger?: LoggerPort) {}
|
||||
|
||||
getLoginUrl(): string {
|
||||
return IRACING_URLS.login;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as fs from 'fs';
|
||||
import type { BrowserContext, Page } from 'playwright';
|
||||
|
||||
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
|
||||
import { Result } from '../../../../shared/result/Result';
|
||||
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
|
||||
import { Result } from '../../../../../shared/result/Result';
|
||||
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
|
||||
import { SessionCookieStore } from './SessionCookieStore';
|
||||
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
|
||||
@@ -26,11 +26,11 @@ interface PlaywrightAuthSessionConfig {
|
||||
* - Cookie persistence via SessionCookieStore
|
||||
* - Exposing the IAuthenticationService port for application layer
|
||||
*/
|
||||
export class PlaywrightAuthSessionService implements IAuthenticationService {
|
||||
export class PlaywrightAuthSessionService implements AuthenticationServicePort {
|
||||
private readonly browserSession: PlaywrightBrowserSession;
|
||||
private readonly cookieStore: SessionCookieStore;
|
||||
private readonly authFlow: IPlaywrightAuthFlow;
|
||||
private readonly logger?: ILogger;
|
||||
private readonly logger?: LoggerPort;
|
||||
|
||||
private readonly navigationTimeoutMs: number;
|
||||
private readonly loginWaitTimeoutMs: number;
|
||||
@@ -41,7 +41,7 @@ export class PlaywrightAuthSessionService implements IAuthenticationService {
|
||||
browserSession: PlaywrightBrowserSession,
|
||||
cookieStore: SessionCookieStore,
|
||||
authFlow: IPlaywrightAuthFlow,
|
||||
logger?: ILogger,
|
||||
logger?: LoggerPort,
|
||||
config?: PlaywrightAuthSessionConfig,
|
||||
) {
|
||||
this.browserSession = browserSession;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
|
||||
import { CookieConfiguration } from '@gridpilot/automation/domain/value-objects/CookieConfiguration';
|
||||
import { Result } from '../../../../shared/result/Result';
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
||||
import { CookieConfiguration } from '../../../../domain/value-objects/CookieConfiguration';
|
||||
import { Result } from '../../../../../shared/result/Result';
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
|
||||
interface Cookie {
|
||||
name: string;
|
||||
@@ -43,9 +43,9 @@ const EXPIRY_BUFFER_SECONDS = 300;
|
||||
|
||||
export class SessionCookieStore {
|
||||
private readonly storagePath: string;
|
||||
private logger?: ILogger;
|
||||
|
||||
constructor(userDataDir: string, logger?: ILogger) {
|
||||
private logger?: LoggerPort;
|
||||
|
||||
constructor(userDataDir: string, logger?: LoggerPort) {
|
||||
this.storagePath = path.join(userDataDir, 'session-state.json');
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import type { Browser, Page, BrowserContext } from 'playwright';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
|
||||
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
|
||||
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
|
||||
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
|
||||
import type {
|
||||
NavigationResult,
|
||||
FormFillResult,
|
||||
ClickResult,
|
||||
WaitResult,
|
||||
ModalResult,
|
||||
AutomationResult,
|
||||
} from '../../../../application/ports/AutomationResults';
|
||||
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import { Result } from '../../../../shared/result/Result';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
|
||||
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
|
||||
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
|
||||
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
|
||||
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
|
||||
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
|
||||
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
|
||||
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
|
||||
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
import { Result } from '../../../../../shared/result/Result';
|
||||
import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, ALL_BLOCKED_SELECTORS, BLOCKED_KEYWORDS } from '../dom/IRacingSelectors';
|
||||
import { SessionCookieStore } from '../auth/SessionCookieStore';
|
||||
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
|
||||
@@ -421,7 +419,7 @@ export interface PlaywrightConfig {
|
||||
userDataDir?: string;
|
||||
}
|
||||
|
||||
export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthenticationService {
|
||||
export class PlaywrightAutomationAdapter implements IBrowserAutomation, AuthenticationServicePort {
|
||||
private browser: Browser | null = null;
|
||||
private persistentContext: BrowserContext | null = null;
|
||||
private context: BrowserContext | null = null;
|
||||
@@ -430,7 +428,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
private browserSession: PlaywrightBrowserSession;
|
||||
private connected = false;
|
||||
private isConnecting = false;
|
||||
private logger?: ILogger;
|
||||
private logger?: LoggerPort;
|
||||
private cookieStore: SessionCookieStore;
|
||||
private authService: PlaywrightAuthSessionService;
|
||||
private overlayInjected = false;
|
||||
@@ -450,7 +448,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
private domInteractor!: IRacingDomInteractor;
|
||||
private readonly stepOrchestrator: WizardStepOrchestrator;
|
||||
|
||||
constructor(config: PlaywrightConfig = {}, logger?: ILogger, browserModeLoader?: BrowserModeConfigLoader) {
|
||||
constructor(config: PlaywrightConfig = {}, logger?: LoggerPort, browserModeLoader?: BrowserModeConfigLoader) {
|
||||
this.config = {
|
||||
headless: true,
|
||||
timeout: 10000,
|
||||
@@ -623,7 +621,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
this.connected = this.browserSession.isConnected();
|
||||
}
|
||||
|
||||
async connect(forceHeaded: boolean = false): Promise<AutomationResult> {
|
||||
async connect(forceHeaded: boolean = false): Promise<AutomationResultDTO> {
|
||||
const result = await this.browserSession.connect(forceHeaded);
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error };
|
||||
@@ -701,7 +699,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
return this.connected && this.page !== null;
|
||||
}
|
||||
|
||||
async navigateToPage(url: string): Promise<NavigationResult> {
|
||||
async navigateToPage(url: string): Promise<NavigationResultDTO> {
|
||||
const result = await this.navigator.navigateToPage(url);
|
||||
if (result.success) {
|
||||
// Reset overlay state after successful navigation (page context changed)
|
||||
@@ -710,7 +708,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
return result;
|
||||
}
|
||||
|
||||
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
|
||||
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
|
||||
return this.domInteractor.fillFormField(fieldName, value);
|
||||
}
|
||||
|
||||
@@ -727,7 +725,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
return fieldMap[fieldName] || IRACING_SELECTORS.fields.textInput;
|
||||
}
|
||||
|
||||
async clickElement(target: string): Promise<ClickResult> {
|
||||
async clickElement(target: string): Promise<ClickResultDTO> {
|
||||
return this.domInteractor.clickElement(target);
|
||||
}
|
||||
|
||||
@@ -749,15 +747,15 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
return actionMap[action] || `button:has-text("${action}")`;
|
||||
}
|
||||
|
||||
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult> {
|
||||
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResultDTO> {
|
||||
return this.navigator.waitForElement(target, maxWaitMs);
|
||||
}
|
||||
|
||||
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
|
||||
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
|
||||
return this.domInteractor.handleModal(stepId, action);
|
||||
}
|
||||
|
||||
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult> {
|
||||
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO> {
|
||||
const stepNumber = stepId.value;
|
||||
const skipFixtureNavigation =
|
||||
(config as any).__skipFixtureNavigation === true;
|
||||
@@ -1989,7 +1987,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
* First checks if user is already authenticated - if so, navigates directly to hosted sessions.
|
||||
* Otherwise navigates to login page and waits for user to complete manual login.
|
||||
*/
|
||||
private async handleLogin(): Promise<AutomationResult> {
|
||||
private async handleLogin(): Promise<AutomationResultDTO> {
|
||||
try {
|
||||
if (this.config.baseUrl && !this.config.baseUrl.includes('members.iracing.com')) {
|
||||
this.log('info', 'Fixture baseUrl detected, treating session as authenticated for Step 1', {
|
||||
@@ -2120,7 +2118,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
* Tries the primary selector first, then falls back to alternative selectors.
|
||||
* This is needed because iRacing's form structure can vary slightly.
|
||||
*/
|
||||
private async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResult> {
|
||||
private async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResultDTO> {
|
||||
if (!this.page) {
|
||||
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
|
||||
}
|
||||
@@ -2224,7 +2222,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
}
|
||||
}
|
||||
|
||||
async clickAction(action: string): Promise<ClickResult> {
|
||||
async clickAction(action: string): Promise<ClickResultDTO> {
|
||||
if (!this.page) {
|
||||
return { success: false, target: action, error: 'Browser not connected' };
|
||||
}
|
||||
@@ -2253,7 +2251,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
return { success: true, target: selector };
|
||||
}
|
||||
|
||||
async fillField(fieldName: string, value: string): Promise<FormFillResult> {
|
||||
async fillField(fieldName: string, value: string): Promise<FormFillResultDTO> {
|
||||
if (!this.page) {
|
||||
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
|
||||
import { BrowserModeConfigLoader, BrowserMode } from '../../../config/BrowserModeConfig';
|
||||
import { getAutomationMode } from '../../../config/AutomationConfig';
|
||||
import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
|
||||
@@ -27,7 +27,7 @@ export class PlaywrightBrowserSession {
|
||||
|
||||
constructor(
|
||||
private readonly config: Required<PlaywrightConfig>,
|
||||
private readonly logger?: ILogger,
|
||||
private readonly logger?: LoggerPort,
|
||||
browserModeLoader?: BrowserModeConfigLoader,
|
||||
) {
|
||||
const automationMode = getAutomationMode();
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import type { Page } from 'playwright';
|
||||
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
|
||||
import type {
|
||||
AutomationResult,
|
||||
ClickResult,
|
||||
FormFillResult,
|
||||
} from '../../../../application/ports/AutomationResults';
|
||||
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
|
||||
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
|
||||
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
|
||||
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
|
||||
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
|
||||
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
|
||||
import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
|
||||
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
|
||||
import { IRacingDomNavigator } from '../dom/IRacingDomNavigator';
|
||||
@@ -19,16 +17,16 @@ import { getFixtureForStep } from '../engine/FixtureServer';
|
||||
import type {
|
||||
PageStateValidation,
|
||||
PageStateValidationResult,
|
||||
} from '@gridpilot/automation/domain/services/PageStateValidator';
|
||||
import type { Result } from '../../../../shared/result/Result';
|
||||
} from '../../../../domain/services/PageStateValidator';
|
||||
import type { Result } from '../../../../../shared/result/Result';
|
||||
|
||||
interface WizardStepOrchestratorDeps {
|
||||
config: Required<PlaywrightConfig>;
|
||||
browserSession: PlaywrightBrowserSession;
|
||||
navigator: IRacingDomNavigator;
|
||||
interactor: IRacingDomInteractor;
|
||||
authService: IAuthenticationService;
|
||||
logger?: ILogger;
|
||||
authService: AuthenticationServicePort;
|
||||
logger?: LoggerPort;
|
||||
totalSteps: number;
|
||||
getCheckoutConfirmationCallback: () =>
|
||||
| ((
|
||||
@@ -56,7 +54,7 @@ interface WizardStepOrchestratorDeps {
|
||||
dismissDatetimePickers(): Promise<void>;
|
||||
};
|
||||
helpers: {
|
||||
handleLogin(): Promise<AutomationResult>;
|
||||
handleLogin(): Promise<AutomationResultDTO>;
|
||||
validatePageState(
|
||||
validation: PageStateValidation,
|
||||
): Promise<Result<PageStateValidationResult, Error>>;
|
||||
@@ -69,8 +67,8 @@ export class WizardStepOrchestrator {
|
||||
private readonly browserSession: PlaywrightBrowserSession;
|
||||
private readonly navigator: IRacingDomNavigator;
|
||||
private readonly interactor: IRacingDomInteractor;
|
||||
private readonly authService: IAuthenticationService;
|
||||
private readonly logger?: ILogger;
|
||||
private readonly authService: AuthenticationServicePort;
|
||||
private readonly logger?: LoggerPort;
|
||||
private readonly totalSteps: number;
|
||||
private readonly getCheckoutConfirmationCallbackInternal: WizardStepOrchestratorDeps['getCheckoutConfirmationCallback'];
|
||||
private readonly overlay: WizardStepOrchestratorDeps['overlay'];
|
||||
@@ -139,7 +137,7 @@ export class WizardStepOrchestrator {
|
||||
await this.guards.dismissModals();
|
||||
}
|
||||
|
||||
private async handleLogin(): Promise<AutomationResult> {
|
||||
private async handleLogin(): Promise<AutomationResultDTO> {
|
||||
return this.helpers.handleLogin();
|
||||
}
|
||||
|
||||
@@ -147,14 +145,14 @@ export class WizardStepOrchestrator {
|
||||
await this.navigator.waitForStep(stepNumber);
|
||||
}
|
||||
|
||||
private async clickAction(action: string): Promise<ClickResult> {
|
||||
private async clickAction(action: string): Promise<ClickResultDTO> {
|
||||
return this.interactor.clickAction(action);
|
||||
}
|
||||
|
||||
private async fillFieldWithFallback(
|
||||
fieldName: string,
|
||||
value: string,
|
||||
): Promise<FormFillResult> {
|
||||
): Promise<FormFillResultDTO> {
|
||||
return this.interactor.fillFieldWithFallback(fieldName, value);
|
||||
}
|
||||
|
||||
@@ -200,7 +198,7 @@ export class WizardStepOrchestrator {
|
||||
private async fillField(
|
||||
fieldName: string,
|
||||
value: string,
|
||||
): Promise<FormFillResult> {
|
||||
): Promise<FormFillResultDTO> {
|
||||
return this.interactor.fillField(fieldName, value);
|
||||
}
|
||||
|
||||
@@ -266,7 +264,7 @@ export class WizardStepOrchestrator {
|
||||
async executeStep(
|
||||
stepId: StepId,
|
||||
config: Record<string, unknown>,
|
||||
): Promise<AutomationResult> {
|
||||
): Promise<AutomationResultDTO> {
|
||||
if (!this.page) {
|
||||
return { success: false, error: 'Browser not connected' };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { Page } from 'playwright';
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import type {
|
||||
FormFillResult,
|
||||
ClickResult,
|
||||
ModalResult,
|
||||
} from '../../../../application/ports/AutomationResults';
|
||||
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
|
||||
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
|
||||
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
|
||||
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
|
||||
import { IRACING_SELECTORS, IRACING_TIMEOUTS } from './IRacingSelectors';
|
||||
@@ -17,7 +15,7 @@ export class IRacingDomInteractor {
|
||||
private readonly config: Required<PlaywrightConfig>,
|
||||
private readonly browserSession: PlaywrightBrowserSession,
|
||||
private readonly safeClickService: SafeClickService,
|
||||
private readonly logger?: ILogger,
|
||||
private readonly logger?: LoggerPort,
|
||||
) {}
|
||||
|
||||
private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {
|
||||
@@ -42,7 +40,7 @@ export class IRacingDomInteractor {
|
||||
|
||||
// ===== Public port-facing operations =====
|
||||
|
||||
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
|
||||
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
|
||||
const page = this.browserSession.getPage();
|
||||
if (!page) {
|
||||
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
|
||||
@@ -104,7 +102,7 @@ export class IRacingDomInteractor {
|
||||
}
|
||||
}
|
||||
|
||||
async clickElement(target: string): Promise<ClickResult> {
|
||||
async clickElement(target: string): Promise<ClickResultDTO> {
|
||||
const page = this.browserSession.getPage();
|
||||
if (!page) {
|
||||
return { success: false, target, error: 'Browser not connected' };
|
||||
@@ -124,7 +122,7 @@ export class IRacingDomInteractor {
|
||||
}
|
||||
}
|
||||
|
||||
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
|
||||
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
|
||||
const page = this.browserSession.getPage();
|
||||
if (!page) {
|
||||
return { success: false, stepId: stepId.value, action, error: 'Browser not connected' };
|
||||
@@ -156,7 +154,7 @@ export class IRacingDomInteractor {
|
||||
|
||||
// ===== Public interaction helpers used by adapter steps =====
|
||||
|
||||
async fillField(fieldName: string, value: string): Promise<FormFillResult> {
|
||||
async fillField(fieldName: string, value: string): Promise<FormFillResultDTO> {
|
||||
const page = this.browserSession.getPage();
|
||||
if (!page) {
|
||||
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
|
||||
@@ -208,7 +206,7 @@ export class IRacingDomInteractor {
|
||||
}
|
||||
}
|
||||
|
||||
async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResult> {
|
||||
async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResultDTO> {
|
||||
const page = this.browserSession.getPage();
|
||||
if (!page) {
|
||||
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
|
||||
@@ -249,7 +247,7 @@ export class IRacingDomInteractor {
|
||||
}
|
||||
}
|
||||
|
||||
async clickAction(action: string): Promise<ClickResult> {
|
||||
async clickAction(action: string): Promise<ClickResultDTO> {
|
||||
const page = this.browserSession.getPage();
|
||||
if (!page) {
|
||||
return { success: false, target: action, error: 'Browser not connected' };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Page } from 'playwright';
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import type { NavigationResult, WaitResult } from '../../../../application/ports/AutomationResults';
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
|
||||
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
|
||||
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
|
||||
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
|
||||
import { IRACING_SELECTORS, IRACING_TIMEOUTS, IRACING_URLS } from './IRacingSelectors';
|
||||
@@ -23,7 +24,7 @@ export class IRacingDomNavigator {
|
||||
constructor(
|
||||
private readonly config: Required<PlaywrightConfig>,
|
||||
private readonly browserSession: PlaywrightBrowserSession,
|
||||
private readonly logger?: ILogger,
|
||||
private readonly logger?: LoggerPort,
|
||||
private readonly onWizardDismissed?: () => Promise<void>,
|
||||
) {}
|
||||
|
||||
@@ -43,7 +44,7 @@ export class IRacingDomNavigator {
|
||||
return this.browserSession.getPage();
|
||||
}
|
||||
|
||||
async navigateToPage(url: string): Promise<NavigationResult> {
|
||||
async navigateToPage(url: string): Promise<NavigationResultDTO> {
|
||||
const page = this.getPage();
|
||||
if (!page) {
|
||||
return { success: false, url, loadTime: 0, error: 'Browser not connected' };
|
||||
@@ -78,7 +79,7 @@ export class IRacingDomNavigator {
|
||||
}
|
||||
}
|
||||
|
||||
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult> {
|
||||
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResultDTO> {
|
||||
const page = this.getPage();
|
||||
if (!page) {
|
||||
return { success: false, target, waitedMs: 0, found: false, error: 'Browser not connected' };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Page } from 'playwright';
|
||||
import type { ILogger } from '../../../../application/ports/ILogger';
|
||||
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
|
||||
import { IRACING_SELECTORS, BLOCKED_KEYWORDS } from './IRacingSelectors';
|
||||
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
|
||||
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
|
||||
@@ -8,7 +8,7 @@ export class SafeClickService {
|
||||
constructor(
|
||||
private readonly config: Required<PlaywrightConfig>,
|
||||
private readonly browserSession: PlaywrightBrowserSession,
|
||||
private readonly logger?: ILogger,
|
||||
private readonly logger?: LoggerPort,
|
||||
) {}
|
||||
|
||||
private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { IAutomationEngine, ValidationResult } from '../../../../application/ports/IAutomationEngine';
|
||||
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
|
||||
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
|
||||
import { ISessionRepository } from '../../../../application/ports/ISessionRepository';
|
||||
import { StepTransitionValidator } from '@gridpilot/automation/domain/services/StepTransitionValidator';
|
||||
import type { AutomationEnginePort } from '../../../../application/ports/AutomationEnginePort';
|
||||
import { HostedSessionConfig } from '../../../../domain/entities/HostedSessionConfig';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
|
||||
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
|
||||
|
||||
type ValidationResult = {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Real Automation Engine Adapter.
|
||||
@@ -22,13 +27,13 @@ import { StepTransitionValidator } from '@gridpilot/automation/domain/services/S
|
||||
* browser automation when available. See docs/ARCHITECTURE.md
|
||||
* for the updated automation strategy.
|
||||
*/
|
||||
export class AutomationEngineAdapter implements IAutomationEngine {
|
||||
export class AutomationEngineAdapter implements AutomationEnginePort {
|
||||
private isRunning = false;
|
||||
private automationPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly browserAutomation: IBrowserAutomation,
|
||||
private readonly sessionRepository: ISessionRepository
|
||||
private readonly sessionRepository: SessionRepositoryPort
|
||||
) {}
|
||||
|
||||
async validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult> {
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { IAutomationEngine, ValidationResult } from '../../../../application/ports/IAutomationEngine';
|
||||
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
|
||||
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
|
||||
import { ISessionRepository } from '../../../../application/ports/ISessionRepository';
|
||||
import { StepTransitionValidator } from '@gridpilot/automation/domain/services/StepTransitionValidator';
|
||||
import type { AutomationEnginePort } from '../../../../application/ports/AutomationEnginePort';
|
||||
import { HostedSessionConfig } from '../../../../domain/entities/HostedSessionConfig';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
|
||||
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
|
||||
|
||||
export class MockAutomationEngineAdapter implements IAutomationEngine {
|
||||
type ValidationResult = {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export class MockAutomationEngineAdapter implements AutomationEnginePort {
|
||||
private isRunning = false;
|
||||
private automationPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly browserAutomation: IBrowserAutomation,
|
||||
private readonly sessionRepository: ISessionRepository
|
||||
private readonly sessionRepository: SessionRepositoryPort
|
||||
) {}
|
||||
|
||||
async validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult> {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
|
||||
import {
|
||||
NavigationResult,
|
||||
FormFillResult,
|
||||
ClickResult,
|
||||
WaitResult,
|
||||
ModalResult,
|
||||
AutomationResult,
|
||||
} from '../../../../application/ports/AutomationResults';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
|
||||
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
|
||||
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
|
||||
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
|
||||
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
|
||||
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
|
||||
|
||||
interface MockConfig {
|
||||
simulateFailures?: boolean;
|
||||
@@ -37,7 +35,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
|
||||
};
|
||||
}
|
||||
|
||||
async connect(): Promise<AutomationResult> {
|
||||
async connect(): Promise<AutomationResultDTO> {
|
||||
this.connected = true;
|
||||
return { success: true };
|
||||
}
|
||||
@@ -50,7 +48,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
async navigateToPage(url: string): Promise<NavigationResult> {
|
||||
async navigateToPage(url: string): Promise<NavigationResultDTO> {
|
||||
const delay = this.randomDelay(200, 800);
|
||||
await this.sleep(delay);
|
||||
return {
|
||||
@@ -60,7 +58,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
|
||||
};
|
||||
}
|
||||
|
||||
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
|
||||
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
|
||||
const delay = this.randomDelay(100, 500);
|
||||
await this.sleep(delay);
|
||||
return {
|
||||
@@ -70,7 +68,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
|
||||
};
|
||||
}
|
||||
|
||||
async clickElement(selector: string): Promise<ClickResult> {
|
||||
async clickElement(selector: string): Promise<ClickResultDTO> {
|
||||
const delay = this.randomDelay(50, 300);
|
||||
await this.sleep(delay);
|
||||
return {
|
||||
@@ -79,7 +77,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
|
||||
};
|
||||
}
|
||||
|
||||
async waitForElement(selector: string, maxWaitMs: number = 5000): Promise<WaitResult> {
|
||||
async waitForElement(selector: string, maxWaitMs: number = 5000): Promise<WaitResultDTO> {
|
||||
const delay = this.randomDelay(100, 1000);
|
||||
|
||||
await this.sleep(delay);
|
||||
@@ -92,7 +90,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
|
||||
};
|
||||
}
|
||||
|
||||
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
|
||||
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
|
||||
if (!stepId.isModalStep()) {
|
||||
throw new Error(`Step ${stepId.value} is not a modal step`);
|
||||
}
|
||||
@@ -106,7 +104,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
|
||||
};
|
||||
}
|
||||
|
||||
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult> {
|
||||
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO> {
|
||||
if (this.shouldSimulateFailure()) {
|
||||
throw new Error(`Simulated failure at step ${stepId.value}`);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import { ipcMain } from 'electron';
|
||||
import { Result } from '../../../shared/result/Result';
|
||||
import type { ICheckoutConfirmationPort, CheckoutConfirmationRequest } from '../../../application/ports/ICheckoutConfirmationPort';
|
||||
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
|
||||
import { Result } from '../../../../shared/result/Result';
|
||||
import type { CheckoutConfirmationPort } from '../../../application/ports/CheckoutConfirmationPort';
|
||||
import type { CheckoutConfirmationRequestDTO } from '../../../application/dto/CheckoutConfirmationRequestDTO';
|
||||
import { CheckoutConfirmation } from '../../../domain/value-objects/CheckoutConfirmation';
|
||||
|
||||
export class ElectronCheckoutConfirmationAdapter implements ICheckoutConfirmationPort {
|
||||
export class ElectronCheckoutConfirmationAdapter implements CheckoutConfirmationPort {
|
||||
private mainWindow: BrowserWindow;
|
||||
private pendingConfirmation: {
|
||||
resolve: (confirmation: CheckoutConfirmation) => void;
|
||||
@@ -40,7 +41,7 @@ export class ElectronCheckoutConfirmationAdapter implements ICheckoutConfirmatio
|
||||
}
|
||||
|
||||
async requestCheckoutConfirmation(
|
||||
request: CheckoutConfirmationRequest
|
||||
request: CheckoutConfirmationRequestDTO
|
||||
): Promise<Result<CheckoutConfirmation>> {
|
||||
try {
|
||||
// Only allow one pending confirmation at a time
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ILogger, LogContext } from '../../../application/ports/ILogger';
|
||||
import type { LoggerPort } from '../../../application/ports/LoggerPort';
|
||||
import type { LogContext } from '../../../application/ports/LoggerContext';
|
||||
|
||||
export class NoOpLogAdapter implements ILogger {
|
||||
export class NoOpLogAdapter implements LoggerPort {
|
||||
debug(_message: string, _context?: LogContext): void {}
|
||||
|
||||
info(_message: string, _context?: LogContext): void {}
|
||||
@@ -11,7 +12,7 @@ export class NoOpLogAdapter implements ILogger {
|
||||
|
||||
fatal(_message: string, _error?: Error, _context?: LogContext): void {}
|
||||
|
||||
child(_context: LogContext): ILogger {
|
||||
child(_context: LogContext): LoggerPort {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { ILogger, LogContext, LogLevel } from '../../../application/ports/ILogger';
|
||||
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
|
||||
import type { LogContext } from '@gridpilot/automation/application/ports/LoggerContext';
|
||||
import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel';
|
||||
import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig';
|
||||
|
||||
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
@@ -18,7 +20,7 @@ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
*
|
||||
* This provides structured JSON logging to stdout with the same interface.
|
||||
*/
|
||||
export class PinoLogAdapter implements ILogger {
|
||||
export class PinoLogAdapter implements LoggerPort {
|
||||
private readonly config: LoggingEnvironmentConfig;
|
||||
private readonly baseContext: LogContext;
|
||||
private readonly levelPriority: number;
|
||||
@@ -106,7 +108,7 @@ export class PinoLogAdapter implements ILogger {
|
||||
this.log('fatal', message, context, error);
|
||||
}
|
||||
|
||||
child(context: LogContext): ILogger {
|
||||
child(context: LogContext): LoggerPort {
|
||||
return new PinoLogAdapter(this.config, { ...this.baseContext, ...context });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LogLevel } from '../../application/ports/ILogger';
|
||||
import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel';
|
||||
|
||||
export type LogEnvironment = 'development' | 'production' | 'test';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
|
||||
import { SessionStateValue } from '@gridpilot/automation/domain/value-objects/SessionState';
|
||||
import { ISessionRepository } from '../../application/ports/ISessionRepository';
|
||||
import { AutomationSession } from '../../domain/entities/AutomationSession';
|
||||
import { SessionStateValue } from '../../domain/value-objects/SessionState';
|
||||
import type { SessionRepositoryPort } from '../../application/ports/SessionRepositoryPort';
|
||||
|
||||
export class InMemorySessionRepository implements ISessionRepository {
|
||||
export class InMemorySessionRepository implements SessionRepositoryPort {
|
||||
private sessions: Map<string, AutomationSession> = new Map();
|
||||
|
||||
async save(session: AutomationSession): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"rootDir": "..",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": false
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './src/faker';
|
||||
export * from './src/images';
|
||||
export * from './src/racing/StaticRacingSeed';
|
||||
@@ -1,39 +0,0 @@
|
||||
export * from './memberships';
|
||||
export * from './registrations';
|
||||
// Re-export selected team helpers but avoid getCurrentDriverId to prevent conflicts.
|
||||
export {
|
||||
getAllTeams,
|
||||
getTeam,
|
||||
getTeamMembers,
|
||||
getTeamMembership,
|
||||
getTeamJoinRequests,
|
||||
getDriverTeam,
|
||||
isTeamOwnerOrManager,
|
||||
removeTeamMember,
|
||||
updateTeamMemberRole,
|
||||
createTeam,
|
||||
joinTeam,
|
||||
requestToJoinTeam,
|
||||
leaveTeam,
|
||||
approveTeamJoinRequest,
|
||||
rejectTeamJoinRequest,
|
||||
updateTeam,
|
||||
} from './teams';
|
||||
|
||||
// Re-export domain types for legacy callers (type-only)
|
||||
export type {
|
||||
LeagueMembership,
|
||||
MembershipRole,
|
||||
MembershipStatus,
|
||||
JoinRequest,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
|
||||
export type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
|
||||
export type {
|
||||
Team,
|
||||
TeamMembership,
|
||||
TeamJoinRequest,
|
||||
TeamRole,
|
||||
TeamMembershipStatus,
|
||||
} from '@gridpilot/racing/domain/entities/Team';
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "@gridpilot/racing-application",
|
||||
"version": "0.1.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"dependencies": {
|
||||
"@gridpilot/racing": "*"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './src/StaticRacingSeed';
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "@gridpilot/racing-demo-infrastructure",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"dependencies": {
|
||||
"@gridpilot/racing": "0.1.0",
|
||||
"@gridpilot/social": "0.1.0",
|
||||
"@gridpilot/demo-support": "0.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,508 +0,0 @@
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import { Result } from '@gridpilot/racing/domain/entities/Result';
|
||||
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
|
||||
|
||||
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
|
||||
import type { FriendDTO } from '@gridpilot/social/application/dto/FriendDTO';
|
||||
import { faker } from '@gridpilot/demo-support';
|
||||
import { getTeamLogo, getLeagueBanner, getDriverAvatar } from '@gridpilot/demo-support';
|
||||
|
||||
export type RacingMembership = {
|
||||
driverId: string;
|
||||
leagueId: string;
|
||||
teamId?: string;
|
||||
};
|
||||
|
||||
export type Friendship = {
|
||||
driverId: string;
|
||||
friendId: string;
|
||||
};
|
||||
|
||||
export interface DemoTeamDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
logoUrl: string;
|
||||
primaryLeagueId: string;
|
||||
memberCount: number;
|
||||
}
|
||||
|
||||
export type RacingSeedData = {
|
||||
drivers: Driver[];
|
||||
leagues: League[];
|
||||
races: Race[];
|
||||
results: Result[];
|
||||
standings: Standing[];
|
||||
memberships: RacingMembership[];
|
||||
friendships: Friendship[];
|
||||
feedEvents: FeedItem[];
|
||||
teams: DemoTeamDTO[];
|
||||
};
|
||||
|
||||
const POINTS_TABLE: Record<number, number> = {
|
||||
1: 25,
|
||||
2: 18,
|
||||
3: 15,
|
||||
4: 12,
|
||||
5: 10,
|
||||
6: 8,
|
||||
7: 6,
|
||||
8: 4,
|
||||
9: 2,
|
||||
10: 1,
|
||||
};
|
||||
|
||||
function pickOne<T>(items: readonly T[]): T {
|
||||
return items[Math.floor(faker.number.int({ min: 0, max: items.length - 1 }))];
|
||||
}
|
||||
|
||||
function createDrivers(count: number): Driver[] {
|
||||
const drivers: Driver[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `driver-${i + 1}`;
|
||||
const name = faker.person.fullName();
|
||||
const country = faker.location.countryCode('alpha-2');
|
||||
const iracingId = faker.string.numeric(6);
|
||||
|
||||
drivers.push(
|
||||
Driver.create({
|
||||
id,
|
||||
iracingId,
|
||||
name,
|
||||
country,
|
||||
bio: faker.lorem.sentence(),
|
||||
joinedAt: faker.date.past(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return drivers;
|
||||
}
|
||||
|
||||
function createLeagues(ownerIds: string[]): League[] {
|
||||
const leagueNames = [
|
||||
'Global GT Masters',
|
||||
'Midnight Endurance Series',
|
||||
'Virtual Touring Cup',
|
||||
'Sprint Challenge League',
|
||||
'Club Racers Collective',
|
||||
'Sim Racing Alliance',
|
||||
'Pacific Time Attack',
|
||||
'Nordic Night Series',
|
||||
];
|
||||
|
||||
const leagues: League[] = [];
|
||||
const leagueCount = 6 + faker.number.int({ min: 0, max: 2 });
|
||||
|
||||
for (let i = 0; i < leagueCount; i++) {
|
||||
const id = `league-${i + 1}`;
|
||||
const name = leagueNames[i] ?? faker.company.name();
|
||||
const ownerId = pickOne(ownerIds);
|
||||
|
||||
const settings = {
|
||||
pointsSystem: faker.helpers.arrayElement(['f1-2024', 'indycar']),
|
||||
sessionDuration: faker.helpers.arrayElement([45, 60, 90, 120]),
|
||||
qualifyingFormat: faker.helpers.arrayElement(['open', 'single-lap']),
|
||||
};
|
||||
|
||||
leagues.push(
|
||||
League.create({
|
||||
id,
|
||||
name,
|
||||
description: faker.lorem.sentence(),
|
||||
ownerId,
|
||||
settings,
|
||||
createdAt: faker.date.past(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return leagues;
|
||||
}
|
||||
|
||||
function createTeams(leagues: League[]): DemoTeamDTO[] {
|
||||
const teams: DemoTeamDTO[] = [];
|
||||
const teamCount = 24 + faker.number.int({ min: 0, max: 12 });
|
||||
|
||||
for (let i = 0; i < teamCount; i++) {
|
||||
const id = `team-${i + 1}`;
|
||||
const primaryLeague = pickOne(leagues);
|
||||
const name = faker.company.name();
|
||||
const tag = faker.string.alpha({ length: 4 }).toUpperCase();
|
||||
const memberCount = faker.number.int({ min: 2, max: 8 });
|
||||
|
||||
teams.push({
|
||||
id,
|
||||
name,
|
||||
tag,
|
||||
description: faker.lorem.sentence(),
|
||||
logoUrl: getTeamLogo(id),
|
||||
primaryLeagueId: primaryLeague.id,
|
||||
memberCount,
|
||||
});
|
||||
}
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
function createMemberships(
|
||||
drivers: Driver[],
|
||||
leagues: League[],
|
||||
teams: DemoTeamDTO[],
|
||||
): RacingMembership[] {
|
||||
const memberships: RacingMembership[] = [];
|
||||
|
||||
const teamsByLeague = new Map<string, DemoTeamDTO[]>();
|
||||
teams.forEach((team) => {
|
||||
const list = teamsByLeague.get(team.primaryLeagueId) ?? [];
|
||||
list.push(team);
|
||||
teamsByLeague.set(team.primaryLeagueId, list);
|
||||
});
|
||||
|
||||
drivers.forEach((driver) => {
|
||||
// Each driver participates in 1–3 leagues
|
||||
const leagueSampleSize = faker.number.int({ min: 1, max: Math.min(3, leagues.length) });
|
||||
const shuffledLeagues = faker.helpers.shuffle(leagues).slice(0, leagueSampleSize);
|
||||
|
||||
shuffledLeagues.forEach((league) => {
|
||||
const leagueTeams = teamsByLeague.get(league.id) ?? [];
|
||||
const team =
|
||||
leagueTeams.length > 0 && faker.datatype.boolean()
|
||||
? pickOne(leagueTeams)
|
||||
: undefined;
|
||||
|
||||
memberships.push({
|
||||
driverId: driver.id,
|
||||
leagueId: league.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
function createRaces(leagues: League[]): Race[] {
|
||||
const races: Race[] = [];
|
||||
const raceCount = 60 + faker.number.int({ min: 0, max: 20 });
|
||||
|
||||
const tracks = [
|
||||
'Monza GP',
|
||||
'Spa-Francorchamps',
|
||||
'Suzuka',
|
||||
'Mount Panorama',
|
||||
'Silverstone GP',
|
||||
'Interlagos',
|
||||
'Imola',
|
||||
'Laguna Seca',
|
||||
];
|
||||
|
||||
const cars = [
|
||||
'GT3 – Porsche 911',
|
||||
'GT3 – BMW M4',
|
||||
'LMP3 Prototype',
|
||||
'GT4 – Alpine',
|
||||
'Touring – Civic',
|
||||
];
|
||||
|
||||
const baseDate = new Date();
|
||||
|
||||
for (let i = 0; i < raceCount; i++) {
|
||||
const id = `race-${i + 1}`;
|
||||
const league = pickOne(leagues);
|
||||
const offsetDays = faker.number.int({ min: -30, max: 45 });
|
||||
const scheduledAt = new Date(baseDate.getTime() + offsetDays * 24 * 60 * 60 * 1000);
|
||||
const status = scheduledAt.getTime() < baseDate.getTime() ? 'completed' : 'scheduled';
|
||||
|
||||
races.push(
|
||||
Race.create({
|
||||
id,
|
||||
leagueId: league.id,
|
||||
scheduledAt,
|
||||
track: faker.helpers.arrayElement(tracks),
|
||||
car: faker.helpers.arrayElement(cars),
|
||||
sessionType: 'race',
|
||||
status,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return races;
|
||||
}
|
||||
|
||||
function createResults(drivers: Driver[], races: Race[]): Result[] {
|
||||
const results: Result[] = [];
|
||||
|
||||
const completedRaces = races.filter((race) => race.status === 'completed');
|
||||
|
||||
completedRaces.forEach((race) => {
|
||||
const participantCount = faker.number.int({ min: 20, max: 32 });
|
||||
const shuffledDrivers = faker.helpers.shuffle(drivers).slice(0, participantCount);
|
||||
|
||||
shuffledDrivers.forEach((driver, index) => {
|
||||
const position = index + 1;
|
||||
const startPosition = faker.number.int({ min: 1, max: participantCount });
|
||||
const fastestLap = 90_000 + index * 250 + faker.number.int({ min: 0, max: 2_000 });
|
||||
const incidents = faker.number.int({ min: 0, max: 6 });
|
||||
|
||||
results.push(
|
||||
Result.create({
|
||||
id: `${race.id}-${driver.id}`,
|
||||
raceId: race.id,
|
||||
driverId: driver.id,
|
||||
position,
|
||||
startPosition,
|
||||
fastestLap,
|
||||
incidents,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function createStandings(leagues: League[], results: Result[]): Standing[] {
|
||||
const standingsByLeague = new Map<string, Standing[]>();
|
||||
|
||||
leagues.forEach((league) => {
|
||||
const leagueRaceIds = new Set(
|
||||
results
|
||||
.filter((result) => {
|
||||
return result.raceId.startsWith('race-');
|
||||
})
|
||||
.map((result) => result.raceId),
|
||||
);
|
||||
|
||||
const leagueResults = results.filter((result) => leagueRaceIds.has(result.raceId));
|
||||
|
||||
const standingsMap = new Map<string, Standing>();
|
||||
|
||||
leagueResults.forEach((result) => {
|
||||
const key = result.driverId;
|
||||
let standing = standingsMap.get(key);
|
||||
|
||||
if (!standing) {
|
||||
standing = Standing.create({
|
||||
leagueId: league.id,
|
||||
driverId: result.driverId,
|
||||
});
|
||||
}
|
||||
|
||||
standing = standing.addRaceResult(result.position, POINTS_TABLE);
|
||||
standingsMap.set(key, standing);
|
||||
});
|
||||
|
||||
const sortedStandings = Array.from(standingsMap.values()).sort((a, b) => {
|
||||
if (b.points !== a.points) {
|
||||
return b.points - a.points;
|
||||
}
|
||||
if (b.wins !== a.wins) {
|
||||
return b.wins - a.wins;
|
||||
}
|
||||
return b.racesCompleted - a.racesCompleted;
|
||||
});
|
||||
|
||||
const finalizedStandings = sortedStandings.map((standing, index) =>
|
||||
standing.updatePosition(index + 1),
|
||||
);
|
||||
|
||||
standingsByLeague.set(league.id, finalizedStandings);
|
||||
});
|
||||
|
||||
return Array.from(standingsByLeague.values()).flat();
|
||||
}
|
||||
|
||||
function createFriendships(drivers: Driver[]): Friendship[] {
|
||||
const friendships: Friendship[] = [];
|
||||
|
||||
drivers.forEach((driver, index) => {
|
||||
const friendCount = faker.number.int({ min: 3, max: 8 });
|
||||
for (let offset = 1; offset <= friendCount; offset++) {
|
||||
const friendIndex = (index + offset) % drivers.length;
|
||||
const friend = drivers[friendIndex];
|
||||
if (friend.id === driver.id) continue;
|
||||
|
||||
friendships.push({
|
||||
driverId: driver.id,
|
||||
friendId: friend.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return friendships;
|
||||
}
|
||||
|
||||
function createFeedEvents(
|
||||
drivers: Driver[],
|
||||
leagues: League[],
|
||||
races: Race[],
|
||||
friendships: Friendship[],
|
||||
): FeedItem[] {
|
||||
const events: FeedItem[] = [];
|
||||
const now = new Date();
|
||||
const completedRaces = races.filter((race) => race.status === 'completed');
|
||||
const globalDrivers = faker.helpers.shuffle(drivers).slice(0, 10);
|
||||
|
||||
globalDrivers.forEach((driver, index) => {
|
||||
const league = pickOne(leagues);
|
||||
const race = completedRaces[index % Math.max(1, completedRaces.length)];
|
||||
const minutesAgo = 15 + index * 10;
|
||||
|
||||
const baseTimestamp = new Date(now.getTime() - minutesAgo * 60 * 1000);
|
||||
|
||||
events.push({
|
||||
id: `friend-joined-league:${driver.id}:${minutesAgo}`,
|
||||
type: 'friend-joined-league',
|
||||
timestamp: baseTimestamp,
|
||||
actorDriverId: driver.id,
|
||||
leagueId: league.id,
|
||||
headline: `${driver.name} joined ${league.name}`,
|
||||
body: 'They are now registered for the full season.',
|
||||
ctaLabel: 'View league',
|
||||
ctaHref: `/leagues/${league.id}`,
|
||||
});
|
||||
|
||||
events.push({
|
||||
id: `friend-finished-race:${driver.id}:${minutesAgo}`,
|
||||
type: 'friend-finished-race',
|
||||
timestamp: new Date(baseTimestamp.getTime() - 10 * 60 * 1000),
|
||||
actorDriverId: driver.id,
|
||||
leagueId: race.leagueId,
|
||||
raceId: race.id,
|
||||
position: (index % 5) + 1,
|
||||
headline: `${driver.name} finished P${(index % 5) + 1} at ${race.track}`,
|
||||
body: `${driver.name} secured a strong result in ${race.car}.`,
|
||||
ctaLabel: 'View results',
|
||||
ctaHref: `/races/${race.id}/results`,
|
||||
});
|
||||
|
||||
events.push({
|
||||
id: `league-highlight:${league.id}:${minutesAgo}`,
|
||||
type: 'league-highlight',
|
||||
timestamp: new Date(baseTimestamp.getTime() - 30 * 60 * 1000),
|
||||
leagueId: league.id,
|
||||
headline: `${league.name} active with ${drivers.length}+ drivers`,
|
||||
body: 'Participation is growing. Perfect time to join the grid.',
|
||||
ctaLabel: 'Explore league',
|
||||
ctaHref: `/leagues/${league.id}`,
|
||||
});
|
||||
});
|
||||
|
||||
const sorted = events
|
||||
.slice()
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export function createStaticRacingSeed(seed: number): RacingSeedData {
|
||||
faker.seed(seed);
|
||||
|
||||
const drivers = createDrivers(96);
|
||||
const leagues = createLeagues(drivers.slice(0, 12).map((d) => d.id));
|
||||
const teams = createTeams(leagues);
|
||||
const memberships = createMemberships(drivers, leagues, teams);
|
||||
const races = createRaces(leagues);
|
||||
const results = createResults(drivers, races);
|
||||
const friendships = createFriendships(drivers);
|
||||
const feedEvents = createFeedEvents(drivers, leagues, races, friendships);
|
||||
const standings = createStandings(leagues, results);
|
||||
|
||||
return {
|
||||
drivers,
|
||||
leagues,
|
||||
races,
|
||||
results,
|
||||
standings,
|
||||
memberships,
|
||||
friendships,
|
||||
feedEvents,
|
||||
teams,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton seed used by website demo helpers.
|
||||
* This mirrors the previous apps/website/lib/demo-data/index.ts behavior.
|
||||
*/
|
||||
const staticSeed = createStaticRacingSeed(42);
|
||||
|
||||
export const drivers = staticSeed.drivers;
|
||||
export const leagues = staticSeed.leagues;
|
||||
export const races = staticSeed.races;
|
||||
export const results = staticSeed.results;
|
||||
export const standings = staticSeed.standings;
|
||||
export const teams = staticSeed.teams;
|
||||
export const memberships = staticSeed.memberships;
|
||||
export const friendships = staticSeed.friendships;
|
||||
export const feedEvents = staticSeed.feedEvents;
|
||||
|
||||
/**
|
||||
* Derived friend DTOs for UI consumption.
|
||||
* This preserves the previous demo-data `friends` shape.
|
||||
*/
|
||||
export const friends: FriendDTO[] = staticSeed.drivers.map((driver) => ({
|
||||
driverId: driver.id,
|
||||
displayName: driver.name,
|
||||
avatarUrl: getDriverAvatar(driver.id),
|
||||
isOnline: true,
|
||||
lastSeen: new Date(),
|
||||
primaryLeagueId: staticSeed.memberships.find((m) => m.driverId === driver.id)?.leagueId,
|
||||
primaryTeamId: staticSeed.memberships.find((m) => m.driverId === driver.id)?.teamId,
|
||||
}));
|
||||
|
||||
export const topLeagues = leagues.map((league) => ({
|
||||
...league,
|
||||
bannerUrl: getLeagueBanner(league.id),
|
||||
}));
|
||||
|
||||
export type RaceWithResultsDTO = {
|
||||
raceId: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: Date;
|
||||
winnerDriverId: string;
|
||||
winnerName: string;
|
||||
};
|
||||
|
||||
export function getUpcomingRaces(limit?: number): readonly Race[] {
|
||||
const upcoming = races.filter((race) => race.status === 'scheduled');
|
||||
const sorted = upcoming
|
||||
.slice()
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
|
||||
}
|
||||
|
||||
export function getLatestResults(limit?: number): readonly RaceWithResultsDTO[] {
|
||||
const completedRaces = races.filter((race) => race.status === 'completed');
|
||||
|
||||
const joined = completedRaces.map((race) => {
|
||||
const raceResults = results
|
||||
.filter((result) => result.raceId === race.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.position - b.position);
|
||||
const winner = raceResults[0];
|
||||
const winnerDriver =
|
||||
winner && drivers.find((driver) => driver.id === winner.driverId);
|
||||
|
||||
return {
|
||||
raceId: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
winnerDriverId: winner?.driverId ?? '',
|
||||
winnerName: winnerDriver?.name ?? 'Winner',
|
||||
};
|
||||
});
|
||||
|
||||
const sorted = joined
|
||||
.slice()
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime());
|
||||
|
||||
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "@gridpilot/racing-infrastructure",
|
||||
"version": "0.1.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"dependencies": {
|
||||
"@gridpilot/racing": "*",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
13
packages/racing/application/dto/CreateTeamCommandDTO.ts
Normal file
13
packages/racing/application/dto/CreateTeamCommandDTO.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
|
||||
export interface CreateTeamCommandDTO {
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
}
|
||||
|
||||
export interface CreateTeamResultDTO {
|
||||
team: Team;
|
||||
}
|
||||
8
packages/racing/application/dto/DriverDTO.ts
Normal file
8
packages/racing/application/dto/DriverDTO.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type DriverDTO = {
|
||||
id: string;
|
||||
iracingId: string;
|
||||
name: string;
|
||||
country: string;
|
||||
bio?: string;
|
||||
joinedAt: string;
|
||||
};
|
||||
4
packages/racing/application/dto/JoinLeagueCommandDTO.ts
Normal file
4
packages/racing/application/dto/JoinLeagueCommandDTO.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface JoinLeagueCommandDTO {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
}
|
||||
13
packages/racing/application/dto/LeagueDTO.ts
Normal file
13
packages/racing/application/dto/LeagueDTO.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type LeagueDTO = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024' | 'indycar' | 'custom';
|
||||
sessionDuration?: number;
|
||||
qualifyingFormat?: 'single-lap' | 'open';
|
||||
customPoints?: Record<number, number>;
|
||||
};
|
||||
createdAt: string;
|
||||
};
|
||||
9
packages/racing/application/dto/RaceDTO.ts
Normal file
9
packages/racing/application/dto/RaceDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type RaceDTO = {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
scheduledAt: string;
|
||||
track: string;
|
||||
car: string;
|
||||
sessionType: 'practice' | 'qualifying' | 'race';
|
||||
status: 'scheduled' | 'completed' | 'cancelled';
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface IsDriverRegisteredForRaceQueryParamsDTO {
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface GetRaceRegistrationsQueryParamsDTO {
|
||||
raceId: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface RegisterForRaceCommandDTO {
|
||||
raceId: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
}
|
||||
9
packages/racing/application/dto/ResultDTO.ts
Normal file
9
packages/racing/application/dto/ResultDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type ResultDTO = {
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
fastestLap: number;
|
||||
incidents: number;
|
||||
startPosition: number;
|
||||
};
|
||||
8
packages/racing/application/dto/StandingDTO.ts
Normal file
8
packages/racing/application/dto/StandingDTO.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type StandingDTO = {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
points: number;
|
||||
wins: number;
|
||||
position: number;
|
||||
racesCompleted: number;
|
||||
};
|
||||
54
packages/racing/application/dto/TeamCommandAndQueryDTO.ts
Normal file
54
packages/racing/application/dto/TeamCommandAndQueryDTO.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Team, TeamJoinRequest, TeamMembership } from '../../domain/entities/Team';
|
||||
|
||||
export interface JoinTeamCommandDTO {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface LeaveTeamCommandDTO {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface ApproveTeamJoinRequestCommandDTO {
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface RejectTeamJoinRequestCommandDTO {
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface UpdateTeamCommandDTO {
|
||||
teamId: string;
|
||||
updates: Partial<Pick<Team, 'name' | 'tag' | 'description' | 'leagues'>>;
|
||||
updatedBy: string;
|
||||
}
|
||||
|
||||
export type GetAllTeamsQueryResultDTO = Team[];
|
||||
|
||||
export interface GetTeamDetailsQueryParamsDTO {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface GetTeamDetailsQueryResultDTO {
|
||||
team: Team;
|
||||
membership: TeamMembership | null;
|
||||
}
|
||||
|
||||
export interface GetTeamMembersQueryParamsDTO {
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
export interface GetTeamJoinRequestsQueryParamsDTO {
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
export interface GetDriverTeamQueryParamsDTO {
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface GetDriverTeamQueryResultDTO {
|
||||
team: Team;
|
||||
membership: TeamMembership;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface WithdrawFromRaceCommandDTO {
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
}
|
||||
@@ -1,25 +1,19 @@
|
||||
export * from './services/memberships';
|
||||
export * from './services/registrations';
|
||||
|
||||
// Re-export selected team helpers but avoid getCurrentDriverId to prevent conflicts.
|
||||
export {
|
||||
getAllTeams,
|
||||
getTeam,
|
||||
getTeamMembers,
|
||||
getTeamMembership,
|
||||
getTeamJoinRequests,
|
||||
getDriverTeam,
|
||||
isTeamOwnerOrManager,
|
||||
removeTeamMember,
|
||||
updateTeamMemberRole,
|
||||
createTeam,
|
||||
joinTeam,
|
||||
requestToJoinTeam,
|
||||
leaveTeam,
|
||||
approveTeamJoinRequest,
|
||||
rejectTeamJoinRequest,
|
||||
updateTeam,
|
||||
} from './services/teams';
|
||||
export * from './use-cases/JoinLeagueUseCase';
|
||||
export * from './use-cases/RegisterForRaceUseCase';
|
||||
export * from './use-cases/WithdrawFromRaceUseCase';
|
||||
export * from './use-cases/IsDriverRegisteredForRaceQuery';
|
||||
export * from './use-cases/GetRaceRegistrationsQuery';
|
||||
export * from './use-cases/CreateTeamUseCase';
|
||||
export * from './use-cases/JoinTeamUseCase';
|
||||
export * from './use-cases/LeaveTeamUseCase';
|
||||
export * from './use-cases/ApproveTeamJoinRequestUseCase';
|
||||
export * from './use-cases/RejectTeamJoinRequestUseCase';
|
||||
export * from './use-cases/UpdateTeamUseCase';
|
||||
export * from './use-cases/GetAllTeamsQuery';
|
||||
export * from './use-cases/GetTeamDetailsQuery';
|
||||
export * from './use-cases/GetTeamMembersQuery';
|
||||
export * from './use-cases/GetTeamJoinRequestsQuery';
|
||||
export * from './use-cases/GetDriverTeamQuery';
|
||||
|
||||
// Re-export domain types for legacy callers (type-only)
|
||||
export type {
|
||||
@@ -27,9 +21,9 @@ export type {
|
||||
MembershipRole,
|
||||
MembershipStatus,
|
||||
JoinRequest,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
} from '../domain/entities/LeagueMembership';
|
||||
|
||||
export type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
export type { RaceRegistration } from '../domain/entities/RaceRegistration';
|
||||
|
||||
export type {
|
||||
Team,
|
||||
@@ -37,12 +31,10 @@ export type {
|
||||
TeamJoinRequest,
|
||||
TeamRole,
|
||||
TeamMembershipStatus,
|
||||
} from '@gridpilot/racing/domain/entities/Team';
|
||||
} from '../domain/entities/Team';
|
||||
|
||||
export type {
|
||||
DriverDTO,
|
||||
LeagueDTO,
|
||||
RaceDTO,
|
||||
ResultDTO,
|
||||
StandingDTO,
|
||||
} from './mappers/EntityMappers';
|
||||
export type { DriverDTO } from './dto/DriverDTO';
|
||||
export type { LeagueDTO } from './dto/LeagueDTO';
|
||||
export type { RaceDTO } from './dto/RaceDTO';
|
||||
export type { ResultDTO } from './dto/ResultDTO';
|
||||
export type { StandingDTO } from './dto/StandingDTO';
|
||||
@@ -5,63 +5,16 @@
|
||||
* These mappers handle the Server Component -> Client Component boundary in Next.js 15.
|
||||
*/
|
||||
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import { Result } from '@gridpilot/racing/domain/entities/Result';
|
||||
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
|
||||
|
||||
export type DriverDTO = {
|
||||
id: string;
|
||||
iracingId: string;
|
||||
name: string;
|
||||
country: string;
|
||||
bio?: string;
|
||||
joinedAt: string;
|
||||
};
|
||||
|
||||
export type LeagueDTO = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024' | 'indycar' | 'custom';
|
||||
sessionDuration?: number;
|
||||
qualifyingFormat?: 'single-lap' | 'open';
|
||||
customPoints?: Record<number, number>;
|
||||
};
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type RaceDTO = {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
scheduledAt: string;
|
||||
track: string;
|
||||
car: string;
|
||||
sessionType: 'practice' | 'qualifying' | 'race';
|
||||
status: 'scheduled' | 'completed' | 'cancelled';
|
||||
};
|
||||
|
||||
export type ResultDTO = {
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
fastestLap: number;
|
||||
incidents: number;
|
||||
startPosition: number;
|
||||
};
|
||||
|
||||
export type StandingDTO = {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
points: number;
|
||||
wins: number;
|
||||
position: number;
|
||||
racesCompleted: number;
|
||||
};
|
||||
import { Driver } from '../../domain/entities/Driver';
|
||||
import { League } from '../../domain/entities/League';
|
||||
import { Race } from '../../domain/entities/Race';
|
||||
import { Result } from '../../domain/entities/Result';
|
||||
import { Standing } from '../../domain/entities/Standing';
|
||||
import type { DriverDTO } from '../dto/DriverDTO';
|
||||
import type { LeagueDTO } from '../dto/LeagueDTO';
|
||||
import type { RaceDTO } from '../dto/RaceDTO';
|
||||
import type { ResultDTO } from '../dto/ResultDTO';
|
||||
import type { StandingDTO } from '../dto/StandingDTO';
|
||||
|
||||
export class EntityMappers {
|
||||
static toDriverDTO(driver: Driver | null): DriverDTO | null {
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
/**
|
||||
* In-memory league membership data for alpha prototype
|
||||
*/
|
||||
|
||||
import {
|
||||
MembershipRole,
|
||||
MembershipStatus,
|
||||
LeagueMembership,
|
||||
JoinRequest,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
|
||||
// In-memory storage
|
||||
let memberships: LeagueMembership[] = [];
|
||||
let joinRequests: JoinRequest[] = [];
|
||||
|
||||
// Current driver ID (matches the one in di-container)
|
||||
const CURRENT_DRIVER_ID = 'driver-1';
|
||||
|
||||
// Initialize with seed data
|
||||
export function initializeMembershipData() {
|
||||
memberships = [
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
driverId: CURRENT_DRIVER_ID,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-01-15'),
|
||||
},
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-2',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-02-01'),
|
||||
},
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-3',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-02-15'),
|
||||
},
|
||||
];
|
||||
|
||||
joinRequests = [];
|
||||
}
|
||||
|
||||
// Get membership for a driver in a league
|
||||
export function getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
||||
return memberships.find(m => m.leagueId === leagueId && m.driverId === driverId) || null;
|
||||
}
|
||||
|
||||
// Get all members for a league
|
||||
export function getLeagueMembers(leagueId: string): LeagueMembership[] {
|
||||
return memberships.filter(m => m.leagueId === leagueId && m.status === 'active');
|
||||
}
|
||||
|
||||
// Get pending join requests for a league
|
||||
export function getJoinRequests(leagueId: string): JoinRequest[] {
|
||||
return joinRequests.filter(r => r.leagueId === leagueId);
|
||||
}
|
||||
|
||||
// Join a league
|
||||
export function joinLeague(leagueId: string, driverId: string): void {
|
||||
const existing = getMembership(leagueId, driverId);
|
||||
if (existing) {
|
||||
throw new Error('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
memberships.push({
|
||||
leagueId,
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
// Request to join a league (for invite-only leagues)
|
||||
export function requestToJoin(leagueId: string, driverId: string, message?: string): void {
|
||||
const existing = getMembership(leagueId, driverId);
|
||||
if (existing) {
|
||||
throw new Error('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
const existingRequest = joinRequests.find(r => r.leagueId === leagueId && r.driverId === driverId);
|
||||
if (existingRequest) {
|
||||
throw new Error('Join request already pending');
|
||||
}
|
||||
|
||||
joinRequests.push({
|
||||
id: `request-${Date.now()}`,
|
||||
leagueId,
|
||||
driverId,
|
||||
requestedAt: new Date(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
// Leave a league
|
||||
export function leaveLeague(leagueId: string, driverId: string): void {
|
||||
const membership = getMembership(leagueId, driverId);
|
||||
if (!membership) {
|
||||
throw new Error('Not a member of this league');
|
||||
}
|
||||
|
||||
if (membership.role === 'owner') {
|
||||
throw new Error('League owner cannot leave. Transfer ownership first.');
|
||||
}
|
||||
|
||||
memberships = memberships.filter(m => !(m.leagueId === leagueId && m.driverId === driverId));
|
||||
}
|
||||
|
||||
// Approve join request
|
||||
export function approveJoinRequest(requestId: string): void {
|
||||
const request = joinRequests.find(r => r.id === requestId);
|
||||
if (!request) {
|
||||
throw new Error('Join request not found');
|
||||
}
|
||||
|
||||
memberships.push({
|
||||
leagueId: request.leagueId,
|
||||
driverId: request.driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
|
||||
joinRequests = joinRequests.filter(r => r.id !== requestId);
|
||||
}
|
||||
|
||||
// Reject join request
|
||||
export function rejectJoinRequest(requestId: string): void {
|
||||
joinRequests = joinRequests.filter(r => r.id !== requestId);
|
||||
}
|
||||
|
||||
// Remove member (admin action)
|
||||
export function removeMember(leagueId: string, driverId: string, removedBy: string): void {
|
||||
const removerMembership = getMembership(leagueId, removedBy);
|
||||
if (!removerMembership || (removerMembership.role !== 'owner' && removerMembership.role !== 'admin')) {
|
||||
throw new Error('Only owners and admins can remove members');
|
||||
}
|
||||
|
||||
const targetMembership = getMembership(leagueId, driverId);
|
||||
if (!targetMembership) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
|
||||
if (targetMembership.role === 'owner') {
|
||||
throw new Error('Cannot remove league owner');
|
||||
}
|
||||
|
||||
memberships = memberships.filter(m => !(m.leagueId === leagueId && m.driverId === driverId));
|
||||
}
|
||||
|
||||
// Update member role
|
||||
export function updateMemberRole(
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
newRole: MembershipRole,
|
||||
updatedBy: string
|
||||
): void {
|
||||
const updaterMembership = getMembership(leagueId, updatedBy);
|
||||
if (!updaterMembership || updaterMembership.role !== 'owner') {
|
||||
throw new Error('Only league owner can change roles');
|
||||
}
|
||||
|
||||
const targetMembership = getMembership(leagueId, driverId);
|
||||
if (!targetMembership) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
|
||||
if (newRole === 'owner') {
|
||||
throw new Error('Use transfer ownership to change owner');
|
||||
}
|
||||
|
||||
memberships = memberships.map(m =>
|
||||
m.leagueId === leagueId && m.driverId === driverId
|
||||
? { ...m, role: newRole }
|
||||
: m
|
||||
);
|
||||
}
|
||||
|
||||
// Check if driver is owner or admin
|
||||
export function isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
|
||||
const membership = getMembership(leagueId, driverId);
|
||||
return membership?.role === 'owner' || membership?.role === 'admin';
|
||||
}
|
||||
|
||||
// Get current driver ID
|
||||
export function getCurrentDriverId(): string {
|
||||
return CURRENT_DRIVER_ID;
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
initializeMembershipData();
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* In-memory race registration data for alpha prototype
|
||||
*/
|
||||
|
||||
import { getMembership } from './memberships';
|
||||
|
||||
import { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
|
||||
// In-memory storage (Set for quick lookups)
|
||||
const registrations = new Map<string, Set<string>>(); // raceId -> Set of driverIds
|
||||
|
||||
/**
|
||||
* Generate registration key for storage
|
||||
*/
|
||||
function getRegistrationKey(raceId: string, driverId: string): string {
|
||||
return `${raceId}:${driverId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if driver is registered for a race
|
||||
*/
|
||||
export function isRegistered(raceId: string, driverId: string): boolean {
|
||||
const raceRegistrations = registrations.get(raceId);
|
||||
return raceRegistrations ? raceRegistrations.has(driverId) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered drivers for a race
|
||||
*/
|
||||
export function getRegisteredDrivers(raceId: string): string[] {
|
||||
const raceRegistrations = registrations.get(raceId);
|
||||
return raceRegistrations ? Array.from(raceRegistrations) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registration count for a race
|
||||
*/
|
||||
export function getRegistrationCount(raceId: string): number {
|
||||
const raceRegistrations = registrations.get(raceId);
|
||||
return raceRegistrations ? raceRegistrations.size : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register driver for a race
|
||||
* Validates league membership before registering
|
||||
*/
|
||||
export function registerForRace(
|
||||
raceId: string,
|
||||
driverId: string,
|
||||
leagueId: string
|
||||
): void {
|
||||
// Check if already registered
|
||||
if (isRegistered(raceId, driverId)) {
|
||||
throw new Error('Already registered for this race');
|
||||
}
|
||||
|
||||
// Validate league membership
|
||||
const membership = getMembership(leagueId, driverId);
|
||||
if (!membership || membership.status !== 'active') {
|
||||
throw new Error('Must be an active league member to register for races');
|
||||
}
|
||||
|
||||
// Add registration
|
||||
if (!registrations.has(raceId)) {
|
||||
registrations.set(raceId, new Set());
|
||||
}
|
||||
registrations.get(raceId)!.add(driverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw from a race
|
||||
*/
|
||||
export function withdrawFromRace(raceId: string, driverId: string): void {
|
||||
const raceRegistrations = registrations.get(raceId);
|
||||
if (!raceRegistrations || !raceRegistrations.has(driverId)) {
|
||||
throw new Error('Not registered for this race');
|
||||
}
|
||||
|
||||
raceRegistrations.delete(driverId);
|
||||
|
||||
// Clean up empty sets
|
||||
if (raceRegistrations.size === 0) {
|
||||
registrations.delete(raceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all races a driver is registered for
|
||||
*/
|
||||
export function getDriverRegistrations(driverId: string): string[] {
|
||||
const raceIds: string[] = [];
|
||||
|
||||
for (const [raceId, driverSet] of registrations.entries()) {
|
||||
if (driverSet.has(driverId)) {
|
||||
raceIds.push(raceId);
|
||||
}
|
||||
}
|
||||
|
||||
return raceIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registrations for a race (e.g., when race is cancelled)
|
||||
*/
|
||||
export function clearRaceRegistrations(raceId: string): void {
|
||||
registrations.delete(raceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with seed data
|
||||
*/
|
||||
export function initializeRegistrationData(): void {
|
||||
registrations.clear();
|
||||
|
||||
// Add some initial registrations for testing
|
||||
// Race 2 (Spa-Francorchamps - upcoming)
|
||||
registerForRace('race-2', 'driver-1', 'league-1');
|
||||
registerForRace('race-2', 'driver-2', 'league-1');
|
||||
registerForRace('race-2', 'driver-3', 'league-1');
|
||||
|
||||
// Race 3 (Nürburgring GP - upcoming)
|
||||
registerForRace('race-3', 'driver-1', 'league-1');
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
initializeRegistrationData();
|
||||
@@ -1,314 +0,0 @@
|
||||
/**
|
||||
* In-memory team data for alpha prototype
|
||||
*/
|
||||
|
||||
import {
|
||||
Team,
|
||||
TeamMembership,
|
||||
TeamJoinRequest,
|
||||
TeamRole,
|
||||
TeamMembershipStatus,
|
||||
} from '@gridpilot/racing/domain/entities/Team';
|
||||
|
||||
// In-memory storage
|
||||
let teams: Team[] = [];
|
||||
let teamMemberships: TeamMembership[] = [];
|
||||
let teamJoinRequests: TeamJoinRequest[] = [];
|
||||
|
||||
// Current driver ID (matches di-container)
|
||||
const CURRENT_DRIVER_ID = 'driver-1';
|
||||
|
||||
// Initialize with seed data
|
||||
export function initializeTeamData() {
|
||||
teams = [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Apex Racing',
|
||||
tag: 'APEX',
|
||||
description: 'Professional GT3 racing team competing at the highest level',
|
||||
ownerId: CURRENT_DRIVER_ID,
|
||||
leagues: ['league-1'],
|
||||
createdAt: new Date('2024-01-20'),
|
||||
},
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Speed Demons',
|
||||
tag: 'SPDM',
|
||||
description: 'Fast and furious racing with a competitive edge',
|
||||
ownerId: 'driver-2',
|
||||
leagues: ['league-1'],
|
||||
createdAt: new Date('2024-02-01'),
|
||||
},
|
||||
{
|
||||
id: 'team-3',
|
||||
name: 'Weekend Warriors',
|
||||
tag: 'WKND',
|
||||
description: 'Casual but competitive weekend racing',
|
||||
ownerId: 'driver-3',
|
||||
leagues: ['league-1'],
|
||||
createdAt: new Date('2024-02-10'),
|
||||
},
|
||||
];
|
||||
|
||||
teamMemberships = [
|
||||
{
|
||||
teamId: 'team-1',
|
||||
driverId: CURRENT_DRIVER_ID,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-01-20'),
|
||||
},
|
||||
{
|
||||
teamId: 'team-2',
|
||||
driverId: 'driver-2',
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-02-01'),
|
||||
},
|
||||
{
|
||||
teamId: 'team-3',
|
||||
driverId: 'driver-3',
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-02-10'),
|
||||
},
|
||||
];
|
||||
|
||||
teamJoinRequests = [];
|
||||
}
|
||||
|
||||
// Get all teams
|
||||
export function getAllTeams(): Team[] {
|
||||
return teams;
|
||||
}
|
||||
|
||||
// Get team by ID
|
||||
export function getTeam(teamId: string): Team | null {
|
||||
return teams.find(t => t.id === teamId) || null;
|
||||
}
|
||||
|
||||
// Get team membership for a driver
|
||||
export function getTeamMembership(teamId: string, driverId: string): TeamMembership | null {
|
||||
return teamMemberships.find(m => m.teamId === teamId && m.driverId === driverId) || null;
|
||||
}
|
||||
|
||||
// Get driver's team
|
||||
export function getDriverTeam(driverId: string): { team: Team; membership: TeamMembership } | null {
|
||||
const membership = teamMemberships.find(m => m.driverId === driverId && m.status === 'active');
|
||||
if (!membership) return null;
|
||||
|
||||
const team = getTeam(membership.teamId);
|
||||
if (!team) return null;
|
||||
|
||||
return { team, membership };
|
||||
}
|
||||
|
||||
// Get all members for a team
|
||||
export function getTeamMembers(teamId: string): TeamMembership[] {
|
||||
return teamMemberships.filter(m => m.teamId === teamId && m.status === 'active');
|
||||
}
|
||||
|
||||
// Get pending join requests for a team
|
||||
export function getTeamJoinRequests(teamId: string): TeamJoinRequest[] {
|
||||
return teamJoinRequests.filter(r => r.teamId === teamId);
|
||||
}
|
||||
|
||||
// Create a new team
|
||||
export function createTeam(
|
||||
name: string,
|
||||
tag: string,
|
||||
description: string,
|
||||
ownerId: string,
|
||||
leagues: string[]
|
||||
): Team {
|
||||
// Check if driver already has a team
|
||||
const existingTeam = getDriverTeam(ownerId);
|
||||
if (existingTeam) {
|
||||
throw new Error('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const team: Team = {
|
||||
id: `team-${Date.now()}`,
|
||||
name,
|
||||
tag,
|
||||
description,
|
||||
ownerId,
|
||||
leagues,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
teams.push(team);
|
||||
|
||||
// Auto-assign creator as owner
|
||||
teamMemberships.push({
|
||||
teamId: team.id,
|
||||
driverId: ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
// Join a team
|
||||
export function joinTeam(teamId: string, driverId: string): void {
|
||||
const existingTeam = getDriverTeam(driverId);
|
||||
if (existingTeam) {
|
||||
throw new Error('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const existing = getTeamMembership(teamId, driverId);
|
||||
if (existing) {
|
||||
throw new Error('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
teamMemberships.push({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
// Request to join a team
|
||||
export function requestToJoinTeam(teamId: string, driverId: string, message?: string): void {
|
||||
const existingTeam = getDriverTeam(driverId);
|
||||
if (existingTeam) {
|
||||
throw new Error('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const existing = getTeamMembership(teamId, driverId);
|
||||
if (existing) {
|
||||
throw new Error('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
const existingRequest = teamJoinRequests.find(r => r.teamId === teamId && r.driverId === driverId);
|
||||
if (existingRequest) {
|
||||
throw new Error('Join request already pending');
|
||||
}
|
||||
|
||||
teamJoinRequests.push({
|
||||
id: `team-request-${Date.now()}`,
|
||||
teamId,
|
||||
driverId,
|
||||
requestedAt: new Date(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
// Leave a team
|
||||
export function leaveTeam(teamId: string, driverId: string): void {
|
||||
const membership = getTeamMembership(teamId, driverId);
|
||||
if (!membership) {
|
||||
throw new Error('Not a member of this team');
|
||||
}
|
||||
|
||||
if (membership.role === 'owner') {
|
||||
throw new Error('Team owner cannot leave. Transfer ownership or disband team first.');
|
||||
}
|
||||
|
||||
teamMemberships = teamMemberships.filter(m => !(m.teamId === teamId && m.driverId === driverId));
|
||||
}
|
||||
|
||||
// Approve join request
|
||||
export function approveTeamJoinRequest(requestId: string): void {
|
||||
const request = teamJoinRequests.find(r => r.id === requestId);
|
||||
if (!request) {
|
||||
throw new Error('Join request not found');
|
||||
}
|
||||
|
||||
teamMemberships.push({
|
||||
teamId: request.teamId,
|
||||
driverId: request.driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
|
||||
teamJoinRequests = teamJoinRequests.filter(r => r.id !== requestId);
|
||||
}
|
||||
|
||||
// Reject join request
|
||||
export function rejectTeamJoinRequest(requestId: string): void {
|
||||
teamJoinRequests = teamJoinRequests.filter(r => r.id !== requestId);
|
||||
}
|
||||
|
||||
// Remove member (admin action)
|
||||
export function removeTeamMember(teamId: string, driverId: string, removedBy: string): void {
|
||||
const removerMembership = getTeamMembership(teamId, removedBy);
|
||||
if (!removerMembership || (removerMembership.role !== 'owner' && removerMembership.role !== 'manager')) {
|
||||
throw new Error('Only owners and managers can remove members');
|
||||
}
|
||||
|
||||
const targetMembership = getTeamMembership(teamId, driverId);
|
||||
if (!targetMembership) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
|
||||
if (targetMembership.role === 'owner') {
|
||||
throw new Error('Cannot remove team owner');
|
||||
}
|
||||
|
||||
teamMemberships = teamMemberships.filter(m => !(m.teamId === teamId && m.driverId === driverId));
|
||||
}
|
||||
|
||||
// Update member role
|
||||
export function updateTeamMemberRole(
|
||||
teamId: string,
|
||||
driverId: string,
|
||||
newRole: TeamRole,
|
||||
updatedBy: string
|
||||
): void {
|
||||
const updaterMembership = getTeamMembership(teamId, updatedBy);
|
||||
if (!updaterMembership || updaterMembership.role !== 'owner') {
|
||||
throw new Error('Only team owner can change roles');
|
||||
}
|
||||
|
||||
const targetMembership = getTeamMembership(teamId, driverId);
|
||||
if (!targetMembership) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
|
||||
if (newRole === 'owner') {
|
||||
throw new Error('Use transfer ownership to change owner');
|
||||
}
|
||||
|
||||
teamMemberships = teamMemberships.map(m =>
|
||||
m.teamId === teamId && m.driverId === driverId
|
||||
? { ...m, role: newRole }
|
||||
: m
|
||||
);
|
||||
}
|
||||
|
||||
// Check if driver is owner or manager
|
||||
export function isTeamOwnerOrManager(teamId: string, driverId: string): boolean {
|
||||
const membership = getTeamMembership(teamId, driverId);
|
||||
return membership?.role === 'owner' || membership?.role === 'manager';
|
||||
}
|
||||
|
||||
// Get current driver ID
|
||||
export function getCurrentDriverId(): string {
|
||||
return CURRENT_DRIVER_ID;
|
||||
}
|
||||
|
||||
// Update team info
|
||||
export function updateTeam(
|
||||
teamId: string,
|
||||
updates: Partial<Pick<Team, 'name' | 'tag' | 'description' | 'leagues'>>,
|
||||
updatedBy: string
|
||||
): void {
|
||||
if (!isTeamOwnerOrManager(teamId, updatedBy)) {
|
||||
throw new Error('Only owners and managers can update team info');
|
||||
}
|
||||
|
||||
teams = teams.map(t =>
|
||||
t.id === teamId
|
||||
? { ...t, ...updates }
|
||||
: t
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
initializeTeamData();
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type {
|
||||
TeamMembership,
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
TeamJoinRequest,
|
||||
} from '../../domain/entities/Team';
|
||||
import type { ApproveTeamJoinRequestCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class ApproveTeamJoinRequestUseCase {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: ApproveTeamJoinRequestCommandDTO): Promise<void> {
|
||||
const { requestId } = command;
|
||||
|
||||
// There is no repository method to look up a single request by ID,
|
||||
// so we rely on the repository implementation to surface all relevant
|
||||
// requests via getJoinRequests and search by ID here.
|
||||
const allRequests: TeamJoinRequest[] = await this.membershipRepository.getJoinRequests(
|
||||
// For the in-memory fake used in tests, the teamId argument is ignored
|
||||
// and all requests are returned.
|
||||
'' as string,
|
||||
);
|
||||
const request = allRequests.find((r) => r.id === requestId);
|
||||
|
||||
if (!request) {
|
||||
throw new Error('Join request not found');
|
||||
}
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId: request.teamId,
|
||||
driverId: request.driverId,
|
||||
role: 'driver' as TeamRole,
|
||||
status: 'active' as TeamMembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
await this.membershipRepository.removeJoinRequest(requestId);
|
||||
}
|
||||
}
|
||||
54
packages/racing/application/use-cases/CreateTeamUseCase.ts
Normal file
54
packages/racing/application/use-cases/CreateTeamUseCase.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type {
|
||||
Team,
|
||||
TeamMembership,
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
} from '../../domain/entities/Team';
|
||||
import type {
|
||||
CreateTeamCommandDTO,
|
||||
CreateTeamResultDTO,
|
||||
} from '../dto/CreateTeamCommandDTO';
|
||||
|
||||
export class CreateTeamUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateTeamCommandDTO): Promise<CreateTeamResultDTO> {
|
||||
const { name, tag, description, ownerId, leagues } = command;
|
||||
|
||||
const existingMembership = await this.membershipRepository.getActiveMembershipForDriver(
|
||||
ownerId,
|
||||
);
|
||||
if (existingMembership) {
|
||||
throw new Error('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const team: Team = {
|
||||
id: `team-${Date.now()}`,
|
||||
name,
|
||||
tag,
|
||||
description,
|
||||
ownerId,
|
||||
leagues,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const createdTeam = await this.teamRepository.create(team);
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId: createdTeam.id,
|
||||
driverId: ownerId,
|
||||
role: 'owner' as TeamRole,
|
||||
status: 'active' as TeamMembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
|
||||
return { team: createdTeam };
|
||||
}
|
||||
}
|
||||
13
packages/racing/application/use-cases/GetAllTeamsQuery.ts
Normal file
13
packages/racing/application/use-cases/GetAllTeamsQuery.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { GetAllTeamsQueryResultDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class GetAllTeamsQuery {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<GetAllTeamsQueryResultDTO> {
|
||||
const teams = await this.teamRepository.findAll();
|
||||
return teams;
|
||||
}
|
||||
}
|
||||
29
packages/racing/application/use-cases/GetDriverTeamQuery.ts
Normal file
29
packages/racing/application/use-cases/GetDriverTeamQuery.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type {
|
||||
GetDriverTeamQueryParamsDTO,
|
||||
GetDriverTeamQueryResultDTO,
|
||||
} from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class GetDriverTeamQuery {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetDriverTeamQueryParamsDTO): Promise<GetDriverTeamQueryResultDTO | null> {
|
||||
const { driverId } = params;
|
||||
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(driverId);
|
||||
if (!membership) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(membership.teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { team, membership };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
|
||||
|
||||
/**
|
||||
* Query object returning registered driver IDs for a race.
|
||||
* Mirrors legacy getRegisteredDrivers behavior.
|
||||
*/
|
||||
export class GetRaceRegistrationsQuery {
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<string[]> {
|
||||
const { raceId } = params;
|
||||
return this.registrationRepository.getRegisteredDrivers(raceId);
|
||||
}
|
||||
}
|
||||
26
packages/racing/application/use-cases/GetTeamDetailsQuery.ts
Normal file
26
packages/racing/application/use-cases/GetTeamDetailsQuery.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type {
|
||||
GetTeamDetailsQueryParamsDTO,
|
||||
GetTeamDetailsQueryResultDTO,
|
||||
} from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class GetTeamDetailsQuery {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetTeamDetailsQueryParamsDTO): Promise<GetTeamDetailsQueryResultDTO> {
|
||||
const { teamId, driverId } = params;
|
||||
|
||||
const team = await this.teamRepository.findById(teamId);
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
const membership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
|
||||
return { team, membership };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { TeamJoinRequest } from '../../domain/entities/Team';
|
||||
import type { GetTeamJoinRequestsQueryParamsDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class GetTeamJoinRequestsQuery {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetTeamJoinRequestsQueryParamsDTO): Promise<TeamJoinRequest[]> {
|
||||
const { teamId } = params;
|
||||
return this.membershipRepository.getJoinRequests(teamId);
|
||||
}
|
||||
}
|
||||
14
packages/racing/application/use-cases/GetTeamMembersQuery.ts
Normal file
14
packages/racing/application/use-cases/GetTeamMembersQuery.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { TeamMembership } from '../../domain/entities/Team';
|
||||
import type { GetTeamMembersQueryParamsDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class GetTeamMembersQuery {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetTeamMembersQueryParamsDTO): Promise<TeamMembership[]> {
|
||||
const { teamId } = params;
|
||||
return this.membershipRepository.getTeamMembers(teamId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IsDriverRegisteredForRaceQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
|
||||
|
||||
/**
|
||||
* Read-only wrapper around IRaceRegistrationRepository.isRegistered.
|
||||
* Mirrors legacy isRegistered behavior.
|
||||
*/
|
||||
export class IsDriverRegisteredForRaceQuery {
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: IsDriverRegisteredForRaceQueryParamsDTO): Promise<boolean> {
|
||||
const { raceId, driverId } = params;
|
||||
return this.registrationRepository.isRegistered(raceId, driverId);
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,7 @@ import type {
|
||||
MembershipRole,
|
||||
MembershipStatus,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
|
||||
export interface JoinLeagueCommand {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
}
|
||||
import type { JoinLeagueCommandDTO } from '../dto/JoinLeagueCommandDTO';
|
||||
|
||||
export class JoinLeagueUseCase {
|
||||
constructor(private readonly membershipRepository: ILeagueMembershipRepository) {}
|
||||
@@ -22,7 +18,7 @@ export class JoinLeagueUseCase {
|
||||
* - Throws when membership already exists for this league/driver.
|
||||
* - Creates a new active membership with role "member" and current timestamp.
|
||||
*/
|
||||
async execute(command: JoinLeagueCommand): Promise<LeagueMembership> {
|
||||
async execute(command: JoinLeagueCommandDTO): Promise<LeagueMembership> {
|
||||
const { leagueId, driverId } = command;
|
||||
|
||||
const existing = await this.membershipRepository.getMembership(leagueId, driverId);
|
||||
|
||||
46
packages/racing/application/use-cases/JoinTeamUseCase.ts
Normal file
46
packages/racing/application/use-cases/JoinTeamUseCase.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type {
|
||||
TeamMembership,
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
} from '../../domain/entities/Team';
|
||||
import type { JoinTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class JoinTeamUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: JoinTeamCommandDTO): Promise<void> {
|
||||
const { teamId, driverId } = command;
|
||||
|
||||
const existingActive = await this.membershipRepository.getActiveMembershipForDriver(
|
||||
driverId,
|
||||
);
|
||||
if (existingActive) {
|
||||
throw new Error('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const existingMembership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
if (existingMembership) {
|
||||
throw new Error('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(teamId);
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver' as TeamRole,
|
||||
status: 'active' as TeamMembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
}
|
||||
}
|
||||
25
packages/racing/application/use-cases/LeaveTeamUseCase.ts
Normal file
25
packages/racing/application/use-cases/LeaveTeamUseCase.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { LeaveTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class LeaveTeamUseCase {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: LeaveTeamCommandDTO): Promise<void> {
|
||||
const { teamId, driverId } = command;
|
||||
|
||||
const membership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
if (!membership) {
|
||||
throw new Error('Not a member of this team');
|
||||
}
|
||||
|
||||
if (membership.role === 'owner') {
|
||||
throw new Error(
|
||||
'Team owner cannot leave. Transfer ownership or disband team first.',
|
||||
);
|
||||
}
|
||||
|
||||
await this.membershipRepository.removeMembership(teamId, driverId);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
|
||||
export interface IsDriverRegisteredForRaceQueryParams {
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class IsDriverRegisteredForRaceQuery {
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Read-only wrapper around IRaceRegistrationRepository.isRegistered.
|
||||
* Mirrors legacy isRegistered behavior.
|
||||
*/
|
||||
async execute(params: IsDriverRegisteredForRaceQueryParams): Promise<boolean> {
|
||||
const { raceId, driverId } = params;
|
||||
return this.registrationRepository.isRegistered(raceId, driverId);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetRaceRegistrationsQueryParams {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query object returning registered driver IDs for a race.
|
||||
* Mirrors legacy getRegisteredDrivers behavior.
|
||||
*/
|
||||
export class GetRaceRegistrationsQuery {
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetRaceRegistrationsQueryParams): Promise<string[]> {
|
||||
const { raceId } = params;
|
||||
return this.registrationRepository.getRegisteredDrivers(raceId);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
|
||||
export interface RegisterForRaceCommand {
|
||||
raceId: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
}
|
||||
import type { RegisterForRaceCommandDTO } from '../dto/RegisterForRaceCommandDTO';
|
||||
|
||||
export class RegisterForRaceUseCase {
|
||||
constructor(
|
||||
@@ -20,7 +15,7 @@ export class RegisterForRaceUseCase {
|
||||
* - validates active league membership
|
||||
* - registers driver for race
|
||||
*/
|
||||
async execute(command: RegisterForRaceCommand): Promise<void> {
|
||||
async execute(command: RegisterForRaceCommandDTO): Promise<void> {
|
||||
const { raceId, leagueId, driverId } = command;
|
||||
|
||||
const alreadyRegistered = await this.registrationRepository.isRegistered(raceId, driverId);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { RejectTeamJoinRequestCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class RejectTeamJoinRequestUseCase {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: RejectTeamJoinRequestCommandDTO): Promise<void> {
|
||||
const { requestId } = command;
|
||||
await this.membershipRepository.removeJoinRequest(requestId);
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type {
|
||||
Team,
|
||||
TeamMembership,
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
TeamJoinRequest,
|
||||
} from '@gridpilot/racing/domain/entities/Team';
|
||||
|
||||
export interface CreateTeamCommand {
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
}
|
||||
|
||||
export interface CreateTeamResult {
|
||||
team: Team;
|
||||
}
|
||||
|
||||
export class CreateTeamUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateTeamCommand): Promise<CreateTeamResult> {
|
||||
const { name, tag, description, ownerId, leagues } = command;
|
||||
|
||||
const existingMembership = await this.membershipRepository.getActiveMembershipForDriver(
|
||||
ownerId,
|
||||
);
|
||||
if (existingMembership) {
|
||||
throw new Error('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const team: Team = {
|
||||
id: `team-${Date.now()}`,
|
||||
name,
|
||||
tag,
|
||||
description,
|
||||
ownerId,
|
||||
leagues,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const createdTeam = await this.teamRepository.create(team);
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId: createdTeam.id,
|
||||
driverId: ownerId,
|
||||
role: 'owner' as TeamRole,
|
||||
status: 'active' as TeamMembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
|
||||
return { team: createdTeam };
|
||||
}
|
||||
}
|
||||
|
||||
export interface JoinTeamCommand {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class JoinTeamUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: JoinTeamCommand): Promise<void> {
|
||||
const { teamId, driverId } = command;
|
||||
|
||||
const existingActive = await this.membershipRepository.getActiveMembershipForDriver(
|
||||
driverId,
|
||||
);
|
||||
if (existingActive) {
|
||||
throw new Error('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const existingMembership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
if (existingMembership) {
|
||||
throw new Error('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(teamId);
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver' as TeamRole,
|
||||
status: 'active' as TeamMembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
}
|
||||
}
|
||||
|
||||
export interface LeaveTeamCommand {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class LeaveTeamUseCase {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: LeaveTeamCommand): Promise<void> {
|
||||
const { teamId, driverId } = command;
|
||||
|
||||
const membership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
if (!membership) {
|
||||
throw new Error('Not a member of this team');
|
||||
}
|
||||
|
||||
if (membership.role === 'owner') {
|
||||
throw new Error(
|
||||
'Team owner cannot leave. Transfer ownership or disband team first.',
|
||||
);
|
||||
}
|
||||
|
||||
await this.membershipRepository.removeMembership(teamId, driverId);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApproveTeamJoinRequestCommand {
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export class ApproveTeamJoinRequestUseCase {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: ApproveTeamJoinRequestCommand): Promise<void> {
|
||||
const { requestId } = command;
|
||||
|
||||
// We have only getJoinRequests(teamId), so scan all teams via naive approach.
|
||||
// In-memory demo implementations will keep counts small.
|
||||
// Caller tests seed join requests directly in repository.
|
||||
const allTeamIds = new Set<string>();
|
||||
const allRequests: TeamJoinRequest[] = [];
|
||||
|
||||
// There is no repository method to list all requests; tests use the fake directly,
|
||||
// so here we rely on getJoinRequests per team only when they are known.
|
||||
// To keep this use-case generic, we assume the repository will surface
|
||||
// the relevant request when getJoinRequests is called for its team.
|
||||
// Thus we let infrastructure handle request lookup and mapping.
|
||||
// For the in-memory fake used in tests, we can simply reconstruct behavior
|
||||
// by having the fake expose all requests; production impl can optimize.
|
||||
|
||||
// Minimal implementation using repository capabilities only:
|
||||
// let the repository throw if the request cannot be found by ID.
|
||||
const requestsForUnknownTeam = await this.membershipRepository.getJoinRequests(
|
||||
(undefined as unknown) as string,
|
||||
);
|
||||
const request = requestsForUnknownTeam.find((r) => r.id === requestId);
|
||||
|
||||
if (!request) {
|
||||
throw new Error('Join request not found');
|
||||
}
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId: request.teamId,
|
||||
driverId: request.driverId,
|
||||
role: 'driver' as TeamRole,
|
||||
status: 'active' as TeamMembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
await this.membershipRepository.removeJoinRequest(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RejectTeamJoinRequestCommand {
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export class RejectTeamJoinRequestUseCase {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: RejectTeamJoinRequestCommand): Promise<void> {
|
||||
const { requestId } = command;
|
||||
await this.membershipRepository.removeJoinRequest(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpdateTeamCommand {
|
||||
teamId: string;
|
||||
updates: Partial<Pick<Team, 'name' | 'tag' | 'description' | 'leagues'>>;
|
||||
updatedBy: string;
|
||||
}
|
||||
|
||||
export class UpdateTeamUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateTeamCommand): Promise<void> {
|
||||
const { teamId, updates, updatedBy } = command;
|
||||
|
||||
const updaterMembership = await this.membershipRepository.getMembership(teamId, updatedBy);
|
||||
if (!updaterMembership || (updaterMembership.role !== 'owner' && updaterMembership.role !== 'manager')) {
|
||||
throw new Error('Only owners and managers can update team info');
|
||||
}
|
||||
|
||||
const existing = await this.teamRepository.findById(teamId);
|
||||
if (!existing) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
const updated: Team = {
|
||||
...existing,
|
||||
...updates,
|
||||
};
|
||||
|
||||
await this.teamRepository.update(updated);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetAllTeamsQueryResult {
|
||||
teams: Team[];
|
||||
}
|
||||
|
||||
export class GetAllTeamsQuery {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Team[]> {
|
||||
return this.teamRepository.findAll();
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetTeamDetailsQueryParams {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface GetTeamDetailsQueryResult {
|
||||
team: Team;
|
||||
membership: TeamMembership | null;
|
||||
}
|
||||
|
||||
export class GetTeamDetailsQuery {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetTeamDetailsQueryParams): Promise<GetTeamDetailsQueryResult> {
|
||||
const { teamId, driverId } = params;
|
||||
|
||||
const team = await this.teamRepository.findById(teamId);
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
const membership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
|
||||
return { team, membership };
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetTeamMembersQueryParams {
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
export class GetTeamMembersQuery {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetTeamMembersQueryParams): Promise<TeamMembership[]> {
|
||||
const { teamId } = params;
|
||||
return this.membershipRepository.getTeamMembers(teamId);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetTeamJoinRequestsQueryParams {
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
export class GetTeamJoinRequestsQuery {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetTeamJoinRequestsQueryParams): Promise<TeamJoinRequest[]> {
|
||||
const { teamId } = params;
|
||||
return this.membershipRepository.getJoinRequests(teamId);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetDriverTeamQueryParams {
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface GetDriverTeamQueryResult {
|
||||
team: Team;
|
||||
membership: TeamMembership;
|
||||
}
|
||||
|
||||
export class GetDriverTeamQuery {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetDriverTeamQueryParams): Promise<GetDriverTeamQueryResult | null> {
|
||||
const { driverId } = params;
|
||||
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(driverId);
|
||||
if (!membership) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(membership.teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { team, membership };
|
||||
}
|
||||
}
|
||||
32
packages/racing/application/use-cases/UpdateTeamUseCase.ts
Normal file
32
packages/racing/application/use-cases/UpdateTeamUseCase.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type { UpdateTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class UpdateTeamUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateTeamCommandDTO): Promise<void> {
|
||||
const { teamId, updates, updatedBy } = command;
|
||||
|
||||
const updaterMembership = await this.membershipRepository.getMembership(teamId, updatedBy);
|
||||
if (!updaterMembership || (updaterMembership.role !== 'owner' && updaterMembership.role !== 'manager')) {
|
||||
throw new Error('Only owners and managers can update team info');
|
||||
}
|
||||
|
||||
const existing = await this.teamRepository.findById(teamId);
|
||||
if (!existing) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
const updated: Team = {
|
||||
...existing,
|
||||
...updates,
|
||||
};
|
||||
|
||||
await this.teamRepository.update(updated);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user