瀏覽代碼

no message

LamGiang 1 月之前
父節點
當前提交
d85f9b818d
共有 35 個文件被更改,包括 938 次插入1280 次删除
  1. 6 0
      EsimLao/Common/Http/AuthRequest.cs
  2. 21 0
      EsimLao/Database/Database/Area.cs
  3. 16 0
      EsimLao/Database/Database/Article.cs
  4. 10 0
      EsimLao/Database/Database/ArticleCategory.cs
  5. 10 0
      EsimLao/Database/Database/Banner.cs
  6. 12 0
      EsimLao/Database/Database/CmsContent.cs
  7. 4 0
      EsimLao/Database/Database/Country.cs
  8. 13 0
      EsimLao/Database/Database/CountryArea.cs
  9. 1 1
      EsimLao/Database/Database/CustomerInfo.cs
  10. 10 0
      EsimLao/Database/Database/CustomerReview.cs
  11. 8 0
      EsimLao/Database/Database/Faq.cs
  12. 8 0
      EsimLao/Database/Database/FaqCategory.cs
  13. 1 1
      EsimLao/Database/Database/MessageQueue.cs
  14. 41 0
      EsimLao/Database/Database/MessageQueueHi.cs
  15. 14 0
      EsimLao/Database/Database/MessageTemplate.cs
  16. 274 53
      EsimLao/Database/Database/ModelContext.cs
  17. 1 1
      EsimLao/Database/Database/OtpVerification.cs
  18. 1 1
      EsimLao/Database/Database/UserToken.cs
  19. 5 0
      EsimLao/Esim.Apis/.config/dotnet-tools.json
  20. 0 12
      EsimLao/Esim.Apis/Business/User/IUserBusiness.cs
  21. 53 809
      EsimLao/Esim.Apis/Business/User/UserBusinessImpl.cs
  22. 21 14
      EsimLao/Esim.Apis/Program.cs
  23. 20 0
      EsimLao/Esim.Apis/Properties/PublishProfiles/FolderProfile.pubxml
  24. 2 2
      EsimLao/Esim.Apis/Singleton/ConfigManager.cs
  25. 0 11
      EsimLao/Esim.Apis/appsettings.json
  26. 3 6
      EsimLao/Esim.SendMail/Esim.SendMail.csproj
  27. 0 268
      EsimLao/Esim.SendMail/Jobs/MessageQueueJob.cs
  28. 237 0
      EsimLao/Esim.SendMail/MessageQueueWorker.cs
  29. 18 39
      EsimLao/Esim.SendMail/Program.cs
  30. 16 0
      EsimLao/Esim.SendMail/Properties/PublishProfiles/FolderProfile.pubxml
  31. 16 15
      EsimLao/Esim.SendMail/Services/EmailService.cs
  32. 7 0
      EsimLao/Esim.SendMail/appsettings.json
  33. 30 25
      EsimLao/Esim.SendMail/log4net.config
  34. 7 1
      EsimLao/EsimLao.sln
  35. 52 21
      EsimLao/docs/api_auth_otp.txt

+ 6 - 0
EsimLao/Common/Http/AuthRequest.cs

@@ -28,6 +28,12 @@ namespace Common.Http
 
         [Required(ErrorMessage = "OTP is required")]
         public string? otpCode { get; set; }
+
+        /// <summary>
+        /// Language code for response messages: "lo" (Lao), "en" (English)
+        /// Default: "lo"
+        /// </summary>
+        public string? lang { get; set; } = "lo";
     }
 }
 

+ 21 - 0
EsimLao/Database/Database/Area.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+
+namespace Database.Database;
+
+public partial class Area
+{
+    public int? Id { get; set; }
+
+    public string? AreaCode { get; set; }
+
+    public string? AreaName1 { get; set; }
+
+    public string? AreaName2 { get; set; }
+
+    public bool? Status { get; set; }
+
+    public string? ImgUrl { get; set; }
+
+    public bool? IsPopular { get; set; }
+}

+ 16 - 0
EsimLao/Database/Database/Article.cs

@@ -47,5 +47,21 @@ public partial class Article
 
     public int? UpdatedBy { get; set; }
 
+    public string? TitleLo { get; set; }
+
+    public string? TitleEn { get; set; }
+
+    public string? SummaryLo { get; set; }
+
+    public string? SummaryEn { get; set; }
+
+    public string? ContentLo { get; set; }
+
+    public string? ContentEn { get; set; }
+
+    public string? MetaDescriptionLo { get; set; }
+
+    public string? MetaDescriptionEn { get; set; }
+
     public virtual ArticleCategory? Category { get; set; }
 }

+ 10 - 0
EsimLao/Database/Database/ArticleCategory.cs

@@ -25,6 +25,16 @@ public partial class ArticleCategory
 
     public DateTime? LastUpdate { get; set; }
 
+    public string? CategoryNameLo { get; set; }
+
+    public string? CategoryNameEn { get; set; }
+
+    public string? DescriptionLo { get; set; }
+
+    public string? DescriptionEn { get; set; }
+
+    public string? Language { get; set; }
+
     public virtual ICollection<Article> Articles { get; set; } = new List<Article>();
 
     public virtual ICollection<ArticleCategory> InverseParent { get; set; } = new List<ArticleCategory>();

+ 10 - 0
EsimLao/Database/Database/Banner.cs

@@ -34,4 +34,14 @@ public partial class Banner
     public DateTime? CreatedDate { get; set; }
 
     public DateTime? LastUpdate { get; set; }
+
+    public string? TitleLo { get; set; }
+
+    public string? TitleEn { get; set; }
+
+    public string? SubtitleLo { get; set; }
+
+    public string? SubtitleEn { get; set; }
+
+    public string? Language { get; set; }
 }

+ 12 - 0
EsimLao/Database/Database/CmsContent.cs

@@ -32,4 +32,16 @@ public partial class CmsContent
     public DateTime? LastUpdate { get; set; }
 
     public int? UpdatedBy { get; set; }
+
+    public string? PageTitleLo { get; set; }
+
+    public string? PageTitleEn { get; set; }
+
+    public string? MetaDescriptionLo { get; set; }
+
+    public string? MetaDescriptionEn { get; set; }
+
+    public string? ContentLo { get; set; }
+
+    public string? ContentEn { get; set; }
 }

+ 4 - 0
EsimLao/Database/Database/Country.cs

@@ -14,4 +14,8 @@ public partial class Country
     public string? CountryName2 { get; set; }
 
     public bool? Status { get; set; }
+
+    public string? ImgUrl { get; set; }
+
+    public bool? IsPopular { get; set; }
 }

+ 13 - 0
EsimLao/Database/Database/CountryArea.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+
+namespace Database.Database;
+
+public partial class CountryArea
+{
+    public int? Id { get; set; }
+
+    public int? CountryId { get; set; }
+
+    public int? AreaId { get; set; }
+}

+ 1 - 1
EsimLao/Database/Database/CustomerInfo.cs

@@ -5,7 +5,7 @@ namespace Database.Database;
 
 public partial class CustomerInfo
 {
-    public int? Id { get; set; }
+    public decimal? Id { get; set; }
 
     public string? SurName { get; set; }
 

+ 10 - 0
EsimLao/Database/Database/CustomerReview.cs

@@ -28,4 +28,14 @@ public partial class CustomerReview
     public DateTime? ApprovedDate { get; set; }
 
     public int? ApprovedBy { get; set; }
+
+    public string? ReviewContentLo { get; set; }
+
+    public string? ReviewContentEn { get; set; }
+
+    public string? DestinationLo { get; set; }
+
+    public string? DestinationEn { get; set; }
+
+    public string? Language { get; set; }
 }

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

@@ -29,5 +29,13 @@ public partial class Faq
 
     public DateTime? LastUpdate { get; set; }
 
+    public string? QuestionLo { get; set; }
+
+    public string? QuestionEn { get; set; }
+
+    public string? AnswerLo { get; set; }
+
+    public string? AnswerEn { get; set; }
+
     public virtual FaqCategory? Category { get; set; }
 }

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

@@ -25,5 +25,13 @@ public partial class FaqCategory
 
     public DateTime? LastUpdate { get; set; }
 
+    public string? CategoryNameLo { get; set; }
+
+    public string? CategoryNameEn { get; set; }
+
+    public string? DescriptionLo { get; set; }
+
+    public string? DescriptionEn { get; set; }
+
     public virtual ICollection<Faq> Faqs { get; set; } = new List<Faq>();
 }

+ 1 - 1
EsimLao/Database/Database/MessageQueue.cs

@@ -33,7 +33,7 @@ public partial class MessageQueue
 
     public string? ErrorMessage { get; set; }
 
-    public int? CreatedBy { get; set; }
+    public decimal? CreatedBy { get; set; }
 
     public DateTime? CreatedDate { get; set; }
 }

+ 41 - 0
EsimLao/Database/Database/MessageQueueHi.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+
+namespace Database.Database;
+
+public partial class MessageQueueHi
+{
+    public int Id { get; set; }
+
+    public bool? MessageType { get; set; }
+
+    public string Recipient { get; set; } = null!;
+
+    public string? Subject { get; set; }
+
+    public string? Content { get; set; }
+
+    public string? TemplateCode { get; set; }
+
+    public string? TemplateData { get; set; }
+
+    public bool? Priority { get; set; }
+
+    public bool? Status { get; set; }
+
+    public DateTime? ScheduledAt { get; set; }
+
+    public DateTime? ProcessedAt { get; set; }
+
+    public byte? RetryCount { get; set; }
+
+    public byte? MaxRetry { get; set; }
+
+    public string? ErrorMessage { get; set; }
+
+    public int? CreatedBy { get; set; }
+
+    public DateTime? CreatedDate { get; set; }
+
+    public DateTime? MovedDate { get; set; }
+}

+ 14 - 0
EsimLao/Database/Database/MessageTemplate.cs

@@ -24,4 +24,18 @@ public partial class MessageTemplate
     public DateTime? CreatedDate { get; set; }
 
     public DateTime? LastUpdate { get; set; }
+
+    public string? TemplateNameLo { get; set; }
+
+    public string? TemplateNameEn { get; set; }
+
+    public string? SubjectLo { get; set; }
+
+    public string? SubjectEn { get; set; }
+
+    public string? ContentLo { get; set; }
+
+    public string? ContentEn { get; set; }
+
+    public string? Language { get; set; }
 }

+ 274 - 53
EsimLao/Database/Database/ModelContext.cs

@@ -17,6 +17,8 @@ public partial class ModelContext : DbContext
 
     public virtual DbSet<AdminUser> AdminUsers { get; set; }
 
+    public virtual DbSet<Area> Areas { get; set; }
+
     public virtual DbSet<Article> Articles { get; set; }
 
     public virtual DbSet<ArticleCategory> ArticleCategories { get; set; }
@@ -31,6 +33,8 @@ public partial class ModelContext : DbContext
 
     public virtual DbSet<Country> Countries { get; set; }
 
+    public virtual DbSet<CountryArea> CountryAreas { get; set; }
+
     public virtual DbSet<CustomerInfo> CustomerInfos { get; set; }
 
     public virtual DbSet<CustomerReview> CustomerReviews { get; set; }
@@ -41,6 +45,8 @@ public partial class ModelContext : DbContext
 
     public virtual DbSet<MessageQueue> MessageQueues { get; set; }
 
+    public virtual DbSet<MessageQueueHi> MessageQueueHis { get; set; }
+
     public virtual DbSet<MessageTemplate> MessageTemplates { get; set; }
 
     public virtual DbSet<OrderDetail> OrderDetails { get; set; }
@@ -49,8 +55,6 @@ public partial class ModelContext : DbContext
 
     public virtual DbSet<OtpVerification> OtpVerifications { get; set; }
 
-    public virtual DbSet<Packg> Packgs { get; set; }
-
     public virtual DbSet<SimInfo> SimInfos { get; set; }
 
     public virtual DbSet<SystemConfig> SystemConfigs { get; set; }
@@ -61,10 +65,6 @@ public partial class ModelContext : DbContext
 
     public virtual DbSet<WsUser> WsUsers { get; set; }
 
-    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
-#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
-        => optionsBuilder.UseOracle("User Id= laos_esim;Password= laos_esim;Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1539))(CONNECT_DATA=(SERVICE_NAME=ORA12C)));User Id= laos_esim;Password= laos_esim;Connection Timeout=120;");
-
     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         modelBuilder
@@ -125,6 +125,39 @@ public partial class ModelContext : DbContext
                 .HasColumnName("USERNAME");
         });
 
+        modelBuilder.Entity<Area>(entity =>
+        {
+            entity
+                .HasNoKey()
+                .ToTable("AREA");
+
+            entity.Property(e => e.AreaCode)
+                .HasMaxLength(100)
+                .IsUnicode(false)
+                .HasColumnName("AREA_CODE");
+            entity.Property(e => e.AreaName1)
+                .HasMaxLength(2000)
+                .IsUnicode(false)
+                .HasColumnName("AREA_NAME1");
+            entity.Property(e => e.AreaName2)
+                .HasMaxLength(2000)
+                .IsUnicode(false)
+                .HasColumnName("AREA_NAME2");
+            entity.Property(e => e.Id)
+                .HasPrecision(10)
+                .HasColumnName("ID");
+            entity.Property(e => e.ImgUrl)
+                .HasMaxLength(200)
+                .IsUnicode(false)
+                .HasColumnName("IMG_URL");
+            entity.Property(e => e.IsPopular)
+                .HasPrecision(1)
+                .HasColumnName("IS_POPULAR");
+            entity.Property(e => e.Status)
+                .HasPrecision(1)
+                .HasColumnName("STATUS");
+        });
+
         modelBuilder.Entity<Article>(entity =>
         {
             entity.HasKey(e => e.Id).HasName("ARTICLE_PK");
@@ -150,6 +183,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Content)
                 .HasColumnType("CLOB")
                 .HasColumnName("CONTENT");
+            entity.Property(e => e.ContentEn)
+                .HasColumnType("CLOB")
+                .HasColumnName("CONTENT_EN");
+            entity.Property(e => e.ContentLo)
+                .HasColumnType("CLOB")
+                .HasColumnName("CONTENT_LO");
             entity.Property(e => e.CoverImageUrl)
                 .HasMaxLength(1000)
                 .IsUnicode(false)
@@ -180,6 +219,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.MetaDescription)
                 .HasMaxLength(500)
                 .HasColumnName("META_DESCRIPTION");
+            entity.Property(e => e.MetaDescriptionEn)
+                .HasMaxLength(500)
+                .HasColumnName("META_DESCRIPTION_EN");
+            entity.Property(e => e.MetaDescriptionLo)
+                .HasMaxLength(500)
+                .HasColumnName("META_DESCRIPTION_LO");
             entity.Property(e => e.MetaKeywords)
                 .HasMaxLength(500)
                 .HasColumnName("META_KEYWORDS");
@@ -195,6 +240,8 @@ public partial class ModelContext : DbContext
                 .HasDefaultValueSql("0")
                 .HasColumnName("STATUS");
             entity.Property(e => e.Summary).HasColumnName("SUMMARY");
+            entity.Property(e => e.SummaryEn).HasColumnName("SUMMARY_EN");
+            entity.Property(e => e.SummaryLo).HasColumnName("SUMMARY_LO");
             entity.Property(e => e.ThumbnailUrl)
                 .HasMaxLength(1000)
                 .IsUnicode(false)
@@ -202,6 +249,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Title)
                 .HasMaxLength(500)
                 .HasColumnName("TITLE");
+            entity.Property(e => e.TitleEn)
+                .HasMaxLength(500)
+                .HasColumnName("TITLE_EN");
+            entity.Property(e => e.TitleLo)
+                .HasMaxLength(500)
+                .HasColumnName("TITLE_LO");
             entity.Property(e => e.UpdatedBy)
                 .HasPrecision(10)
                 .HasColumnName("UPDATED_BY");
@@ -228,6 +281,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.CategoryName)
                 .HasMaxLength(200)
                 .HasColumnName("CATEGORY_NAME");
+            entity.Property(e => e.CategoryNameEn)
+                .HasMaxLength(200)
+                .HasColumnName("CATEGORY_NAME_EN");
+            entity.Property(e => e.CategoryNameLo)
+                .HasMaxLength(200)
+                .HasColumnName("CATEGORY_NAME_LO");
             entity.Property(e => e.CategorySlug)
                 .HasMaxLength(200)
                 .IsUnicode(false)
@@ -239,6 +298,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Description)
                 .HasMaxLength(1000)
                 .HasColumnName("DESCRIPTION");
+            entity.Property(e => e.DescriptionEn)
+                .HasMaxLength(1000)
+                .HasColumnName("DESCRIPTION_EN");
+            entity.Property(e => e.DescriptionLo)
+                .HasMaxLength(1000)
+                .HasColumnName("DESCRIPTION_LO");
             entity.Property(e => e.DisplayOrder)
                 .HasPrecision(5)
                 .HasDefaultValueSql("1")
@@ -247,6 +312,11 @@ public partial class ModelContext : DbContext
                 .HasMaxLength(500)
                 .IsUnicode(false)
                 .HasColumnName("ICON_URL");
+            entity.Property(e => e.Language)
+                .HasMaxLength(10)
+                .IsUnicode(false)
+                .HasDefaultValueSql("'lo'\n")
+                .HasColumnName("LANGUAGE");
             entity.Property(e => e.LastUpdate)
                 .HasColumnType("DATE")
                 .HasColumnName("LAST_UPDATE");
@@ -295,6 +365,11 @@ public partial class ModelContext : DbContext
                 .HasMaxLength(1000)
                 .IsUnicode(false)
                 .HasColumnName("IMAGE_URL");
+            entity.Property(e => e.Language)
+                .HasMaxLength(10)
+                .IsUnicode(false)
+                .HasDefaultValueSql("'lo'  -- Ngôn ngữ mặc định\n")
+                .HasColumnName("LANGUAGE");
             entity.Property(e => e.LastUpdate)
                 .HasColumnType("DATE")
                 .HasColumnName("LAST_UPDATE");
@@ -322,9 +397,21 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Subtitle)
                 .HasMaxLength(1000)
                 .HasColumnName("SUBTITLE");
+            entity.Property(e => e.SubtitleEn)
+                .HasMaxLength(1000)
+                .HasColumnName("SUBTITLE_EN");
+            entity.Property(e => e.SubtitleLo)
+                .HasMaxLength(1000)
+                .HasColumnName("SUBTITLE_LO");
             entity.Property(e => e.Title)
                 .HasMaxLength(500)
                 .HasColumnName("TITLE");
+            entity.Property(e => e.TitleEn)
+                .HasMaxLength(500)
+                .HasColumnName("TITLE_EN");
+            entity.Property(e => e.TitleLo)
+                .HasMaxLength(500)
+                .HasColumnName("TITLE_LO");
         });
 
         modelBuilder.Entity<CmsContent>(entity =>
@@ -344,6 +431,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Content)
                 .HasColumnType("CLOB")
                 .HasColumnName("CONTENT");
+            entity.Property(e => e.ContentEn)
+                .HasColumnType("CLOB")
+                .HasColumnName("CONTENT_EN");
+            entity.Property(e => e.ContentLo)
+                .HasColumnType("CLOB")
+                .HasColumnName("CONTENT_LO");
             entity.Property(e => e.CreatedBy)
                 .HasPrecision(10)
                 .HasColumnName("CREATED_BY");
@@ -362,6 +455,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.MetaDescription)
                 .HasMaxLength(500)
                 .HasColumnName("META_DESCRIPTION");
+            entity.Property(e => e.MetaDescriptionEn)
+                .HasMaxLength(500)
+                .HasColumnName("META_DESCRIPTION_EN");
+            entity.Property(e => e.MetaDescriptionLo)
+                .HasMaxLength(500)
+                .HasColumnName("META_DESCRIPTION_LO");
             entity.Property(e => e.MetaKeywords)
                 .HasMaxLength(500)
                 .HasColumnName("META_KEYWORDS");
@@ -376,6 +475,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.PageTitle)
                 .HasMaxLength(500)
                 .HasColumnName("PAGE_TITLE");
+            entity.Property(e => e.PageTitleEn)
+                .HasMaxLength(500)
+                .HasColumnName("PAGE_TITLE_EN");
+            entity.Property(e => e.PageTitleLo)
+                .HasMaxLength(500)
+                .HasColumnName("PAGE_TITLE_LO");
             entity.Property(e => e.PageType)
                 .HasMaxLength(50)
                 .IsUnicode(false)
@@ -487,11 +592,35 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Id)
                 .HasPrecision(10)
                 .HasColumnName("ID");
+            entity.Property(e => e.ImgUrl)
+                .HasMaxLength(200)
+                .IsUnicode(false)
+                .HasColumnName("IMG_URL");
+            entity.Property(e => e.IsPopular)
+                .HasPrecision(1)
+                .HasColumnName("IS_POPULAR");
             entity.Property(e => e.Status)
                 .HasPrecision(1)
                 .HasColumnName("STATUS");
         });
 
+        modelBuilder.Entity<CountryArea>(entity =>
+        {
+            entity
+                .HasNoKey()
+                .ToTable("COUNTRY_AREA");
+
+            entity.Property(e => e.AreaId)
+                .HasPrecision(10)
+                .HasColumnName("AREA_ID");
+            entity.Property(e => e.CountryId)
+                .HasPrecision(10)
+                .HasColumnName("COUNTRY_ID");
+            entity.Property(e => e.Id)
+                .HasPrecision(10)
+                .HasColumnName("ID");
+        });
+
         modelBuilder.Entity<CustomerInfo>(entity =>
         {
             entity
@@ -580,6 +709,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Destination)
                 .HasMaxLength(200)
                 .HasColumnName("DESTINATION");
+            entity.Property(e => e.DestinationEn)
+                .HasMaxLength(200)
+                .HasColumnName("DESTINATION_EN");
+            entity.Property(e => e.DestinationLo)
+                .HasMaxLength(200)
+                .HasColumnName("DESTINATION_LO");
             entity.Property(e => e.DisplayOrder)
                 .HasPrecision(5)
                 .HasDefaultValueSql("1")
@@ -588,11 +723,18 @@ public partial class ModelContext : DbContext
                 .HasPrecision(1)
                 .HasDefaultValueSql("0")
                 .HasColumnName("IS_FEATURED");
+            entity.Property(e => e.Language)
+                .HasMaxLength(10)
+                .IsUnicode(false)
+                .HasDefaultValueSql("'lo'\n")
+                .HasColumnName("LANGUAGE");
             entity.Property(e => e.Rating)
                 .HasPrecision(1)
                 .HasDefaultValueSql("5")
                 .HasColumnName("RATING");
             entity.Property(e => e.ReviewContent).HasColumnName("REVIEW_CONTENT");
+            entity.Property(e => e.ReviewContentEn).HasColumnName("REVIEW_CONTENT_EN");
+            entity.Property(e => e.ReviewContentLo).HasColumnName("REVIEW_CONTENT_LO");
             entity.Property(e => e.Status)
                 .HasPrecision(1)
                 .HasDefaultValueSql("0")
@@ -614,6 +756,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Answer)
                 .HasColumnType("CLOB")
                 .HasColumnName("ANSWER");
+            entity.Property(e => e.AnswerEn)
+                .HasColumnType("CLOB")
+                .HasColumnName("ANSWER_EN");
+            entity.Property(e => e.AnswerLo)
+                .HasColumnType("CLOB")
+                .HasColumnName("ANSWER_LO");
             entity.Property(e => e.CategoryId)
                 .HasPrecision(10)
                 .HasColumnName("CATEGORY_ID");
@@ -643,6 +791,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Question)
                 .HasMaxLength(1000)
                 .HasColumnName("QUESTION");
+            entity.Property(e => e.QuestionEn)
+                .HasMaxLength(1000)
+                .HasColumnName("QUESTION_EN");
+            entity.Property(e => e.QuestionLo)
+                .HasMaxLength(1000)
+                .HasColumnName("QUESTION_LO");
             entity.Property(e => e.Status)
                 .HasPrecision(1)
                 .HasDefaultValueSql("1")
@@ -670,6 +824,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.CategoryName)
                 .HasMaxLength(200)
                 .HasColumnName("CATEGORY_NAME");
+            entity.Property(e => e.CategoryNameEn)
+                .HasMaxLength(200)
+                .HasColumnName("CATEGORY_NAME_EN");
+            entity.Property(e => e.CategoryNameLo)
+                .HasMaxLength(200)
+                .HasColumnName("CATEGORY_NAME_LO");
             entity.Property(e => e.CategorySlug)
                 .HasMaxLength(200)
                 .IsUnicode(false)
@@ -681,6 +841,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Description)
                 .HasMaxLength(1000)
                 .HasColumnName("DESCRIPTION");
+            entity.Property(e => e.DescriptionEn)
+                .HasMaxLength(1000)
+                .HasColumnName("DESCRIPTION_EN");
+            entity.Property(e => e.DescriptionLo)
+                .HasMaxLength(1000)
+                .HasColumnName("DESCRIPTION_LO");
             entity.Property(e => e.DisplayOrder)
                 .HasPrecision(5)
                 .HasDefaultValueSql("1")
@@ -771,6 +937,84 @@ public partial class ModelContext : DbContext
                 .HasColumnName("TEMPLATE_DATA");
         });
 
+        modelBuilder.Entity<MessageQueueHi>(entity =>
+        {
+            entity.HasKey(e => e.Id).HasName("MESSAGE_QUEUE_HIS_PK");
+
+            entity.ToTable("MESSAGE_QUEUE_HIS");
+
+            entity.HasIndex(e => e.MovedDate, "MESSAGE_QUEUE_HIS_DATE_IDX");
+
+            entity.HasIndex(e => new { e.Recipient, e.MovedDate }, "MESSAGE_QUEUE_HIS_RECIPIENT_IDX");
+
+            entity.HasIndex(e => new { e.Status, e.MovedDate }, "MESSAGE_QUEUE_HIS_STATUS_IDX");
+
+            entity.Property(e => e.Id)
+                .HasPrecision(10)
+                .ValueGeneratedNever()
+                .HasColumnName("ID");
+            entity.Property(e => e.Content)
+                .HasColumnType("NCLOB")
+                .HasColumnName("CONTENT");
+            entity.Property(e => e.CreatedBy)
+                .HasPrecision(10)
+                .HasColumnName("CREATED_BY");
+            entity.Property(e => e.CreatedDate)
+                .HasPrecision(6)
+                .HasDefaultValueSql("SYSTIMESTAMP")
+                .HasColumnName("CREATED_DATE");
+            entity.Property(e => e.ErrorMessage)
+                .HasMaxLength(2000)
+                .IsUnicode(false)
+                .HasColumnName("ERROR_MESSAGE");
+            entity.Property(e => e.MaxRetry)
+                .HasPrecision(3)
+                .HasDefaultValueSql("3")
+                .HasColumnName("MAX_RETRY");
+            entity.Property(e => e.MessageType)
+                .IsRequired()
+                .HasPrecision(1)
+                .HasDefaultValueSql("1 ")
+                .HasColumnName("MESSAGE_TYPE");
+            entity.Property(e => e.MovedDate)
+                .HasPrecision(6)
+                .HasDefaultValueSql("SYSTIMESTAMP")
+                .HasColumnName("MOVED_DATE");
+            entity.Property(e => e.Priority)
+                .HasPrecision(1)
+                .HasDefaultValueSql("0")
+                .HasColumnName("PRIORITY");
+            entity.Property(e => e.ProcessedAt)
+                .HasPrecision(6)
+                .HasColumnName("PROCESSED_AT");
+            entity.Property(e => e.Recipient)
+                .HasMaxLength(500)
+                .IsUnicode(false)
+                .HasColumnName("RECIPIENT");
+            entity.Property(e => e.RetryCount)
+                .HasPrecision(3)
+                .HasDefaultValueSql("0")
+                .HasColumnName("RETRY_COUNT");
+            entity.Property(e => e.ScheduledAt)
+                .HasPrecision(6)
+                .HasColumnName("SCHEDULED_AT");
+            entity.Property(e => e.Status)
+                .IsRequired()
+                .HasPrecision(1)
+                .HasDefaultValueSql("0 ")
+                .HasColumnName("STATUS");
+            entity.Property(e => e.Subject)
+                .HasMaxLength(500)
+                .HasColumnName("SUBJECT");
+            entity.Property(e => e.TemplateCode)
+                .HasMaxLength(100)
+                .IsUnicode(false)
+                .HasColumnName("TEMPLATE_CODE");
+            entity.Property(e => e.TemplateData)
+                .HasColumnType("CLOB")
+                .HasColumnName("TEMPLATE_DATA");
+        });
+
         modelBuilder.Entity<MessageTemplate>(entity =>
         {
             entity.HasKey(e => e.Id).HasName("MESSAGE_TEMPLATE_PK");
@@ -786,10 +1030,21 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Content)
                 .HasColumnType("CLOB")
                 .HasColumnName("CONTENT");
+            entity.Property(e => e.ContentEn)
+                .HasColumnType("CLOB")
+                .HasColumnName("CONTENT_EN");
+            entity.Property(e => e.ContentLo)
+                .HasColumnType("CLOB")
+                .HasColumnName("CONTENT_LO");
             entity.Property(e => e.CreatedDate)
                 .HasDefaultValueSql("SYSDATE")
                 .HasColumnType("DATE")
                 .HasColumnName("CREATED_DATE");
+            entity.Property(e => e.Language)
+                .HasMaxLength(10)
+                .IsUnicode(false)
+                .HasDefaultValueSql("'lo'  -- Ngôn ngữ mặc định\n")
+                .HasColumnName("LANGUAGE");
             entity.Property(e => e.LastUpdate)
                 .HasColumnType("DATE")
                 .HasColumnName("LAST_UPDATE");
@@ -804,6 +1059,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.Subject)
                 .HasMaxLength(500)
                 .HasColumnName("SUBJECT");
+            entity.Property(e => e.SubjectEn)
+                .HasMaxLength(500)
+                .HasColumnName("SUBJECT_EN");
+            entity.Property(e => e.SubjectLo)
+                .HasMaxLength(500)
+                .HasColumnName("SUBJECT_LO");
             entity.Property(e => e.TemplateCode)
                 .HasMaxLength(50)
                 .IsUnicode(false)
@@ -811,6 +1072,12 @@ public partial class ModelContext : DbContext
             entity.Property(e => e.TemplateName)
                 .HasMaxLength(200)
                 .HasColumnName("TEMPLATE_NAME");
+            entity.Property(e => e.TemplateNameEn)
+                .HasMaxLength(200)
+                .HasColumnName("TEMPLATE_NAME_EN");
+            entity.Property(e => e.TemplateNameLo)
+                .HasMaxLength(200)
+                .HasColumnName("TEMPLATE_NAME_LO");
             entity.Property(e => e.Variables)
                 .HasMaxLength(1000)
                 .IsUnicode(false)
@@ -928,53 +1195,6 @@ public partial class ModelContext : DbContext
                 .HasColumnName("USER_EMAIL");
         });
 
-        modelBuilder.Entity<Packg>(entity =>
-        {
-            entity
-                .HasNoKey()
-                .ToTable("PACKG");
-
-            entity.Property(e => e.AmountData)
-                .HasPrecision(10)
-                .HasColumnName("AMOUNT_DATA");
-            entity.Property(e => e.CountryId)
-                .HasPrecision(10)
-                .HasColumnName("COUNTRY_ID");
-            entity.Property(e => e.DayDuration)
-                .HasPrecision(5)
-                .HasColumnName("DAY_DURATION");
-            entity.Property(e => e.Description)
-                .IsUnicode(false)
-                .HasColumnName("DESCRIPTION");
-            entity.Property(e => e.DisplayPrice)
-                .HasColumnType("NUMBER(12,2)")
-                .HasColumnName("DISPLAY_PRICE");
-            entity.Property(e => e.Id)
-                .HasPrecision(10)
-                .HasColumnName("ID");
-            entity.Property(e => e.IsUnlimited)
-                .HasPrecision(1)
-                .HasColumnName("IS_UNLIMITED");
-            entity.Property(e => e.PackageCode)
-                .HasMaxLength(100)
-                .IsUnicode(false)
-                .HasColumnName("PACKAGE_CODE");
-            entity.Property(e => e.PackageName)
-                .HasMaxLength(200)
-                .IsUnicode(false)
-                .HasColumnName("PACKAGE_NAME");
-            entity.Property(e => e.SellPrice)
-                .HasPrecision(12)
-                .HasColumnName("SELL_PRICE");
-            entity.Property(e => e.Status)
-                .HasPrecision(10)
-                .HasColumnName("STATUS");
-            entity.Property(e => e.Title)
-                .HasMaxLength(2000)
-                .IsUnicode(false)
-                .HasColumnName("TITLE");
-        });
-
         modelBuilder.Entity<SimInfo>(entity =>
         {
             entity
@@ -1173,6 +1393,7 @@ public partial class ModelContext : DbContext
         modelBuilder.HasSequence("ARTICLE_SEQ");
         modelBuilder.HasSequence("BANNER_SEQ");
         modelBuilder.HasSequence("CMS_CONTENT_SEQ");
+        modelBuilder.HasSequence("CONFIG_SEQ");
         modelBuilder.HasSequence("CONTACT_FORM_SEQ");
         modelBuilder.HasSequence("COUNTRY_SEQ");
         modelBuilder.HasSequence("CUSTOMER_INFO_SEQ");

+ 1 - 1
EsimLao/Database/Database/OtpVerification.cs

@@ -7,7 +7,7 @@ public partial class OtpVerification
 {
     public int Id { get; set; }
 
-    public int? CustomerId { get; set; }
+    public decimal? CustomerId { get; set; }
 
     public string UserEmail { get; set; } = null!;
 

+ 1 - 1
EsimLao/Database/Database/UserToken.cs

@@ -7,7 +7,7 @@ public partial class UserToken
 {
     public int Id { get; set; }
 
-    public int CustomerId { get; set; }
+    public decimal? CustomerId { get; set; }
 
     public string AccessToken { get; set; } = null!;
 

+ 5 - 0
EsimLao/Esim.Apis/.config/dotnet-tools.json

@@ -0,0 +1,5 @@
+{
+  "version": 1,
+  "isRoot": true,
+  "tools": {}
+}

+ 0 - 12
EsimLao/Esim.Apis/Business/User/IUserBusiness.cs

@@ -19,17 +19,5 @@ namespace Esim.Apis.Business
         Task<IActionResult> RequestOtp(HttpRequest httpRequest, RequestOtpReq request);
         Task<IActionResult> VerifyOtp(HttpRequest httpRequest, VerifyOtpReq request);
 
-        //Task<IActionResult> DetectMsisdn(HttpRequest httpRequest, DetectMsisdnReq request);
-        //Task<IActionResult> UserInvite(HttpRequest httpRequest, UserInviteReq request);
-
-        //Task<IActionResult> LoadVendorPackage(
-        //    HttpRequest httpRequest,
-        //    LoadVendorPackageReq request
-        //);
-
-        //Task<IActionResult> VendorPackageRegister(
-        //    HttpRequest httpRequest,
-        //    VendorPackageRegisterReq request
-        //);
     }
 }

+ 53 - 809
EsimLao/Esim.Apis/Business/User/UserBusinessImpl.cs

@@ -36,780 +36,6 @@ namespace Esim.Apis.Business
             return configuration.GetSection(key).Value ?? "";
         }
 
-        //public async Task<IActionResult> DetectMsisdn(
-        //    HttpRequest httpRequest,
-        //    DetectMsisdnReq request
-        //)
-        //{
-        //    var url = httpRequest.Path;
-        //    var json = JsonConvert.SerializeObject(request);
-        //    log.Debug("URL: " + url + " => Request: " + json);
-        //    try
-        //    {
-        //        //// add prize to user
-        //        //var addPrizeResult = await QuestLogic.AddPrizeForUser(
-        //        //    dbContext,
-        //        //    "67075723423",
-        //        //    101
-        //        //);
-
-
-        //        var msisdn = DotnetLib.Logic.ReuseLogic.TelemorValidateMsisdn(GetParameter("CountryCode"), request.msisdn);
-        //        if (msisdn == null)
-        //        {
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                ConfigManager.Instance.GetConfigWebValue("MSISDN_INVALID"),
-        //                new { }
-        //            );
-        //        }
-        //        Account? account = null;
-        //        if (request.accessToken != null)
-        //        {
-        //            // check access token
-        //            account = dbContext
-        //                .Accounts.Where(x => x.Msisdn == msisdn)
-        //                .FirstOrDefault();
-        //            if (account == null)
-        //            {
-        //                // create account
-        //                Account accountCreate = new Account
-        //                {
-        //                    Id = (decimal)await DbLogic.GenIdAsync(dbContext, "ACCOUNT_SEQ"),
-        //                    Msisdn = msisdn,
-        //                    Password = CommonLogic.GenPassword(6),
-        //                    RefreshToken = request.accessToken,
-        //                    Username = msisdn,
-        //                    Birthday = DateTime.Now,
-        //                    CreatedDate = DateTime.Now,
-        //                };
-        //                dbContext.Accounts.Add(accountCreate);
-        //                await dbContext.SaveChangesAsync();
-        //                account = accountCreate;
-        //            }
-        //        }
-        //        else if (request.msisdn != null && request.password != null)
-        //        {
-        //            // after checking access token
-        //            // check user
-        //            account = dbContext
-        //                .Accounts.Where(x => x.Msisdn == msisdn && x.Password == request.password)
-        //                .FirstOrDefault();
-        //            if (account == null)
-        //            {
-        //                return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                    log,
-        //                    url,
-        //                    json,
-        //                    CommonErrorCode.Error,
-        //                    "Invalid msisdn or password",
-        //                    new { }
-        //                );
-        //            }
-        //        }
-        //        else
-        //        {
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                "Invalid msisdn or password",
-        //                new { }
-        //            );
-        //        }
-        //        // gen token
-        //        string token = CommonLogic.GenToken(configuration, account.Msisdn, account.Id.ToString());
-        //        string refreshToken = CommonLogic.GenRefreshToken(configuration, account.Msisdn);
-
-        //        // update token
-        //        account.RefreshToken = token;
-        //        dbContext.Accounts.Update(account);
-        //        await dbContext.SaveChangesAsync();
-
-        //        // get inviting mission
-        //        var mission = dbContext
-        //            .Missions.Where(x =>
-        //                x.Type == CommonConstant.TypeInviting
-        //                && x.Status == CommonConstant.StatusActive
-        //                && x.FromTime <= DateTime.Now
-        //                && x.ToTime >= DateTime.Now
-        //            ).FirstOrDefault();
-
-        //        if (mission != null)
-        //        {
-        //            // check user inviting
-        //            var inviting = dbContext.InvitingHistories.Where(
-        //                x => x.Receiver == account.Msisdn && x.Status == CommonConstant.StatusSuccess && x.InvitedTime >= mission.FromTime && x.InvitedTime <= mission.ToTime
-        //            ).FirstOrDefault();
-        //            if (inviting != null)
-        //            {
-        //                inviting.Status = CommonConstant.StatusClaimed;
-        //                dbContext.InvitingHistories.Update(inviting);
-        //                await dbContext.SaveChangesAsync();
-        //                // check time required
-        //                var invitingClaimed = dbContext.InvitingHistories.Where(x => x.Msisdn == inviting.Msisdn && x.Status == CommonConstant.StatusClaimed && x.InvitedTime >= mission.FromTime && x.InvitedTime <= mission.ToTime).Count();
-        //                if (invitingClaimed >= mission.RequiredTime)
-        //                {
-        //                    log.Debug("User invited, add prize for user");
-
-        //                    var accountInviting = dbContext.Accounts.Where(x => x.Msisdn == inviting.Msisdn).FirstOrDefault();
-        //                    if (accountInviting == null)
-        //                    {
-        //                        log.Error("Not found account for inviting msisdn: " + inviting.Msisdn);
-        //                    }
-        //                    else
-        //                    {
-        //                        // update mission completed status to claimed
-        //                        await QuestLogic.SetCompletedMissionForUserAsync(dbContext, accountInviting.Msisdn, accountInviting.Id, mission);
-
-        //                        // add prize for user
-        //                        var addPrizeRes = await QuestLogic.AddPrizeForUser(
-        //                             dbContext,
-        //                             accountInviting.Msisdn,
-        //                             mission.Id
-        //                         );
-
-        //                        // send mt to owner
-        //                        await MtLogic.SendMt(
-        //                            dbContext,
-        //                            accountInviting.Msisdn,
-        //                            ConfigManager.Instance.GetConfigSmsValue("OWNER_INVITE_SUCCESS")
-        //                        );
-        //                    }
-        //                }
-        //            }
-        //            else
-        //            {
-        //                log.Debug($"No inviting mission implement for {account.Msisdn}");
-        //            }
-        //        }
-
-        //        bool canPlay = true;
-        //        decimal? vendorPackageId = null;
-
-        //        // check if user completed vendor package
-        //        var campaignOpening = dbContext
-        //            .Campaigns.Where(
-        //                x =>
-        //                    x.Status == CommonConstant.StatusActive
-        //                    && x.FromTime <= DateTime.Now
-        //                    && x.ToTime >= DateTime.Now
-        //            )
-        //            .FirstOrDefault();
-        //        if (campaignOpening != null)
-        //        {
-        //            // check login history
-        //            var now = DateTime.Now;
-        //            // get config
-        //            var timeDiff = ConfigManager.Instance.GetConfigAppValue("LOGIN_TIME_DIFF") ?? "1440";
-
-
-        //            var missionLogin = dbContext
-        //                .Missions.Where(x => x.Type == CommonConstant.TypeLogin && x.CampaignId == campaignOpening.Id)
-        //                .FirstOrDefault();
-        //            if (missionLogin != null)
-        //            {
-        //                // kiểm tra mission đã được set completed chưa
-        //                var checkCompleted = dbContext
-        //                    .MissionCompleteds.Where(
-        //                        x =>
-        //                            x.Msisdn == account.Msisdn
-        //                            && x.AccountId == account.Id
-        //                            && x.MissionId == missionLogin.Id
-        //                    )
-        //                    .Count();
-
-        //                if (checkCompleted > 0)
-        //                {
-        //                    // mission completed rồi, không cần check login nữa
-        //                    log.Debug($"Misson login {missionLogin.Id} has completed for {msisdn}");
-        //                }
-        //                else
-        //                {
-        //                    log.Debug($"Checking login mission {missionLogin.Id} for {msisdn}");
-
-        //                    // check time required
-        //                    var start = DateTime.Now.AddMinutes(-(int)missionLogin.RequiredTime * int.Parse(timeDiff));
-
-        //                    var completedLogin = dbContext
-        //                    .MissionHistories.Where(
-        //                        x =>
-        //                            x.Msisdn == account.Msisdn
-        //                            && x.AccountId == account.Id
-        //                            && x.MissionId == missionLogin.Id
-        //                            && x.ExecutedTime >= start
-        //                            && x.ExecutedTime <= DateTime.Now
-        //                    )
-        //                    .Count();
-
-        //                    // lần login này đc tính vào completed login
-        //                    if (completedLogin >= missionLogin.RequiredTime - 1)
-        //                    {
-        //                        // save history for this login
-        //                        var missionHistory = new MissionHistory
-        //                        {
-        //                            Id = (decimal)await DbLogic.GenIdAsync(dbContext, "MISSION_HISTORY_SEQ"),
-        //                            Msisdn = account.Msisdn,
-        //                            MissionId = missionLogin.Id,
-        //                            ExecutedTime = DateTime.Now,
-        //                            Status = CommonConstant.StatusSuccess,
-        //                            AccountId = account.Id
-        //                        };
-        //                        dbContext.MissionHistories.Add(missionHistory);
-        //                        await dbContext.SaveChangesAsync();
-
-        //                        // set completed mission
-        //                        var completedMission = await QuestLogic.SetCompletedMissionForUserAsync(
-        //                            dbContext,
-        //                            account.Msisdn,
-        //                            account.Id,
-        //                            missionLogin
-        //                        );
-        //                        if (completedMission != CommonErrorCode.Success)
-        //                        {
-        //                            log.Error("Set completed mission error: " + completedMission);
-        //                        }
-        //                        else
-        //                        {
-        //                            log.Debug("Set completed mission successfully");
-        //                        }
-        //                    }
-        //                    else
-        //                    {
-        //                        // check time diff to save history
-        //                        var startDay = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0);
-        //                        var endDay = startDay.AddMinutes(int.Parse(timeDiff));
-        //                        var loginHistory = dbContext
-        //                            .MissionHistories.Where(
-        //                                x =>
-        //                                    x.Msisdn == account.Msisdn
-        //                                    && x.AccountId == account.Id
-        //                                    && x.ExecutedTime >= startDay
-        //                                    && x.ExecutedTime <= endDay
-        //                                    && x.MissionId == missionLogin.Id
-        //                            )
-        //                            .FirstOrDefault();
-        //                        if (loginHistory == null)
-        //                        {
-        //                            // add history
-        //                            var missionHistory = new MissionHistory
-        //                            {
-        //                                Id = (decimal)await DbLogic.GenIdAsync(dbContext, "MISSION_HISTORY_SEQ"),
-        //                                Msisdn = account.Msisdn,
-        //                                MissionId = missionLogin.Id,
-        //                                ExecutedTime = DateTime.Now,
-        //                                Status = CommonConstant.StatusSuccess,
-        //                                AccountId = account.Id
-        //                            };
-        //                            dbContext.MissionHistories.Add(missionHistory);
-        //                            await dbContext.SaveChangesAsync();
-
-        //                            // add prize for user
-        //                            if (missionLogin.PrizeIdEachTime != null && missionLogin.PrizeIdEachTime > 0)
-        //                            {
-        //                                var addPrizeRes = await QuestLogic.AddPrizeForUser(
-        //                                    dbContext,
-        //                                    account.Msisdn,
-        //                                    missionLogin.Id,
-        //                                    missionLogin.PrizeIdEachTime
-        //                                );
-        //                            }
-        //                        }
-        //                        else
-        //                        {
-        //                            log.Debug($"User already login in {timeDiff}");
-        //                        }
-        //                    }
-
-        //                }
-        //            }
-
-        //            // check vendor package
-        //            var vendorPackage = dbContext
-        //            .VendorPackages.Where(
-        //                x =>
-        //                    (x.Id == campaignOpening.VendorPackageId)
-        //                    && x.Status == CommonConstant.StatusActive
-        //            )
-        //            .FirstOrDefault();
-        //            if (vendorPackage == null)
-        //            {
-        //                log.Error("Not found vendor package");
-        //                canPlay = true;
-        //            }
-        //            else
-        //            {
-        //                vendorPackageId = vendorPackage.Id;
-        //                // check history
-        //                var vendorPackageHistory = dbContext
-        //                    .VendorPackageHistories.Where(
-        //                        x =>
-        //                            x.Msisdn == account.Msisdn
-        //                            && x.VendorPackageId == vendorPackage.Id
-        //                            && x.Status == CommonConstant.StatusSuccess
-        //                    // && x.CompletedTime >= startDay
-        //                    // && x.CompletedTime <= endDay
-        //                    )
-        //                    .OrderByDescending(x => x.CompletedTime)
-        //                    .FirstOrDefault();
-
-        //                bool canCheckRegister = false;
-        //                if (vendorPackageHistory != null)
-        //                {
-        //                    // check if completed today
-        //                    var endTime = vendorPackage.Period == "DAILY"
-        //                        ? vendorPackageHistory.CompletedTime.AddDays(1)
-        //                        : vendorPackage.Period == "WEEKLY"
-        //                            ? vendorPackageHistory.CompletedTime.AddDays(7)
-        //                            : vendorPackage.Period == "MONTHLY"
-        //                                ? vendorPackageHistory.CompletedTime.AddMonths(1)
-        //                                : vendorPackage.Period == "YEARLY"
-        //                                    ? vendorPackageHistory.CompletedTime.AddYears(1)
-        //                                    : vendorPackageHistory.CompletedTime;
-
-        //                    if (DateTime.Now <= endTime)
-        //                    {
-        //                        log.Debug($"User {account.Msisdn} has completed vendor package {vendorPackage.Code} today");
-        //                        canPlay = true;
-        //                    }
-        //                    else
-        //                    {
-        //                        canCheckRegister = true;
-        //                    }
-        //                }
-        //                else
-        //                {
-        //                    canCheckRegister = true;
-        //                }
-        //                if (canCheckRegister)
-        //                {
-        //                    // check user registered service
-        //                    int checkRegistered = await QuestLogic.CheckRegisterServiceForUserAsync(dbContext, account.Msisdn, vendorPackage.Code);
-        //                    if (checkRegistered == CommonConstant.UserRegistered)
-        //                    {
-        //                        log.Debug($"User {account.Msisdn} registered vendor package {vendorPackage.Code}");
-        //                        canPlay = true;
-        //                        // save vendor package history
-        //                        var vendorPackageHistoryNew = new VendorPackageHistory
-        //                        {
-        //                            Id = (decimal)await DbLogic.GenIdAsync(dbContext, "VENDOR_PACKAGE_HISTORY_SEQ"),
-        //                            Msisdn = account.Msisdn,
-        //                            VendorPackageId = vendorPackage.Id,
-        //                            Status = CommonConstant.StatusSuccess,
-        //                            CompletedTime = DateTime.Now,
-        //                            AccountId = account.Id
-        //                        };
-        //                        dbContext.VendorPackageHistories.Add(vendorPackageHistoryNew);
-        //                        await dbContext.SaveChangesAsync();
-        //                    }
-        //                    else
-        //                    {
-        //                        log.Debug($"User {account.Msisdn} not registered vendor package {vendorPackage.Code}");
-        //                        canPlay = false;
-        //                    }
-        //                }
-        //            }
-        //        }
-        //        return DotnetLib.Http.HttpResponse.BuildResponse(
-        //            log,
-        //            url,
-        //            json,
-        //            CommonErrorCode.Success,
-        //            canPlay ? ConfigManager.Instance.GetConfigWebValue("NEED_BUY_VENDOR") : "Success",
-        //            new
-        //            {
-        //                token,
-        //                refreshToken,
-        //                account = new
-        //                {
-        //                    account.Msisdn,
-        //                    account.Username,
-        //                    account.Birthday
-        //                },
-        //                canPlay,
-        //                vendorPackageId
-        //            }
-        //        );
-        //    }
-        //    catch (Exception exception)
-        //    {
-        //        log.Error("Exception: ", exception);
-        //    }
-        //    return DotnetLib.Http.HttpResponse.BuildResponse(
-        //        log,
-        //        url,
-        //        json,
-        //        CommonErrorCode.Error,
-        //        ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
-        //        new { }
-        //    );
-        //}
-
-        //public async Task<IActionResult> UserInvite(HttpRequest httpRequest, UserInviteReq request)
-        //{
-        //    var url = httpRequest.Path;
-        //    var json = JsonConvert.SerializeObject(request);
-        //    log.Debug("URL: " + url + " => Request: " + json);
-        //    try
-        //    {
-        //        var msisdn = CommonLogic.GetDataFromToken(configuration, httpRequest, "Msisdn");
-        //        var msisdnInvited = DotnetLib.Logic.ReuseLogic.TelemorValidateMsisdn(GetParameter("CountryCode"), request.receiver);
-        //        if (msisdnInvited == null)
-        //        {
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                "Invalid msisdn",
-        //                new { }
-        //            );
-        //        }
-        //        log.Debug("msisdn: " + msisdn);
-        //        log.Debug("msisdnInvited: " + msisdnInvited);
-        //        // check time inviting
-        //        var inviting = dbContext
-        //            .InvitingHistories.Where(x => x.Receiver == msisdnInvited)
-        //            .OrderByDescending(x => x.InvitedTime)
-        //            .ToList();
-        //        if (inviting.Count > 0)
-        //        {
-        //            if (inviting.Any(x => x.Status == CommonConstant.StatusClaimed))
-        //            {
-        //                return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                    log,
-        //                    url,
-        //                    json,
-        //                    CommonErrorCode.Error,
-        //                    ConfigManager.Instance.GetConfigWebValue("INVITE_BEFORE"),
-        //                    new { }
-        //                );
-        //            }
-        //            else if (inviting.Any(x => x.InvitedTime.AddHours(24) >= DateTime.Now))
-        //            {
-        //                return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                    log,
-        //                    url,
-        //                    json,
-        //                    CommonErrorCode.Error,
-        //                    ConfigManager.Instance.GetConfigWebValue("INVITE_BEFORE"),
-        //                    new { }
-        //                );
-        //            }
-        //        }
-
-        //        // this user is valid for inviting
-        //        var getInvitingHis = inviting.FindAll(
-        //            x => x.Msisdn == msisdn && x.Receiver == msisdnInvited
-        //        );
-        //        if (getInvitingHis.Count() > 0)
-        //        {
-        //            getInvitingHis[0].InvitedTime = DateTime.Now;
-        //            getInvitingHis[0].Status = CommonConstant.StatusSuccess;
-
-        //            dbContext.InvitingHistories.Update(getInvitingHis[0]);
-        //        }
-        //        else
-        //        {
-        //            var invitingCreate = new InvitingHistory
-        //            {
-        //                Id = (decimal)await DbLogic.GenIdAsync(dbContext, "INVITING_HISTORY_SEQ"),
-        //                Msisdn = msisdn!,
-        //                Receiver = msisdnInvited,
-        //                InvitedTime = DateTime.Now,
-        //                Status = CommonConstant.StatusSuccess,
-        //                AccountId = decimal.Parse(CommonLogic.GetDataFromToken(configuration, httpRequest, "AccountId")!)
-        //            };
-        //            dbContext.InvitingHistories.Add(invitingCreate);
-
-        //            // send mt to owner
-        //            await MtLogic.SendMt(
-        //                dbContext,
-        //                msisdnInvited,
-        //                ConfigManager.Instance.GetConfigSmsValue("PARTNER_INVITE_SUCCESS")
-        //            );
-        //        }
-        //        await dbContext.SaveChangesAsync();
-
-        //        return DotnetLib.Http.HttpResponse.BuildResponse(
-        //            log,
-        //            url,
-        //            json,
-        //            CommonErrorCode.Success,
-        //            ConfigManager.Instance.GetConfigWebValue("INVITE_SUCCESS"),
-        //            new { }
-        //        );
-        //    }
-        //    catch (Exception exception)
-        //    {
-        //        log.Error("Exception: ", exception);
-        //    }
-        //    return DotnetLib.Http.HttpResponse.BuildResponse(
-        //        log,
-        //        url,
-        //        json,
-        //        CommonErrorCode.Error,
-        //        ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
-        //        new { }
-        //    );
-        //}
-
-        //public async Task<IActionResult> VendorPackageRegister(
-        //    HttpRequest httpRequest,
-        //    VendorPackageRegisterReq request
-        //)
-        //{
-        //    var url = httpRequest.Path;
-        //    var json = JsonConvert.SerializeObject(request);
-        //    log.Debug("URL: " + url + " => Request: " + json);
-        //    try
-        //    {
-        //        var msisdn = CommonLogic.GetDataFromToken(configuration, httpRequest, "Msisdn");
-
-        //        if (msisdn == null)
-        //        {
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                ConfigManager.Instance.GetConfigWebValue("MSISDN_INVALID"),
-        //                new { }
-        //            );
-        //        }
-        //        log.Debug("msisdn: " + msisdn);
-        //        // call soap to register
-        //        var webService = dbContext
-        //            .Webservices.Where(x => x.WsCode == "SUBSCRIBE")
-        //            .FirstOrDefault();
-        //        if (webService == null)
-        //        {
-        //            log.Error("Not found webservice SUBSCRIBE");
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                "Not found webservice SUBSCRIBE",
-        //                new { }
-        //            );
-        //        }
-
-        //        // check vendor package
-        //        var vendorPackage = dbContext
-        //            .VendorPackages.Where(
-        //                x =>
-        //                    x.Id == request.vendorPackageId
-        //                    && x.Status == CommonConstant.StatusActive
-        //            )
-        //            .FirstOrDefault();
-        //        if (vendorPackage == null)
-        //        {
-        //            log.Error("Not found vendor package");
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                "Not found vendor package",
-        //                new { }
-        //            );
-        //        }
-
-        //        // check payment channel
-        //        var paymentChannel = dbContext
-        //            .PaymentChannels.Where(
-        //                x =>
-        //                    x.Id == request.paymentChannelId
-        //                    && x.Status == CommonConstant.StatusActive
-        //            )
-        //            .FirstOrDefault();
-        //        if (paymentChannel == null)
-        //        {
-        //            log.Error("Not found payment channel");
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                "Not found payment channel",
-        //                new { }
-        //            );
-        //        }
-
-        //        string transactionId = DotnetLib.Logic.ReuseLogic.GenerateUniqueNumber();
-        //        string body = webService
-        //                .MsgTemplate!.Replace("#MSISDN#", msisdn)
-        //                .Replace("#SERVICE_ID#", vendorPackage.Code)
-        //                .Replace("#PARAM#", "0")
-        //                .Replace("#TRANS_ID#", transactionId);
-
-        //        var (errorCode, envelope) = await QuestLogic.RegisterServiceForUserAsync(
-        //            dbContext,
-        //            webService.Wsdl!,
-        //            msisdn,
-        //            body,
-        //            vendorPackage.Code,
-        //            transactionId
-        //        );
-
-        //        if (errorCode != CommonErrorCode.Success || envelope == null)
-        //        {
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                 ConfigManager.Instance.GetConfigWebValue("REGISTER_FAILED"),
-        //                new { }
-        //            );
-        //        }
-        //        // Lấy danh sách gói:
-        //        var subscriberResponse = envelope.Body!.SubscriberResponse;
-
-        //        if (subscriberResponse != null && subscriberResponse.ResponseCode == "0")
-        //        {
-        //            log.Debug("Register vendor package successfully");
-        //            // save vendor package register to db
-        //            var vendorPackageHistory = new VendorPackageHistory
-        //            {
-        //                Id = (decimal)await DbLogic.GenIdAsync(dbContext, "VENDOR_PACKAGE_HISTORY_SEQ"),
-        //                Msisdn = msisdn!,
-        //                VendorPackageId = vendorPackage.Id,
-        //                Status = CommonConstant.StatusSuccess,
-        //                CompletedTime = DateTime.Now,
-        //                AccountId = decimal.Parse(CommonLogic.GetDataFromToken(configuration, httpRequest, "AccountId")!)
-        //            };
-
-        //            // send mt
-        //            await MtLogic.SendMt(
-        //                dbContext,
-        //                msisdn!,
-        //                ConfigManager.Instance.GetConfigSmsValue("VENDOR_BUYING_SUCCESS").Replace("%MONEY%", vendorPackage.Price.ToString())
-        //            );
-
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Success,
-        //                ConfigManager.Instance.GetConfigWebValue("REGISTER_SUCCESS").Replace("%SERVICE%", vendorPackage.Code),
-        //                new { }
-        //            );
-        //        }
-        //        else
-        //        {
-        //            log.Error("Register vendor package failed");
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                ConfigManager.Instance.GetConfigWebValue("REGISTER_FAILED"),
-        //                new { }
-        //            );
-        //        }
-        //    }
-        //    catch (Exception exception)
-        //    {
-        //        log.Error("Exception: ", exception);
-        //    }
-        //    return DotnetLib.Http.HttpResponse.BuildResponse(
-        //        log,
-        //        url,
-        //        json,
-        //        CommonErrorCode.Error,
-        //        ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
-        //        new { }
-        //    );
-        //}
-
-        //public async Task<IActionResult> LoadVendorPackage(HttpRequest httpRequest, LoadVendorPackageReq request)
-        //{
-        //    var url = httpRequest.Path;
-        //    var json = JsonConvert.SerializeObject(request);
-        //    log.Debug("URL: " + url + " => Request: " + json);
-        //    try
-        //    {
-        //        // check if user completed vendor package
-        //        var campaignOpening = dbContext
-        //            .Campaigns.Where(
-        //                x =>
-        //                    x.Status == CommonConstant.StatusActive
-        //                    && x.FromTime <= DateTime.Now
-        //                    && x.ToTime >= DateTime.Now
-        //            )
-        //            .FirstOrDefault();
-        //        if (campaignOpening == null)
-        //        {
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                "No active campaign",
-        //                new { }
-        //            );
-        //        }
-        //        // load vendor package
-        //        var vendorPackage = dbContext
-        //            .VendorPackages
-        //            .Where(x => x.Status == CommonConstant.StatusActive && x.Id == campaignOpening.VendorPackageId)
-        //            .Select(x => new
-        //            {
-        //                x.Id,
-        //                x.Name,
-        //                x.Code,
-        //                x.Description,
-        //                x.Price,
-        //                x.Type,
-        //                x.Period,
-        //                x.MoneyType,
-        //                x.Introduction,
-        //            })
-        //            .FirstOrDefault();
-        //        if (vendorPackage == null)
-        //        {
-        //            return DotnetLib.Http.HttpResponse.BuildResponse(
-        //                log,
-        //                url,
-        //                json,
-        //                CommonErrorCode.Error,
-        //                "No vendor package found",
-        //                new { }
-        //            );
-        //        }
-        //        return DotnetLib.Http.HttpResponse.BuildResponse(
-        //            log,
-        //            url,
-        //            json,
-        //            CommonErrorCode.Success,
-        //            "Load vendor package successfully",
-        //            new
-        //            {
-        //                vendorPackage
-        //            }
-        //        );
-        //    }
-        //    catch (Exception exception)
-        //    {
-        //        log.Error("Exception: ", exception);
-        //    }
-        //    return DotnetLib.Http.HttpResponse.BuildResponse(
-        //        log,
-        //        url,
-        //        json,
-        //        CommonErrorCode.Error,
-        //        ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
-        //        new { }
-        //    );
-        //}
-
-        #region Auth Methods - OTP Login
 
         /// <summary>
         /// Request OTP to be sent to email
@@ -828,7 +54,7 @@ namespace Esim.Apis.Business
                         url,
                         json,
                         CommonErrorCode.RequiredFieldMissing,
-                        "Email is required",
+                        ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED"),
                         new { }
                     );
                 }
@@ -841,12 +67,12 @@ namespace Esim.Apis.Business
                     .Where(c => c.Email == request.email)
                     .FirstOrDefault();
 
-                int? customerId = customer?.Id;
+                decimal? customerId = customer?.Id;
 
                 if (customer == null)
                 {
-                    // Create new customer record
-                    var newCustomerId = (int)await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
+                    // Create new customer record - manually get ID from Oracle sequence
+                    var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
                     var newCustomer = new CustomerInfo
                     {
                         Id = newCustomerId,
@@ -873,10 +99,10 @@ namespace Esim.Apis.Business
 
                 // Create new OTP record
                 int otpExpireMinutes = 5;
-                var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
+                //var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
                 var otpVerification = new OtpVerification
                 {
-                    Id = otpId,
+                    //Id = otpId,
                     CustomerId = customerId,
                     UserEmail = request.email,
                     OtpCode = otpCode,
@@ -890,33 +116,47 @@ namespace Esim.Apis.Business
                 dbContext.OtpVerifications.Add(otpVerification);
                 await dbContext.SaveChangesAsync();
 
-                // Add to MESSAGE_QUEUE for background email sending using template
-                var messageId = (int)await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
-                
-                // Determine template code based on language
-                // TemplateCode format: OTP_LOGIN (default Vietnamese), OTP_LOGIN_EN, OTP_LOGIN_LO
-                string lang = (request.lang ?? "vi").ToLower();
-                string templateCode = lang switch
-                {
-                    "en" => "OTP_LOGIN_EN",
-                    "lo" => "OTP_LOGIN_LO",
-                    _ => "OTP_LOGIN"  // Default Vietnamese
-                };
+                // Add to MESSAGE_QUEUE for background email sending
+                // Resolve template content now so Worker only needs to send email
+                string lang = (request.lang ?? "lo").ToLower();
+                string templateCode = "OTP_LOGIN";
+
+                // Query template and get language-specific content
+                var template = dbContext.MessageTemplates
+                    .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status == true);
 
-                // Prepare template data as JSON
-                var templateData = System.Text.Json.JsonSerializer.Serialize(new
+                if (template == null)
                 {
-                    OTP_CODE = otpCode,
-                    EXPIRE_MINUTES = otpExpireMinutes.ToString()
-                });
+                    log.Error($"Template '{templateCode}' not found in MESSAGE_TEMPLATE");
+                    throw new Exception($"Email template '{templateCode}' not found");
+                }
+
+                // Get subject based on language (fallback to default column if _LO/_EN is null)
+                string emailSubject = lang == "en"
+                    ? (template.SubjectEn ?? template.Subject ?? "")
+                    : (template.SubjectLo ?? template.Subject ?? "");
+
+                // Get content based on language (fallback to default column if _LO/_EN is null)
+                string emailContent = lang == "en"
+                    ? (template.ContentEn ?? template.Content ?? "")
+                    : (template.ContentLo ?? template.Content ?? "");
+
+                // Replace placeholders in content
+                emailContent = emailContent
+                    .Replace("{{OTP_CODE}}", otpCode)
+                    .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
+
+                // Replace placeholders in subject (if any)
+                emailSubject = emailSubject
+                    .Replace("{{OTP_CODE}}", otpCode)
+                    .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
 
                 var emailMessage = new MessageQueue
                 {
-                    Id = messageId,
                     MessageType = 1, // Email
                     Recipient = request.email,
-                    TemplateCode = templateCode, // Use template based on language
-                    TemplateData = templateData,
+                    Subject = emailSubject,     // Pre-resolved subject
+                    Content = emailContent,     // Pre-resolved content
                     Priority = true, // High priority
                     Status = 0, // Pending
                     ScheduledAt = DateTime.Now,
@@ -936,7 +176,7 @@ namespace Esim.Apis.Business
                     url,
                     json,
                     CommonErrorCode.Success,
-                    "OTP sent successfully",
+                    ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
                     new
                     {
                         email = request.email,
@@ -970,16 +210,20 @@ namespace Esim.Apis.Business
             {
                 if (string.IsNullOrEmpty(request.email) || string.IsNullOrEmpty(request.otpCode))
                 {
+                    string lang = (request.lang ?? "lo").ToLower();
                     return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         url,
                         json,
                         CommonErrorCode.RequiredFieldMissing,
-                        "Email and OTP are required",
+                        ConfigManager.Instance.GetConfigWebValue("EMAIL_OTP_REQUIRED", lang),
                         new { }
                     );
                 }
 
+                // Get language for response messages
+                string responseLang = (request.lang ?? "lo").ToLower();
+
                 // Find valid OTP
                 var otpRecord = dbContext.OtpVerifications
                     .Where(o => o.UserEmail == request.email
@@ -1005,7 +249,7 @@ namespace Esim.Apis.Business
                                 url,
                                 json,
                                 CommonErrorCode.OtpAlreadyUsed,
-                                "OTP has already been used",
+                                ConfigManager.Instance.GetConfigWebValue("OTP_ALREADY_USED", responseLang),
                                 new { }
                             );
                         }
@@ -1016,7 +260,7 @@ namespace Esim.Apis.Business
                                 url,
                                 json,
                                 CommonErrorCode.OtpExpired,
-                                "OTP has expired",
+                                ConfigManager.Instance.GetConfigWebValue("OTP_EXPIRED", responseLang),
                                 new { }
                             );
                         }
@@ -1027,7 +271,7 @@ namespace Esim.Apis.Business
                         url,
                         json,
                         CommonErrorCode.OtpInvalid,
-                        "Invalid OTP",
+                        ConfigManager.Instance.GetConfigWebValue("OTP_INVALID", responseLang),
                         new { }
                     );
                 }
@@ -1047,7 +291,7 @@ namespace Esim.Apis.Business
                         url,
                         json,
                         CommonErrorCode.UserNotFound,
-                        "Customer not found",
+                        ConfigManager.Instance.GetConfigWebValue("USER_NOT_FOUND", responseLang),
                         new { }
                     );
                 }
@@ -1081,7 +325,7 @@ namespace Esim.Apis.Business
                 var userToken = new UserToken
                 {
                     Id = tokenId,
-                    CustomerId = customer.Id.Value,
+                    CustomerId = customer.Id,
                     AccessToken = accessToken,
                     RefreshToken = refreshToken,
                     TokenType = "Bearer",
@@ -1102,10 +346,10 @@ namespace Esim.Apis.Business
                     url,
                     json,
                     CommonErrorCode.Success,
-                    "Login successful",
+                    ConfigManager.Instance.GetConfigWebValue("LOGIN_SUCCESS", responseLang),
                     new
                     {
-                        userId = customer.Id.Value,
+                        userId = customer.Id,
                         email = customer.Email ?? "",
                         fullName = $"{customer.SurName} {customer.LastName}".Trim(),
                         avatarUrl = customer.AvatarUrl,

+ 21 - 14
EsimLao/Esim.Apis/Program.cs

@@ -1,4 +1,5 @@
 using System.Text;
+using Common.Global;
 using Database.Database;
 using Esim.Apis.Business;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -7,11 +8,16 @@ using Microsoft.IdentityModel.Tokens;
 
 var builder = WebApplication.CreateBuilder(args);
 
+// Set global configuration for use by singletons like ConfigManager
+GlobalConfig.Configuration = builder.Configuration;
+
 // Add services to the container.
 builder.Services.AddControllersWithViews();
 
-// Add DbContext
-builder.Services.AddDbContext<ModelContext>();
+// Add DbContext with Oracle provider
+var connectionString = builder.Configuration.GetSection("Connection").Value;
+builder.Services.AddDbContext<ModelContext>(options =>
+    options.UseOracle(connectionString));
 
 // Add Business Services
 builder.Services.AddScoped<IUserBusiness, UserBusinessImpl>();
@@ -45,19 +51,20 @@ builder.Services.AddEndpointsApiExplorer();
 builder.Services.AddSwaggerGen();
 
 var app = builder.Build();
-
+app.UseSwagger();
+app.UseSwaggerUI();
 // Configure the HTTP request pipeline.
-if (app.Environment.IsDevelopment())
-{
-    app.UseSwagger();
-    app.UseSwaggerUI();
-}
-else
-{
-    app.UseExceptionHandler("/Home/Error");
-    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
-    app.UseHsts();
-}
+//if (app.Environment.IsDevelopment())
+//{
+//    app.UseSwagger();
+//    app.UseSwaggerUI();
+//}
+//else
+//{
+//    app.UseExceptionHandler("/Home/Error");
+//    // 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.UseRouting();

+ 20 - 0
EsimLao/Esim.Apis/Properties/PublishProfiles/FolderProfile.pubxml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
+<Project>
+  <PropertyGroup>
+    <DeleteExistingFiles>true</DeleteExistingFiles>
+    <ExcludeApp_Data>false</ExcludeApp_Data>
+    <LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
+    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
+    <LastUsedPlatform>Any CPU</LastUsedPlatform>
+    <PublishProvider>FileSystem</PublishProvider>
+    <PublishUrl>D:\Code\Ex_publish\Lao\Esim\apis</PublishUrl>
+    <WebPublishMethod>FileSystem</WebPublishMethod>
+    <_TargetId>Folder</_TargetId>
+    <SiteUrlToLaunchAfterPublish />
+    <TargetFramework>net9.0</TargetFramework>
+    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
+    <ProjectGuid>f7de4e65-3ca0-47dc-89fc-727cdf7558b8</ProjectGuid>
+    <SelfContained>true</SelfContained>
+  </PropertyGroup>
+</Project>

+ 2 - 2
EsimLao/Esim.Apis/Singleton/ConfigManager.cs

@@ -29,7 +29,7 @@ namespace Esim.Apis.Singleton
             log.Debug("ConfigManager initialized");
         }
 
-        public string GetConfigWebValue(string configName, string lang = "te")
+        public string GetConfigWebValue(string configName, string lang = "lo")
         {
             var config = appConfigs.FirstOrDefault(c => c.Name == configName && c.Type == "WEB");
             if (config != null)
@@ -41,7 +41,7 @@ namespace Esim.Apis.Singleton
             return string.Empty;
         }
 
-        public string GetConfigSmsValue(string configName, string lang = "te")
+        public string GetConfigSmsValue(string configName, string lang = "lo")
         {
             var config = appConfigs.FirstOrDefault(c => c.Name == configName && c.Type == "SMS");
             if (config != null)

+ 0 - 11
EsimLao/Esim.Apis/appsettings.json

@@ -12,17 +12,6 @@
     "Issuer": "EsimLao",
     "Audience": "EsimLaoClient"
   },
-  "Email": {
-    "SmtpServer": "smtp.gmail.com",
-    "SmtpPort": 587,
-    "SenderEmail": "your-email@gmail.com",
-    "SenderName": "EsimLao",
-    "SenderPassword": "your-app-password",
-    "EnableSsl": true
-  },
-  "MessageQueueJob": {
-    "IntervalSeconds": 30
-  },
   "Kestrel": {
     "EndPoints": {
       "Http": {

+ 3 - 6
EsimLao/Esim.SendMail/Esim.SendMail.csproj

@@ -5,15 +5,15 @@
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <OutputType>Exe</OutputType>
+    <!-- Disable trimming for Oracle and other reflection-based libraries -->
+    <PublishTrimmed>false</PublishTrimmed>
+    <PublishAot>false</PublishAot>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="log4net" Version="2.0.15" />
     <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
     <PackageReference Include="MailKit" Version="4.8.0" />
     <PackageReference Include="MimeKit" Version="4.8.0" />
-    <PackageReference Include="Quartz" Version="3.11.0" />
-    <PackageReference Include="Quartz.Extensions.Hosting" Version="3.11.0" />
   </ItemGroup>
 
   <ItemGroup>
@@ -24,9 +24,6 @@
     <None Update="appsettings.json">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
-    <None Update="log4net.config">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
   </ItemGroup>
 
 </Project>

+ 0 - 268
EsimLao/Esim.SendMail/Jobs/MessageQueueJob.cs

@@ -1,268 +0,0 @@
-using Database.Database;
-using Esim.SendMail.Services;
-using Microsoft.EntityFrameworkCore;
-using Quartz;
-
-namespace Esim.SendMail.Jobs;
-
-/// <summary>
-/// Optimized background job for high-volume email sending
-/// - Batch processing
-/// - Move sent messages to history table for optimal queue performance
-/// - Parallel execution with rate limiting
-/// </summary>
-[DisallowConcurrentExecution] // Prevent overlapping executions
-public class MessageQueueJob : IJob
-{
-    private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(MessageQueueJob));
-
-    private readonly ModelContext _dbContext;
-    private readonly IEmailService _emailService;
-    private readonly int _maxMessagesPerRun;
-
-    // Message types
-    private const int MESSAGE_TYPE_EMAIL = 1;
-    private const int MESSAGE_TYPE_SMS = 2;
-    private const int MESSAGE_TYPE_PUSH = 3;
-
-    // Message statuses
-    private const int STATUS_PENDING = 0;
-    private const int STATUS_PROCESSING = 1;
-    private const int STATUS_SUCCESS = 2;
-    private const int STATUS_FAILED = 3;
-
-    public MessageQueueJob(ModelContext dbContext, IEmailService emailService, Microsoft.Extensions.Configuration.IConfiguration configuration)
-    {
-        _dbContext = dbContext;
-        _emailService = emailService;
-        _maxMessagesPerRun = int.Parse(configuration["Job:MaxMessagesPerRun"] ?? "500");
-    }
-
-    public async Task Execute(IJobExecutionContext context)
-    {
-        var startTime = DateTime.Now;
-        log.Debug("MessageQueueJob started");
-
-        try
-        {
-            // Get pending messages with FOR UPDATE SKIP LOCKED pattern
-            // This allows multiple instances to run without conflicts
-            var pendingMessages = await _dbContext.MessageQueues
-                .Where(m => m.Status == STATUS_PENDING
-                    && (m.ScheduledAt == null || m.ScheduledAt <= DateTime.Now)
-                    && (m.RetryCount == null || m.RetryCount < m.MaxRetry))
-                .OrderBy(m => m.Priority) // true (high priority) first
-                .ThenBy(m => m.CreatedDate)
-                .Take(_maxMessagesPerRun)
-                .ToListAsync();
-
-            if (!pendingMessages.Any())
-            {
-                log.Debug("No pending messages");
-                return;
-            }
-
-            log.Info($"Processing {pendingMessages.Count} messages");
-
-            // Mark all as processing first (atomic update)
-            var messageIds = pendingMessages.Select(m => m.Id).ToList();
-            await _dbContext.Database.ExecuteSqlRawAsync(
-                $"UPDATE MESSAGE_QUEUE SET STATUS = {STATUS_PROCESSING} WHERE ID IN ({string.Join(",", messageIds)})");
-
-            // Process by message type
-            var emailMessages = pendingMessages.Where(m => m.MessageType == MESSAGE_TYPE_EMAIL).ToList();
-            var smsMessages = pendingMessages.Where(m => m.MessageType == MESSAGE_TYPE_SMS).ToList();
-            var pushMessages = pendingMessages.Where(m => m.MessageType == MESSAGE_TYPE_PUSH).ToList();
-
-            // Process emails in parallel batches
-            if (emailMessages.Any())
-            {
-                await ProcessEmailsAsync(emailMessages);
-            }
-
-            // Process SMS (placeholder)
-            foreach (var msg in smsMessages)
-            {
-                msg.Status = STATUS_FAILED;
-                msg.ErrorMessage = "SMS not implemented";
-                msg.ProcessedAt = DateTime.Now;
-            }
-
-            // Process Push (placeholder)
-            foreach (var msg in pushMessages)
-            {
-                msg.Status = STATUS_FAILED;
-                msg.ErrorMessage = "Push not implemented";
-                msg.ProcessedAt = DateTime.Now;
-            }
-
-            await _dbContext.SaveChangesAsync();
-
-            // Move completed messages to history table
-            await MoveToHistoryAsync(pendingMessages.Where(m => m.Status == STATUS_SUCCESS || m.Status == STATUS_FAILED).ToList());
-
-            var elapsed = DateTime.Now - startTime;
-            log.Info($"Job completed in {elapsed.TotalMilliseconds:F0}ms - Processed: {pendingMessages.Count}");
-        }
-        catch (Exception ex)
-        {
-            log.Error("MessageQueueJob error", ex);
-        }
-    }
-
-    private async Task ProcessEmailsAsync(List<MessageQueue> messages)
-    {
-        log.Info($"Processing {messages.Count} emails");
-
-        var emailBatch = messages.Select(m => new EmailMessage
-        {
-            Id = m.Id,
-            To = m.Recipient,
-            Subject = GetEmailSubject(m),
-            Body = GetEmailContent(m),
-            IsHtml = true
-        }).ToList();
-
-        // Process in parallel with rate limiting
-        var tasks = new List<Task>();
-        foreach (var msg in messages)
-        {
-            var emailMsg = emailBatch.First(e => e.Id == msg.Id);
-            
-            tasks.Add(Task.Run(async () =>
-            {
-                bool success = await _emailService.SendEmailAsync(
-                    emailMsg.To,
-                    emailMsg.Subject,
-                    emailMsg.Body,
-                    emailMsg.IsHtml);
-
-                if (success)
-                {
-                    msg.Status = STATUS_SUCCESS;
-                    msg.ProcessedAt = DateTime.Now;
-                    msg.ErrorMessage = null;
-                }
-                else
-                {
-                    HandleFailure(msg);
-                }
-            }));
-        }
-
-        await Task.WhenAll(tasks);
-    }
-
-    private string GetEmailSubject(MessageQueue message)
-    {
-        if (!string.IsNullOrEmpty(message.Subject))
-            return message.Subject;
-
-        // Get from template if available
-        if (!string.IsNullOrEmpty(message.TemplateCode))
-        {
-            var template = _dbContext.MessageTemplates
-                .FirstOrDefault(t => t.TemplateCode == message.TemplateCode && t.Status == true);
-            if (template != null)
-                return template.Subject ?? "No Subject";
-        }
-
-        return "No Subject";
-    }
-
-    private string GetEmailContent(MessageQueue message)
-    {
-        string content = message.Content ?? "";
-
-        // Use template if specified
-        if (!string.IsNullOrEmpty(message.TemplateCode))
-        {
-            var template = _dbContext.MessageTemplates
-                .FirstOrDefault(t => t.TemplateCode == message.TemplateCode && t.Status == true);
-
-            if (template != null)
-            {
-                content = template.Content ?? content;
-            }
-        }
-
-        // Replace placeholders
-        if (!string.IsNullOrEmpty(message.TemplateData))
-        {
-            try
-            {
-                var data = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(message.TemplateData);
-                if (data != null)
-                {
-                    foreach (var kvp in data)
-                    {
-                        content = content.Replace($"{{{{{kvp.Key}}}}}", kvp.Value);
-                    }
-                }
-            }
-            catch (Exception ex)
-            {
-                log.Warn($"Failed to parse template data: {ex.Message}");
-            }
-        }
-
-        return content;
-    }
-
-    private void HandleFailure(MessageQueue message)
-    {
-        message.RetryCount = (byte?)((message.RetryCount ?? 0) + 1);
-        message.ProcessedAt = DateTime.Now;
-
-        if (message.RetryCount >= message.MaxRetry)
-        {
-            message.Status = STATUS_FAILED;
-            log.Warn($"Message {message.Id} failed after max retries");
-        }
-        else
-        {
-            message.Status = STATUS_PENDING; // Will be retried
-            log.Debug($"Message {message.Id} will retry ({message.RetryCount}/{message.MaxRetry})");
-        }
-    }
-
-    /// <summary>
-    /// Move completed messages to MESSAGE_QUEUE_HIS for optimal queue performance
-    /// </summary>
-    private async Task MoveToHistoryAsync(List<MessageQueue> completedMessages)
-    {
-        if (!completedMessages.Any()) return;
-
-        try
-        {
-            var ids = completedMessages.Select(m => m.Id).ToList();
-            var idsString = string.Join(",", ids);
-
-            // Insert into history table
-            var insertSql = $@"
-                INSERT INTO MESSAGE_QUEUE_HIS 
-                (ID, MESSAGE_TYPE, RECIPIENT, SUBJECT, CONTENT, TEMPLATE_CODE, TEMPLATE_DATA, 
-                 PRIORITY, STATUS, SCHEDULED_AT, PROCESSED_AT, RETRY_COUNT, MAX_RETRY, 
-                 ERROR_MESSAGE, CREATED_BY, CREATED_DATE, MOVED_DATE)
-                SELECT 
-                    ID, MESSAGE_TYPE, RECIPIENT, SUBJECT, CONTENT, TEMPLATE_CODE, TEMPLATE_DATA,
-                    PRIORITY, STATUS, SCHEDULED_AT, PROCESSED_AT, RETRY_COUNT, MAX_RETRY,
-                    ERROR_MESSAGE, CREATED_BY, CREATED_DATE, SYSDATE
-                FROM MESSAGE_QUEUE
-                WHERE ID IN ({idsString})";
-
-            await _dbContext.Database.ExecuteSqlRawAsync(insertSql);
-
-            // Delete from main queue
-            var deleteSql = $"DELETE FROM MESSAGE_QUEUE WHERE ID IN ({idsString})";
-            await _dbContext.Database.ExecuteSqlRawAsync(deleteSql);
-
-            log.Info($"Moved {completedMessages.Count} messages to history");
-        }
-        catch (Exception ex)
-        {
-            log.Error($"Failed to move messages to history: {ex.Message}", ex);
-            // Don't throw - messages are already processed, just not moved to history
-        }
-    }
-}

+ 237 - 0
EsimLao/Esim.SendMail/MessageQueueWorker.cs

@@ -0,0 +1,237 @@
+using Database.Database;
+using Esim.SendMail.Services;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Esim.SendMail;
+
+/// <summary>
+/// High-performance background worker for processing message queue.
+/// Uses native .NET BackgroundService for maximum .NET 9 compatibility.
+/// Features:
+/// - Configurable polling interval
+/// - Batch processing
+/// - Automatic retry handling
+/// - Graceful shutdown
+/// </summary>
+public class MessageQueueWorker : BackgroundService
+{
+    private readonly ILogger<MessageQueueWorker> _logger;
+    private readonly IServiceProvider _serviceProvider;
+    private readonly IEmailService _emailService;
+    private readonly int _intervalSeconds;
+    private readonly int _maxMessagesPerRun;
+
+    // Message types
+    private const int MESSAGE_TYPE_EMAIL = 1;
+    private const int MESSAGE_TYPE_SMS = 2;
+    private const int MESSAGE_TYPE_PUSH = 3;
+
+    // Message statuses
+    private const int STATUS_PENDING = 0;
+    private const int STATUS_PROCESSING = 1;
+    private const int STATUS_SUCCESS = 2;
+    private const int STATUS_FAILED = 3;
+
+    public MessageQueueWorker(
+        ILogger<MessageQueueWorker> logger,
+        IServiceProvider serviceProvider,
+        IEmailService emailService,
+        IConfiguration configuration)
+    {
+        _logger = logger;
+        _serviceProvider = serviceProvider;
+        _emailService = emailService;
+        _intervalSeconds = int.Parse(configuration["Job:IntervalSeconds"] ?? "10");
+        _maxMessagesPerRun = int.Parse(configuration["Job:MaxMessagesPerRun"] ?? "500");
+    }
+
+    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+    {
+        _logger.LogInformation("MessageQueueWorker started. Interval: {Interval}s, MaxPerRun: {Max}", 
+            _intervalSeconds, _maxMessagesPerRun);
+
+        while (!stoppingToken.IsCancellationRequested)
+        {
+            try
+            {
+                await ProcessMessagesAsync(stoppingToken);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in message processing loop");
+            }
+
+            await Task.Delay(TimeSpan.FromSeconds(_intervalSeconds), stoppingToken);
+        }
+
+        _logger.LogInformation("MessageQueueWorker stopped");
+    }
+
+    private async Task ProcessMessagesAsync(CancellationToken stoppingToken)
+    {
+        var startTime = DateTime.Now;
+
+        // Create a new scope for DbContext (transient)
+        using var scope = _serviceProvider.CreateScope();
+        var dbContext = scope.ServiceProvider.GetRequiredService<ModelContext>();
+
+        try
+        {
+            // Get pending messages
+            var pendingMessages = await dbContext.MessageQueues
+                .Where(m => m.Status == STATUS_PENDING
+                    && (m.ScheduledAt == null || m.ScheduledAt <= DateTime.Now)
+                    && (m.RetryCount == null || m.RetryCount < m.MaxRetry))
+                .OrderBy(m => m.Priority)
+                .ThenBy(m => m.CreatedDate)
+                .Take(_maxMessagesPerRun)
+                .ToListAsync(stoppingToken);
+
+            if (!pendingMessages.Any())
+            {
+                _logger.LogDebug("No pending messages");
+                return;
+            }
+
+            _logger.LogInformation("Processing {Count} messages", pendingMessages.Count);
+
+            // Mark all as processing
+            var messageIds = pendingMessages.Select(m => m.Id).ToList();
+            await dbContext.Database.ExecuteSqlRawAsync(
+                $"UPDATE MESSAGE_QUEUE SET STATUS = {STATUS_PROCESSING} WHERE ID IN ({string.Join(",", messageIds)})",
+                stoppingToken);
+
+            // Process by message type
+            var emailMessages = pendingMessages.Where(m => m.MessageType == MESSAGE_TYPE_EMAIL).ToList();
+
+            // Process emails
+            if (emailMessages.Any())
+            {
+                await ProcessEmailsAsync(dbContext, emailMessages, stoppingToken);
+            }
+
+            // Mark SMS/Push as not implemented
+            foreach (var msg in pendingMessages.Where(m => m.MessageType != MESSAGE_TYPE_EMAIL))
+            {
+                msg.Status = STATUS_FAILED;
+                msg.ErrorMessage = "Message type not implemented";
+                msg.ProcessedAt = DateTime.Now;
+            }
+
+            await dbContext.SaveChangesAsync(stoppingToken);
+
+            // Move completed messages to history
+            await MoveToHistoryAsync(dbContext, 
+                pendingMessages.Where(m => m.Status == STATUS_SUCCESS || m.Status == STATUS_FAILED).ToList(),
+                stoppingToken);
+
+            var elapsed = DateTime.Now - startTime;
+            _logger.LogInformation("Processed {Count} messages in {Elapsed:F0}ms", 
+                pendingMessages.Count, elapsed.TotalMilliseconds);
+        }
+        catch (OperationCanceledException)
+        {
+            _logger.LogInformation("Processing cancelled");
+        }
+        catch (Exception ex)
+        {
+            _logger.LogError(ex, "Error processing messages");
+        }
+    }
+
+    private async Task ProcessEmailsAsync(ModelContext dbContext, List<MessageQueue> messages, CancellationToken stoppingToken)
+    {
+        _logger.LogInformation("Processing {Count} emails", messages.Count);
+
+        var tasks = messages.Select(async msg =>
+        {
+            if (stoppingToken.IsCancellationRequested) return;
+
+            try
+            {
+                // Subject and Content are pre-resolved at insert time
+                var subject = msg.Subject ?? "No Subject";
+                var content = msg.Content ?? "";
+
+                bool success = await _emailService.SendEmailAsync(
+                    msg.Recipient ?? "",
+                    subject,
+                    content,
+                    true);
+
+                _logger.LogDebug("Email sent to {Recipient}", msg.Recipient);
+
+                if (success)
+                {
+                    msg.Status = STATUS_SUCCESS;
+                    msg.ProcessedAt = DateTime.Now;
+                    msg.ErrorMessage = null;
+                }
+                else
+                {
+                    HandleFailure(msg);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Failed to send email {Id}", msg.Id);
+                msg.ErrorMessage = ex.Message;
+                HandleFailure(msg);
+            }
+        });
+
+        await Task.WhenAll(tasks);
+    }
+
+    private void HandleFailure(MessageQueue message)
+    {
+        message.RetryCount = (byte?)((message.RetryCount ?? 0) + 1);
+        message.ProcessedAt = DateTime.Now;
+
+        if (message.RetryCount >= message.MaxRetry)
+        {
+            message.Status = STATUS_FAILED;
+            _logger.LogWarning("Message {Id} failed after max retries", message.Id);
+        }
+        else
+        {
+            message.Status = STATUS_PENDING;
+            _logger.LogDebug("Message {Id} will retry ({RetryCount}/{MaxRetry})", 
+                message.Id, message.RetryCount, message.MaxRetry);
+        }
+    }
+
+    private async Task MoveToHistoryAsync(ModelContext dbContext, List<MessageQueue> completedMessages, CancellationToken stoppingToken)
+    {
+        if (!completedMessages.Any()) return;
+
+        try
+        {
+            var idsString = string.Join(",", completedMessages.Select(m => m.Id));
+
+            var insertSql = $@"
+                INSERT INTO MESSAGE_QUEUE_HIS 
+                (ID, MESSAGE_TYPE, RECIPIENT, SUBJECT, CONTENT, TEMPLATE_CODE, TEMPLATE_DATA, 
+                 PRIORITY, STATUS, SCHEDULED_AT, PROCESSED_AT, RETRY_COUNT, MAX_RETRY, 
+                 ERROR_MESSAGE, CREATED_BY, CREATED_DATE, MOVED_DATE)
+                SELECT 
+                    ID, MESSAGE_TYPE, RECIPIENT, SUBJECT, CONTENT, TEMPLATE_CODE, TEMPLATE_DATA,
+                    PRIORITY, STATUS, SCHEDULED_AT, PROCESSED_AT, RETRY_COUNT, MAX_RETRY,
+                    ERROR_MESSAGE, CREATED_BY, CREATED_DATE, SYSDATE
+                FROM MESSAGE_QUEUE
+                WHERE ID IN ({idsString})";
+
+            await dbContext.Database.ExecuteSqlRawAsync(insertSql, stoppingToken);
+
+            var deleteSql = $"DELETE FROM MESSAGE_QUEUE WHERE ID IN ({idsString})";
+            await dbContext.Database.ExecuteSqlRawAsync(deleteSql, stoppingToken);
+
+            _logger.LogInformation("Moved {Count} messages to history", completedMessages.Count);
+        }
+        catch (Exception ex)
+        {
+            _logger.LogError(ex, "Failed to move messages to history");
+        }
+    }
+}

+ 18 - 39
EsimLao/Esim.SendMail/Program.cs

@@ -1,67 +1,46 @@
 using Database.Database;
-using Esim.SendMail.Jobs;
 using Esim.SendMail.Services;
 using Microsoft.EntityFrameworkCore;
-using Quartz;
+using Microsoft.Extensions.Logging;
 
 namespace Esim.SendMail;
 
 public class Program
 {
-    private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(Program));
-
     public static async Task Main(string[] args)
     {
-        // Configure log4net
-        var logRepository = log4net.LogManager.GetRepository(System.Reflection.Assembly.GetEntryAssembly());
-        log4net.Config.XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config"));
-
-        log.Info("===========================================");
-        log.Info("Esim.SendMail Service Starting...");
-        log.Info("===========================================");
-
         var builder = Host.CreateApplicationBuilder(args);
 
         // Add configuration
         builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
 
+        // Configure logging
+        builder.Logging.ClearProviders();
+        builder.Logging.AddConsole();
+        builder.Logging.SetMinimumLevel(LogLevel.Information);
+
         // Add DbContext
         var connectionString = builder.Configuration["Connection"];
         builder.Services.AddDbContext<ModelContext>(options =>
-            options.UseOracle(connectionString));
+            options.UseOracle(connectionString), ServiceLifetime.Transient);
 
         // Add Email Service as singleton for connection pooling
         builder.Services.AddSingleton<IEmailService, HighPerformanceEmailService>();
 
-        // Configure Quartz
-        var jobIntervalSeconds = int.Parse(builder.Configuration["Job:IntervalSeconds"] ?? "10");
-        
-        builder.Services.AddQuartz(q =>
-        {
-            q.UseMicrosoftDependencyInjectionJobFactory();
-
-            var jobKey = new JobKey("MessageQueueJob");
-            
-            q.AddJob<MessageQueueJob>(opts => opts.WithIdentity(jobKey));
-            
-            q.AddTrigger(opts => opts
-                .ForJob(jobKey)
-                .WithIdentity("MessageQueueJob-trigger")
-                .StartNow()
-                .WithSimpleSchedule(x => x
-                    .WithIntervalInSeconds(jobIntervalSeconds)
-                    .RepeatForever()));
-        });
-
-        builder.Services.AddQuartzHostedService(q =>
-        {
-            q.WaitForJobsToComplete = true;
-        });
+        // Add the background worker service
+        builder.Services.AddHostedService<MessageQueueWorker>();
 
         var host = builder.Build();
 
-        log.Info($"Job interval: {jobIntervalSeconds} seconds");
-        log.Info("Service started successfully. Press Ctrl+C to stop.");
+        // Log startup info
+        var logger = host.Services.GetRequiredService<ILogger<Program>>();
+        var jobInterval = builder.Configuration["Job:IntervalSeconds"] ?? "10";
+        
+        logger.LogInformation("===========================================");
+        logger.LogInformation("Esim.SendMail Service Starting...");
+        logger.LogInformation("===========================================");
+        logger.LogInformation("Job interval: {IntervalSeconds} seconds", jobInterval);
+        logger.LogInformation("Service started successfully. Press Ctrl+C to stop.");
 
         await host.RunAsync();
     }

+ 16 - 0
EsimLao/Esim.SendMail/Properties/PublishProfiles/FolderProfile.pubxml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
+<Project>
+  <PropertyGroup>
+    <Configuration>Release</Configuration>
+    <Platform>Any CPU</Platform>
+    <PublishDir>D:\Code\Ex_publish\Lao\Esim\sendmail</PublishDir>
+    <PublishProtocol>FileSystem</PublishProtocol>
+    <_TargetId>Folder</_TargetId>
+    <TargetFramework>net9.0</TargetFramework>
+    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
+    <SelfContained>true</SelfContained>
+    <PublishSingleFile>false</PublishSingleFile>
+    <PublishTrimmed>true</PublishTrimmed>
+  </PropertyGroup>
+</Project>

+ 16 - 15
EsimLao/Esim.SendMail/Services/EmailService.cs

@@ -3,6 +3,7 @@ using MailKit.Net.Smtp;
 using MailKit.Security;
 using MimeKit;
 using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
 
 namespace Esim.SendMail.Services;
 
@@ -19,7 +20,7 @@ public interface IEmailService
 
 public class EmailMessage
 {
-    public int Id { get; set; }
+    public decimal Id { get; set; }
     public string To { get; set; } = null!;
     public string Subject { get; set; } = null!;
     public string Body { get; set; } = null!;
@@ -35,8 +36,7 @@ public class EmailResult
 
 public class HighPerformanceEmailService : IEmailService, IDisposable
 {
-    private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(HighPerformanceEmailService));
-
+    private readonly ILogger<HighPerformanceEmailService> _logger;
     private readonly IConfiguration _configuration;
     private readonly string _smtpServer;
     private readonly int _smtpPort;
@@ -53,8 +53,9 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
     private readonly SemaphoreSlim _poolSemaphore;
     private bool _disposed = false;
 
-    public HighPerformanceEmailService(IConfiguration configuration)
+    public HighPerformanceEmailService(ILogger<HighPerformanceEmailService> logger, IConfiguration configuration)
     {
+        _logger = logger;
         _configuration = configuration;
         _smtpServer = _configuration["Email:SmtpServer"] ?? "smtp.gmail.com";
         _smtpPort = int.Parse(_configuration["Email:SmtpPort"] ?? "587");
@@ -75,7 +76,7 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
 
     private void InitializeConnectionPool()
     {
-        log.Info($"Initializing SMTP connection pool with {_connectionPoolSize} connections");
+        _logger.LogInformation("Initializing SMTP connection pool with {PoolSize} connections", _connectionPoolSize);
         
         for (int i = 0; i < _connectionPoolSize; i++)
         {
@@ -89,11 +90,11 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
             }
             catch (Exception ex)
             {
-                log.Warn($"Failed to create initial SMTP connection {i + 1}: {ex.Message}");
+                _logger.LogWarning("Failed to create initial SMTP connection {Index}: {Message}", i + 1, ex.Message);
             }
         }
         
-        log.Info($"Connection pool initialized with {_connectionPool.Count} connections");
+        _logger.LogInformation("Connection pool initialized with {Count} connections", _connectionPool.Count);
     }
 
     private SmtpClient? CreateConnectedClient()
@@ -111,7 +112,7 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
         }
         catch (Exception ex)
         {
-            log.Error($"Failed to create SMTP connection: {ex.Message}");
+            _logger.LogError("Failed to create SMTP connection: {Message}", ex.Message);
             return null;
         }
     }
@@ -172,19 +173,19 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
             client = await GetClientFromPoolAsync();
             if (client == null)
             {
-                log.Error("Failed to get SMTP client from pool");
+                _logger.LogError("Failed to get SMTP client from pool");
                 return false;
             }
 
             var message = CreateMimeMessage(to, subject, body, isHtml);
             await client.SendAsync(message);
             
-            log.Debug($"Email sent to {to}");
+            _logger.LogDebug("Email sent to {To}", to);
             return true;
         }
         catch (Exception ex)
         {
-            log.Error($"Failed to send email to {to}: {ex.Message}");
+            _logger.LogError("Failed to send email to {To}: {Message}", to, ex.Message);
             
             // Force reconnect on error
             if (client != null)
@@ -211,7 +212,7 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
         var messageList = messages.ToList();
         if (!messageList.Any()) return 0;
 
-        log.Info($"Sending batch of {messageList.Count} emails");
+        _logger.LogInformation("Sending batch of {Count} emails", messageList.Count);
         
         int successCount = 0;
         var semaphore = new SemaphoreSlim(_maxConcurrentSends);
@@ -239,7 +240,7 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
         var results = await Task.WhenAll(tasks);
         successCount = results.Count(r => r);
         
-        log.Info($"Batch complete: {successCount}/{messageList.Count} successful");
+        _logger.LogInformation("Batch complete: {SuccessCount}/{TotalCount} successful", successCount, messageList.Count);
         return successCount;
     }
 
@@ -269,7 +270,7 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
         if (_disposed) return;
         _disposed = true;
 
-        log.Info("Disposing email service...");
+        _logger.LogInformation("Disposing email service...");
 
         while (_connectionPool.TryTake(out var client))
         {
@@ -285,6 +286,6 @@ public class HighPerformanceEmailService : IEmailService, IDisposable
         }
 
         _poolSemaphore.Dispose();
-        log.Info("Email service disposed");
+        _logger.LogInformation("Email service disposed");
     }
 }

+ 7 - 0
EsimLao/Esim.SendMail/appsettings.json

@@ -21,5 +21,12 @@
       "Default": "Information",
       "Microsoft.Hosting.Lifetime": "Information"
     }
+  },
+  "Kestrel": {
+    "EndPoints": {
+      "Http": {
+        "Url": "http://0.0.0.0:8361"
+      }
+    }
   }
 }

+ 30 - 25
EsimLao/Esim.SendMail/log4net.config

@@ -1,27 +1,32 @@
 <?xml version="1.0" encoding="utf-8"?>
 <log4net>
-  <root>
-    <level value="DEBUG" />
-    <appender-ref ref="ConsoleAppender" />
-    <appender-ref ref="RollingFileAppender" />
-  </root>
-
-  <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
-    <layout type="log4net.Layout.PatternLayout">
-      <conversionPattern value="%date{HH:mm:ss} [%level] %logger - %message%newline" />
-    </layout>
-  </appender>
-
-  <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
-    <file value="../../logs/send_mail/sendmail.log" />
-    <appendToFile value="true" />
-    <rollingStyle value="Date" />
-    <datePattern value="yyyyMMdd" />
-    <maxSizeRollBackups value="30" />
-    <maximumFileSize value="10MB" />
-    <staticLogFileName value="true" />
-    <layout type="log4net.Layout.PatternLayout">
-      <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
-    </layout>
-  </appender>
-</log4net>
+	<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
+		<!-- File log hiện tại -->
+		<file value="../../logs/send_mail/full.log" />
+		<appendToFile value="true" />
+		<!-- Rolling theo ngày + size -->
+		<rollingStyle value="Composite" />
+		<datePattern value="dd_MM_yyyy'.log'" />
+		<!-- Hậu tố ngày -->
+		<staticLogFileName value="true" />
+		<!-- File mới nhất luôn là full.log -->
+		<!-- Giới hạn dung lượng -->
+		<maximumFileSize value="10MB" />
+		<maxSizeRollBackups value="30" />
+		<!-- Format log -->
+		<layout type="log4net.Layout.PatternLayout">
+			<conversionPattern value="%date %-5level %logger - %message%newline" />
+		</layout>
+	</appender>
+	<appender name="console" type="log4net.Appender.ConsoleAppender">
+		<layout type="log4net.Layout.PatternLayout">
+			<conversionPattern value="%date %level - %message%newline" />
+		</layout>
+		<threshold value="ALL" />
+	</appender>
+	<root>
+		<level value="DEBUG" />
+		<appender-ref ref="RollingFileAppender" />
+		<appender-ref ref="console" />
+	</root>
+</log4net>

+ 7 - 1
EsimLao/EsimLao.sln

@@ -1,7 +1,7 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 17
-VisualStudioVersion = 17.14.36203.30 d17.14
+VisualStudioVersion = 17.14.36203.30
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database", "Database\Database.csproj", "{52517689-CC3F-4095-8F1A-89A70A149B27}"
 EndProject
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esim.Apis", "Esim.Apis\Esim
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esim.Cms", "Esim.Cms\Esim.Cms.csproj", "{15E20E1D-CA3B-4229-93BC-340D80E6DE25}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esim.SendMail", "Esim.SendMail\Esim.SendMail.csproj", "{AAF9AD79-8889-47BF-86BF-FAD4A013BD4D}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -33,6 +35,10 @@ Global
 		{15E20E1D-CA3B-4229-93BC-340D80E6DE25}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{15E20E1D-CA3B-4229-93BC-340D80E6DE25}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{15E20E1D-CA3B-4229-93BC-340D80E6DE25}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AAF9AD79-8889-47BF-86BF-FAD4A013BD4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AAF9AD79-8889-47BF-86BF-FAD4A013BD4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AAF9AD79-8889-47BF-86BF-FAD4A013BD4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AAF9AD79-8889-47BF-86BF-FAD4A013BD4D}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 52 - 21
EsimLao/docs/api_auth_otp.md → EsimLao/docs/api_auth_otp.txt

@@ -2,7 +2,7 @@
 
 ## Overview
 API xác thực người dùng qua email với OTP (One-Time Password).
-
+URL_UAT : http://149.28.132.56:8360/
 ---
 
 ## 1. Request OTP
@@ -23,7 +23,7 @@ POST /apis/auth/request-otp
 ```json
 {
     "email": "user@example.com",
-    "lang": "vi"
+    "lang": "lo"  // Default: "lo" / en
 }
 ```
 
@@ -37,7 +37,7 @@ POST /apis/auth/request-otp
 ```json
 {
     "code": 0,
-    "message": "OTP sent successfully",
+    "message": "<Config: OTP_SENT_SUCCESS>",
     "data": {
         "email": "user@example.com",
         "expireInSeconds": 300
@@ -47,9 +47,17 @@ POST /apis/auth/request-otp
 
 ### Response Error (200)
 ```json
+// Email không được cung cấp
+{
+    "code": -801,
+    "message": "<Config: EMAIL_REQUIRED>",
+    "data": {}
+}
+
+// Lỗi hệ thống
 {
-    "code": 1,
-    "message": "Email is required",
+    "code": -6,
+    "message": "<Config: SYSTEM_FAILURE>",
     "data": {}
 }
 ```
@@ -57,8 +65,8 @@ POST /apis/auth/request-otp
 ### Response Fields
 | Field | Type | Description |
 |-------|------|-------------|
-| code | int | 0 = Success, khác 0 = Error |
-| message | string | Thông báo kết quả |
+| code | int | 0 = Success, khác 0 = Error (xem Error Codes) |
+| message | string | Thông báo từ CONFIG table (theo ngôn ngữ) |
 | data.email | string | Email đã gửi OTP |
 | data.expireInSeconds | int | Thời gian OTP hết hạn (giây) |
 
@@ -88,21 +96,23 @@ POST /apis/auth/verify-otp
 ```json
 {
     "email": "user@example.com",
-    "otpCode": "123456"
+    "otpCode": "123456",
+    "lang": "lo"  // Default: "lo" / en
 }
 ```
 
 ### Parameters
-| Field | Type | Required | Description |
-|-------|------|----------|-------------|
-| email | string | Yes | Email đã nhận OTP |
-| otpCode | string | Yes | Mã OTP 6 số |
+| Field | Type | Required | Default | Description |
+|-------|------|----------|---------|-------------|
+| email | string | Yes | - | Email đã nhận OTP |
+| otpCode | string | Yes | - | Mã OTP 6 số |
+| lang | string | No | "lo" | Ngôn ngữ thông báo: `lo` (ລາວ), `en` (English) |
 
 ### Response Success (200)
 ```json
 {
     "code": 0,
-    "message": "Login successful",
+    "message": "<Config: LOGIN_SUCCESS>",
     "data": {
         "userId": 12345,
         "email": "user@example.com",
@@ -117,24 +127,45 @@ POST /apis/auth/verify-otp
 
 ### Response Error Cases
 ```json
+// Thiếu email hoặc OTP
+{
+    "code": -801,
+    "message": "<Config: EMAIL_OTP_REQUIRED>",
+    "data": {}
+}
+
 // OTP không hợp lệ
 {
-    "code": 1,
-    "message": "Invalid OTP",
+    "code": -201,
+    "message": "<Config: OTP_INVALID>",
     "data": {}
 }
 
 // OTP đã được sử dụng
 {
-    "code": 1,
-    "message": "OTP has already been used",
+    "code": -203,
+    "message": "<Config: OTP_ALREADY_USED>",
     "data": {}
 }
 
 // OTP đã hết hạn
 {
-    "code": 1,
-    "message": "OTP has expired",
+    "code": -202,
+    "message": "<Config: OTP_EXPIRED>",
+    "data": {}
+}
+
+// Không tìm thấy người dùng
+{
+    "code": -300,
+    "message": "<Config: USER_NOT_FOUND>",
+    "data": {}
+}
+
+// Lỗi hệ thống
+{
+    "code": -6,
+    "message": "<Config: SYSTEM_FAILURE>",
     "data": {}
 }
 ```
@@ -142,8 +173,8 @@ POST /apis/auth/verify-otp
 ### Response Fields
 | Field | Type | Description |
 |-------|------|-------------|
-| code | int | 0 = Success, khác 0 = Error |
-| message | string | Thông báo kết quả |
+| code | int | 0 = Success, khác 0 = Error (xem Error Codes) |
+| message | string | Thông báo từ CONFIG table (theo ngôn ngữ) |
 | data.userId | int | ID người dùng |
 | data.email | string | Email người dùng |
 | data.fullName | string | Họ tên đầy đủ |