removed commited plan file
This commit is contained in:
parent
8cd602c5cd
commit
f381f034d3
|
|
@ -1,621 +0,0 @@
|
|||
# Provider-Specific Pricing for Reports — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the hard-coded 0.39 CHF/kWh electricity price with real per-provider tariffs from ELCOM, so report savings reflect each installation's actual network provider rate.
|
||||
|
||||
**Architecture:** New `ProviderTariff` SQLite model stores ELCOM tariff data (total + breakdown) per provider per year. New `PricingService` fetches tariffs via SPARQL, caches in DB, and exposes a lookup method. All three report services (`WeeklyReportService`, `ReportAggregationService`, `ReportEmailService`) replace the hard-coded constant with a dynamic lookup using the installation's `NetworkProvider` field.
|
||||
|
||||
**Tech Stack:** C#/.NET, SQLite-net ORM, SPARQL (ELCOM/LINDAS endpoint), Flurl HTTP
|
||||
|
||||
**Decisions made during brainstorming:**
|
||||
- Fetch total price now, store breakdown components for future use
|
||||
- Fetch once per provider+year, store in SQLite (ELCOM tariffs are fixed per year)
|
||||
- Only new reports going forward use real tariffs (existing stored reports untouched)
|
||||
- Fallback to 0.39 CHF/kWh if provider has no tariff data
|
||||
- Scope: accurate savings only (no provider comparison or ROI features yet)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create ProviderTariff Data Model
|
||||
|
||||
**Files:**
|
||||
- Create: `csharp/App/Backend/DataTypes/ProviderTariff.cs`
|
||||
|
||||
**Step 1: Create the data model**
|
||||
|
||||
```csharp
|
||||
using SQLite;
|
||||
|
||||
namespace InnovEnergy.App.Backend.DataTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Cached ELCOM electricity tariff for a network provider and year.
|
||||
/// Fetched from lindas.admin.ch/elcom/electricityprice via SPARQL.
|
||||
/// Tariffs are fixed per year — fetched once and stored permanently.
|
||||
/// </summary>
|
||||
public class ProviderTariff
|
||||
{
|
||||
[PrimaryKey, AutoIncrement]
|
||||
public Int64 Id { get; set; }
|
||||
|
||||
[Indexed]
|
||||
public String ProviderName { get; set; } = "";
|
||||
|
||||
[Indexed]
|
||||
public Int32 Year { get; set; }
|
||||
|
||||
// Total electricity price (CHF/kWh) — used for savings calculation
|
||||
public Double TotalPricePerKwh { get; set; }
|
||||
|
||||
// Breakdown components (CHF/kWh) — stored for future use
|
||||
public Double GridUsagePerKwh { get; set; } // Netznutzung
|
||||
public Double EnergyPerKwh { get; set; } // Energielieferung
|
||||
public Double FeesPerKwh { get; set; } // Abgaben an Gemeinwesen + KEV/SDL
|
||||
|
||||
public String FetchedAt { get; set; } = "";
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Register table in Db.cs**
|
||||
|
||||
In `csharp/App/Backend/Database/Db.cs`:
|
||||
|
||||
Add table accessor after line 39 (after TicketTimelineEvents):
|
||||
```csharp
|
||||
public static TableQuery<ProviderTariff> ProviderTariffs => Connection.Table<ProviderTariff>();
|
||||
```
|
||||
|
||||
Add `CreateTable` call inside the `RunInTransaction` block (after line 77, after TicketTimelineEvent):
|
||||
```csharp
|
||||
Connection.CreateTable<ProviderTariff>();
|
||||
```
|
||||
|
||||
**Step 3: Build to verify**
|
||||
|
||||
Run: `cd csharp/App/Backend && dotnet build`
|
||||
Expected: Build succeeds
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add csharp/App/Backend/DataTypes/ProviderTariff.cs csharp/App/Backend/Database/Db.cs
|
||||
git commit -m "feat: add ProviderTariff data model for ELCOM pricing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add Database Read/Create Methods for ProviderTariff
|
||||
|
||||
**Files:**
|
||||
- Modify: `csharp/App/Backend/Database/Read.cs`
|
||||
- Modify: `csharp/App/Backend/Database/Create.cs`
|
||||
|
||||
**Step 1: Add read method in Read.cs**
|
||||
|
||||
Add at the end of the file (before closing brace):
|
||||
```csharp
|
||||
public static ProviderTariff? GetProviderTariff(string providerName, int year)
|
||||
{
|
||||
return ProviderTariffs
|
||||
.FirstOrDefault(t => t.ProviderName == providerName && t.Year == year);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add create method in Create.cs**
|
||||
|
||||
Add at the end of the file (before closing brace):
|
||||
```csharp
|
||||
public static void InsertProviderTariff(ProviderTariff tariff)
|
||||
{
|
||||
Connection.Insert(tariff);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Build to verify**
|
||||
|
||||
Run: `cd csharp/App/Backend && dotnet build`
|
||||
Expected: Build succeeds
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add csharp/App/Backend/Database/Read.cs csharp/App/Backend/Database/Create.cs
|
||||
git commit -m "feat: add DB read/create methods for ProviderTariff"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Create PricingService
|
||||
|
||||
**Files:**
|
||||
- Create: `csharp/App/Backend/Services/PricingService.cs`
|
||||
|
||||
**Step 1: Create the service**
|
||||
|
||||
This service:
|
||||
1. Exposes `GetElectricityPrice(providerName, year)` -> returns CHF/kWh (double)
|
||||
2. Checks SQLite cache first
|
||||
3. If not cached, fetches from ELCOM SPARQL and stores
|
||||
4. Falls back to 0.39 if provider not found or fetch fails
|
||||
|
||||
```csharp
|
||||
using Flurl.Http;
|
||||
using InnovEnergy.App.Backend.Database;
|
||||
using InnovEnergy.App.Backend.DataTypes;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace InnovEnergy.App.Backend.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides electricity tariffs per network provider and year.
|
||||
/// Source: ELCOM/LINDAS SPARQL endpoint (Swiss electricity price database).
|
||||
/// Caches results in SQLite — tariffs are fixed per year.
|
||||
/// Falls back to 0.39 CHF/kWh if provider data is unavailable.
|
||||
/// </summary>
|
||||
public static class PricingService
|
||||
{
|
||||
private const double FallbackPricePerKwh = 0.39;
|
||||
private const string SparqlEndpoint = "https://ld.admin.ch/query";
|
||||
|
||||
/// <summary>
|
||||
/// Get the total electricity price for a provider in a given year.
|
||||
/// Returns cached value if available, otherwise fetches from ELCOM.
|
||||
/// Falls back to 0.39 CHF/kWh if data is unavailable.
|
||||
/// </summary>
|
||||
public static async Task<double> GetElectricityPriceAsync(string providerName, int year)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerName))
|
||||
return FallbackPricePerKwh;
|
||||
|
||||
// Check DB cache first
|
||||
var cached = Db.GetProviderTariff(providerName, year);
|
||||
if (cached is not null)
|
||||
return cached.TotalPricePerKwh;
|
||||
|
||||
// Fetch from ELCOM
|
||||
var tariff = await FetchTariffFromElcomAsync(providerName, year);
|
||||
if (tariff is null)
|
||||
{
|
||||
Console.WriteLine($"[PricingService] No ELCOM data for '{providerName}' year {year}, using fallback {FallbackPricePerKwh} CHF/kWh.");
|
||||
return FallbackPricePerKwh;
|
||||
}
|
||||
|
||||
// Cache in DB
|
||||
Db.InsertProviderTariff(tariff);
|
||||
Console.WriteLine($"[PricingService] Cached tariff for '{providerName}' year {year}: {tariff.TotalPricePerKwh:F4} CHF/kWh.");
|
||||
return tariff.TotalPricePerKwh;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronous convenience wrapper for use in report generation.
|
||||
/// </summary>
|
||||
public static double GetElectricityPrice(string providerName, int year)
|
||||
{
|
||||
return GetElectricityPriceAsync(providerName, year).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static async Task<ProviderTariff?> FetchTariffFromElcomAsync(string providerName, int year)
|
||||
{
|
||||
try
|
||||
{
|
||||
// ELCOM SPARQL query for H4 household profile (standard household 4500 kWh/year)
|
||||
// H4 is the most common reference category for residential installations.
|
||||
// The query fetches tariff components: gridusage, energy, charge (fees).
|
||||
// Total = gridusage + energy + charge
|
||||
var sparqlQuery = $@"
|
||||
PREFIX schema: <http://schema.org/>
|
||||
PREFIX cube: <https://cube.link/>
|
||||
PREFIX elcom: <https://energy.ld.admin.ch/elcom/electricityprice/dimension/>
|
||||
|
||||
SELECT ?gridusage ?energy ?charge
|
||||
FROM <https://lindas.admin.ch/elcom/electricityprice>
|
||||
WHERE {{
|
||||
?obs a cube:Observation ;
|
||||
elcom:operator ?op ;
|
||||
elcom:period <https://ld.admin.ch/time/year/{year}> ;
|
||||
elcom:category <https://energy.ld.admin.ch/elcom/electricityprice/category/H4> ;
|
||||
elcom:product <https://energy.ld.admin.ch/elcom/electricityprice/product/total> ;
|
||||
elcom:gridusage ?gridusage ;
|
||||
elcom:energy ?energy ;
|
||||
elcom:charge ?charge .
|
||||
?op schema:name ""{EscapeSparql(providerName)}"" .
|
||||
}}
|
||||
LIMIT 1";
|
||||
|
||||
var response = await SparqlEndpoint
|
||||
.WithHeader("Accept", "application/sparql-results+json")
|
||||
.PostUrlEncodedAsync(new { query = sparqlQuery });
|
||||
|
||||
var json = await response.GetStringAsync();
|
||||
var parsed = JObject.Parse(json);
|
||||
var bindings = parsed["results"]?["bindings"];
|
||||
|
||||
if (bindings is null || !bindings.Any())
|
||||
return null;
|
||||
|
||||
var first = bindings.First();
|
||||
var gridUsage = ParseRpToChf(first["gridusage"]?["value"]?.ToString());
|
||||
var energy = ParseRpToChf(first["energy"]?["value"]?.ToString());
|
||||
var charge = ParseRpToChf(first["charge"]?["value"]?.ToString());
|
||||
var total = gridUsage + energy + charge;
|
||||
|
||||
if (total <= 0)
|
||||
return null;
|
||||
|
||||
return new ProviderTariff
|
||||
{
|
||||
ProviderName = providerName,
|
||||
Year = year,
|
||||
TotalPricePerKwh = Math.Round(total, 4),
|
||||
GridUsagePerKwh = Math.Round(gridUsage, 4),
|
||||
EnergyPerKwh = Math.Round(energy, 4),
|
||||
FeesPerKwh = Math.Round(charge, 4),
|
||||
FetchedAt = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[PricingService] ELCOM fetch failed for '{providerName}' year {year}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELCOM values may be in Rp./kWh (centimes) or CHF/kWh.
|
||||
/// Values > 1 are likely Rp./kWh and need /100 conversion.
|
||||
/// Values <= 1 are already CHF/kWh.
|
||||
/// </summary>
|
||||
private static double ParseRpToChf(string? value)
|
||||
{
|
||||
if (!double.TryParse(value, System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var parsed))
|
||||
return 0;
|
||||
|
||||
// ELCOM typically returns Rp./kWh (centimes), convert to CHF
|
||||
return parsed > 1 ? parsed / 100.0 : parsed;
|
||||
}
|
||||
|
||||
private static string EscapeSparql(string value)
|
||||
{
|
||||
return value.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important: SPARQL query validation required**
|
||||
- The exact SPARQL predicates (`elcom:gridusage`, `elcom:energy`, `elcom:charge`) need verification against the live endpoint before implementing.
|
||||
- Test manually first:
|
||||
```bash
|
||||
curl -X POST https://ld.admin.ch/query \
|
||||
-H "Accept: application/sparql-results+json" \
|
||||
-d "query=PREFIX schema: <http://schema.org/> PREFIX cube: <https://cube.link/> PREFIX elcom: <https://energy.ld.admin.ch/elcom/electricityprice/dimension/> SELECT ?gridusage ?energy ?charge FROM <https://lindas.admin.ch/elcom/electricityprice> WHERE { ?obs a cube:Observation ; elcom:operator ?op ; elcom:period <https://ld.admin.ch/time/year/2025> ; elcom:category <https://energy.ld.admin.ch/elcom/electricityprice/category/H4> ; elcom:product <https://energy.ld.admin.ch/elcom/electricityprice/product/total> ; elcom:gridusage ?gridusage ; elcom:energy ?energy ; elcom:charge ?charge . ?op schema:name \"BKW Energie AG\" . } LIMIT 1"
|
||||
```
|
||||
- If predicates or URIs differ, adjust `PricingService.cs` accordingly.
|
||||
- The H4 category (standard household, 4500 kWh/year) is the best default for residential Sodistore installations.
|
||||
- ELCOM docs: https://www.elcom.admin.ch/elcom/en/home/open-data-api.html
|
||||
|
||||
**Step 2: Build to verify**
|
||||
|
||||
Run: `cd csharp/App/Backend && dotnet build`
|
||||
Expected: Build succeeds
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add csharp/App/Backend/Services/PricingService.cs
|
||||
git commit -m "feat: add PricingService for ELCOM tariff lookup with DB caching"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Verify SPARQL Query Against Live Endpoint
|
||||
|
||||
**This is a critical validation step before integrating into reports.**
|
||||
|
||||
**Step 1: Test the SPARQL query**
|
||||
|
||||
Run the curl command from Task 3 notes against the live ELCOM endpoint with a known provider name (e.g., one from the existing `NetworkProviderService` list).
|
||||
|
||||
**Step 2: Inspect the response**
|
||||
|
||||
Check:
|
||||
- Are the predicate names correct (`elcom:gridusage`, `elcom:energy`, `elcom:charge`)?
|
||||
- What units are the values in (Rp./kWh or CHF/kWh)?
|
||||
- Does the H4 category exist?
|
||||
- Does the year URI format `<https://ld.admin.ch/time/year/2025>` work?
|
||||
|
||||
**Step 3: Adjust PricingService if needed**
|
||||
|
||||
If predicates, URIs, or units differ from what's in the code, update the SPARQL query and unit conversion logic in `PricingService.cs`.
|
||||
|
||||
**Step 4: Commit any fixes**
|
||||
|
||||
```bash
|
||||
git add csharp/App/Backend/Services/PricingService.cs
|
||||
git commit -m "fix: adjust SPARQL query to match live ELCOM endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Integrate Pricing into WeeklyReportService
|
||||
|
||||
**Files:**
|
||||
- Modify: `csharp/App/Backend/Services/WeeklyReportService.cs`
|
||||
|
||||
**Step 1: Replace hard-coded price in savings calculation (around line 181)**
|
||||
|
||||
Before:
|
||||
```csharp
|
||||
const double ElectricityPriceCHF = 0.39;
|
||||
var totalEnergySaved = Math.Round(currentSummary.TotalConsumption - currentSummary.TotalGridImport, 1);
|
||||
var totalSavingsCHF = Math.Round(totalEnergySaved * ElectricityPriceCHF, 0);
|
||||
```
|
||||
|
||||
After:
|
||||
```csharp
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
var reportYear = DateTime.Parse(currentWeekDays.First().Date).Year;
|
||||
var electricityPrice = await PricingService.GetElectricityPriceAsync(
|
||||
installation?.NetworkProvider ?? "", reportYear);
|
||||
var totalEnergySaved = Math.Round(currentSummary.TotalConsumption - currentSummary.TotalGridImport, 1);
|
||||
var totalSavingsCHF = Math.Round(totalEnergySaved * electricityPrice, 0);
|
||||
```
|
||||
|
||||
Note: The `installation` variable may already exist earlier in the method. If so, reuse it instead of fetching again. Check the method context.
|
||||
|
||||
**Step 2: Replace hard-coded price in AI prompt section (around line 265)**
|
||||
|
||||
Before:
|
||||
```csharp
|
||||
const double ElectricityPriceCHF = 0.39;
|
||||
```
|
||||
|
||||
After: Remove this line. Use the `electricityPrice` variable from step 1 (pass it as a parameter to the AI insight method, or compute it there).
|
||||
|
||||
The key point: wherever `ElectricityPriceCHF` appears, replace with the dynamic value.
|
||||
|
||||
**Step 3: Build to verify**
|
||||
|
||||
Run: `cd csharp/App/Backend && dotnet build`
|
||||
Expected: Build succeeds
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add csharp/App/Backend/Services/WeeklyReportService.cs
|
||||
git commit -m "feat: use provider-specific tariff in weekly report savings"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Integrate Pricing into ReportAggregationService
|
||||
|
||||
**Files:**
|
||||
- Modify: `csharp/App/Backend/Services/ReportAggregationService.cs`
|
||||
|
||||
**Step 1: Remove the class-level constant (line 14)**
|
||||
|
||||
Before:
|
||||
```csharp
|
||||
private const Double ElectricityPriceCHF = 0.39;
|
||||
```
|
||||
|
||||
After: Remove this line entirely.
|
||||
|
||||
**Step 2: Update monthly aggregation (around line 366)**
|
||||
|
||||
The method already has `installationId` and fetches the installation. Add pricing lookup:
|
||||
|
||||
Before:
|
||||
```csharp
|
||||
var savingsCHF = Math.Round(energySaved * ElectricityPriceCHF, 0);
|
||||
```
|
||||
|
||||
After:
|
||||
```csharp
|
||||
var electricityPrice = await PricingService.GetElectricityPriceAsync(
|
||||
installation?.NetworkProvider ?? "", year);
|
||||
var savingsCHF = Math.Round(energySaved * electricityPrice, 0);
|
||||
```
|
||||
|
||||
Note: `installation` is already fetched at line 373 (`Db.GetInstallationById(installationId)`). Move the fetch before the savings calculation if needed, or reuse.
|
||||
|
||||
**Step 3: Update yearly aggregation (around line 477)**
|
||||
|
||||
Before:
|
||||
```csharp
|
||||
var savingsCHF = Math.Round(energySaved * ElectricityPriceCHF, 0);
|
||||
```
|
||||
|
||||
After:
|
||||
```csharp
|
||||
var electricityPrice = await PricingService.GetElectricityPriceAsync(
|
||||
installation?.NetworkProvider ?? "", year);
|
||||
var savingsCHF = Math.Round(energySaved * electricityPrice, 0);
|
||||
```
|
||||
|
||||
Note: `installation` is already fetched at line 488. Same pattern as monthly.
|
||||
|
||||
**Step 4: Update AI prompt references**
|
||||
|
||||
Search for any remaining `ElectricityPriceCHF` references in the file (AI prompt strings around lines 693, 728). Replace with the dynamic value passed into the AI generation methods.
|
||||
|
||||
**Step 5: Build to verify**
|
||||
|
||||
Run: `cd csharp/App/Backend && dotnet build`
|
||||
Expected: Build succeeds
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add csharp/App/Backend/Services/ReportAggregationService.cs
|
||||
git commit -m "feat: use provider-specific tariff in monthly/yearly report savings"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Update Email Templates with Dynamic Price
|
||||
|
||||
**Files:**
|
||||
- Modify: `csharp/App/Backend/Services/ReportEmailService.cs`
|
||||
|
||||
**Step 1: Make `AtRate` field dynamic**
|
||||
|
||||
The `EmailStrings` and `AggregatedEmailStrings` records have an `AtRate` field with hard-coded "bei 0.39 CHF/kWh" / "a 0.39 CHF/kWh" / etc.
|
||||
|
||||
Change the approach: instead of hard-coding the rate in the language strings, pass the rate as a parameter and format it dynamically.
|
||||
|
||||
Update the `GetWeeklyStrings` method to accept a `double electricityPrice` parameter:
|
||||
|
||||
Before (example for German, line 106):
|
||||
```csharp
|
||||
AtRate: "bei 0.39 CHF/kWh",
|
||||
```
|
||||
|
||||
After:
|
||||
```csharp
|
||||
AtRate: $"bei {electricityPrice:F2} CHF/kWh",
|
||||
```
|
||||
|
||||
Apply the same pattern for all 4 languages (de, fr, it, en) in both:
|
||||
- `GetWeeklyStrings()` — 4 language variants (lines ~90-200)
|
||||
- `GetAggregatedStrings()` — 8 language+type variants (lines ~524-574)
|
||||
|
||||
The preposition varies by language:
|
||||
- German: `"bei {price:F2} CHF/kWh"`
|
||||
- French: `"a {price:F2} CHF/kWh"`
|
||||
- Italian: `"a {price:F2} CHF/kWh"`
|
||||
- English: `"at {price:F2} CHF/kWh"`
|
||||
|
||||
**Step 2: Update callers to pass the price**
|
||||
|
||||
Wherever `GetWeeklyStrings(language)` or `GetAggregatedStrings(language, type)` is called, pass the electricity price as an additional parameter. The price should come from the report generation context (already computed in Tasks 5-6).
|
||||
|
||||
**Step 3: Build to verify**
|
||||
|
||||
Run: `cd csharp/App/Backend && dotnet build`
|
||||
Expected: Build succeeds
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add csharp/App/Backend/Services/ReportEmailService.cs
|
||||
git commit -m "feat: display actual provider tariff rate in report emails"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Add GetProviderTariff API Endpoint (Optional but Recommended)
|
||||
|
||||
**Files:**
|
||||
- Modify: `csharp/App/Backend/Controller.cs`
|
||||
|
||||
**Step 1: Add endpoint for frontend to query tariff**
|
||||
|
||||
This allows the frontend Information tab to display the current tariff next to the provider selector.
|
||||
|
||||
Add in Controller.cs (near the existing `GetNetworkProviders` endpoint around line 756):
|
||||
|
||||
```csharp
|
||||
[HttpGet(nameof(GetProviderTariff))]
|
||||
public async Task<ActionResult<object>> GetProviderTariff(Token authToken, string providerName, int year)
|
||||
{
|
||||
var session = Db.GetSession(authToken);
|
||||
if (session is null)
|
||||
return Unauthorized();
|
||||
|
||||
var price = await PricingService.GetElectricityPriceAsync(providerName, year);
|
||||
var tariff = Db.GetProviderTariff(providerName, year);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
providerName,
|
||||
year,
|
||||
totalPricePerKwh = price,
|
||||
gridUsagePerKwh = tariff?.GridUsagePerKwh ?? 0,
|
||||
energyPerKwh = tariff?.EnergyPerKwh ?? 0,
|
||||
feesPerKwh = tariff?.FeesPerKwh ?? 0,
|
||||
isFallback = tariff is null
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Build to verify**
|
||||
|
||||
Run: `cd csharp/App/Backend && dotnet build`
|
||||
Expected: Build succeeds
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add csharp/App/Backend/Controller.cs
|
||||
git commit -m "feat: add GetProviderTariff API endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: End-to-End Verification
|
||||
|
||||
**Step 1: Full build**
|
||||
|
||||
Run: `cd csharp/App/Backend && dotnet build`
|
||||
Expected: Build succeeds with no warnings related to our changes
|
||||
|
||||
**Step 2: Test SPARQL query manually**
|
||||
|
||||
Pick 2-3 known provider names from the ELCOM list and verify the pricing query returns sensible values (typical Swiss residential rates are 0.20-0.45 CHF/kWh).
|
||||
|
||||
**Step 3: Verify fallback behavior**
|
||||
|
||||
Test with:
|
||||
- Empty provider name -> should return 0.39
|
||||
- Unknown provider name -> should return 0.39
|
||||
- Valid provider -> should return actual ELCOM rate
|
||||
|
||||
**Step 4: Review all changes**
|
||||
|
||||
```bash
|
||||
git diff main --stat
|
||||
git log --oneline main..HEAD
|
||||
```
|
||||
|
||||
Verify:
|
||||
- No remaining hard-coded 0.39 in report calculation code
|
||||
- Email templates use dynamic formatting
|
||||
- ProviderTariff table is registered in Db.cs
|
||||
- PricingService has proper error handling and fallback
|
||||
|
||||
---
|
||||
|
||||
## Summary of All Files Changed
|
||||
|
||||
| File | Action | Purpose |
|
||||
|------|--------|---------|
|
||||
| `DataTypes/ProviderTariff.cs` | Create | SQLite model for cached tariffs |
|
||||
| `Database/Db.cs` | Modify | Register table + accessor |
|
||||
| `Database/Read.cs` | Modify | Add `GetProviderTariff()` query |
|
||||
| `Database/Create.cs` | Modify | Add `InsertProviderTariff()` |
|
||||
| `Services/PricingService.cs` | Create | ELCOM fetch + cache + fallback logic |
|
||||
| `Services/WeeklyReportService.cs` | Modify | Use dynamic price (2 places) |
|
||||
| `Services/ReportAggregationService.cs` | Modify | Use dynamic price (monthly + yearly) |
|
||||
| `Services/ReportEmailService.cs` | Modify | Dynamic rate in 12 language strings |
|
||||
| `Controller.cs` | Modify | Optional: GetProviderTariff endpoint |
|
||||
|
||||
## Fallback Behavior
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| No NetworkProvider set on installation | Uses 0.39 CHF/kWh |
|
||||
| Provider not found in ELCOM | Uses 0.39 CHF/kWh, logs warning |
|
||||
| ELCOM endpoint unavailable | Uses 0.39 CHF/kWh, logs error |
|
||||
| Tariff already cached in DB | Returns cached value (no network call) |
|
||||
| New year, same provider | Fetches new year's tariff from ELCOM |
|
||||
|
||||
## Future Extensions (Not In Scope)
|
||||
|
||||
- Provider comparison ("your rate vs. average")
|
||||
- ROI/payback calculation
|
||||
- Time-of-use / dynamic spot pricing
|
||||
- Tariff breakdown display in reports
|
||||
- Category selection (H1-H8 profiles beyond default H4)
|
||||
Loading…
Reference in New Issue