Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

## [2026-05-20]

### Added
- Green Route Advisor v1 scenario comparison endpoint: `POST /api/v1/shipments/compare`.
Comment on lines +9 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix heading spacing in changelog sections (MD022).

### Added, ### Changed, and ### Known Limitations should each be followed by a blank line.

Proposed fix
 ### Added
+
 - Green Route Advisor v1 scenario comparison endpoint: `POST /api/v1/shipments/compare`.
@@
 ### Changed
+
 - Shipment service now compares at least two scenarios using existing `SustainabilityService` logic, with explicit rule `MIN_ESTIMATED_CO2E` and methodology version `GLEC Framework v3`.
@@
 ### Known Limitations
+
 - Full project frontend lint remains blocked by pre-existing issues outside feature scope.

Also applies to: 19-20, 23-24

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 9 - 10, Changelog headings like "### Added", "###
Changed", and "### Known Limitations" are missing a blank line after them
(MD022); update CHANGELOG.md to ensure each of those headings is followed by a
single blank line (e.g., add a newline after the "### Added" before the list
item), and apply the same fix for the other occurrences of those headings noted
in the comment so every section has a blank line after its heading.

- Scenario comparison DTOs for request/response payloads:
- `backend/src/main/java/com/ecotrack/dto/ScenarioComparisonRequestDTO.java`
- `backend/src/main/java/com/ecotrack/dto/ScenarioComparisonResponseDTO.java`
- `backend/src/main/java/com/ecotrack/dto/ScenarioInputDTO.java`
- `backend/src/main/java/com/ecotrack/dto/ScenarioResultDTO.java`
- Shipment Hub UI support for side-by-side estimated scenario comparison and preferred scenario highlighting.
- Client-side Shipment Hub status filter behavior and empty-state handling.

### Changed
- Shipment service now compares at least two scenarios using existing `SustainabilityService` logic, with explicit rule `MIN_ESTIMATED_CO2E` and methodology version `GLEC Framework v3`.
- Shipment Hub status filter resets to `All Statuses` after creating a shipment.

### Known Limitations
- Full project frontend lint remains blocked by pre-existing issues outside feature scope.
- Backend test execution in this environment is blocked by local Java compiler/toolchain initialization error.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.ecotrack.dto.ShipmentDTO;
import com.ecotrack.dto.ShipmentDetailDTO;
import com.ecotrack.dto.ScenarioComparisonRequestDTO;
import com.ecotrack.dto.ScenarioComparisonResponseDTO;
import com.ecotrack.model.enums.ShipmentStatus;
import com.ecotrack.service.ShipmentService;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -53,4 +55,9 @@ public ShipmentDTO updateStatus(@PathVariable UUID id, @RequestBody Map<String,
public List<ShipmentDTO> getLive() {
return shipmentService.getLive();
}

@PostMapping("/compare")
public ScenarioComparisonResponseDTO compare(@RequestBody ScenarioComparisonRequestDTO request) {
return shipmentService.compareScenarios(request.getScenarios());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ecotrack.dto;

import lombok.Data;

import java.util.List;

@Data
public class ScenarioComparisonRequestDTO {
private List<ScenarioInputDTO> scenarios;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ecotrack.dto;

import lombok.Builder;
import lombok.Data;

import java.util.List;

@Data
@Builder
public class ScenarioComparisonResponseDTO {
private String preferredScenario;
private String rankingRule;
private String methodologyVersion;
private List<ScenarioResultDTO> scenarios;
}
16 changes: 16 additions & 0 deletions backend/src/main/java/com/ecotrack/dto/ScenarioInputDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ecotrack.dto;

import lombok.Data;

import java.util.UUID;

@Data
public class ScenarioInputDTO {
private String name;
private String origin;
private String destination;
private Double distanceKm;
private Double payloadTons;
private String transportMode;
private UUID vehicleId;
}
19 changes: 19 additions & 0 deletions backend/src/main/java/com/ecotrack/dto/ScenarioResultDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ecotrack.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ScenarioResultDTO {
private String scenario;
private String origin;
private String destination;
private String transportMode;
private Double distanceKm;
private Double payloadTons;
private String vehicleModel;
private Double estimatedCo2;
private String estimateLabel;
private boolean preferred;
}
75 changes: 75 additions & 0 deletions backend/src/main/java/com/ecotrack/service/ShipmentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import com.ecotrack.dto.ShipmentDTO;
import com.ecotrack.dto.ShipmentDetailDTO;
import com.ecotrack.dto.ScenarioComparisonResponseDTO;
import com.ecotrack.dto.ScenarioInputDTO;
import com.ecotrack.dto.ScenarioResultDTO;
import com.ecotrack.model.EmissionLog;
import com.ecotrack.model.Shipment;
import com.ecotrack.model.Vehicle;
import com.ecotrack.model.enums.FuelType;
import com.ecotrack.model.enums.ShipmentStatus;
import com.ecotrack.model.enums.TransportMode;
import com.ecotrack.repository.EmissionLogRepository;
import com.ecotrack.repository.ShipmentRepository;
import com.ecotrack.repository.VehicleRepository;
Expand All @@ -19,6 +23,8 @@
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.Comparator;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
Expand Down Expand Up @@ -99,6 +105,75 @@ public List<ShipmentDTO> getLive() {
.stream().map(this::toDTO).collect(Collectors.toList());
}

public ScenarioComparisonResponseDTO compareScenarios(List<ScenarioInputDTO> scenarios) {
if (scenarios == null || scenarios.size() < 2) {
throw new IllegalArgumentException("At least 2 scenarios are required for comparison");
}

List<ScenarioResultDTO> results = scenarios.stream()
.map(this::buildScenarioResult)
.collect(Collectors.toList());
Comment on lines +113 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against null scenario items before mapping.

If any element in scenarios is null, Line 114 triggers a NullPointerException in buildScenarioResult, causing an internal error instead of a clear validation response.

Suggested fix
     List<ScenarioResultDTO> results = scenarios.stream()
+            .peek(s -> {
+                if (s == null) {
+                    throw new IllegalArgumentException("Scenario entries must not be null");
+                }
+            })
             .map(this::buildScenarioResult)
             .collect(Collectors.toList());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/java/com/ecotrack/service/ShipmentService.java` around lines
113 - 115, The stream mapping over scenarios can encounter null elements and
cause a NullPointerException inside buildScenarioResult; before mapping, filter
out or validate null entries from the scenarios collection (e.g., replace
scenarios.stream().map(this::buildScenarioResult) with
scenarios.stream().filter(Objects::nonNull).map(this::buildScenarioResult) or
perform an explicit pre-check that throws a clear validation exception when any
scenario is null), or alternatively detect nulls and throw a domain validation
exception with a descriptive message so callers receive a proper validation
response instead of an NPE.


ScenarioResultDTO preferred = results.stream()
.min(Comparator.comparing(ScenarioResultDTO::getEstimatedCo2))
.orElseThrow(() -> new IllegalArgumentException("No scenarios to compare"));

results.forEach(result -> result.setPreferred(Objects.equals(result.getScenario(), preferred.getScenario())));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preferred flag computation is incorrect when scenario names are duplicated.

Line 121 compares by scenario name, so duplicate names can mark multiple cards as preferred. Mark preference by object identity (or index), not display name.

Suggested fix
-        results.forEach(result -> result.setPreferred(Objects.equals(result.getScenario(), preferred.getScenario())));
+        results.forEach(result -> result.setPreferred(result == preferred));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/java/com/ecotrack/service/ShipmentService.java` at line 121,
The current lambda in results.forEach uses Objects.equals(result.getScenario(),
preferred.getScenario()) which can mark multiple items preferred if scenario
names duplicate; change the comparison to use object identity or a stable unique
identifier instead — for example, in the results.forEach(...) that calls
result.setPreferred(...), compare result == preferred (reference equality) or
compare a unique id getter (e.g., result.getId().equals(preferred.getId()))
rather than comparing result.getScenario() and preferred.getScenario().


return ScenarioComparisonResponseDTO.builder()
.preferredScenario(preferred.getScenario())
.rankingRule("MIN_ESTIMATED_CO2E")
.methodologyVersion("GLEC Framework v3")
.scenarios(results)
.build();
}

private ScenarioResultDTO buildScenarioResult(ScenarioInputDTO input) {
if (input.getDistanceKm() == null || input.getDistanceKm() <= 0) {
throw new IllegalArgumentException("distanceKm must be greater than 0");
}
if (input.getPayloadTons() == null || input.getPayloadTons() <= 0) {
throw new IllegalArgumentException("payloadTons must be greater than 0");
}
if (input.getVehicleId() == null) {
throw new IllegalArgumentException("vehicleId is required");
}

TransportMode mode;
try {
mode = TransportMode.valueOf(input.getTransportMode().toUpperCase());
} catch (Exception ex) {
throw new IllegalArgumentException("Unsupported transportMode: " + input.getTransportMode());
}
Comment on lines +145 to +147

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "ShipmentService.java" -type f

Repository: Ostaps/ecotrack

Length of output: 125


🏁 Script executed:

wc -l ./backend/src/main/java/com/ecotrack/service/ShipmentService.java

Repository: Ostaps/ecotrack

Length of output: 129


🏁 Script executed:

sed -n '135,155p' ./backend/src/main/java/com/ecotrack/service/ShipmentService.java

Repository: Ostaps/ecotrack

Length of output: 1015


🏁 Script executed:

cat > /tmp/verify_exception.java << 'EOF'
// Verifying IllegalArgumentException constructor
public class TestException {
    public static void main(String[] args) {
        try {
            throw new Exception("root cause");
        } catch (Exception ex) {
            // This should compile - IllegalArgumentException accepts Throwable cause
            throw new IllegalArgumentException("Message", ex);
        }
    }
}
EOF
# Just checking Java syntax - IllegalArgumentException(String message, Throwable cause) is standard Java API
echo "IllegalArgumentException constructors:"
echo "1. IllegalArgumentException(String message)"
echo "2. IllegalArgumentException(String message, Throwable cause)"
echo "3. IllegalArgumentException(Throwable cause)"

Repository: Ostaps/ecotrack

Length of output: 248


Preserve the original exception cause when wrapping transport mode parse errors.

Line 146 drops the caught exception ex, making error diagnostics and debugging harder. Pass the exception as the cause parameter to maintain the full error chain.

Suggested fix
         } catch (Exception ex) {
-            throw new IllegalArgumentException("Unsupported transportMode: " + input.getTransportMode());
+            throw new IllegalArgumentException("Unsupported transportMode: " + input.getTransportMode(), ex);
         }
🧰 Tools
🪛 PMD (7.24.0)

[Medium] 146-146: PreserveStackTrace (Best Practices): Thrown exception does not preserve the stack trace of exception 'ex' on all code paths

(PreserveStackTrace (Best Practices))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/java/com/ecotrack/service/ShipmentService.java` around lines
145 - 147, The catch block in ShipmentService that handles transport mode
parsing currently throws a new IllegalArgumentException without preserving the
original exception; change the throw to include the caught exception as the
cause (use the constructor that accepts a Throwable) so the original exception
`ex` is passed through when rethrowing from the catch in the method that parses
`input.getTransportMode()`.


Vehicle vehicle = vehicleRepository.findById(input.getVehicleId())
.orElseThrow(() -> new EntityNotFoundException("Vehicle not found: " + input.getVehicleId()));

Shipment shipment = Shipment.builder()
.origin(input.getOrigin())
.destination(input.getDestination())
.distanceKm(input.getDistanceKm())
.payloadTons(input.getPayloadTons())
.transportMode(mode)
.vehicle(vehicle)
.build();

double estimate = sustainabilityService.calculateEmissions(shipment, vehicle);

return ScenarioResultDTO.builder()
.scenario(input.getName())
.origin(input.getOrigin())
.destination(input.getDestination())
.transportMode(mode.name())
.distanceKm(input.getDistanceKm())
.payloadTons(input.getPayloadTons())
.vehicleModel(vehicle.getModel())
.estimatedCo2(estimate)
.estimateLabel("Estimated")
.preferred(false)
.build();
}

// ── Mapping helpers ──────────────────────────────────────────────────────

public ShipmentDTO toDTO(Shipment s) {
Expand Down
57 changes: 57 additions & 0 deletions docs/green-route-advisor-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Green Route Advisor v1

## Scope
Green Route Advisor v1 adds pre-shipment scenario comparison in Shipment Hub while reusing the existing emissions calculation path.
Comment on lines +3 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add blank lines after section headings to satisfy markdownlint MD022.

Several headings are followed immediately by content. Insert one empty line after each affected heading so docs lint passes consistently.

Proposed fix
 ## Scope
+
 Green Route Advisor v1 adds pre-shipment scenario comparison in Shipment Hub while reusing the existing emissions calculation path.
@@
 ## API Contract
+
 Endpoint: `POST /api/v1/shipments/compare`
@@
 ## UI Behavior (Shipment Hub)
+
 File: `frontend/src/pages/ShipmentHub.jsx`
@@
 ## Related Shipment Hub Fix
+
 The Shipment Hub status dropdown now:
@@
 ## Verification Status
+
 Implemented and wired across backend/frontend. Full-suite validation is partially blocked by known pre-existing/global environment issues:

Also applies to: 16-17, 35-36, 43-44, 50-51

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 3-3: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/green-route-advisor-v1.md` around lines 3 - 4, Several Markdown section
headings (e.g., "## Scope" and the other headings called out in the review) are
followed immediately by content which violates markdownlint MD022; fix by
inserting a single blank line immediately after each affected heading (every
line that begins with # or ## in this document), ensuring headings such as "##
Scope" have one empty line before the following paragraph so the linter passes.


In scope:
- Compare at least two scenarios side-by-side from planning inputs.
- Determine preferred scenario by lowest estimated CO2e.
- Return methodology traceability in API response.
- Visually mark compared values as `Estimated`.

Out of scope:
- Any parallel emissions engine or alternate formula path.
- Multi-objective optimization (cost/time/carbon weighting).

## API Contract
Endpoint: `POST /api/v1/shipments/compare`

Request body:
- `scenarios`: array of scenario objects, minimum 2.
- Scenario fields: `name`, `origin`, `destination`, `distanceKm`, `payloadTons`, `transportMode`, `vehicleId`.

Response body:
- `preferredScenario`: scenario name with lowest estimated CO2e.
- `rankingRule`: `MIN_ESTIMATED_CO2E`.
- `methodologyVersion`: `GLEC Framework v3`.
- `scenarios`: result array with per-scenario estimate and `estimateLabel` (`Estimated`).

Validation behavior:
- Rejects requests with fewer than two scenarios.
- Rejects non-positive `distanceKm` and `payloadTons`.
- Rejects unsupported `transportMode` values.
- Rejects unknown `vehicleId` values.

## UI Behavior (Shipment Hub)
File: `frontend/src/pages/ShipmentHub.jsx`

- Adds a Green Route Advisor compare panel with two scenario inputs.
- Calls `compareShipmentScenarios` from `frontend/src/api/shipments.js`.
- Renders side-by-side estimated results and highlights preferred scenario.
- Shows ranking rule and methodology version returned by backend.

## Related Shipment Hub Fix
The Shipment Hub status dropdown now:
- Updates React state on selection.
- Filters shipment rows client-side (no extra request).
- Shows an explicit empty state when no rows match.
- Resets to `All Statuses` after successful shipment creation.

## Verification Status
Implemented and wired across backend/frontend. Full-suite validation is partially blocked by known pre-existing/global environment issues:
- Frontend project-wide lint includes unrelated violations in other pages.
- Backend `mvn test` fails in this environment due to toolchain initialization error.

Targeted validation completed during implementation:
- Changed Shipment Hub file lint passes.
- Compare flow wiring and response rendering are implemented and integrated.
5 changes: 5 additions & 0 deletions frontend/src/api/shipments.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ export const getLiveShipments = async () => {
const { data } = await client.get('/shipments/live');
return data;
};

export const compareShipmentScenarios = async (scenarios) => {
const { data } = await client.post('/shipments/compare', { scenarios });
return data;
};
Loading