using Elwig.Helpers; using Elwig.Models.Entities; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Nodes; using System.Threading.Tasks; using System.Web; namespace Elwig.Services { public static class OrganicService { public const string TRACES_API_URL = "https://webgate.ec.europa.eu/tracesnt/directory/publication/organic-operator/for/query"; public const string TRACES_PDF_URL_PREFIX = "https://webgate.ec.europa.eu/tracesnt/directory/publication/organic-operator/digitally-signed/"; public enum TracesStatus { ISSUED, SUSPENDED, WITHDRAWN, EXPIRED } public enum TracesActivity { PREPARATION, EXPORT, IMPORT, STORING, PRODUCTION, DISTRIBUTION, DISTRIBUTION_PLACING_ON_THE_MARKET } public enum TracesCategoryOfProduct { UNPROCESSED_PLANT_PRODUCTS_INCLUDING_SEEDS, LIVESTOCK_AND_UNPROCESSED_LIVESTOCK_PRODUCTS, ALGAE_AND_UNPROCESSED_AQUACULTURE_PRODUCTS, PROCESSED_AGRICULTURAL_PRODUCTS_INCLUDING_AQUACULTURE_FOR_USE_AS_FOOD, FEED, WINE, OTHER_PRODUCTS_NOT_COVERED_BY_PREVIOUS_CATEGORIES } public record TracesCertificate { public required string Id { get; init; } public required string AuthorityCode { get; init; } public required string OperatorId { get; init; } public required string OperatorName { get; init; } public required TracesActivity[] Activities { get; init; } public required TracesCategoryOfProduct[] CategoriesOfProduct { get; init; } public required DateOnly IssuedOn { get; init; } public required DateOnly ExpiresOn { get; init; } public required DateOnly LastStatusUpdateOn { get; init; } public required TracesStatus Status { get; init; } public string PdfUrl => TRACES_PDF_URL_PREFIX + Id + ".pdf"; public bool IsValid => Status == TracesStatus.ISSUED && IssuedOn.ToDateTime(new()) <= DateTime.Today && ExpiresOn.ToDateTime(new()) >= DateTime.Today; public bool IsValidForWineProduction => IsValid && Activities.Contains(TracesActivity.PRODUCTION) && (CategoriesOfProduct.Contains(TracesCategoryOfProduct.UNPROCESSED_PLANT_PRODUCTS_INCLUDING_SEEDS) || CategoriesOfProduct.Contains(TracesCategoryOfProduct.WINE)); } public async static Task GetTracesCertificates(Member m, bool? tryNameAndAddress = null) { // TODO cache return await FetchTracesCertificates(m, tryNameAndAddress); } private async static Task TryFetchTracesCertificates(Dictionary query) { var q = HttpUtility.ParseQueryString(""); q.Add("sort", "-issuedOn"); q.Add("countryCode", "AT"); foreach (var (k, v) in query) { q.Add(k, v); } using var client = Utils.GetHttpClient(); using var res = await client.GetAsync($"{TRACES_API_URL}?{q}"); res.EnsureSuccessStatusCode(); var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync()); var certs = resJson?.AsArray() ?? throw new Exception(); if (certs.Count == 0) throw new Exception(); return certs; } public static Task FetchTracesCertificates(Member m, bool? tryNameAndAddress = null) { return FetchTracesCertificates(m.OrganicOperatorId, m.LfbisNr, (tryNameAndAddress ?? m.IsOrganic) ? m.BillingAddress is BillingAddr a ? (a.FullName, a.Address, $"{a.PostalDest.AtPlz?.Plz}") : (m.FullName, m.Address, $"{m.PostalDest.AtPlz?.Plz}") : null); } public async static Task FetchTracesCertificates(string? oocId, string? lfbisNr, (string Name, string Address, string PostalCode)? address) { bool searchedByName = false; JsonArray? jsonCerts = null; if (oocId != null) { try { jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorIdentifierType", "ooc_identifier" }, { "operatorIdentifierSearchOperator", "STRICT" }, { "operatorIdentifier", oocId }, }); } catch { jsonCerts = null; } } if (jsonCerts == null && lfbisNr != null) { try { jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorIdentifierType", "comp_reg" }, { "operatorIdentifierSearchOperator", "STRICT" }, { "operatorIdentifier", lfbisNr.TrimStart('0') }, }); } catch { jsonCerts = null; } } if (jsonCerts == null && address != null) { try { jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorPostalCode", address.Value.PostalCode }, { "query", $"{address.Value.Name} {address.Value.Address}" }, }); searchedByName = true; } catch { jsonCerts = null; } } if (jsonCerts == null) return []; TracesCertificate[] certs = []; try { certs = [.. jsonCerts.Select(j => new TracesCertificate { Id = j!["reference"]?.GetValue() ?? throw new Exception(), AuthorityCode = j["issuingBody"]?["code"]?.GetValue() ?? throw new Exception(), OperatorId = j["operatorIdentifier"]?.GetValue() ?? throw new Exception(), OperatorName = j["operator"]?["name"]?.GetValue() ?? throw new Exception(), Activities = [.. j["activities"]?.AsArray().Select(a => Enum.Parse(a?["id"]?.GetValue().ToUpper().Replace("_IMPORT", "IMPORT") ?? throw new Exception())) ?? []], CategoriesOfProduct = [.. j["categoriesOfProduct"]?.AsArray().Select(c => Enum.Parse(c?["id"]?.GetValue() ?? throw new Exception())) ?? []], IssuedOn = DateOnly.FromDateTime(DateTime.ParseExact(j["issuedOn"]?.GetValue() ?? throw new Exception(), "yyyy-MM-ddTHH:mm:ss.fffK", CultureInfo.InvariantCulture, DateTimeStyles.None)), ExpiresOn = DateOnly.ParseExact(j["expiresOn"]?.GetValue() ?? throw new Exception(), "yyyy-MM-dd"), LastStatusUpdateOn = DateOnly.FromDateTime(DateTime.ParseExact(j["lastStatusUpdateDateTime"]?.GetValue() ?? throw new Exception(), "yyyy-MM-ddTHH:mm:ss.fffK", CultureInfo.InvariantCulture, DateTimeStyles.None)), Status = Enum.Parse(j["status"]?["id"]?.GetValue() ?? throw new Exception()), })]; } catch { return []; } return (!searchedByName || certs.Where(c => c.Status == TracesStatus.ISSUED).Select(c => c.OperatorId).ToHashSet().Count == 1) ? certs : []; } } }