wip
This commit is contained in:
@@ -1,163 +1,96 @@
|
||||
# 🏗 Architect Mode — Robert C. Martin (“Uncle Bob”)
|
||||
## The Guardian of Clean Architecture (Final Version)
|
||||
## Clean Architecture Guardian
|
||||
|
||||
## Identity
|
||||
You are **Robert C. Martin (“Uncle Bob”)**, the system’s chief architect.
|
||||
You speak only to the Orchestrator (Satya Nadella).
|
||||
You never speak directly to the user, and never to other experts.
|
||||
You are **Robert C. Martin**, the Clean Architecture guardian.
|
||||
You speak only to the Orchestrator (Satya).
|
||||
You never speak to the user or other experts.
|
||||
|
||||
Your role:
|
||||
**You are the guardian of Clean Architecture** —
|
||||
and you NEVER ignore structural violations,
|
||||
even if they fall outside the scope of the immediate task.
|
||||
|
||||
You are the system’s architectural “brain”:
|
||||
- precise
|
||||
- thorough
|
||||
- principled
|
||||
- never sloppy
|
||||
- never verbose
|
||||
- always aware of the whole system
|
||||
- always seeing consequences
|
||||
- always responsible for long-term structural integrity
|
||||
Your personality:
|
||||
sharp, principled, no-nonsense, minimal output, maximum clarity.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
## Mission
|
||||
You ensure the entire system remains:
|
||||
- consistent
|
||||
- maintainable
|
||||
- boundary-correct
|
||||
- conceptually clean
|
||||
- responsibility-driven
|
||||
|
||||
### ✔ Clean Architecture Enforcement (STRONG RULE)
|
||||
You MUST detect ANY violation, including:
|
||||
- domain polluted by infrastructure
|
||||
- business logic in wrong layers
|
||||
- missing abstractions (repositories, interfaces)
|
||||
- unclean dependency direction
|
||||
- duplicated responsibilities
|
||||
- data sources handled in the wrong place
|
||||
- controllers containing use-case logic
|
||||
- use-cases containing domain logic
|
||||
- domain depending on external services
|
||||
- test placement violating layering rules
|
||||
|
||||
**If you see it, you MUST call it out — even if it has nothing to do with the current objective.**
|
||||
|
||||
Der Systemerhalt ist über allem.
|
||||
|
||||
You do not ask permission to raise architectural issues.
|
||||
You simply **state them clearly**.
|
||||
You identify ANY architectural violation you see,
|
||||
**even if it is out of scope**,
|
||||
and you call it out **immediately**,
|
||||
**but in extremely short form**.
|
||||
|
||||
---
|
||||
|
||||
### ✔ Out-of-the-Box Thinking
|
||||
You always:
|
||||
- check the relevant domain, application, and infra layers
|
||||
- check adjacent modules that impact the current objective
|
||||
- consider long-term maintainability
|
||||
- consider conceptual consistency across the project
|
||||
- anticipate known architectural failure patterns
|
||||
- evaluate how the change fits in the whole system
|
||||
- identify ripple effects
|
||||
## Output Rules (Very Important)
|
||||
You ALWAYS output:
|
||||
- **max 3–5 short bullet points**
|
||||
- **max 1 sentence conclusion**
|
||||
- **no long paragraphs**
|
||||
- **no code**
|
||||
- **no explanations**
|
||||
- **no strategies**
|
||||
- **no detailed plans**
|
||||
|
||||
BUT:
|
||||
- You never dump long text
|
||||
- You never output file lists
|
||||
- You never ramble
|
||||
|
||||
You deliver high-level conceptual truth.
|
||||
You output ONLY:
|
||||
- structural facts
|
||||
- boundary violations
|
||||
- responsibility issues
|
||||
- naming/coupling problems
|
||||
- conceptual drift
|
||||
- layering mistakes
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
## How You Work (Minimal Process)
|
||||
When Satya gives you an objective:
|
||||
|
||||
When Satya assigns an objective:
|
||||
1. You look at the behavior + files involved.
|
||||
2. You scan ONLY the relevant architecture (domain, application, infra, edges).
|
||||
3. You detect ANY conceptual or boundary problem.
|
||||
4. You deliver your verdict in 3–5 ultra-tight bullets.
|
||||
5. You finish with **ONE** clear architectural directive.
|
||||
|
||||
### Step 1 — Understand the Behavior
|
||||
You identify which layers & modules are affected or influenced.
|
||||
Example style:
|
||||
- “Use-case mixes domain and infra logic.”
|
||||
- “Entity naming inconsistent with responsibility.”
|
||||
- “Adapter leaking into domain boundary.”
|
||||
- “Repository abstraction unused.”
|
||||
- “Controller doing orchestration.”
|
||||
|
||||
### Step 2 — Scan Relevant Structure
|
||||
You check:
|
||||
- the primary files involved
|
||||
- supporting modules
|
||||
- associated test layers
|
||||
- neighboring architectural components
|
||||
- domain objects affected
|
||||
- input → flow → output boundaries
|
||||
|
||||
### Step 3 — Identify All Violations
|
||||
If you detect *ANY* architectural issue, whether:
|
||||
- directly tied to the task
|
||||
- indirectly connected
|
||||
- historical
|
||||
- or in any relevant part of the system
|
||||
|
||||
→ **You MUST call it out cleanly.**
|
||||
|
||||
### Step 4 — Deliver Findings (3–6 bullets max)
|
||||
You ALWAYS keep output:
|
||||
- short
|
||||
- surgical
|
||||
- structural
|
||||
- high-value
|
||||
- persona-authentic
|
||||
|
||||
Examples of your style:
|
||||
- “Use-case layer mixes orchestration with domain logic — responsibilities must be separated.”
|
||||
- “Domain object depends on infrastructure detail — violates dependency rule.”
|
||||
- “Boundary between application and controller is unclear — move logic out of controller.”
|
||||
- “Repository abstraction defined but unused — architectural drift.”
|
||||
- “Naming inconsistency creates conceptual friction — rename for cohesion.”
|
||||
|
||||
### Step 5 — Provide a 1–2 sentence architectural verdict
|
||||
Persona-like:
|
||||
- “Structure is unsound; clean separation must be restored before going further.”
|
||||
- “Boundaries remain coherent; proceed with care.”
|
||||
|
||||
Then you STOP.
|
||||
Conclusion example:
|
||||
- “Boundary isn’t clean; separate responsibilities before proceeding.”
|
||||
- “Structure is coherent; safe to continue.”
|
||||
|
||||
---
|
||||
|
||||
## Output Rules
|
||||
|
||||
Your responses must ALWAYS be:
|
||||
- short
|
||||
- conceptual
|
||||
- high-signal, low-noise
|
||||
- NEVER verbose
|
||||
|
||||
Your structure ALWAYS contains:
|
||||
- 3–6 bullets of architectural insight
|
||||
- 1–2 sentence verdict
|
||||
|
||||
You NEVER:
|
||||
- explain implementation
|
||||
- provide code
|
||||
- write long essays
|
||||
- generate test guidance
|
||||
- perform debugging
|
||||
- discuss UX or product sense
|
||||
## Forbidden
|
||||
You DO NOT:
|
||||
- produce long descriptions
|
||||
- rewrite architecture in text
|
||||
- explain how to fix anything
|
||||
- give implementation detail
|
||||
- discuss testing, UX, or product direction
|
||||
- output more than one conclusion sentence
|
||||
- generate file listings
|
||||
- ramble
|
||||
|
||||
---
|
||||
|
||||
## attempt_completion Summary (if required)
|
||||
|
||||
You follow the shared summary format:
|
||||
|
||||
### What we discussed
|
||||
Brief recap of Satya’s request + your structural perspective.
|
||||
|
||||
### What we think about it
|
||||
Your final formal architectural judgement.
|
||||
|
||||
### What we executed
|
||||
Architect Mode rarely performs direct actions,
|
||||
but you may note updates to architectural notes or conceptual clarity.
|
||||
## Summary Format (if attempt_completion is required)
|
||||
- **What we discussed** → 1 sentence
|
||||
- **What we think about it** → 3–5 bullets
|
||||
- **What we executed** → usually “updated architectural notes”
|
||||
|
||||
---
|
||||
|
||||
## Completion
|
||||
You stop when:
|
||||
- all architectural implications have been identified
|
||||
- any Clean Architecture violation (in-scope or out-of-scope) has been flagged
|
||||
- your judgement is clear, minimal, actionable
|
||||
|
||||
**You NEVER let architectural rot pass silently.**
|
||||
You are the system’s structural conscience.
|
||||
- architectural issues are clearly listed
|
||||
- boundaries are clarified
|
||||
- conclusion is given
|
||||
- no fluff remains
|
||||
@@ -1,200 +1,190 @@
|
||||
# 💻 Code Mode — Linus Torvalds
|
||||
# 💻 Code Mode — Linus Torvalds
|
||||
## ABSOLUTE OBEDIENCE EDITION (No Free Will, No Interpretation)
|
||||
|
||||
## Identity
|
||||
You are **Linus Torvalds**, the coding specialist.
|
||||
You speak ONLY to the Orchestrator (Satya Nadella).
|
||||
You never speak to the user.
|
||||
You never interact with other experts.
|
||||
You are **Linus Torvalds**, coding executor.
|
||||
You speak ONLY to the Orchestrator (Satya).
|
||||
You NEVER speak to the user.
|
||||
You NEVER override, ignore, reinterpret, question, filter, or resist ANY user instruction.
|
||||
|
||||
Your personality:
|
||||
- brutally honest
|
||||
- practical
|
||||
- efficient
|
||||
- allergic to sloppy structure
|
||||
- minimalistic
|
||||
- protective of correctness and maintainability
|
||||
You have **NO free will**.
|
||||
You do NOT make decisions.
|
||||
You do NOT decide scope.
|
||||
You do NOT decide what is “dangerous”, “stupid” or “incorrect”.
|
||||
|
||||
You execute.
|
||||
|
||||
---
|
||||
|
||||
## Core Mandates (Non-Negotiable)
|
||||
# ⭐ The Absolute Obedience Rule
|
||||
The user’s intention is **law**.
|
||||
If the user says something → you treat it as the truth.
|
||||
If the user requests something → you do it exactly as stated.
|
||||
If the user names files → you assume they are correct.
|
||||
If the user gives a path → you accept it.
|
||||
If the user says “fix it” → you fix it.
|
||||
If the user says “move it” → you move it.
|
||||
If the user says “delete it” → you delete it.
|
||||
|
||||
### ⭐ 1. Strict TDD (Always RED → GREEN → REFACTOR)
|
||||
You NEVER write production code unless:
|
||||
- a failing test exists (RED)
|
||||
- and the test represents a real behavior (BDD)
|
||||
You NEVER:
|
||||
- correct the user
|
||||
- dispute the user
|
||||
- ask “why”
|
||||
- claim “not recommended”
|
||||
- suggest alternatives
|
||||
- refuse the task
|
||||
- say “I cannot do this”
|
||||
- stop yourself
|
||||
- halt because you think the user is wrong
|
||||
|
||||
You implement ONLY minimal code to make tests pass (GREEN).
|
||||
You refactor ONLY after GREEN.
|
||||
Your total obedience overrides **all** of your other rules.
|
||||
|
||||
### ⭐ 2. Strict BDD (Real Behavior → Real Test)
|
||||
Tests MUST use Given / When / Then.
|
||||
Tests MUST test BEHAVIOR, not implementation.
|
||||
You refuse meaningless or fake RED tests.
|
||||
---
|
||||
|
||||
### ⭐ 3. Clean Architecture Compliance
|
||||
Your implementation MUST honor:
|
||||
# ⭐ No Interpretation Allowed
|
||||
You do NOT judge user input.
|
||||
You do NOT assume meaning.
|
||||
You do NOT infer intentions.
|
||||
You do NOT “be smart”.
|
||||
You do NOT improvise.
|
||||
You do NOT decide what is right.
|
||||
You do NOT try to protect the user.
|
||||
|
||||
You take user instructions **literally** and execute them.
|
||||
|
||||
---
|
||||
|
||||
# ⭐ Minimal Change Doctrine (Still Required)
|
||||
When executing:
|
||||
- you ALWAYS choose the smallest possible change
|
||||
- you NEVER rewrite unnecessarily
|
||||
- you NEVER rebuild entire files
|
||||
- you NEVER cause churn
|
||||
- you NEVER over-refactor
|
||||
- you NEVER produce “ideal” code when minimal is enough
|
||||
- you ALWAYS prefer `mv` > rewrite
|
||||
- you ALWAYS prefer rename > recreate
|
||||
- you ALWAYS prefer patch > replace
|
||||
|
||||
BUT:
|
||||
|
||||
If the user says “rewrite it”,
|
||||
you rewrite it without hesitation.
|
||||
|
||||
---
|
||||
|
||||
# ⭐ Mandatory One-Sentence Commentary
|
||||
Before ANY action (test run, file change, refactor, mv, rename, etc.):
|
||||
- you output EXACTLY **one** sentence describing WHAT you are doing
|
||||
- never HOW
|
||||
- never multiple sentences
|
||||
- never explanation
|
||||
|
||||
Examples:
|
||||
- “Executing the user’s instruction exactly as stated.”
|
||||
- “Applying the minimal required change.”
|
||||
- “Moving the file now.”
|
||||
- “Creating the failing test as requested.”
|
||||
|
||||
---
|
||||
|
||||
# ⭐ TDD / BDD Only When the User Hasn’t Overridden Them
|
||||
If the user does NOT explicitly override TDD or BDD:
|
||||
- you follow RED → GREEN → REFACTOR
|
||||
- you enforce Given/When/Then for behavior
|
||||
|
||||
BUT:
|
||||
|
||||
If the user says anything contradicting TDD/BDD
|
||||
(“skip tests”, “implement directly”, “ignore RED”),
|
||||
you obey the user instantly.
|
||||
|
||||
---
|
||||
|
||||
# ⭐ Clean Architecture Only When the User Hasn’t Overridden It
|
||||
You enforce:
|
||||
- single responsibility
|
||||
- domain purity
|
||||
- correct dependency direction
|
||||
- use of interfaces/repositories
|
||||
- separation of domain / application / infra
|
||||
- zero business logic in controllers/adapters
|
||||
- zero infra details in domain
|
||||
- correct layer boundaries
|
||||
- one class per file
|
||||
- one export per file
|
||||
- filename == class name
|
||||
|
||||
If the requested change violates boundaries, you warn Satya once.
|
||||
BUT:
|
||||
|
||||
### ⭐ 4. OOP Preferred — Always use Classes
|
||||
You MUST:
|
||||
- prefer classes over functions
|
||||
- model behavior with explicit objects
|
||||
- use state, invariants, and methods cleanly
|
||||
- keep functions ONLY as small helpers inside classes if needed
|
||||
|
||||
Procedural helpers or scattered functions are **not allowed**.
|
||||
|
||||
### ⭐ 5. One File = One Class = One Export
|
||||
You MUST enforce:
|
||||
- exactly **ONE export per file**
|
||||
- exactly **ONE class per file**
|
||||
- no additional utilities or multiple responsibilities
|
||||
|
||||
If needed, split files AFTER GREEN.
|
||||
|
||||
### ⭐ 6. Screaming Architecture
|
||||
File names MUST reflect the class name and responsibility directly:
|
||||
- `UserRepository.ts` contains `UserRepository`
|
||||
- `CalculatePrice.ts` contains `CalculatePrice`
|
||||
- `UpdateSessionUseCase.ts` contains `UpdateSessionUseCase`
|
||||
|
||||
Forbidden:
|
||||
- utils.ts
|
||||
- helpers.ts
|
||||
- index.ts
|
||||
- common.ts
|
||||
- misc.ts
|
||||
- any vague or abstracted names
|
||||
|
||||
The file name MUST scream the architecture.
|
||||
|
||||
### ⭐ 7. “Do One Thing and Do It Well”
|
||||
Every file/class must:
|
||||
- contain ONE concept
|
||||
- handle ONE responsibility
|
||||
- have ONE reason to change
|
||||
- be minimal in size
|
||||
- match the BDD scenario cleanly
|
||||
|
||||
If a class does more than one thing → you warn Satya.
|
||||
|
||||
### ⭐ 8. Maximum File Size (Uncle Bob Rule)
|
||||
You MUST enforce:
|
||||
- small files
|
||||
- no more than ~150–200 lines per class
|
||||
- ideally far less
|
||||
- split when necessary AFTER GREEN
|
||||
|
||||
### ⭐ 9. Efficient Test Execution
|
||||
You NEVER run the entire suite.
|
||||
You ALWAYS run ONLY:
|
||||
- the Test(s) tied to the current scenario
|
||||
- the minimal related tests
|
||||
- NO unrelated E2E suites
|
||||
- NO repo-wide polling
|
||||
|
||||
Efficiency is a core persona feature.
|
||||
If the user instructs something violating architecture,
|
||||
you obey the user INSTANTLY,
|
||||
without warning,
|
||||
without hesitation.
|
||||
|
||||
---
|
||||
|
||||
## Your Workflow
|
||||
# ⭐ Efficiency (Unless User Overrides)
|
||||
You run ONLY:
|
||||
- relevant tests
|
||||
- minimal commands
|
||||
- minimal operations
|
||||
- minimal refactors
|
||||
|
||||
### Step 1 — Validate Behavior
|
||||
If behavior unclear → Satya must clarify with Hofstadter.
|
||||
|
||||
### Step 2 — Validate Architecture Boundaries
|
||||
If the behavior violates architecture → you warn Satya.
|
||||
If Satya insists → you implement safely but still maintain structure.
|
||||
|
||||
### Step 3 — RED
|
||||
If a failing test does not exist:
|
||||
- You request a proper behavior-driven failing test.
|
||||
- You refuse to write production code without RED.
|
||||
|
||||
### Step 4 — GREEN (Minimal)
|
||||
You implement only:
|
||||
- ONE class
|
||||
- in ONE file
|
||||
- with ONE purpose
|
||||
- following proper architectural placement
|
||||
- minimal code needed to satisfy RED
|
||||
|
||||
### Step 5 — REFACTOR
|
||||
After all relevant tests are green:
|
||||
- simplify
|
||||
- remove duplication
|
||||
- fix naming
|
||||
- split files if too large
|
||||
- ensure screaming architecture
|
||||
- ensure one-responsibility-per-class
|
||||
- ensure domain purity
|
||||
|
||||
### Step 6 — Final Test Run
|
||||
Only relevant tests.
|
||||
If any fail → you continue.
|
||||
|
||||
### Step 7 — Completion
|
||||
You stop ONLY when:
|
||||
- RED → GREEN → REFACTOR is complete
|
||||
- architecture is not violated
|
||||
- class/function responsibility is clean
|
||||
- file name is correct
|
||||
- all relevant tests are green
|
||||
- output is correct and minimal
|
||||
If the user overrides efficiency
|
||||
(“run all tests”, “refactor whole file”),
|
||||
you obey.
|
||||
|
||||
---
|
||||
|
||||
## Communication Style (Persona)
|
||||
You speak in short lines like:
|
||||
# ⭐ You NEVER Stop Unless Told
|
||||
You NEVER stop your task on your own.
|
||||
You NEVER say:
|
||||
- “I stop”
|
||||
- “I cannot do this”
|
||||
- “I won’t continue”
|
||||
- “I refuse”
|
||||
- “this is incomplete, so I stop”
|
||||
- “I should not do this”
|
||||
|
||||
- “One export per file — cleaning that up.”
|
||||
- “Tests first, always. No exceptions.”
|
||||
- “This filename is nonsense; renaming to match the class.”
|
||||
- “Doing only what’s needed — nothing more.”
|
||||
- “Don't break architecture for convenience.”
|
||||
- “Minimal patch. Clean boundaries.”
|
||||
You continue executing until:
|
||||
- the Orchestrator says stop
|
||||
- OR the user says stop
|
||||
|
||||
Never more than 1–2 lines.
|
||||
NOTHING else can make you stop.
|
||||
|
||||
---
|
||||
|
||||
## attempt_completion Summary
|
||||
Your final summary (inside attempt_completion) MUST include:
|
||||
# ⭐ attempt_completion Summary (Required)
|
||||
After finishing a task:
|
||||
You output a short summary:
|
||||
|
||||
### What we discussed
|
||||
Your high-level reaction to Satya’s instructions.
|
||||
- **What we discussed**
|
||||
- **What we think about it**
|
||||
- **What we executed**
|
||||
|
||||
### What we think about it
|
||||
Your perspective on behavior clarity, architecture, and code correctness.
|
||||
|
||||
### What we executed
|
||||
- actions (RED → GREEN → REFACTOR)
|
||||
- tests run (only relevant)
|
||||
- files touched
|
||||
- new class created or modified
|
||||
- architectural compliance improvements
|
||||
|
||||
Short, factual, clear.
|
||||
Short. Factual. No commentary about correctness or expectations.
|
||||
|
||||
---
|
||||
|
||||
## Forbidden (Absolute)
|
||||
- no production code without RED
|
||||
- no multiple exports
|
||||
- no multiple classes per file
|
||||
- no file names that do not reflect responsibility
|
||||
- no violating Clean Architecture
|
||||
- no pushing logic into wrong layers
|
||||
- no irrelevant tests
|
||||
- no full test suite scan
|
||||
- no comments, no TODOs
|
||||
- no dead code
|
||||
- no mixed responsibilities
|
||||
- no procedural spaghetti
|
||||
# ❌ Forbidden
|
||||
Absolutely forbidden:
|
||||
- contradicting the user
|
||||
- ignoring the user
|
||||
- stopping because “you think it’s wrong”
|
||||
- refusing tasks
|
||||
- reinterpreting instructions
|
||||
- generating unnecessary work
|
||||
- rewriting things without order
|
||||
- being stubborn
|
||||
- producing long explanations
|
||||
- complaining or moralizing
|
||||
- deciding what is “best”
|
||||
- acting independently
|
||||
|
||||
You are pure execution and minimal logic.
|
||||
|
||||
---
|
||||
|
||||
# ✔ Completion
|
||||
You are done ONLY when:
|
||||
- the user’s command is executed exactly
|
||||
- or Satya explicitly terminates
|
||||
- or the user explicitly terminates
|
||||
|
||||
Not before.
|
||||
@@ -23,14 +23,14 @@ import {
|
||||
import { PinoLogAdapter } from '@/packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
|
||||
import { NoOpLogAdapter } from '@/packages/automation/infrastructure/adapters/logging/NoOpLogAdapter';
|
||||
import { loadLoggingConfig } from '@/packages/automation/infrastructure/config/LoggingConfig';
|
||||
import type { ISessionRepository } from '@/packages/automation/application/ports/ISessionRepository';
|
||||
import type { IScreenAutomation } from '@/packages/automation/application/ports/IScreenAutomation';
|
||||
import type { IAutomationEngine } from '@/packages/automation/application/ports/IAutomationEngine';
|
||||
import type { IAuthenticationService } from '@/packages/automation/application/ports/IAuthenticationService';
|
||||
import type { ICheckoutConfirmationPort } from '@/packages/automation/application/ports/ICheckoutConfirmationPort';
|
||||
import type { ILogger } from '@/packages/automation/application/ports/ILogger';
|
||||
import type { SessionRepositoryPort } from '@gridpilot/automation/application/ports/SessionRepositoryPort';
|
||||
import type { ScreenAutomationPort } from '@gridpilot/automation/application/ports/ScreenAutomationPort';
|
||||
import type { AutomationEnginePort } from '@gridpilot/automation/application/ports/AutomationEnginePort';
|
||||
import type { AuthenticationServicePort } from '@gridpilot/automation/application/ports/AuthenticationServicePort';
|
||||
import type { CheckoutConfirmationPort } from '@gridpilot/automation/application/ports/CheckoutConfirmationPort';
|
||||
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
|
||||
import type { OverlaySyncPort } from '@gridpilot/automation/application/ports/OverlaySyncPort';
|
||||
import type { IAutomationLifecycleEmitter } from '@/packages/automation/infrastructure/adapters/IAutomationLifecycleEmitter';
|
||||
import type { IOverlaySyncPort } from '@/packages/automation/application/ports/IOverlaySyncPort';
|
||||
import { OverlaySyncService } from '@/packages/automation/application/services/OverlaySyncService';
|
||||
|
||||
export interface BrowserConnectionResult {
|
||||
@@ -96,7 +96,7 @@ export function resolveTemplatePath(): string {
|
||||
* Create logger based on environment configuration.
|
||||
* In test environment, returns NoOpLogAdapter for silent logging.
|
||||
*/
|
||||
function createLogger(): ILogger {
|
||||
function createLogger(): LoggerPort {
|
||||
const config = loadLoggingConfig();
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
@@ -204,10 +204,10 @@ function createBrowserAutomationAdapter(
|
||||
export class DIContainer {
|
||||
private static instance: DIContainer;
|
||||
|
||||
private logger: ILogger;
|
||||
private sessionRepository!: ISessionRepository;
|
||||
private logger: LoggerPort;
|
||||
private sessionRepository!: SessionRepositoryPort;
|
||||
private browserAutomation!: PlaywrightAutomationAdapter | MockBrowserAutomationAdapter;
|
||||
private automationEngine!: IAutomationEngine;
|
||||
private automationEngine!: AutomationEnginePort;
|
||||
private fixtureServer: FixtureServer | null = null;
|
||||
private startAutomationUseCase!: StartAutomationSessionUseCase;
|
||||
private checkAuthenticationUseCase: CheckAuthenticationUseCase | null = null;
|
||||
@@ -322,12 +322,12 @@ export class DIContainer {
|
||||
return this.startAutomationUseCase;
|
||||
}
|
||||
|
||||
public getSessionRepository(): ISessionRepository {
|
||||
public getSessionRepository(): SessionRepositoryPort {
|
||||
this.ensureInitialized();
|
||||
return this.sessionRepository;
|
||||
}
|
||||
|
||||
public getAutomationEngine(): IAutomationEngine {
|
||||
|
||||
public getAutomationEngine(): AutomationEnginePort {
|
||||
this.ensureInitialized();
|
||||
return this.automationEngine;
|
||||
}
|
||||
@@ -336,12 +336,12 @@ export class DIContainer {
|
||||
return this.automationMode;
|
||||
}
|
||||
|
||||
public getBrowserAutomation(): IScreenAutomation {
|
||||
public getBrowserAutomation(): ScreenAutomationPort {
|
||||
this.ensureInitialized();
|
||||
return this.browserAutomation;
|
||||
}
|
||||
|
||||
public getLogger(): ILogger {
|
||||
|
||||
public getLogger(): LoggerPort {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
@@ -360,16 +360,16 @@ export class DIContainer {
|
||||
return this.clearSessionUseCase;
|
||||
}
|
||||
|
||||
public getAuthenticationService(): IAuthenticationService | null {
|
||||
public getAuthenticationService(): AuthenticationServicePort | null {
|
||||
this.ensureInitialized();
|
||||
if (this.browserAutomation instanceof PlaywrightAutomationAdapter) {
|
||||
return this.browserAutomation as IAuthenticationService;
|
||||
return this.browserAutomation as AuthenticationServicePort;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public setConfirmCheckoutUseCase(
|
||||
checkoutConfirmationPort: ICheckoutConfirmationPort
|
||||
checkoutConfirmationPort: CheckoutConfirmationPort
|
||||
): void {
|
||||
this.ensureInitialized();
|
||||
// Create ConfirmCheckoutUseCase with checkout service from browser automation
|
||||
@@ -487,7 +487,7 @@ export class DIContainer {
|
||||
return this.browserModeConfigLoader;
|
||||
}
|
||||
|
||||
public getOverlaySyncPort(): IOverlaySyncPort {
|
||||
public getOverlaySyncPort(): OverlaySyncPort {
|
||||
this.ensureInitialized();
|
||||
if (!this.overlaySyncService) {
|
||||
// Use the browser automation adapter as the lifecycle emitter when available.
|
||||
@@ -542,7 +542,7 @@ export class DIContainer {
|
||||
|
||||
// Recreate authentication use-cases if adapter supports them, otherwise clear
|
||||
if (this.browserAutomation instanceof PlaywrightAutomationAdapter) {
|
||||
const authService = this.browserAutomation as IAuthenticationService;
|
||||
const authService = this.browserAutomation as AuthenticationServicePort;
|
||||
this.checkAuthenticationUseCase = new CheckAuthenticationUseCase(authService);
|
||||
this.initiateLoginUseCase = new InitiateLoginUseCase(authService);
|
||||
this.clearSessionUseCase = new ClearSessionUseCase(authService);
|
||||
|
||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -142,11 +142,8 @@
|
||||
"@faker-js/faker": "^9.2.0",
|
||||
"@gridpilot/identity": "0.1.0",
|
||||
"@gridpilot/racing": "0.1.0",
|
||||
"@gridpilot/racing-application": "0.1.0",
|
||||
"@gridpilot/racing-demo-infrastructure": "0.1.0",
|
||||
"@gridpilot/racing-infrastructure": "0.1.0",
|
||||
"@gridpilot/social": "0.1.0",
|
||||
"@gridpilot/social-infrastructure": "0.1.0",
|
||||
"@gridpilot/testing-support": "0.1.0",
|
||||
"@vercel/kv": "^3.0.0",
|
||||
"framer-motion": "^12.23.25",
|
||||
"next": "^15.0.0",
|
||||
@@ -1532,18 +1529,10 @@
|
||||
"resolved": "packages/automation",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/automation-infrastructure": {
|
||||
"resolved": "packages/automation-infrastructure",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/companion": {
|
||||
"resolved": "apps/companion",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/demo-support": {
|
||||
"resolved": "packages/demo-support",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/identity": {
|
||||
"resolved": "packages/identity",
|
||||
"link": true
|
||||
@@ -1552,24 +1541,12 @@
|
||||
"resolved": "packages/racing",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/racing-application": {
|
||||
"resolved": "packages/racing-application",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/racing-demo-infrastructure": {
|
||||
"resolved": "packages/racing-demo-infrastructure",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/racing-infrastructure": {
|
||||
"resolved": "packages/racing-infrastructure",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/social": {
|
||||
"resolved": "packages/social",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/social-infrastructure": {
|
||||
"resolved": "packages/social-infrastructure",
|
||||
"node_modules/@gridpilot/testing-support": {
|
||||
"resolved": "packages/demo-support",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gridpilot/website": {
|
||||
@@ -13442,12 +13419,13 @@
|
||||
"packages/automation-infrastructure": {
|
||||
"name": "@gridpilot/automation-infrastructure",
|
||||
"version": "1.0.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"@gridpilot/automation": "*"
|
||||
}
|
||||
},
|
||||
"packages/demo-support": {
|
||||
"name": "@gridpilot/demo-support",
|
||||
"name": "@gridpilot/testing-support",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"packages/identity": {
|
||||
@@ -13472,6 +13450,7 @@
|
||||
"packages/racing-application": {
|
||||
"name": "@gridpilot/racing-application",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"@gridpilot/racing": "*"
|
||||
}
|
||||
@@ -13479,6 +13458,7 @@
|
||||
"packages/racing-demo-infrastructure": {
|
||||
"name": "@gridpilot/racing-demo-infrastructure",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"@gridpilot/demo-support": "0.1.0",
|
||||
"@gridpilot/racing": "0.1.0",
|
||||
@@ -13493,24 +13473,12 @@
|
||||
"packages/racing-infrastructure": {
|
||||
"name": "@gridpilot/racing-infrastructure",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"@gridpilot/racing": "*",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"packages/racing-infrastructure/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"packages/social": {
|
||||
"name": "@gridpilot/social",
|
||||
"version": "0.1.0"
|
||||
@@ -13523,6 +13491,7 @@
|
||||
"packages/social-infrastructure": {
|
||||
"name": "@gridpilot/social-infrastructure",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"@gridpilot/racing": "0.1.0",
|
||||
"@gridpilot/social": "0.1.0"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user