From c064b597cc04e0060147df14ca91c1b138fac66c Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 19 Dec 2025 11:32:16 +0100 Subject: [PATCH] module cleanup --- apps/api/src/domain/league/LeagueService.ts | 105 +++-- docs/architecture/DATA_FLOW.md | 446 +++++++------------- 2 files changed, 209 insertions(+), 342 deletions(-) diff --git a/apps/api/src/domain/league/LeagueService.ts b/apps/api/src/domain/league/LeagueService.ts index 6eb566567..0b336420b 100644 --- a/apps/api/src/domain/league/LeagueService.ts +++ b/apps/api/src/domain/league/LeagueService.ts @@ -1,38 +1,20 @@ import { Inject, Injectable } from '@nestjs/common'; -import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO'; import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO'; -import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO'; import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO'; -import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO'; -import { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO'; -import { TransferLeagueOwnershipOutputDTO } from './dtos/TransferLeagueOwnershipOutputDTO'; -import { LeagueJoinRequestWithDriverDTO } from './dtos/LeagueJoinRequestWithDriverDTO'; import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO'; import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO'; import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO'; import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO'; -import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO'; import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO'; -import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO'; +import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO'; +import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO'; +import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO'; import { LeagueAdminDTO } from './dtos/LeagueAdminDTO'; import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO'; -import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO'; -import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO'; import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO'; -import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO'; -import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO'; import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO'; -import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO'; -import { LeagueStatsDTO } from './dtos/LeagueStatsDTO'; -import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO'; -import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO'; -import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO'; -import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO'; -import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO'; -import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO'; - -// Core imports for entities -import type { League } from '@core/racing/domain/entities/League'; +import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO'; +import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO'; // Core imports for view models import type { LeagueScoringConfigViewModel } from '@core/racing/application/presenters/ILeagueScoringConfigPresenter'; @@ -46,18 +28,13 @@ import type { GetLeagueAdminPermissionsViewModel } from '@core/racing/applicatio import type { RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter'; import type { UpdateLeagueMemberRoleViewModel } from '@core/racing/application/presenters/IUpdateLeagueMemberRolePresenter'; import type { GetLeagueOwnerSummaryViewModel } from '@core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter'; -import type { GetLeagueProtestsViewModel } from '@core/racing/application/presenters/IGetLeagueProtestsPresenter'; -import type { GetLeagueSeasonsViewModel } from '@core/racing/application/presenters/IGetLeagueSeasonsPresenter'; -import type { GetLeagueMembershipsViewModel } from '@core/racing/application/presenters/IGetLeagueMembershipsPresenter'; import type { LeagueStandingsViewModel } from '@core/racing/application/presenters/ILeagueStandingsPresenter'; -import type { LeagueScheduleViewModel } from '@core/racing/application/presenters/ILeagueSchedulePresenter'; import type { LeagueStatsViewModel } from '@core/racing/application/presenters/ILeagueStatsPresenter'; import type { LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter'; import type { CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter'; import type { JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter'; import type { TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter'; - // Core imports import type { Logger } from '@core/shared/application/Logger'; @@ -92,13 +69,9 @@ import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPr import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter'; import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter'; import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter'; -import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter'; import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter'; -import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter'; -import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter'; import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter'; import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter'; -import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter'; import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter'; import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter'; import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter'; @@ -106,6 +79,9 @@ import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMember import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter'; import { JoinLeaguePresenter } from './presenters/JoinLeaguePresenter'; import { TransferLeagueOwnershipPresenter } from './presenters/TransferLeagueOwnershipPresenter'; +import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter'; +import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter'; +import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter'; // Tokens import { LOGGER_TOKEN } from './LeagueProviders'; @@ -258,7 +234,9 @@ export class LeagueService { if (result.isErr()) { throw new Error(result.unwrapErr().code); } - return result.unwrap(); + const presenter = new GetLeagueProtestsPresenter(); + presenter.present(result.unwrap()); + return presenter.getViewModel() as LeagueAdminProtestsDTO; } async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise { @@ -267,7 +245,9 @@ export class LeagueService { if (result.isErr()) { throw new Error(result.unwrapErr().code); } - return result.unwrap().seasons; + const presenter = new GetLeagueSeasonsPresenter(); + presenter.present(result.unwrap()); + return presenter.getViewModel().seasons; } async getLeagueMemberships(leagueId: string): Promise { @@ -276,16 +256,19 @@ export class LeagueService { if (result.isErr()) { throw new Error(result.unwrapErr().code); } - return result.unwrap(); + const presenter = new GetLeagueMembershipsPresenter(); + presenter.present(result.unwrap()); + return presenter.getViewModel().memberships as LeagueMembershipsDTO; } async getLeagueStandings(leagueId: string): Promise { this.logger.debug('Getting league standings', { leagueId }); - - return await this.getLeagueStandingsUseCase.execute(leagueId); + const result = await this.getLeagueStandingsUseCase.execute(leagueId); + // The use case returns a view model directly, so we return it as-is + return result as unknown as LeagueStandingsViewModel; } - async getLeagueSchedule(leagueId: string): Promise { + async getLeagueSchedule(leagueId: string): Promise> { this.logger.debug('Getting league schedule', { leagueId }); const result = await this.getLeagueScheduleUseCase.execute({ leagueId }); if (result.isErr()) { @@ -320,10 +303,48 @@ export class LeagueService { // In a full implementation, we'd have a use case that gets league basic info const ownerSummary = config ? await this.getLeagueOwnerSummary({ ownerId: 'placeholder', leagueId }) : null; + // Convert config from view model to DTO format manually with proper types + const configForm = config ? { + leagueId: config.leagueId, + basics: { + name: config.basics.name, + description: config.basics.description, + visibility: config.basics.visibility as 'public' | 'private', + }, + structure: { + mode: config.structure.mode as 'solo' | 'team', + }, + championships: [], // TODO: Map championships from view model + scoring: { + type: 'standard' as const, // TODO: Map from view model + points: 25, // TODO: Map from view model + }, + dropPolicy: { + strategy: config.dropPolicy.strategy as 'none' | 'worst_n', + n: config.dropPolicy.n ?? 0, + }, + timings: { + raceDayOfWeek: 'sunday' as const, // TODO: Map from view model + raceTimeHour: 20, // TODO: Map from view model + raceTimeMinute: 0, // TODO: Map from view model + }, + stewarding: { + decisionMode: config.stewarding.decisionMode === 'steward_vote' ? 'committee_vote' as const : 'single_steward' as const, + requireDefense: config.stewarding.requireDefense, + defenseTimeLimit: config.stewarding.defenseTimeLimit, + voteTimeLimit: config.stewarding.voteTimeLimit, + protestDeadlineHours: config.stewarding.protestDeadlineHours, + stewardingClosesHours: config.stewarding.stewardingClosesHours, + notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest, + notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired, + requiredVotes: config.stewarding.requiredVotes ?? 0, + }, + } : null; + return { - joinRequests, - ownerSummary, - config: { form: config }, + joinRequests: joinRequests.joinRequests, + ownerSummary: ownerSummary?.summary || null, + config: { form: configForm }, protests, seasons, }; @@ -411,7 +432,7 @@ export class LeagueService { }; } const presenter = new TransferLeagueOwnershipPresenter(); - presenter.present(result.unwrap()); + presenter.present({ success: true }); return presenter.getViewModel(); } diff --git a/docs/architecture/DATA_FLOW.md b/docs/architecture/DATA_FLOW.md index 52ef449e2..26bc4cf6c 100644 --- a/docs/architecture/DATA_FLOW.md +++ b/docs/architecture/DATA_FLOW.md @@ -1,361 +1,207 @@ -Frontend data shapes: View Models, Presenters, and API Client (Strict) +Frontend & Backend Output Shapes – Clean Architecture (Strict, Final) -This document defines the exact placement and responsibilities for frontend data shapes. -It is designed to leave no room for interpretation. +This document defines the exact responsibilities, naming, and placement of all data shapes involved in delivering data from Core → API → Frontend UI. + +It resolves all ambiguity around Presenters, View Models, DTOs, and Output Ports. +There is no overlap of terminology across layers. ⸻ -1. Definitions +1. Core Layer (Application / Use Cases) -API DTOs +Core Output Ports (formerly “Presenters”) -Transport shapes owned by the API boundary (HTTP). They are not used directly by UI components. +In the Core, a Presenter is not a UI concept. -View Models +It is an Output Port that defines how a Use Case emits its result. -UI-owned shapes. They represent exactly what the UI needs and nothing else. +Rules + • Core Output Ports: + • define what data is emitted + • do not store state + • do not expose getters + • do not reference DTOs or View Models + • Core never pulls data back from an output port + • Core calls present() and stops -Website Presenters +Naming + • *OutputPort + • *Result (pure application result) -Pure mappers that convert API DTOs (or core use-case outputs) into View Models. +Example -API Client +export interface CompleteDriverOnboardingResult { + readonly success: boolean; + readonly driverId?: string; + readonly error?: string; +} -A thin HTTP wrapper that returns API DTOs only and performs no business logic. +export interface CompleteDriverOnboardingOutputPort { + present(result: CompleteDriverOnboardingResult): void; +} + +The Core does not know or care what happens after present() is called. ⸻ -2. Directory layout (exact) +2. API Layer (Delivery / Adapter) -apps/website -├── app/ # Next.js routes/pages -├── components/ # React components (UI only) -├── lib/ -│ ├── api/ # API client (HTTP only) -│ ├── dtos/ # API DTO types (transport shapes) -│ ├── view-models/ # View Models (UI-owned shapes) -│ ├── presenters/ # Presenters: DTO -> ViewModel mapping -│ ├── services/ # UI orchestration (calls api + presenters) -│ └── index.ts +API Presenters (Response Mappers) -No additional folders for these concerns are allowed. +API Presenters are Adapters. + +They: + • implement Core Output Ports + • translate Core Results into API Response DTOs + • store response state temporarily for the controller + +They are not View Models. + +Rules + • API Presenters: + • implement a Core Output Port + • map Core Results → API Responses + • may store state internally + • API Presenters must not: + • contain business logic + • reference frontend View Models + +Naming + • *Presenter or *ResponseMapper + • Output types end with Response or ApiResponse ⸻ -3. View Models (placement and rules) +3. Frontend Layer (apps/website) -Where they live +View Models (UI-Owned, Final Form) -View Models MUST live in: +A View Model represents fully prepared UI state. -apps/website/lib/view-models +Only the frontend has Views — therefore only the frontend has View Models. -What they may contain - • UI-ready primitives (strings, numbers, booleans) - • UI-specific derived fields (e.g., isOwner, badgeLabel, formattedDate) - • UI-specific structures (e.g., grouped arrays, flattened objects) +Rules + • View Models: + • live only in apps/website + • accept API Response DTOs as input + • expose UI-ready data and helpers + • View Models must not: + • contain domain logic + • validate business rules + • perform side effects + • be sent back to the server -What they must NOT contain - • Domain entities or value objects - • API transport metadata - • Validation logic - • Network or persistence concerns - -Rule - -Components consume only View Models. +Naming + • *ViewModel ⸻ -4. API DTOs in the website (placement and rules) +4. Website Presenters (DTO → ViewModel) -Clarification +Website Presenters are pure mappers. -The website does have DTOs, but only API DTOs. +They: + • convert API Response DTOs into View Models + • perform formatting and reshaping + • are deterministic and side-effect free -These DTOs exist exclusively to type HTTP communication with the backend API. -They are not UI models. +They are not Core Presenters. + +Rules + • Input: API DTOs + • Output: View Models + • Must not: + • call APIs + • read storage + • perform decisions ⸻ -Where they live +5. API Client (Frontend) -Website-side API DTO types MUST live in: +The API Client is a thin HTTP layer. -apps/website/lib/dtos - -What they represent - • Exact transport shapes sent/received via HTTP - • Backend API contracts - • No UI assumptions - -Who may use them - • API client - • Website presenters - -Who must NOT use them - • React components - • Pages - • UI logic - -Rule - -API DTOs stop at the presenter boundary. - -Components must never consume API DTOs directly. - -⸻ - -5. Presenters (website) (placement and rules) - -Where they live - -Website presenters MUST live in: - -apps/website/lib/presenters - -What they do - • Convert API DTOs into View Models - • Perform UI-friendly formatting and structuring - • Are pure and deterministic - -What they must NOT do - • Make API calls - • Read from localStorage/cookies directly - • Contain business rules or decisions - • Perform side effects - -Rule - -Presenters output View Models. Presenters never output API DTOs. - -⸻ - -6. Do website presenters use View Models? - -Yes. Strictly: - -Website presenters MUST output View Models and MUST NOT output API DTOs. - -Flow is always: - -API DTO -> Presenter -> View Model -> Component - - -⸻ - -7. API client (website) (placement and rules) - -Where it lives - -The API client MUST live in: - -apps/website/lib/api - -What it does +Rules • Sends HTTP requests - • Returns API DTOs - • Performs authentication header/cookie handling only at transport level - • Does not map to View Models - -What it must NOT do - • Format or reshape responses for UI - • Contain business rules - • Contain decision logic - -Rule - -The API client has no knowledge of View Models. + • Returns API DTOs only + • Must not: + • return View Models + • contain business logic + • format data for UI ⸻ -8. Website service layer (strict orchestration) +6. Website Services (Orchestration) -Where it lives +Website Services orchestrate: + • API Client calls + • Website Presenter mappings -Website orchestration MUST live in: +They are the only layer allowed to touch both. -apps/website/lib/services - -What it does - • Calls the API client - • Calls presenters to map DTO -> View Model - • Returns View Models to pages/components - -What it must NOT do - • Contain domain logic - • Modify core invariants - • Return API DTOs - -Rule - -Services are the only layer allowed to call both api/ and presenters/. - -Components must not call the API client directly. +Rules + • Services: + • call API Client + • call Website Presenters + • return View Models only + • Components never touch API Client or DTOs ⸻ -9. Allowed dependency directions (frontend) +7. Final Data Flow (Unambiguous) -Within apps/website: +Core Use Case + → OutputPort.present(Result) -components -> services -> (api + presenters) -> (dtos + view-models) +API Presenter (Adapter) + → maps Result → ApiResponse -Strict rules: - • components may import only view-models and services - • presenters may import dtos and view-models only - • api may import dtos only - • services may import api, presenters, view-models +API Controller + → returns ApiResponse (JSON) + +Frontend API Client + → returns ApiResponse DTO + +Website Presenter + → maps DTO → ViewModel + +UI Component + → consumes ViewModel -Forbidden: - • components importing api - • components importing dtos - • presenters importing api - • api importing view-models - • any website code importing core domain entities ⸻ -10. Naming rules (strict) - • View Models end with ViewModel - • API DTOs end with Dto - • Presenters end with Presenter - • Services end with Service - • One export per file - • File name equals exported symbol (PascalCase) +8. Terminology Rules (Strict) + +Term Layer Meaning +OutputPort Core Use case output contract +Result Core Pure application result +Presenter (API) apps/api Maps Result → API Response +Response / ApiResponse apps/api HTTP transport shape +Presenter (Website) apps/website Maps DTO → ViewModel +ViewModel apps/website UI-ready state + +No term is reused with a different meaning. ⸻ -11. Final "no ambiguity" summary - • View Models live in apps/website/lib/view-models - • API DTOs live in apps/website/lib/dtos - • Presenters live in apps/website/lib/presenters and map DTO -> ViewModel - • API client lives in apps/website/lib/api and returns DTOs only - • Services live in apps/website/lib/services and return View Models only - • Components consume View Models only and never touch API DTOs or API clients +9. Non-Negotiable Rules + • Core has no DTOs + • Core has no View Models + • API has no View Models + • Frontend has no Core Results + • View Models exist only in the frontend + • Presenters mean different things per layer, but: + • Core = Output Port + • API = Adapter + • Website = Mapper ⸻ -12. Clean Architecture Flow Diagram +10. Final Merksatz -```mermaid -graph TD - A[UI Components] --> B[Services] - B --> C[API Client] - B --> D[Presenters] - C --> E[API DTOs] - D --> E - D --> F[View Models] - A --> F +The Core emits results. +The API transports them. +The Frontend interprets them. - style A fill:#e1f5fe - style B fill:#f3e5f5 - style C fill:#fff3e0 - style D fill:#e8f5e8 - style E fill:#ffebee - style F fill:#e3f2fd -``` - -**Flow Explanation:** -- UI Components consume only View Models -- Services orchestrate API calls and presenter mappings -- API Client returns raw API DTOs -- Presenters transform API DTOs into UI-ready View Models -- Strict dependency direction: UI → Services → (API + Presenters) → (DTOs + ViewModels) - -⸻ - -13. Enforcement Guidelines - -**ESLint Rules:** -- Direct imports from `apiClient` are forbidden - use services instead -- Direct imports from `dtos` in UI components are forbidden - use ViewModels instead -- Direct imports from `api/*` in UI components are forbidden - use services instead - -**TypeScript Path Mappings:** -- Use `@/lib/dtos` for API DTO imports -- Use `@/lib/view-models` for View Model imports -- Use `@/lib/presenters` for Presenter imports -- Use `@/lib/services` for Service imports -- Use `@/lib/api` for API client imports - -**Import Restrictions:** -- Components may import only view-models and services -- Presenters may import dtos and view-models only -- API may import dtos only -- Services may import api, presenters, view-models -- Forbidden: components importing api, components importing dtos, presenters importing api, api importing view-models - -**Verification Commands:** -```bash -npm run build # Ensure TypeScript compiles -npm run lint # Ensure ESLint rules pass -npm run test # Ensure all tests pass -``` - -⸻ - -14. Architecture Examples - -**Before (Violates Rules):** -```typescript -// In a page component - BAD -import { apiClient } from '@/lib/apiClient'; -import type { RaceResultDto } from '@/lib/dtos/RaceResultDto'; - -const RacePage = () => { - const [data, setData] = useState(); - // Direct API call and DTO usage in UI - useEffect(() => { - apiClient.getRaceResults().then(setData); - }, []); - return
{data?.map(d => d.position)}
; -}; -``` - -**After (Clean Architecture):** -```typescript -// In a page component - GOOD -import { RaceResultsService } from '@/lib/services/RaceResultsService'; -import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel'; - -const RacePage = () => { - const [data, setData] = useState(); - useEffect(() => { - RaceResultsService.getResults().then(setData); - }, []); - return
{data?.map(d => d.formattedPosition)}
; -}; -``` - -**Service Implementation:** -```typescript -// apps/website/lib/services/RaceResultsService.ts -import { apiClient } from '@/lib/api'; -import { RaceResultsPresenter } from '@/lib/presenters/RaceResultsPresenter'; - -export class RaceResultsService { - static async getResults(): Promise { - const dtos = await apiClient.getRaceResults(); - return RaceResultsPresenter.present(dtos); - } -} -``` - -**Presenter Implementation:** -```typescript -// apps/website/lib/presenters/RaceResultsPresenter.ts -import type { RaceResultDto } from '@/lib/dtos/RaceResultDto'; -import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel'; - -export class RaceResultsPresenter { - static present(dtos: RaceResultDto[]): RaceResultViewModel[] { - return dtos.map(dto => ({ - id: dto.id, - formattedPosition: `${dto.position}${dto.position === 1 ? 'st' : dto.position === 2 ? 'nd' : dto.position === 3 ? 'rd' : 'th'}`, - driverName: dto.driverName, - // ... other UI-specific formatting - })); - } -} -``` \ No newline at end of file +If a type tries to do more than one of these — it is incorrectly placed. \ No newline at end of file