Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,5 @@ From `src/BikeTracking.Frontend`:
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan:
[specs/026-year-stats-dashboard/plan.md](../specs/026-year-stats-dashboard/plan.md)
[specs/027-mileage-rate-savings/plan.md](../specs/027-mileage-rate-savings/plan.md)
<!-- SPECKIT END -->
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/026-year-stats-dashboard"
"feature_directory": "specs/027-mileage-rate-savings"
}
2 changes: 1 addition & 1 deletion specs/012-dashboard-stats/research.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ introducing Tailwind.
## Decision 6: Expose two baseline savings calculations plus a combined total

**Decision**: The dashboard money-saved model will include:
- `MileageRateSavings`: `miles * mileageRateCents / 100`
- `MileageRateSavings`: `miles * mileageRateCents`
- `FuelCostAvoided`: `(miles / averageCarMpg) * gasPricePerGallon`
- `CombinedSavings`: sum of the two only when both components are calculable for the included rides

Expand Down
2 changes: 1 addition & 1 deletion specs/018-advanced-dashboard/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fuel_cost_estimated = any ride in window has GasPricePerGallon IS NULL
### Mileage Rate Savings (per window)

```
mileage_rate_savings_for_ride = ride.Miles × ride.SnapshotMileageRateCents / 100
mileage_rate_savings_for_ride = ride.Miles × ride.SnapshotMileageRateCents
total_mileage_rate_savings = Σ mileage_rate_savings_for_ride

mileage_rate_savings = null if no rides have SnapshotMileageRateCents set
Expand Down
2 changes: 1 addition & 1 deletion specs/018-advanced-dashboard/research.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The response includes `FuelCostEstimated = true` for a given time window when an

**Question**: What is the formula and data source for mileage-rate savings?

**Decision**: Mileage-rate savings use **per-ride `SnapshotMileageRateCents`** (already stored as of spec 012). Formula per ride: `miles × snapshotMileageRateCents / 100`. Summed across all rides in a window.
**Decision**: Mileage-rate savings use **per-ride `SnapshotMileageRateCents`** (already stored as of spec 012). Formula per ride: `miles × snapshotMileageRateCents`. Summed across all rides in a window.

If `SnapshotMileageRateCents IS NULL` for a ride, that ride contributes $0 to mileage-rate savings. If the user's current `MileageRateCents` setting is NULL, the reminder card is shown. The reminder flag is derived from `UserSettings.MileageRateCents IS NULL` — not from ride snapshots (a user may have set it after early rides).

Expand Down
34 changes: 34 additions & 0 deletions specs/027-mileage-rate-savings/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Specification Quality Checklist: Split Savings Display Metrics

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-07-09
**Feature**: [/workspaces/neCodeBikeTracking/specs/027-mileage-rate-savings/spec.md](/workspaces/neCodeBikeTracking/specs/027-mileage-rate-savings/spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- Validation pass 1 complete: all checklist items passed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Contract: Dashboard Split Savings Metrics

Base endpoint: `GET /api/dashboard`

This feature keeps the existing dashboard endpoint and requires the consumer-facing savings section to expose two independently displayable metrics in the response consumed by the dashboard page.

## Response Fragment (savings section)

```json
{
"totals": {
"moneySaved": {
"mileageRateSavings": 42.5,
"fuelCostAvoided": 18.2,
"qualifiedRideCount": 7
}
}
}
```

## Consumer Rules for Spec #27

1. Render **Mileage rate savings** from `totals.moneySaved.mileageRateSavings`.
2. Render **Gallons-based savings** from `totals.moneySaved.fuelCostAvoided` (field name remains unchanged in this feature).
3. `totals.moneySaved.combinedSavings` is removed from this contract for the split-savings scope.
4. Preserve existing currency formatting and rounding conventions.
5. Backend and frontend assertions must explicitly verify split fields are present and `combinedSavings` is absent in `totals.moneySaved`.

## Formula Requirements (Spec Source of Truth)

- Mileage rate savings = `configuredMileageRate * periodMiles` (rate source: `UserSettings.MileageRateCents` for dashboard aggregations)
- Gallons-based savings = `gallonsSaved * miles`

Backend contracts and frontend TypeScript models/tests must stay synchronized for these existing fields in the same change.
45 changes: 45 additions & 0 deletions specs/027-mileage-rate-savings/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Phase 1 Data Model: Split Savings Display Metrics

No new persisted entities or migrations are required. This feature updates computed/read models used by dashboard/results presentation.

## Existing Entities Read

### `RideEntity` (unchanged schema)
- `Miles: decimal`
- Inputs required to derive gallons-based metric as defined in spec (`gallonsSaved` source from existing ride snapshot-derived data path)

### `UserSettingsEntity` (unchanged schema)
- `MileageRateCents: decimal?` (rate source for dashboard month/year mileage-rate savings aggregation)

### `DashboardMoneySaved` projection (`src/BikeTracking.Api/Contracts/DashboardContracts.cs`)
- `MileageRateSavings: decimal?`
- `FuelCostAvoided: decimal?` (mapped to displayed "Gallons-based savings" metric)
- `QualifiedRideCount: int`

## Feature-Level Derived Metrics

### Mileage Rate Savings
- **Label**: `Mileage rate savings`
- **Formula**: `configuredMileageRate * periodMiles`
- **Validation/Rules**:
- Uses current user settings `MileageRateCents` as rate source for dashboard period totals.
- Renders as formatted currency with current rounding rules.
- Zero values still render.

### Gallons-Based Savings
- **Label**: `Gallons-based savings`
- **Formula**: `gallonsSaved * miles`
- **Validation/Rules**:
- Uses the existing dashboard savings data path for `gallonsSaved` derivation.
- Renders as formatted currency with current rounding rules.
- Zero values still render.

## Presentation State Rules

- Two separate lines must always be rendered together in the dashboard/results summary section.
- The target view must not show a merged single-total savings value.
- Existing formatting/rounding and missing-data fallback behavior remain unchanged.

## Relationship/Flow

`UserSettingsEntity.MileageRateCents` + `RideEntity.Miles` → `GetDashboardService` / `GetYearStatsDashboardService` aggregation → `DashboardMoneySaved` / `YearStatsSavingsPoint` contract → frontend typed model → split metric display
87 changes: 87 additions & 0 deletions specs/027-mileage-rate-savings/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Implementation Plan: Split Savings Display Metrics

**Branch**: `027-mileage-rate-savings` | **Date**: 2026-07-09 | **Spec**: [spec.md](spec.md)

**Input**: Feature specification from `specs/027-mileage-rate-savings/spec.md`

## Summary

Split the dashboard/results savings presentation into two explicit metrics and stop presenting a merged single-total savings value in that view. Keep scope limited to savings calculation/display, preserve existing formatting/rounding behavior, and keep backend/frontend tests aligned with the two formulas defined in the spec:
- Mileage rate savings = `configuredMileageRate * periodMiles` (using `UserSettings.MileageRateCents` for dashboard period aggregations)
- Gallons-based savings = `gallonsSaved * miles`
- Canonical mapping: UI label "Gallons-based savings" -> contract field `totals.moneySaved.fuelCostAvoided`

## Technical Context

**Language/Version**: .NET 10 (C# backend), F# 9 (domain), TypeScript 5.x + React 19 (frontend)

**Primary Dependencies**: ASP.NET Core Minimal API, EF Core (SQLite), React Router, xUnit, Vitest + React Testing Library, Playwright

**Storage**: SQLite via existing ride/settings entities (no new storage engine)

**Testing**: `dotnet test BikeTracking.slnx`, `cd src/BikeTracking.Frontend && npm run test:unit`, `cd src/BikeTracking.Frontend && npm run test:e2e`

**Target Platform**: Local-first web app (Aspire-hosted API + React frontend)

**Project Type**: Web application (backend + frontend)

**Performance Goals**: Out of scope for this feature; no explicit performance acceptance target added

**Constraints**:
- Do not broaden to unrelated dashboard redesign work
- Keep currency/unit/rounding behavior unchanged
- Keep ride-entry and persisted data shape unchanged
- Keep mileage-rate source aligned to current settings for dashboard month/year aggregation logic

**Scale/Scope**: Single dashboard/results savings card/section and its backing aggregation/tests

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

| Directive | Status | Notes |
|-----------|--------|-------|
| DevContainer environment | ✅ PASS | No environment/tooling change required |
| Trunk-based delivery + PR flow | ✅ PASS | Work remains on existing feature branch |
| TDD mandatory | ✅ PASS | Plan requires backend + frontend failing tests first |
| E2E required on every PR | ✅ PASS | Add/adjust Playwright coverage for split metrics |
| Ports/adapters boundaries | ✅ PASS | Changes remain in existing dashboard service + contracts + UI layers |
| Result-style domain outcomes | ✅ PASS | No exception-driven flow introduced |
| Transactional relational write model | ✅ PASS (N/A) | Read/compute/display update only; no new writes |
| Local-first runtime posture | ✅ PASS | SQLite + local stack unchanged |

**Post-design re-check (Phase 1)**: ✅ All gates remain green. Design remains additive/tight and does not introduce new services, infra, or persistence shape changes.

## Project Structure

### Documentation (this feature)

```text
specs/027-mileage-rate-savings/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── dashboard-split-savings-contract.md
└── tasks.md # generated later by /speckit.tasks
```

### Source Code (repository root)

```text
src/BikeTracking.Api/
├── Application/Dashboard/GetDashboardService.cs # update savings aggregation output as needed
├── Contracts/DashboardContracts.cs # keep split fields explicit; remove merged display dependency
└── Tests via src/BikeTracking.Api.Tests/Application/Dashboard/

src/BikeTracking.Frontend/src/
├── pages/dashboard/dashboard-page.tsx # render two labeled metrics; remove merged line in summary display
├── services/dashboard-api.ts # keep frontend contract/types aligned
└── tests under pages/dashboard and tests/e2e

src/BikeTracking.Frontend/tests/e2e/
└── dashboard.spec.ts / savings-calculation.spec.ts # validate both metrics appear together
```

**Structure Decision**: Use existing web-app layout and modify only existing dashboard service/contracts/UI/test files directly tied to split-savings behavior.
55 changes: 55 additions & 0 deletions specs/027-mileage-rate-savings/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Quickstart Validation: Split Savings Display Metrics

Use this to validate spec #27 end-to-end after implementation.

## Prerequisites

1. Run app stack:
- `dotnet run --project src/BikeTracking.AppHost`
2. Have rider account with ride data containing miles, plus configured user mileage rate (`MileageRateCents`) in settings.

## Backend Validation

1. Run backend tests:
- `dotnet test BikeTracking.slnx`
2. Confirm dashboard aggregation tests assert both formulas independently (mileage-rate and gallons-based).

## Frontend Validation

1. Run frontend unit tests:
- `cd src/BikeTracking.Frontend && npm run test:unit`
2. Run E2E tests:
- `cd src/BikeTracking.Frontend && npm run test:e2e`
3. Confirm tests verify:
- Both labels render: **Mileage rate savings** and **Gallons-based savings**
- Both values render simultaneously
- No merged single-total savings display in the target view

## Manual Check

1. Log in and open dashboard/results view.
2. Verify two separate savings lines are present with expected labels.
3. Verify values match spec formulas.
4. Verify formatting/rounding behavior matches prior behavior.
5. Verify zero values still render (not hidden).

## Final Validation Matrix

| Area | Command/Check | Expected Result |
|------|----------------|-----------------|
| Backend suite | `dotnet test BikeTracking.slnx` | Pass; dashboard tests assert split formulas and no merged field in `/api/dashboard` payload |
| Frontend unit suite | `cd src/BikeTracking.Frontend && npm run test:unit` | Pass; dashboard page tests assert both labels, rounded currency values, and zero-value visibility |
| Frontend E2E suite | `cd src/BikeTracking.Frontend && npm run test:e2e` | Pass; split savings rows render together and merged savings label is absent |
| Manual dashboard check | Open dashboard/results after login | Both split labels visible with correctly formatted values and no merged line |

## Command Checklist

- [ ] `dotnet test BikeTracking.slnx`
- [ ] `cd src/BikeTracking.Frontend && npm run test:unit`
- [ ] `cd src/BikeTracking.Frontend && npm run test:e2e`

## References

- Spec: [spec.md](spec.md)
- Data model: [data-model.md](data-model.md)
- Contract: [contracts/dashboard-split-savings-contract.md](contracts/dashboard-split-savings-contract.md)
45 changes: 45 additions & 0 deletions specs/027-mileage-rate-savings/research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Phase 0 Research: Split Savings Display Metrics

## Decision 1: Keep scope to existing dashboard/results path

- **Decision**: Limit changes to the existing dashboard summary savings presentation and its direct backend aggregation/contract path (`GetDashboardService`, `DashboardContracts`, dashboard page/types/tests).
- **Rationale**: The spec is narrowly focused on splitting displayed savings metrics and preserving existing behavior elsewhere.
- **Alternatives considered**: Broad dashboard-wide savings redesign (rejected as out-of-scope).

## Decision 2: Treat spec formulas as authoritative for this feature

- **Decision**: Use the formulas defined in the feature spec as the source of truth:
- Mileage rate savings = `configuredMileageRate * periodMiles` (using current user settings mileage rate for dashboard month/year aggregation).
- Gallons-based savings = `gallonsSaved * miles`.
- **Rationale**: The user requested that planning stay tight and use formulas already defined in spec #27.
- **Alternatives considered**: Preserving legacy merged-savings semantics only (rejected because it conflicts with FR-001..FR-004).

## Decision 3: Preserve formatting/rounding rules by reusing current formatting utilities

- **Decision**: Keep existing currency/unit/rounding behavior in the frontend formatter and backend rounding utilities; only split labeling/value wiring.
- **Rationale**: FR-005 explicitly requires unchanged presentation conventions.
- **Alternatives considered**: Introducing new formatting logic for split metrics (rejected to avoid regressions).

## Decision 4: Keep contract alignment explicit

- **Decision**: Document and validate the `/api/dashboard` money-saved fields used for split display, and synchronize backend/frontend tests if any field semantics are adjusted.
- **Rationale**: FR-007/FR-008 require contract and test synchronization across layers.
- **Alternatives considered**: Frontend-only change without contract verification (rejected because it can hide backend/frontend drift).

## Open Items

None. Technical context clarifications are resolved for planning scope.

## Post-Implementation Notes

### Confirmed Decisions

- Dashboard `moneySaved` contract remains split-only (`mileageRateSavings`, `fuelCostAvoided`, `qualifiedRideCount`) and omits `combinedSavings` for this endpoint.
- Split savings rows continue using existing currency formatting and away-from-zero rounding conventions.
- Ride-entry create/edit behavior remains unchanged; no new ride contracts, entities, or migrations were introduced.

### Non-Goals Reaffirmed

- No redesign of advanced dashboard UX beyond keeping savings formula source aligned with dashboard month/year settings-based aggregation.
- No schema or migration work for ride-entry persistence.
- No changes to gas-price/weather collection behavior outside existing ride-entry flows.
Loading