diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index ba88e3d..e87337c 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
@@ -44,44 +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_CLIENTID='${{ secrets.AUTH0_CLIENTID }}' &&
- 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 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
"
-
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..bd7ccd7 100644
--- a/Autumn.API/Program.cs
+++ b/Autumn.API/Program.cs
@@ -1,25 +1,138 @@
-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 =>
+ {
+ var domain = configuration["Auth0:Domain"] ?? "";
+ if (!domain.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ domain = $"https://{domain}";
+ options.Authority = 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();
- public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
- WebHost.CreateDefaultBuilder(args)
- .UseStartup();
+// ── Seed ────────────────────────────────────────────────────────
+using (var scope = app.Services.CreateScope())
+{
+ var settings = scope.ServiceProvider.GetRequiredService();
+ var client = new MongoClient(settings.ConnectionString);
+ var db = client.GetDatabase(settings.DatabaseName);
+
+ // 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();
+app.UseStaticFiles();
+
+// ── Map endpoints ───────────────────────────────────────────────
+app.MapSearchEndpoints();
+app.MapBrowseEndpoints();
+app.MapDutyEndpoints();
+app.MapNoteEndpoints();
+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/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.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": "*"
+}
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