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)); } 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 async static Task FetchTracesCertificatesOfMember(Member m, bool? tryNameAndAddress = null) { bool searchedByName = false; JsonArray? jsonCerts = null; if (m.OrganicOperatorId != null) { try { jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorIdentifierType", "ooc_identifier" }, { "operatorIdentifierSearchOperator", "STRICT" }, { "operatorIdentifier", m.OrganicOperatorId }, }); } catch { jsonCerts = null; } } if (jsonCerts == null && m.LfbisNr != null) { try { jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorIdentifierType", "comp_reg" }, { "operatorIdentifierSearchOperator", "STRICT" }, { "operatorIdentifier", m.LfbisNr.TrimStart('0') }, }); } catch { jsonCerts = null; } } if (jsonCerts == null && (tryNameAndAddress ?? m.IsOrganic)) { try { jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorPostalCode", $"{(m.BillingAddress?.PostalDest ?? m.PostalDest).AtPlz?.Plz}" }, { "query", m.BillingAddress is BillingAddr a ? $"{a.FullName} {a.Address}" : $"{m.FullName} {m.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 : []; } } }