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 Normal, Dictionary? 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"); } public bool ConsiderCustomModifiers { get => GetConsider("consider_custom_modifiers"); set => SetConsider(value, "consider_custom_modifiers"); } public double NetWeightModifier { get => GetWeightModifier("net_weight_modifier", "Rebelzuschlag"); set => SetWeightModifier(value, "net_weight_modifier", "Rebelzuschlag"); } public double GrossWeightModifier { get => GetWeightModifier("gross_weight_modifier"); set => SetWeightModifier(value, "gross_weight_modifier"); } private bool GetConsider(string name, string? wgMasterName = null) { return ((Mode == CalculationMode.Elwig) ? Data[name] : Data[wgMasterName ?? ""])?.AsValue().GetValue() ?? 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 ?? ""); } } private double GetWeightModifier(string name, string? wgMasterName = null) { var isElwig = (Mode == CalculationMode.Elwig); var val = (isElwig ? Data[name] : Data[wgMasterName ?? ""])?.AsValue().GetValue() ?? 0; return isElwig ? val : val / 100.0; } private void SetWeightModifier(double value, string name, string? wgMasterName = null) { var isElwig = (Mode == CalculationMode.Elwig); if (Mode == CalculationMode.WgMaster && wgMasterName == null) { return; } else if (value != 0) { Data[isElwig ? name : wgMasterName ?? ""] = isElwig ? value : value * 100.0; } else { Data.Remove(isElwig ? name : wgMasterName ?? ""); } } public BillingData(JsonObject data) { Data = data; var mode = Data["mode"]?.GetValue(); 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 GetCurveData(JsonObject data, CurveMode mode) { var dict = new Dictionary(); 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() ?? throw new InvalidOperationException(); } return dict; } protected Dictionary GetCurves() { var dict = new Dictionary(); var curves = GetCurvesEntry(); foreach (var c in curves) { var obj = c?.AsObject() ?? throw new InvalidOperationException(); var id = obj["id"]?.GetValue() ?? throw new InvalidOperationException(); var cMode = (obj["mode"]?.GetValue() == "kmw") ? CurveMode.Kmw : CurveMode.Oe; double quw = cMode == CurveMode.Oe ? 73 : 15; Dictionary c1; Dictionary? 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 GetSelection(JsonNode value, IEnumerable vaributes) { if (value is JsonValue flatRate) { return vaributes.ToDictionary(e => e, _ => flatRate); } if (value is not JsonObject data) { throw new InvalidOperationException(); } Dictionary dict; if (data["default"] is JsonValue def) { dict = vaributes.ToDictionary(e => e, _ => def); } else { dict = []; } var conv = data .Where(p => p.Key != "default") .Select(p => (new RawVaribute(p.Key), p.Value)) .OrderBy(p => (p.Item1.SortId != null ? 10 : 0) + (p.Item1.AttrId != null ? 12 : 0) + (p.Item1.CultId != null ? 11 : 0)) .ToList(); foreach (var (idx, v) in conv) { var curve = v?.AsValue() ?? throw new InvalidOperationException(); foreach (var i in vaributes.Where(e => (idx.SortId == null || idx.SortId == e.SortId) && (idx.AttrId == null || idx.AttrId == e.AttrId) && (idx.CultId == null || idx.CultId == e.CultId))) { dict[i] = curve; } } return dict; } public static decimal GetCurveValueAt(Dictionary 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 JsonObject GraphToJson(Graph graph, string mode) { var x = graph.DataX; var y = graph.DataY; var prec = graph.Precision; try { return new JsonObject() { ["15kmw"] = Math.Round(y.Distinct().Single(), prec) }; } catch { } var data = new JsonObject(); if (y[0] != y[1]) { data[$"{x[0]}{mode}"] = Math.Round(y[0], prec); } for (int i = 1; i < x.Length - 1; i++) { var d1 = Math.Round(y[i] - y[i - 1], prec); var d2 = Math.Round(y[i + 1] - y[i], prec); if (d1 != d2) { data[$"{x[i]}{mode}"] = Math.Round(y[i], prec); } } if (y[^1] != y[^2]) { data[$"{x[^1]}{mode}"] = Math.Round(y[^1], prec); } return data; } protected static JsonNode GraphEntryToJson(GraphEntry entry) { try { if (entry.GebundenFlatBonus == null) { return JsonValue.Create((decimal)entry.DataGraph.DataY.Distinct().Single()); } } catch { } 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"] = (decimal)entry.GebundenFlatBonus; } else if (entry.GebundenGraph != null) { curve["geb"] = GraphToJson(entry.GebundenGraph, entry.Mode.ToString().ToLower()); } return curve; } protected static (Dictionary>, Dictionary>) GetReverseKeys(JsonObject data, bool strict = true) { Dictionary> rev1 = []; Dictionary> rev2 = []; foreach (var (k, v) in data) { if (k == "default" || (strict && (k.StartsWith('/') || !k.Contains('/'))) || v is not JsonValue val) { continue; } else if (val.TryGetValue(out var dec)) { rev2[dec] = rev2.GetValueOrDefault(dec) ?? []; rev2[dec].Add(k); } else if (val.TryGetValue(out var cur)) { rev1[cur] = rev1.GetValueOrDefault(cur) ?? []; rev1[cur].Add(k); } } return (rev1, rev2); } protected static void CollapsePaymentData(JsonObject data, IEnumerable vaributes, bool useDefault = true) { var (rev1, rev2) = GetReverseKeys(data); if (!data.ContainsKey("default")) { foreach (var (v, ks) in rev1) { if ((ks.Count >= vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) { foreach (var k in ks) data.Remove(k); data["default"] = v; CollapsePaymentData(data, vaributes, useDefault); return; } } foreach (var (v, ks) in rev2) { if ((ks.Count >= vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) { foreach (var k in ks) data.Remove(k); data["default"] = v; CollapsePaymentData(data, vaributes, useDefault); return; } } } var attributes = data .Select(e => e.Key) .Where(k => k.Length > 3 && k.Contains('/')) .Select(k => k.Split('/')[1]) .Distinct() .ToList(); foreach (var idx in attributes) { var len = vaributes.Count(e => $"{e.AttrId}{(e.CultId != null && e.CultId != "" ? "-" : "")}{e.CultId}" == idx); foreach (var (v, ks) in rev1) { var myKs = ks.Where(k => k.EndsWith($"/{idx}")).ToList(); if (myKs.Count > 1 && ((myKs.Count >= len * 0.5 && useDefault) || myKs.Count == len)) { foreach (var k in myKs) data.Remove(k); data[(idx.StartsWith('-') && !useDefault ? "" : "/") + idx] = v; } } foreach (var (v, ks) in rev2) { var myKs = ks.Where(k => k.EndsWith($"/{idx}")).ToList(); if (myKs.Count > 1 && ((myKs.Count >= len * 0.5 && useDefault) || myKs.Count == len)) { foreach (var k in myKs) data.Remove(k); data[(idx.StartsWith('-') && !useDefault ? "" : "/") + idx] = v; } } } if (!useDefault) return; var keys = data.Select(p => p.Key).ToList(); foreach (var k in keys) { if (k.Length == 3 && k.EndsWith('/') && !keys.Contains(k[..2])) { data.Remove(k, out var val); data.Add(k[..2], val); } else if (k.Contains("/-")) { data.Remove(k, out var val); data.Add(k.Replace("/-", "-"), val); } } (rev1, rev2) = GetReverseKeys(data, false); var keyVaributes = data .Select(e => e.Key.Split('-')[0]) .Where(e => e.Length > 0 && e != "default") .Distinct() .ToList(); foreach (var idx in keyVaributes) { var len = data.Count(e => e.Key == idx || (e.Key.Length > idx.Length && e.Key.StartsWith(idx) && e.Key[idx.Length] == '-')); foreach (var (v, ks) in rev1) { var myKs = ks.Where(k => k == idx || (k.Length > idx.Length && k.StartsWith(idx) && k[idx.Length] == '-' && !data.ContainsKey(k[idx.Length..]))).ToList(); if (myKs.Count == len) { foreach (var k in myKs) { if (k != idx) data.Remove(k); } } } foreach (var (v, ks) in rev2) { var myKs = ks.Where(k => k == idx || (k.Length > idx.Length && k.StartsWith(idx) && k[idx.Length] == '-' && !data.ContainsKey(k[idx.Length..]))).ToList(); if (myKs.Count == len) { foreach (var k in myKs) { if (k != idx) data.Remove(k); } } } } } public static JsonObject FromGraphEntries( IEnumerable graphEntries, BillingData? origData = null, IEnumerable? vaributes = null, bool useDefaultPayment = true, bool useDefaultQuality = true ) { var payment = new JsonObject(); var qualityWei = new JsonObject(); var curves = new JsonArray(); int curveId = 0; foreach (var entry in graphEntries) { var curve = GraphEntryToJson(entry); JsonValue node; if (curve is JsonObject obj) { obj["id"] = ++curveId; node = JsonValue.Create($"curve:{curveId}"); curves.Add(obj); } else if (curve is JsonValue val && val.TryGetValue(out var flat)) { node = JsonValue.Create(flat); } else { continue; } foreach (var c in entry.Vaributes) { var v = new RawVaribute(c.Variety!.SortId, c.Attribute?.AttrId ?? "", c.Cultivation?.CultId); if (v.CultId == "") v.CultId = null; if (entry.Abgewertet) {; qualityWei[v.ToString()] = node.DeepClone(); } else { payment[v.ToString()] = node.DeepClone(); } } } CollapsePaymentData(payment, vaributes ?? payment.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultPayment); CollapsePaymentData(qualityWei, vaributes ?? qualityWei.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultQuality); var data = new JsonObject { ["mode"] = "elwig", ["version"] = 1, }; if (origData?.ConsiderDelieryModifiers == true) data["consider_delivery_modifiers"] = true; if (origData?.ConsiderContractPenalties == true) data["consider_contract_penalties"] = true; if (origData?.ConsiderTotalPenalty == true) data["consider_total_penalty"] = true; if (origData?.ConsiderAutoBusinessShares == true) data["consider_auto_business_shares"] = true; if (payment.Count == 0) { data["payment"] = 0; } else if (payment.Count == 1 && payment.First().Key == "default") { data["payment"] = payment.Single().Value?.DeepClone(); } else { data["payment"] = payment; } if (qualityWei.Count == 1 && qualityWei.First().Key == "default") { data["quality"] = new JsonObject() { ["WEI"] = qualityWei.Single().Value?.DeepClone() }; } else if (qualityWei.Count >= 1) { data["quality"] = new JsonObject() { ["WEI"] = qualityWei }; } data["curves"] = curves; return data; } } }