Repository: krystxf/metro-now
Pull request #73: feat(be): extend route type
Author: login: krystxf
Created: 2025-05-06T00:18:27Z | Merged: 2025-05-06T00:41:34Z

# What
- `isSubstitute`
- `isNight`
- `vehicleType`
- make departure predicted time not nullable


Diff:
diff --git a/apps/backend/src/modules/departure/schema.graphql b/apps/backend/src/modules/departure/schema.graphql
index d4d14671..86caa79d 100644
--- a/apps/backend/src/modules/departure/schema.graphql
+++ b/apps/backend/src/modules/departure/schema.graphql
@@ -1,5 +1,5 @@
 type DepartureTime {
-    predicted: ISODateTime
+    predicted: ISODateTime!
     scheduled: ISODateTime!
 }
 
diff --git a/apps/backend/src/modules/route/route.const.ts b/apps/backend/src/modules/route/route.const.ts
new file mode 100644
index 00000000..4501e274
--- /dev/null
+++ b/apps/backend/src/modules/route/route.const.ts
@@ -0,0 +1,18 @@
+export const METRO_LINES = ["A", "B", "C", "D"];
+
+export const TRAIN_PREFIXES = [
+    "L",
+    "R",
+    "S",
+    "T", // turistic
+    "U",
+    "V",
+];
+
+export const BUS_PREFIXES = [
+    "AE", // airport
+    "K", // stadion strahov bus
+    "BB", // brumlovka
+    "MHD ",
+    "ŠKOLNÍ ",
+];
diff --git a/apps/backend/src/modules/route/route.resolver.ts b/apps/backend/src/modules/route/route.resolver.ts
index 55fa66a0..df81b0e8 100644
--- a/apps/backend/src/modules/route/route.resolver.ts
+++ b/apps/backend/src/modules/route/route.resolver.ts
@@ -1,6 +1,8 @@
-import { Args, Query, Resolver } from "@nestjs/graphql";
+import { Args, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql";
 
 import { RouteService } from "src/modules/route/route.service";
+import { VehicleType } from "src/types/graphql.generated";
+import { ParentType } from "src/types/parent";
 
 @Resolver("Route")
 export class RouteResolver {
@@ -15,4 +17,21 @@ export class RouteResolver {
     getMany() {
         return this.routeService.getManyGraphQL();
     }
+
+    @ResolveField("isSubstitute")
+    getIsSubstitute(@Parent() route: ParentType<typeof this.getMany>) {
+        return this.routeService.isSubstitute(route.name);
+    }
+
+    @ResolveField("isNight")
+    getIsNight(@Parent() route: ParentType<typeof this.getMany>) {
+        return this.routeService.isNight(route.name);
+    }
+
+    @ResolveField("vehicleType")
+    getVehicleType(
+        @Parent() route: ParentType<typeof this.getMany>,
+    ): VehicleType {
+        return this.routeService.getVehicleType(route.name);
+    }
 }
diff --git a/apps/backend/src/modules/route/route.service.ts b/apps/backend/src/modules/route/route.service.ts
index dbc0fed5..5ce2785b 100644
--- a/apps/backend/src/modules/route/route.service.ts
+++ b/apps/backend/src/modules/route/route.service.ts
@@ -1,8 +1,14 @@
 import { Injectable } from "@nestjs/common";
 import { Prisma } from "@prisma/client";
-import { group, unique } from "radash";
+import { group } from "radash";
 
 import { PrismaService } from "src/modules/prisma/prisma.service";
+import {
+    BUS_PREFIXES,
+    METRO_LINES,
+    TRAIN_PREFIXES,
+} from "src/modules/route/route.const";
+import { VehicleType } from "src/types/graphql.generated";
 
 const gtfsRouteSelect = {
     id: true,
@@ -38,49 +44,6 @@ const gtfsRouteSelect = {
 export class RouteService {
     constructor(private prisma: PrismaService) {}
 
-    async getRoute(id: string) {
-        const route = await this.prisma.gtfsRoute.findFirst({
-            select: gtfsRouteSelect,
-            where: {
-                id,
-            },
-        });
-
-        const routeStops = await this.prisma.gtfsRouteStop.findMany({
-            select: gtfsRouteSelect.GtfsRouteStop.select,
-            where: {
-                routeId: id,
-            },
-            orderBy: {
-                stopSequence: "asc",
-            },
-        });
-
-        const stops = await this.prisma.platform.findMany({
-            select: gtfsRouteSelect.GtfsRouteStop.select.platform.select,
-            where: {
-                id: {
-                    in: unique(
-                        routeStops
-                            .map((item) => item.stopId)
-                            .filter((item) => item !== null),
-                    ),
-                },
-            },
-        });
-
-        return {
-            ...route,
-            directions: group(
-                routeStops.map((routeStop) => ({
-                    ...routeStop,
-                    stop: stops.find((stop) => stop.id === routeStop.stopId),
-                })),
-                (item) => item.directionId,
-            ),
-        };
-    }
-
     private processRoute(
         route: Prisma.GtfsRouteGetPayload<{ select: typeof gtfsRouteSelect }>,
     ) {
@@ -122,4 +85,59 @@ export class RouteService {
 
         return this.processRoute(route);
     }
+
+    isSubstitute(routeName: string): boolean {
+        return routeName.startsWith("X");
+    }
+
+    private getNameWithoutSubstitute(routeName: string): string {
+        return this.isSubstitute(routeName) ? routeName.slice(1) : routeName;
+    }
+
+    isNight(routeName: string): boolean {
+        const routeNameParsed = this.getNameWithoutSubstitute(routeName);
+        const routeNumber = parseInt(routeNameParsed);
+
+        if (isNaN(routeNumber)) {
+            return false;
+        }
+
+        return (
+            (routeNumber >= 90 && routeNumber < 100) ||
+            (routeNumber >= 900 && routeNumber < 1000)
+        );
+    }
+
+    getVehicleType(routeName: string): VehicleType {
+        const routeNameParsed = this.getNameWithoutSubstitute(routeName);
+        const routeNumber = parseInt(routeNameParsed);
+
+        if (!isNaN(routeNumber)) {
+            if (routeNumber === 58 || routeNumber === 59) {
+                return VehicleType.TROLLEYBUS;
+            }
+            if (routeNumber < 100) {
+                return VehicleType.TRAM;
+            }
+            return VehicleType.BUS;
+        }
+        if (METRO_LINES.includes(routeNameParsed)) {
+            return VehicleType.SUBWAY;
+        }
+        if (routeName.startsWith("P")) {
+            return VehicleType.FERRY;
+        }
+        if (routeName === "LD") {
+            return VehicleType.FUNICULAR;
+        }
+        if (
+            TRAIN_PREFIXES.some((prefix) => routeNameParsed.startsWith(prefix))
+        ) {
+            return VehicleType.TRAIN;
+        }
+        if (BUS_PREFIXES.some((prefix) => routeNameParsed.startsWith(prefix))) {
+            return VehicleType.BUS;
+        }
+        return VehicleType.BUS;
+    }
 }
diff --git a/apps/backend/src/modules/route/schema.graphql b/apps/backend/src/modules/route/schema.graphql
index b6057eda..f654461d 100644
--- a/apps/backend/src/modules/route/schema.graphql
+++ b/apps/backend/src/modules/route/schema.graphql
@@ -1,3 +1,13 @@
+enum VehicleType {
+    BUS
+    TRAM
+    FERRY
+    TRAIN
+    FUNICULAR
+    SUBWAY
+    TROLLEYBUS
+}
+
 type RouteDirection {
     id: ID!
     """
@@ -10,6 +20,18 @@ type Route {
     id: ID!
     name: String
     directions: [RouteDirection!]!
+    isSubstitute: Boolean!
+
+    """
+    might not be accurate if isSubstitute is true
+    (shows the original vehicle type, not the substitute vehicle type)
+    """
+    vehicleType: VehicleType!
+
+    """
+    applies to BUS and TRAM only - defaults to false
+    """
+    isNight: Boolean!
 }
 
 type Query {


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR extends the route type functionality by introducing new route properties including isSubstitute, isNight, and vehicleType.  
- Added constants for metro lines, train prefixes, and bus prefixes.  
- Introduced isSubstitute function in the utils module.  
- Added new resolve field methods (isSubstitute, isNight, vehicleType) in the route resolver supporting the new route features.

### Reviewed Changes

Copilot reviewed 2 out of 4 changed files in this pull request and generated 2 comments.

| File                                                        | Description                                                  |
| ----------------------------------------------------------- | ------------------------------------------------------------ |
| apps/backend/src/modules/route/route.utils.ts              | Added constant arrays and isSubstitute utility                |
| apps/backend/src/modules/route/route.resolver.ts             | Implemented new GraphQL resolver fields for isSubstitute, isNight, and vehicleType |

<details>
<summary>Files not reviewed (2)</summary>

* **apps/backend/src/modules/departure/schema.graphql**: Language not supported
* **apps/backend/src/modules/route/schema.graphql**: Language not supported
</details>



state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T00:19:09Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/route/route.utils.ts (1)</summary><blockquote>

`3-11`: **Clear train prefix definitions with helpful comments**

The train prefixes are well-defined and the comment for "T" is helpful. Consider adding similar comments for other prefixes to improve code documentation.


Consider adding brief comments for the other train prefixes to explain what they represent, similar to how you've done for "T".

</blockquote></details>
<details>
<summary>apps/backend/src/modules/route/route.resolver.ts (2)</summary><blockquote>

`81-88`: **Good fallback logic for vehicle type determination**

The code properly checks prefixes for TRAIN and BUS types, with a reasonable default to BUS when no other conditions match.


Consider adding a comment explaining why BUS is chosen as the default fallback to help future developers understand this decision.

---

`52-71`: <details>
<summary>❓ Verification inconclusive</summary>

**Improve vehicleType resolution logic for numeric routes**

The current implementation has good handling for substitute routes and numeric parsing.


Consider adding comments explaining why routes 58 and 59 are specifically identified as trolleybus routes, as this domain knowledge might not be obvious to all developers.


Verify that these are the only trolleybus routes that will ever exist in your system. If there might be more in the future, consider a more extensible approach:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search the codebase for any other references to trolleybus routes
grep -r "trolleybus" --include="*.ts" --include="*.js" --include="*.md" .
```

Length of output: 73

---



</details>

**Clarify and Future-Proof Trolleybus Route Logic**

Please add context and verify the hard-coded trolleybus routes to make the intent clear and support future changes:

• File: apps/backend/src/modules/route/route.resolver.ts  
  Lines: ~52–71  

• Add a brief comment above the `if (routeNumber === 58 || routeNumber === 59)` block explaining why routes 58 and 59 are trolleybuses (e.g. “58 and 59 are city-designated trolleybus lines”).  
• Confirm that routes 58 and 59 are the only trolleybus lines now and will remain so; if additional trolleybus routes may be added later, consider:  
  – Moving these numbers into a shared constant or configuration (e.g. `TROLLEYBUS_ROUTE_NUMBERS = [58, 59]`)  
  – Using a lookup or data-driven approach instead of hard-coding.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a779e4360459b83349e0f35c62e406539ae6d0e4 and b015272935a05cbab0a9f18339731f1137b8b72d.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/backend/src/modules/departure/schema.graphql` (1 hunks)
* `apps/backend/src/modules/route/route.resolver.ts` (2 hunks)
* `apps/backend/src/modules/route/route.utils.ts` (1 hunks)
* `apps/backend/src/modules/route/schema.graphql` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>apps/backend/src/modules/route/route.resolver.ts (2)</summary><blockquote>

<details>
<summary>apps/backend/src/types/parent.ts (1)</summary>

* `ParentType` (3-5)

</details>
<details>
<summary>apps/backend/src/modules/route/route.utils.ts (4)</summary>

* `isSubstitute` (20-22)
* `METRO_LINES` (1-1)
* `TRAIN_PREFIXES` (3-10)
* `BUS_PREFIXES` (12-18)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/departure/schema.graphql (1)</summary>

`2-2`: **Non-nullable change for predicted field**

The change from nullable to non-nullable for the `predicted` field ensures a more reliable API contract. This is a good improvement, making sure clients always receive a valid datetime value.

Just ensure that your backend implementation always provides a valid value for this field, as nulls will now cause GraphQL execution errors instead of being passed to clients.

</details>
<details>
<summary>apps/backend/src/modules/route/route.utils.ts (3)</summary>

`1-2`: **Well-defined metro constants**

Good job defining the metro lines as constants. This makes the code more maintainable and prevents hardcoding values throughout the codebase.

---

`12-19`: **Comprehensive bus prefix list with good documentation**

The bus prefixes list is well-documented with comments explaining the special cases. This helps other developers understand the domain-specific knowledge.

---

`20-22`: **Simple but effective substitute detection function**

The `isSubstitute` function is concise and clearly expresses its intent. The function name clearly describes what it does, which is good for readability.

</details>
<details>
<summary>apps/backend/src/modules/route/schema.graphql (4)</summary>

`1-9`: **Comprehensive VehicleType enum**

The VehicleType enum covers a wide range of transit vehicle types, which is good for future extensibility. The naming follows standard GraphQL enum conventions (all caps).

---

`23-24`: **Clear isSubstitute field definition**

The isSubstitute field is properly defined as a non-nullable Boolean, which is appropriate for this type of flag field.

---

`25-30`: **Helpful comment on vehicleType field limitations**

The comment about potential inaccuracy in vehicleType when isSubstitute is true is valuable. It correctly sets expectations for API consumers about the data they'll receive.

---

`31-35`: **Clear documentation for isNight field**

The comment specifying that isNight only applies to BUS and TRAM is important for API users to understand the field's limitations.

</details>
<details>
<summary>apps/backend/src/modules/route/route.resolver.ts (3)</summary>

`4-11`: **Well-organized imports**

Good job organizing imports by grouping related imports together. The explicit imports from route.utils.ts make it clear which utilities are being used.

---

`27-30`: **Concise isSubstitute resolver implementation**

The implementation is clean and reuses the utility function appropriately.

---

`72-80`: **Clear vehicle type checks for non-numeric routes**

The checks for SUBWAY, FERRY, and FUNICULAR are straightforward and easy to understand.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T00:21:25Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR extends the route type by introducing three new route characteristic methods and updates the GraphQL resolver to expose them. Key changes include:
- Addition of the isSubstitute, isNight, and getVehicleType methods in the route service.
- Extension of the GraphQL resolver with new fields that leverage the service methods.
- Introduction of a constants file with updated prefix lists used to determine route types.

### Reviewed Changes

Copilot reviewed 3 out of 5 changed files in this pull request and generated 3 comments.

| File | Description |
| ---- | ----------- |
| apps/backend/src/modules/route/route.service.ts | New methods for route type determination and removal of legacy getRoute implementation |
| apps/backend/src/modules/route/route.resolver.ts | Extended resolver to expose the new route properties |
| apps/backend/src/modules/route/route.const.ts | New constants definitions used in route type logic |

<details>
<summary>Files not reviewed (2)</summary>

* **apps/backend/src/modules/departure/schema.graphql**: Language not supported
* **apps/backend/src/modules/route/schema.graphql**: Language not supported
</details>



state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T00:37:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/route/route.service.ts (2)</summary><blockquote>

`97-109`: **Consider documenting night route classification logic**

The implementation for detecting night routes based on numeric ranges (90-99 and 900-999) is logical, but lacks documentation explaining why these specific ranges indicate night routes. This might not be obvious to developers unfamiliar with the transit system's conventions.

Also, be cautious of potential edge cases with mixed alphanumeric route names (e.g., "95A"), which might be incorrectly classified by the current implementation.

Consider adding a comment explaining the night route classification rules:

```diff
 isNight(routeName: string): boolean {
+    // Night routes are identified by numbers in ranges 90-99 and 900-999
+    // according to Prague public transport convention
     const routeNameParsed = this.getNameWithoutSubstitute(routeName);
     const routeNumber = parseInt(routeNameParsed);
```

---

`111-142`: **Improve maintainability of vehicle type classification**

The `getVehicleType` method contains a significant number of conditional checks without documentation explaining the business rules. While the code is functional, it could be enhanced for better maintainability and clarity.


Consider these improvements:

1. Add comments documenting the transit system rules that determine vehicle types
2. Consider using a more declarative approach to mapping patterns to vehicle types
3. Consider adding logging for unexpected route patterns instead of silently defaulting to BUS

Example refactor:

```diff
 getVehicleType(routeName: string): VehicleType {
+    // Process the route name to handle substitute routes
     const routeNameParsed = this.getNameWithoutSubstitute(routeName);
     const routeNumber = parseInt(routeNameParsed);

     if (!isNaN(routeNumber)) {
+        // Routes 58 and 59 are special trolleybus routes in Prague
         if (routeNumber === 58 || routeNumber === 59) {
             return VehicleType.TROLLEYBUS;
         }
+        // Routes with numbers 1-99 are trams
         if (routeNumber < 100) {
             return VehicleType.TRAM;
         }
+        // Routes with numbers 100+ are buses
         return VehicleType.BUS;
     }
+    // Metro lines are identified by single letters A, B, C, D
     if (METRO_LINES.includes(routeNameParsed)) {
         return VehicleType.SUBWAY;
     }
+    // Ferry routes start with 'P'
     if (routeName.startsWith("P")) {
         return VehicleType.FERRY;
     }
+    // LD is the funicular route (Lanová Dráha)
     if (routeName === "LD") {
         return VehicleType.FUNICULAR;
     }
+    // Train routes start with specific prefixes
     if (
         TRAIN_PREFIXES.some((prefix) => routeNameParsed.startsWith(prefix))
     ) {
         return VehicleType.TRAIN;
     }
+    // Bus routes with special prefixes
     if (BUS_PREFIXES.some((prefix) => routeNameParsed.startsWith(prefix))) {
         return VehicleType.BUS;
     }
+    // Default fallback for unrecognized patterns
+    console.warn(`Unrecognized route pattern for vehicle type: ${routeName}`);
     return VehicleType.BUS;
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b015272935a05cbab0a9f18339731f1137b8b72d and 66dcecbad6bc0f81a0089ef0c0434937149b26fb.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/backend/src/modules/route/route.const.ts` (1 hunks)
* `apps/backend/src/modules/route/route.resolver.ts` (2 hunks)
* `apps/backend/src/modules/route/route.service.ts` (2 hunks)
* `apps/backend/src/modules/route/schema.graphql` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/backend/src/modules/route/route.const.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* apps/backend/src/modules/route/route.resolver.ts
* apps/backend/src/modules/route/schema.graphql

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>apps/backend/src/modules/route/route.service.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/route/route.const.ts (3)</summary>

* `METRO_LINES` (1-1)
* `TRAIN_PREFIXES` (3-10)
* `BUS_PREFIXES` (12-18)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build
* GitHub Check: Format check 💅

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>apps/backend/src/modules/route/route.service.ts (3)</summary>

`3-11`: **Import changes reflect new functionality requirements**

The imports have been updated to include the necessary constants and types for the new route classification functionality, while removing the unused `unique` import. These changes are appropriate for the new features being added.

---

`89-95`: **Well-implemented utility methods for substitute routes**

The methods `isSubstitute` and `getNameWithoutSubstitute` are well-designed with clear responsibilities and good naming practices. The implementation is straightforward and handles the substitute route identification logic effectively.

---

`88-142`: **Good implementation of route classification methods**

The added methods for route classification are well-structured and implement the required functionality for the new route properties: `isSubstitute`, `isNight`, and `vehicleType`. The methods follow a consistent pattern of handling substitute routes and have clear, focused responsibilities.

These changes align well with the PR objectives to extend the route type with additional properties.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T00:38:44Z

Review comments:
diff_hunk: @@ -15,4 +23,67 @@ export class RouteResolver {
     getMany() {
         return this.routeService.getManyGraphQL();
     }
+
+    @ResolveField("isSubstitute")
+    getIsSubstitute(@Parent() route: ParentType<typeof this.getMany>) {
+        return isSubstitute(route.name);
+    }
+
+    @ResolveField("isNight")
+    getIsNight(@Parent() route: ParentType<typeof this.getMany>) {
+        const regex = /(X)?[0-9]{2,3}/;
+        if (!regex.test(route.name)) {
+            return false;
+        }
+
+        const routeNumber = route.name.match(/(X)?[0-9]{2,3}/)?.[0];
+        if (routeNumber === undefined) {
+            return false;
+        }
+
+        const routeNumberInt = parseInt(routeNumber);
path: apps/backend/src/modules/route/route.resolver.ts
user: login: Copilot
body: Consider removing the substitute prefix (e.g., 'X') from the route name before applying the regex in getIsNight. This adjustment will align its behavior with getVehicleType and ensure correct numeric parsing for routes marked as substitute.
```suggestion
        const routeName = isSubstitute(route.name)
            ? route.name.slice(1)
            : route.name;

        const regex = /^[0-9]{2,3}$/;
        if (!regex.test(routeName)) {
            return false;
        }

        const routeNumberInt = parseInt(routeName);
```
created_at: 2025-05-06T00:19:09Z
author_association: NONE
diff_hunk: @@ -15,4 +23,67 @@ export class RouteResolver {
     getMany() {
         return this.routeService.getManyGraphQL();
     }
+
+    @ResolveField("isSubstitute")
+    getIsSubstitute(@Parent() route: ParentType<typeof this.getMany>) {
+        return isSubstitute(route.name);
+    }
+
+    @ResolveField("isNight")
+    getIsNight(@Parent() route: ParentType<typeof this.getMany>) {
+        const regex = /(X)?[0-9]{2,3}/;
+        if (!regex.test(route.name)) {
+            return false;
+        }
+
+        const routeNumber = route.name.match(/(X)?[0-9]{2,3}/)?.[0];
+        if (routeNumber === undefined) {
+            return false;
+        }
+
+        const routeNumberInt = parseInt(routeNumber);
+
+        return (
+            (routeNumberInt >= 90 && routeNumberInt < 100) ||
+            (routeNumberInt >= 900 && routeNumberInt < 1000)
+        );
+    }
+
+    @ResolveField("vehicleType")
+    getVehicleType(@Parent() route: ParentType<typeof this.getMany>) {
+        const routeName = isSubstitute(route.name)
+            ? route.name.slice(1)
+            : route.name;
+
+        const routeNumberParsed = parseInt(routeName);
+        const routeNumber = isNaN(routeNumberParsed)
+            ? undefined
+            : routeNumberParsed;
+
+        if (routeNumber !== undefined) {
+            if (routeNumber === 58 || routeNumber === 59) {
+                return VehicleType.TROLLEYBUS;
+            }
+            if (routeNumber < 100) {
+                return VehicleType.TRAM;
+            }
+            return VehicleType.BUS;
+        }
+        if (METRO_LINES.includes(routeName)) {
+            return VehicleType.SUBWAY;
+        }
+        if (routeName.startsWith("P")) {
+            return VehicleType.FERRY;
+        }
+        if (routeName === "LD") {
+            return VehicleType.FUNICULAR;
+        }
+        if (TRAIN_PREFIXES.some((prefix) => routeName.startsWith(prefix))) {
+            return VehicleType.TRAIN;
+        }
+        if (BUS_PREFIXES.some((prefix) => routeName.startsWith(prefix))) {
+            return VehicleType.BUS;
+        }
+        return VehicleType.BUS;
path: apps/backend/src/modules/route/route.resolver.ts
user: login: Copilot
body: [nitpick] The fallback logic for vehicleType redundantly returns BUS. Consider consolidating this logic to improve clarity and reduce potential confusion.
```suggestion

```
created_at: 2025-05-06T00:19:09Z
author_association: NONE
diff_hunk: @@ -15,4 +23,67 @@ export class RouteResolver {
     getMany() {
         return this.routeService.getManyGraphQL();
     }
+
+    @ResolveField("isSubstitute")
+    getIsSubstitute(@Parent() route: ParentType<typeof this.getMany>) {
+        return isSubstitute(route.name);
+    }
+
+    @ResolveField("isNight")
+    getIsNight(@Parent() route: ParentType<typeof this.getMany>) {
+        const regex = /(X)?[0-9]{2,3}/;
+        if (!regex.test(route.name)) {
+            return false;
+        }
+
+        const routeNumber = route.name.match(/(X)?[0-9]{2,3}/)?.[0];
+        if (routeNumber === undefined) {
+            return false;
+        }
+
+        const routeNumberInt = parseInt(routeNumber);
+
+        return (
+            (routeNumberInt >= 90 && routeNumberInt < 100) ||
+            (routeNumberInt >= 900 && routeNumberInt < 1000)
+        );
+    }
path: apps/backend/src/modules/route/route.resolver.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Refactor regex pattern matching in isNight resolver**

The implementation contains duplicated regex logic and can be optimized.


```diff
@ResolveField("isNight")
getIsNight(@Parent() route: ParentType<typeof this.getMany>) {
-    const regex = /(X)?[0-9]{2,3}/;
-    if (!regex.test(route.name)) {
-        return false;
-    }
-
-    const routeNumber = route.name.match(/(X)?[0-9]{2,3}/)?.[0];
-    if (routeNumber === undefined) {
-        return false;
-    }
-
-    const routeNumberInt = parseInt(routeNumber);
+    // Extract only numeric part regardless of X prefix
+    const numericMatch = route.name.match(/(?:X)?([0-9]{2,3})/);
+    if (!numericMatch || !numericMatch[1]) {
+        return false;
+    }
+
+    const routeNumberInt = parseInt(numericMatch[1]);

    return (
        (routeNumberInt >= 90 && routeNumberInt < 100) ||
        (routeNumberInt >= 900 && routeNumberInt < 1000)
    );
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    @ResolveField("isNight")
    getIsNight(@Parent() route: ParentType<typeof this.getMany>) {
        // Extract only numeric part regardless of X prefix
        const numericMatch = route.name.match(/(?:X)?([0-9]{2,3})/);
        if (!numericMatch || !numericMatch[1]) {
            return false;
        }

        const routeNumberInt = parseInt(numericMatch[1]);

        return (
            (routeNumberInt >= 90 && routeNumberInt < 100) ||
            (routeNumberInt >= 900 && routeNumberInt < 1000)
        );
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T00:21:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,18 @@
+export const METRO_LINES = ["A", "B", "C", "D"];
+
+export const TRAIN_PREFIXES = [
+    "L",
+    "R",
+    "S",
+    "T", // turistic
+    "U",
+    "V",
+];
+
+export const BUS_PREFIXES = [
+    "AE", // airport
+    "K", // stadion strahov bus
+    "BB", // brumlovka
+    "MHD ",
path: apps/backend/src/modules/route/route.const.ts
user: login: Copilot
body: Consider removing or clarifying the trailing whitespace in 'MHD ' to ensure consistent string matching for route prefixes.
```suggestion
    "MHD",
```
created_at: 2025-05-06T00:37:22Z
author_association: NONE
diff_hunk: @@ -122,4 +85,59 @@ export class RouteService {
 
         return this.processRoute(route);
     }
+
+    isSubstitute(routeName: string): boolean {
+        return routeName.startsWith("X");
+    }
+
+    private getNameWithoutSubstitute(routeName: string): string {
+        return this.isSubstitute(routeName) ? routeName.slice(1) : routeName;
+    }
+
+    isNight(routeName: string): boolean {
+        const routeNameParsed = this.getNameWithoutSubstitute(routeName);
+        const routeNumber = parseInt(routeNameParsed);
+
+        if (isNaN(routeNumber)) {
+            return false;
+        }
+
+        return (
+            (routeNumber >= 90 && routeNumber < 100) ||
+            (routeNumber >= 900 && routeNumber < 1000)
+        );
+    }
+
+    getVehicleType(routeName: string): VehicleType {
+        const routeNameParsed = this.getNameWithoutSubstitute(routeName);
+        const routeNumber = parseInt(routeNameParsed);
+
+        if (!isNaN(routeNumber)) {
+            if (routeNumber === 58 || routeNumber === 59) {
+                return VehicleType.TROLLEYBUS;
+            }
+            if (routeNumber < 100) {
+                return VehicleType.TRAM;
+            }
+            return VehicleType.BUS;
+        }
+        if (METRO_LINES.includes(routeNameParsed)) {
+            return VehicleType.SUBWAY;
+        }
+        if (routeName.startsWith("P")) {
+            return VehicleType.FERRY;
+        }
+        if (routeName === "LD") {
+            return VehicleType.FUNICULAR;
+        }
+        if (
+            TRAIN_PREFIXES.some((prefix) => routeNameParsed.startsWith(prefix))
+        ) {
+            return VehicleType.TRAIN;
+        }
+        if (BUS_PREFIXES.some((prefix) => routeNameParsed.startsWith(prefix))) {
+            return VehicleType.BUS;
+        }
+        return VehicleType.BUS;
path: apps/backend/src/modules/route/route.service.ts
user: login: Copilot
body: [nitpick] The fallback to return VehicleType.BUS appears in multiple branches; consider refactoring the logic to reduce redundancy and improve readability.
```suggestion
            // No explicit return here; fallback will handle it.
        }
        return VehicleType.BUS; // Consolidated fallback.
```
created_at: 2025-05-06T00:37:22Z
author_association: NONE
diff_hunk: @@ -122,4 +85,59 @@ export class RouteService {
 
         return this.processRoute(route);
     }
+
path: apps/backend/src/modules/route/route.service.ts
user: login: Copilot
body: [nitpick] Consider adding brief comments to explain the logic in the new isSubstitute, isNight, and getVehicleType methods for future maintainability.
```suggestion

    /**
     * Determines if the route is a substitute route.
     * Substitute routes are identified by names starting with "X".
     */
```
created_at: 2025-05-06T00:37:23Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-05-06T00:18:31Z
body: [vc]: #pLuD7TGC3nKfDmhrYPZ8guKejL6Y3SLt9YLylzzF3P0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJtZXRyby1ub3ciLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoibWV0cm8tbm93LWdpdC1mZWF0LWJlLWV4dGVuZC1yb3V0ZS1rcnlzdG9mcy1wcm9qZWN0cy1lMjMyMjI1My52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9rcnlzdG9mcy1wcm9qZWN0cy1lMjMyMjI1My9tZXRyby1ub3cvRXNRS3JUOEFNa1NtZFptR2ZYUFB1eVNDOFZFciIsInByZXZpZXdVcmwiOiJtZXRyby1ub3ctZ2l0LWZlYXQtYmUtZXh0ZW5kLXJvdXRlLWtyeXN0b2ZzLXByb2plY3RzLWUyMzIyMjUzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJyb290RGlyZWN0b3J5IjoiYXBwcy93ZWIifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **metro-now** | ✅ Ready ([Inspect](https://vercel.com/krystofs-projects-e2322253/metro-now/EsQKrT8AMkSmdZmGfXPPuySC8VEr)) | [Visit Preview](https://metro-now-git-feat-be-extend-route-krystofs-projects-e2322253.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/metro-now-git-feat-be-extend-route-krystofs-projects-e2322253.vercel.app?via=pr-comment-feedback-link) | May 6, 2025 0:37am |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-06T00:21:21Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

The changes introduce new classification logic and fields for routes in the backend. The GraphQL schema and resolver for routes are extended with `isSubstitute`, `isNight`, and `vehicleType` fields. Supporting logic and constants are added to determine these properties based on route names, and the `predicted` field in `DepartureTime` is made non-nullable.

## Changes

| Files/Paths                                                                                   | Change Summary                                                                                                                      |
|----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| apps/backend/src/modules/departure/schema.graphql                                             | Made the `predicted` field in the `DepartureTime` type non-nullable.                                                                |
| apps/backend/src/modules/route/route.resolver.ts                                              | Added resolver methods for `isSubstitute`, `isNight`, and `vehicleType` fields on the `Route` type.                                 |
| apps/backend/src/modules/route/schema.graphql                                                 | Added `isSubstitute`, `isNight`, and `vehicleType` fields to the `Route` type; introduced the `VehicleType` enum.                  |
| apps/backend/src/modules/route/route.const.ts                                                 | Added new constants: `METRO_LINES`, `TRAIN_PREFIXES`, and `BUS_PREFIXES` arrays for route classification.                           |
| apps/backend/src/modules/route/route.service.ts                                               | Removed the `getRoute` method; added `isSubstitute`, `isNight`, `getVehicleType`, and a helper for route name parsing.              |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant GraphQL API
    participant RouteResolver
    participant RouteService

    Client->>GraphQL API: Query Route { isSubstitute, isNight, vehicleType }
    GraphQL API->>RouteResolver: Resolve isSubstitute
    RouteResolver->>RouteService: isSubstitute(route.name)
    RouteService-->>RouteResolver: Boolean
    GraphQL API->>RouteResolver: Resolve isNight
    RouteResolver->>RouteService: isNight(route.name)
    RouteService-->>RouteResolver: Boolean
    GraphQL API->>RouteResolver: Resolve vehicleType
    RouteResolver->>RouteService: getVehicleType(route.name)
    RouteService-->>RouteResolver: VehicleType
    RouteResolver-->>GraphQL API: Aggregated Route fields
    GraphQL API-->>Client: Route { isSubstitute, isNight, vehicleType }
```

## Possibly related PRs

- krystxf/metro-now#64: Introduces the initial GTFS-based route endpoint and database models, which the current PR extends with additional route classification and vehicle type logic.

## Suggested labels

`enhancement`, `backend`

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=krystxf/metro-now&utm_content=73):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE