Tau.Acuvim/portal/src/Tau.Acuvim.Portal/DTOs/FleetCostDtos.cs
Diseri Pearson 7627306800 Cross-customer cost compute on the Admin side
Per-customer cost over a UTC time range, joining the fleet hierarchy:
hourly_per_device CA × Devices × Sites × Municipalities × active Tariff
× period (in the municipality's local time) → per-bucket kWh × rate.
Per-day rollup + per-device breakdown. VAT applied per bucket so cost
stays correct across mid-window tariff changes.

Backend
- FleetCostService.ComputeAsync(customerId, fromUtc, toUtc):
  - Loads device→site→municipality mapping (small per customer).
  - Loads tariffs (with periods) grouped by municipality, ordered by
    EffectiveFrom desc for active-tariff lookup.
  - Reads hourly buckets in range from fleet.hourly_per_device via
    raw ADO (the CA isn't an EF entity).
  - For each bucket: pick active tariff for the bucket's date, convert
    bucket start to local time via Municipality.TimeZoneId, pick the
    matching period (or default rate), compute base + VAT.
  - Rolls up per UTC date + per device. Tracks BucketsWithoutTariff
    when site has no muni or no tariff covers the bucket date.
- New DTOs: FleetCostDto / FleetCostDayDto / FleetCostDeviceDto.
- Endpoint: GET /api/fleet/customers/{id}/cost?from&to. AdminOnly.
  Validation: to > from, range <= 366 days (BadRequest on violation).
- Period-selection helper duplicated from CostCalculator (5 lines;
  generic abstraction across TariffPeriod / FleetTariffPeriod is
  more code than the duplication).
- Fixed monthly charges deliberately NOT applied (whole-month billing
  concept; FixedMonthlyChargesIncluded=false in the response).

Frontend
- AdminCustomerDetailPage gets a Cost tab:
  - RangePicker with quick ranges (Today, Last 7d, Last 30d, This
    month). Default last 7 days.
  - 4 Statistic cards: total kWh, base cost, VAT, total.
  - Warning alerts: when buckets-without-tariff > 0; always-on info
    that fixed monthly charges aren't included.
  - Per-day table + per-device table.

Verified end-to-end on the running stack
- Patched DEV0001's existing site to LocalMunicipalityId=1
  (Phase 23 test municipality with Domestic TOU tariff).
- Ingested 3 measurements at 10:00 / 10:20 / 10:40 UTC with kWh
  totals 2000 / 2020 / 2050 → hourly CA bucket has delta = 50 kWh.
- Total kWh in window = 56 (50 from new bucket + 6 from earlier
  Phase 14 backfill bucket).
- Tariff resolution: 10:00-12:00 UTC = 12:00-14:00 SAST, which is
  neither Peak (17:00-20:00 weekdays) nor Off-Peak (weekends only)
  → defaults to 2.50/kWh.
- 56 × 2.50 = 140.00 base ✓
- 140 × 0.15 = 21.00 VAT ✓
- Total 161.00 ✓ exactly what the API returned.

Docs: FLEET-DESIGN.md §11 row updated — tariff sync + cost compute
both marked as shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 11:45:44 +02:00

30 lines
707 B
C#

namespace Tau.Acuvim.Portal.DTOs;
public sealed record FleetCostDayDto(
DateOnly Date,
double Kwh,
decimal BaseCost,
decimal VatAmount,
decimal TotalCost);
public sealed record FleetCostDeviceDto(
Guid DeviceId,
string DeviceName,
string? MunicipalityName,
double Kwh,
decimal BaseCost,
decimal TotalCost);
public sealed record FleetCostDto(
DateTime FromUtc,
DateTime ToUtc,
double TotalKwh,
decimal TotalBaseCost,
decimal TotalVatAmount,
decimal TotalCost,
int Buckets,
int BucketsWithoutTariff,
IReadOnlyList<FleetCostDayDto> Daily,
IReadOnlyList<FleetCostDeviceDto> PerDevice,
bool FixedMonthlyChargesIncluded);