using Elwig.Helpers; using Elwig.Models.Entities; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; 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 static readonly ConcurrentDictionary _memberCerts = []; public async static Task GetTracesCertificates(Member m, bool? tryNameAndAddress = null) { if (_memberCerts.TryGetValue(m.MgNr, out var entry) && entry.Timestamp >= DateTime.Now.AddDays(-1)) return entry.Certificates; var certs = await FetchTracesCertificates(m, tryNameAndAddress); _memberCerts[m.MgNr] = (DateTime.Now, certs); return certs; } 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()); return resJson?.AsArray() ?? throw new JsonException(); } 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 = null, string? lfbisNr = null, (string Name, string Address, string PostalCode)? address = null) { bool searchedByName = false; JsonArray jsonCerts = []; if (jsonCerts.Count == 0 && oocId != null) { jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorIdentifierType", "ooc_identifier" }, { "operatorIdentifierSearchOperator", "STRICT" }, { "operatorIdentifier", oocId }, }); } if (jsonCerts.Count == 0 && lfbisNr != null) { jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorIdentifierType", "comp_reg" }, { "operatorIdentifierSearchOperator", "STRICT" }, { "operatorIdentifier", lfbisNr.TrimStart('0') }, }); } if (jsonCerts.Count == 0 && address != null) { var query = $"{Regex.Replace(address.Value.Name, @"\(.*?\)", "")} {address.Value.Address}"; jsonCerts = await TryFetchTracesCertificates(new Dictionary { { "operatorPostalCode", address.Value.PostalCode }, { "query", query.Length > 40 ? query[..query.LastIndexOf(' ', 40)] : query }, }); searchedByName = true; } if (jsonCerts.Count == 0) return []; TracesCertificate[] 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()), })]; return (!searchedByName || certs.Where(c => c.Status == TracesStatus.ISSUED).Select(c => c.OperatorId).ToHashSet().Count == 1) ? certs : []; } public static string? GetTracesSearchUrl(Member m, bool? tryNameAndAddress = null) { return GetTracesSearchUrl(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 static string? GetTracesSearchUrl(string? oocId = null, string? lfbisNr = null, (string Name, string Address, string PostalCode)? address = null) { var baseUrl = "https://webgate.ec.europa.eu/tracesnt/directory/publication/organic-operator/index"; if (oocId != null) { return $"{baseUrl}#!?countryCode=AT&operatorIdentifierType=ooc_identifier&operatorIdentifierSearchOperator=STRICT&operatorIdentifier={oocId}&states=ISSUED"; } else if (lfbisNr != null) { return $"{baseUrl}#!?countryCode=AT&operatorIdentifierType=comp_reg&operatorIdentifierSearchOperator=STRICT&operatorIdentifier={lfbisNr.TrimStart('0')}&states=ISSUED"; } else if (address != null) { return $"{baseUrl}#!?countryCode=AT&operatorPostalCode={address?.PostalCode}&query={address?.Name.Replace(' ', '+')}+{address?.Address.Replace(' ', '+')}&states=ISSUED"; } else { return null; } } public static string? GetEasyCertSearchUrl(string lfbisNr) { var baseUrl = "https://www.easy-cert.com/htm/suchergebnis.htm"; return $"{baseUrl}?CountryCode=AT&CustomerNumber={lfbisNr.TrimStart('0')}"; } public static string? GetBiocSearchUrl(string lfbisNr) { var baseUrl = "https://www.bioc.info/search/producersearchresult"; return $"{baseUrl}?producerSearch[country]=a1e51f85-27b5-a1e3-65f7-561569884d38&producerSearch[operatorId]={lfbisNr.TrimStart('0')}"; } } }