using MeterVault.Core.Analysis;
using MeterVault.Core.Analysis.Costing;
using MeterVault.Core.Analysis.Totals;
using MeterVault.Core.Domain;
namespace MeterVault.Core.Tests.Analysis.Costing;
///
/// Builders for the cost-calculator tests: real buckets from the period resolver and bucket planner (Berlin), tariffs,
/// and quantities per part. Nothing here reads the wall clock.
///
internal static class CostingTestData
{
public const int Electricity = 1;
public const int Water = 2;
public const int Oil = 3;
public const int Grid = 10;
public const int Heater = 11;
public const int Export = 12;
public const int WaterMeter = 20;
public const int OilTank = 30;
/// A "now" long after every range the tests price, so custom ranges resolve as complete.
public static readonly DateTimeOffset FarFuture = AnalysisClock.Utc(2040, 1, 1);
public static DateOnly D(int year, int month, int day) => new(year, month, day);
/// The buckets of the complete range – (inclusive), as the planner cuts them.
public static IReadOnlyList Buckets(BucketSize size, DateOnly first, DateOnly last)
{
var period = PeriodResolver.Resolve(PeriodPreset.Custom, first, last, FarFuture, AnalysisClock.Berlin);
return Plan(period, size);
}
/// The buckets of a to-date preset at (Berlin).
public static IReadOnlyList ToDate(PeriodPreset preset, DateTimeOffset now, BucketSize size) =>
Plan(PeriodResolver.Resolve(preset, null, null, now, AnalysisClock.Berlin), size);
/// A bucket spelled out, for the cases a planner never produces (a bucket reaching past today).
public static AnalysisBucket Bucket(DateOnly first, DateOnly end, BucketSize size = BucketSize.Month) =>
new(first, end, AnalysisClock.At(AnalysisClock.Berlin, first.Year, first.Month, first.Day),
AnalysisClock.At(AnalysisClock.Berlin, end.Year, end.Month, end.Day), size);
public static Tariff Tariff(
int id,
TariffScope scope,
int? scopeId,
TariffComponent component,
double value,
string unit,
DateOnly from,
DateOnly? to = null) => new()
{
Id = id,
ScopeType = scope,
ScopeId = scopeId,
Component = component,
Value = value,
Unit = unit,
ValidFrom = from,
ValidTo = to,
};
/// A unit price of an energy type.
public static Tariff TypePrice(int id, int energyTypeId, double value, string unit, DateOnly from, DateOnly? to = null) =>
Tariff(id, TariffScope.EnergyType, energyTypeId, TariffComponent.UnitPrice, value, unit, from, to);
/// A meter's own unit price.
public static Tariff MeterPrice(int id, int meterId, double value, string unit, DateOnly from, DateOnly? to = null) =>
Tariff(id, TariffScope.Meter, meterId, TariffComponent.UnitPrice, value, unit, from, to);
public static Tariff BasePrice(int id, TariffScope scope, int? scopeId, double value, string unit, DateOnly from, DateOnly? to = null) =>
Tariff(id, scope, scopeId, TariffComponent.BasePrice, value, unit, from, to);
public static Tariff FeedInPrice(int id, int energyTypeId, double value, string unit, DateOnly from) =>
Tariff(id, TariffScope.EnergyType, energyTypeId, TariffComponent.FeedIn, value, unit, from);
public static TariffBook Book(params Tariff[] tariffs) => TariffBook.Create(tariffs, "EUR");
/// Every part's quantity from a per-day amount: the sum over its days.
public static List Daily(IReadOnlyList buckets, Func perDay) =>
[.. CostCalculator.Parts(buckets).Select(p => CostQuantity.Known(p, DaysOf(p).Sum(perDay)))];
/// Every part's quantity from an amount per whole local month, prorated by days when a part is shorter.
public static List Monthly(IReadOnlyList buckets, Func perMonth) =>
[.. CostCalculator.Parts(buckets).Select(p => perMonth(p.Month) is { } amount
? CostQuantity.Known(p, amount * p.Days / DateTime.DaysInMonth(p.Month.Year, p.Month.Month))
: CostQuantity.Unknown(p))];
/// Every part the same amount.
public static List Each(IReadOnlyList buckets, double amount) =>
[.. CostCalculator.Parts(buckets).Select(p => CostQuantity.Known(p, amount))];
public static CostLine Line(
int meterId,
int energyTypeId,
IReadOnlyList quantities,
string unit = "kWh",
BillLineKind kind = BillLineKind.UnitPrice,
ServicePeriod? service = null) =>
new(meterId, energyTypeId, kind, unit, quantities, service);
public static CostResult Price(
IReadOnlyList buckets,
TariffBook book,
IEnumerable lines,
DateOnly? today = null,
IEnumerable? standing = null,
IEnumerable? manual = null) =>
CostCalculator.Calculate(new CostRequest(
buckets,
today ?? D(2039, 12, 31),
book,
[.. lines],
standing is null ? null : [.. standing],
manual is null ? null : [.. manual]));
public static ManualCost Manual(int id, DateOnly start, double amount, int? categoryId = null, int? meterId = null, DateOnly? end = null, string currency = "EUR") => new()
{
Id = id,
CategoryId = categoryId,
MeterId = meterId,
PeriodStart = start,
PeriodEnd = end ?? start,
Amount = amount,
Currency = currency,
};
/// The cost of the bucket starting on .
public static CostAmount At(this IReadOnlyList cells, IReadOnlyList buckets, DateOnly first)
{
for (var i = 0; i < buckets.Count; i++)
{
if (buckets[i].FirstDay == first)
{
return cells[i];
}
}
throw new InvalidOperationException($"No bucket starts on {first:yyyy-MM-dd}.");
}
private static IEnumerable DaysOf(CostPart part) =>
Enumerable.Range(0, part.Days).Select(part.FirstDay.AddDays);
private static IReadOnlyList Plan(ResolvedPeriod period, BucketSize size)
{
var plan = BucketPlanner.Plan(period, size, maxPoints: 10_000);
Assert.False(plan.Refused);
return plan.Buckets;
}
}