From dfa7e4496d384f3eb49a7df7e30a3b7f9697d797 Mon Sep 17 00:00:00 2001 From: Samson Maborukoje Date: Sat, 14 Feb 2026 13:12:58 +0000 Subject: [PATCH 1/8] version 3 upgrade --- .gitignore | 11 + Autumn.API/Autumn.API.csproj | 33 +- Autumn.API/Contract/V1/ApiRoutes.cs | 43 - .../V1/Requests/ClassifyCommodityRequest.cs | 12 - .../Contract/V1/Requests/DutyRequest.cs | 37 - .../Contract/V1/Requests/SearchRequest.cs | 15 - .../Contract/V1/Requests/UserRequest.cs | 13 - .../V1/Responses/AuthFailedResponse.cs | 12 - .../V1/Responses/AuthSuccessResponse.cs | 12 - .../V1/Responses/ClassifyCommodityResponse.cs | 32 - .../Contract/V1/Responses/CurrencyObject.cs | 14 - .../Contract/V1/Responses/CurrencyResponse.cs | 16 - .../V1/Responses/CustomsTariffObject.cs | 23 - .../Contract/V1/Responses/DocumentObject.cs | 29 - .../Contract/V1/Responses/DutyResponse.cs | 43 - .../Contract/V1/Responses/HSCodeObject.cs | 27 - .../V1/Responses/HscodeToDocumentObject.cs | 23 - .../V1/Responses/KeywordAPIResponsecs.cs | 23 - .../Contract/V1/Responses/KeywordResponse.cs | 16 - .../Contract/V1/Responses/NoteResponse.cs | 22 - .../Contract/V1/Responses/SearchResponse.cs | 18 - .../Contract/V1/Responses/TagsResult.cs | 31 - Autumn.API/Contract/V2/ApiRoutes.cs | 19 - .../Contract/V2/Requests/SearchRequest.cs | 16 - .../Contract/V2/Responses/HSCodeObject.cs | 27 - .../Contract/V2/Responses/ResultModel.cs | 18 - .../Contract/V2/Responses/SearchResponse.cs | 19 - .../Controllers/V1/CodeListController.cs | 123 - Autumn.API/Controllers/V1/DutyController.cs | 106 - .../Controllers/V1/IdentityController.cs | 55 - .../Controllers/V1/KeywordsController.cs | 77 - Autumn.API/Controllers/V1/NoteController.cs | 123 - Autumn.API/Controllers/V1/SearchController.cs | 73 - Autumn.API/Controllers/V2/SearchController.cs | 169 - Autumn.API/Dto/ApiDtos.cs | 200 + Autumn.API/Endpoints/AdminEndpoints.cs | 153 + Autumn.API/Endpoints/BrowseEndpoints.cs | 55 + Autumn.API/Endpoints/CodeListEndpoints.cs | 108 + Autumn.API/Endpoints/DutyEndpoints.cs | 127 + Autumn.API/Endpoints/NoteEndpoints.cs | 109 + Autumn.API/Endpoints/SearchEndpoints.cs | 90 + Autumn.API/Profiles/SearchRequestProfile.cs | 15 - Autumn.API/Program.cs | 143 +- Autumn.API/Properties/launchSettings.json | 27 +- Autumn.API/Startup.cs | 159 - Autumn.API/appsettings.Development.json | 13 - Autumn.API/appsettings.example.json | 44 + Autumn.API/appsettings.json | 30 - Autumn.BL/Autumn.Service.csproj | 2 +- Autumn.BL/DependencyInjection.cs | 1 + Autumn.BL/Interface/ICountryService.cs | 10 + Autumn.BL/Interface/ICustomsTariffService.cs | 2 + Autumn.BL/Interface/IHsCodeService.cs | 1 + Autumn.BL/Interface/IProductService.cs | 1 + Autumn.BL/Services/Classification.cs | 332 +- Autumn.BL/Services/CountryService.cs | 20 + Autumn.BL/Services/CustomsTariffService.cs | 5 + Autumn.BL/Services/HsCodeService.cs | 3 + Autumn.BL/Services/ProductService.cs | 3 + Autumn.Domain/Autumn.Domain.csproj | 2 +- Autumn.Domain/Models/Country.cs | 17 + Autumn.Domain/Models/CustomsTariff.cs | 12 +- Autumn.Domain/Models/Document.cs | 1 + Autumn.Domain/Models/SearchLog.cs | 2 + Autumn.Domain/Models/StoreDatabaseSettings.cs | 2 + .../Autumn.Infrastructure.csproj | 2 +- Autumn.Repository/DependencyInjection.cs | 1 + .../Interface/ICountryRepository.cs | 9 + .../Interface/ICustomsTariffRepository.cs | 2 + .../Interface/IHsCodeRepository.cs | 1 + .../Interface/IProductRepository.cs | 1 + .../Repository/CountryRepository.cs | 17 + .../Repository/CustomsTariffRepository.cs | 10 + .../Repository/HsCodeRepository.cs | 66 + .../Repository/ProductRepository.cs | 55 +- Autumn.SPA/.gitignore | 24 + Autumn.SPA/README.md | 16 + Autumn.SPA/eslint.config.js | 29 + Autumn.SPA/index.html | 14 + Autumn.SPA/package-lock.json | 3441 +++++++++++++++++ Autumn.SPA/package.json | 30 + Autumn.SPA/public/favicon.svg | 16 + Autumn.SPA/public/vite.svg | 1 + Autumn.SPA/src/App.jsx | 110 + Autumn.SPA/src/api.js | 42 + Autumn.SPA/src/app.css | 118 + Autumn.SPA/src/assets/react.svg | 1 + Autumn.SPA/src/components/Badge.jsx | 16 + Autumn.SPA/src/components/BrowseView.jsx | 216 ++ Autumn.SPA/src/components/CalcView.jsx | 230 ++ Autumn.SPA/src/components/ConfBar.jsx | 14 + Autumn.SPA/src/components/CountrySelector.jsx | 52 + Autumn.SPA/src/components/Header.jsx | 32 + Autumn.SPA/src/components/Home.jsx | 55 + Autumn.SPA/src/components/OwlLogo.jsx | 18 + Autumn.SPA/src/components/SearchBar.jsx | 148 + Autumn.SPA/src/components/SearchView.jsx | 311 ++ Autumn.SPA/src/components/TariffPrompt.jsx | 12 + Autumn.SPA/src/components/Toast.jsx | 31 + Autumn.SPA/src/data/sections.js | 38 + Autumn.SPA/src/main.jsx | 10 + Autumn.SPA/vite.config.js | 13 + Autumn.UI/Autumn.UI.csproj | 2 +- Autumn.UIML.Model/Autumn.UIML.Model.csproj | 2 +- Autumn.UIML.Model/ConsumeModel.cs | 44 +- README.md | 246 +- 106 files changed, 6721 insertions(+), 1794 deletions(-) delete mode 100644 Autumn.API/Contract/V1/ApiRoutes.cs delete mode 100644 Autumn.API/Contract/V1/Requests/ClassifyCommodityRequest.cs delete mode 100644 Autumn.API/Contract/V1/Requests/DutyRequest.cs delete mode 100644 Autumn.API/Contract/V1/Requests/SearchRequest.cs delete mode 100644 Autumn.API/Contract/V1/Requests/UserRequest.cs delete mode 100644 Autumn.API/Contract/V1/Responses/AuthFailedResponse.cs delete mode 100644 Autumn.API/Contract/V1/Responses/AuthSuccessResponse.cs delete mode 100644 Autumn.API/Contract/V1/Responses/ClassifyCommodityResponse.cs delete mode 100644 Autumn.API/Contract/V1/Responses/CurrencyObject.cs delete mode 100644 Autumn.API/Contract/V1/Responses/CurrencyResponse.cs delete mode 100644 Autumn.API/Contract/V1/Responses/CustomsTariffObject.cs delete mode 100644 Autumn.API/Contract/V1/Responses/DocumentObject.cs delete mode 100644 Autumn.API/Contract/V1/Responses/DutyResponse.cs delete mode 100644 Autumn.API/Contract/V1/Responses/HSCodeObject.cs delete mode 100644 Autumn.API/Contract/V1/Responses/HscodeToDocumentObject.cs delete mode 100644 Autumn.API/Contract/V1/Responses/KeywordAPIResponsecs.cs delete mode 100644 Autumn.API/Contract/V1/Responses/KeywordResponse.cs delete mode 100644 Autumn.API/Contract/V1/Responses/NoteResponse.cs delete mode 100644 Autumn.API/Contract/V1/Responses/SearchResponse.cs delete mode 100644 Autumn.API/Contract/V1/Responses/TagsResult.cs delete mode 100644 Autumn.API/Contract/V2/ApiRoutes.cs delete mode 100644 Autumn.API/Contract/V2/Requests/SearchRequest.cs delete mode 100644 Autumn.API/Contract/V2/Responses/HSCodeObject.cs delete mode 100644 Autumn.API/Contract/V2/Responses/ResultModel.cs delete mode 100644 Autumn.API/Contract/V2/Responses/SearchResponse.cs delete mode 100644 Autumn.API/Controllers/V1/CodeListController.cs delete mode 100644 Autumn.API/Controllers/V1/DutyController.cs delete mode 100644 Autumn.API/Controllers/V1/IdentityController.cs delete mode 100644 Autumn.API/Controllers/V1/KeywordsController.cs delete mode 100644 Autumn.API/Controllers/V1/NoteController.cs delete mode 100644 Autumn.API/Controllers/V1/SearchController.cs delete mode 100644 Autumn.API/Controllers/V2/SearchController.cs create mode 100644 Autumn.API/Dto/ApiDtos.cs create mode 100644 Autumn.API/Endpoints/AdminEndpoints.cs create mode 100644 Autumn.API/Endpoints/BrowseEndpoints.cs create mode 100644 Autumn.API/Endpoints/CodeListEndpoints.cs create mode 100644 Autumn.API/Endpoints/DutyEndpoints.cs create mode 100644 Autumn.API/Endpoints/NoteEndpoints.cs create mode 100644 Autumn.API/Endpoints/SearchEndpoints.cs delete mode 100644 Autumn.API/Profiles/SearchRequestProfile.cs delete mode 100644 Autumn.API/Startup.cs delete mode 100644 Autumn.API/appsettings.Development.json create mode 100644 Autumn.API/appsettings.example.json delete mode 100644 Autumn.API/appsettings.json create mode 100644 Autumn.BL/Interface/ICountryService.cs create mode 100644 Autumn.BL/Services/CountryService.cs create mode 100644 Autumn.Domain/Models/Country.cs create mode 100644 Autumn.Repository/Interface/ICountryRepository.cs create mode 100644 Autumn.Repository/Repository/CountryRepository.cs create mode 100644 Autumn.SPA/.gitignore create mode 100644 Autumn.SPA/README.md create mode 100644 Autumn.SPA/eslint.config.js create mode 100644 Autumn.SPA/index.html create mode 100644 Autumn.SPA/package-lock.json create mode 100644 Autumn.SPA/package.json create mode 100644 Autumn.SPA/public/favicon.svg create mode 100644 Autumn.SPA/public/vite.svg create mode 100644 Autumn.SPA/src/App.jsx create mode 100644 Autumn.SPA/src/api.js create mode 100644 Autumn.SPA/src/app.css create mode 100644 Autumn.SPA/src/assets/react.svg create mode 100644 Autumn.SPA/src/components/Badge.jsx create mode 100644 Autumn.SPA/src/components/BrowseView.jsx create mode 100644 Autumn.SPA/src/components/CalcView.jsx create mode 100644 Autumn.SPA/src/components/ConfBar.jsx create mode 100644 Autumn.SPA/src/components/CountrySelector.jsx create mode 100644 Autumn.SPA/src/components/Header.jsx create mode 100644 Autumn.SPA/src/components/Home.jsx create mode 100644 Autumn.SPA/src/components/OwlLogo.jsx create mode 100644 Autumn.SPA/src/components/SearchBar.jsx create mode 100644 Autumn.SPA/src/components/SearchView.jsx create mode 100644 Autumn.SPA/src/components/TariffPrompt.jsx create mode 100644 Autumn.SPA/src/components/Toast.jsx create mode 100644 Autumn.SPA/src/data/sections.js create mode 100644 Autumn.SPA/src/main.jsx create mode 100644 Autumn.SPA/vite.config.js diff --git a/.gitignore b/.gitignore index a4fe18b..75ce13d 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,14 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# App settings with secrets +**/appsettings.json +**/appsettings.Development.json + +# Claude Code +.claude/ + +# Temp files +nul +hs-codes-redesign.jsx diff --git a/Autumn.API/Autumn.API.csproj b/Autumn.API/Autumn.API.csproj index 846e47f..43dddde 100644 --- a/Autumn.API/Autumn.API.csproj +++ b/Autumn.API/Autumn.API.csproj @@ -1,35 +1,22 @@ - netcoreapp2.2 - InProcess + net10.0 + enable + enable - + + + - - all - true - - - - - - + + + + - - - - - - - - - - - diff --git a/Autumn.API/Contract/V1/ApiRoutes.cs b/Autumn.API/Contract/V1/ApiRoutes.cs deleted file mode 100644 index e660859..0000000 --- a/Autumn.API/Contract/V1/ApiRoutes.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1 -{ - public static class ApiRoutes - { - public const string Root = "api"; - public const string Version = "v1"; - //public const string Base = Root + "/" + Version; - public const string Base = Version; - public static class Search - { - public const string Get = Base + "/search"; - } - public static class Note - { - public const string Get = Base + "/note/{hscode}"; - } - public static class Duty - { - public const string Get = Base + "/duty"; - } - public static class CodeList - { - public const string Currency = Base + "/codelist/currency"; - public const string Tags = Base + "/codelist/tags/{query?}"; - public const string Products = Base + "/codelist/products/{query?}"; - } - public static class Identity - { - public const string Login = Base + "/identity/login"; - public const string Register = Base + "/identity/register"; - } - - public static class Keyword - { - public const string Get = Base + "/keyword/get"; - } - } -} diff --git a/Autumn.API/Contract/V1/Requests/ClassifyCommodityRequest.cs b/Autumn.API/Contract/V1/Requests/ClassifyCommodityRequest.cs deleted file mode 100644 index eb9618a..0000000 --- a/Autumn.API/Contract/V1/Requests/ClassifyCommodityRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Requests -{ - public class ClassifyCommodityRequest - { - public string ItemDescription { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Requests/DutyRequest.cs b/Autumn.API/Contract/V1/Requests/DutyRequest.cs deleted file mode 100644 index 629235a..0000000 --- a/Autumn.API/Contract/V1/Requests/DutyRequest.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Requests -{ - public class DutyRequest - { - [BindProperty] - [Required] - [Display(Name = "Commodity Description")] - public string ProductDesc { get; set; } - [BindProperty] - [Required] - [Display(Name = "HS Code")] - public string HSCode { get; set; } - [BindProperty] - [Required] - [Display(Name = "Cost Price")] - public decimal Cost { get; set; } - [BindProperty] - [Required] - [Display(Name = "Freight Amount")] - public decimal Freight { get; set; } - [BindProperty] - [Required] - [Display(Name = "Insurance Amount")] - public decimal Insurance { get; set; } - [BindProperty] - [Required] - [Display(Name = "Currency")] - public string Currency { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Requests/SearchRequest.cs b/Autumn.API/Contract/V1/Requests/SearchRequest.cs deleted file mode 100644 index ca64900..0000000 --- a/Autumn.API/Contract/V1/Requests/SearchRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Requests -{ - public class SearchRequest - { - public string id { get; set; } - public string pid { get; set; } - public string level { get; set; } - public string keyword { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Requests/UserRequest.cs b/Autumn.API/Contract/V1/Requests/UserRequest.cs deleted file mode 100644 index 6b95bb7..0000000 --- a/Autumn.API/Contract/V1/Requests/UserRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Requests -{ - public class UserRequest - { - public string Username { get; set; } - public string Password { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/AuthFailedResponse.cs b/Autumn.API/Contract/V1/Responses/AuthFailedResponse.cs deleted file mode 100644 index 18470b0..0000000 --- a/Autumn.API/Contract/V1/Responses/AuthFailedResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class AuthFailedResponse - { - public IEnumerable Error { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/AuthSuccessResponse.cs b/Autumn.API/Contract/V1/Responses/AuthSuccessResponse.cs deleted file mode 100644 index 9d93c88..0000000 --- a/Autumn.API/Contract/V1/Responses/AuthSuccessResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class AuthSuccessResponse - { - public string Token { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/ClassifyCommodityResponse.cs b/Autumn.API/Contract/V1/Responses/ClassifyCommodityResponse.cs deleted file mode 100644 index 728103b..0000000 --- a/Autumn.API/Contract/V1/Responses/ClassifyCommodityResponse.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class ClassifyCommodityResponse - { - - public bool Success { get; set; } - public IEnumerable Errors { get; set; } - - public string HSCode { get; set; } - public string Accuracy { get; set; } - public HSCodeTariff Record { get; set; } - } - public class HSCodeTariff - { - public long Id { get; set; } - public string Code { get; set; } - public string Description { get; set; } - public string Duty { get; set; } - public string Levy { get; set; } - public string VAT { get; set; } - public string NAC { get; set; } - public string SUR { get; set; } - public string ETL { get; set; } - public string CIS { get; set; } - - } -} diff --git a/Autumn.API/Contract/V1/Responses/CurrencyObject.cs b/Autumn.API/Contract/V1/Responses/CurrencyObject.cs deleted file mode 100644 index de78b65..0000000 --- a/Autumn.API/Contract/V1/Responses/CurrencyObject.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class CurrencyObject - { - public string CurrencyCode { get; set; } - public string Rate { get; set; } - public string TimeStamp { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/CurrencyResponse.cs b/Autumn.API/Contract/V1/Responses/CurrencyResponse.cs deleted file mode 100644 index dba4451..0000000 --- a/Autumn.API/Contract/V1/Responses/CurrencyResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class CurrencyResponse - { - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - public List Records { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/CustomsTariffObject.cs b/Autumn.API/Contract/V1/Responses/CustomsTariffObject.cs deleted file mode 100644 index bf2d292..0000000 --- a/Autumn.API/Contract/V1/Responses/CustomsTariffObject.cs +++ /dev/null @@ -1,23 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses { - public class CustomsTariffObject - { - public string Id { get; set; } - public string Header { get; set; } - public string HSCode { get; set; } - public string Description { get; set; } - public string DUTY { get; set; } - public string LEVY { get; set; } - public string VAT { get; set; } - public string NAC { get; set; } - public string SUR { get; set; } - public string ETLS { get; set; } - public string CISS { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/DocumentObject.cs b/Autumn.API/Contract/V1/Responses/DocumentObject.cs deleted file mode 100644 index 1414657..0000000 --- a/Autumn.API/Contract/V1/Responses/DocumentObject.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using System; -using System.Collections.Generic; - -namespace Autumn.API.Contract.V1.Responses -{ - public class DocumentObject - { - - public string Id { get; set; } - public int? Level { get; set; } - public string Parent { get; set; } - public string Code { get; set; } - public string Description { get; set; } - public string Country { get; set; } - public string Issuer { get; set; } - public string Validity { get; set; } - public string DurationForIssue { get; set; } - public string ApplicationForm { get; set; } - public string InspectionFee { get; set; } - public string PermitNew { get; set; } - public string PermitRenewal { get; set; } - public string LateRenewal { get; set; } - public string PnsupportingDocument { get; set; } - public string PrsupportingDocument { get; set; } - public string Remark { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/DutyResponse.cs b/Autumn.API/Contract/V1/Responses/DutyResponse.cs deleted file mode 100644 index 9e1e464..0000000 --- a/Autumn.API/Contract/V1/Responses/DutyResponse.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class DutyResponse - { - public IEnumerable Error { get; set; } - public bool Success { get; set; } - - public string ProductDesc { get; set; } - public string HSCode { get; set; } - public decimal Cost { get; set; } - public decimal Freight { get; set; } - public decimal Insurance { get; set; } - public string Currency { get; set; } - public decimal ExRate { get; set; } - public decimal CF { get; set; } - public decimal CIF { get; set; } - public decimal CIFLocal { get; set; } - public string IDRate { get; set; } - public string VATRate { get; set; } - public string ETLRate { get; set; } - public string SURRate { get; set; } - public string CISSRate { get; set; } - public string NACRate { get; set; } - public string LEVYRate { get; set; } - public decimal IDPayableLocal { get; set; } - public decimal VATPayableLocal { get; set; } - public decimal ETLPayableLocal { get; set; } - public decimal SURPayableLocal { get; set; } - public decimal CISSPayable { get; set; } - public decimal CISSPayableLocal { get; set; } - public decimal NACPayable { get; set; } - public decimal NACPayableLocal { get; set; } - public decimal LEVYPayableLocal { get; set; } - public decimal TotalPayableLocal { get; set; } - public string HSCodeDescription { get; set; } - - } -} diff --git a/Autumn.API/Contract/V1/Responses/HSCodeObject.cs b/Autumn.API/Contract/V1/Responses/HSCodeObject.cs deleted file mode 100644 index 0828ff1..0000000 --- a/Autumn.API/Contract/V1/Responses/HSCodeObject.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - - public class HSCodeObject - { - public string PId { get; set; } - - public long Order { get; set; } - public long Level { get; set; } - - public string Id { get; set; } - - public string ParentId { get; set; } - - public string Code { get; set; } - - public string ParentCode { get; set; } - public string Description { get; set; } - public string SelfExplanatory { get; set; } - - } -} diff --git a/Autumn.API/Contract/V1/Responses/HscodeToDocumentObject.cs b/Autumn.API/Contract/V1/Responses/HscodeToDocumentObject.cs deleted file mode 100644 index 817e82f..0000000 --- a/Autumn.API/Contract/V1/Responses/HscodeToDocumentObject.cs +++ /dev/null @@ -1,23 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using System; -using System.Collections.Generic; - -namespace Autumn.API.Contract.V1.Responses -{ - public class HscodeToDocumentObject - { - public string Id { get; set; } - public string Country { get; set; } - public string Agency { get; set; } - public string Hscode { get; set; } - public string HscodeLocal { get; set; } - public string Description { get; set; } - public string ImpGeneral { get; set; } - public string ImpFinishedProductsInRetailPack { get; set; } - public string ImpBulkConsignments { get; set; } - public string ImpChemicalsOrRawMaterials { get; set; } - public string ImpSupermktOrRestaurant { get; set; } - public string ExpGeneral { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/KeywordAPIResponsecs.cs b/Autumn.API/Contract/V1/Responses/KeywordAPIResponsecs.cs deleted file mode 100644 index bae7d00..0000000 --- a/Autumn.API/Contract/V1/Responses/KeywordAPIResponsecs.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class KeywordAPIResponsecs - { - public string status { get; set; } - public string message { get; set; } - public string queryPrefix { get; set; } - public string fullQuery { get; set; } - public string query { get; set; } - public string type { get; set; } - public List results { get; set; } - - } - public class Terms - { - public string term { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/KeywordResponse.cs b/Autumn.API/Contract/V1/Responses/KeywordResponse.cs deleted file mode 100644 index 352d252..0000000 --- a/Autumn.API/Contract/V1/Responses/KeywordResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class KeywordResponse - { - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - } - -} diff --git a/Autumn.API/Contract/V1/Responses/NoteResponse.cs b/Autumn.API/Contract/V1/Responses/NoteResponse.cs deleted file mode 100644 index dd904fd..0000000 --- a/Autumn.API/Contract/V1/Responses/NoteResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class NoteResponse - { - - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - public List Records { get; set; } - public List Documents { get; set; } - public List Tariff { get; set; } - public List RecordsToDocuments { get; set; } - } - - -} diff --git a/Autumn.API/Contract/V1/Responses/SearchResponse.cs b/Autumn.API/Contract/V1/Responses/SearchResponse.cs deleted file mode 100644 index 950254e..0000000 --- a/Autumn.API/Contract/V1/Responses/SearchResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class SearchResponse - { - - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - public List Records { get; set; } - } - -} diff --git a/Autumn.API/Contract/V1/Responses/TagsResult.cs b/Autumn.API/Contract/V1/Responses/TagsResult.cs deleted file mode 100644 index fd1dbe1..0000000 --- a/Autumn.API/Contract/V1/Responses/TagsResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class TagsResult - { - - [JsonProperty("success")] - public bool Success { get; set; } - - [JsonProperty("results")] - public List Results { get; set; } - } - - public class Result - { - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - - [JsonProperty("text")] - public string Text { get; set; } - } - -} diff --git a/Autumn.API/Contract/V2/ApiRoutes.cs b/Autumn.API/Contract/V2/ApiRoutes.cs deleted file mode 100644 index 1ab8848..0000000 --- a/Autumn.API/Contract/V2/ApiRoutes.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V2 -{ - public static class ApiRoutes - { - public const string Root = "api"; - public const string Version = "v2"; - //public const string Base = Root + "/" + Version; - public const string Base = Version; - public static class Search - { - public const string Get = Base + "/search"; - } - } -} diff --git a/Autumn.API/Contract/V2/Requests/SearchRequest.cs b/Autumn.API/Contract/V2/Requests/SearchRequest.cs deleted file mode 100644 index 203fee7..0000000 --- a/Autumn.API/Contract/V2/Requests/SearchRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V2.Requests -{ - public class SearchRequest - { - public string id { get; set; } - public string pid { get; set; } - public string level { get; set; } - public string keyword { get; set; } - public string settings { get; set; } - } -} diff --git a/Autumn.API/Contract/V2/Responses/HSCodeObject.cs b/Autumn.API/Contract/V2/Responses/HSCodeObject.cs deleted file mode 100644 index a78a870..0000000 --- a/Autumn.API/Contract/V2/Responses/HSCodeObject.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V2.Responses -{ - - public class HSCodeObject - { - public string PId { get; set; } - - public long Order { get; set; } - public long Level { get; set; } - - public string Id { get; set; } - - public string ParentId { get; set; } - - public string Code { get; set; } - - public string ParentCode { get; set; } - public string Description { get; set; } - public string SelfExplanatory { get; set; } - - } -} diff --git a/Autumn.API/Contract/V2/Responses/ResultModel.cs b/Autumn.API/Contract/V2/Responses/ResultModel.cs deleted file mode 100644 index d4a89c9..0000000 --- a/Autumn.API/Contract/V2/Responses/ResultModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.Domain.Models; - -namespace Autumn.API.Contract.V2.Responses -{ - public class ResultModel - { - public List HSCodes { get; internal set; } - public string Prediction { get; internal set; } - public float Rating { get; internal set; } - public List Tags { get; internal set; } - public string Code { get; internal set; } - public List PHSCodes { get; internal set; } - } -} diff --git a/Autumn.API/Contract/V2/Responses/SearchResponse.cs b/Autumn.API/Contract/V2/Responses/SearchResponse.cs deleted file mode 100644 index 412352c..0000000 --- a/Autumn.API/Contract/V2/Responses/SearchResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V2.Responses -{ - public class SearchResponse - { - - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - public List Records { get; set; } - public bool ai { get; set; } - } - -} diff --git a/Autumn.API/Controllers/V1/CodeListController.cs b/Autumn.API/Controllers/V1/CodeListController.cs deleted file mode 100644 index 5a3be2e..0000000 --- a/Autumn.API/Controllers/V1/CodeListController.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Models; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [ApiController] - public class CodeListController : ControllerBase - { - - private readonly CurrencyService _currencyService; - private readonly ProductService _productService; - - public CodeListController(CurrencyService currencyService, ProductService productService) - { - _currencyService = currencyService; - _productService = productService; - } - - //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - [HttpGet(ApiRoutes.CodeList.Currency)] - public async Task CurrencyAsync() - { - CurrencyResponse response = new CurrencyResponse(); - try - { - var currency = await _currencyService.GetAsync(); - response.Records = currency.Select(x => new CurrencyObject { CurrencyCode = x.CurrencyCode, Rate = x.Rate, TimeStamp = x.TimeStamp }).ToList(); - response.Success = true; - return Ok(response); - } - catch (Exception ex) - { - return BadRequest(new CurrencyResponse { Success = false, Error = new[] { ex.Message } }); - } - } - - [HttpGet(ApiRoutes.CodeList.Tags)] - public async Task TagsAsync(string query = null) - { - - TagsResult tr = new TagsResult(); - try - { - List p = await _productService.GetByTagsAsync(query); - var tags = p.Select(x => x.Tags); - tr.Results = new List(); - foreach (var tagarr in tags) - { - foreach (var tag in tagarr) - { - - if (tr.Results.Any(x => x.Name != tag)) - { - Result r = new Result { Name = tag, Text = tag, Value = tag }; - tr.Results.Add(r); - } - else if (tr.Results.Count == 0) - { - Result r = new Result { Name = tag, Text = tag, Value = tag }; - tr.Results.Add(r); - } - } - } - tr.Success = true; - return new JsonResult(tr); - } - catch (Exception ex) - { - tr.Success = false; - return new JsonResult(tr); - } - - } - [HttpGet(ApiRoutes.CodeList.Products)] - public async Task ProductsAsync(string query = null) - { - - TagsResult tr = new TagsResult(); - try - { - List p = await _productService.GetLikeKeywordAsync(query); - // var tags = p.Select(x => x.Tags); - tr.Results = new List(); - - foreach (var tag in p) - { - if (tr.Results.Count == 0) - { - Result r = new Result { Name = tag.Keyword, Text = tag.Keyword, Value = tag.Code }; - tr.Results.Add(r); - } - else - { - if (tr.Results.Any(x => x.Name != tag.Keyword)) - { - Result r = new Result { Name = tag.Keyword, Text = tag.Keyword, Value = tag.Code }; - tr.Results.Add(r); - } - } - } - - tr.Success = true; - return new JsonResult(tr); - } - catch (Exception ex) - { - tr.Success = false; - return new JsonResult(tr); - } - - } - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/DutyController.cs b/Autumn.API/Controllers/V1/DutyController.cs deleted file mode 100644 index a5d5b00..0000000 --- a/Autumn.API/Controllers/V1/DutyController.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Requests; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [Authorize] - [ApiController] - public class DutyController : ControllerBase - { - private readonly CustomsTariffService _tariffService; - private readonly CurrencyService _currencyService; - - public DutyController(CustomsTariffService tariffService, CurrencyService currencyService) - { - _tariffService = tariffService; - _currencyService = currencyService; - } - - - // [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - [HttpGet(ApiRoutes.Duty.Get)] - public async Task GetAsync([FromQuery] DutyRequest request) - { - try - { - DutyResponse response = new DutyResponse(); - if (ModelState.IsValid) - { - //Get HS Code Tariff - var tariff = _tariffService.GetByHSCode(request.HSCode); - var currency = _currencyService.GetByCurrency(request.Currency); - var cif = request.Cost + request.Insurance + request.Freight; - var cf = request.Cost + request.Freight; - response.ExRate = decimal.Parse(currency.Rate); - var duty = cif * (decimal.Parse(tariff.DUTY) / 100); - var vat = (cif + duty) * (decimal.Parse(tariff.VAT) / 100); - var sur = cif * (decimal.Parse(tariff.SUR) / 100); - var etl = cif * (decimal.Parse(tariff.ETLS) / 100); - var ciss = cif * (decimal.Parse(tariff.CISS) / 100); - var nac = cif * (decimal.Parse(tariff.NAC) / 100); - var levy = cif * (decimal.Parse(tariff.LEVY) / 100); - - - response.ProductDesc = request.ProductDesc; - response.HSCode = request.HSCode; - response.Cost = request.Cost; - response.Freight = request.Freight; - response.Insurance = request.Insurance; - response.Currency = request.Currency; - - response.CF = cf; - response.CIF = cif; - response.CIFLocal = cif * response.ExRate; - response.IDRate = tariff.DUTY; - response.IDPayableLocal = duty * response.ExRate; - response.VATRate = tariff.VAT; - response.VATPayableLocal = vat * response.ExRate; - response.ETLRate = tariff.ETLS; - response.ETLPayableLocal = etl * response.ExRate; - response.SURRate = tariff.SUR; - response.SURPayableLocal = sur * response.ExRate; - response.CISSRate = tariff.CISS; - response.CISSPayableLocal = ciss * response.ExRate; - response.NACRate = tariff.NAC; - response.NACPayableLocal = nac * response.ExRate; - response.LEVYRate = tariff.LEVY; - response.LEVYPayableLocal = levy * response.ExRate; - response.TotalPayableLocal = (duty + vat + sur + etl + ciss + nac + levy) * response.ExRate; - response.HSCodeDescription = tariff.Description; - - response.Success = true; - - return Ok(response); - } - else { - StringBuilder sb = new StringBuilder(); - foreach (var modelState in ModelState.Values) - { - foreach (var error in modelState.Errors) - { - sb.Append(error.ErrorMessage); - sb.AppendLine(); - sb.Append(error.Exception.Message); - } - } - return BadRequest(new DutyResponse { Success = false, Error = new[] { sb.ToString() } }); - } - } - catch (Exception ex) - { - return BadRequest(new DutyResponse { Success = false, Error = new[] { ex.Message } }); - } - } - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/IdentityController.cs b/Autumn.API/Controllers/V1/IdentityController.cs deleted file mode 100644 index 158640e..0000000 --- a/Autumn.API/Controllers/V1/IdentityController.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Requests; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [ApiController] - public class IdentityController : ControllerBase - { - private readonly IdentityService _identityService; - - public IdentityController(IdentityService identityService) { - _identityService = identityService; - } - - [HttpPost(ApiRoutes.Identity.Register)] - public async Task Register([FromForm] UserRequest request) - { - if (!ModelState.IsValid) - { - return BadRequest(new AuthFailedResponse { Error = ModelState.Values.SelectMany(x => x.Errors.Select(xx => xx.ErrorMessage)) }); - } - - var authResponse = _identityService.Register(request.Username, request.Password); - if (!authResponse.Success) - { - return BadRequest(new AuthFailedResponse - { - Error = authResponse.Errors - }); - } - return Ok(new AuthSuccessResponse { Token = authResponse.Token }); - } - - [HttpPost(ApiRoutes.Identity.Login)] - public IActionResult Login([FromForm] UserRequest request) - { - var authResponse = _identityService.Login(request.Username, request.Password); - if (!authResponse.Success) - { - return BadRequest(new AuthFailedResponse - { - Error = authResponse.Errors - }); - } - return Ok(new AuthSuccessResponse { Token = authResponse.Token }); - } - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/KeywordsController.cs b/Autumn.API/Controllers/V1/KeywordsController.cs deleted file mode 100644 index 1733011..0000000 --- a/Autumn.API/Controllers/V1/KeywordsController.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; -using RestSharp; -using Autumn.Domain.Models; -using Microsoft.AspNetCore.Authorization; - -namespace Autumn.API.V1 -{ - [Authorize] - //[Route("api/[controller]")] - [ApiController] - public class KeywordsController : ControllerBase - { - - private readonly KeywordService _keywordService; - private readonly ProductService _productService; - - public KeywordsController(KeywordService keywordService, ProductService productService) - { - _keywordService = keywordService; - _productService = productService; - } - - [HttpGet(ApiRoutes.Keyword.Get)] - public async Task GetAsync() - { - try - { - var products = await _productService.GetAsync(); - var keywords = await _keywordService.GetAsync(); - var remaining = products.Where(x=> !keywords.Select(s=>s.ParentKeyword).Contains(x.Keyword)).OrderBy(a=>a.Id); - - foreach (var r in remaining) - { - try - { - if (r.Keyword.Split().Count() < 10) - { - var client = new RestClient("https://uscensus.prod.3ceonline.com/ui/autocomplete"); - client.Timeout = -1; - var request = new RestRequest(Method.POST); - request.AddHeader("Content-Type", "application/json"); - request.AddParameter("application/json", "{\"query\":\"" + r.Keyword + "\"}", ParameterType.RequestBody); - IRestResponse response = client.Execute(request); - - var rep = JsonConvert.DeserializeObject(response.Content); - //Console.WriteLine(response.Content); - if (rep != null) - { - foreach (var term in rep.results) - { - _keywordService.Create(new Keyword { ParentKeyword = r.Keyword, ChildKeyword = term.term }); - } - } - - } - } - catch { } - } - return Ok(new KeywordResponse { Success = true, Error = new[] { "ok" } }); - } - catch (Exception ex) - { - return BadRequest(new KeywordResponse { Success = false, Error = new[] { ex.Message } }); - } - } - - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/NoteController.cs b/Autumn.API/Controllers/V1/NoteController.cs deleted file mode 100644 index ececb4b..0000000 --- a/Autumn.API/Controllers/V1/NoteController.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [Authorize] - [ApiController] - public class NoteController : ControllerBase - { - - private readonly HSCodeService _hscodeService; - private readonly DocumentService _documentService; - private readonly HSCodeToDocumentService _hscodeToDocumentService; - private readonly CustomsTariffService _customsTariffService; - - public NoteController(HSCodeService hscodeService, DocumentService documentService, HSCodeToDocumentService hscodeToDocumentService, CustomsTariffService customsTariffService) - { - _hscodeService = hscodeService; - _documentService = documentService; - _hscodeToDocumentService = hscodeToDocumentService; - _customsTariffService = customsTariffService; - } - - [Authorize] - //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - [HttpGet(ApiRoutes.Note.Get)] - public async Task GetAsync(string hscode) - { - try - { - var documentTask = _documentService.GetAsync(); - var hscodeTask = _hscodeService.GetWithHSCodeOptionsAsync(hscode, null, null); - var hsdocsTask = _hscodeToDocumentService.GetWithCodeAsync(hscode); - var tariff = _customsTariffService.GetByHeaderAsync(hscode); - - var hscodeList = await hscodeTask; - var hscodeToDocumentList = await hsdocsTask; - var documentList = await documentTask; - var tariffList = await tariff; - - var hscodeObj = hscodeList.Select(x => new HSCodeObject - { - Code = x.Code, - Description = x.Description, - Id = x.Id, - Level = x.Level, - Order = x.Order, - ParentCode = x.ParentCode, - ParentId = x.ParentId, - PId = x.PId, - SelfExplanatory = x.SelfExplanatory - }).ToList(); - - var hscodeToDocumentObj = hscodeToDocumentList.Select(x => new HscodeToDocumentObject - { - Agency = x.Agency, - Country = x.Country, - Description = x.Description, - ExpGeneral = x.ExpGeneral, - Hscode = x.Hscode, - HscodeLocal = x.HscodeLocal, - Id = x.Id, - ImpBulkConsignments = x.ImpBulkConsignments, - ImpChemicalsOrRawMaterials = x.ImpChemicalsOrRawMaterials, - ImpFinishedProductsInRetailPack = x.ImpFinishedProductsInRetailPack, - ImpGeneral = x.ImpGeneral, - ImpSupermktOrRestaurant = x.ImpSupermktOrRestaurant - }).ToList(); - - var documentObj = documentList.Select(x => new DocumentObject - { - ApplicationForm = x.ApplicationForm, - Code = x.Code, - Country = x.Country, - Description = x.Description, - DurationForIssue = x.DurationForIssue, - Id = x.Id, - InspectionFee = x.InspectionFee, - Issuer = x.Issuer, - LateRenewal = x.LateRenewal, - Level = x.Level, - Parent = x.Parent, - PermitNew = x.PermitNew, - PermitRenewal = x.PermitRenewal, - PnsupportingDocument = x.PnsupportingDocument, - PrsupportingDocument = x.PrsupportingDocument, - Remark = x.Remark, - Validity = x.Validity - }).ToList(); - - var tariffObj = tariffList.Select(x => new CustomsTariffObject - { - CISS = x.CISS, - Description = x.Description, - DUTY = x.DUTY, - ETLS = x.ETLS, - Header = x.Header, - HSCode = x.HSCode, - Id = x.Id, - LEVY = x.LEVY, - NAC = x.NAC, - SUR = x.SUR, - VAT = x.VAT - }).ToList(); - - return Ok(new NoteResponse { Success = true, Documents = documentObj, Records = hscodeObj, RecordsToDocuments = hscodeToDocumentObj, Tariff = tariffObj }); - } - catch (Exception ex) - { - return BadRequest(new NoteResponse { Success = false, Error = new[] { ex.Message } }); - } - } - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/SearchController.cs b/Autumn.API/Controllers/V1/SearchController.cs deleted file mode 100644 index 4db2f62..0000000 --- a/Autumn.API/Controllers/V1/SearchController.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Requests; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Infra; -using Autumn.Domain.Models; -using Autumn.Domain.Services; -using Autumn_UIML.Model; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [Authorize] - // [Route("api/[controller]")] - [ApiController] - public class SearchController : ControllerBase - { - private readonly HSCodeService _hscodeService; - private readonly IPredict _predict; - - public SearchController(HSCodeService hscodeService, IPredict predict) - { - _hscodeService = hscodeService; - _predict = predict; - } - //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - [HttpGet(ApiRoutes.Search.Get)] - public async Task GetAsync([FromQuery] SearchRequest request) - { - try - { - List hscodes = new List(); - if (string.IsNullOrEmpty(request.keyword)) - { - hscodes = await _hscodeService.GetWithOptionsAsync(request.id, request.pid, request.level); - } - else - { - //ProductDesc = productDesc; - var ai = _predict.GetHSCode(request.keyword); - var aiarr = ai.Prediction.Split('-'); - hscodes = await _hscodeService.GetWithHSCodeOptionsAsync(null, aiarr[1], null); - } - var records = hscodes.Select(x => new HSCodeObject - { - Code = x.Code, - Description = x.Description, - Id = x.Id, - Level = x.Level, - Order = x.Order, - ParentCode = x.ParentCode, - ParentId = x.ParentId, - PId = x.PId, - SelfExplanatory = x.SelfExplanatory - }).ToList(); - - return Ok(new SearchResponse { Success = true, Records = records }); - } - catch (Exception ex) - { - return BadRequest(new SearchResponse { Success = false, Error = new[] { ex.Message } }); - } - } - - - } -} diff --git a/Autumn.API/Controllers/V2/SearchController.cs b/Autumn.API/Controllers/V2/SearchController.cs deleted file mode 100644 index b0a7aaa..0000000 --- a/Autumn.API/Controllers/V2/SearchController.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; -using Autumn.API.Contract.V2; -using Autumn.API.Contract.V2.Requests; -using Autumn.API.Contract.V2.Responses; -using Autumn.BL.Interface.V2; -using Autumn.BL.Models.Request.V2; -using Autumn.Domain.Infra; -using Autumn.Domain.Models; -using Autumn.Domain.Services; -using Autumn_UIML.Model; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; - -namespace Autumn.API.V2 -{ - [Authorize] - // [Route("api/[controller]")] - [ApiController] - public class SearchController : ControllerBase - { - private readonly HSCodeService _hscodeService; - private readonly IPredict _predict; - private readonly ProductService _productService; - private IConfiguration _configuration; - private readonly IClassification _classification; - private readonly IMapper _mapper; - - public SearchController(IConfiguration configuration, HSCodeService hscodeService, IPredict predict, ProductService productService, IClassification classification, IMapper mapper) - { - _hscodeService = hscodeService; - _predict = predict; - _productService = productService; - _configuration = configuration; - _classification = classification; - _mapper = mapper; - } - //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - /*[HttpGet(ApiRoutes.Search.Get)] - public async Task GetAsync([FromQuery] SearchRequest request) - { - try - { - SearchResponse response = new SearchResponse { Success = true }; - ResultModel rm = new ResultModel(); - var records = new List(); - - //Do Navigation or Tag Query - rm.Prediction = string.Empty;// item.Key; - rm.Code = request.pid;// aiarr[1]; - rm.Rating = 0;// item.Value; - rm.Tags = new List(); - rm.PHSCodes = new List(); - rm.HSCodes = new List(); - - if (!string.IsNullOrEmpty(request.settings)) - { - if (request.settings == "nav") - { - rm.HSCodes = await _hscodeService.GetWithOptionsAsync(request.id, request.pid, request.level); - if (!string.IsNullOrEmpty(request.pid)) - rm.PHSCodes = await _hscodeService.GetWithOptionsAsync(request.pid, null, null); - - } - else if (request.settings == "tag") - { - rm.HSCodes = await _hscodeService.GetWithHSCodeOptionsAsync(request.id, request.pid, request.level); - if (!string.IsNullOrEmpty(request.pid)) - rm.PHSCodes = await _hscodeService.GetWithHSCodeOptionsAsync(request.pid, null, null); - } - - records.Add(rm); - - response.Records = records; - return Ok(response); - } - else - { - List products = await _productService.GetByKeywordAsync(request.keyword); - - var ctn = products.Count(x => x.Tags != null); - - if (products.Count > 0) - { - foreach (var product in products) - { - rm = new ResultModel(); - rm.Tags = new List(); - // var aiarr = product.Key.Split('-'); - rm.HSCodes = await _hscodeService.GetWithHSCodeOptionsAsync(product.Code, null, null); - //rm.HSCodes = Result2; - rm.Prediction = product.Keyword; - rm.Code = product.Code; - if (product.Tags != null) - rm.Tags.AddRange(product.Tags); - //rm.Rating = item.Value; - rm.PHSCodes = await _hscodeService.GetWithOptionsAsync(rm.HSCodes.FirstOrDefault().ParentId, null, null); - records.Add(rm); - if (ctn == 0) return Ok(new SearchResponse { Success = true, Records = records }); - } - - } - else if (products.Count == 0) - { - var ai = GetHSCode(request.keyword, double.Parse(_configuration["SiteSettings:Threshold"])); - if (ai.Count > 0) response.ai = true; - - rm = new ResultModel(); - rm.HSCodes = new List(); - foreach (var item in ai) - { - var aiarr = item.Key.Split('-'); - rm.HSCodes.AddRange(await _hscodeService.GetWithHSCodeOptionsAsync(aiarr[1], null, null)); - //rm.HSCodes = Result2; - //rm.Code = aiarr[1]; - //rm.Rating = item.Value; - } - - rm.Prediction = request.keyword; - rm.PHSCodes = new List(); - rm.Tags = new List(); - records.Add(rm); - } - response.Records = records; - return Ok(response); - } - } - catch (Exception ex) - { - return BadRequest(new SearchResponse { Success = false, Error = new[] { ex.Message } }); - } - } - - private Dictionary GetHSCode(string product, double threshold) - { - ModelInput data = new ModelInput - { - Keyword = product - }; - // Make a single prediction on the sample data and print results - Dictionary predictionResult = ConsumeModel.Predict(data, threshold); - - return predictionResult; - }*/ - [HttpGet(ApiRoutes.Search.Get)] - public async Task GetAsync([FromQuery] SearchRequest request) - { - - - SearchResponse response = new SearchResponse { Success = true }; - ResultModel rm = new ResultModel(); - var records = new List(); - var resquetMapped = _mapper.Map(request); - var resp = await _classification.SearchAsync(resquetMapped); - if (resp.Success) - return Ok(resp); - else - return BadRequest(resp); - - } - - } -} diff --git a/Autumn.API/Dto/ApiDtos.cs b/Autumn.API/Dto/ApiDtos.cs new file mode 100644 index 0000000..8f911d3 --- /dev/null +++ b/Autumn.API/Dto/ApiDtos.cs @@ -0,0 +1,200 @@ +namespace Autumn.API.Dto; + +// ── Search ────────────────────────────────────────────────────── + +public class SearchApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + public Dictionary> Records { get; set; } = new(); +} + +public class SearchResultDto +{ + public List HSCodes { get; set; } = new(); + public List ParentHSCodes { get; set; } = new(); + public string Prediction { get; set; } = string.Empty; + public float Rating { get; set; } + public List Tags { get; set; } = new(); + public string Code { get; set; } = string.Empty; +} + +public class HSCodeDto +{ + public string Id { get; set; } = string.Empty; + public string ParentId { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public string ParentCode { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string? SelfExplanatory { get; set; } + public long Level { get; set; } + public long Order { get; set; } +} + +// ── Browse ────────────────────────────────────────────────────── + +public class BrowseApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + public List Records { get; set; } = new(); +} + +// ── Duty Calculator ───────────────────────────────────────────── + +public class DutyRequest +{ + public string ProductDesc { get; set; } = string.Empty; + public string HSCode { get; set; } = string.Empty; + public decimal Cost { get; set; } + public decimal Freight { get; set; } + public decimal Insurance { get; set; } + public string Currency { get; set; } = string.Empty; +} + +public class DutyApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + + // Input echo + public string ProductDesc { get; set; } = string.Empty; + public string HSCode { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + public decimal Cost { get; set; } + public decimal Freight { get; set; } + public decimal Insurance { get; set; } + public string Currency { get; set; } = string.Empty; + + // Calculated + public decimal CIF { get; set; } + + // Dynamic rate breakdown (works for any country) + public List Breakdown { get; set; } = new(); + public decimal TotalDuty { get; set; } + + public string HSCodeDescription { get; set; } = string.Empty; +} + +public class DutyLineItem +{ + public string Code { get; set; } = string.Empty; + public string Label { get; set; } = string.Empty; + public decimal Rate { get; set; } + public decimal Amount { get; set; } +} + +// ── Note ──────────────────────────────────────────────────────── + +public class NoteApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + public List Records { get; set; } = new(); + public List Documents { get; set; } = new(); + public List RecordsToDocuments { get; set; } = new(); + public List Tariff { get; set; } = new(); +} + +public class DocumentDto +{ + public string Id { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string? Country { get; set; } + public string? Issuer { get; set; } + public string? Level { get; set; } + public string? Parent { get; set; } + public string? Validity { get; set; } + public string? DurationForIssue { get; set; } + public string? ApplicationForm { get; set; } + public string? InspectionFee { get; set; } + public string? PermitNew { get; set; } + public string? PermitRenewal { get; set; } + public string? LateRenewal { get; set; } + public string? PnsupportingDocument { get; set; } + public string? PrsupportingDocument { get; set; } + public string? Remark { get; set; } +} + +public class HSCodeToDocumentDto +{ + public string Id { get; set; } = string.Empty; + public string? Agency { get; set; } + public string? Country { get; set; } + public string? Hscode { get; set; } + public string? HscodeLocal { get; set; } + public string? Description { get; set; } + public string? ImpGeneral { get; set; } + public string? ImpFinishedProductsInRetailPack { get; set; } + public string? ImpBulkConsignments { get; set; } + public string? ImpChemicalsOrRawMaterials { get; set; } + public string? ImpSupermktOrRestaurant { get; set; } + public string? ExpGeneral { get; set; } +} + +public class CustomsTariffDto +{ + public string Id { get; set; } = string.Empty; + public string? Country { get; set; } + public string? Header { get; set; } + public string HSCode { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string? DUTY { get; set; } + public string? VAT { get; set; } + public string? LEVY { get; set; } + public string? NAC { get; set; } + public string? SUR { get; set; } + public string? ETLS { get; set; } + public string? CISS { get; set; } + public string? NHIL { get; set; } + public string? GETFUND { get; set; } + public string? IDF { get; set; } + public string? RDF { get; set; } +} + +// ── CodeList ──────────────────────────────────────────────────── + +public class CurrencyApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + public List Records { get; set; } = new(); +} + +public class CurrencyDto +{ + public string CurrencyCode { get; set; } = string.Empty; + public string Rate { get; set; } = string.Empty; + public string? TimeStamp { get; set; } +} + +public class TagsApiResponse +{ + public bool Success { get; set; } + public List Results { get; set; } = new(); +} + +public class TagResult +{ + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; +} + +// ── Countries ────────────────────────────────────────────────── + +public class CountryApiResponse +{ + public bool Success { get; set; } + public List Records { get; set; } = new(); +} + +public class CountryDto +{ + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Flag { get; set; } = string.Empty; + public string Currency { get; set; } = string.Empty; + public string Symbol { get; set; } = string.Empty; +} diff --git a/Autumn.API/Endpoints/AdminEndpoints.cs b/Autumn.API/Endpoints/AdminEndpoints.cs new file mode 100644 index 0000000..fe092df --- /dev/null +++ b/Autumn.API/Endpoints/AdminEndpoints.cs @@ -0,0 +1,153 @@ +using Autumn.API.Dto; +using Autumn.Domain.Models; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class AdminEndpoints +{ + public static void MapAdminEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/admin") + .WithTags("Admin") + .RequireAuthorization(); + + // Dashboard + group.MapGet("/dashboard", GetDashboard); + + // Products CRUD + group.MapGet("/products", GetProducts); + group.MapGet("/products/{id}", GetProduct); + group.MapPost("/products", CreateProduct); + group.MapPut("/products/{id}", UpdateProduct); + group.MapDelete("/products/{id}", DeleteProduct); + + // HS Codes CRUD + group.MapGet("/codes", GetCodes); + group.MapGet("/codes/{id}", GetCode); + group.MapPut("/codes/{id}", UpdateCode); + + // Tariffs CRUD + group.MapGet("/tariffs", GetTariffs); + group.MapGet("/tariffs/{id}", GetTariff); + group.MapPost("/tariffs", CreateTariff); + group.MapPut("/tariffs/{id}", UpdateTariff); + group.MapDelete("/tariffs/{id}", DeleteTariff); + + // Query Logs + group.MapGet("/querylogs", GetQueryLogs); + } + + // ── Dashboard ─────────────────────────────────────────────── + + private static async Task GetDashboard( + IProductService productService, + IHsCodeService hsCodeService, + ICustomsTariffService tariffService) + { + var products = await productService.GetAsync(); + var codes = await hsCodeService.GetAsync(); + var tariffs = await tariffService.GetAsync(); + + return Results.Ok(new + { + ProductCount = products.Count, + HSCodeCount = codes.Count, + TariffCount = tariffs.Count + }); + } + + // ── Products ──────────────────────────────────────────────── + + private static async Task GetProducts(IProductService productService) + { + var products = await productService.GetAsync(); + return Results.Ok(products); + } + + private static async Task GetProduct(IProductService productService, string id) + { + var product = await productService.GetAsync(id); + return product is null ? Results.NotFound() : Results.Ok(product); + } + + private static async Task CreateProduct(IProductService productService, Product product) + { + var created = await productService.CreateAsync(product); + return Results.Created($"/api/admin/products/{created.Id}", created); + } + + private static async Task UpdateProduct(IProductService productService, string id, Product product) + { + await productService.UpdateAsync(id, product); + return Results.NoContent(); + } + + private static async Task DeleteProduct(IProductService productService, string id) + { + await productService.RemoveAsync(id); + return Results.NoContent(); + } + + // ── HS Codes ──────────────────────────────────────────────── + + private static async Task GetCodes(IHsCodeService hsCodeService) + { + var codes = await hsCodeService.GetAsync(); + return Results.Ok(codes); + } + + private static async Task GetCode(IHsCodeService hsCodeService, string id) + { + var code = await hsCodeService.GetAsync(id); + return code is null ? Results.NotFound() : Results.Ok(code); + } + + private static async Task UpdateCode(IHsCodeService hsCodeService, string id, HSCode code) + { + await hsCodeService.UpdateAsync(id, code); + return Results.NoContent(); + } + + // ── Tariffs ───────────────────────────────────────────────── + + private static async Task GetTariffs(ICustomsTariffService tariffService, string? country = null) + { + var tariffs = await tariffService.GetAsync(); + if (!string.IsNullOrEmpty(country)) + tariffs = tariffs.Where(t => t.Country == country || (t.Country == null && country == "NG")).ToList(); + return Results.Ok(tariffs); + } + + private static async Task GetTariff(ICustomsTariffService tariffService, string id) + { + var tariff = await tariffService.GetAsync(id); + return tariff is null ? Results.NotFound() : Results.Ok(tariff); + } + + private static async Task CreateTariff(ICustomsTariffService tariffService, CustomsTariff tariff) + { + var created = await tariffService.CreateAsync(tariff); + return Results.Created($"/api/admin/tariffs/{created.Id}", created); + } + + private static async Task UpdateTariff(ICustomsTariffService tariffService, string id, CustomsTariff tariff) + { + await tariffService.UpdateAsync(id, tariff); + return Results.NoContent(); + } + + private static async Task DeleteTariff(ICustomsTariffService tariffService, string id) + { + await tariffService.RemoveAsync(id); + return Results.NoContent(); + } + + // ── Query Logs ────────────────────────────────────────────── + + private static async Task GetQueryLogs(ISearchLogService searchLogService) + { + var logs = await searchLogService.GetAsync(); + return Results.Ok(logs); + } +} diff --git a/Autumn.API/Endpoints/BrowseEndpoints.cs b/Autumn.API/Endpoints/BrowseEndpoints.cs new file mode 100644 index 0000000..446fa86 --- /dev/null +++ b/Autumn.API/Endpoints/BrowseEndpoints.cs @@ -0,0 +1,55 @@ +using Autumn.API.Dto; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class BrowseEndpoints +{ + public static void MapBrowseEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/browse") + .WithTags("Browse") + .AllowAnonymous(); + + group.MapGet("/", Browse); + } + + private static async Task Browse( + IHsCodeService hsCodeService, + string? code = null, + string? parentCode = null, + string? parentId = null, + string? level = null) + { + try + { + List hscodes; + + if (!string.IsNullOrEmpty(parentId)) + { + // ID-based navigation (matches Razor page pattern) + hscodes = await hsCodeService.GetWithOptionsAsync(null, parentId, level); + } + else + { + hscodes = await hsCodeService.GetWithHSCodeOptionsAsync(code, parentCode, level); + } + + var records = hscodes.Select(SearchEndpoints.MapHSCode).ToList(); + + return Results.Ok(new BrowseApiResponse + { + Success = true, + Records = records + }); + } + catch (Exception ex) + { + return Results.BadRequest(new BrowseApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } +} diff --git a/Autumn.API/Endpoints/CodeListEndpoints.cs b/Autumn.API/Endpoints/CodeListEndpoints.cs new file mode 100644 index 0000000..4eaa6b3 --- /dev/null +++ b/Autumn.API/Endpoints/CodeListEndpoints.cs @@ -0,0 +1,108 @@ +using Autumn.API.Dto; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class CodeListEndpoints +{ + public static void MapCodeListEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/codelist") + .WithTags("Reference Data") + .AllowAnonymous(); + + group.MapGet("/countries", GetCountries); + group.MapGet("/currency", GetCurrencies); + group.MapGet("/products/{query?}", GetProducts); + group.MapGet("/tags/{query?}", GetTags); + } + + private static async Task GetCountries(ICountryService countryService) + { + var countries = await countryService.GetAsync(); + return Results.Ok(new CountryApiResponse + { + Success = true, + Records = countries.Select(c => new CountryDto + { + Code = c.Code ?? string.Empty, + Name = c.Name ?? string.Empty, + Flag = c.Flag ?? string.Empty, + Currency = c.Currency ?? string.Empty, + Symbol = c.Symbol ?? string.Empty + }).ToList() + }); + } + + private static async Task GetCurrencies(ICurrencyService currencyService) + { + try + { + var currencies = await currencyService.GetAsync(); + return Results.Ok(new CurrencyApiResponse + { + Success = true, + Records = currencies.Select(c => new CurrencyDto + { + CurrencyCode = c.CurrencyCode ?? string.Empty, + Rate = c.Rate.ToString(), + TimeStamp = c.TimeStamp.ToString("o") + }).ToList() + }); + } + catch (Exception ex) + { + return Results.BadRequest(new CurrencyApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } + + private static async Task GetProducts(IProductService productService, string? query = null) + { + try + { + var products = string.IsNullOrEmpty(query) + ? await productService.GetAsync() + : await productService.GetLikeKeywordAsync(query); + + var results = products.Select(p => new TagResult + { + Name = p.Keyword ?? string.Empty, + Value = p.Code ?? string.Empty, + Text = p.Keyword ?? string.Empty + }).ToList(); + + return Results.Ok(new TagsApiResponse { Success = true, Results = results }); + } + catch (Exception ex) + { + return Results.BadRequest(new TagsApiResponse { Success = false }); + } + } + + private static async Task GetTags(IProductService productService, string? query = null) + { + try + { + var products = string.IsNullOrEmpty(query) + ? await productService.GetAsync() + : await productService.GetByTagsAsync(query); + + var results = products.Select(p => new TagResult + { + Name = p.Keyword ?? string.Empty, + Value = p.Code ?? string.Empty, + Text = p.Keyword ?? string.Empty + }).ToList(); + + return Results.Ok(new TagsApiResponse { Success = true, Results = results }); + } + catch (Exception ex) + { + return Results.BadRequest(new TagsApiResponse { Success = false }); + } + } +} diff --git a/Autumn.API/Endpoints/DutyEndpoints.cs b/Autumn.API/Endpoints/DutyEndpoints.cs new file mode 100644 index 0000000..8c9c23e --- /dev/null +++ b/Autumn.API/Endpoints/DutyEndpoints.cs @@ -0,0 +1,127 @@ +using Autumn.API.Dto; +using Autumn.Domain.Models; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class DutyEndpoints +{ + private static readonly Dictionary RateLabels = new() + { + ["DUTY"] = "Import Duty", + ["VAT"] = "VAT", + ["LEVY"] = "Levy", + ["SUR"] = "Surcharge", + ["ETLS"] = "ETL", + ["CISS"] = "CISS", + ["NAC"] = "NAC", + ["NHIL"] = "NHIL", + ["GETFUND"] = "GETFund", + ["IDF"] = "IDF", + ["RDF"] = "RDF" + }; + + public static void MapDutyEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/duty") + .WithTags("Duty Calculator") + .AllowAnonymous(); + + group.MapGet("/", CalculateDuty); + } + + private static async Task CalculateDuty( + ICustomsTariffService tariffService, + string HSCode, + string Country = "NG", + string ProductDesc = "", + decimal Cost = 0, + decimal Freight = 0, + decimal Insurance = 0, + string Currency = "USD") + { + try + { + // Try exact HSCode match first, then fall back to Header match + var tariff = await tariffService.GetByHSCodeAndCountryAsync(HSCode, Country); + if (tariff == null) + { + var headerMatches = await tariffService.GetByHeaderAndCountryAsync(HSCode, Country); + tariff = headerMatches.FirstOrDefault(); + } + if (tariff == null) + return Results.BadRequest(new DutyApiResponse + { + Success = false, + Error = new[] { $"No tariff found for HS Code: {HSCode} in country: {Country}" } + }); + + var cif = Cost + Insurance + Freight; + + // Build dynamic breakdown from all non-null/non-zero rate fields + var breakdown = new List(); + decimal dutyAmount = 0; // track import duty for VAT base calculation + + foreach (var (code, rateStr) in GetRateFields(tariff)) + { + if (!decimal.TryParse(rateStr, out var rate) || rate == 0) + continue; + + // VAT is typically calculated on CIF + Import Duty + var baseAmount = code == "VAT" ? cif + dutyAmount : cif; + var amount = baseAmount * (rate / 100); + + if (code == "DUTY") + dutyAmount = amount; + + breakdown.Add(new DutyLineItem + { + Code = code, + Label = RateLabels.GetValueOrDefault(code, code), + Rate = rate, + Amount = Math.Round(amount, 2) + }); + } + + return Results.Ok(new DutyApiResponse + { + Success = true, + ProductDesc = ProductDesc, + HSCode = HSCode, + Country = Country, + Cost = Cost, + Freight = Freight, + Insurance = Insurance, + Currency = Currency, + CIF = cif, + Breakdown = breakdown, + TotalDuty = Math.Round(breakdown.Sum(b => b.Amount), 2), + HSCodeDescription = tariff.Description ?? string.Empty + }); + } + catch (Exception ex) + { + return Results.BadRequest(new DutyApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } + + private static IEnumerable<(string Code, string? Rate)> GetRateFields(CustomsTariff tariff) + { + // Yield DUTY first so its amount is available for VAT base calculation + yield return ("DUTY", tariff.DUTY); + yield return ("VAT", tariff.VAT); + yield return ("LEVY", tariff.LEVY); + yield return ("SUR", tariff.SUR); + yield return ("ETLS", tariff.ETLS); + yield return ("CISS", tariff.CISS); + yield return ("NAC", tariff.NAC); + yield return ("NHIL", tariff.NHIL); + yield return ("GETFUND", tariff.GETFUND); + yield return ("IDF", tariff.IDF); + yield return ("RDF", tariff.RDF); + } +} diff --git a/Autumn.API/Endpoints/NoteEndpoints.cs b/Autumn.API/Endpoints/NoteEndpoints.cs new file mode 100644 index 0000000..d2bf5d1 --- /dev/null +++ b/Autumn.API/Endpoints/NoteEndpoints.cs @@ -0,0 +1,109 @@ +using Autumn.API.Dto; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class NoteEndpoints +{ + public static void MapNoteEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/note") + .WithTags("Notes") + .AllowAnonymous(); + + group.MapGet("/{hscode}", GetNote); + } + + private static async Task GetNote( + IHsCodeService hsCodeService, + IDocumentService documentService, + IHsCodeDocumentService hsCodeDocumentService, + ICustomsTariffService tariffService, + string hscode, + string? country = null) + { + try + { + // Run queries in parallel + var hscodeTask = hsCodeService.GetWithHSCodeOptionsAsync(hscode, null, null); + var documentTask = documentService.GetAsync(); + var hsdocsTask = hsCodeDocumentService.GetWithCodeAsync(hscode); + var tariffTask = tariffService.GetByHeaderAndCountryAsync(hscode, country ?? "NG"); + + await Task.WhenAll(hscodeTask, documentTask, hsdocsTask, tariffTask); + + var hscodeList = await hscodeTask; + var documentList = await documentTask; + var hscodeToDocumentList = await hsdocsTask; + var tariffList = await tariffTask; + + return Results.Ok(new NoteApiResponse + { + Success = true, + Records = hscodeList.Select(SearchEndpoints.MapHSCode).ToList(), + Documents = documentList.Select(x => new DocumentDto + { + Id = x.Id ?? string.Empty, + Code = x.Code ?? string.Empty, + Description = x.Description ?? string.Empty, + Country = x.Country, + Issuer = x.Issuer, + Level = x.Level?.ToString(), + Parent = x.Parent, + Validity = x.Validity, + DurationForIssue = x.DurationForIssue, + ApplicationForm = x.ApplicationForm, + InspectionFee = x.InspectionFee, + PermitNew = x.PermitNew, + PermitRenewal = x.PermitRenewal, + LateRenewal = x.LateRenewal, + PnsupportingDocument = x.PnsupportingDocument, + PrsupportingDocument = x.PrsupportingDocument, + Remark = x.Remark + }).ToList(), + RecordsToDocuments = hscodeToDocumentList.Select(x => new HSCodeToDocumentDto + { + Id = x.Id ?? string.Empty, + Agency = x.Agency, + Country = x.Country, + Hscode = x.Hscode, + HscodeLocal = x.HscodeLocal, + Description = x.Description, + ImpGeneral = x.ImpGeneral, + ImpFinishedProductsInRetailPack = x.ImpFinishedProductsInRetailPack, + ImpBulkConsignments = x.ImpBulkConsignments, + ImpChemicalsOrRawMaterials = x.ImpChemicalsOrRawMaterials, + ImpSupermktOrRestaurant = x.ImpSupermktOrRestaurant, + ExpGeneral = x.ExpGeneral + }).ToList(), + Tariff = tariffList.Select(x => new CustomsTariffDto + { + Id = x.Id ?? string.Empty, + Country = x.Country, + Header = x.Header, + HSCode = x.HSCode ?? string.Empty, + Description = x.Description ?? string.Empty, + DUTY = x.DUTY, + VAT = x.VAT, + LEVY = x.LEVY, + NAC = x.NAC, + SUR = x.SUR, + ETLS = x.ETLS, + CISS = x.CISS, + NHIL = x.NHIL, + GETFUND = x.GETFUND, + IDF = x.IDF, + RDF = x.RDF + }).ToList() + }); + } + catch (Exception ex) + { + return Results.BadRequest(new NoteApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } +} diff --git a/Autumn.API/Endpoints/SearchEndpoints.cs b/Autumn.API/Endpoints/SearchEndpoints.cs new file mode 100644 index 0000000..53a8c24 --- /dev/null +++ b/Autumn.API/Endpoints/SearchEndpoints.cs @@ -0,0 +1,90 @@ +using Autumn.API.Dto; +using Autumn.Service.Interface; +using Autumn.BL.Models.Request.V3; + +namespace Autumn.API.Endpoints; + +public static class SearchEndpoints +{ + public static void MapSearchEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/search") + .WithTags("Search") + .AllowAnonymous(); + + group.MapGet("/", Search); + } + + private static async Task Search( + IClassification classification, + string? keyword = null, + string? id = null, + string? pid = null, + string? level = null, + string? settings = null) + { + try + { + var request = new BLSearchRequest + { + id = id, + pid = pid, + level = level, + keyword = keyword, + settings = settings + }; + + var resp = await classification.SearchAsync(request); + + if (!resp.Success) + return Results.BadRequest(new SearchApiResponse + { + Success = false, + Error = resp.Error + }); + + // Map BLSearchResponse to clean API response + var response = new SearchApiResponse { Success = true }; + + if (resp.Records != null) + { + foreach (var kvp in resp.Records) + { + var results = kvp.Value.Select(r => new SearchResultDto + { + Prediction = r.Prediction ?? string.Empty, + Rating = r.Rating, + Code = r.Code ?? string.Empty, + Tags = r.Tags ?? new List(), + HSCodes = r.HSCodes?.Select(MapHSCode).ToList() ?? new List(), + ParentHSCodes = r.PHSCodes?.Select(MapHSCode).ToList() ?? new List() + }).ToList(); + + response.Records[kvp.Key] = results; + } + } + + return Results.Ok(response); + } + catch (Exception ex) + { + return Results.BadRequest(new SearchApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } + + internal static HSCodeDto MapHSCode(Autumn.Domain.Models.HSCode x) => new() + { + Id = x.Id ?? string.Empty, + ParentId = x.ParentId ?? string.Empty, + Code = x.Code ?? string.Empty, + ParentCode = x.ParentCode ?? string.Empty, + Description = x.Description ?? string.Empty, + SelfExplanatory = x.SelfExplanatory, + Level = x.Level, + Order = x.Order + }; +} diff --git a/Autumn.API/Profiles/SearchRequestProfile.cs b/Autumn.API/Profiles/SearchRequestProfile.cs deleted file mode 100644 index 9a6b12b..0000000 --- a/Autumn.API/Profiles/SearchRequestProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using AutoMapper; -using Autumn.API.Contract.V2.Requests; -using Autumn.BL.Models.Request.V2; - -namespace Autumn.API.Profiles -{ - public class SearchRequestProfile : Profile - { - public SearchRequestProfile() - { - CreateMap(); - } - } -} diff --git a/Autumn.API/Program.cs b/Autumn.API/Program.cs index 67865c1..4da52f5 100644 --- a/Autumn.API/Program.cs +++ b/Autumn.API/Program.cs @@ -1,25 +1,128 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Autumn.API +using System.Threading.RateLimiting; +using Autumn.API.Endpoints; +using Autumn.Domain.Models; +using Autumn.Infrastructure; +using Autumn.Service; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.RateLimiting; +using MongoDB.Driver; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; + +// ── MongoDB & SQL database services ───────────────────────────── +builder.Services.AddDocumentDatabaseServices(configuration); +builder.Services.AddRelationalDatabaseServices(configuration); + +// ── Repository & Business services ────────────────────────────── +builder.Services.AddRepositoryServices(); +builder.Services.AddBusinessServices(); + +// ── CORS ──────────────────────────────────────────────────────── +builder.Services.AddCors(options => { - public class Program + options.AddPolicy("AllowSPA", policy => { - public static void Main(string[] args) - { - Console.Title = "Autumn API"; - CreateWebHostBuilder(args).Build().Run(); - } + var origins = configuration.GetSection("Cors:AllowedOrigins").Get() + ?? new[] { "http://localhost:5173", "http://localhost:5174" }; + + policy.WithOrigins(origins) + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +// ── Rate Limiting ──────────────────────────────────────────────── +builder.Services.AddRateLimiter(options => +{ + options.RejectionStatusCode = 429; + options.GlobalLimiter = PartitionedRateLimiter.Create(ctx => + RateLimitPartition.GetFixedWindowLimiter( + ctx.Connection.RemoteIpAddress?.ToString() ?? "anonymous", + _ => new FixedWindowRateLimiterOptions + { + PermitLimit = 30, + Window = TimeSpan.FromSeconds(60), + QueueLimit = 0 + })); +}); + +// ── Auth0 JWT Authentication ──────────────────────────────────── +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = configuration["Auth0:Domain"]; + options.Audience = configuration["Auth0:Audience"]; + }); + +builder.Services.AddAuthorization(); + +// ── OpenAPI / Swagger ─────────────────────────────────────────── +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "HS Codes API", + Version = "v1", + Description = "HS Commodity Classification & Duty Calculator API" + }); +}); + +var app = builder.Build(); + +// ── Seed ──────────────────────────────────────────────────────── +using (var scope = app.Services.CreateScope()) +{ + var settings = scope.ServiceProvider.GetRequiredService(); + var client = new MongoClient(settings.ConnectionString); + var db = client.GetDatabase(settings.DatabaseName); - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); + // Set Country = "NG" on existing tariff records that have no country + var tariffs = db.GetCollection(settings.CustomsTariffStoreCollectionName); + var filter = Builders.Filter.Eq(t => t.Country, null); + var update = Builders.Update.Set(t => t.Country, "NG"); + var result = await tariffs.UpdateManyAsync(filter, update); + if (result.ModifiedCount > 0) + app.Logger.LogInformation("Seed: Updated {Count} tariff records with Country = 'NG'", result.ModifiedCount); + + // Seed countries collection if empty + var countries = db.GetCollection(settings.CountryStoreCollectionName); + var countryCount = await countries.CountDocumentsAsync(Builders.Filter.Empty); + if (countryCount == 0) + { + var seedCountries = new List + { + new() { Code = "NG", Name = "Nigeria", Flag = "\U0001F1F3\U0001F1EC", Currency = "NGN", Symbol = "\u20A6" }, + new() { Code = "GH", Name = "Ghana", Flag = "\U0001F1EC\U0001F1ED", Currency = "GHS", Symbol = "GH\u20B5" }, + new() { Code = "KE", Name = "Kenya", Flag = "\U0001F1F0\U0001F1EA", Currency = "KES", Symbol = "KSh" }, + new() { Code = "ZA", Name = "South Africa", Flag = "\U0001F1FF\U0001F1E6", Currency = "ZAR", Symbol = "R" }, + new() { Code = "GB", Name = "United Kingdom", Flag = "\U0001F1EC\U0001F1E7", Currency = "GBP", Symbol = "\u00A3" }, + }; + await countries.InsertManyAsync(seedCountries); + app.Logger.LogInformation("Seed: Inserted {Count} countries", seedCountries.Count); } } + +// ── Middleware pipeline ───────────────────────────────────────── +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "HS Codes API v1")); +} + +app.UseCors("AllowSPA"); +app.UseRateLimiter(); +app.UseAuthentication(); +app.UseAuthorization(); + +// ── Map endpoints ─────────────────────────────────────────────── +app.MapSearchEndpoints(); +app.MapBrowseEndpoints(); +app.MapDutyEndpoints(); +app.MapNoteEndpoints(); +app.MapCodeListEndpoints(); +app.MapAdminEndpoints(); + +app.Run(); diff --git a/Autumn.API/Properties/launchSettings.json b/Autumn.API/Properties/launchSettings.json index d70d56b..36d1032 100644 --- a/Autumn.API/Properties/launchSettings.json +++ b/Autumn.API/Properties/launchSettings.json @@ -1,30 +1,23 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:5003", - "sslPort": 0 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", + "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "http": { + "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "api/values", + "applicationUrl": "http://localhost:5174", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Autumn.API": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "api/values", + "applicationUrl": "https://localhost:7235;http://localhost:5174", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:5001" + } } } -} \ No newline at end of file +} diff --git a/Autumn.API/Startup.cs b/Autumn.API/Startup.cs deleted file mode 100644 index 98e3c2a..0000000 --- a/Autumn.API/Startup.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; -using Autumn.BL.Interface.V2; -using Autumn.BL.Services.V2; -using Autumn.Domain.Data; -using Autumn.Domain.Infra; -using Autumn.Domain.Models; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; - -namespace Autumn.API -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - - //services.AddDbContext(options => - // options.UseSqlServer( - // Configuration.GetConnectionString("DefaultConnection"))); - services.Configure( - Configuration.GetSection(nameof(StoreDatabaseSettings))); - - services.AddSingleton(sp => - sp.GetRequiredService>().Value); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - //services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); - services.AddMvcCore() - .AddAuthorization() - .AddJsonFormatters(); - - services.AddAutoMapper(typeof(Startup)); - - - services.AddAuthentication("Bearer") - .AddJwtBearer("Bearer", options => - { - options.Authority = Configuration["SiteSettings:SSOURL"]; - options.RequireHttpsMetadata = false; - - options.Audience = "autumnapi"; - }); - - services.AddCors(options => - { - // this defines a CORS policy called "default" - options.AddPolicy("default", policy => - { - policy.WithOrigins("http://localhost:5003") - .AllowAnyHeader() - .AllowAnyMethod(); - }); - }); - // Register the Swagger generator, defining 1 or more Swagger documents - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo - { - Version = "v1", - Title = "HS Codes", - Description = "HS Commodity Classification API", - TermsOfService = new Uri("https://example.com/terms"), - Contact = new OpenApiContact - { - Name = "Support", - Email = string.Empty, - Url = new Uri("https://example.com/support"), - }, - License = new OpenApiLicense - { - Name = "Use under LICX", - Url = new Uri("https://example.com/license"), - } - }); - //First we define the security scheme - c.AddSecurityDefinition("Bearer", //Name the security scheme - new OpenApiSecurityScheme - { - Description = "JWT Authorization header using the Bearer scheme.", - Type = SecuritySchemeType.Http, //We set the scheme type to http since we're using bearer authentication - Scheme = "bearer" //The name of the HTTP Authorization scheme to be used in the Authorization header. In this case "bearer". - }); - - c.AddSecurityRequirement(new OpenApiSecurityRequirement{ - { - new OpenApiSecurityScheme{ - Reference = new OpenApiReference{ - Id = "Bearer", //The name of the previously defined security scheme. - Type = ReferenceType.SecurityScheme - } - },new List() - } - }); - }); - - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseCors("default"); - app.UseAuthentication(); - app.UseMvc(); - - // Enable middleware to serve generated Swagger as a JSON endpoint. - app.UseSwagger(); - - // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), - // specifying the Swagger JSON endpoint. - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "Autumn API V1"); - }); - } - } -} diff --git a/Autumn.API/appsettings.Development.json b/Autumn.API/appsettings.Development.json deleted file mode 100644 index dfc6fd5..0000000 --- a/Autumn.API/appsettings.Development.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "SiteSettings": { - "Threshold": "0.02", - "SSOURL": "http://localhost:5000" - }, - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/Autumn.API/appsettings.example.json b/Autumn.API/appsettings.example.json new file mode 100644 index 0000000..5147f5f --- /dev/null +++ b/Autumn.API/appsettings.example.json @@ -0,0 +1,44 @@ +{ + "StoreDatabaseSettings": { + "HSCodeStoreCollectionName": "hscodes", + "ProductStoreCollectionName": "products", + "Product2StoreCollectionName": "Products2", + "KeywordStoreCollectionName": "keywords", + "SearchLogStoreCollectionName": "SearchLog", + "DocumentStoreCollectionName": "Documents", + "CurrencyStoreCollectionName": "currencies", + "IdentityStoreCollectionName": "Identities", + "CustomsTariffStoreCollectionName": "tariffs", + "HSCodeToDocumentStoreCollectionName": "HSCodeToDocuments", + "RequirementStoreCollectionName": "requirements", + "CountryStoreCollectionName": "countries", + "ConnectionString": "mongodb+srv://:@.mongodb.net/", + "DatabaseName": "ClassificationDb" + }, + "ConnectionStrings": { + "DefaultConnection": "" + }, + "Auth0": { + "Domain": "https://your-auth0-domain.auth0.com/", + "Audience": "autumnapi" + }, + "SiteSettings": { + "Threshold": "0.1", + "GroqApiKey": "", + "GroqModel": "llama-3.1-8b-instant" + }, + "Cors": { + "AllowedOrigins": [ + "http://localhost:5173", + "http://localhost:5174", + "http://localhost:5003" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Autumn.API/appsettings.json b/Autumn.API/appsettings.json deleted file mode 100644 index cad529c..0000000 --- a/Autumn.API/appsettings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "StoreDatabaseSettings": { - "HSCodeStoreCollectionName": "HSCodes", - "ProductStoreCollectionName": "Products", - "KeywordStoreCollectionName": "Keyword", - "SearchLogStoreCollectionName": "SearchLog", - "DocumentStoreCollectionName": "Documents", - "CurrencyStoreCollectionName": "Currencies", - "IdentityStoreCollectionName": "Identities", - "CustomsTariffStoreCollectionName": "CustomsTariff", - "HSCodeToDocumentStoreCollectionName": "HSCodeToDocuments", - "ConnectionString": "mongodb://localhost:27017", - "DatabaseName": "ClassificationDb" - }, - "ConnectionStrings": { - //"DefaultConnection": "Server=(local)\\SAMABOS;Database=classification;Trusted_Connection=True;MultipleActiveResultSets=true" - - "DefaultConnection": "Server=198.38.83.33;Database=Uthman_avvs;User ID=uthman_tradehubuser;Password=Dem@ter1al!ze7;Trusted_Connection=False;" - }, - "SiteSettings": { - "Threshold": "0.02", - "SSOURL": "http://104.154.117.94:5000" - }, - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/Autumn.BL/Autumn.Service.csproj b/Autumn.BL/Autumn.Service.csproj index 6798169..dae27cc 100644 --- a/Autumn.BL/Autumn.Service.csproj +++ b/Autumn.BL/Autumn.Service.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 diff --git a/Autumn.BL/DependencyInjection.cs b/Autumn.BL/DependencyInjection.cs index a8a1c9b..f57dba4 100644 --- a/Autumn.BL/DependencyInjection.cs +++ b/Autumn.BL/DependencyInjection.cs @@ -15,6 +15,7 @@ public static void AddBusinessServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Autumn.BL/Interface/ICountryService.cs b/Autumn.BL/Interface/ICountryService.cs new file mode 100644 index 0000000..08e67e5 --- /dev/null +++ b/Autumn.BL/Interface/ICountryService.cs @@ -0,0 +1,10 @@ +using Autumn.Domain.Models; +using System.Threading.Tasks; + +namespace Autumn.Service.Interface +{ + public interface ICountryService : IBaseService + { + Task GetByCodeAsync(string code); + } +} diff --git a/Autumn.BL/Interface/ICustomsTariffService.cs b/Autumn.BL/Interface/ICustomsTariffService.cs index 6313197..529755c 100644 --- a/Autumn.BL/Interface/ICustomsTariffService.cs +++ b/Autumn.BL/Interface/ICustomsTariffService.cs @@ -8,5 +8,7 @@ public interface ICustomsTariffService : IBaseService { Task> GetByHeaderAsync(string header); Task GetByHSCodeAsync(string hscode); + Task GetByHSCodeAndCountryAsync(string hscode, string country); + Task> GetByHeaderAndCountryAsync(string header, string country); } } diff --git a/Autumn.BL/Interface/IHsCodeService.cs b/Autumn.BL/Interface/IHsCodeService.cs index b47593d..7ab280d 100644 --- a/Autumn.BL/Interface/IHsCodeService.cs +++ b/Autumn.BL/Interface/IHsCodeService.cs @@ -8,5 +8,6 @@ public interface IHsCodeService : IBaseService { Task> GetWithHSCodeOptionsAsync(string code, string pcode, string level); Task> GetWithOptionsAsync(string id, string pid, string level); + Task> SearchByDescriptionAsync(string keyword, int limit = 20); } } diff --git a/Autumn.BL/Interface/IProductService.cs b/Autumn.BL/Interface/IProductService.cs index bdba899..d941379 100644 --- a/Autumn.BL/Interface/IProductService.cs +++ b/Autumn.BL/Interface/IProductService.cs @@ -9,5 +9,6 @@ public interface IProductService : IBaseService Task> GetByKeywordAsync(string keyword); Task> GetByTagsAsync(string tag); Task> GetLikeKeywordAsync(string keyword); + Task> SearchByKeywordAsync(string keyword, int limit = 20); } } diff --git a/Autumn.BL/Services/Classification.cs b/Autumn.BL/Services/Classification.cs index dca14d3..1094a2c 100644 --- a/Autumn.BL/Services/Classification.cs +++ b/Autumn.BL/Services/Classification.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Autumn.BL.Models.Request.V3; using Autumn.BL.Models.Response.V3; @@ -9,6 +10,7 @@ using Autumn_UIML.Model; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using RestSharp; namespace Autumn.Service @@ -34,80 +36,144 @@ public async Task SearchAsync(BLSearchRequest request) { try { - BLSearchResponse response = new BLSearchResponse { Success = true }; - BLResultModel rm = new BLResultModel(); - var rms = new List(); + var response = new BLSearchResponse { Success = true }; var records = new Dictionary>(); - //Do Navigation or Tag Query - rm.Prediction = string.Empty;// item.Key; - rm.Code = request.pid;// aiarr[1]; - rm.Rating = 0;// item.Value; - rm.Tags = new List(); - rm.PHSCodes = new List(); - rm.HSCodes = new List(); - + // Navigation mode — handle separately + if (!string.IsNullOrEmpty(request.settings)) + { + var rm = new BLResultModel + { + Prediction = string.Empty, + Code = request.pid, + Rating = 0, + Tags = new List(), + PHSCodes = new List(), + HSCodes = new List() + }; + var rms = new List(); + return await Navigation(request, response, rm, rms, records); + } + // --- Blended search: run DB stages concurrently --- + var allResults = new List(); + // DB stages in parallel: exact match + Atlas Search + description + var exactTask = _productService.GetByKeywordAsync(request.keyword); + var searchTask = _productService.SearchByKeywordAsync(request.keyword); + var descTask = _hsCodeService.SearchByDescriptionAsync(request.keyword); + await Task.WhenAll(exactTask, searchTask, descTask); - //Naigation Logic this should be seperated + var exactProducts = exactTask.Result ?? new List(); + var searchProducts = searchTask.Result ?? new List(); + var hsResults = descTask.Result ?? new List(); - if (!string.IsNullOrEmpty(request.settings)) + // Process exact matches (highest confidence: 0.88–0.97) + var exactRms = new List(); + for (var pi = 0; pi < exactProducts.Count; pi++) { - return await Navigation(request, response, rm, rms, records); + var conf = Math.Max(0.88f, 0.97f - pi * 0.03f); + await LoadProduct(exactRms, exactProducts[pi], conf); } - else - { - // Check if there is a direct match from the database - List products = await _productService.GetByKeywordAsync(request.keyword); - - var ctn = products.Count(x => x.Tags != null); + allResults.AddRange(exactRms); + // Process Atlas Search / regex matches (medium confidence: 0.60–0.82) + var searchRms = new List(); + for (var ri = 0; ri < searchProducts.Count; ri++) + { + var conf = Math.Max(0.60f, 0.82f - ri * 0.02f); + await LoadProduct(searchRms, searchProducts[ri], conf); + } + allResults.AddRange(searchRms); - if (products.Count > 0) + // Process description matches (lower confidence: 0.40–0.73) + for (var hi = 0; hi < hsResults.Count; hi++) + { + var hs = hsResults[hi]; + var levelBonus = hs.Level == 4 ? 0.05f : 0f; + var conf = Math.Max(0.40f, 0.68f + levelBonus - hi * 0.02f); + var rm = new BLResultModel + { + Prediction = hs.Description, + Code = hs.Code, + Rating = conf, + Tags = new List(), + HSCodes = new List { hs }, + PHSCodes = new List() + }; + // Build ancestor chain + var ancestors = new List(); + var cur = hs; + while (cur != null && !string.IsNullOrEmpty(cur.ParentId)) { - foreach (var product in products) + var parents = await _hsCodeService.GetWithOptionsAsync(cur.ParentId, null, null); + var parent = parents.FirstOrDefault(); + if (parent != null) { - rm = await LoadProduct(rms, product); - if (ctn == 0) - { - records.Add("match", rms); - return new BLSearchResponse { Success = true, Records = records }; - } + ancestors.Insert(0, parent); + cur = parent; } - records.Add("match", rms); - return new BLSearchResponse { Success = true, Records = records }; - + else break; } - else if (products.Count == 0) - { - // there is no direct match from the database Attempt Synonyms - var synonyms = await GetMatchSynonyms(request.keyword); - if (synonyms.Count > 0) - { - foreach (var product in synonyms) - { - rm = await LoadProduct(rms, product); - } - records.Add("synonym", rms); - response.Records = records; - return response; + rm.PHSCodes = ancestors; + allResults.Add(rm); + } - } - else - { + // Groq fallback: only call LLM if DB stages returned no high-confidence results + var bestConfidence = allResults.Count > 0 ? allResults.Max(r => r.Rating) : 0f; + if (bestConfidence < 0.7f) + { + var groqResults = await GroqClassifyAsync(request.keyword); + allResults.AddRange(groqResults); + } - //Attempt Prediction - rm = await AIMethod(request, response, rms); - records.Add("ai", rms); - response.Records = records; - return response; - } + // Deduplicate by HS code, keeping the highest-confidence entry + var merged = allResults + .GroupBy(r => r.Code ?? r.HSCodes?.FirstOrDefault()?.Code ?? "") + .Where(g => !string.IsNullOrEmpty(g.Key)) + .Select(g => g.OrderByDescending(r => r.Rating).First()) + .OrderByDescending(r => r.Rating) + .Take(20) + .ToList(); + + if (merged.Count > 0) + { + records["match"] = merged; + response.Records = records; + return response; + } + + // Fallback: synonyms (only if no results from primary stages) + var synonyms = await GetMatchSynonyms(request.keyword); + if (synonyms.Count > 0) + { + var synRms = new List(); + for (var si = 0; si < synonyms.Count; si++) + { + var conf = Math.Max(0.35f, 0.58f - si * 0.03f); + await LoadProduct(synRms, synonyms[si], conf); } + records["synonym"] = synRms; response.Records = records; return response; } + + // Last resort: ML model prediction + try + { + var aiRms = new List(); + await AIMethod(request, response, aiRms); + if (aiRms.Count > 0) + records["ai"] = aiRms; + } + catch + { + // ML model may not be available + } + + response.Records = records; + return response; } catch (Exception ex) { @@ -115,19 +181,35 @@ public async Task SearchAsync(BLSearchRequest request) } } - private async Task LoadProduct(List rms, Product product) + private async Task LoadProduct(List rms, Product product, float confidence = 0.5f) { BLResultModel rm = new BLResultModel(); + rm.Rating = confidence; rm.Tags = new List(); - // var aiarr = product.Key.Split('-'); + rm.PHSCodes = new List(); rm.HSCodes = await _hsCodeService.GetWithHSCodeOptionsAsync(product.Code, null, null); - //rm.HSCodes = Result2; rm.Prediction = product.Keyword; rm.Code = product.Code; if (product.Tags != null) rm.Tags.AddRange(product.Tags); - //rm.Rating = item.Value; - rm.PHSCodes = await _hsCodeService.GetWithOptionsAsync(rm.HSCodes.FirstOrDefault().ParentId, null, null); + // Build full ancestor chain (Section → Chapter → Heading) + if (rm.HSCodes.Count > 0) + { + var ancestors = new List(); + var current = rm.HSCodes.FirstOrDefault(); + while (current != null && !string.IsNullOrEmpty(current.ParentId)) + { + var parents = await _hsCodeService.GetWithOptionsAsync(current.ParentId, null, null); + var parent = parents.FirstOrDefault(); + if (parent != null) + { + ancestors.Insert(0, parent); + current = parent; + } + else break; + } + rm.PHSCodes = ancestors; + } rms.Add(rm); return rm; } @@ -138,7 +220,7 @@ private async Task> GetMatchSynonyms(string keyword) var synonyms = await GetSynonyms(keyword.ToLower()); foreach (var synonym in synonyms.ToList()) { - List productExist = await _productService.GetLikeKeywordAsync(synonym); + List productExist = await _productService.GetLikeKeywordAsync(synonym) ?? new List(); //if (productExist.Count == 0) synonyms.Remove(synonym); collector.AddRange(productExist); @@ -158,7 +240,7 @@ private async Task> GetSynonyms(string keyword) req.AddHeader("x-rapidapi-host", "languagetools.p.rapidapi.com"); var resp = await client.ExecuteAsync(req); var synonyms = JsonConvert.DeserializeObject(resp.Content); - return synonyms.Synonyms; + return synonyms?.Synonyms ?? new List(); } catch { @@ -180,9 +262,7 @@ private async Task AIMethod(BLSearchRequest request, BLSearchResp { var aiarr = item.Key.Split('-'); rm.HSCodes.AddRange(await _hsCodeService.GetWithHSCodeOptionsAsync(aiarr[1], null, null)); - //rm.HSCodes = Result2; - //rm.Code = aiarr[1]; - //rm.Rating = item.Value; + rm.Rating = item.Value; } rm.Prediction = request.keyword; @@ -215,6 +295,132 @@ private async Task Navigation(BLSearchRequest request, BLSearc return response; } + private async Task> GroqClassifyAsync(string keyword) + { + var results = new List(); + var apiKey = _configuration["SiteSettings:GroqApiKey"]; + if (string.IsNullOrEmpty(apiKey)) + return results; + + try + { + var model = _configuration["SiteSettings:GroqModel"] ?? "llama-3.1-8b-instant"; + var client = new RestClient("https://api.groq.com/openai/v1/chat/completions"); + var req = new RestRequest { Method = Method.Post }; + req.AddHeader("Authorization", $"Bearer {apiKey}"); + req.AddHeader("Content-Type", "application/json"); + + var systemPrompt = @"You are an HS Code classification expert. Given a product description, return the most likely Harmonized System (HS) codes at the 4-digit or 6-digit heading level. + +Rules: +- Return ONLY a JSON array of objects with ""code"" and ""description"" fields +- Each code should be a valid HS code (4 or 6 digits, e.g. ""8471"" or ""847130"") +- Return up to 5 most likely codes, ordered by confidence +- Do not include any text outside the JSON array +- Format codes without dots or spaces + +Example response: +[{""code"":""8471"",""description"":""Automatic data processing machines""},{""code"":""8473"",""description"":""Parts and accessories for office machines""}]"; + + var body = new + { + model, + messages = new[] + { + new { role = "system", content = systemPrompt }, + new { role = "user", content = $"Classify this product: {keyword}" } + }, + temperature = 0.1, + max_tokens = 512 + }; + + req.AddJsonBody(body); + var resp = await client.ExecuteAsync(req); + + if (!resp.IsSuccessful || string.IsNullOrEmpty(resp.Content)) + return results; + + var json = JObject.Parse(resp.Content); + var content = json["choices"]?[0]?["message"]?["content"]?.ToString(); + if (string.IsNullOrEmpty(content)) + return results; + + // Extract JSON array from response (LLM may wrap it in markdown code blocks) + var arrayMatch = Regex.Match(content, @"\[.*\]", RegexOptions.Singleline); + if (!arrayMatch.Success) + return results; + + var predictions = JArray.Parse(arrayMatch.Value); + + for (var i = 0; i < predictions.Count; i++) + { + var code = predictions[i]["code"]?.ToString(); + var desc = predictions[i]["description"]?.ToString() ?? ""; + if (string.IsNullOrEmpty(code)) + continue; + + // Look up the code in the database + var hsCodes = await _hsCodeService.GetWithHSCodeOptionsAsync(code, null, null); + if (hsCodes.Count == 0) + { + // Try partial match — search children with this prefix + hsCodes = await _hsCodeService.GetWithHSCodeOptionsAsync(null, code, null); + } + + var foundInDb = hsCodes.Count > 0; + + // Log every Groq prediction for accuracy tracking (fire-and-forget) + _ = _searchlogService.CreateAsync(new SearchLog + { + Keyword = keyword, + Prediction = $"{code}-{desc}", + Rating = i + 1, + Threshold = 0, + Source = "groq", + FoundInDb = foundInDb, + Created = DateTime.Now + }); + + if (!foundInDb) + continue; + + var conf = Math.Max(0.45f, 0.75f - i * 0.06f); + var rm = new BLResultModel + { + Prediction = desc.Length > 0 ? desc : keyword, + Code = code, + Rating = conf, + Tags = new List { "ai" }, + HSCodes = hsCodes, + PHSCodes = new List() + }; + + // Build ancestor chain + var ancestors = new List(); + var cur = hsCodes.FirstOrDefault(); + while (cur != null && !string.IsNullOrEmpty(cur.ParentId)) + { + var parents = await _hsCodeService.GetWithOptionsAsync(cur.ParentId, null, null); + var parent = parents.FirstOrDefault(); + if (parent != null) + { + ancestors.Insert(0, parent); + cur = parent; + } + else break; + } + rm.PHSCodes = ancestors; + results.Add(rm); + } + } + catch + { + // Groq API not available — return empty + } + + return results; + } + public Dictionary GetHSCode(string product, double threshold) { ModelInput data = new ModelInput diff --git a/Autumn.BL/Services/CountryService.cs b/Autumn.BL/Services/CountryService.cs new file mode 100644 index 0000000..0a50fb3 --- /dev/null +++ b/Autumn.BL/Services/CountryService.cs @@ -0,0 +1,20 @@ +using Autumn.Domain.Models; +using Autumn.Infrastructure.Interface; +using Autumn.Service.Interface; +using System.Threading.Tasks; + +namespace Autumn.Service +{ + public class CountryService : BaseService, ICountryService + { + protected readonly ICountryRepository _repository; + + public CountryService(ICountryRepository repository) : base(repository) + { + _repository = repository; + } + + public async Task GetByCodeAsync(string code) => + await _repository.GetByCodeAsync(code); + } +} diff --git a/Autumn.BL/Services/CustomsTariffService.cs b/Autumn.BL/Services/CustomsTariffService.cs index 20efeca..78ac947 100644 --- a/Autumn.BL/Services/CustomsTariffService.cs +++ b/Autumn.BL/Services/CustomsTariffService.cs @@ -18,5 +18,10 @@ public async Task GetByHSCodeAsync(string hscode) => await _repository.GetByHSCodeAsync(hscode); public async Task> GetByHeaderAsync(string header) => await _repository.GetByHeaderAsync(header); + + public async Task GetByHSCodeAndCountryAsync(string hscode, string country) => + await _repository.GetByHSCodeAndCountryAsync(hscode, country); + public async Task> GetByHeaderAndCountryAsync(string header, string country) => + await _repository.GetByHeaderAndCountryAsync(header, country); } } diff --git a/Autumn.BL/Services/HsCodeService.cs b/Autumn.BL/Services/HsCodeService.cs index 3fc08aa..06075b1 100644 --- a/Autumn.BL/Services/HsCodeService.cs +++ b/Autumn.BL/Services/HsCodeService.cs @@ -19,5 +19,8 @@ public async Task> GetWithOptionsAsync(string id, string parentId, public async Task> GetWithHSCodeOptionsAsync(string code, string parentCode, string level) => await _hsCodeRepository.GetWithHSCodeOptionsAsync(code, parentCode, level); + public async Task> SearchByDescriptionAsync(string keyword, int limit = 20) => + await _hsCodeRepository.SearchByDescriptionAsync(keyword, limit); + } } diff --git a/Autumn.BL/Services/ProductService.cs b/Autumn.BL/Services/ProductService.cs index 7f4a484..881a42f 100644 --- a/Autumn.BL/Services/ProductService.cs +++ b/Autumn.BL/Services/ProductService.cs @@ -24,5 +24,8 @@ public async Task> GetByKeywordAsync(string keyword) => public async Task> GetLikeKeywordAsync(string keyword) => await _productRepository.GetLikeKeywordAsync(keyword); + public async Task> SearchByKeywordAsync(string keyword, int limit = 20) => + await _productRepository.SearchByKeywordAsync(keyword, limit); + } } diff --git a/Autumn.Domain/Autumn.Domain.csproj b/Autumn.Domain/Autumn.Domain.csproj index 44b56dd..b20615b 100644 --- a/Autumn.Domain/Autumn.Domain.csproj +++ b/Autumn.Domain/Autumn.Domain.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 diff --git a/Autumn.Domain/Models/Country.cs b/Autumn.Domain/Models/Country.cs new file mode 100644 index 0000000..9ab003b --- /dev/null +++ b/Autumn.Domain/Models/Country.cs @@ -0,0 +1,17 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Autumn.Domain.Models +{ + public class Country + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + public string Code { get; set; } + public string Name { get; set; } + public string Flag { get; set; } + public string Currency { get; set; } + public string Symbol { get; set; } + } +} diff --git a/Autumn.Domain/Models/CustomsTariff.cs b/Autumn.Domain/Models/CustomsTariff.cs index 6818b91..14abac4 100644 --- a/Autumn.Domain/Models/CustomsTariff.cs +++ b/Autumn.Domain/Models/CustomsTariff.cs @@ -7,20 +7,30 @@ namespace Autumn.Domain.Models { + [BsonIgnoreExtraElements] public class CustomsTariff { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } + public string Country { get; set; } public string Header { get; set; } public string HSCode { get; set; } public string Description { get; set; } + // Common public string DUTY { get; set; } - public string LEVY { get; set; } public string VAT { get; set; } + public string LEVY { get; set; } + // Nigeria public string NAC { get; set; } public string SUR { get; set; } public string ETLS { get; set; } public string CISS { get; set; } + // Ghana + public string NHIL { get; set; } + public string GETFUND { get; set; } + // Kenya + public string IDF { get; set; } + public string RDF { get; set; } } } diff --git a/Autumn.Domain/Models/Document.cs b/Autumn.Domain/Models/Document.cs index 05fa50c..db3ebf6 100644 --- a/Autumn.Domain/Models/Document.cs +++ b/Autumn.Domain/Models/Document.cs @@ -5,6 +5,7 @@ namespace Autumn.Domain.Models { + [BsonIgnoreExtraElements] public partial class Document { diff --git a/Autumn.Domain/Models/SearchLog.cs b/Autumn.Domain/Models/SearchLog.cs index 2eb0b95..5474805 100644 --- a/Autumn.Domain/Models/SearchLog.cs +++ b/Autumn.Domain/Models/SearchLog.cs @@ -13,6 +13,8 @@ public class SearchLog public string Prediction { get; set; } public double Rating { get; set; } public double Threshold { get; set; } + public string Source { get; set; } + public bool FoundInDb { get; set; } public DateTime Created { get; set; } } diff --git a/Autumn.Domain/Models/StoreDatabaseSettings.cs b/Autumn.Domain/Models/StoreDatabaseSettings.cs index ffc32dc..ae47838 100644 --- a/Autumn.Domain/Models/StoreDatabaseSettings.cs +++ b/Autumn.Domain/Models/StoreDatabaseSettings.cs @@ -15,6 +15,7 @@ public class StoreDatabaseSettings : IStoreDatabaseSettings public string CustomsTariffStoreCollectionName { get; set; } public string HSCodeToDocumentStoreCollectionName { get; set; } public string RequirementStoreCollectionName { get; set; } + public string CountryStoreCollectionName { get; set; } public string ConnectionString { get; set; } public string DatabaseName { get; set; } } @@ -32,6 +33,7 @@ public interface IStoreDatabaseSettings string CustomsTariffStoreCollectionName { get; set; } string HSCodeToDocumentStoreCollectionName { get; set; } string RequirementStoreCollectionName { get; set; } + string CountryStoreCollectionName { get; set; } string ConnectionString { get; set; } string DatabaseName { get; set; } } diff --git a/Autumn.Repository/Autumn.Infrastructure.csproj b/Autumn.Repository/Autumn.Infrastructure.csproj index 8c16921..2b0ac37 100644 --- a/Autumn.Repository/Autumn.Infrastructure.csproj +++ b/Autumn.Repository/Autumn.Infrastructure.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/Autumn.Repository/DependencyInjection.cs b/Autumn.Repository/DependencyInjection.cs index fc91519..d1f0687 100644 --- a/Autumn.Repository/DependencyInjection.cs +++ b/Autumn.Repository/DependencyInjection.cs @@ -22,6 +22,7 @@ public static void AddRepositoryServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } diff --git a/Autumn.Repository/Interface/ICountryRepository.cs b/Autumn.Repository/Interface/ICountryRepository.cs new file mode 100644 index 0000000..3bdca32 --- /dev/null +++ b/Autumn.Repository/Interface/ICountryRepository.cs @@ -0,0 +1,9 @@ +using Autumn.Domain.Models; + +namespace Autumn.Infrastructure.Interface +{ + public interface ICountryRepository : IBaseRepository + { + Task GetByCodeAsync(string code); + } +} diff --git a/Autumn.Repository/Interface/ICustomsTariffRepository.cs b/Autumn.Repository/Interface/ICustomsTariffRepository.cs index 1709227..da9a6bf 100644 --- a/Autumn.Repository/Interface/ICustomsTariffRepository.cs +++ b/Autumn.Repository/Interface/ICustomsTariffRepository.cs @@ -6,5 +6,7 @@ public interface ICustomsTariffRepository : IBaseRepository { Task> GetByHeaderAsync(string header); Task GetByHSCodeAsync(string hscode); + Task GetByHSCodeAndCountryAsync(string hscode, string country); + Task> GetByHeaderAndCountryAsync(string header, string country); } } diff --git a/Autumn.Repository/Interface/IHsCodeRepository.cs b/Autumn.Repository/Interface/IHsCodeRepository.cs index 2ae0b65..34d24f1 100644 --- a/Autumn.Repository/Interface/IHsCodeRepository.cs +++ b/Autumn.Repository/Interface/IHsCodeRepository.cs @@ -6,5 +6,6 @@ public interface IHsCodeRepository : IBaseRepository { Task> GetWithHSCodeOptionsAsync(string code, string parentCode, string level); Task> GetWithOptionsAsync(string id, string parentId, string level); + Task> SearchByDescriptionAsync(string keyword, int limit = 20); } } diff --git a/Autumn.Repository/Interface/IProductRepository.cs b/Autumn.Repository/Interface/IProductRepository.cs index dc45ed6..b685a85 100644 --- a/Autumn.Repository/Interface/IProductRepository.cs +++ b/Autumn.Repository/Interface/IProductRepository.cs @@ -8,5 +8,6 @@ public interface IProductRepository : IBaseRepository Task> GetByKeywordAsync(string keyword); Task> GetByTagsAsync(string tag); Task> GetLikeKeywordAsync(string keyword); + Task> SearchByKeywordAsync(string keyword, int limit = 20); } } diff --git a/Autumn.Repository/Repository/CountryRepository.cs b/Autumn.Repository/Repository/CountryRepository.cs new file mode 100644 index 0000000..4a9fd1b --- /dev/null +++ b/Autumn.Repository/Repository/CountryRepository.cs @@ -0,0 +1,17 @@ +using Autumn.Domain.Models; +using Autumn.Infrastructure.Interface; +using MongoDB.Driver; + +namespace Autumn.Infrastructure.Repository +{ + public class CountryRepository : BaseRepository, ICountryRepository + { + public CountryRepository(IStoreDatabaseSettings settings) + : base(settings, settings.CountryStoreCollectionName) + { + } + + public async Task GetByCodeAsync(string code) => + await _collection.Find(x => x.Code == code).FirstOrDefaultAsync(); + } +} diff --git a/Autumn.Repository/Repository/CustomsTariffRepository.cs b/Autumn.Repository/Repository/CustomsTariffRepository.cs index 8f67e45..4a4607a 100644 --- a/Autumn.Repository/Repository/CustomsTariffRepository.cs +++ b/Autumn.Repository/Repository/CustomsTariffRepository.cs @@ -14,5 +14,15 @@ public async Task GetByHSCodeAsync(string hscode) => await _collection.Find(x => x.HSCode == hscode).FirstOrDefaultAsync(); public async Task> GetByHeaderAsync(string header) => await _collection.Find(x => x.Header == header).ToListAsync(); + + public async Task GetByHSCodeAndCountryAsync(string hscode, string country) => + await _collection.Find(x => + x.HSCode == hscode && (x.Country == country || (x.Country == null && country == "NG")) + ).FirstOrDefaultAsync(); + + public async Task> GetByHeaderAndCountryAsync(string header, string country) => + await _collection.Find(x => + x.Header == header && (x.Country == country || (x.Country == null && country == "NG")) + ).ToListAsync(); } } diff --git a/Autumn.Repository/Repository/HsCodeRepository.cs b/Autumn.Repository/Repository/HsCodeRepository.cs index d9b5c65..606340d 100644 --- a/Autumn.Repository/Repository/HsCodeRepository.cs +++ b/Autumn.Repository/Repository/HsCodeRepository.cs @@ -1,5 +1,6 @@ using Autumn.Domain.Models; using Autumn.Infrastructure.Interface; +using MongoDB.Bson; using MongoDB.Driver; namespace Autumn.Infrastructure.Repository @@ -64,6 +65,71 @@ public async Task> GetWithHSCodeOptionsAsync(string code, string pa return resp.OrderBy(x => x.Order).ToList(); } + /// + /// Atlas Search with fuzzy matching on Description field, filtered to levels 3 & 4. + /// Requires an Atlas Search index named "default" on the hscodes collection. + /// Falls back to tokenized regex if Atlas Search is unavailable. + /// + public async Task> SearchByDescriptionAsync(string keyword, int limit = 20) + { + if (string.IsNullOrEmpty(keyword?.Trim())) + return new List(); + + // Try Atlas Search first + try + { + var searchStage = new BsonDocument("$search", new BsonDocument + { + { "index", "hscodes-index" }, + { "compound", new BsonDocument + { + { "must", new BsonArray + { + new BsonDocument("text", new BsonDocument + { + { "query", keyword }, + { "path", "Description" }, + { "fuzzy", new BsonDocument { { "maxEdits", 1 }, { "prefixLength", 2 } } } + }) + } + }, + { "filter", new BsonArray + { + new BsonDocument("range", new BsonDocument + { + { "path", "Level" }, + { "gte", 3 }, + { "lte", 4 } + }) + } + } + } + } + }); + var limitStage = new BsonDocument("$limit", limit); + + var pipeline = PipelineDefinition.Create(searchStage, limitStage); + var results = await _collection.Aggregate(pipeline).ToListAsync(); + if (results.Count > 0) + return results; + } + catch + { + // Atlas Search not available — fall through to regex + } + + // Fallback: tokenized regex (split query into words, match all in any order) + var words = keyword.Trim().Split(new[] { ' ', '-', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(w => System.Text.RegularExpressions.Regex.Escape(w)); + var pattern = string.Join("", words.Select(w => $"(?=.*{w})")) + ".*"; + + var filter = Builders.Filter.And( + Builders.Filter.Regex(x => x.Description, new BsonRegularExpression(pattern, "i")), + Builders.Filter.In(x => x.Level, new long[] { 3, 4 }) + ); + return await _collection.Find(filter).Limit(limit).SortBy(x => x.Level).ToListAsync(); + } + // Override methods for specific operations if necessary public override async Task InsertOneAsync(HSCode x) { diff --git a/Autumn.Repository/Repository/ProductRepository.cs b/Autumn.Repository/Repository/ProductRepository.cs index f919d43..8cfaff6 100644 --- a/Autumn.Repository/Repository/ProductRepository.cs +++ b/Autumn.Repository/Repository/ProductRepository.cs @@ -1,5 +1,7 @@ -using Autumn.Domain.Models; +using System.Text.RegularExpressions; +using Autumn.Domain.Models; using Autumn.Infrastructure.Interface; +using MongoDB.Bson; using MongoDB.Driver; namespace Autumn.Infrastructure.Repository @@ -15,12 +17,9 @@ public async Task> GetByTagsAsync(string tag) { if (string.IsNullOrEmpty(tag)) return await _collection.Find(x => x.Tags != null).ToListAsync(); - //return await base.GetAsync(); else return await _collection.Find(x => x.Tags.Contains(tag)).ToListAsync(); } - // public List GetByKeyword(string keyword) => - // _collection.Find(x => x.Keyword == keyword).ToList(); public async Task> GetByKeywordAsync(string keyword) => await _collection.Find(x => x.Keyword.ToLower() == keyword.ToLower()).ToListAsync(); @@ -29,7 +28,6 @@ public async Task> GetLikeKeywordAsync(string keyword) { if (string.IsNullOrEmpty(keyword)) { - //return await _collection.Find(x => true).ToListAsync(); return await base.GetAsync(); } else @@ -38,6 +36,53 @@ public async Task> GetLikeKeywordAsync(string keyword) } } + /// + /// Atlas Search with fuzzy matching on the Keyword field. + /// Requires an Atlas Search index named "default" on the products collection. + /// Falls back to tokenized regex if Atlas Search is unavailable. + /// + public async Task> SearchByKeywordAsync(string keyword, int limit = 20) + { + if (string.IsNullOrEmpty(keyword?.Trim())) + return new List(); + + // Try Atlas Search first (word splitting + fuzzy matching) + try + { + var searchStage = new BsonDocument("$search", new BsonDocument + { + { "index", "product-index" }, + { "text", new BsonDocument + { + { "query", keyword }, + { "path", "Keyword" }, + { "fuzzy", new BsonDocument { { "maxEdits", 1 }, { "prefixLength", 2 } } } + } + } + }); + var limitStage = new BsonDocument("$limit", limit); + + var pipeline = PipelineDefinition.Create(searchStage, limitStage); + var results = await _collection.Aggregate(pipeline).ToListAsync(); + if (results.Count > 0) + return results; + } + catch + { + // Atlas Search not available — fall through to regex + } + + // Fallback: tokenized regex (split query into words, match all in any order) + var words = keyword.Trim().Split(new[] { ' ', '-', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(w => Regex.Escape(w)); + var pattern = string.Join("", words.Select(w => $"(?=.*{w})")) + ".*"; + + var filter = Builders.Filter.Regex( + x => x.Keyword, + new BsonRegularExpression(pattern, "i")); + return await _collection.Find(filter).Limit(limit).ToListAsync(); + } + public override async Task CreateAsync(Product entity) { var now = DateTime.Now; diff --git a/Autumn.SPA/.gitignore b/Autumn.SPA/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/Autumn.SPA/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Autumn.SPA/README.md b/Autumn.SPA/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/Autumn.SPA/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/Autumn.SPA/eslint.config.js b/Autumn.SPA/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/Autumn.SPA/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/Autumn.SPA/index.html b/Autumn.SPA/index.html new file mode 100644 index 0000000..9be6324 --- /dev/null +++ b/Autumn.SPA/index.html @@ -0,0 +1,14 @@ + + + + + + + + HS.Codes + + +
+ + + diff --git a/Autumn.SPA/package-lock.json b/Autumn.SPA/package-lock.json new file mode 100644 index 0000000..6b8a00c --- /dev/null +++ b/Autumn.SPA/package-lock.json @@ -0,0 +1,3441 @@ +{ + "name": "autumn-spa", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "autumn-spa", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "lucide-react": "^0.564.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.564.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.564.0.tgz", + "integrity": "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/Autumn.SPA/package.json b/Autumn.SPA/package.json new file mode 100644 index 0000000..1787163 --- /dev/null +++ b/Autumn.SPA/package.json @@ -0,0 +1,30 @@ +{ + "name": "autumn-spa", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "lucide-react": "^0.564.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } +} diff --git a/Autumn.SPA/public/favicon.svg b/Autumn.SPA/public/favicon.svg new file mode 100644 index 0000000..6febc41 --- /dev/null +++ b/Autumn.SPA/public/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Autumn.SPA/public/vite.svg b/Autumn.SPA/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/Autumn.SPA/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Autumn.SPA/src/App.jsx b/Autumn.SPA/src/App.jsx new file mode 100644 index 0000000..8be8a45 --- /dev/null +++ b/Autumn.SPA/src/App.jsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from "react"; +import { api } from "./api"; +import Header from "./components/Header"; +import Home from "./components/Home"; +import SearchView from "./components/SearchView"; +import CalcView from "./components/CalcView"; +import BrowseView from "./components/BrowseView"; +import Toast from "./components/Toast"; + +export default function App() { + const [mode, setMode] = useState("light"); + const [view, setView] = useState("home"); + const [query, setQuery] = useState(""); + const [country, setCountry] = useState("NG"); + const [countries, setCountries] = useState([]); + // Cross-view state for calculator prefill + const [calcInit, setCalcInit] = useState({ hscode: "", product: "" }); + + useEffect(() => { + document.documentElement.classList.toggle("dark", mode === "dark"); + }, [mode]); + + useEffect(() => { + api.countries().then((resp) => { + if (resp.success && resp.records) { + setCountries(resp.records); + } + }).catch(() => {}); + }, []); + + const onReset = () => { + setView("home"); + setQuery(""); + }; + + // Navigate from SearchView → CalcView with HS code prefilled + const goToCalc = (hscode, product) => { + setCalcInit({ hscode: hscode || "", product: product || "" }); + setView("calculator"); + }; + + // Navigate from CalcView → SearchView to find an HS code + const goToSearch = (product) => { + setQuery(product || ""); + setView("results"); + }; + + return ( +
+
+ + {view === "home" && ( + {}} + onNavigate={setView} + /> + )} + + {view === "results" && ( + + )} + + {view === "calculator" && ( + + )} + + {view === "browse" && ( + + )} + + + +
+ © 2025 HS.Codes — Open source commodity classification. + GitHub → +
+
+ ); +} diff --git a/Autumn.SPA/src/api.js b/Autumn.SPA/src/api.js new file mode 100644 index 0000000..e1d27dc --- /dev/null +++ b/Autumn.SPA/src/api.js @@ -0,0 +1,42 @@ +const BASE = '/api'; + +const json = async (r) => { + if (r.status === 429) { + window.dispatchEvent(new CustomEvent('api:ratelimit')); + throw new Error('Too many requests. Please wait a moment and try again.'); + } + return r.json(); +}; + +export const api = { + search: (keyword) => + fetch(`${BASE}/search?keyword=${encodeURIComponent(keyword)}`).then(json), + + browse: ({ code, parentCode, parentId, level } = {}) => { + const params = new URLSearchParams(); + if (code) params.set('code', code); + if (parentCode) params.set('parentCode', parentCode); + if (parentId) params.set('parentId', parentId); + if (level != null) params.set('level', String(level)); + return fetch(`${BASE}/browse?${params}`).then(json); + }, + + duty: ({ HSCode, Country, ProductDesc, Cost, Freight, Insurance, Currency }) => + fetch(`${BASE}/duty?${new URLSearchParams({ + HSCode, Country, ProductDesc: ProductDesc || '', + Cost: String(Cost), Freight: String(Freight), + Insurance: String(Insurance), Currency: Currency || 'USD' + })}`).then(json), + + note: (hscode, country) => + fetch(`${BASE}/note/${encodeURIComponent(hscode)}?country=${encodeURIComponent(country)}`).then(json), + + countries: () => + fetch(`${BASE}/codelist/countries`).then(json), + + currencies: () => + fetch(`${BASE}/codelist/currency`).then(json), + + products: (query) => + fetch(`${BASE}/codelist/products/${encodeURIComponent(query || '')}`).then(json), +}; diff --git a/Autumn.SPA/src/app.css b/Autumn.SPA/src/app.css new file mode 100644 index 0000000..b0d927f --- /dev/null +++ b/Autumn.SPA/src/app.css @@ -0,0 +1,118 @@ +@import "tailwindcss"; + +/* ===== TAILWIND THEME EXTENSIONS ===== */ +@theme { + --font-sans: 'DM Sans', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + + --color-bg: var(--theme-bg); + --color-surface: var(--theme-surface); + --color-surface2: var(--theme-surface2); + --color-surface3: var(--theme-surface3); + --color-border: var(--theme-border); + --color-border-hover: var(--theme-border-hover); + --color-fg: var(--theme-text); + --color-fg-sec: var(--theme-text-sec); + --color-fg-dim: var(--theme-text-dim); + --color-accent: var(--theme-accent); + --color-accent-hover: var(--theme-accent-hover); + --color-accent-dim: var(--theme-accent-dim); + --color-accent-border: var(--theme-accent-border); + --color-success: var(--theme-success); + --color-success-dim: var(--theme-success-dim); + --color-info: var(--theme-info); + --color-info-dim: var(--theme-info-dim); + --color-input-bg: var(--theme-input-bg); + --color-header-bg: var(--theme-header-bg); + --color-danger: var(--theme-danger); + --color-btn-text: var(--theme-btn-text); + --color-logo-glow: var(--theme-logo-glow); + --color-warning: var(--theme-warning); + --color-warning-dim: var(--theme-warning-dim); + + --animate-fade-up: fade-up 0.4s ease; + --animate-fade-up-fast: fade-up 0.25s ease; + --animate-spin-loading: spin-loading 0.7s linear infinite; +} + +/* ===== LIGHT MODE (default) ===== */ +:root { + --theme-bg: #FFFFFF; + --theme-surface: #FFFFFF; + --theme-surface2: #F4F7F5; + --theme-surface3: #E9EFEC; + --theme-border: #DBE4DF; + --theme-border-hover: #C2D0C8; + --theme-text: #111110; + --theme-text-sec: #3D4A42; + --theme-text-dim: #728070; + --theme-accent: #059669; + --theme-accent-hover: #06B47D; + --theme-accent-dim: rgba(5,150,105,0.08); + --theme-accent-border: rgba(5,150,105,0.22); + --theme-success: #059669; + --theme-success-dim: rgba(5,150,105,0.10); + --theme-info: #2563EB; + --theme-info-dim: rgba(37,99,235,0.10); + --theme-input-bg: #F9FBF9; + --theme-header-bg: rgba(255,255,255,0.92); + --theme-grad: linear-gradient(135deg,#059669,#047857); + --theme-danger: #DC3545; + --theme-btn-text: #FFFFFF; + --theme-logo-glow: rgba(5,150,105,0.18); + --theme-warning: #D97706; + --theme-warning-dim: rgba(217,119,6,0.08); +} + +/* ===== DARK MODE ===== */ +.dark { + --theme-bg: #0A0A0A; + --theme-surface: #111111; + --theme-surface2: #181818; + --theme-surface3: #1F1F1F; + --theme-border: #262626; + --theme-border-hover: #333333; + --theme-text: #E8E6E1; + --theme-text-sec: #9CA3AF; + --theme-text-dim: #6B7280; + --theme-accent: #C9A84C; + --theme-accent-hover: #DBBD64; + --theme-accent-dim: rgba(201,168,76,0.10); + --theme-accent-border: rgba(201,168,76,0.25); + --theme-success: #4EA87A; + --theme-success-dim: rgba(78,168,122,0.12); + --theme-info: #5B8DD9; + --theme-info-dim: rgba(91,141,217,0.12); + --theme-input-bg: #131313; + --theme-header-bg: rgba(10,10,10,0.88); + --theme-grad: linear-gradient(135deg,#C9A84C,#A08535); + --theme-danger: #E05A4E; + --theme-btn-text: #0A0A0A; + --theme-logo-glow: rgba(201,168,76,0.20); + --theme-warning: #E09840; + --theme-warning-dim: rgba(224,152,64,0.12); +} + +/* ===== KEYFRAMES ===== */ +@keyframes fade-up { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes spin-loading { + to { transform: rotate(360deg); } +} + +@keyframes slideIn { + from { opacity: 0; transform: translateX(100%); } + to { opacity: 1; transform: translateX(0); } +} + +/* ===== GLOBAL ===== */ +@layer base { + * { box-sizing: border-box; margin: 0; } + html, body, #root { width: 100%; min-height: 100vh; } + ::selection { background: var(--theme-accent-dim); } +} + +.bg-grad { background: var(--theme-grad); } diff --git a/Autumn.SPA/src/assets/react.svg b/Autumn.SPA/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/Autumn.SPA/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Autumn.SPA/src/components/Badge.jsx b/Autumn.SPA/src/components/Badge.jsx new file mode 100644 index 0000000..dd72a99 --- /dev/null +++ b/Autumn.SPA/src/components/Badge.jsx @@ -0,0 +1,16 @@ +const variants = { + default: "bg-accent-dim text-accent", + ai: "bg-[rgba(139,92,246,0.12)] text-[#A78BFA]", + match: "bg-success-dim text-success", + synonym: "bg-info-dim text-info", +}; + +export default function Badge({ children, variant = "default", small }) { + return ( + + {children} + + ); +} diff --git a/Autumn.SPA/src/components/BrowseView.jsx b/Autumn.SPA/src/components/BrowseView.jsx new file mode 100644 index 0000000..1f4800f --- /dev/null +++ b/Autumn.SPA/src/components/BrowseView.jsx @@ -0,0 +1,216 @@ +import { useState, useEffect } from "react"; +import { ChevronRight, Package } from "lucide-react"; +import { api } from "../api"; +import { SECTIONS, ICON_MAP } from "../data/sections"; +import TariffPrompt from "./TariffPrompt"; + +const RATE_LABELS = { + duty: "Import Duty", vat: "VAT", levy: "Levy", sur: "Surcharge", + etls: "ETL", ciss: "CISS", nac: "NAC", nhil: "NHIL", + getfund: "GETFund", idf: "IDF", rdf: "RDF", +}; +const RATE_FIELDS = ["duty", "vat", "levy", "sur", "etls", "ciss", "nac", "nhil", "getfund", "idf", "rdf"]; + +function formatTariffEntries(tariff) { + if (!tariff) return []; + return RATE_FIELDS + .filter(f => tariff[f] && tariff[f] !== "0") + .map(f => [RATE_LABELS[f] || f, `${tariff[f]}%`]); +} + +export default function BrowseView({ country, countries, setCountry }) { + // path entries: { code, description, id, level, childLevel } + const [path, setPath] = useState([]); + const [children, setChildren] = useState(null); + const [leafDetail, setLeafDetail] = useState(null); + const [loading, setLoading] = useState(false); + + const isRoot = path.length === 0; + const selCountry = countries.find(c => c.code === country); + + const loadChildren = async (parentId, parentCode, level) => { + setLoading(true); setChildren(null); setLeafDetail(null); + try { + const params = parentId ? { parentId, level } : { parentCode, level }; + const resp = await api.browse(params); + if (resp.success && resp.records?.length > 0) { + setChildren(resp.records.sort((a, b) => a.code.localeCompare(b.code))); + } else { + setChildren(null); + // No children — it's a leaf, load tariff + const code = parentCode || path[path.length - 1]?.code; + if (code) { + const noteResp = await api.note(code, country || "NG"); + if (noteResp.success) setLeafDetail(noteResp); + } + } + } catch { + setChildren(null); + } + setLoading(false); + }; + + const navigateSection = (section) => { + const entry = { code: section.parentCode, description: `Section ${section.code} — ${section.title}`, id: null, level: 1, childLevel: 2 }; + setPath([entry]); + loadChildren(null, section.parentCode, 2); + }; + + const navigateTo = async (child) => { + const nextLevel = child.level + 1; + const entry = { code: child.code, description: child.description, id: child.id, level: child.level, childLevel: nextLevel }; + setPath(prev => [...prev, entry]); + setLoading(true); setChildren(null); setLeafDetail(null); + try { + const resp = await api.browse({ parentId: child.id, level: nextLevel }); + if (resp.success && resp.records?.length > 0) { + setChildren(resp.records.sort((a, b) => a.code.localeCompare(b.code))); + } else { + const noteResp = await api.note(child.code, country || "NG"); + if (noteResp.success) setLeafDetail(noteResp); + } + } catch { + const noteResp = await api.note(child.code, country || "NG").catch(() => null); + if (noteResp?.success) setLeafDetail(noteResp); + } + setLoading(false); + }; + + const browseToLevel = (idx) => { + if (idx < 0) { + setPath([]); setChildren(null); setLeafDetail(null); + return; + } + const newPath = path.slice(0, idx + 1); + setPath(newPath); + const target = newPath[newPath.length - 1]; + loadChildren(target.id, target.id ? null : target.code, target.childLevel); + }; + + useEffect(() => { + if (leafDetail && country && path.length > 0) { + const code = path[path.length - 1].code; + api.note(code, country).then(resp => { if (resp.success) setLeafDetail(resp); }).catch(() => {}); + } + }, [country]); + + const leafTariffEntries = leafDetail?.tariff?.[0] ? formatTariffEntries(leafDetail.tariff[0]) : []; + + return ( +
+

Browse Harmonized System

+

Navigate the WCO Harmonized System nomenclature.

+ + {/* Root grid */} + {isRoot && !loading && ( + <> +
+ {SECTIONS.map((s, i) => { + const Icon = ICON_MAP[s.code] || Package; + return ( + + ); + })} +
+

+ Try: Machinery & Electrical Equipment → Chapter 84 → 8471 → 8471.30 +

+ + )} + + {/* Hierarchy tree + children / leaf */} + {!isRoot && ( +
+
Navigation
+ + {/* "All Sections" root link */} + + + {/* Path hierarchy rows */} +
+ {path.map((entry, idx) => { + const isLast = idx === path.length - 1; + const isLeaf = isLast && leafDetail && !children; + const isClickable = !isLast; + return ( + + ); + })} +
+ + {/* Loading */} + {loading && ( +
+
+

Loading…

+
+ )} + + {/* Child items below the hierarchy */} + {children && !loading && ( +
+ {children.length === 0 ? ( +
No sub-items found.
+ ) : children.map((child, ci) => ( + + ))} +
+ )} + + {/* Leaf detail — tariff */} + {leafDetail && !loading && ( +
+ {leafDetail.records?.[0]?.description && ( +

{leafDetail.records[0].description}

+ )} +
+ Applicable Tariffs {selCountry && — {selCountry.flag} {selCountry.name}} +
+ {!country ? ( + + ) : country !== "NG" ? ( +
Tariff data for {selCountry?.name || country} coming soon.
+ ) : leafTariffEntries.length > 0 ? ( +
+ {leafTariffEntries.map(([l, v], ti) => ( +
+
{l}
+
{v}
+
+ ))} +
+ ) : ( +
No tariff data available for this code.
+ )} +
+ )} +
+ )} +
+ ); +} diff --git a/Autumn.SPA/src/components/CalcView.jsx b/Autumn.SPA/src/components/CalcView.jsx new file mode 100644 index 0000000..56150a9 --- /dev/null +++ b/Autumn.SPA/src/components/CalcView.jsx @@ -0,0 +1,230 @@ +import { useState } from "react"; +import { Calculator, Search } from "lucide-react"; +import { api } from "../api"; +import TariffPrompt from "./TariffPrompt"; + +const labelCls = "block text-[11px] font-bold text-fg-dim mb-1 uppercase tracking-[0.05em]"; +const inputCls = "w-full px-3 py-2.5 rounded-lg border border-border bg-input-bg text-fg text-sm outline-none font-sans transition-colors duration-150 focus:border-accent"; + +// Strip commas to get raw number string +const rawNum = (v) => v.replace(/,/g, ""); + +// Format a number string with commas and 2 decimals +const fmtInput = (v) => { + const n = parseFloat(rawNum(v)); + if (isNaN(n)) return ""; + return n.toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +}; + +export default function CalcView({ country, countries, setCountry, onSearchCode, initialHscode, initialProduct }) { + const [calc, setCalc] = useState({ + product: initialProduct || "", + hscode: initialHscode || "", + cost: "", + freight: "", + insurance: "", + }); + // Track which money field is focused (show raw value while editing) + const [focused, setFocused] = useState(null); + const [calcOut, setCalcOut] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const selCountry = countries.find(c => c.code === country); + const currency = selCountry?.currency?.split(" ")[0] || "NGN"; + + const parseVal = (k) => parseFloat(rawNum(calc[k])) || 0; + + const doCalc = (e) => { + e.preventDefault(); + if (!country || !calc.hscode) return; + setBusy(true); + setError(null); + api.duty({ + HSCode: calc.hscode, + Country: country, + ProductDesc: calc.product, + Cost: parseVal("cost"), + Freight: parseVal("freight"), + Insurance: parseVal("insurance"), + Currency: currency, + }).then((resp) => { + if (resp.success) { + setCalcOut(resp); + } else { + setError(resp.error?.[0] || "Calculation failed"); + setCalcOut(null); + } + }).catch((err) => { + setError(err.message || "Network error"); + setCalcOut(null); + }).finally(() => setBusy(false)); + }; + + const fmt = (n) => currency + " " + Number(n).toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + + const canSubmit = country && country === "NG"; + + const update = (k, v) => setCalc(prev => ({ ...prev, [k]: v })); + + // Allow only digits, one decimal point, and commas while typing + const handleMoneyChange = (k, raw) => { + const cleaned = raw.replace(/[^0-9.,]/g, ""); + update(k, cleaned); + }; + + // On blur: format with commas and 2 decimals, clamp to >= 0 + const handleMoneyBlur = (k) => { + setFocused(null); + const n = parseFloat(rawNum(calc[k])); + if (isNaN(n) || n <= 0) { + update(k, ""); + } else { + update(k, fmtInput(calc[k])); + } + }; + + // On focus: show raw number (no commas) for easy editing + const handleMoneyFocus = (k) => { + setFocused(k); + const n = parseFloat(rawNum(calc[k])); + if (!isNaN(n) && n > 0) { + update(k, String(n)); + } + }; + + // Display value: formatted when not focused, raw when focused + const moneyVal = (k) => focused === k ? calc[k] : (calc[k] ? fmtInput(calc[k]) : ""); + + return ( +
+

Import Duty Calculator

+

Estimate total import duties, taxes, and levies payable.

+ + {!country &&
} + +
+
+ {country && ( +
+ {selCountry?.flag} +
+
{selCountry?.name}
+
{currency}
+
+ +
+ )} + +
+ + update("product", e.target.value)} className={inputCls} /> +
+ +
+
+ + {onSearchCode && ( + + )} +
+ update("hscode", e.target.value)} className={inputCls} /> +
+ +
+ + handleMoneyChange("cost", e.target.value)} + onFocus={() => handleMoneyFocus("cost")} + onBlur={() => handleMoneyBlur("cost")} + className={inputCls} /> +
+ +
+ + handleMoneyChange("freight", e.target.value)} + onFocus={() => handleMoneyFocus("freight")} + onBlur={() => handleMoneyBlur("freight")} + className={inputCls} /> +
+ +
+ + handleMoneyChange("insurance", e.target.value)} + onFocus={() => handleMoneyFocus("insurance")} + onBlur={() => handleMoneyBlur("insurance")} + className={inputCls} /> +
+ + +
+ +
+ {error && ( +
+ {error} +
+ )} + {!calcOut && !error ? ( +
+ +

{!country ? "Select a country to get started" : country !== "NG" ? `Duty calculator for ${selCountry?.name || country} coming soon.` : "Fill in the form and click Calculate"}

+
+ ) : calcOut && ( +
+
+ {selCountry?.flag} +
+
{selCountry?.name} Import Duties
+
HS Code: {calcOut.hsCode}
+
+
+ {calcOut.hsCodeDescription && ( +
+ {calcOut.hsCodeDescription} +
+ )} +
CIF Summary ({currency})
+
+ {[ + ["Cost", fmt(calcOut.cost)], + ["Freight", fmt(calcOut.freight)], + ["Insurance", fmt(calcOut.insurance)], + ["CIF Total", fmt(calcOut.cif)], + ].map(([l, v], i) => ( +
+
{l}
+
{v}
+
+ ))} +
+
Duties, Taxes & Levies ({currency})
+
+ {(calcOut.breakdown || []).map((b, i) => ( +
+ {b.label} ({b.rate}%) + {fmt(b.amount)} +
+ ))} +
+
+ Total Duties & Taxes + {fmt(calcOut.totalDuty)} +
+

Disclaimer: Values are estimates only. Final duties determined by Customs at port of clearance.

+
+ )} +
+
+
+ ); +} diff --git a/Autumn.SPA/src/components/ConfBar.jsx b/Autumn.SPA/src/components/ConfBar.jsx new file mode 100644 index 0000000..c85e262 --- /dev/null +++ b/Autumn.SPA/src/components/ConfBar.jsx @@ -0,0 +1,14 @@ +export default function ConfBar({ v }) { + const pct = Math.round(v * 100); + const barColor = pct >= 85 ? 'bg-success' : pct >= 60 ? 'bg-accent' : 'bg-[#e8a820]'; + const textColor = pct >= 85 ? 'text-success' : pct >= 60 ? 'text-accent' : 'text-[#e8a820]'; + return ( +
+
+
+
+ {pct}% +
+ ); +} diff --git a/Autumn.SPA/src/components/CountrySelector.jsx b/Autumn.SPA/src/components/CountrySelector.jsx new file mode 100644 index 0000000..8b04ba8 --- /dev/null +++ b/Autumn.SPA/src/components/CountrySelector.jsx @@ -0,0 +1,52 @@ +import { useState, useRef, useEffect } from "react"; +import { Globe, ChevronDown } from "lucide-react"; + +export default function CountrySelector({ value, onChange, countries = [], compact }) { + const [open, setOpen] = useState(false); + const selected = countries.find(c => c.code === value); + const wrapRef = useRef(null); + + useEffect(() => { + const h = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }; + document.addEventListener("mousedown", h); + return () => document.removeEventListener("mousedown", h); + }, []); + + return ( +
+ + {open && ( +
+ {countries.map(c => ( + + ))} +
+ )} +
+ ); +} diff --git a/Autumn.SPA/src/components/Header.jsx b/Autumn.SPA/src/components/Header.jsx new file mode 100644 index 0000000..2a8adae --- /dev/null +++ b/Autumn.SPA/src/components/Header.jsx @@ -0,0 +1,32 @@ +import { Sun, Moon } from "lucide-react"; +import OwlLogo from "./OwlLogo"; +import CountrySelector from "./CountrySelector"; + +export default function Header({ country, setCountry, countries, view, setView, mode, setMode, onReset }) { + return ( +
+
+
+ + HS.Codes +
+
+ +
+ {[["home", "Home"], ["results", "Classify"], ["calculator", "Calculator"], ["browse", "Browse"]].map(([k, l]) => ( + + ))} +
+ +
+
+
+ ); +} diff --git a/Autumn.SPA/src/components/Home.jsx b/Autumn.SPA/src/components/Home.jsx new file mode 100644 index 0000000..7fe9717 --- /dev/null +++ b/Autumn.SPA/src/components/Home.jsx @@ -0,0 +1,55 @@ +import { Search, Calculator, LayoutGrid, Zap, ArrowRight } from "lucide-react"; +import SearchBar from "./SearchBar"; + +function IconBox({ icon: Icon }) { + return ( +
+ +
+ ); +} + +export default function Home({ query, onQueryChange, busy, onSearch, onNavigate }) { + const handleSubmit = (e) => { + e.preventDefault(); + if (query.trim()) onNavigate("results"); + }; + + const cards = [ + { icon: Search, title: "Import Classification", desc: "AI-powered search with predictions, synonym matching, and hierarchical HS code navigation.", go: "results" }, + { icon: Calculator, title: "Duty Calculator", desc: "Calculate import duties, VAT, levies, and total landed cost for your imports.", go: "calculator" }, + { icon: LayoutGrid, title: "Browse HS Codes", desc: "Navigate the complete Harmonized System — 21 sections, 96 chapters, 5000+ subheadings.", go: "browse" }, + ]; + + return ( +
+
+
+ AI-Powered Classification +
+

+ Commodity Codes &
Tariff Classification +

+

+ Classify commodities, calculate duties & taxes, and navigate the Harmonized System. +

+
+ +
+
+
+ {cards.map((c, i) => ( + + ))} +
+
+ ); +} diff --git a/Autumn.SPA/src/components/OwlLogo.jsx b/Autumn.SPA/src/components/OwlLogo.jsx new file mode 100644 index 0000000..e65fa44 --- /dev/null +++ b/Autumn.SPA/src/components/OwlLogo.jsx @@ -0,0 +1,18 @@ +export default function OwlLogo({ size = 30 }) { + return ( +
+ + + + + + + + + +
+ ); +} diff --git a/Autumn.SPA/src/components/SearchBar.jsx b/Autumn.SPA/src/components/SearchBar.jsx new file mode 100644 index 0000000..e2cf924 --- /dev/null +++ b/Autumn.SPA/src/components/SearchBar.jsx @@ -0,0 +1,148 @@ +import { useRef, useEffect, useState, useCallback } from "react"; +import { Search } from "lucide-react"; +import { api } from "../api"; + +export default function SearchBar({ query, onQueryChange, busy, onSubmit, autoFocus, large }) { + const ref = useRef(null); + const debounceRef = useRef(null); + const blurRef = useRef(null); + const [suggestions, setSuggestions] = useState([]); + const [show, setShow] = useState(false); + const [activeIdx, setActiveIdx] = useState(-1); + + useEffect(() => { + if (autoFocus && ref.current) ref.current.focus(); + }, [autoFocus]); + + const fetchSuggestions = useCallback((q) => { + clearTimeout(debounceRef.current); + if (!q || q.trim().length < 2) { + setSuggestions([]); + setShow(false); + return; + } + debounceRef.current = setTimeout(() => { + api.products(q.trim()).then((resp) => { + if (resp.success && resp.results?.length > 0) { + setSuggestions(resp.results.slice(0, 8)); + setShow(true); + } else { + setSuggestions([]); + setShow(false); + } + }).catch(() => { + setSuggestions([]); + setShow(false); + }); + }, 300); + }, []); + + const handleChange = (e) => { + const val = e.target.value; + onQueryChange(val); + setActiveIdx(-1); + fetchSuggestions(val); + }; + + const selectSuggestion = (text) => { + onQueryChange(text); + setSuggestions([]); + setShow(false); + setActiveIdx(-1); + // Auto-submit after selecting + setTimeout(() => { + ref.current?.closest("form")?.requestSubmit(); + }, 0); + }; + + const handleKeyDown = (e) => { + if (!show || suggestions.length === 0) return; + if (e.key === "ArrowDown") { + e.preventDefault(); + setActiveIdx((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setActiveIdx((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1)); + } else if (e.key === "Enter" && activeIdx >= 0) { + e.preventDefault(); + selectSuggestion(suggestions[activeIdx].text); + } else if (e.key === "Escape") { + setShow(false); + setActiveIdx(-1); + } + }; + + const handleBlur = () => { + // Delay to allow click on suggestion + blurRef.current = setTimeout(() => setShow(false), 150); + }; + + const handleFocus = () => { + clearTimeout(blurRef.current); + if (suggestions.length > 0 && query.trim().length >= 2) setShow(true); + }; + + // Highlight the matching portion of a suggestion + const highlight = (text) => { + const q = query.trim().toLowerCase(); + const idx = text.toLowerCase().indexOf(q); + if (idx < 0) return text; + return ( + <> + {text.slice(0, idx)} + {text.slice(idx, idx + q.length)} + {text.slice(idx + q.length)} + + ); + }; + + return ( +
+
+
+
+ +
+ + +
+ + {show && suggestions.length > 0 && ( +
+ {suggestions.map((s, i) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/Autumn.SPA/src/components/SearchView.jsx b/Autumn.SPA/src/components/SearchView.jsx new file mode 100644 index 0000000..a5aa2fe --- /dev/null +++ b/Autumn.SPA/src/components/SearchView.jsx @@ -0,0 +1,311 @@ +import { useState, useEffect } from "react"; +import { ChevronDown, ChevronRight, Calculator } from "lucide-react"; +import { api } from "../api"; +import SearchBar from "./SearchBar"; +import Badge from "./Badge"; + +const RATE_LABELS = { + duty: "Import Duty", vat: "VAT", levy: "Levy", sur: "Surcharge", + etls: "ETL", ciss: "CISS", nac: "NAC", nhil: "NHIL", + getfund: "GETFund", idf: "IDF", rdf: "RDF", +}; + +const RATE_FIELDS = ["duty", "vat", "levy", "sur", "etls", "ciss", "nac", "nhil", "getfund", "idf", "rdf"]; + +function formatTariffEntries(tariff) { + if (!tariff) return []; + return RATE_FIELDS + .filter(f => tariff[f] && tariff[f] !== "0") + .map(f => [RATE_LABELS[f] || f, `${tariff[f]}%`]); +} + +// Deduplicate: remove results whose code is a parent of another result +function deduplicateResults(flat) { + const allParentCodes = new Set(); + flat.forEach(r => { + (r.parentHSCodes || []).forEach(p => allParentCodes.add(p.code)); + }); + return flat.filter(r => { + const code = r.hsCodes?.[0]?.code || r.code; + return !allParentCodes.has(code); + }); +} + +export default function SearchView({ query, onQueryChange, country, countries, setCountry, onCalc }) { + const [busy, setBusy] = useState(false); + const [results, setResults] = useState(null); + const [exp, setExp] = useState(null); + const [noteData, setNoteData] = useState({}); + // drill[idx] = { path: [{code,desc}], items: [...] | null, leaf: noteResp | null, loading: bool } + const [drill, setDrill] = useState({}); + + const doSearch = () => { + if (!query.trim()) return; + setBusy(true); + setResults(null); + setExp(null); + setNoteData({}); + setDrill({}); + api.search(query).then((resp) => { + if (resp.success && resp.records) { + const flat = []; + for (const [src, items] of Object.entries(resp.records)) { + for (const item of items) { + flat.push({ ...item, src }); + } + } + flat.sort((a, b) => b.rating - a.rating); + setResults(deduplicateResults(flat)); + } else { + setResults([]); + } + }).catch(() => setResults([])).finally(() => setBusy(false)); + }; + + // Auto-search on mount if query is present (e.g. coming from Home) + useEffect(() => { + if (query.trim() && !results && !busy) doSearch(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const handleSubmit = (e) => { + e.preventDefault(); + doSearch(); + }; + + // Try to load children using parentId + level; if none, load leaf tariff + const drillLoad = async (idx, parentId, leafCode, path, childLevel) => { + setDrill(prev => ({ ...prev, [idx]: { path, items: null, leaf: null, loading: true } })); + try { + const resp = await api.browse({ parentId, level: childLevel }); + if (resp.success && resp.records?.length > 0) { + setDrill(prev => ({ ...prev, [idx]: { path, items: resp.records.sort((a, b) => a.code.localeCompare(b.code)), leaf: null, loading: false } })); + } else { + // Leaf — load tariff + const noteResp = await api.note(leafCode, country || "NG"); + setDrill(prev => ({ ...prev, [idx]: { path, items: null, leaf: noteResp.success ? noteResp : null, loading: false } })); + } + } catch { + setDrill(prev => ({ ...prev, [idx]: { path, items: [], leaf: null, loading: false } })); + } + }; + + const handleDrillNav = (idx, child) => { + const dd = drill[idx] || { path: [] }; + const lastId = dd.path.length > 0 ? dd.path[dd.path.length - 1].id : (results[idx].hsCodes?.[0]?.id); + if (lastId === child.id) return; + const newPath = [...dd.path, { code: child.code, desc: child.description, level: child.level, id: child.id }]; + drillLoad(idx, child.id, child.code, newPath, child.level + 1); + }; + + const handleExpand = (idx) => { + if (exp === idx) { setExp(null); return; } + setExp(idx); + const r = results[idx]; + const code = r.hsCodes?.[0]?.code || r.code; + const level = r.hsCodes?.[0]?.level || 0; + if (code && country && !noteData[code + country]) { + api.note(code, country).then((resp) => { + if (resp.success) { + setNoteData(prev => ({ ...prev, [code + country]: resp })); + } + }).catch(() => {}); + } + // Auto-load children for non-leaf codes using ID-based navigation + const id = r.hsCodes?.[0]?.id; + if (level < 4 && !drill[idx] && id) { + drillLoad(idx, id, code, [], level + 1); + } + }; + + useEffect(() => { + if (results && exp != null && country) { + const r = results[exp]; + const code = r.hsCodes?.[0]?.code || r.code; + if (code && !noteData[code + country]) { + api.note(code, country).then((resp) => { + if (resp.success) setNoteData(prev => ({ ...prev, [code + country]: resp })); + }).catch(() => {}); + } + } + }, [country]); + + const selCountry = countries.find(c => c.code === country); + + return ( +
+

Classify Commodity

+

Describe your product to get predicted HS codes with confidence scores.

+
+ +
+ + {busy && ( +
+
+

Running classification…

+
+ )} + + {results && !busy && ( +
+
+ + + Found {results.length} classification{results.length !== 1 ? "s" : ""} for "{query}" + +
+
+ {results.map((r, i) => { + const code = r.hsCodes?.[0]?.code || r.code; + const desc = r.hsCodes?.[0]?.description || r.prediction; + const hierarchy = [...(r.parentHSCodes || []), ...(r.hsCodes || [])].sort((a, b) => a.level - b.level); + const nd = country ? noteData[code + country] : null; + const tariffEntries = nd?.tariff?.[0] ? formatTariffEntries(nd.tariff[0]) : []; + + return ( +
+ + {exp === i && (() => { + const dd = drill[i]; + const isLeafResult = (r.hsCodes?.[0]?.level || 0) >= 4; + const drillLeaf = dd?.leaf; + const drillTariffEntries = drillLeaf?.tariff?.[0] ? formatTariffEntries(drillLeaf.tariff[0]) : []; + + // Unified hierarchy: static parents + drill path + const fullHierarchy = [...hierarchy, ...(dd?.path || []).map(p => ({ code: p.code || "", description: p.desc, id: p.id, level: p.level }))]; + // If drill reached a leaf, add it to hierarchy + if (drillLeaf?.records?.[0]) { + const lr = drillLeaf.records[0]; + fullHierarchy.push({ code: lr.code, description: lr.description, id: lr.id, level: lr.level, isLeaf: true }); + } + + // Determine which tariff to show + const showTariff = isLeafResult ? tariffEntries : drillTariffEntries; + const showTariffReady = isLeafResult ? !!nd : !!drillLeaf; + + return ( +
+
HS Code Hierarchy
+
+ {fullHierarchy.map((h, hi) => { + const isLast = hi === fullHierarchy.length - 1 && (isLeafResult || drillLeaf || (!dd?.items)); + const isClickable = hi < fullHierarchy.length - 1 || (dd?.items && !isLast); + // Clicking a hierarchy item navigates back to show its children + const handleClick = () => { + if (hi < hierarchy.length) { + const target = hierarchy[hi]; + const targetId = target.id || r.hsCodes?.[0]?.id; + if (hi === hierarchy.length - 1) { + drillLoad(i, targetId, target.code, [], (target.level || 0) + 1); + } else { + return; + } + } else { + const drillIdx = hi - hierarchy.length; + const target = dd.path[drillIdx]; + const trimmedPath = dd.path.slice(0, drillIdx + 1); + drillLoad(i, target.id, target.code, trimmedPath, target.level + 1); + } + }; + return ( + + ); + })} +
+ + {/* Loading */} + {dd?.loading && ( +
Loading…
+ )} + + {/* Child items below the hierarchy */} + {!isLeafResult && dd?.items && !dd.loading && ( +
+ {dd.items.length === 0 ? ( +
No sub-items found.
+ ) : dd.items.map((child, ci) => ( + + ))} +
+ )} + + {/* Tariffs — shown for leaf results or when drill reaches a leaf */} + {(isLeafResult || drillLeaf) && !dd?.loading && (() => { + const leafCode = isLeafResult ? code : (drillLeaf?.records?.[0]?.code || code); + const leafDesc = isLeafResult ? desc : (drillLeaf?.records?.[0]?.description || desc); + return ( +
+
+
+ Applicable Tariffs {selCountry && — {selCountry.flag} {selCountry.name}} +
+ {onCalc && ( + + )} +
+ {!country ? ( +
Select a country to view tariffs.
+ ) : country !== "NG" ? ( +
Tariff data for {selCountry?.name || country} coming soon.
+ ) : showTariff.length > 0 ? ( +
+ {showTariff.map(([l, v], ti) => ( +
+
{l}
+
{v}
+
+ ))} +
+ ) : showTariffReady ? ( +
No tariff data for this code.
+ ) : ( +
Loading tariff data…
+ )} +
+ ); + })()} +
+ ); + })()} +
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/Autumn.SPA/src/components/TariffPrompt.jsx b/Autumn.SPA/src/components/TariffPrompt.jsx new file mode 100644 index 0000000..b70e7b6 --- /dev/null +++ b/Autumn.SPA/src/components/TariffPrompt.jsx @@ -0,0 +1,12 @@ +import { Globe } from "lucide-react"; +import CountrySelector from "./CountrySelector"; + +export default function TariffPrompt({ onSelect, countries }) { + return ( +
+ + Select a country to view applicable tariffs and duties + +
+ ); +} diff --git a/Autumn.SPA/src/components/Toast.jsx b/Autumn.SPA/src/components/Toast.jsx new file mode 100644 index 0000000..2d685b4 --- /dev/null +++ b/Autumn.SPA/src/components/Toast.jsx @@ -0,0 +1,31 @@ +import { useState, useEffect, useCallback } from "react"; +import { AlertTriangle, X } from "lucide-react"; + +export default function Toast() { + const [visible, setVisible] = useState(false); + const [msg, setMsg] = useState(""); + + const show = useCallback((text) => { + setMsg(text); + setVisible(true); + setTimeout(() => setVisible(false), 5000); + }, []); + + useEffect(() => { + const handler = () => show("Too many requests — please wait a moment before trying again."); + window.addEventListener("api:ratelimit", handler); + return () => window.removeEventListener("api:ratelimit", handler); + }, [show]); + + if (!visible) return null; + + return ( +
+ + {msg} + +
+ ); +} diff --git a/Autumn.SPA/src/data/sections.js b/Autumn.SPA/src/data/sections.js new file mode 100644 index 0000000..4a7ea3a --- /dev/null +++ b/Autumn.SPA/src/data/sections.js @@ -0,0 +1,38 @@ +import { + Fish, Wheat, Droplets, Wine, Pickaxe, Beaker, Recycle, Briefcase, + TreePine, FileText, Shirt, Footprints, Mountain, Gem, Hammer, Cpu, + Ship, Glasses, Target, Armchair, Palette +} from "lucide-react"; + +export const ICON_MAP = { + "01-05": Fish, "06-14": Wheat, "15": Droplets, "16-24": Wine, + "25-27": Pickaxe, "28-38": Beaker, "39-40": Recycle, "41-43": Briefcase, + "44-46": TreePine, "47-49": FileText, "50-63": Shirt, "64-67": Footprints, + "68-70": Mountain, "71": Gem, "72-83": Hammer, "84-85": Cpu, + "86-89": Ship, "90-92": Glasses, "93": Target, "94-96": Armchair, "97": Palette, +}; + +// parentCode maps each section to the Roman numeral used in the DB +export const SECTIONS = [ + { code: "01-05", parentCode: "I", title: "Live Animals & Animal Products" }, + { code: "06-14", parentCode: "II", title: "Vegetable Products" }, + { code: "15", parentCode: "III", title: "Animal or Vegetable Fats & Oils" }, + { code: "16-24", parentCode: "IV", title: "Foodstuffs, Beverages & Tobacco" }, + { code: "25-27", parentCode: "V", title: "Mineral Products" }, + { code: "28-38", parentCode: "VI", title: "Chemical Products" }, + { code: "39-40", parentCode: "VII", title: "Plastics & Rubber" }, + { code: "41-43", parentCode: "VIII", title: "Hides, Skins & Leather" }, + { code: "44-46", parentCode: "IX", title: "Wood & Wood Products" }, + { code: "47-49", parentCode: "X", title: "Paper & Paperboard" }, + { code: "50-63", parentCode: "XI", title: "Textiles & Textile Articles" }, + { code: "64-67", parentCode: "XII", title: "Footwear, Headgear & Umbrellas" }, + { code: "68-70", parentCode: "XIII", title: "Stone, Ceramic & Glass" }, + { code: "71", parentCode: "XIV", title: "Precious Metals & Stones" }, + { code: "72-83", parentCode: "XV", title: "Base Metals & Articles" }, + { code: "84-85", parentCode: "XVI", title: "Machinery & Electrical Equipment" }, + { code: "86-89", parentCode: "XVII", title: "Vehicles, Aircraft & Vessels" }, + { code: "90-92", parentCode: "XVIII", title: "Optical, Medical & Musical Instruments" }, + { code: "93", parentCode: "XIX", title: "Arms & Ammunition" }, + { code: "94-96", parentCode: "XX", title: "Furniture, Toys & Misc. Goods" }, + { code: "97", parentCode: "XXI", title: "Works of Art & Antiques" }, +]; diff --git a/Autumn.SPA/src/main.jsx b/Autumn.SPA/src/main.jsx new file mode 100644 index 0000000..0c51ee7 --- /dev/null +++ b/Autumn.SPA/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './app.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/Autumn.SPA/vite.config.js b/Autumn.SPA/vite.config.js new file mode 100644 index 0000000..bf1bfef --- /dev/null +++ b/Autumn.SPA/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [tailwindcss(), react()], + server: { + port: 5173, + proxy: { + '/api': 'http://localhost:5174' + } + } +}) diff --git a/Autumn.UI/Autumn.UI.csproj b/Autumn.UI/Autumn.UI.csproj index d9197d2..6d70d0e 100644 --- a/Autumn.UI/Autumn.UI.csproj +++ b/Autumn.UI/Autumn.UI.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable 5285dfc4-9770-4f88-8bf7-81ef89605414 diff --git a/Autumn.UIML.Model/Autumn.UIML.Model.csproj b/Autumn.UIML.Model/Autumn.UIML.Model.csproj index 4ad2636..4f2f05d 100644 --- a/Autumn.UIML.Model/Autumn.UIML.Model.csproj +++ b/Autumn.UIML.Model/Autumn.UIML.Model.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + net10.0 diff --git a/Autumn.UIML.Model/ConsumeModel.cs b/Autumn.UIML.Model/ConsumeModel.cs index 7249f0a..bf24642 100644 --- a/Autumn.UIML.Model/ConsumeModel.cs +++ b/Autumn.UIML.Model/ConsumeModel.cs @@ -1,5 +1,3 @@ -// This file was auto-generated by ML.NET Model Builder. - using System; using System.Collections.Generic; using System.Linq; @@ -12,38 +10,26 @@ namespace Autumn_UIML.Model { public class ConsumeModel { - // For more info on consuming ML.NET models, visit https://aka.ms/model-builder-consume - // Method for consuming model in your app - public static ModelOutput Predict(ModelInput input) + // Cached prediction engine — loaded once, reused for all predictions + private static readonly Lazy<(PredictionEngine Engine, DataViewSchema Schema)> _cached = new(() => { - - // Create new MLContext - MLContext mlContext = new MLContext(); - - // Load model & create prediction engine + var mlContext = new MLContext(); string modelPath = AppDomain.CurrentDomain.BaseDirectory + "MLModel.zip"; - ITransformer mlModel = mlContext.Model.Load(modelPath, out var modelInputSchema); - var predEngine = mlContext.Model.CreatePredictionEngine(mlModel); + ITransformer mlModel = mlContext.Model.Load(modelPath, out _); + var engine = mlContext.Model.CreatePredictionEngine(mlModel); + return (engine, engine.OutputSchema); + }); - // Use model to make prediction on input data - ModelOutput result = predEngine.Predict(input); - return result; + public static ModelOutput Predict(ModelInput input) + { + return _cached.Value.Engine.Predict(input); } public static Dictionary Predict(ModelInput input, double threshold) { - - // Create new MLContext - MLContext mlContext = new MLContext(); - - // Load model & create prediction engine - string modelPath = AppDomain.CurrentDomain.BaseDirectory + "MLModel.zip"; - ITransformer mlModel = mlContext.Model.Load(modelPath, out var modelInputSchema); - var predEngine = mlContext.Model.CreatePredictionEngine(mlModel); - - // Use model to make prediction on input data - ModelOutput result = predEngine.Predict(input); - Dictionary confidence = GetScoresWithLabelsSorted(predEngine.OutputSchema, "Score", result.Score, threshold); + var (engine, schema) = _cached.Value; + ModelOutput result = engine.Predict(input); + Dictionary confidence = GetScoresWithLabelsSorted(schema, "Score", result.Score, threshold); return confidence; } @@ -55,13 +41,11 @@ private static Dictionary GetScoresWithLabelsSorted(DataViewSchem var slotNames = new VBuffer>(); column.Value.GetSlotNames(ref slotNames); - var names = new string[slotNames.Length]; var num = 0; foreach (var denseValue in slotNames.DenseValues()) { float scoreInternal = scores[num++]; - //if (scoreInternal > threshold) - result.Add(denseValue.ToString(), scoreInternal); + result.Add(denseValue.ToString(), scoreInternal); } return result.OrderByDescending(c => c.Value).Take(10).ToDictionary(i => i.Key, i => i.Value); diff --git a/README.md b/README.md index cf4e076..cc6c130 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,66 @@ # HS Codes Classification System -A comprehensive .NET application for Harmonized System (HS) code classification and commodity search, featuring AI-powered prediction capabilities and a modern web interface. +A comprehensive .NET application for Harmonized System (HS) code classification and commodity search, featuring AI-powered prediction, LLM-assisted classification, and a modern React SPA. ## Overview -This system provides intelligent HS code classification for international trade, combining traditional database search with machine learning predictions to help users find the most appropriate HS codes for their products. +This system provides intelligent HS code classification for international trade, combining MongoDB Atlas Search, machine learning predictions, and LLM-powered classification (Groq) to help users find the most appropriate HS codes for their products. ## Features -- **AI-Powered Classification**: Machine learning model for automatic HS code prediction -- **Hierarchical Navigation**: Browse HS codes through structured categories -- **Product Search**: Search and classify products with detailed descriptions -- **Customs Tariff Information**: Access tariff rates and import/export requirements +- **Blended Search Pipeline**: Multi-stage search combining exact match, Atlas Search with fuzzy matching, description search, and LLM fallback +- **LLM Classification (Groq)**: Automatic fallback to Llama 3.1 for natural language product queries when database results are low-confidence +- **ML.NET Prediction**: Pre-trained classification model for HS code prediction +- **MongoDB Atlas Search**: Full-text search with fuzzy matching on products and HS code descriptions +- **Modern React SPA**: Tailwind CSS frontend with dark mode, autocomplete, and hierarchy navigation +- **Duty Calculator**: Import duty and tax calculation with multi-country support +- **Hierarchical Navigation**: Browse HS codes through structured categories (Section > Chapter > Heading) +- **Rate Limiting**: Per-IP request throttling to protect the API +- **Search Analytics**: Groq prediction logging with accuracy tracking +- **Auth0 Authentication**: Secured admin endpoints with JWT - **Currency Exchange Rates**: Real-time currency conversion support -- **Document Management**: Store and manage classification documents -- **Search Analytics**: Track and log search patterns for optimization -- **Multi-Environment Support**: Development, staging, and production configurations ## Architecture -The application follows a clean architecture pattern with the following layers: +The application follows a clean architecture pattern: -- **Autumn.UI**: Web application with Razor Pages -- **Autumn.API**: RESTful API with Swagger documentation -- **Autumn.BL**: Business logic and services -- **Autumn.Domain**: Domain models and entities -- **Autumn.Repository**: Data access layer -- **Autumn.UIML.Model**: Machine learning model for predictions +``` +├── Autumn.SPA/ # React SPA (Vite + Tailwind CSS) +├── Autumn.API/ # Minimal API endpoints (.NET 10) +├── Autumn.BL/ # Business logic & services +├── Autumn.Domain/ # Domain models & entities +├── Autumn.Repository/ # MongoDB & SQL data access +├── Autumn.UIML.Model/ # ML.NET prediction model +└── docker-compose.yml # Container orchestration +``` ## Technology Stack -- **.NET 9.0**: Core framework -- **ASP.NET Core**: Web framework -- **MongoDB**: Document database for HS codes and products +- **.NET 10.0**: Core framework +- **ASP.NET Core Minimal APIs**: RESTful endpoints with Swagger +- **React 19 + Vite**: Single-page application +- **Tailwind CSS 4**: Utility-first styling with light/dark theme +- **MongoDB Atlas**: Document database with Atlas Search - **SQL Server**: Relational database for structured data -- **ML.NET**: Machine learning framework -- **AutoMapper**: Object mapping -- **Auth0**: Authentication and authorization -- **Docker**: Containerization -- **Nginx**: Reverse proxy and load balancer +- **ML.NET 1.5**: Machine learning classification +- **Groq API (Llama 3.1 8B)**: LLM-powered HS code classification fallback +- **Auth0**: JWT authentication and authorization +- **Docker + Nginx**: Containerization and reverse proxy ## Prerequisites -- .NET 9.0 SDK -- MongoDB -- SQL Server +- .NET 10.0 SDK +- Node.js 18+ +- MongoDB (Atlas recommended for full-text search) - Docker (optional) ## Quick Start ### Using Docker (Recommended) -1. Clone the repository: - ```bash -git clone +git clone https://github.com/samabos/hscodesdotnet.git cd hscodesdotnet -``` - -2. Start the application: - -```bash docker-compose up -d ``` @@ -68,153 +68,145 @@ The application will be available at `http://localhost` ### Manual Setup -1. **Configure Databases**: - - - Set up MongoDB instance - - Configure SQL Server connection - - Update connection strings in `appsettings.json` - -2. **Build and Run**: - -```bash -dotnet restore -dotnet build -dotnet run --project Autumn.UI -``` - -3. **API Documentation**: - - Navigate to `/swagger` for API documentation - - Available at `http://localhost:5000/swagger` - -## Configuration - -### Database Settings - -Update the following in `appsettings.json`: +1. **Configure Settings** — Update `Autumn.API/appsettings.json`: ```json { "StoreDatabaseSettings": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb+srv://...", "DatabaseName": "ClassificationDb" }, - "ConnectionStrings": { - "DefaultConnection": "your-sql-server-connection-string" + "SiteSettings": { + "Threshold": "0.1", + "GroqApiKey": "your-groq-api-key", + "GroqModel": "llama-3.1-8b-instant" + }, + "Auth0": { + "Domain": "https://your-auth0-domain.auth0.com/", + "Audience": "autumnapi" } } ``` -### Authentication +2. **Run the API**: -Configure Auth0 settings: +```bash +dotnet run --project Autumn.API +``` -```json -{ - "Auth0": { - "Domain": "your-auth0-domain", - "ClientId": "your-client-id" - } -} +3. **Run the SPA** (development): + +```bash +cd Autumn.SPA +npm install +npm run dev ``` -## API Endpoints +4. **API Documentation**: Navigate to `/swagger` + +## Search Pipeline -### Search Operations +The search system uses a blended multi-stage approach: -- `GET /api/v1/search` - Search HS codes -- `GET /api/v1/classify/commodity` - Classify commodity -- `GET /api/v1/note` - Get classification notes +| Stage | Source | Confidence | Description | +|-------|--------|-----------|-------------| +| 1 | Exact Match | 0.88–0.97 | Direct keyword match in products collection | +| 2 | Atlas Search | 0.60–0.82 | Fuzzy full-text search with word splitting | +| 3 | Description Search | 0.40–0.73 | Atlas Search on HS code descriptions (Level 3–4) | +| 4 | Groq LLM | 0.45–0.75 | Llama 3.1 classification (fallback when best result < 0.70) | +| 5 | Synonyms | 0.35–0.58 | RapidAPI synonym expansion (fallback) | +| 6 | ML.NET Model | Variable | Pre-trained classifier (last resort) | -### Data Management +Stages 1–3 run concurrently. Stage 4 only triggers when no high-confidence results are found. Results are deduplicated by HS code, keeping the highest-confidence entry. -- `GET /api/v1/hscode` - Retrieve HS codes -- `GET /api/v1/product` - Product information -- `GET /api/v1/currency` - Currency exchange rates +## API Endpoints + +### Public Endpoints (rate limited: 30 req/min per IP) -## Machine Learning +- `GET /api/search` — Search and classify products +- `GET /api/browse` — Browse HS code hierarchy +- `GET /api/duty` — Calculate import duties and taxes +- `GET /api/note/{hscode}` — Get notes, documents, and tariffs +- `GET /api/codelist/countries` — List countries +- `GET /api/codelist/currency` — Currency exchange rates +- `GET /api/codelist/products/{query?}` — Product autocomplete -The system includes a pre-trained ML model for HS code prediction: +### Admin Endpoints (requires Auth0 JWT) -- **Model**: ML.NET classification model -- **Input**: Product descriptions and keywords -- **Output**: Predicted HS codes with confidence scores -- **Threshold**: Configurable confidence threshold (default: 0.02) +- `GET /api/admin/dashboard` — Dashboard statistics +- CRUD: `/api/admin/products`, `/api/admin/codes`, `/api/admin/tariffs` +- `GET /api/admin/querylogs` — Search analytics -## Development +## MongoDB Atlas Search Indexes -### Project Structure +Create these indexes on your Atlas cluster for optimal search: +**`product-index`** on `products` collection: +```json +{ "mappings": { "dynamic": true, "fields": { "Keyword": { "type": "string", "analyzer": "lucene.standard" } } } } ``` -├── Autumn.UI/ # Web application -├── Autumn.API/ # REST API -├── Autumn.BL/ # Business logic -├── Autumn.Domain/ # Domain models -├── Autumn.Repository/ # Data access -├── Autumn.UIML.Model/ # ML model -└── docker-compose.yml # Container orchestration + +**`hscodes-index`** on `hscodes` collection: +```json +{ "mappings": { "dynamic": true, "fields": { "Description": { "type": "string", "analyzer": "lucene.standard" }, "Level": { "type": "number" } } } } ``` -### Adding New Features +## Groq LLM Integration -1. Create domain models in `Autumn.Domain` -2. Implement repository interfaces in `Autumn.Repository` -3. Add business logic in `Autumn.BL` -4. Create API controllers in `Autumn.API` -5. Update UI pages in `Autumn.UI` +The system uses Groq's free API tier (Llama 3.1 8B) as a smart fallback: -## Deployment +- Only called when database stages return low-confidence results (< 0.70) +- Predictions are logged to `SearchLog` with `Source = "groq"` and `FoundInDb` flag for accuracy tracking +- Free tier: 14,400 requests/day, 30 requests/minute +- Get your API key at [console.groq.com](https://console.groq.com) -### Production Deployment +## Deployment -1. **Build Docker Image**: +### Production with Docker ```bash docker build -t hscodes:latest . -``` - -2. **Deploy with Docker Compose**: - -```bash docker-compose -f docker-compose.prod.yml up -d ``` -3. **Configure Nginx**: - - Update `nginx.conf` for your domain - - Set up SSL certificates - - Configure load balancing - ### Environment Variables -Set the following environment variables for production: - - `ASPNETCORE_ENVIRONMENT=Production` -- `ConnectionStrings__DefaultConnection` - `StoreDatabaseSettings__ConnectionString` +- `SiteSettings__GroqApiKey` - `Auth0__Domain` -- `Auth0__ClientId` +- `Auth0__Audience` ## Contributing 1. Fork the repository 2. Create a feature branch 3. Make your changes -4. Add tests if applicable -5. Submit a pull request +4. Submit a pull request ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -## Support - -For support and questions: - -- Create an issue in the repository -- Contact the development team -- Check the API documentation at `/swagger` - ## Changelog +### Version 3.0 + +- Upgraded to .NET 10.0 with Minimal APIs +- New React 19 SPA with Tailwind CSS 4 (replaces Razor Pages) +- Light/dark theme with modern UI +- MongoDB Atlas Search with fuzzy matching +- Groq LLM integration (Llama 3.1 8B) as smart classification fallback +- Blended concurrent search pipeline (exact + Atlas + description + LLM) +- Groq prediction logging with accuracy tracking (`Source`, `FoundInDb` fields) +- Per-IP rate limiting (30 req/min) +- Rate limit toast notifications in frontend +- Product autocomplete with debounced search +- Hierarchy-style browse navigation +- Import duty calculator with multi-country support +- Cached ML.NET model (Lazy singleton) +- Tokenized regex fallback for searches with spaces/hyphens + ### Version 1.09 - Updated to .NET 9.0 From 1cacc9aa04d69266f8d29ee8aef230e42ae9b390 Mon Sep 17 00:00:00 2001 From: Samson Maborukoje Date: Sat, 14 Feb 2026 13:38:04 +0000 Subject: [PATCH 2/8] deployment update --- .github/workflows/deploy.yml | 8 +++++--- Autumn.API/Program.cs | 7 +++++++ Dockerfile | 29 +++++++++++++++++++++++++++++ docker-compose.prod.yml | 9 ++++++--- docker-compose.staging.yml | 7 +++++-- docker-compose.yml | 10 ++++++++-- nginx-staging.conf | 5 +++-- nginx.conf | 16 ++++++++-------- 8 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 Dockerfile diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ba88e3d..9ed4846 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: run: | docker buildx build --platform linux/amd64 \ -t ${{ secrets.DOCKER_USERNAME }}/hscodes:${{ github.run_number }} \ - --file Autumn.UI/Dockerfile \ + --file Dockerfile \ --push . - name: Set up SSH agent @@ -71,14 +71,16 @@ jobs: git pull origin $BRANCH && if [ '$ENVIRONMENT' == 'production' ]; then export AUTH0_DOMAIN='${{ secrets.AUTH0_DOMAIN }}' && - export AUTH0_CLIENTID='${{ secrets.AUTH0_CLIENTID }}' && + export AUTH0_AUDIENCE='${{ secrets.AUTH0_AUDIENCE }}' && + export GROQAPIKEY='${{ secrets.GROQAPIKEY }}' && export CONNECTION_STRING='${{ secrets.PROD_CONNECTION_STRING }}' && export GITHUB_RUN_NUMBER=$RUN_NUMBER && docker-compose -f ./docker-compose.prod.yml down && docker-compose -f ./docker-compose.prod.yml up -d --build; else export AUTH0_DOMAIN='${{ secrets.AUTH0_DOMAIN }}' && - export AUTH0_CLIENTID='${{ secrets.AUTH0_CLIENTID }}' && + export AUTH0_AUDIENCE='${{ secrets.AUTH0_AUDIENCE }}' && + export GROQAPIKEY='${{ secrets.GROQAPIKEY }}' && export CONNECTION_STRING='${{ secrets.STAGING_CONNECTION_STRING }}' && export GITHUB_RUN_NUMBER=$RUN_NUMBER && docker-compose -f ./docker-compose.staging.yml up -d --build; diff --git a/Autumn.API/Program.cs b/Autumn.API/Program.cs index 4da52f5..21b99ad 100644 --- a/Autumn.API/Program.cs +++ b/Autumn.API/Program.cs @@ -116,6 +116,7 @@ app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStaticFiles(); // ── Map endpoints ─────────────────────────────────────────────── app.MapSearchEndpoints(); @@ -125,4 +126,10 @@ app.MapCodeListEndpoints(); app.MapAdminEndpoints(); +// ── SPA fallback (serves index.html for client-side routes) ───── +if (!app.Environment.IsDevelopment()) +{ + app.MapFallbackToFile("index.html"); +} + app.Run(); diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b71271 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Stage 1: Build React SPA +FROM node:20-alpine AS spa-build +WORKDIR /app +COPY Autumn.SPA/package*.json ./ +RUN npm ci +COPY Autumn.SPA/ ./ +RUN npm run build + +# Stage 2: Build .NET API +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS api-build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Autumn.API/Autumn.API.csproj", "Autumn.API/"] +COPY ["Autumn.BL/Autumn.Service.csproj", "Autumn.BL/"] +COPY ["Autumn.Domain/Autumn.Domain.csproj", "Autumn.Domain/"] +COPY ["Autumn.Repository/Autumn.Infrastructure.csproj", "Autumn.Repository/"] +COPY ["Autumn.UIML.Model/Autumn.UIML.Model.csproj", "Autumn.UIML.Model/"] +RUN dotnet restore "Autumn.API/Autumn.API.csproj" +COPY . . +WORKDIR "/src/Autumn.API" +RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Stage 3: Runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app +COPY --from=api-build /app/publish . +COPY --from=spa-build /app/dist ./wwwroot +EXPOSE 8080 +ENTRYPOINT ["dotnet", "Autumn.API.dll"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 8d92dfa..225c87c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -18,16 +18,19 @@ services: - autumn-ui-network web: - image: "sageprojects/hscodes:${GITHUB_RUN_NUMBER}" # Replace with your actual DockerHub image name + image: "sageprojects/hscodes:${GITHUB_RUN_NUMBER}" container_name: autumn-ui-web restart: always expose: - - "8080" # Expose app port to the NGINX service + - "8080" environment: - ASPNETCORE_ENVIRONMENT=Production - StoreDatabaseSettings__ConnectionString=${CONNECTION_STRING} - Auth0__Domain=${AUTH0_DOMAIN} - - Auth0__ClientId=${AUTH0_CLIENTID} + - Auth0__Audience=${AUTH0_AUDIENCE} + - SiteSettings__GroqApiKey=${GROQAPIKEY} + - SiteSettings__GroqModel=llama-3.1-8b-instant + - SiteSettings__Threshold=0.1 networks: - autumn-ui-network diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 4072e34..3539134 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -19,12 +19,15 @@ services: container_name: autumn-ui-web-staging restart: always expose: - - "9090" + - "8080" environment: - ASPNETCORE_ENVIRONMENT=Staging - StoreDatabaseSettings__ConnectionString=${CONNECTION_STRING} - Auth0__Domain=${AUTH0_DOMAIN} - - Auth0__ClientId=${AUTH0_CLIENTID} + - Auth0__Audience=${AUTH0_AUDIENCE} + - SiteSettings__GroqApiKey=${GROQAPIKEY} + - SiteSettings__GroqModel=llama-3.1-8b-instant + - SiteSettings__Threshold=0.1 networks: - autumn-ui-network diff --git a/docker-compose.yml b/docker-compose.yml index 3919f0b..7f19dcc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,13 +17,19 @@ services: - autumn-ui-network web: - image: "sageprojects/hscodes:1.09" # Replace with your actual DockerHub image name + build: . container_name: autumn-ui-web restart: always expose: - - "8080" # Expose app port to the NGINX service + - "8080" environment: - ASPNETCORE_ENVIRONMENT=Development + - StoreDatabaseSettings__ConnectionString=${CONNECTION_STRING} + - Auth0__Domain=${AUTH0_DOMAIN} + - Auth0__Audience=${AUTH0_AUDIENCE} + - SiteSettings__GroqApiKey=${GROQAPIKEY} + - SiteSettings__GroqModel=llama-3.1-8b-instant + - SiteSettings__Threshold=0.1 networks: - autumn-ui-network diff --git a/nginx-staging.conf b/nginx-staging.conf index 2015662..f42df02 100644 --- a/nginx-staging.conf +++ b/nginx-staging.conf @@ -1,8 +1,9 @@ server { - listen 8443; + listen 80; + # Proxy all traffic to the .NET app (serves both API and SPA) location / { - proxy_pass http://web:9090; # web is the service name, and 8080 is the exposed port inside the container + proxy_pass http://web:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/nginx.conf b/nginx.conf index a334ddc..65f2ea9 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,6 +1,6 @@ server { listen 80; - server_name www.hs.codes; + server_name www.hs.codes hs.codes; # Redirect all HTTP traffic to HTTPS return 301 https://$host$request_uri; @@ -8,21 +8,21 @@ server { server { listen 443 ssl; - server_name www.hs.codes; + server_name www.hs.codes hs.codes; # SSL Certificate files - ssl_certificate /etc/ssl/hs.codes/hs.codes.crt; # Path to your certificate file - ssl_certificate_key /etc/ssl/hs.codes/hs.codes.key; # Path to your private key - ssl_trusted_certificate /etc/ssl/hs.codes/ca_bundle.crt; # Path to the CA bundle + ssl_certificate /etc/ssl/hs.codes/hs.codes.crt; + ssl_certificate_key /etc/ssl/hs.codes/hs.codes.key; + ssl_trusted_certificate /etc/ssl/hs.codes/ca_bundle.crt; - # Security and Protocols (recommended settings) + # Security and Protocols ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; - # Proxy configuration + # Proxy all traffic to the .NET app (serves both API and SPA) location / { - proxy_pass http://web:8080; # Forward traffic to your backend + proxy_pass http://web:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 0416242ef9663aec4c76e6a964c570195ca1ed55 Mon Sep 17 00:00:00 2001 From: Samson Maborukoje Date: Sat, 14 Feb 2026 14:01:25 +0000 Subject: [PATCH 3/8] fix deployment --- .github/workflows/deploy.yml | 6 +++--- docker-compose.prod.yml | 2 -- docker-compose.staging.yml | 2 -- docker-compose.yml | 2 -- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9ed4846..6bf20eb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -75,15 +75,15 @@ jobs: export GROQAPIKEY='${{ secrets.GROQAPIKEY }}' && export CONNECTION_STRING='${{ secrets.PROD_CONNECTION_STRING }}' && export GITHUB_RUN_NUMBER=$RUN_NUMBER && - docker-compose -f ./docker-compose.prod.yml down && - docker-compose -f ./docker-compose.prod.yml up -d --build; + docker compose -f ./docker compose.prod.yml down && + docker compose -f ./docker compose.prod.yml up -d --build; else export AUTH0_DOMAIN='${{ secrets.AUTH0_DOMAIN }}' && export AUTH0_AUDIENCE='${{ secrets.AUTH0_AUDIENCE }}' && export GROQAPIKEY='${{ secrets.GROQAPIKEY }}' && export CONNECTION_STRING='${{ secrets.STAGING_CONNECTION_STRING }}' && export GITHUB_RUN_NUMBER=$RUN_NUMBER && - docker-compose -f ./docker-compose.staging.yml up -d --build; + docker compose -f ./docker compose.staging.yml up -d --build; fi " diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 225c87c..a6e72bc 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: nginx: image: nginx:latest diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 3539134..7689f1c 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: nginx: image: nginx:latest diff --git a/docker-compose.yml b/docker-compose.yml index 7f19dcc..3a5d986 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: nginx: image: nginx:latest From 01d57293865bf44d7aec3dac95479ff54825cab2 Mon Sep 17 00:00:00 2001 From: Samson Maborukoje Date: Sat, 14 Feb 2026 14:04:30 +0000 Subject: [PATCH 4/8] correct file name for docker compose --- .github/workflows/deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6bf20eb..8f13db2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -75,15 +75,15 @@ jobs: export GROQAPIKEY='${{ secrets.GROQAPIKEY }}' && export CONNECTION_STRING='${{ secrets.PROD_CONNECTION_STRING }}' && export GITHUB_RUN_NUMBER=$RUN_NUMBER && - docker compose -f ./docker compose.prod.yml down && - docker compose -f ./docker compose.prod.yml up -d --build; + docker compose -f ./docker-compose.prod.yml down && + docker compose -f ./docker-compose.prod.yml up -d --build; else export AUTH0_DOMAIN='${{ secrets.AUTH0_DOMAIN }}' && export AUTH0_AUDIENCE='${{ secrets.AUTH0_AUDIENCE }}' && export GROQAPIKEY='${{ secrets.GROQAPIKEY }}' && export CONNECTION_STRING='${{ secrets.STAGING_CONNECTION_STRING }}' && export GITHUB_RUN_NUMBER=$RUN_NUMBER && - docker compose -f ./docker compose.staging.yml up -d --build; + docker compose -f ./docker-compose.staging.yml up -d --build; fi " From abd0526e8db102bd823481c17e4120a99022a128 Mon Sep 17 00:00:00 2001 From: Samson Maborukoje Date: Sat, 14 Feb 2026 14:24:31 +0000 Subject: [PATCH 5/8] `Update docker-compose.prod.yml to map HTTPS traffic` --- docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index a6e72bc..e728eb2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -5,7 +5,7 @@ services: restart: always ports: - "80:80" # Map HTTP traffic to NGINX - - "443:443" # Optional: Map HTTPS traffic if using SSL + - "443:443" # Map HTTPS traffic if using SSLtest volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro # Mount custom NGINX config - /etc/ssl/hs.codes:/etc/ssl/hs.codes:ro # Mount SSL files from VPS From 6b360456e35be49c89274e65d491d60449dc038d Mon Sep 17 00:00:00 2001 From: Samson Maborukoje Date: Sat, 14 Feb 2026 14:30:37 +0000 Subject: [PATCH 6/8] fix: use .env file for Docker env vars instead of export Environment variables set via export in SSH sessions don't persist when Docker restarts containers, causing 502 errors. Now the CI writes a .env file and uploads it to VPS via scp, which Docker Compose reads automatically on container start. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/deploy.yml | 68 ++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8f13db2..e87337c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -44,46 +44,46 @@ jobs: run: | if [ "${{ github.event_name }}" == "push" ]; then ENVIRONMENT="production" - BRANCH="main" # Deploy from the main branch for production + BRANCH="main" + DEPLOY_DIR="/root/hscodes" + COMPOSE_FILE="docker-compose.prod.yml" + CONNECTION_STRING="${{ secrets.PROD_CONNECTION_STRING }}" else ENVIRONMENT="staging" - BRANCH="${{ github.head_ref }}" # Deploy from the PR branch for staging + BRANCH="${{ github.head_ref }}" + DEPLOY_DIR="/root/hscodes-staging" + COMPOSE_FILE="docker-compose.staging.yml" + CONNECTION_STRING="${{ secrets.STAGING_CONNECTION_STRING }}" fi - RUN_NUMBER=${{ github.run_number }} # Get the GitHub Run Number + RUN_NUMBER=${{ github.run_number }} echo "Deploying to $ENVIRONMENT environment..." + # Write .env file locally and upload to VPS + cat > /tmp/deploy.env << EOF + AUTH0_DOMAIN=${{ secrets.AUTH0_DOMAIN }} + AUTH0_AUDIENCE=${{ secrets.AUTH0_AUDIENCE }} + GROQAPIKEY=${{ secrets.GROQAPIKEY }} + CONNECTION_STRING=${CONNECTION_STRING} + GITHUB_RUN_NUMBER=${RUN_NUMBER} + EOF + # Remove leading whitespace from heredoc + sed -i 's/^[[:space:]]*//' /tmp/deploy.env + + ssh ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} "mkdir -p ${DEPLOY_DIR}" + scp /tmp/deploy.env ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:${DEPLOY_DIR}/.env + + # Deploy on VPS ssh ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} " - if [ '$ENVIRONMENT' == 'production' ]; then - mkdir -p /root/hscodes && - cd /root/hscodes; - else - mkdir -p /root/hscodes-staging && - cd /root/hscodes-staging; - fi && - if [ ! -d .git ]; then - echo 'Directory is not a git repository. Cloning...'; - git clone https://github.com/samabos/hscodesdotnet.git .; - fi; + cd ${DEPLOY_DIR} && + if [ ! -d .git ]; then + echo 'Cloning repository...' && + git clone https://github.com/samabos/hscodesdotnet.git .; + fi && git reset --hard HEAD && - git fetch origin $BRANCH && - git checkout $BRANCH && - git pull origin $BRANCH && - if [ '$ENVIRONMENT' == 'production' ]; then - export AUTH0_DOMAIN='${{ secrets.AUTH0_DOMAIN }}' && - export AUTH0_AUDIENCE='${{ secrets.AUTH0_AUDIENCE }}' && - export GROQAPIKEY='${{ secrets.GROQAPIKEY }}' && - export CONNECTION_STRING='${{ secrets.PROD_CONNECTION_STRING }}' && - export GITHUB_RUN_NUMBER=$RUN_NUMBER && - docker compose -f ./docker-compose.prod.yml down && - docker compose -f ./docker-compose.prod.yml up -d --build; - else - export AUTH0_DOMAIN='${{ secrets.AUTH0_DOMAIN }}' && - export AUTH0_AUDIENCE='${{ secrets.AUTH0_AUDIENCE }}' && - export GROQAPIKEY='${{ secrets.GROQAPIKEY }}' && - export CONNECTION_STRING='${{ secrets.STAGING_CONNECTION_STRING }}' && - export GITHUB_RUN_NUMBER=$RUN_NUMBER && - docker compose -f ./docker-compose.staging.yml up -d --build; - fi + git fetch origin ${BRANCH} && + git checkout ${BRANCH} && + git pull origin ${BRANCH} && + docker compose -f ./${COMPOSE_FILE} down && + docker compose -f ./${COMPOSE_FILE} up -d " - From 59780b33273c5a7ec01b4328632dd605c4bb74d5 Mon Sep 17 00:00:00 2001 From: Samson Maborukoje Date: Sat, 14 Feb 2026 14:38:55 +0000 Subject: [PATCH 7/8] add appsettings --- Autumn.API/appsettings.Production.json | 43 ++++++++++++++++++++++++++ Autumn.API/appsettings.Staging.json | 42 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 Autumn.API/appsettings.Production.json create mode 100644 Autumn.API/appsettings.Staging.json diff --git a/Autumn.API/appsettings.Production.json b/Autumn.API/appsettings.Production.json new file mode 100644 index 0000000..415a834 --- /dev/null +++ b/Autumn.API/appsettings.Production.json @@ -0,0 +1,43 @@ +{ + "StoreDatabaseSettings": { + "HSCodeStoreCollectionName": "hscodes", + "ProductStoreCollectionName": "products", + "Product2StoreCollectionName": "Products2", + "KeywordStoreCollectionName": "keywords", + "SearchLogStoreCollectionName": "SearchLog", + "DocumentStoreCollectionName": "Documents", + "CurrencyStoreCollectionName": "currencies", + "IdentityStoreCollectionName": "Identities", + "CustomsTariffStoreCollectionName": "tariffs", + "HSCodeToDocumentStoreCollectionName": "HSCodeToDocuments", + "RequirementStoreCollectionName": "requirements", + "CountryStoreCollectionName": "countries", + "ConnectionString": "", + "DatabaseName": "ClassificationDb" + }, + "ConnectionStrings": { + "DefaultConnection": "" + }, + "Auth0": { + "Domain": "", + "Audience": "" + }, + "SiteSettings": { + "Threshold": "0.1", + "GroqApiKey": "", + "GroqModel": "llama-3.1-8b-instant" + }, + "Cors": { + "AllowedOrigins": [ + "https://hs.codes", + "https://www.hs.codes" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Autumn.API/appsettings.Staging.json b/Autumn.API/appsettings.Staging.json new file mode 100644 index 0000000..614a8de --- /dev/null +++ b/Autumn.API/appsettings.Staging.json @@ -0,0 +1,42 @@ +{ + "StoreDatabaseSettings": { + "HSCodeStoreCollectionName": "hscodes", + "ProductStoreCollectionName": "products", + "Product2StoreCollectionName": "Products2", + "KeywordStoreCollectionName": "keywords", + "SearchLogStoreCollectionName": "SearchLog", + "DocumentStoreCollectionName": "Documents", + "CurrencyStoreCollectionName": "currencies", + "IdentityStoreCollectionName": "Identities", + "CustomsTariffStoreCollectionName": "tariffs", + "HSCodeToDocumentStoreCollectionName": "HSCodeToDocuments", + "RequirementStoreCollectionName": "requirements", + "CountryStoreCollectionName": "countries", + "ConnectionString": "", + "DatabaseName": "ClassificationDb" + }, + "ConnectionStrings": { + "DefaultConnection": "" + }, + "Auth0": { + "Domain": "", + "Audience": "" + }, + "SiteSettings": { + "Threshold": "0.1", + "GroqApiKey": "", + "GroqModel": "llama-3.1-8b-instant" + }, + "Cors": { + "AllowedOrigins": [ + "http://hs.codes:8443" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 72e406b964e1e739c399389f2b26078b5d654db6 Mon Sep 17 00:00:00 2001 From: Samson Maborukoje Date: Sat, 14 Feb 2026 14:45:50 +0000 Subject: [PATCH 8/8] `Update Auth0 JWT bearer configuration to handle missing or non-HTTPS domain` --- Autumn.API/Program.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Autumn.API/Program.cs b/Autumn.API/Program.cs index 21b99ad..bd7ccd7 100644 --- a/Autumn.API/Program.cs +++ b/Autumn.API/Program.cs @@ -51,7 +51,10 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { - options.Authority = configuration["Auth0:Domain"]; + var domain = configuration["Auth0:Domain"] ?? ""; + if (!domain.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + domain = $"https://{domain}"; + options.Authority = domain; options.Audience = configuration["Auth0:Audience"]; });