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