280 lines
12 KiB
C#
280 lines
12 KiB
C#
using Newtonsoft.Json;
|
|
using NJsonSchema;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text.Json.Nodes;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Elwig.Helpers.Billing {
|
|
public class BillingData {
|
|
|
|
public enum CalculationMode { Elwig, WgMaster }
|
|
public enum CurveMode { Oe, Kmw }
|
|
public record struct Curve(CurveMode Mode, Dictionary<double, decimal> Normal, Dictionary<double, decimal>? Gebunden);
|
|
|
|
public static JsonSchema? Schema { get; private set; }
|
|
|
|
public static async Task Init() {
|
|
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Elwig.Resources.Schemas.PaymentVariantData.json");
|
|
Schema = await JsonSchema.FromJsonAsync(stream ?? throw new ArgumentException("JSON schema not found"));
|
|
}
|
|
|
|
public readonly JsonObject Data;
|
|
public readonly CalculationMode Mode;
|
|
|
|
public bool ConsiderDelieryModifiers {
|
|
get => GetConsider("consider_delivery_modifiers");
|
|
set => SetConsider(value, "consider_delivery_modifiers");
|
|
}
|
|
public bool ConsiderContractPenalties {
|
|
get => GetConsider("consider_contract_penalties");
|
|
set => SetConsider(value, "consider_contract_penalties");
|
|
}
|
|
public bool ConsiderTotalPenalty {
|
|
get => GetConsider("consider_total_penalty");
|
|
set => SetConsider(value, "consider_total_penalty");
|
|
}
|
|
public bool ConsiderAutoBusinessShares {
|
|
get => GetConsider("consider_auto_business_shares");
|
|
set => SetConsider(value, "consider_auto_business_shares");
|
|
}
|
|
|
|
private bool GetConsider(string name, string? wgMasterName = null) {
|
|
return ((Mode == CalculationMode.Elwig) ? Data[name] : Data[wgMasterName ?? ""])?.AsValue().GetValue<bool>() ?? false;
|
|
}
|
|
|
|
private void SetConsider(bool value, string name, string? wgMasterName = null) {
|
|
if (Mode == CalculationMode.WgMaster && wgMasterName == null) {
|
|
return;
|
|
} else if (value) {
|
|
Data[(Mode == CalculationMode.Elwig) ? name : wgMasterName ?? ""] = value;
|
|
} else {
|
|
Data.Remove((Mode == CalculationMode.Elwig) ? name : wgMasterName ?? "");
|
|
}
|
|
}
|
|
|
|
public BillingData(JsonObject data) {
|
|
Data = data;
|
|
var mode = Data["mode"]?.GetValue<string>();
|
|
Mode = (mode == "elwig") ? CalculationMode.Elwig : CalculationMode.WgMaster;
|
|
}
|
|
|
|
protected static JsonObject ParseJson(string json) {
|
|
if (Schema == null) throw new InvalidOperationException("Schema has to be initialized first");
|
|
try {
|
|
var errors = Schema.Validate(json);
|
|
if (errors.Count != 0) throw new ArgumentException("Invalid JSON data");
|
|
return JsonNode.Parse(json)?.AsObject() ?? throw new ArgumentException("Invalid JSON data");
|
|
} catch (JsonReaderException) {
|
|
throw new ArgumentException("Invalid JSON data");
|
|
}
|
|
}
|
|
|
|
public static BillingData FromJson(string json) {
|
|
return new(ParseJson(json));
|
|
}
|
|
|
|
protected JsonArray GetCurvesEntry() {
|
|
return Data[Mode == CalculationMode.Elwig ? "curves" : "Kurven"]?.AsArray() ?? throw new InvalidOperationException();
|
|
}
|
|
|
|
protected JsonNode GetPaymentEntry() {
|
|
return Data[Mode == CalculationMode.Elwig ? "payment" : "AuszahlungSorten"] ?? throw new InvalidOperationException();
|
|
}
|
|
|
|
protected JsonObject? GetQualityEntry() {
|
|
return Data[Mode == CalculationMode.Elwig ? "quality" : "AuszahlungSortenQualitätsstufe"]?.AsObject();
|
|
}
|
|
|
|
private static Dictionary<double, decimal> GetCurveData(JsonObject data, CurveMode mode) {
|
|
var dict = new Dictionary<double, decimal>();
|
|
foreach (var (index, price) in data) {
|
|
double idx;
|
|
bool? gtlt = index.StartsWith('>') ? true : index.StartsWith('<') ? false : null;
|
|
if (index.EndsWith("kmw")) {
|
|
idx = double.Parse(index[(gtlt != null ? 1 : 0)..^3], CultureInfo.InvariantCulture);
|
|
if (mode == CurveMode.Oe) {
|
|
idx = Utils.KmwToOe(idx);
|
|
}
|
|
} else if (index.EndsWith("oe")) {
|
|
idx = double.Parse(index[(gtlt != null ? 1 : 0)..^2], CultureInfo.InvariantCulture);
|
|
if (mode == CurveMode.Kmw) {
|
|
idx = Utils.OeToKmw(idx);
|
|
}
|
|
} else {
|
|
throw new InvalidOperationException();
|
|
}
|
|
if (gtlt == true) {
|
|
idx = Math.BitIncrement(idx);
|
|
} else if (gtlt == false) {
|
|
idx = Math.BitDecrement(idx);
|
|
}
|
|
dict[idx] = price?.AsValue().GetValue<decimal>() ?? throw new InvalidOperationException();
|
|
}
|
|
return dict;
|
|
}
|
|
|
|
protected Dictionary<int, Curve> GetCurves() {
|
|
var dict = new Dictionary<int, Curve>();
|
|
var curves = GetCurvesEntry();
|
|
foreach (var c in curves) {
|
|
var obj = c?.AsObject() ?? throw new InvalidOperationException();
|
|
var id = obj["id"]?.GetValue<int>() ?? throw new InvalidOperationException();
|
|
var cMode = (obj["mode"]?.GetValue<string>() == "kmw") ? CurveMode.Kmw : CurveMode.Oe;
|
|
double quw = cMode == CurveMode.Oe ? 73 : 15;
|
|
|
|
Dictionary<double, decimal> c1;
|
|
Dictionary<double, decimal>? c2 = null;
|
|
var norm = obj["data"];
|
|
if (norm is JsonObject) {
|
|
c1 = GetCurveData(norm.AsObject(), cMode);
|
|
} else if (norm?.AsValue().TryGetValue(out decimal v) == true) {
|
|
c1 = new() { { quw, v } };
|
|
} else {
|
|
throw new InvalidOperationException();
|
|
}
|
|
var geb = obj["geb"];
|
|
if (geb is JsonObject) {
|
|
c2 = GetCurveData(geb.AsObject(), cMode);
|
|
} else if (geb?.AsValue().TryGetValue(out decimal v) == true) {
|
|
var splitVal = GetCurveValueAt(c1, quw);
|
|
c2 = c1.ToDictionary(e => e.Key, e => e.Value + (e.Key >= quw ? v : 0));
|
|
c2[quw] = splitVal + v;
|
|
c2[Math.BitDecrement(quw)] = splitVal;
|
|
}
|
|
dict.Add(id, new(cMode, c1, c2));
|
|
}
|
|
return dict;
|
|
}
|
|
|
|
protected static Dictionary<string, JsonValue> GetSelection(JsonNode value, IEnumerable<string> attributeVariants) {
|
|
if (value is JsonValue flatRate) {
|
|
return attributeVariants.ToDictionary(e => e, _ => flatRate);
|
|
} if (value is not JsonObject data) {
|
|
throw new InvalidOperationException();
|
|
}
|
|
Dictionary<string, JsonValue> dict;
|
|
if (data["default"] is JsonValue def) {
|
|
dict = attributeVariants.ToDictionary(e => e, _ => def);
|
|
} else {
|
|
dict = [];
|
|
}
|
|
|
|
var variants = data.Where(p => !p.Key.StartsWith('/') && p.Key.Length == 2);
|
|
var attributes = data.Where(p => p.Key.StartsWith('/'));
|
|
var others = data.Where(p => !p.Key.StartsWith('/') && p.Key.Length > 2 && p.Key != "default");
|
|
foreach (var (idx, v) in variants) {
|
|
var curve = v?.AsValue() ?? throw new InvalidOperationException();
|
|
foreach (var i in attributeVariants.Where(e => e.StartsWith(idx[..^1]))) {
|
|
dict[i] = curve;
|
|
}
|
|
}
|
|
foreach (var (idx, v) in attributes) {
|
|
var curve = v?.AsValue() ?? throw new InvalidOperationException();
|
|
foreach (var i in attributeVariants.Where(e => e[2..] == idx[1..])) {
|
|
dict[i] = curve;
|
|
}
|
|
}
|
|
foreach (var (idx, v) in others) {
|
|
var curve = v?.AsValue() ?? throw new InvalidOperationException();
|
|
dict[idx.Replace("/", "")] = curve;
|
|
}
|
|
|
|
return dict;
|
|
}
|
|
|
|
public static decimal GetCurveValueAt(Dictionary<double, decimal> curve, double key) {
|
|
if (curve.Count == 1) return curve.First().Value;
|
|
|
|
var lt = curve.Keys.Where(v => v <= key);
|
|
var gt = curve.Keys.Where(v => v >= key);
|
|
if (!lt.Any()) {
|
|
return curve[gt.Min()];
|
|
} else if (!gt.Any()) {
|
|
return curve[lt.Max()];
|
|
}
|
|
|
|
var max = lt.Max();
|
|
var min = gt.Min();
|
|
if (max == min) return curve[key];
|
|
|
|
var p1 = ((decimal)key - (decimal)min) / ((decimal)max - (decimal)min);
|
|
var p2 = 1 - p1;
|
|
return curve[min] * p2 + curve[max] * p1;
|
|
}
|
|
|
|
protected static JsonNode GraphToJson(Graph graph, string mode) {
|
|
var x = graph.DataX;
|
|
var y = graph.DataY;
|
|
if (y.Distinct().Count() == 1) {
|
|
return JsonValue.Create(graph.DataY[0]);
|
|
}
|
|
|
|
var data = new JsonObject();
|
|
|
|
if (y[0] != y[1]) {
|
|
data.Add(new KeyValuePair<string, JsonNode?>(x[0] + mode, Math.Round(y[0], graph.Precision)));
|
|
}
|
|
for (int i = 1; i < x.Length - 1; i++) {
|
|
if (Math.Round(y[i] - y[i - 1], 10) != Math.Round(y[i + 1] - y[i], 10)) {
|
|
data.Add(new KeyValuePair<string, JsonNode?>(x[i] + mode, Math.Round(y[i], graph.Precision)));
|
|
}
|
|
}
|
|
if (y[^1] != y[^2]) {
|
|
data.Add(new KeyValuePair<string, JsonNode?>(x[^1] + mode, Math.Round(y[^1], graph.Precision)));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
protected static JsonObject GraphEntryToJson(GraphEntry entry) {
|
|
var curve = new JsonObject {
|
|
["id"] = entry.Id,
|
|
["mode"] = entry.Mode.ToString().ToLower(),
|
|
};
|
|
|
|
curve["data"] = GraphToJson(entry.DataGraph, entry.Mode.ToString().ToLower());
|
|
|
|
if (entry.GebundenFlatBonus != null) {
|
|
curve["geb"] = entry.GebundenFlatBonus;
|
|
} else if (entry.GebundenGraph != null) {
|
|
curve["geb"] = GraphToJson(entry.GebundenGraph, entry.Mode.ToString().ToLower());
|
|
}
|
|
|
|
return curve;
|
|
}
|
|
|
|
public JsonObject FromGraphEntries(IEnumerable<GraphEntry> graphEntries) {
|
|
var payment = new JsonObject();
|
|
var curves = new JsonArray();
|
|
|
|
foreach (var entry in graphEntries) {
|
|
curves.Add(GraphEntryToJson(entry));
|
|
foreach (var contract in entry.Contracts) {
|
|
payment[$"{contract.Variety?.SortId}/{contract.Attribute?.AttrId}"] = $"curve:{entry.Id}";
|
|
}
|
|
}
|
|
|
|
var data = new JsonObject {
|
|
["mode"] = "elwig",
|
|
["version"] = 1,
|
|
};
|
|
if (ConsiderDelieryModifiers)
|
|
data["consider_delivery_modifiers"] = true;
|
|
if (ConsiderContractPenalties)
|
|
data["consider_contract_penalties"] = true;
|
|
if (ConsiderTotalPenalty)
|
|
data["consider_total_penalty"] = true;
|
|
if (ConsiderAutoBusinessShares)
|
|
data["consider_auto_business_shares"] = true;
|
|
|
|
data["payment"] = payment;
|
|
data["curves"] = curves;
|
|
|
|
return data;
|
|
}
|
|
}
|
|
}
|