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
36 changes: 36 additions & 0 deletions specs/012-dashboard-stats/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Specification Quality Checklist: Rider Dashboard Statistics

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-06
**Feature**: [spec.md](../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

- Clarifications resolved: baseline averages include average miles per ride and average ride duration.
- Clarifications resolved: first optional suggestions are estimated gallons avoided and goal progress.
- Specification is ready for `/speckit.plan`.
253 changes: 253 additions & 0 deletions specs/012-dashboard-stats/contracts/api-contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# API Contracts: Rider Dashboard Statistics

**Feature**: 012-dashboard-stats
**Date**: 2026-04-06
**Primary base paths**: `/api/dashboard`, `/api/users/me/settings`

---

## New Endpoint

### `GET /api/dashboard`

Authenticated rider-only endpoint returning the full dashboard view model.

```csharp
public sealed record DashboardResponse(
DashboardTotals Totals,
DashboardAverages Averages,
DashboardCharts Charts,
IReadOnlyList<DashboardMetricSuggestion> Suggestions,
DashboardMissingData MissingData,
DateTime GeneratedAtUtc
);

public sealed record DashboardTotals(
DashboardMileageMetric CurrentMonthMiles,
DashboardMileageMetric YearToDateMiles,
DashboardMileageMetric AllTimeMiles,
DashboardMoneySaved MoneySaved
);

public sealed record DashboardMileageMetric(
decimal Miles,
int RideCount,
string Period
);

public sealed record DashboardMoneySaved(
decimal? MileageRateSavings,
decimal? FuelCostAvoided,
decimal? CombinedSavings,
int QualifiedRideCount
);

public sealed record DashboardAverages(
decimal? AverageTemperature,
decimal? AverageMilesPerRide,
decimal? AverageRideMinutes
);

public sealed record DashboardCharts(
IReadOnlyList<DashboardMileagePoint> MileageByMonth,
IReadOnlyList<DashboardSavingsPoint> SavingsByMonth
);

public sealed record DashboardMileagePoint(
string MonthKey,
string Label,
decimal Miles
);

public sealed record DashboardSavingsPoint(
string MonthKey,
string Label,
decimal? MileageRateSavings,
decimal? FuelCostAvoided,
decimal? CombinedSavings
);

public sealed record DashboardMetricSuggestion(
string MetricKey,
string Title,
string Description,
bool IsEnabled
);

public sealed record DashboardMissingData(
int RidesMissingSavingsSnapshot,
int RidesMissingGasPrice,
int RidesMissingTemperature,
int RidesMissingDuration
);
```

**Behavior**:
- `200 OK` for authenticated riders, even when no rides exist.
- `401 Unauthorized` when the caller is unauthenticated.
- Empty-state responses still contain empty chart arrays or zeroed mileage cards instead of errors.

---

## Modified Existing Contract: `UserSettingsUpsertRequest`

File: `src/BikeTracking.Api/Contracts/UsersContracts.cs`

```csharp
public sealed record UserSettingsUpsertRequest(
decimal? AverageCarMpg,
decimal? YearlyGoalMiles,
decimal? OilChangePrice,
decimal? MileageRateCents,
string? LocationLabel,
decimal? Latitude,
decimal? Longitude,
bool? DashboardGallonsAvoidedEnabled,
bool? DashboardGoalProgressEnabled
);
```

**Semantics**:
- Both new fields participate in the existing partial-update semantics.
- Omitting a field leaves the prior persisted value unchanged.

---

## Modified Existing Contract: `UserSettingsView`

```csharp
public sealed record UserSettingsView(
decimal? AverageCarMpg,
decimal? YearlyGoalMiles,
decimal? OilChangePrice,
decimal? MileageRateCents,
string? LocationLabel,
decimal? Latitude,
decimal? Longitude,
bool DashboardGallonsAvoidedEnabled,
bool DashboardGoalProgressEnabled,
DateTime? UpdatedAtUtc
);
```

These fields allow the frontend to render suggestion state and settings defaults consistently.

---

## Modified Existing Event Payload Factories

Files:
- `RideRecordedEventPayload.cs`
- `RideEditedEventPayload.cs`

New additive optional fields:

```csharp
decimal? SnapshotAverageCarMpg = null,
decimal? SnapshotMileageRateCents = null,
decimal? SnapshotYearlyGoalMiles = null,
decimal? SnapshotOilChangePrice = null
```

These are additive and backwards-compatible for existing call sites.

---

## Frontend TypeScript Contracts

### `dashboard-api.ts`

```typescript
export interface DashboardResponse {
totals: DashboardTotals;
averages: DashboardAverages;
charts: DashboardCharts;
suggestions: DashboardMetricSuggestion[];
missingData: DashboardMissingData;
generatedAtUtc: string;
}

export interface DashboardTotals {
currentMonthMiles: DashboardMileageMetric;
yearToDateMiles: DashboardMileageMetric;
allTimeMiles: DashboardMileageMetric;
moneySaved: DashboardMoneySaved;
}

export interface DashboardMileageMetric {
miles: number;
rideCount: number;
period: string;
}

export interface DashboardMoneySaved {
mileageRateSavings: number | null;
fuelCostAvoided: number | null;
combinedSavings: number | null;
qualifiedRideCount: number;
}

export interface DashboardAverages {
averageTemperature: number | null;
averageMilesPerRide: number | null;
averageRideMinutes: number | null;
}

export interface DashboardCharts {
mileageByMonth: DashboardMileagePoint[];
savingsByMonth: DashboardSavingsPoint[];
}

export interface DashboardMileagePoint {
monthKey: string;
label: string;
miles: number;
}

export interface DashboardSavingsPoint {
monthKey: string;
label: string;
mileageRateSavings: number | null;
fuelCostAvoided: number | null;
combinedSavings: number | null;
}

export interface DashboardMetricSuggestion {
metricKey: "gallonsAvoided" | "goalProgress";
title: string;
description: string;
isEnabled: boolean;
}

export interface DashboardMissingData {
ridesMissingSavingsSnapshot: number;
ridesMissingGasPrice: number;
ridesMissingTemperature: number;
ridesMissingDuration: number;
}
```

### `users-api.ts`

Add to both request and response interfaces:

```typescript
dashboardGallonsAvoidedEnabled?: boolean | null;
dashboardGoalProgressEnabled?: boolean | null;
```

For the view shape:

```typescript
dashboardGallonsAvoidedEnabled: boolean;
dashboardGoalProgressEnabled: boolean;
```

---

## Compatibility Notes

- Existing user settings callers remain compatible because the new fields are additive.
- Existing ride-history API consumers remain unchanged.
- The dashboard no longer depends on ride-history pagination hacks, but `/miles` can be preserved as
a client-side redirect to the new dashboard route for continuity.
Loading
Loading