Fleet sync: Municipalities + Tariffs + TariffPeriods
Mirror the customer-side rate hierarchy (Municipality → Tariff →
TariffPeriod) to the central DB, scoped by CustomerId.
Per-municipality rate structure is preserved exactly as customers
configure it locally — each tariff still references its municipality
and carries its own per-period TOU rates.
Backend
- New fleet entities: FleetMunicipality, FleetTariff, FleetTariffPeriod.
Composite PKs (CustomerId, Id) like Sites/Devices so two customers'
rate trees never collide. FleetTariff has a real FK to
FleetMunicipality on (CustomerId, MunicipalityId); FleetTariffPeriod
cascades from its FleetTariff parent.
- Two new push batch types: "municipalities" (full set) and "tariffs"
(full set with periods nested inside each tariff). Push order per
tick: sites → devices → municipalities → tariffs → measurements.
- FleetIngestService dispatches the new types. Municipalities upsert
by (CustomerId, Id). Tariffs run inside a single transaction per
batch: upsert the tariff row, DELETE all periods for that tariff,
INSERT the new set — atomic period replacement matches the
customer-side UpsertTariffRequest semantics.
- FleetPushState gains ResourceMunicipalities + ResourceTariffs
constants so the per-resource cursor/backoff state has slots for them.
- FleetQueryService.GetCustomerDetailAsync now includes municipalities
and tariffs (with periods, with municipality name joined client-side
from the lookup dict). New DTOs: FleetMunicipalityViewDto,
FleetTariffViewDto, FleetTariffPeriodViewDto.
- AdminDbContext migration AddFleetRates creates the three tables and
their indexes/FKs.
Frontend
- Customer detail page gains a "Tariffs (N)" tab. Each tariff renders
as a collapsible card with its municipality, active flag, effective
window, default rate / fixed charge / VAT, and an inline period
table (Period name, day-of-week bitmask formatted as MTWTFSS, start,
end, rate). Empty state when no tariffs synced yet.
Verified end-to-end on the dev host
- Created "Phase23 Test City" municipality + "Domestic TOU" tariff
with two periods (Peak weekdays 17:00-20:00 @ 3.75, Off-Peak
weekends 00:00-23:59 @ 1.20) on the Client.
- Within one push tick (~20s) all three rows landed on Admin:
fleet.Municipalities (1), fleet.Tariffs (1), fleet.TariffPeriods (2).
- /api/fleet/customers/{id}/detail returns the full tree with
municipality name resolved.
Tests: 61/61 still passing.
Design doc (FLEET-DESIGN.md §11) updated — tariff sync row marked
shipped, cross-customer cost compute flagged as the natural next step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3333202f3a
commit
b654997fc9
@ -316,7 +316,7 @@ Database__ConnectionString=Host=timescaledb;... # central fleet DB (separ
|
|||||||
|
|
||||||
| Seam | Where v2 picks it up |
|
| Seam | Where v2 picks it up |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Tariff sync + cross-customer cost | New `fleet.Tariffs` table, push tariff change events from customer, derived cost in `fleet_daily_per_customer`. |
|
| ~~Tariff sync~~ + cross-customer cost | **Tariff sync shipped.** `fleet.Municipalities` + `fleet.Tariffs` + `fleet.TariffPeriods` mirror the customer-side rate hierarchy scoped by `CustomerId`. New push batch types: `municipalities`, `tariffs` (with periods nested). Per-municipality rates preserved. **Cost compute on Admin side is the natural next step** — a `FleetCostService` joining `fleet.hourly_per_device` × tariffs → per-customer kWh × rate. |
|
||||||
| Per-customer Postgres RLS for multi-Admin-user setups | Add `current_setting('app.customer_filter')`-based RLS policies on `fleet.*` tables; Admin role to customer-scope mapping in `IdentityRole.CustomerId`. |
|
| Per-customer Postgres RLS for multi-Admin-user setups | Add `current_setting('app.customer_filter')`-based RLS policies on `fleet.*` tables; Admin role to customer-scope mapping in `IdentityRole.CustomerId`. |
|
||||||
| Bidirectional Admin → Customer commands | New WebSocket or long-poll channel on customer side; gated by mutual cert or a second token. |
|
| Bidirectional Admin → Customer commands | New WebSocket or long-poll channel on customer side; gated by mutual cert or a second token. |
|
||||||
| Branding sync (for the "Admin sees customer's brand when drilling in" niceness) | Push branding row from customer; Admin renders the customer's brand on Customer-detail pages. |
|
| Branding sync (for the "Admin sees customer's brand when drilling in" niceness) | Push branding row from customer; Admin renders the customer's brand on Customer-detail pages. |
|
||||||
|
|||||||
@ -57,6 +57,36 @@ export interface FleetIngestEvent {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FleetMunicipalityView {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
timeZoneId: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FleetTariffPeriodView {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
daysOfWeek: number;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
ratePerKwh: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FleetTariffView {
|
||||||
|
id: number;
|
||||||
|
municipalityId: number;
|
||||||
|
municipalityName: string | null;
|
||||||
|
name: string;
|
||||||
|
effectiveFrom: string;
|
||||||
|
effectiveTo: string | null;
|
||||||
|
defaultRatePerKwh: number;
|
||||||
|
fixedMonthlyCharge: number;
|
||||||
|
vatPercentage: number;
|
||||||
|
isActive: boolean;
|
||||||
|
periods: FleetTariffPeriodView[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface FleetCustomerDetail {
|
export interface FleetCustomerDetail {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
@ -67,6 +97,8 @@ export interface FleetCustomerDetail {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
sites: FleetSite[];
|
sites: FleetSite[];
|
||||||
devices: FleetDevice[];
|
devices: FleetDevice[];
|
||||||
|
municipalities: FleetMunicipalityView[];
|
||||||
|
tariffs: FleetTariffView[];
|
||||||
recentMeasurements: FleetRecentMeasurement[];
|
recentMeasurements: FleetRecentMeasurement[];
|
||||||
recentIngestEvents: FleetIngestEvent[];
|
recentIngestEvents: FleetIngestEvent[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
fetchFleetCustomerDetail, type FleetSite, type FleetDevice,
|
fetchFleetCustomerDetail, type FleetSite, type FleetDevice,
|
||||||
type FleetRecentMeasurement, type FleetIngestEvent,
|
type FleetRecentMeasurement, type FleetIngestEvent,
|
||||||
|
type FleetTariffView, type FleetTariffPeriodView,
|
||||||
} from '../api/fleet';
|
} from '../api/fleet';
|
||||||
import { fetchGrafanaConfig } from '../api/grafana';
|
import { fetchGrafanaConfig } from '../api/grafana';
|
||||||
|
|
||||||
@ -129,9 +130,83 @@ export function AdminCustomerDetailPage() {
|
|||||||
label: `Devices (${data.devices.length})`,
|
label: `Devices (${data.devices.length})`,
|
||||||
children: <Table<FleetDevice> rowKey="id" columns={deviceCols} dataSource={data.devices} pagination={false} size="small" />,
|
children: <Table<FleetDevice> rowKey="id" columns={deviceCols} dataSource={data.devices} pagination={false} size="small" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'tariffs',
|
||||||
|
label: `Tariffs (${data.tariffs.length})`,
|
||||||
|
children: <TariffsTab tariffs={data.tariffs} municipalitiesCount={data.municipalities.length} />,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tariffs tab: collapsible per-tariff cards with the period table inline ─
|
||||||
|
function TariffsTab({ tariffs, municipalitiesCount }: { tariffs: FleetTariffView[]; municipalitiesCount: number }) {
|
||||||
|
if (tariffs.length === 0) {
|
||||||
|
return (
|
||||||
|
<Text type="secondary">
|
||||||
|
No tariffs synced yet ({municipalitiesCount} municipalities). They appear here after the
|
||||||
|
customer's next push tick.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodCols: ColumnsType<FleetTariffPeriodView> = [
|
||||||
|
{ title: 'Period', dataIndex: 'name', key: 'name', render: (v: string) => <Text strong>{v}</Text> },
|
||||||
|
{
|
||||||
|
title: 'Days', dataIndex: 'daysOfWeek', key: 'd',
|
||||||
|
render: (mask: number) => formatDays(mask),
|
||||||
|
},
|
||||||
|
{ title: 'Start', dataIndex: 'startTime', key: 's' },
|
||||||
|
{ title: 'End', dataIndex: 'endTime', key: 'e' },
|
||||||
|
{ title: 'Rate (/kWh)', dataIndex: 'ratePerKwh', key: 'r', render: (n: number) => n.toFixed(4) },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
{tariffs.map((t) => (
|
||||||
|
<Card
|
||||||
|
key={t.id}
|
||||||
|
size="small"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<Text strong>{t.name}</Text>
|
||||||
|
<Text type="secondary">·</Text>
|
||||||
|
<Text>{t.municipalityName ?? `(municipality #${t.municipalityId})`}</Text>
|
||||||
|
{t.isActive ? <Tag color="green">Active</Tag> : <Tag>Inactive</Tag>}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{t.effectiveFrom}{t.effectiveTo ? ` → ${t.effectiveTo}` : ' →'}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Descriptions size="small" column={3} style={{ marginBottom: 12 }}>
|
||||||
|
<Descriptions.Item label="Default rate">{t.defaultRatePerKwh.toFixed(4)}/kWh</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Fixed monthly">{t.fixedMonthlyCharge.toFixed(2)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="VAT">{t.vatPercentage.toFixed(2)}%</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
{t.periods.length > 0 ? (
|
||||||
|
<Table<FleetTariffPeriodView>
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
columns={periodCols}
|
||||||
|
dataSource={t.periods}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">No TOU periods — default rate applies always.</Text>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DAY_LABELS = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
|
||||||
|
function formatDays(mask: number): string {
|
||||||
|
return DAY_LABELS.filter((_, i) => (mask & (1 << i)) !== 0).join('') || '—';
|
||||||
|
}
|
||||||
|
|||||||
@ -36,6 +36,25 @@ public sealed record FleetRecentMeasurementDto(
|
|||||||
double ActivePowerKw,
|
double ActivePowerKw,
|
||||||
double? EnergyImportedKwh);
|
double? EnergyImportedKwh);
|
||||||
|
|
||||||
|
public sealed record FleetMunicipalityViewDto(
|
||||||
|
int Id, string Name, string? TimeZoneId, bool IsActive);
|
||||||
|
|
||||||
|
public sealed record FleetTariffPeriodViewDto(
|
||||||
|
int Id, string Name, int DaysOfWeek, string StartTime, string EndTime, decimal RatePerKwh);
|
||||||
|
|
||||||
|
public sealed record FleetTariffViewDto(
|
||||||
|
int Id,
|
||||||
|
int MunicipalityId,
|
||||||
|
string? MunicipalityName,
|
||||||
|
string Name,
|
||||||
|
DateOnly EffectiveFrom,
|
||||||
|
DateOnly? EffectiveTo,
|
||||||
|
decimal DefaultRatePerKwh,
|
||||||
|
decimal FixedMonthlyCharge,
|
||||||
|
decimal VatPercentage,
|
||||||
|
bool IsActive,
|
||||||
|
IReadOnlyList<FleetTariffPeriodViewDto> Periods);
|
||||||
|
|
||||||
public sealed record FleetCustomerDetailDto(
|
public sealed record FleetCustomerDetailDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
string Code,
|
string Code,
|
||||||
@ -46,5 +65,7 @@ public sealed record FleetCustomerDetailDto(
|
|||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
IReadOnlyList<FleetSiteDto> Sites,
|
IReadOnlyList<FleetSiteDto> Sites,
|
||||||
IReadOnlyList<FleetDeviceDto> Devices,
|
IReadOnlyList<FleetDeviceDto> Devices,
|
||||||
|
IReadOnlyList<FleetMunicipalityViewDto> Municipalities,
|
||||||
|
IReadOnlyList<FleetTariffViewDto> Tariffs,
|
||||||
IReadOnlyList<FleetRecentMeasurementDto> RecentMeasurements,
|
IReadOnlyList<FleetRecentMeasurementDto> RecentMeasurements,
|
||||||
IReadOnlyList<FleetIngestEventDto> RecentIngestEvents);
|
IReadOnlyList<FleetIngestEventDto> RecentIngestEvents);
|
||||||
|
|||||||
@ -33,3 +33,32 @@ public sealed record FleetMeasurementDto(
|
|||||||
DateTime ReceivedAt);
|
DateTime ReceivedAt);
|
||||||
|
|
||||||
public sealed record FleetIngestResult(int Accepted, int Rejected, string[] Errors);
|
public sealed record FleetIngestResult(int Accepted, int Rejected, string[] Errors);
|
||||||
|
|
||||||
|
// ── Rates sync (Municipality → Tariff → TariffPeriod) ──────────────────
|
||||||
|
// Push includes municipalities first, then tariffs with their periods nested.
|
||||||
|
|
||||||
|
public sealed record FleetMunicipalityDto(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
string? TimeZoneId,
|
||||||
|
bool IsActive);
|
||||||
|
|
||||||
|
public sealed record FleetTariffPeriodWireDto(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
int DaysOfWeek,
|
||||||
|
string StartTime,
|
||||||
|
string EndTime,
|
||||||
|
decimal RatePerKwh);
|
||||||
|
|
||||||
|
public sealed record FleetTariffWireDto(
|
||||||
|
int Id,
|
||||||
|
int MunicipalityId,
|
||||||
|
string Name,
|
||||||
|
DateOnly EffectiveFrom,
|
||||||
|
DateOnly? EffectiveTo,
|
||||||
|
decimal DefaultRatePerKwh,
|
||||||
|
decimal FixedMonthlyCharge,
|
||||||
|
decimal VatPercentage,
|
||||||
|
bool IsActive,
|
||||||
|
IReadOnlyList<FleetTariffPeriodWireDto> Periods);
|
||||||
|
|||||||
@ -18,6 +18,9 @@ public class AdminDbContext : IdentityDbContext<ApplicationUser, IdentityRole, s
|
|||||||
public DbSet<FleetSite> FleetSites => Set<FleetSite>();
|
public DbSet<FleetSite> FleetSites => Set<FleetSite>();
|
||||||
public DbSet<FleetDevice> FleetDevices => Set<FleetDevice>();
|
public DbSet<FleetDevice> FleetDevices => Set<FleetDevice>();
|
||||||
public DbSet<FleetPowerMeasurement> FleetPowerMeasurements => Set<FleetPowerMeasurement>();
|
public DbSet<FleetPowerMeasurement> FleetPowerMeasurements => Set<FleetPowerMeasurement>();
|
||||||
|
public DbSet<FleetMunicipality> FleetMunicipalities => Set<FleetMunicipality>();
|
||||||
|
public DbSet<FleetTariff> FleetTariffs => Set<FleetTariff>();
|
||||||
|
public DbSet<FleetTariffPeriod> FleetTariffPeriods => Set<FleetTariffPeriod>();
|
||||||
public DbSet<IngestEvent> IngestEvents => Set<IngestEvent>();
|
public DbSet<IngestEvent> IngestEvents => Set<IngestEvent>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
@ -84,5 +87,48 @@ public class AdminDbContext : IdentityDbContext<ApplicationUser, IdentityRole, s
|
|||||||
.HasForeignKey(x => x.CustomerId)
|
.HasForeignKey(x => x.CustomerId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Entity<FleetMunicipality>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("Municipalities", schema: "fleet");
|
||||||
|
entity.HasKey(x => new { x.CustomerId, x.Id });
|
||||||
|
entity.HasOne(x => x.Customer)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.CustomerId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<FleetTariff>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("Tariffs", schema: "fleet");
|
||||||
|
entity.HasKey(x => new { x.CustomerId, x.Id });
|
||||||
|
entity.Property(x => x.DefaultRatePerKwh).HasPrecision(10, 4);
|
||||||
|
entity.Property(x => x.FixedMonthlyCharge).HasPrecision(10, 2);
|
||||||
|
entity.Property(x => x.VatPercentage).HasPrecision(5, 2);
|
||||||
|
entity.HasIndex(x => new { x.CustomerId, x.MunicipalityId, x.EffectiveFrom });
|
||||||
|
entity.HasOne(x => x.Customer)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.CustomerId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
entity.HasOne(x => x.Municipality)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => new { x.CustomerId, x.MunicipalityId })
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<FleetTariffPeriod>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("TariffPeriods", schema: "fleet");
|
||||||
|
entity.HasKey(x => new { x.CustomerId, x.Id });
|
||||||
|
entity.Property(x => x.RatePerKwh).HasPrecision(10, 4);
|
||||||
|
entity.HasOne(x => x.Customer)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.CustomerId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
entity.HasOne(x => x.Tariff)
|
||||||
|
.WithMany(t => t.Periods)
|
||||||
|
.HasForeignKey(x => new { x.CustomerId, x.TariffId })
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Tau.Acuvim.Portal.Domain.Fleet;
|
||||||
|
|
||||||
|
public class FleetMunicipality
|
||||||
|
{
|
||||||
|
// Preserved from the customer-side Municipality.Id (int autoincrement).
|
||||||
|
// Composite PK with CustomerId so two customers' municipalities don't collide.
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public Guid CustomerId { get; set; }
|
||||||
|
public Customer? Customer { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? TimeZoneId { get; set; }
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
public DateTime ReceivedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@ -7,6 +7,8 @@ public class FleetPushState
|
|||||||
{
|
{
|
||||||
public const string ResourceSites = "sites";
|
public const string ResourceSites = "sites";
|
||||||
public const string ResourceDevices = "devices";
|
public const string ResourceDevices = "devices";
|
||||||
|
public const string ResourceMunicipalities = "municipalities";
|
||||||
|
public const string ResourceTariffs = "tariffs";
|
||||||
public const string ResourceMeasurements = "measurements";
|
public const string ResourceMeasurements = "measurements";
|
||||||
|
|
||||||
[MaxLength(20)]
|
[MaxLength(20)]
|
||||||
|
|||||||
31
portal/src/Tau.Acuvim.Portal/Domain/Fleet/FleetTariff.cs
Normal file
31
portal/src/Tau.Acuvim.Portal/Domain/Fleet/FleetTariff.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Tau.Acuvim.Portal.Domain.Fleet;
|
||||||
|
|
||||||
|
public class FleetTariff
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public Guid CustomerId { get; set; }
|
||||||
|
public Customer? Customer { get; set; }
|
||||||
|
|
||||||
|
// Refers to (CustomerId, MunicipalityId) on FleetMunicipality.
|
||||||
|
public int MunicipalityId { get; set; }
|
||||||
|
public FleetMunicipality? Municipality { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateOnly EffectiveFrom { get; set; }
|
||||||
|
public DateOnly? EffectiveTo { get; set; }
|
||||||
|
|
||||||
|
public decimal DefaultRatePerKwh { get; set; }
|
||||||
|
public decimal FixedMonthlyCharge { get; set; }
|
||||||
|
public decimal VatPercentage { get; set; }
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
public DateTime ReceivedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public List<FleetTariffPeriod> Periods { get; set; } = new();
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Tau.Acuvim.Portal.Domain.Rates;
|
||||||
|
|
||||||
|
namespace Tau.Acuvim.Portal.Domain.Fleet;
|
||||||
|
|
||||||
|
public class FleetTariffPeriod
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public Guid CustomerId { get; set; }
|
||||||
|
public Customer? Customer { get; set; }
|
||||||
|
|
||||||
|
public int TariffId { get; set; }
|
||||||
|
public FleetTariff? Tariff { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DayOfWeekFlag DaysOfWeek { get; set; }
|
||||||
|
|
||||||
|
public TimeOnly StartTime { get; set; }
|
||||||
|
public TimeOnly EndTime { get; set; }
|
||||||
|
|
||||||
|
public decimal RatePerKwh { get; set; }
|
||||||
|
}
|
||||||
770
portal/src/Tau.Acuvim.Portal/Migrations/Admin/20260518090118_AddFleetRates.Designer.cs
generated
Normal file
770
portal/src/Tau.Acuvim.Portal/Migrations/Admin/20260518090118_AddFleetRates.Designer.cs
generated
Normal file
@ -0,0 +1,770 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Tau.Acuvim.Portal.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Tau.Acuvim.Portal.Migrations.Admin
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AdminDbContext))]
|
||||||
|
[Migration("20260518090118_AddFleetRates")]
|
||||||
|
partial class AddFleetRates
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("app")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.8")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Branding.WhiteLabelSettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("AccentColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("ApplicationName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("FooterText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("LogoUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("PrimaryColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("SecondaryColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("WhiteLabelSettings", "app");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.Customer", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FirstSeenAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastSeenAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PreviousTokenExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("PreviousTokenHash")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("TokenIssuedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("TokenRotatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("PreviousTokenHash");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Customers", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetDevice", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("SiteId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("CustomerId", "Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId", "SiteId");
|
||||||
|
|
||||||
|
b.ToTable("Devices", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetMunicipality", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZoneId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("CustomerId", "Id");
|
||||||
|
|
||||||
|
b.ToTable("Municipalities", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetPowerMeasurement", b =>
|
||||||
|
{
|
||||||
|
b.Property<DateTime>("Time")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("DeviceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<double>("ActivePowerKw")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<double?>("ApparentPowerKva")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<double?>("EnergyExportedKwh")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<double?>("EnergyImportedKwh")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<double?>("FrequencyHz")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<double?>("PowerFactor")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<double?>("ReactivePowerKvar")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<string>("Source")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<double?>("VoltageV")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.HasKey("Time", "CustomerId", "DeviceId");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId", "Time")
|
||||||
|
.IsDescending(false, true);
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId", "DeviceId", "Time")
|
||||||
|
.IsDescending(false, false, true);
|
||||||
|
|
||||||
|
b.ToTable("PowerMeasurements", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetSite", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int?>("LocalMunicipalityId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("CustomerId", "Id");
|
||||||
|
|
||||||
|
b.ToTable("Sites", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariff", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal>("DefaultRatePerKwh")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("numeric(10,4)");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EffectiveFrom")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("EffectiveTo")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<decimal>("FixedMonthlyCharge")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("numeric(10,2)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("MunicipalityId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal>("VatPercentage")
|
||||||
|
.HasPrecision(5, 2)
|
||||||
|
.HasColumnType("numeric(5,2)");
|
||||||
|
|
||||||
|
b.HasKey("CustomerId", "Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId", "MunicipalityId", "EffectiveFrom");
|
||||||
|
|
||||||
|
b.ToTable("Tariffs", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariffPeriod", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("DaysOfWeek")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<TimeOnly>("EndTime")
|
||||||
|
.HasColumnType("time without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<decimal>("RatePerKwh")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("numeric(10,4)");
|
||||||
|
|
||||||
|
b.Property<TimeOnly>("StartTime")
|
||||||
|
.HasColumnType("time without time zone");
|
||||||
|
|
||||||
|
b.Property<int>("TariffId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("CustomerId", "Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId", "TariffId");
|
||||||
|
|
||||||
|
b.ToTable("TariffPeriods", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.IngestEvent", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("BatchBytes")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("BatchType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("ClientHwm")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Error")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("RowsAccepted")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RowsRejected")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<TimeSpan?>("TimeSpread")
|
||||||
|
.HasColumnType("interval");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId", "ReceivedAt")
|
||||||
|
.IsDescending(false, true);
|
||||||
|
|
||||||
|
b.ToTable("IngestEvents", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Identity.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetDevice", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.FleetSite", "Site")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId", "SiteId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
|
||||||
|
b.Navigation("Site");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetMunicipality", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetPowerMeasurement", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.FleetDevice", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId", "DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetSite", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariff", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.FleetMunicipality", "Municipality")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId", "MunicipalityId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
|
||||||
|
b.Navigation("Municipality");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariffPeriod", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.FleetTariff", "Tariff")
|
||||||
|
.WithMany("Periods")
|
||||||
|
.HasForeignKey("CustomerId", "TariffId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
|
||||||
|
b.Navigation("Tariff");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.IngestEvent", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariff", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Periods");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Tau.Acuvim.Portal.Migrations.Admin
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFleetRates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Municipalities",
|
||||||
|
schema: "fleet",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
TimeZoneId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||||
|
IsActive = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
ReceivedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Municipalities", x => new { x.CustomerId, x.Id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Municipalities_Customers_CustomerId",
|
||||||
|
column: x => x.CustomerId,
|
||||||
|
principalSchema: "fleet",
|
||||||
|
principalTable: "Customers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Tariffs",
|
||||||
|
schema: "fleet",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
MunicipalityId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
EffectiveFrom = table.Column<DateOnly>(type: "date", nullable: false),
|
||||||
|
EffectiveTo = table.Column<DateOnly>(type: "date", nullable: true),
|
||||||
|
DefaultRatePerKwh = table.Column<decimal>(type: "numeric(10,4)", precision: 10, scale: 4, nullable: false),
|
||||||
|
FixedMonthlyCharge = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false),
|
||||||
|
VatPercentage = table.Column<decimal>(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
ReceivedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tariffs", x => new { x.CustomerId, x.Id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tariffs_Customers_CustomerId",
|
||||||
|
column: x => x.CustomerId,
|
||||||
|
principalSchema: "fleet",
|
||||||
|
principalTable: "Customers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tariffs_Municipalities_CustomerId_MunicipalityId",
|
||||||
|
columns: x => new { x.CustomerId, x.MunicipalityId },
|
||||||
|
principalSchema: "fleet",
|
||||||
|
principalTable: "Municipalities",
|
||||||
|
principalColumns: new[] { "CustomerId", "Id" },
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TariffPeriods",
|
||||||
|
schema: "fleet",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TariffId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
DaysOfWeek = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
StartTime = table.Column<TimeOnly>(type: "time without time zone", nullable: false),
|
||||||
|
EndTime = table.Column<TimeOnly>(type: "time without time zone", nullable: false),
|
||||||
|
RatePerKwh = table.Column<decimal>(type: "numeric(10,4)", precision: 10, scale: 4, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TariffPeriods", x => new { x.CustomerId, x.Id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TariffPeriods_Customers_CustomerId",
|
||||||
|
column: x => x.CustomerId,
|
||||||
|
principalSchema: "fleet",
|
||||||
|
principalTable: "Customers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TariffPeriods_Tariffs_CustomerId_TariffId",
|
||||||
|
columns: x => new { x.CustomerId, x.TariffId },
|
||||||
|
principalSchema: "fleet",
|
||||||
|
principalTable: "Tariffs",
|
||||||
|
principalColumns: new[] { "CustomerId", "Id" },
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TariffPeriods_CustomerId_TariffId",
|
||||||
|
schema: "fleet",
|
||||||
|
table: "TariffPeriods",
|
||||||
|
columns: new[] { "CustomerId", "TariffId" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tariffs_CustomerId_MunicipalityId_EffectiveFrom",
|
||||||
|
schema: "fleet",
|
||||||
|
table: "Tariffs",
|
||||||
|
columns: new[] { "CustomerId", "MunicipalityId", "EffectiveFrom" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TariffPeriods",
|
||||||
|
schema: "fleet");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Tariffs",
|
||||||
|
schema: "fleet");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Municipalities",
|
||||||
|
schema: "fleet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -295,6 +295,34 @@ namespace Tau.Acuvim.Portal.Migrations.Admin
|
|||||||
b.ToTable("Devices", "fleet");
|
b.ToTable("Devices", "fleet");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetMunicipality", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZoneId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("CustomerId", "Id");
|
||||||
|
|
||||||
|
b.ToTable("Municipalities", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetPowerMeasurement", b =>
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetPowerMeasurement", b =>
|
||||||
{
|
{
|
||||||
b.Property<DateTime>("Time")
|
b.Property<DateTime>("Time")
|
||||||
@ -376,6 +404,89 @@ namespace Tau.Acuvim.Portal.Migrations.Admin
|
|||||||
b.ToTable("Sites", "fleet");
|
b.ToTable("Sites", "fleet");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariff", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal>("DefaultRatePerKwh")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("numeric(10,4)");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EffectiveFrom")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("EffectiveTo")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<decimal>("FixedMonthlyCharge")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("numeric(10,2)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("MunicipalityId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal>("VatPercentage")
|
||||||
|
.HasPrecision(5, 2)
|
||||||
|
.HasColumnType("numeric(5,2)");
|
||||||
|
|
||||||
|
b.HasKey("CustomerId", "Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId", "MunicipalityId", "EffectiveFrom");
|
||||||
|
|
||||||
|
b.ToTable("Tariffs", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariffPeriod", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("DaysOfWeek")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<TimeOnly>("EndTime")
|
||||||
|
.HasColumnType("time without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<decimal>("RatePerKwh")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("numeric(10,4)");
|
||||||
|
|
||||||
|
b.Property<TimeOnly>("StartTime")
|
||||||
|
.HasColumnType("time without time zone");
|
||||||
|
|
||||||
|
b.Property<int>("TariffId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("CustomerId", "Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId", "TariffId");
|
||||||
|
|
||||||
|
b.ToTable("TariffPeriods", "fleet");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.IngestEvent", b =>
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.IngestEvent", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -566,6 +677,17 @@ namespace Tau.Acuvim.Portal.Migrations.Admin
|
|||||||
b.Navigation("Site");
|
b.Navigation("Site");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetMunicipality", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetPowerMeasurement", b =>
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetPowerMeasurement", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.FleetDevice", null)
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.FleetDevice", null)
|
||||||
@ -586,6 +708,44 @@ namespace Tau.Acuvim.Portal.Migrations.Admin
|
|||||||
b.Navigation("Customer");
|
b.Navigation("Customer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariff", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.FleetMunicipality", "Municipality")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId", "MunicipalityId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
|
||||||
|
b.Navigation("Municipality");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariffPeriod", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.FleetTariff", "Tariff")
|
||||||
|
.WithMany("Periods")
|
||||||
|
.HasForeignKey("CustomerId", "TariffId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
|
||||||
|
b.Navigation("Tariff");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.IngestEvent", b =>
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.IngestEvent", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
b.HasOne("Tau.Acuvim.Portal.Domain.Fleet.Customer", "Customer")
|
||||||
@ -596,6 +756,11 @@ namespace Tau.Acuvim.Portal.Migrations.Admin
|
|||||||
|
|
||||||
b.Navigation("Customer");
|
b.Navigation("Customer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Tau.Acuvim.Portal.Domain.Fleet.FleetTariff", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Periods");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ using Tau.Acuvim.Portal.DTOs;
|
|||||||
|
|
||||||
namespace Tau.Acuvim.Portal.Services;
|
namespace Tau.Acuvim.Portal.Services;
|
||||||
|
|
||||||
public enum FleetBatchType { Sites, Devices, Measurements }
|
public enum FleetBatchType { Sites, Devices, Municipalities, Tariffs, Measurements }
|
||||||
|
|
||||||
public sealed class FleetIngestService(
|
public sealed class FleetIngestService(
|
||||||
AdminDbContext db,
|
AdminDbContext db,
|
||||||
@ -52,6 +52,8 @@ public sealed class FleetIngestService(
|
|||||||
{
|
{
|
||||||
FleetBatchType.Sites => (await IngestSitesAsync(customer.Id, rawBody, ct), null),
|
FleetBatchType.Sites => (await IngestSitesAsync(customer.Id, rawBody, ct), null),
|
||||||
FleetBatchType.Devices => (await IngestDevicesAsync(customer.Id, rawBody, ct), null),
|
FleetBatchType.Devices => (await IngestDevicesAsync(customer.Id, rawBody, ct), null),
|
||||||
|
FleetBatchType.Municipalities => (await IngestMunicipalitiesAsync(customer.Id, rawBody, ct), null),
|
||||||
|
FleetBatchType.Tariffs => (await IngestTariffsAsync(customer.Id, rawBody, ct), null),
|
||||||
FleetBatchType.Measurements => await IngestMeasurementsAsync(customer.Id, rawBody, ct),
|
FleetBatchType.Measurements => await IngestMeasurementsAsync(customer.Id, rawBody, ct),
|
||||||
_ => throw new InvalidOperationException()
|
_ => throw new InvalidOperationException()
|
||||||
};
|
};
|
||||||
@ -86,6 +88,8 @@ public sealed class FleetIngestService(
|
|||||||
{
|
{
|
||||||
"sites" => (type = FleetBatchType.Sites) is var _ && true,
|
"sites" => (type = FleetBatchType.Sites) is var _ && true,
|
||||||
"devices" => (type = FleetBatchType.Devices) is var _ && true,
|
"devices" => (type = FleetBatchType.Devices) is var _ && true,
|
||||||
|
"municipalities" => (type = FleetBatchType.Municipalities) is var _ && true,
|
||||||
|
"tariffs" => (type = FleetBatchType.Tariffs) is var _ && true,
|
||||||
"measurements" => (type = FleetBatchType.Measurements) is var _ && true,
|
"measurements" => (type = FleetBatchType.Measurements) is var _ && true,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
@ -213,6 +217,125 @@ public sealed class FleetIngestService(
|
|||||||
return (new FleetIngestResult(affected, rejected, Array.Empty<string>()), spread);
|
return (new FleetIngestResult(affected, rejected, Array.Empty<string>()), spread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Municipalities: upsert by (CustomerId, Id) ─────────────────────────
|
||||||
|
private async Task<FleetIngestResult> IngestMunicipalitiesAsync(Guid customerId, string body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = JsonSerializer.Deserialize<FleetMunicipalityDto[]>(body, JsonOpts) ?? Array.Empty<FleetMunicipalityDto>();
|
||||||
|
if (rows.Length == 0) return new FleetIngestResult(0, 0, Array.Empty<string>());
|
||||||
|
|
||||||
|
var sql = new StringBuilder(
|
||||||
|
"""
|
||||||
|
INSERT INTO fleet."Municipalities" ("CustomerId","Id","Name","TimeZoneId","IsActive","ReceivedAt") VALUES
|
||||||
|
""");
|
||||||
|
var ps = new List<NpgsqlParameter>(rows.Length * 5);
|
||||||
|
for (int i = 0; i < rows.Length; i++)
|
||||||
|
{
|
||||||
|
if (i > 0) sql.Append(',');
|
||||||
|
var b = i * 5;
|
||||||
|
sql.Append($" (@cust,@p{b},@p{b + 1},@p{b + 2},@p{b + 3},@p{b + 4})");
|
||||||
|
ps.Add(new($"p{b}", rows[i].Id));
|
||||||
|
ps.Add(new($"p{b + 1}", rows[i].Name));
|
||||||
|
ps.Add(new($"p{b + 2}", (object?)rows[i].TimeZoneId ?? DBNull.Value));
|
||||||
|
ps.Add(new($"p{b + 3}", rows[i].IsActive));
|
||||||
|
ps.Add(new($"p{b + 4}", DateTime.UtcNow));
|
||||||
|
}
|
||||||
|
ps.Add(new("cust", customerId));
|
||||||
|
sql.Append("""
|
||||||
|
ON CONFLICT ("CustomerId","Id") DO UPDATE SET
|
||||||
|
"Name"=EXCLUDED."Name",
|
||||||
|
"TimeZoneId"=EXCLUDED."TimeZoneId",
|
||||||
|
"IsActive"=EXCLUDED."IsActive",
|
||||||
|
"ReceivedAt"=EXCLUDED."ReceivedAt";
|
||||||
|
""");
|
||||||
|
await db.Database.ExecuteSqlRawAsync(sql.ToString(), ps, ct);
|
||||||
|
return new FleetIngestResult(rows.Length, 0, Array.Empty<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tariffs (with periods nested): upsert tariff + replace its periods ─
|
||||||
|
// One transaction per batch so a tariff is never observed mid-update with stale periods.
|
||||||
|
private async Task<FleetIngestResult> IngestTariffsAsync(Guid customerId, string body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = JsonSerializer.Deserialize<FleetTariffWireDto[]>(body, JsonOpts) ?? Array.Empty<FleetTariffWireDto>();
|
||||||
|
if (rows.Length == 0) return new FleetIngestResult(0, 0, Array.Empty<string>());
|
||||||
|
|
||||||
|
await using var tx = await db.Database.BeginTransactionAsync(ct);
|
||||||
|
|
||||||
|
foreach (var t in rows)
|
||||||
|
{
|
||||||
|
// Upsert the tariff row.
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
INSERT INTO fleet."Tariffs"
|
||||||
|
("CustomerId","Id","MunicipalityId","Name","EffectiveFrom","EffectiveTo",
|
||||||
|
"DefaultRatePerKwh","FixedMonthlyCharge","VatPercentage","IsActive","ReceivedAt")
|
||||||
|
VALUES (@cust,@id,@muni,@name,@from,@to,@def,@fix,@vat,@active,@recv)
|
||||||
|
ON CONFLICT ("CustomerId","Id") DO UPDATE SET
|
||||||
|
"MunicipalityId"=EXCLUDED."MunicipalityId",
|
||||||
|
"Name"=EXCLUDED."Name",
|
||||||
|
"EffectiveFrom"=EXCLUDED."EffectiveFrom",
|
||||||
|
"EffectiveTo"=EXCLUDED."EffectiveTo",
|
||||||
|
"DefaultRatePerKwh"=EXCLUDED."DefaultRatePerKwh",
|
||||||
|
"FixedMonthlyCharge"=EXCLUDED."FixedMonthlyCharge",
|
||||||
|
"VatPercentage"=EXCLUDED."VatPercentage",
|
||||||
|
"IsActive"=EXCLUDED."IsActive",
|
||||||
|
"ReceivedAt"=EXCLUDED."ReceivedAt";
|
||||||
|
""",
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
new NpgsqlParameter("cust", customerId),
|
||||||
|
new NpgsqlParameter("id", t.Id),
|
||||||
|
new NpgsqlParameter("muni", t.MunicipalityId),
|
||||||
|
new NpgsqlParameter("name", t.Name),
|
||||||
|
new NpgsqlParameter("from", t.EffectiveFrom),
|
||||||
|
new NpgsqlParameter("to", (object?)t.EffectiveTo ?? DBNull.Value),
|
||||||
|
new NpgsqlParameter("def", t.DefaultRatePerKwh),
|
||||||
|
new NpgsqlParameter("fix", t.FixedMonthlyCharge),
|
||||||
|
new NpgsqlParameter("vat", t.VatPercentage),
|
||||||
|
new NpgsqlParameter("active", t.IsActive),
|
||||||
|
new NpgsqlParameter("recv", DateTime.UtcNow),
|
||||||
|
},
|
||||||
|
ct);
|
||||||
|
|
||||||
|
// Replace the period set for this tariff atomically.
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
DELETE FROM fleet."TariffPeriods"
|
||||||
|
WHERE "CustomerId" = @cust AND "TariffId" = @tariffId;
|
||||||
|
""",
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
new NpgsqlParameter("cust", customerId),
|
||||||
|
new NpgsqlParameter("tariffId", t.Id),
|
||||||
|
},
|
||||||
|
ct);
|
||||||
|
|
||||||
|
foreach (var p in t.Periods)
|
||||||
|
{
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
INSERT INTO fleet."TariffPeriods"
|
||||||
|
("CustomerId","Id","TariffId","Name","DaysOfWeek","StartTime","EndTime","RatePerKwh")
|
||||||
|
VALUES (@cust,@id,@tariffId,@name,@days,@start,@end,@rate);
|
||||||
|
""",
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
new NpgsqlParameter("cust", customerId),
|
||||||
|
new NpgsqlParameter("id", p.Id),
|
||||||
|
new NpgsqlParameter("tariffId", t.Id),
|
||||||
|
new NpgsqlParameter("name", p.Name),
|
||||||
|
new NpgsqlParameter("days", p.DaysOfWeek),
|
||||||
|
new NpgsqlParameter("start", TimeOnly.Parse(p.StartTime)),
|
||||||
|
new NpgsqlParameter("end", TimeOnly.Parse(p.EndTime)),
|
||||||
|
new NpgsqlParameter("rate", p.RatePerKwh),
|
||||||
|
},
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.CommitAsync(ct);
|
||||||
|
return new FleetIngestResult(rows.Length, 0, Array.Empty<string>());
|
||||||
|
}
|
||||||
|
|
||||||
private async Task WriteEventAsync(
|
private async Task WriteEventAsync(
|
||||||
Guid customerId, string? batchType, int accepted, int rejected, int batchBytes,
|
Guid customerId, string? batchType, int accepted, int rejected, int batchBytes,
|
||||||
string? cursor, string? error, TimeSpan? spread, CancellationToken ct)
|
string? cursor, string? error, TimeSpan? spread, CancellationToken ct)
|
||||||
|
|||||||
@ -34,6 +34,12 @@ public sealed class FleetPushClient(
|
|||||||
public Task<FleetPushResult> PushMeasurementsAsync(IReadOnlyList<FleetMeasurementDto> rows, DateTime cursor, CancellationToken ct)
|
public Task<FleetPushResult> PushMeasurementsAsync(IReadOnlyList<FleetMeasurementDto> rows, DateTime cursor, CancellationToken ct)
|
||||||
=> PushAsync("measurements", rows, cursor, ct);
|
=> PushAsync("measurements", rows, cursor, ct);
|
||||||
|
|
||||||
|
public Task<FleetPushResult> PushMunicipalitiesAsync(IReadOnlyList<FleetMunicipalityDto> rows, DateTime cursor, CancellationToken ct)
|
||||||
|
=> PushAsync("municipalities", rows, cursor, ct);
|
||||||
|
|
||||||
|
public Task<FleetPushResult> PushTariffsAsync(IReadOnlyList<FleetTariffWireDto> rows, DateTime cursor, CancellationToken ct)
|
||||||
|
=> PushAsync("tariffs", rows, cursor, ct);
|
||||||
|
|
||||||
private async Task<FleetPushResult> PushAsync<T>(
|
private async Task<FleetPushResult> PushAsync<T>(
|
||||||
string batchType,
|
string batchType,
|
||||||
IReadOnlyList<T> rows,
|
IReadOnlyList<T> rows,
|
||||||
|
|||||||
@ -68,9 +68,57 @@ public sealed class FleetPushService(
|
|||||||
var devicesOk = await PushDevicesAsync(db, client, ct);
|
var devicesOk = await PushDevicesAsync(db, client, ct);
|
||||||
if (!devicesOk) return;
|
if (!devicesOk) return;
|
||||||
|
|
||||||
|
var munisOk = await PushMunicipalitiesAsync(db, client, ct);
|
||||||
|
if (!munisOk) return;
|
||||||
|
|
||||||
|
var tariffsOk = await PushTariffsAsync(db, client, ct);
|
||||||
|
if (!tariffsOk) return;
|
||||||
|
|
||||||
await PushMeasurementsAsync(db, client, ct);
|
await PushMeasurementsAsync(db, client, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Municipalities: full set every tick ────────────────────────────────
|
||||||
|
private async Task<bool> PushMunicipalitiesAsync(AppDbContext db, FleetPushClient client, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var state = await GetStateAsync(db, FleetPushState.ResourceMunicipalities, ct);
|
||||||
|
var rows = await db.Municipalities.AsNoTracking()
|
||||||
|
.Select(m => new FleetMunicipalityDto(m.Id, m.Name, m.TimeZoneId, m.IsActive))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
if (rows.Count == 0)
|
||||||
|
{
|
||||||
|
await MarkSuccessAsync(db, state, cursor: DateTime.UtcNow, ct);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var result = await client.PushMunicipalitiesAsync(rows, DateTime.UtcNow, ct);
|
||||||
|
return await HandleResultAsync(db, state, result, cursor: DateTime.UtcNow, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tariffs (with periods nested): full set every tick ─────────────────
|
||||||
|
private async Task<bool> PushTariffsAsync(AppDbContext db, FleetPushClient client, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var state = await GetStateAsync(db, FleetPushState.ResourceTariffs, ct);
|
||||||
|
var tariffs = await db.Tariffs.AsNoTracking()
|
||||||
|
.Include(t => t.Periods)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
if (tariffs.Count == 0)
|
||||||
|
{
|
||||||
|
await MarkSuccessAsync(db, state, cursor: DateTime.UtcNow, ct);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var rows = tariffs.Select(t => new FleetTariffWireDto(
|
||||||
|
t.Id, t.MunicipalityId, t.Name,
|
||||||
|
t.EffectiveFrom, t.EffectiveTo,
|
||||||
|
t.DefaultRatePerKwh, t.FixedMonthlyCharge, t.VatPercentage, t.IsActive,
|
||||||
|
t.Periods.Select(p => new FleetTariffPeriodWireDto(
|
||||||
|
p.Id, p.Name, (int)p.DaysOfWeek,
|
||||||
|
p.StartTime.ToString("HH:mm"),
|
||||||
|
p.EndTime.ToString("HH:mm"),
|
||||||
|
p.RatePerKwh)).ToList()
|
||||||
|
)).ToList();
|
||||||
|
var result = await client.PushTariffsAsync(rows, DateTime.UtcNow, ct);
|
||||||
|
return await HandleResultAsync(db, state, result, cursor: DateTime.UtcNow, ct);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Sites: full set every tick ─────────────────────────────────────────
|
// ── Sites: full set every tick ─────────────────────────────────────────
|
||||||
private async Task<bool> PushSitesAsync(AppDbContext db, FleetPushClient client, CancellationToken ct)
|
private async Task<bool> PushSitesAsync(AppDbContext db, FleetPushClient client, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -93,10 +93,34 @@ public sealed class FleetQueryService(AdminDbContext db)
|
|||||||
e.BatchBytes, e.ClientHwm, e.TimeSpread, e.Error))
|
e.BatchBytes, e.ClientHwm, e.TimeSpread, e.Error))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var munis = await db.FleetMunicipalities.AsNoTracking()
|
||||||
|
.Where(m => m.CustomerId == id)
|
||||||
|
.OrderBy(m => m.Name)
|
||||||
|
.Select(m => new FleetMunicipalityViewDto(m.Id, m.Name, m.TimeZoneId, m.IsActive))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
var muniNameMap = munis.ToDictionary(m => m.Id, m => m.Name);
|
||||||
|
|
||||||
|
var tariffs = await db.FleetTariffs.AsNoTracking()
|
||||||
|
.Where(t => t.CustomerId == id)
|
||||||
|
.Include(t => t.Periods)
|
||||||
|
.OrderByDescending(t => t.EffectiveFrom)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var tariffDtos = tariffs.Select(t => new FleetTariffViewDto(
|
||||||
|
t.Id, t.MunicipalityId, muniNameMap.GetValueOrDefault(t.MunicipalityId),
|
||||||
|
t.Name, t.EffectiveFrom, t.EffectiveTo,
|
||||||
|
t.DefaultRatePerKwh, t.FixedMonthlyCharge, t.VatPercentage, t.IsActive,
|
||||||
|
t.Periods.OrderBy(p => p.StartTime).Select(p => new FleetTariffPeriodViewDto(
|
||||||
|
p.Id, p.Name, (int)p.DaysOfWeek,
|
||||||
|
p.StartTime.ToString("HH:mm"),
|
||||||
|
p.EndTime.ToString("HH:mm"),
|
||||||
|
p.RatePerKwh)).ToList()
|
||||||
|
)).ToList();
|
||||||
|
|
||||||
return new FleetCustomerDetailDto(
|
return new FleetCustomerDetailDto(
|
||||||
c.Id, c.Code, c.Name, c.IsActive,
|
c.Id, c.Code, c.Name, c.IsActive,
|
||||||
c.FirstSeenAt, c.LastSeenAt, c.CreatedAt,
|
c.FirstSeenAt, c.LastSeenAt, c.CreatedAt,
|
||||||
sites, devices, recentDtos, events);
|
sites, devices, munis, tariffDtos, recentDtos, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-customer today: measurement count + kWh imported delta via raw SQL hitting
|
// Per-customer today: measurement count + kWh imported delta via raw SQL hitting
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user