소스 검색

update api faq , driver, content, article

ducnt 4 주 전
부모
커밋
78132c9240

+ 5 - 0
EsimLao/Common/Common.csproj

@@ -26,4 +26,9 @@
 	<ItemGroup>
 	<ItemGroup>
 		<ProjectReference Include="..\Database\Database.csproj" />
 		<ProjectReference Include="..\Database\Database.csproj" />
 	</ItemGroup>
 	</ItemGroup>
+	<ItemGroup>
+	  <Reference Include="DotnetLib">
+	    <HintPath>..\lib\DotnetLib.dll</HintPath>
+	  </Reference>
+	</ItemGroup>
 </Project>
 </Project>

+ 14 - 5
EsimLao/Common/CommonLogic.cs

@@ -155,19 +155,28 @@ public class CommonLogic
 
 
     /// <summary>
     /// <summary>
     /// Get language from request header or body
     /// Get language from request header or body
-    /// Priority: header Accept-Language > body lang > default "lo"
+    /// Priority: header Lang > header Accept-Language > body lang > default "lo"
     /// </summary>
     /// </summary>
     /// <param name="httpRequest">HTTP request</param>
     /// <param name="httpRequest">HTTP request</param>
     /// <param name="bodyLang">Language from request body (optional)</param>
     /// <param name="bodyLang">Language from request body (optional)</param>
     /// <returns>Language code: "lo" or "en"</returns>
     /// <returns>Language code: "lo" or "en"</returns>
     public static string GetLanguage(HttpRequest httpRequest, string? bodyLang = null)
     public static string GetLanguage(HttpRequest httpRequest, string? bodyLang = null)
     {
     {
-        // Check header first
-        var headerLang = httpRequest.Headers["Accept-Language"].FirstOrDefault();
-        if (!string.IsNullOrEmpty(headerLang))
+        // Check custom Lang header first (highest priority)
+        var langHeader = httpRequest.Headers["Lang"].FirstOrDefault();
+        if (!string.IsNullOrEmpty(langHeader))
+        {
+            var lang = langHeader.ToLower();
+            if (lang == "en" || lang == "lo")
+                return lang;
+        }
+
+        // Check Accept-Language header
+        var acceptLangHeader = httpRequest.Headers["Accept-Language"].FirstOrDefault();
+        if (!string.IsNullOrEmpty(acceptLangHeader))
         {
         {
             // Accept-Language can be "en", "lo", "en-US,en;q=0.9", etc.
             // Accept-Language can be "en", "lo", "en-US,en;q=0.9", etc.
-            var lang = headerLang.Split(',')[0].Split('-')[0].ToLower();
+            var lang = acceptLangHeader.Split(',')[0].Split('-')[0].ToLower();
             if (lang == "en" || lang == "lo")
             if (lang == "en" || lang == "lo")
                 return lang;
                 return lang;
         }
         }

+ 1 - 0
EsimLao/Common/Constant/CommonConstant.cs

@@ -163,6 +163,7 @@ public static class ApiUrlConstant
     // Article URLs
     // Article URLs
     public const String ArticleCategoryUrl = "/apis/article/category";
     public const String ArticleCategoryUrl = "/apis/article/category";
     public const String ArticleLoadUrl = "/apis/article/load";
     public const String ArticleLoadUrl = "/apis/article/load";
+    public const String ArticleDetailUrl = "/apis/article/detail";
 
 
     // Content URLs
     // Content URLs
     public const String BannerLoadUrl = "/apis/content/banner";
     public const String BannerLoadUrl = "/apis/content/banner";

+ 31 - 0
EsimLao/Common/Http/ArticleDetailReq.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Common.Http
+{
+    /// <summary>
+    /// Request to get article detail by ID or slug
+    /// </summary>
+    public class ArticleDetailReq
+    {
+        /// <summary>
+        /// Language code for response: "lo" (Lao), "en" (English)
+        /// Default: "lo"
+        /// </summary>
+        public string? lang { get; set; } = "lo";
+
+        /// <summary>
+        /// Article ID (primary way to get detail)
+        /// </summary>
+        public int? id { get; set; }
+
+        /// <summary>
+        /// Article slug (alternative to ID for SEO-friendly URLs)
+        /// </summary>
+        public string? slug { get; set; }
+    }
+}

+ 0 - 5
EsimLao/Common/Http/ArticleRequest.cs

@@ -68,10 +68,5 @@ namespace Common.Http
         /// Filter by featured articles only
         /// Filter by featured articles only
         /// </summary>
         /// </summary>
         public bool? isFeatured { get; set; }
         public bool? isFeatured { get; set; }
-
-        /// <summary>
-        /// Article slug for getting single article detail
-        /// </summary>
-        public string? slug { get; set; }
     }
     }
 }
 }

+ 2 - 0
EsimLao/Common/Http/ContentRequest.cs

@@ -37,6 +37,8 @@ namespace Common.Http
         public string? lang { get; set; } = "lo";
         public string? lang { get; set; } = "lo";
         public int pageNumber { get; set; } = 0;
         public int pageNumber { get; set; } = 0;
         public int pageSize { get; set; } = 10;
         public int pageSize { get; set; } = 10;
+        /// <summary>Parent category ID (null = get root categories)</summary>
+        public int? parentId { get; set; }
     }
     }
 
 
     public class FaqLoadReq
     public class FaqLoadReq

+ 6 - 0
EsimLao/Database/Database/FaqCategory.cs

@@ -15,6 +15,8 @@ public partial class FaqCategory
 
 
     public string? IconUrl { get; set; }
     public string? IconUrl { get; set; }
 
 
+    public int? ParentId { get; set; }
+
     public short? DisplayOrder { get; set; }
     public short? DisplayOrder { get; set; }
 
 
     public bool? Status { get; set; }
     public bool? Status { get; set; }
@@ -33,5 +35,9 @@ public partial class FaqCategory
 
 
     public string? DescriptionEn { get; set; }
     public string? DescriptionEn { get; set; }
 
 
+    public virtual FaqCategory? ParentCategory { get; set; }
+
+    public virtual ICollection<FaqCategory> ChildCategories { get; set; } = new List<FaqCategory>();
+
     public virtual ICollection<Faq> Faqs { get; set; } = new List<Faq>();
     public virtual ICollection<Faq> Faqs { get; set; } = new List<Faq>();
 }
 }

+ 8 - 0
EsimLao/Database/Database/ModelContext.cs

@@ -932,10 +932,18 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.LastUpdate)
             entity.Property(e => e.LastUpdate)
                 .HasColumnType("DATE")
                 .HasColumnType("DATE")
                 .HasColumnName("LAST_UPDATE");
                 .HasColumnName("LAST_UPDATE");
+            entity.Property(e => e.ParentId)
+                .HasPrecision(10)
+                .HasColumnName("PARENT_ID");
             entity.Property(e => e.Status)
             entity.Property(e => e.Status)
                 .HasPrecision(1)
                 .HasPrecision(1)
                 .HasDefaultValueSql("1")
                 .HasDefaultValueSql("1")
                 .HasColumnName("STATUS");
                 .HasColumnName("STATUS");
+
+            entity.HasOne(d => d.ParentCategory)
+                .WithMany(p => p.ChildCategories)
+                .HasForeignKey(d => d.ParentId)
+                .HasConstraintName("FAQ_CATEGORY_PARENT_FK");
         });
         });
 
 
         modelBuilder.Entity<MessageQueue>(entity =>
         modelBuilder.Entity<MessageQueue>(entity =>

+ 115 - 66
EsimLao/Esim.Apis/Business/Article/ArticleBusinessImpl.cs

@@ -84,7 +84,7 @@ namespace Esim.Apis.Business
                     })
                     })
                     .ToList();
                     .ToList();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
                     log,
                     url,
                     url,
                     json,
                     json,
@@ -107,7 +107,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", exception);
                 log.Error("Exception: ", exception);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log,
                 log,
                 url,
                 url,
                 json,
                 json,
@@ -131,68 +131,6 @@ namespace Esim.Apis.Business
                 int pageNumber = request.pageNumber < 0 ? 0 : request.pageNumber;
                 int pageNumber = request.pageNumber < 0 ? 0 : request.pageNumber;
                 int pageSize = request.pageSize <= 0 ? 10 : request.pageSize;
                 int pageSize = request.pageSize <= 0 ? 10 : request.pageSize;
 
 
-                // If slug is provided, return single article detail
-                if (!string.IsNullOrEmpty(request.slug))
-                {
-                    var article = dbContext.Articles
-                        .Where(a => a.Slug == request.slug && a.Status == true)
-                        .Select(a => new
-                        {
-                            a.Id,
-                            title = lang == "en"
-                                ? (a.TitleEn ?? a.Title)
-                                : (a.TitleLo ?? a.Title),
-                            a.Slug,
-                            summary = lang == "en"
-                                ? (a.SummaryEn ?? a.Summary)
-                                : (a.SummaryLo ?? a.Summary),
-                            content = lang == "en"
-                                ? (a.ContentEn ?? a.Content)
-                                : (a.ContentLo ?? a.Content),
-                            a.ThumbnailUrl,
-                            a.CoverImageUrl,
-                            metaDescription = lang == "en"
-                                ? (a.MetaDescriptionEn ?? a.MetaDescription)
-                                : (a.MetaDescriptionLo ?? a.MetaDescription),
-                            a.MetaKeywords,
-                            a.CategoryId,
-                            a.ViewCount,
-                            a.IsFeatured,
-                            a.PublishedDate,
-                            a.CreatedDate
-                        })
-                        .FirstOrDefault();
-
-                    if (article == null)
-                    {
-                        return ApiResponseHelper.BuildResponse(
-                            log,
-                            url,
-                            json,
-                            CommonErrorCode.Error,
-                            ConfigManager.Instance.GetConfigWebValue("ARTICLE_NOT_FOUND", lang),
-                            new { }
-                        );
-                    }
-
-                    // Increment view count
-                    var articleEntity = dbContext.Articles.FirstOrDefault(a => a.Slug == request.slug);
-                    if (articleEntity != null)
-                    {
-                        articleEntity.ViewCount = (articleEntity.ViewCount ?? 0) + 1;
-                        await dbContext.SaveChangesAsync();
-                    }
-
-                    return ApiResponseHelper.BuildResponse(
-                        log,
-                        url,
-                        json,
-                        CommonErrorCode.Success,
-                        ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
-                        new { article }
-                    );
-                }
-
                 // Query articles list
                 // Query articles list
                 var query = dbContext.Articles
                 var query = dbContext.Articles
                     .Where(a => a.Status == true);
                     .Where(a => a.Status == true);
@@ -239,7 +177,7 @@ namespace Esim.Apis.Business
                     })
                     })
                     .ToList();
                     .ToList();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
                     log,
                     url,
                     url,
                     json,
                     json,
@@ -262,7 +200,118 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", exception);
                 log.Error("Exception: ", exception);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
+                log,
+                url,
+                json,
+                CommonErrorCode.SystemError,
+                ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
+                new { }
+            );
+        }
+
+        /// <summary>
+        /// Get article detail by ID or slug
+        /// </summary>
+        public async Task<IActionResult> ArticleDetail(HttpRequest httpRequest, ArticleDetailReq request)
+        {
+            var url = httpRequest.Path;
+            var json = JsonConvert.SerializeObject(request);
+            log.Debug("URL: " + url + " => Request: " + json);
+            try
+            {
+                // Validate request - must have either id or slug
+                if (!request.id.HasValue)
+                {
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log,
+                        url,
+                        json,
+                        CommonErrorCode.RequiredFieldMissing,
+                        ConfigManager.Instance.GetConfigWebValue("REQUIRED_FIELD_MISSING"),
+                        new { }
+                    );
+                }
+
+                string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
+
+                // Query by ID or slug
+                var query = dbContext.Articles.Where(a => a.Status == true);
+
+                if (request.id.HasValue)
+                {
+                    query = query.Where(a => a.Id == request.id.Value);
+                }
+                else if (!string.IsNullOrEmpty(request.slug))
+                {
+                    query = query.Where(a => a.Slug == request.slug);
+                }
+
+                var article = query
+                    .Select(a => new
+                    {
+                        a.Id,
+                        title = lang == "en"
+                            ? (a.TitleEn ?? a.Title)
+                            : (a.TitleLo ?? a.Title),
+                        a.Slug,
+                        summary = lang == "en"
+                            ? (a.SummaryEn ?? a.Summary)
+                            : (a.SummaryLo ?? a.Summary),
+                        content = lang == "en"
+                            ? (a.ContentEn ?? a.Content)
+                            : (a.ContentLo ?? a.Content),
+                        a.ThumbnailUrl,
+                        a.CoverImageUrl,
+                        metaDescription = lang == "en"
+                            ? (a.MetaDescriptionEn ?? a.MetaDescription)
+                            : (a.MetaDescriptionLo ?? a.MetaDescription),
+                        a.MetaKeywords,
+                        a.CategoryId,
+                        a.ViewCount,
+                        a.IsFeatured,
+                        a.PublishedDate,
+                        a.CreatedDate
+                    })
+                    .FirstOrDefault();
+
+                if (article == null)
+                {
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log,
+                        url,
+                        json,
+                        CommonErrorCode.Error,
+                        ConfigManager.Instance.GetConfigWebValue("ARTICLE_NOT_FOUND", lang),
+                        new { }
+                    );
+                }
+
+                // Increment view count
+                var articleEntity = request.id.HasValue
+                    ? dbContext.Articles.FirstOrDefault(a => a.Id == request.id.Value)
+                    : dbContext.Articles.FirstOrDefault(a => a.Slug == request.slug);
+
+                if (articleEntity != null)
+                {
+                    articleEntity.ViewCount = (articleEntity.ViewCount ?? 0) + 1;
+                    await dbContext.SaveChangesAsync();
+                }
+
+                return DotnetLib.Http.HttpResponse.BuildResponse(
+                    log,
+                    url,
+                    json,
+                    CommonErrorCode.Success,
+                    ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
+                    new { article }
+                );
+            }
+            catch (Exception exception)
+            {
+                log.Error("Exception: ", exception);
+            }
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log,
                 log,
                 url,
                 url,
                 json,
                 json,

+ 1 - 0
EsimLao/Esim.Apis/Business/Article/IArticleBusiness.cs

@@ -11,5 +11,6 @@ namespace Esim.Apis.Business
     {
     {
         Task<IActionResult> ArticleCategory(HttpRequest httpRequest, ArticleCategoryReq request);
         Task<IActionResult> ArticleCategory(HttpRequest httpRequest, ArticleCategoryReq request);
         Task<IActionResult> ArticleLoad(HttpRequest httpRequest, ArticleLoadReq request);
         Task<IActionResult> ArticleLoad(HttpRequest httpRequest, ArticleLoadReq request);
+        Task<IActionResult> ArticleDetail(HttpRequest httpRequest, ArticleDetailReq request);
     }
     }
 }
 }

+ 42 - 21
EsimLao/Esim.Apis/Business/Content/ContentBusinessImpl.cs

@@ -73,7 +73,7 @@ namespace Esim.Apis.Business
                     })
                     })
                     .ToList();
                     .ToList();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log, url, json,
                     log, url, json,
                     CommonErrorCode.Success,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
@@ -84,7 +84,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", ex);
                 log.Error("Exception: ", ex);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log, url, json,
                 log, url, json,
                 CommonErrorCode.SystemError,
                 CommonErrorCode.SystemError,
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
@@ -136,7 +136,7 @@ namespace Esim.Apis.Business
                     })
                     })
                     .ToList();
                     .ToList();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log, url, json,
                     log, url, json,
                     CommonErrorCode.Success,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
@@ -147,7 +147,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", ex);
                 log.Error("Exception: ", ex);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log, url, json,
                 log, url, json,
                 CommonErrorCode.SystemError,
                 CommonErrorCode.SystemError,
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
@@ -167,7 +167,7 @@ namespace Esim.Apis.Business
                 // Validate required fields
                 // Validate required fields
                 if (string.IsNullOrEmpty(request.customerName) || string.IsNullOrEmpty(request.reviewContent))
                 if (string.IsNullOrEmpty(request.customerName) || string.IsNullOrEmpty(request.reviewContent))
                 {
                 {
-                    return ApiResponseHelper.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log, url, json,
                         log, url, json,
                         CommonErrorCode.RequiredFieldMissing,
                         CommonErrorCode.RequiredFieldMissing,
                         ConfigManager.Instance.GetConfigWebValue("REQUIRED_FIELD_MISSING", lang),
                         ConfigManager.Instance.GetConfigWebValue("REQUIRED_FIELD_MISSING", lang),
@@ -190,7 +190,7 @@ namespace Esim.Apis.Business
                 dbContext.CustomerReviews.Add(review);
                 dbContext.CustomerReviews.Add(review);
                 await dbContext.SaveChangesAsync();
                 await dbContext.SaveChangesAsync();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log, url, json,
                     log, url, json,
                     CommonErrorCode.Success,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("REVIEW_SUBMITTED", lang),
                     ConfigManager.Instance.GetConfigWebValue("REVIEW_SUBMITTED", lang),
@@ -201,7 +201,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", ex);
                 log.Error("Exception: ", ex);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log, url, json,
                 log, url, json,
                 CommonErrorCode.SystemError,
                 CommonErrorCode.SystemError,
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
@@ -246,7 +246,7 @@ namespace Esim.Apis.Business
                     })
                     })
                     .ToList();
                     .ToList();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log, url, json,
                     log, url, json,
                     CommonErrorCode.Success,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
@@ -257,7 +257,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", ex);
                 log.Error("Exception: ", ex);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log, url, json,
                 log, url, json,
                 CommonErrorCode.SystemError,
                 CommonErrorCode.SystemError,
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
@@ -312,7 +312,7 @@ namespace Esim.Apis.Business
                     })
                     })
                     .ToList();
                     .ToList();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log, url, json,
                     log, url, json,
                     CommonErrorCode.Success,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
@@ -323,7 +323,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", ex);
                 log.Error("Exception: ", ex);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log, url, json,
                 log, url, json,
                 CommonErrorCode.SystemError,
                 CommonErrorCode.SystemError,
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
@@ -406,7 +406,7 @@ namespace Esim.Apis.Business
                     })
                     })
                     .ToList();
                     .ToList();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log, url, json,
                     log, url, json,
                     CommonErrorCode.Success,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
                     ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
@@ -427,7 +427,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", exception);
                 log.Error("Exception: ", exception);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log, url, json,
                 log, url, json,
                 CommonErrorCode.SystemError,
                 CommonErrorCode.SystemError,
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
@@ -444,22 +444,43 @@ namespace Esim.Apis.Business
             log.Debug("URL: " + url);
             log.Debug("URL: " + url);
             try
             try
             {
             {
-                // Get distinct brands with device count
-                var brands = dbContext.DeviceEsimCompatibilities
+                // Get language from header
+                string lang = CommonLogic.GetLanguage(httpRequest);
+
+                // Get all active devices for client-side filtering
+                var allDevices = dbContext.DeviceEsimCompatibilities
                     .Where(d => d.Status == true && d.SupportsEsim == true)
                     .Where(d => d.Status == true && d.SupportsEsim == true)
+                    .OrderBy(d => d.DisplayOrder)
+                    .ThenBy(d => d.ModelName)
+                    .ToList();
+
+                // Group devices by brand and include full device list
+                var brands = allDevices
                     .GroupBy(d => d.Brand)
                     .GroupBy(d => d.Brand)
                     .Select(g => new
                     .Select(g => new
                     {
                     {
                         brand = g.Key,
                         brand = g.Key,
                         deviceCount = g.Count(),
                         deviceCount = g.Count(),
-                        popularCount = g.Count(d => d.IsPopular == true)
+                        popularCount = g.Count(d => d.IsPopular == true),
+                        devices = g.Select(d => new
+                        {
+                            d.Id,
+                            modelName = lang == "en"
+                                ? (d.ModelNameEn ?? d.ModelName)
+                                : (d.ModelNameLo ?? d.ModelName),
+                            d.Category,
+                            d.IsPopular,
+                            d.DisplayOrder,
+                            notes = lang == "en"
+                                ? (d.NotesEn ?? d.Notes)
+                                : (d.NotesLo ?? d.Notes)
+                        }).ToList()
                     })
                     })
                     .OrderBy(b => b.brand)
                     .OrderBy(b => b.brand)
                     .ToList();
                     .ToList();
 
 
                 // Get distinct categories with device count
                 // Get distinct categories with device count
-                var categories = dbContext.DeviceEsimCompatibilities
-                    .Where(d => d.Status == true && d.SupportsEsim == true)
+                var categories = allDevices
                     .GroupBy(d => d.Category)
                     .GroupBy(d => d.Category)
                     .Select(g => new
                     .Select(g => new
                     {
                     {
@@ -469,10 +490,10 @@ namespace Esim.Apis.Business
                     .OrderBy(c => c.category)
                     .OrderBy(c => c.category)
                     .ToList();
                     .ToList();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log, url, "",
                     log, url, "",
                     CommonErrorCode.Success,
                     CommonErrorCode.Success,
-                    ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS"),
+                    ConfigManager.Instance.GetConfigWebValue("LOAD_SUCCESS", lang),
                     new
                     new
                     {
                     {
                         brands = brands,
                         brands = brands,
@@ -484,7 +505,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", exception);
                 log.Error("Exception: ", exception);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log, url, "",
                 log, url, "",
                 CommonErrorCode.SystemError,
                 CommonErrorCode.SystemError,
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
                 ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),

+ 55 - 31
EsimLao/Esim.Apis/Business/User/UserBusinessImpl.cs

@@ -49,7 +49,7 @@ namespace Esim.Apis.Business
             {
             {
                 if (string.IsNullOrEmpty(request.email))
                 if (string.IsNullOrEmpty(request.email))
                 {
                 {
-                    return ApiResponseHelper.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         log,
                         url,
                         url,
                         json,
                         json,
@@ -94,33 +94,44 @@ namespace Esim.Apis.Business
                     customerId = newCustomerId;
                     customerId = newCustomerId;
                 }
                 }
 
 
-                // Invalidate previous unused OTPs for this email
-                var oldOtps = dbContext.OtpVerifications
+                // Check if there's an existing OTP record for this email (unused, not expired)
+                int otpExpireMinutes = 5;
+                var existingOtp = dbContext.OtpVerifications
                     .Where(o => o.UserEmail == request.email && o.IsUsed == false)
                     .Where(o => o.UserEmail == request.email && o.IsUsed == false)
-                    .ToList();
+                    .OrderByDescending(o => o.CreatedDate)
+                    .FirstOrDefault();
 
 
-                foreach (var oldOtp in oldOtps)
+                if (existingOtp != null)
                 {
                 {
-                    oldOtp.IsUsed = true;
+                    // UPDATE existing record instead of creating new one
+                    existingOtp.OtpCode = otpCode;
+                    existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
+                    existingOtp.AttemptCount = 0; // Reset attempt count
+                    existingOtp.CustomerId = customerId; // Update customer ID if changed
+                    
+                    log.Info($"Updated existing OTP record ID={existingOtp.Id} for {request.email}");
                 }
                 }
-
-                // Create new OTP record
-                int otpExpireMinutes = 5;
-                var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
-                var otpVerification = new OtpVerification
+                else
                 {
                 {
-                    Id = otpId,
-                    CustomerId = customerId,
-                    UserEmail = request.email,
-                    OtpCode = otpCode,
-                    OtpType = 1, // Login OTP
-                    ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes),
-                    IsUsed = false,
-                    AttemptCount = 0,
-                    CreatedDate = DateTime.Now
-                };
+                    // INSERT new record only if none exists
+                    var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
+                    var otpVerification = new OtpVerification
+                    {
+                        Id = otpId,
+                        CustomerId = customerId,
+                        UserEmail = request.email,
+                        OtpCode = otpCode,
+                        OtpType = 1, // Login OTP
+                        ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes),
+                        IsUsed = false,
+                        AttemptCount = 0,
+                        CreatedDate = DateTime.Now
+                    };
+
+                    dbContext.OtpVerifications.Add(otpVerification);
+                    log.Info($"Created new OTP record for {request.email}");
+                }
 
 
-                dbContext.OtpVerifications.Add(otpVerification);
                 await dbContext.SaveChangesAsync();
                 await dbContext.SaveChangesAsync();
 
 
                 // Skip email sending for test account
                 // Skip email sending for test account
@@ -183,7 +194,20 @@ namespace Esim.Apis.Business
 
 
                 log.Info($"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
                 log.Info($"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
 
 
-                return ApiResponseHelper.BuildResponse(
+                //return DotnetLib.Http.HttpResponse.BuildResponse(
+                //    log,
+                //    url,
+                //    json,
+                //    CommonErrorCode.Success,
+                //    ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
+                //    new
+                //    {
+                //        email = request.email,
+                //        expireInSeconds = otpExpireMinutes * 60
+                //    }
+                //);
+
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
                     log,
                     url,
                     url,
                     json,
                     json,
@@ -200,7 +224,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", exception);
                 log.Error("Exception: ", exception);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log,
                 log,
                 url,
                 url,
                 json,
                 json,
@@ -223,7 +247,7 @@ namespace Esim.Apis.Business
                 if (string.IsNullOrEmpty(request.email) || string.IsNullOrEmpty(request.otpCode))
                 if (string.IsNullOrEmpty(request.email) || string.IsNullOrEmpty(request.otpCode))
                 {
                 {
                     string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
                     string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
-                    return ApiResponseHelper.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         log,
                         url,
                         url,
                         json,
                         json,
@@ -256,7 +280,7 @@ namespace Esim.Apis.Business
                     {
                     {
                         if (anyOtp.IsUsed == true)
                         if (anyOtp.IsUsed == true)
                         {
                         {
-                            return ApiResponseHelper.BuildResponse(
+                            return DotnetLib.Http.HttpResponse.BuildResponse(
                                 log,
                                 log,
                                 url,
                                 url,
                                 json,
                                 json,
@@ -267,7 +291,7 @@ namespace Esim.Apis.Business
                         }
                         }
                         if (anyOtp.ExpiredAt <= DateTime.Now)
                         if (anyOtp.ExpiredAt <= DateTime.Now)
                         {
                         {
-                            return ApiResponseHelper.BuildResponse(
+                            return DotnetLib.Http.HttpResponse.BuildResponse(
                                 log,
                                 log,
                                 url,
                                 url,
                                 json,
                                 json,
@@ -278,7 +302,7 @@ namespace Esim.Apis.Business
                         }
                         }
                     }
                     }
 
 
-                    return ApiResponseHelper.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         log,
                         url,
                         url,
                         json,
                         json,
@@ -298,7 +322,7 @@ namespace Esim.Apis.Business
 
 
                 if (customer == null)
                 if (customer == null)
                 {
                 {
-                    return ApiResponseHelper.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         log,
                         url,
                         url,
                         json,
                         json,
@@ -353,7 +377,7 @@ namespace Esim.Apis.Business
                 dbContext.UserTokens.Add(userToken);
                 dbContext.UserTokens.Add(userToken);
                 await dbContext.SaveChangesAsync();
                 await dbContext.SaveChangesAsync();
 
 
-                return ApiResponseHelper.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
                     log,
                     url,
                     url,
                     json,
                     json,
@@ -375,7 +399,7 @@ namespace Esim.Apis.Business
             {
             {
                 log.Error("Exception: ", exception);
                 log.Error("Exception: ", exception);
             }
             }
-            return ApiResponseHelper.BuildResponse(
+            return DotnetLib.Http.HttpResponse.BuildResponse(
                 log,
                 log,
                 url,
                 url,
                 json,
                 json,

+ 13 - 2
EsimLao/Esim.Apis/Controllers/ArticleController.cs

@@ -45,7 +45,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.ArticleCategoryUrl)]
         [Route(ApiUrlConstant.ArticleCategoryUrl)]
-        public async Task<IActionResult> ArticleCategory(ArticleCategoryReq request)
+        public async Task<IActionResult> ArticleCategory([FromBody] ArticleCategoryReq request)
         {
         {
             return await articleBusiness.ArticleCategory(HttpContext.Request, request);
             return await articleBusiness.ArticleCategory(HttpContext.Request, request);
         }
         }
@@ -56,11 +56,22 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.ArticleLoadUrl)]
         [Route(ApiUrlConstant.ArticleLoadUrl)]
-        public async Task<IActionResult> ArticleLoad(ArticleLoadReq request)
+        public async Task<IActionResult> ArticleLoad([FromBody] ArticleLoadReq request)
         {
         {
             return await articleBusiness.ArticleLoad(HttpContext.Request, request);
             return await articleBusiness.ArticleLoad(HttpContext.Request, request);
         }
         }
 
 
+        /// <summary>
+        /// Get article detail by ID or slug
+        /// POST /apis/article/detail
+        /// </summary>
+        [HttpPost]
+        [Route(ApiUrlConstant.ArticleDetailUrl)]
+        public async Task<IActionResult> ArticleDetail([FromBody] ArticleDetailReq request)
+        {
+            return await articleBusiness.ArticleDetail(HttpContext.Request, request);
+        }
+
         #endregion
         #endregion
     }
     }
 }
 }

+ 6 - 6
EsimLao/Esim.Apis/Controllers/ContentController.cs

@@ -29,7 +29,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.BannerLoadUrl)]
         [Route(ApiUrlConstant.BannerLoadUrl)]
-        public async Task<IActionResult> BannerLoad(BannerLoadReq request)
+        public async Task<IActionResult> BannerLoad([FromBody] BannerLoadReq request)
         {
         {
             return await contentBusiness.BannerLoad(HttpContext.Request, request);
             return await contentBusiness.BannerLoad(HttpContext.Request, request);
         }
         }
@@ -40,7 +40,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.CustomerReviewLoadUrl)]
         [Route(ApiUrlConstant.CustomerReviewLoadUrl)]
-        public async Task<IActionResult> CustomerReviewLoad(CustomerReviewLoadReq request)
+        public async Task<IActionResult> CustomerReviewLoad([FromBody] CustomerReviewLoadReq request)
         {
         {
             return await contentBusiness.CustomerReviewLoad(HttpContext.Request, request);
             return await contentBusiness.CustomerReviewLoad(HttpContext.Request, request);
         }
         }
@@ -51,7 +51,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.CustomerReviewCreateUrl)]
         [Route(ApiUrlConstant.CustomerReviewCreateUrl)]
-        public async Task<IActionResult> CustomerReviewCreate(CustomerReviewCreateReq request)
+        public async Task<IActionResult> CustomerReviewCreate([FromBody] CustomerReviewCreateReq request)
         {
         {
             return await contentBusiness.CustomerReviewCreate(HttpContext.Request, request);
             return await contentBusiness.CustomerReviewCreate(HttpContext.Request, request);
         }
         }
@@ -62,7 +62,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.FaqCategoryLoadUrl)]
         [Route(ApiUrlConstant.FaqCategoryLoadUrl)]
-        public async Task<IActionResult> FaqCategoryLoad(FaqCategoryLoadReq request)
+        public async Task<IActionResult> FaqCategoryLoad([FromBody] FaqCategoryLoadReq request)
         {
         {
             return await contentBusiness.FaqCategoryLoad(HttpContext.Request, request);
             return await contentBusiness.FaqCategoryLoad(HttpContext.Request, request);
         }
         }
@@ -73,7 +73,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.FaqLoadUrl)]
         [Route(ApiUrlConstant.FaqLoadUrl)]
-        public async Task<IActionResult> FaqLoad(FaqLoadReq request)
+        public async Task<IActionResult> FaqLoad([FromBody] FaqLoadReq request)
         {
         {
             return await contentBusiness.FaqLoad(HttpContext.Request, request);
             return await contentBusiness.FaqLoad(HttpContext.Request, request);
         }
         }
@@ -84,7 +84,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.DeviceCompatibilityLoadUrl)]
         [Route(ApiUrlConstant.DeviceCompatibilityLoadUrl)]
-        public async Task<IActionResult> DeviceCompatibilityLoad(DeviceCompatibilityReq request)
+        public async Task<IActionResult> DeviceCompatibilityLoad([FromBody] DeviceCompatibilityReq request)
         {
         {
             return await contentBusiness.DeviceCompatibilityLoad(HttpContext.Request, request);
             return await contentBusiness.DeviceCompatibilityLoad(HttpContext.Request, request);
         }
         }

+ 2 - 2
EsimLao/Esim.Apis/Controllers/UserController.cs

@@ -43,7 +43,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.RequestOtpUrl)]
         [Route(ApiUrlConstant.RequestOtpUrl)]
-        public async Task<IActionResult> RequestOtp(RequestOtpReq request)
+        public async Task<IActionResult> RequestOtp([FromBody] RequestOtpReq request)
         {
         {
             return await userBusiness.RequestOtp(HttpContext.Request, request);
             return await userBusiness.RequestOtp(HttpContext.Request, request);
         }
         }
@@ -54,7 +54,7 @@ namespace RevoSystem.Apis.Controllers
         /// </summary>
         /// </summary>
         [HttpPost]
         [HttpPost]
         [Route(ApiUrlConstant.VerifyOtpUrl)]
         [Route(ApiUrlConstant.VerifyOtpUrl)]
-        public async Task<IActionResult> VerifyOtp(VerifyOtpReq request)
+        public async Task<IActionResult> VerifyOtp([FromBody] VerifyOtpReq request)
         {
         {
             return await userBusiness.VerifyOtp(HttpContext.Request, request);
             return await userBusiness.VerifyOtp(HttpContext.Request, request);
         }
         }

+ 7 - 2
EsimLao/Esim.Apis/Program.cs

@@ -12,7 +12,12 @@ var builder = WebApplication.CreateBuilder(args);
 GlobalConfig.Configuration = builder.Configuration;
 GlobalConfig.Configuration = builder.Configuration;
 
 
 // Add services to the container.
 // Add services to the container.
-builder.Services.AddControllersWithViews();
+builder.Services.AddControllersWithViews()
+    .AddJsonOptions(options =>
+    {
+        // Use camelCase for JSON property names (categoryId instead of CategoryId)
+        options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
+    });
 
 
 // Add DbContext with Oracle provider
 // Add DbContext with Oracle provider
 var connectionString = builder.Configuration.GetSection("Connection").Value;
 var connectionString = builder.Configuration.GetSection("Connection").Value;
@@ -43,7 +48,7 @@ builder.Services.AddCors(options =>
 });
 });
 
 
 // Configure JWT Authentication
 // Configure JWT Authentication
-var jwtKey = builder.Configuration["Jwt:Key"] ?? "EsimLaoSecretKey12345678901234567890";
+var jwtKey = builder.Configuration["Jwt:Key"] ?? "EsimLaoSecretKey1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD";
 var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "EsimLao";
 var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "EsimLao";
 var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "EsimLaoClient";
 var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "EsimLaoClient";
 
 

+ 1 - 1
EsimLao/Esim.Apis/appsettings.json

@@ -8,7 +8,7 @@
   },
   },
   "AllowedHosts": "*",
   "AllowedHosts": "*",
   "Jwt": {
   "Jwt": {
-    "Key": "EsimLaoSecretKey12345678901234567890",
+    "Key": "EsimLaoSecretKey1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD",
     "Issuer": "EsimLao",
     "Issuer": "EsimLao",
     "Audience": "EsimLaoClient"
     "Audience": "EsimLaoClient"
   },
   },

BIN
EsimLao/lib/DotnetLib.dll