diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/CreateReportCommandValidator.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/CreateReportCommandValidator.cs
new file mode 100644
index 00000000..80265f3f
--- /dev/null
+++ b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/CreateReportCommandValidator.cs
@@ -0,0 +1,38 @@
+using FluentValidation;
+using AdsAnalyticsService.API.Application.Commands;
+
+namespace AdsAnalyticsService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateReportCommand.
+/// VI: Validator cho CreateReportCommand.
+///
+public class CreateReportCommandValidator : AbstractValidator
+{
+ public CreateReportCommandValidator()
+ {
+ RuleFor(x => x.AdvertiserId)
+ .NotEmpty()
+ .WithMessage("Advertiser ID is required / Advertiser ID là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithMessage("Report name is required / Tên báo cáo là bắt buộc")
+ .MaximumLength(200)
+ .WithMessage("Report name max 200 characters / Tên báo cáo tối đa 200 ký tự");
+
+ RuleFor(x => x.ReportType)
+ .IsInEnum()
+ .WithMessage("Report type is invalid / Loại báo cáo không hợp lệ");
+
+ RuleFor(x => x.StartDate)
+ .NotEmpty()
+ .WithMessage("Start date is required / Ngày bắt đầu là bắt buộc");
+
+ RuleFor(x => x.EndDate)
+ .NotEmpty()
+ .WithMessage("End date is required / Ngày kết thúc là bắt buộc")
+ .GreaterThan(x => x.StartDate)
+ .WithMessage("End date must be after start date / Ngày kết thúc phải sau ngày bắt đầu");
+ }
+}
diff --git a/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/AddFundsCommandValidator.cs b/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/AddFundsCommandValidator.cs
new file mode 100644
index 00000000..fbdf3519
--- /dev/null
+++ b/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/AddFundsCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using AdsBillingService.API.Application.Commands;
+
+namespace AdsBillingService.API.Application.Validations;
+
+///
+/// EN: Validator for AddFundsCommand.
+/// VI: Validator cho AddFundsCommand.
+///
+public class AddFundsCommandValidator : AbstractValidator
+{
+ public AddFundsCommandValidator()
+ {
+ RuleFor(x => x.AccountId)
+ .NotEmpty()
+ .WithMessage("Account ID is required / Account ID là bắt buộc");
+
+ RuleFor(x => x.Amount)
+ .GreaterThan(0)
+ .WithMessage("Amount must be greater than 0 / Số tiền phải lớn hơn 0")
+ .LessThanOrEqualTo(1_000_000_000)
+ .WithMessage("Amount must not exceed 1,000,000,000 / Số tiền không được vượt quá 1,000,000,000");
+ }
+}
diff --git a/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/ChargeAdvertiserCommandValidator.cs b/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/ChargeAdvertiserCommandValidator.cs
new file mode 100644
index 00000000..395b6476
--- /dev/null
+++ b/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/ChargeAdvertiserCommandValidator.cs
@@ -0,0 +1,38 @@
+using FluentValidation;
+using AdsBillingService.API.Application.Commands;
+
+namespace AdsBillingService.API.Application.Validations;
+
+///
+/// EN: Validator for ChargeAdvertiserCommand.
+/// VI: Validator cho ChargeAdvertiserCommand.
+///
+public class ChargeAdvertiserCommandValidator : AbstractValidator
+{
+ private static readonly string[] ValidChargeTypes = ["impression", "click"];
+
+ public ChargeAdvertiserCommandValidator()
+ {
+ RuleFor(x => x.AdvertiserId)
+ .NotEmpty()
+ .WithMessage("Advertiser ID is required / Advertiser ID là bắt buộc");
+
+ RuleFor(x => x.CampaignId)
+ .NotEmpty()
+ .WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
+
+ RuleFor(x => x.AdId)
+ .NotEmpty()
+ .WithMessage("Ad ID is required / Ad ID là bắt buộc");
+
+ RuleFor(x => x.ChargeType)
+ .NotEmpty()
+ .WithMessage("Charge type is required / Loại charge là bắt buộc")
+ .Must(ct => ValidChargeTypes.Contains(ct))
+ .WithMessage("Charge type must be 'impression' or 'click' / Loại charge phải là 'impression' hoặc 'click'");
+
+ RuleFor(x => x.Amount)
+ .GreaterThan(0)
+ .WithMessage("Amount must be greater than 0 / Số tiền phải lớn hơn 0");
+ }
+}
diff --git a/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/CreateBillingAccountCommandValidator.cs b/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/CreateBillingAccountCommandValidator.cs
new file mode 100644
index 00000000..1f4d29ca
--- /dev/null
+++ b/services/ads-billing-service-net/src/AdsBillingService.API/Application/Validations/CreateBillingAccountCommandValidator.cs
@@ -0,0 +1,26 @@
+using FluentValidation;
+using AdsBillingService.API.Application.Commands;
+
+namespace AdsBillingService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateBillingAccountCommand.
+/// VI: Validator cho CreateBillingAccountCommand.
+///
+public class CreateBillingAccountCommandValidator : AbstractValidator
+{
+ private static readonly string[] ValidPaymentMethods = ["prepaid", "postpaid"];
+
+ public CreateBillingAccountCommandValidator()
+ {
+ RuleFor(x => x.AdvertiserId)
+ .NotEmpty()
+ .WithMessage("Advertiser ID is required / Advertiser ID là bắt buộc");
+
+ RuleFor(x => x.PaymentMethod)
+ .NotEmpty()
+ .WithMessage("Payment method is required / Phương thức thanh toán là bắt buộc")
+ .Must(pm => ValidPaymentMethods.Contains(pm))
+ .WithMessage("Payment method must be 'prepaid' or 'postpaid' / Phương thức thanh toán phải là 'prepaid' hoặc 'postpaid'");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/AudienceQueryHandlers.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/AudienceQueryHandlers.cs
new file mode 100644
index 00000000..53bd71de
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/AudienceQueryHandlers.cs
@@ -0,0 +1,115 @@
+using AdsManagerService.API.Controllers;
+using AdsManagerService.Domain.AggregatesModel.AudienceAggregate;
+using AdsManagerService.Infrastructure;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace AdsManagerService.API.Application.Queries;
+
+///
+/// EN: Handler for ListAudiencesQuery - returns audiences for an advertiser.
+/// VI: Handler cho ListAudiencesQuery - trả về audiences cho nhà quảng cáo.
+///
+public class ListAudiencesQueryHandler : IRequestHandler>
+{
+ private readonly AdsManagerServiceContext _context;
+
+ public ListAudiencesQueryHandler(AdsManagerServiceContext context)
+ {
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ }
+
+ public async Task> Handle(ListAudiencesQuery request, CancellationToken cancellationToken)
+ {
+ // EN: Get custom audiences for the advertiser / VI: Lấy custom audiences cho nhà quảng cáo
+ var customAudiences = await _context.CustomAudiences
+ .AsNoTracking()
+ .Where(a => a.AdvertiserId == request.AdvertiserId)
+ .OrderByDescending(a => a.CreatedAt)
+ .Select(a => new AudienceDto
+ {
+ Id = a.Id,
+ Name = a.Name,
+ Type = "custom",
+ Size = a.Size,
+ CreatedAt = a.CreatedAt
+ })
+ .ToListAsync(cancellationToken);
+
+ // EN: Get lookalike audiences for the advertiser / VI: Lấy lookalike audiences cho nhà quảng cáo
+ var lookalikeAudiences = await _context.LookalikeAudiences
+ .AsNoTracking()
+ .Where(a => a.AdvertiserId == request.AdvertiserId)
+ .OrderByDescending(a => a.CreatedAt)
+ .Select(a => new AudienceDto
+ {
+ Id = a.Id,
+ Name = a.Name,
+ Type = "lookalike",
+ Size = a.Size,
+ CreatedAt = a.CreatedAt
+ })
+ .ToListAsync(cancellationToken);
+
+ // EN: Merge and sort by creation date / VI: Gộp và sắp xếp theo ngày tạo
+ var allAudiences = customAudiences
+ .Concat(lookalikeAudiences)
+ .OrderByDescending(a => a.CreatedAt)
+ .ToList();
+
+ return allAudiences;
+ }
+}
+
+///
+/// EN: Handler for GetAudienceByIdQuery - returns a single audience by ID.
+/// VI: Handler cho GetAudienceByIdQuery - trả về một audience theo ID.
+///
+public class GetAudienceByIdQueryHandler : IRequestHandler
+{
+ private readonly AdsManagerServiceContext _context;
+
+ public GetAudienceByIdQueryHandler(AdsManagerServiceContext context)
+ {
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ }
+
+ public async Task Handle(GetAudienceByIdQuery request, CancellationToken cancellationToken)
+ {
+ // EN: Try custom audience first / VI: Thử custom audience trước
+ var customAudience = await _context.CustomAudiences
+ .AsNoTracking()
+ .FirstOrDefaultAsync(a => a.Id == request.AudienceId, cancellationToken);
+
+ if (customAudience != null)
+ {
+ return new AudienceDto
+ {
+ Id = customAudience.Id,
+ Name = customAudience.Name,
+ Type = "custom",
+ Size = customAudience.Size,
+ CreatedAt = customAudience.CreatedAt
+ };
+ }
+
+ // EN: Try lookalike audience / VI: Thử lookalike audience
+ var lookalikeAudience = await _context.LookalikeAudiences
+ .AsNoTracking()
+ .FirstOrDefaultAsync(a => a.Id == request.AudienceId, cancellationToken);
+
+ if (lookalikeAudience != null)
+ {
+ return new AudienceDto
+ {
+ Id = lookalikeAudience.Id,
+ Name = lookalikeAudience.Name,
+ Type = "lookalike",
+ Size = lookalikeAudience.Size,
+ CreatedAt = lookalikeAudience.CreatedAt
+ };
+ }
+
+ return null;
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/ActivateCampaignCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/ActivateCampaignCommandValidator.cs
new file mode 100644
index 00000000..6fe3f8b4
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/ActivateCampaignCommandValidator.cs
@@ -0,0 +1,18 @@
+using FluentValidation;
+using AdsManagerService.API.Application.Commands;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for ActivateCampaignCommand.
+/// VI: Validator cho ActivateCampaignCommand.
+///
+public class ActivateCampaignCommandValidator : AbstractValidator
+{
+ public ActivateCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty()
+ .WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/ApproveAdCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/ApproveAdCommandValidator.cs
new file mode 100644
index 00000000..2ee34b62
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/ApproveAdCommandValidator.cs
@@ -0,0 +1,18 @@
+using FluentValidation;
+using AdsManagerService.API.Controllers;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for ApproveAdCommand.
+/// VI: Validator cho ApproveAdCommand.
+///
+public class ApproveAdCommandValidator : AbstractValidator
+{
+ public ApproveAdCommandValidator()
+ {
+ RuleFor(x => x.AdId)
+ .NotEmpty()
+ .WithMessage("Ad ID is required / Ad ID là bắt buộc");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateAdCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateAdCommandValidator.cs
new file mode 100644
index 00000000..02bae277
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateAdCommandValidator.cs
@@ -0,0 +1,57 @@
+using FluentValidation;
+using AdsManagerService.API.Application.Commands;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateAdCommand.
+/// VI: Validator cho CreateAdCommand.
+///
+public class CreateAdCommandValidator : AbstractValidator
+{
+ private static readonly string[] ValidFormats = ["single_image", "single_video", "carousel", "collection", "stories"];
+
+ public CreateAdCommandValidator()
+ {
+ RuleFor(x => x.AdSetId)
+ .NotEmpty()
+ .WithMessage("Ad set ID is required / Ad set ID là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithMessage("Ad name is required / Tên quảng cáo là bắt buộc")
+ .MaximumLength(200)
+ .WithMessage("Ad name max 200 characters / Tên quảng cáo tối đa 200 ký tự");
+
+ RuleFor(x => x.Format)
+ .NotEmpty()
+ .WithMessage("Ad format is required / Định dạng quảng cáo là bắt buộc")
+ .Must(f => ValidFormats.Contains(f))
+ .WithMessage("Invalid format. Must be one of: single_image, single_video, carousel, collection, stories / Định dạng không hợp lệ");
+
+ RuleFor(x => x.Headline)
+ .MaximumLength(100)
+ .When(x => x.Headline != null)
+ .WithMessage("Headline max 100 characters / Tiêu đề tối đa 100 ký tự");
+
+ RuleFor(x => x.PrimaryText)
+ .MaximumLength(500)
+ .When(x => x.PrimaryText != null)
+ .WithMessage("Primary text max 500 characters / Nội dung chính tối đa 500 ký tự");
+
+ RuleFor(x => x.CallToAction)
+ .MaximumLength(50)
+ .When(x => x.CallToAction != null)
+ .WithMessage("Call to action max 50 characters / Lời kêu gọi tối đa 50 ký tự");
+
+ RuleFor(x => x.DestinationUrl)
+ .MaximumLength(2000)
+ .When(x => x.DestinationUrl != null)
+ .WithMessage("Destination URL max 2000 characters / URL đích tối đa 2000 ký tự");
+
+ RuleFor(x => x.CreativeUrl)
+ .MaximumLength(2000)
+ .When(x => x.CreativeUrl != null)
+ .WithMessage("Creative URL max 2000 characters / URL sáng tạo tối đa 2000 ký tự");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateAdSetCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateAdSetCommandValidator.cs
new file mode 100644
index 00000000..f73b834e
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateAdSetCommandValidator.cs
@@ -0,0 +1,66 @@
+using FluentValidation;
+using AdsManagerService.API.Application.Commands;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateAdSetCommand.
+/// VI: Validator cho CreateAdSetCommand.
+///
+public class CreateAdSetCommandValidator : AbstractValidator
+{
+ private static readonly string[] ValidBidTypes = ["cpc", "cpm", "ocpm", "automatic"];
+
+ public CreateAdSetCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty()
+ .WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithMessage("Ad set name is required / Tên ad set là bắt buộc")
+ .MaximumLength(200)
+ .WithMessage("Ad set name max 200 characters / Tên ad set tối đa 200 ký tự");
+
+ RuleFor(x => x.DailyBudget)
+ .GreaterThan(0)
+ .WithMessage("Daily budget must be greater than 0 / Ngân sách hàng ngày phải lớn hơn 0");
+
+ RuleFor(x => x.BidType)
+ .NotEmpty()
+ .WithMessage("Bid type is required / Loại bid là bắt buộc")
+ .Must(bt => ValidBidTypes.Contains(bt))
+ .WithMessage("Bid type must be one of: cpc, cpm, ocpm, automatic / Loại bid phải là cpc, cpm, ocpm hoặc automatic");
+
+ RuleFor(x => x.BidAmount)
+ .GreaterThan(0)
+ .When(x => x.BidAmount.HasValue)
+ .WithMessage("Bid amount must be greater than 0 / Giá bid phải lớn hơn 0");
+
+ RuleFor(x => x.MinAge)
+ .InclusiveBetween(13, 65)
+ .When(x => x.MinAge.HasValue)
+ .WithMessage("Min age must be between 13 and 65 / Tuổi tối thiểu phải từ 13 đến 65");
+
+ RuleFor(x => x.MaxAge)
+ .InclusiveBetween(13, 65)
+ .When(x => x.MaxAge.HasValue)
+ .WithMessage("Max age must be between 13 and 65 / Tuổi tối đa phải từ 13 đến 65");
+
+ RuleFor(x => x.MaxAge)
+ .GreaterThanOrEqualTo(x => x.MinAge)
+ .When(x => x.MinAge.HasValue && x.MaxAge.HasValue)
+ .WithMessage("Max age must be greater than or equal to min age / Tuổi tối đa phải lớn hơn hoặc bằng tuổi tối thiểu");
+
+ RuleFor(x => x.Locations)
+ .MaximumLength(2000)
+ .When(x => x.Locations != null)
+ .WithMessage("Locations max 2000 characters / Vị trí tối đa 2000 ký tự");
+
+ RuleFor(x => x.Interests)
+ .MaximumLength(2000)
+ .When(x => x.Interests != null)
+ .WithMessage("Interests max 2000 characters / Sở thích tối đa 2000 ký tự");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateCampaignCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateCampaignCommandValidator.cs
new file mode 100644
index 00000000..75ed59ce
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateCampaignCommandValidator.cs
@@ -0,0 +1,59 @@
+using FluentValidation;
+using AdsManagerService.API.Application.Commands;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateCampaignCommand.
+/// VI: Validator cho CreateCampaignCommand.
+///
+public class CreateCampaignCommandValidator : AbstractValidator
+{
+ private static readonly string[] ValidObjectives = ["awareness", "traffic", "conversion", "engagement", "app_install", "lead_generation"];
+ private static readonly string[] ValidBudgetTypes = ["daily", "lifetime"];
+
+ public CreateCampaignCommandValidator()
+ {
+ RuleFor(x => x.AdvertiserId)
+ .NotEmpty()
+ .WithMessage("Advertiser ID is required / Advertiser ID là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithMessage("Campaign name is required / Tên chiến dịch là bắt buộc")
+ .MaximumLength(200)
+ .WithMessage("Campaign name max 200 characters / Tên chiến dịch tối đa 200 ký tự");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000)
+ .When(x => x.Description != null)
+ .WithMessage("Description max 1000 characters / Mô tả tối đa 1000 ký tự");
+
+ RuleFor(x => x.Objective)
+ .NotEmpty()
+ .WithMessage("Objective is required / Mục tiêu là bắt buộc")
+ .Must(o => ValidObjectives.Contains(o))
+ .WithMessage("Invalid objective. Must be one of: awareness, traffic, conversion, engagement, app_install, lead_generation / Mục tiêu không hợp lệ");
+
+ RuleFor(x => x.BudgetType)
+ .NotEmpty()
+ .WithMessage("Budget type is required / Loại ngân sách là bắt buộc")
+ .Must(bt => ValidBudgetTypes.Contains(bt))
+ .WithMessage("Budget type must be 'daily' or 'lifetime' / Loại ngân sách phải là 'daily' hoặc 'lifetime'");
+
+ RuleFor(x => x.BudgetAmount)
+ .GreaterThan(0)
+ .WithMessage("Budget amount must be greater than 0 / Ngân sách phải lớn hơn 0");
+
+ RuleFor(x => x.Currency)
+ .NotEmpty()
+ .WithMessage("Currency is required / Đơn vị tiền tệ là bắt buộc")
+ .MaximumLength(3)
+ .WithMessage("Currency code max 3 characters / Mã tiền tệ tối đa 3 ký tự");
+
+ RuleFor(x => x.EndDate)
+ .GreaterThan(x => x.StartDate)
+ .When(x => x.StartDate.HasValue && x.EndDate.HasValue)
+ .WithMessage("End date must be after start date / Ngày kết thúc phải sau ngày bắt đầu");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/DeleteCampaignCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/DeleteCampaignCommandValidator.cs
new file mode 100644
index 00000000..24b3648c
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/DeleteCampaignCommandValidator.cs
@@ -0,0 +1,18 @@
+using FluentValidation;
+using AdsManagerService.API.Application.Commands;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for DeleteCampaignCommand.
+/// VI: Validator cho DeleteCampaignCommand.
+///
+public class DeleteCampaignCommandValidator : AbstractValidator
+{
+ public DeleteCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty()
+ .WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/PauseCampaignCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/PauseCampaignCommandValidator.cs
new file mode 100644
index 00000000..a1467172
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/PauseCampaignCommandValidator.cs
@@ -0,0 +1,18 @@
+using FluentValidation;
+using AdsManagerService.API.Application.Commands;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for PauseCampaignCommand.
+/// VI: Validator cho PauseCampaignCommand.
+///
+public class PauseCampaignCommandValidator : AbstractValidator
+{
+ public PauseCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty()
+ .WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/RejectAdCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/RejectAdCommandValidator.cs
new file mode 100644
index 00000000..5f14467c
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/RejectAdCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using AdsManagerService.API.Controllers;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for RejectAdCommand.
+/// VI: Validator cho RejectAdCommand.
+///
+public class RejectAdCommandValidator : AbstractValidator
+{
+ public RejectAdCommandValidator()
+ {
+ RuleFor(x => x.AdId)
+ .NotEmpty()
+ .WithMessage("Ad ID is required / Ad ID là bắt buộc");
+
+ RuleFor(x => x.Reason)
+ .NotEmpty()
+ .WithMessage("Rejection reason is required / Lý do từ chối là bắt buộc")
+ .MaximumLength(1000)
+ .WithMessage("Rejection reason max 1000 characters / Lý do từ chối tối đa 1000 ký tự");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/SubmitAdForReviewCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/SubmitAdForReviewCommandValidator.cs
new file mode 100644
index 00000000..a15c1ac5
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/SubmitAdForReviewCommandValidator.cs
@@ -0,0 +1,18 @@
+using FluentValidation;
+using AdsManagerService.API.Application.Commands;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for SubmitAdForReviewCommand.
+/// VI: Validator cho SubmitAdForReviewCommand.
+///
+public class SubmitAdForReviewCommandValidator : AbstractValidator
+{
+ public SubmitAdForReviewCommandValidator()
+ {
+ RuleFor(x => x.AdId)
+ .NotEmpty()
+ .WithMessage("Ad ID is required / Ad ID là bắt buộc");
+ }
+}
diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/UpdateCampaignCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/UpdateCampaignCommandValidator.cs
new file mode 100644
index 00000000..5b728ca7
--- /dev/null
+++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/UpdateCampaignCommandValidator.cs
@@ -0,0 +1,29 @@
+using FluentValidation;
+using AdsManagerService.API.Application.Commands;
+
+namespace AdsManagerService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateCampaignCommand.
+/// VI: Validator cho UpdateCampaignCommand.
+///
+public class UpdateCampaignCommandValidator : AbstractValidator
+{
+ public UpdateCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty()
+ .WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithMessage("Campaign name is required / Tên chiến dịch là bắt buộc")
+ .MaximumLength(200)
+ .WithMessage("Campaign name max 200 characters / Tên chiến dịch tối đa 200 ký tự");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000)
+ .When(x => x.Description != null)
+ .WithMessage("Description max 1000 characters / Mô tả tối đa 1000 ký tự");
+ }
+}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/RecordConversionCommandValidator.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/RecordConversionCommandValidator.cs
new file mode 100644
index 00000000..32319e81
--- /dev/null
+++ b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/RecordConversionCommandValidator.cs
@@ -0,0 +1,42 @@
+using FluentValidation;
+using AdsTrackingService.API.Application.Commands;
+
+namespace AdsTrackingService.API.Application.Validations;
+
+///
+/// EN: Validator for RecordConversionCommand.
+/// VI: Validator cho RecordConversionCommand.
+///
+public class RecordConversionCommandValidator : AbstractValidator
+{
+ public RecordConversionCommandValidator()
+ {
+ RuleFor(x => x.AdvertiserId)
+ .NotEmpty()
+ .WithMessage("Advertiser ID is required / Advertiser ID là bắt buộc");
+
+ RuleFor(x => x.CampaignId)
+ .NotEmpty()
+ .WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty()
+ .WithMessage("User ID is required / User ID là bắt buộc");
+
+ RuleFor(x => x.ConversionType)
+ .NotEmpty()
+ .WithMessage("Conversion type is required / Loại conversion là bắt buộc")
+ .MaximumLength(100)
+ .WithMessage("Conversion type max 100 characters / Loại conversion tối đa 100 ký tự");
+
+ RuleFor(x => x.ConversionValue)
+ .GreaterThanOrEqualTo(0)
+ .WithMessage("Conversion value must be >= 0 / Giá trị conversion phải >= 0");
+
+ RuleFor(x => x.Currency)
+ .NotEmpty()
+ .WithMessage("Currency is required / Đơn vị tiền tệ là bắt buộc")
+ .MaximumLength(3)
+ .WithMessage("Currency code max 3 characters / Mã tiền tệ tối đa 3 ký tự");
+ }
+}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/TrackPixelEventCommandValidator.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/TrackPixelEventCommandValidator.cs
new file mode 100644
index 00000000..4484f88a
--- /dev/null
+++ b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/TrackPixelEventCommandValidator.cs
@@ -0,0 +1,42 @@
+using FluentValidation;
+using AdsTrackingService.API.Application.Commands;
+
+namespace AdsTrackingService.API.Application.Validations;
+
+///
+/// EN: Validator for TrackPixelEventCommand.
+/// VI: Validator cho TrackPixelEventCommand.
+///
+public class TrackPixelEventCommandValidator : AbstractValidator
+{
+ public TrackPixelEventCommandValidator()
+ {
+ RuleFor(x => x.PixelCode)
+ .NotEmpty()
+ .WithMessage("Pixel code is required / Mã pixel là bắt buộc")
+ .MaximumLength(100)
+ .WithMessage("Pixel code max 100 characters / Mã pixel tối đa 100 ký tự");
+
+ RuleFor(x => x.AdId)
+ .NotEmpty()
+ .WithMessage("Ad ID is required / Ad ID là bắt buộc");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty()
+ .WithMessage("User ID is required / User ID là bắt buộc");
+
+ RuleFor(x => x.EventType)
+ .IsInEnum()
+ .WithMessage("Event type is invalid / Loại sự kiện không hợp lệ");
+
+ RuleFor(x => x.UserAgent)
+ .MaximumLength(500)
+ .When(x => x.UserAgent != null)
+ .WithMessage("User agent max 500 characters / User agent tối đa 500 ký tự");
+
+ RuleFor(x => x.IpAddress)
+ .MaximumLength(45)
+ .When(x => x.IpAddress != null)
+ .WithMessage("IP address max 45 characters / Địa chỉ IP tối đa 45 ký tự");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/AcceptCircleInviteCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/AcceptCircleInviteCommandValidator.cs
new file mode 100644
index 00000000..7a661cc5
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/AcceptCircleInviteCommandValidator.cs
@@ -0,0 +1,20 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for AcceptCircleInviteCommand.
+/// VI: Validator cho AcceptCircleInviteCommand.
+///
+public class AcceptCircleInviteCommandValidator : AbstractValidator
+{
+ public AcceptCircleInviteCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.InviteId)
+ .NotEmpty().WithMessage("Invite ID is required / ID lời mời là bắt buộc");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/AdjustMinerPointsCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/AdjustMinerPointsCommandValidator.cs
new file mode 100644
index 00000000..47c53c49
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/AdjustMinerPointsCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for AdjustMinerPointsCommand.
+/// VI: Validator cho AdjustMinerPointsCommand.
+///
+public class AdjustMinerPointsCommandValidator : AbstractValidator
+{
+ public AdjustMinerPointsCommandValidator()
+ {
+ RuleFor(x => x.MinerId)
+ .NotEmpty().WithMessage("Miner ID is required / ID thợ đào là bắt buộc");
+
+ RuleFor(x => x.Amount)
+ .NotEqual(0).WithMessage("Amount must not be zero / Số lượng không được bằng 0");
+
+ RuleFor(x => x.Reason)
+ .NotEmpty().WithMessage("Reason is required / Lý do là bắt buộc")
+ .MaximumLength(500).WithMessage("Reason max 500 chars / Lý do tối đa 500 ký tự");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/ApplyReferralCodeCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/ApplyReferralCodeCommandValidator.cs
new file mode 100644
index 00000000..db60aa94
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/ApplyReferralCodeCommandValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for ApplyReferralCodeCommand.
+/// VI: Validator cho ApplyReferralCodeCommand.
+///
+public class ApplyReferralCodeCommandValidator : AbstractValidator
+{
+ public ApplyReferralCodeCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.ReferralCode)
+ .NotEmpty().WithMessage("Referral code is required / Mã giới thiệu là bắt buộc")
+ .MaximumLength(50).WithMessage("Referral code max 50 chars / Mã giới thiệu tối đa 50 ký tự");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/BanMinerCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/BanMinerCommandValidator.cs
new file mode 100644
index 00000000..0c9b9adc
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/BanMinerCommandValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for BanMinerCommand.
+/// VI: Validator cho BanMinerCommand.
+///
+public class BanMinerCommandValidator : AbstractValidator
+{
+ public BanMinerCommandValidator()
+ {
+ RuleFor(x => x.MinerId)
+ .NotEmpty().WithMessage("Miner ID is required / ID thợ đào là bắt buộc");
+
+ RuleFor(x => x.Reason)
+ .NotEmpty().WithMessage("Reason is required / Lý do là bắt buộc")
+ .MaximumLength(500).WithMessage("Reason max 500 chars / Lý do tối đa 500 ký tự");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/ClaimMiningRewardCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/ClaimMiningRewardCommandValidator.cs
new file mode 100644
index 00000000..1bbae7ab
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/ClaimMiningRewardCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for ClaimMiningRewardCommand.
+/// VI: Validator cho ClaimMiningRewardCommand.
+///
+public class ClaimMiningRewardCommandValidator : AbstractValidator
+{
+ public ClaimMiningRewardCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/CreateCircleCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/CreateCircleCommandValidator.cs
new file mode 100644
index 00000000..00e6b2f5
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/CreateCircleCommandValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateCircleCommand.
+/// VI: Validator cho CreateCircleCommand.
+///
+public class CreateCircleCommandValidator : AbstractValidator
+{
+ public CreateCircleCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("Circle name is required / Tên vòng tròn là bắt buộc")
+ .MaximumLength(100).WithMessage("Circle name max 100 chars / Tên vòng tròn tối đa 100 ký tự");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/InviteToCircleCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/InviteToCircleCommandValidator.cs
new file mode 100644
index 00000000..827a8f41
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/InviteToCircleCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for InviteToCircleCommand.
+/// VI: Validator cho InviteToCircleCommand.
+///
+public class InviteToCircleCommandValidator : AbstractValidator
+{
+ public InviteToCircleCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.TargetMinerId)
+ .NotEmpty().WithMessage("Target miner ID is required / ID thợ đào mục tiêu là bắt buộc");
+
+ RuleFor(x => x)
+ .Must(x => x.UserId != x.TargetMinerId)
+ .WithMessage("Cannot invite yourself to circle / Không thể mời chính mình vào vòng tròn");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/RemoveCircleMemberCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/RemoveCircleMemberCommandValidator.cs
new file mode 100644
index 00000000..1a517a02
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/RemoveCircleMemberCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for RemoveCircleMemberCommand.
+/// VI: Validator cho RemoveCircleMemberCommand.
+///
+public class RemoveCircleMemberCommandValidator : AbstractValidator
+{
+ public RemoveCircleMemberCommandValidator()
+ {
+ RuleFor(x => x.OwnerId)
+ .NotEmpty().WithMessage("Owner ID is required / ID chủ sở hữu là bắt buộc");
+
+ RuleFor(x => x.MemberId)
+ .NotEmpty().WithMessage("Member ID is required / ID thành viên là bắt buộc");
+
+ RuleFor(x => x)
+ .Must(x => x.OwnerId != x.MemberId)
+ .WithMessage("Cannot remove yourself from circle / Không thể xóa chính mình khỏi vòng tròn");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/ResetMinerStreakCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/ResetMinerStreakCommandValidator.cs
new file mode 100644
index 00000000..b9077672
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/ResetMinerStreakCommandValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for ResetMinerStreakCommand.
+/// VI: Validator cho ResetMinerStreakCommand.
+///
+public class ResetMinerStreakCommandValidator : AbstractValidator
+{
+ public ResetMinerStreakCommandValidator()
+ {
+ RuleFor(x => x.MinerId)
+ .NotEmpty().WithMessage("Miner ID is required / ID thợ đào là bắt buộc");
+
+ RuleFor(x => x.Reason)
+ .NotEmpty().WithMessage("Reason is required / Lý do là bắt buộc")
+ .MaximumLength(500).WithMessage("Reason max 500 chars / Lý do tối đa 500 ký tự");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/RestoreMinerCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/RestoreMinerCommandValidator.cs
new file mode 100644
index 00000000..95d05515
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/RestoreMinerCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for RestoreMinerCommand.
+/// VI: Validator cho RestoreMinerCommand.
+///
+public class RestoreMinerCommandValidator : AbstractValidator
+{
+ public RestoreMinerCommandValidator()
+ {
+ RuleFor(x => x.MinerId)
+ .NotEmpty().WithMessage("Miner ID is required / ID thợ đào là bắt buộc");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/StartMiningCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/StartMiningCommandValidator.cs
new file mode 100644
index 00000000..0f514e86
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/StartMiningCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for StartMiningCommand.
+/// VI: Validator cho StartMiningCommand.
+///
+public class StartMiningCommandValidator : AbstractValidator
+{
+ public StartMiningCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/SuspendMinerCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/SuspendMinerCommandValidator.cs
new file mode 100644
index 00000000..0a9232a3
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/SuspendMinerCommandValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for SuspendMinerCommand.
+/// VI: Validator cho SuspendMinerCommand.
+///
+public class SuspendMinerCommandValidator : AbstractValidator
+{
+ public SuspendMinerCommandValidator()
+ {
+ RuleFor(x => x.MinerId)
+ .NotEmpty().WithMessage("Miner ID is required / ID thợ đào là bắt buộc");
+
+ RuleFor(x => x.Reason)
+ .NotEmpty().WithMessage("Reason is required / Lý do là bắt buộc")
+ .MaximumLength(500).WithMessage("Reason max 500 chars / Lý do tối đa 500 ký tự");
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateMiningConfigCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateMiningConfigCommandValidator.cs
new file mode 100644
index 00000000..fefec9b0
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateMiningConfigCommandValidator.cs
@@ -0,0 +1,26 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateMiningConfigCommand.
+/// VI: Validator cho UpdateMiningConfigCommand.
+///
+public class UpdateMiningConfigCommandValidator : AbstractValidator
+{
+ public UpdateMiningConfigCommandValidator()
+ {
+ RuleFor(x => x.BaseRate)
+ .GreaterThan(0).WithMessage("Base rate must be positive / Tỷ lệ cơ bản phải dương")
+ .When(x => x.BaseRate.HasValue);
+
+ RuleFor(x => x.SessionDurationHours)
+ .InclusiveBetween(1, 24).WithMessage("Session duration must be 1-24 hours / Thời lượng phiên phải từ 1-24 giờ")
+ .When(x => x.SessionDurationHours.HasValue);
+
+ RuleFor(x => x.MaxSessionsPerDay)
+ .InclusiveBetween(1, 10).WithMessage("Max sessions per day must be 1-10 / Số phiên tối đa mỗi ngày phải từ 1-10")
+ .When(x => x.MaxSessionsPerDay.HasValue);
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateReferralConfigCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateReferralConfigCommandValidator.cs
new file mode 100644
index 00000000..21abdb65
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateReferralConfigCommandValidator.cs
@@ -0,0 +1,22 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateReferralConfigCommand.
+/// VI: Validator cho UpdateReferralConfigCommand.
+///
+public class UpdateReferralConfigCommandValidator : AbstractValidator
+{
+ public UpdateReferralConfigCommandValidator()
+ {
+ RuleFor(x => x.BonusPerReferral)
+ .GreaterThan(0).WithMessage("Bonus per referral must be positive / Thưởng mỗi giới thiệu phải dương")
+ .When(x => x.BonusPerReferral.HasValue);
+
+ RuleFor(x => x.MaxBonusCap)
+ .GreaterThan(0).WithMessage("Max bonus cap must be positive / Giới hạn thưởng tối đa phải dương")
+ .When(x => x.MaxBonusCap.HasValue);
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateStreakConfigCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateStreakConfigCommandValidator.cs
new file mode 100644
index 00000000..9191a520
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateStreakConfigCommandValidator.cs
@@ -0,0 +1,18 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateStreakConfigCommand.
+/// VI: Validator cho UpdateStreakConfigCommand.
+///
+public class UpdateStreakConfigCommandValidator : AbstractValidator
+{
+ public UpdateStreakConfigCommandValidator()
+ {
+ RuleFor(x => x.GracePeriodDays)
+ .InclusiveBetween(0, 7).WithMessage("Grace period must be 0-7 days / Thời gian gia hạn phải từ 0-7 ngày")
+ .When(x => x.GracePeriodDays.HasValue);
+ }
+}
diff --git a/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateSystemConfigCommandValidator.cs b/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateSystemConfigCommandValidator.cs
new file mode 100644
index 00000000..46cd71a6
--- /dev/null
+++ b/services/mining-service-net/src/MiningService.API/Application/Validations/UpdateSystemConfigCommandValidator.cs
@@ -0,0 +1,40 @@
+using FluentValidation;
+using MiningService.API.Application.Commands;
+
+namespace MiningService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateSystemConfigCommand.
+/// VI: Validator cho UpdateSystemConfigCommand.
+///
+public class UpdateSystemConfigCommandValidator : AbstractValidator
+{
+ public UpdateSystemConfigCommandValidator()
+ {
+ RuleFor(x => x)
+ .Must(x => x.Mining != null || x.Streak != null || x.Referral != null)
+ .WithMessage("At least one config section must be provided / Phải cung cấp ít nhất một mục cấu hình");
+
+ When(x => x.Mining != null, () =>
+ {
+ RuleFor(x => x.Mining!.BaseRate)
+ .GreaterThan(0).WithMessage("Base rate must be positive / Tỷ lệ cơ bản phải dương")
+ .When(x => x.Mining!.BaseRate.HasValue);
+
+ RuleFor(x => x.Mining!.SessionDurationHours)
+ .InclusiveBetween(1, 24).WithMessage("Session duration must be 1-24 hours / Thời lượng phiên phải từ 1-24 giờ")
+ .When(x => x.Mining!.SessionDurationHours.HasValue);
+ });
+
+ When(x => x.Referral != null, () =>
+ {
+ RuleFor(x => x.Referral!.BonusPerReferral)
+ .GreaterThan(0).WithMessage("Bonus per referral must be positive / Thưởng mỗi giới thiệu phải dương")
+ .When(x => x.Referral!.BonusPerReferral.HasValue);
+
+ RuleFor(x => x.Referral!.MaxBonusCap)
+ .GreaterThan(0).WithMessage("Max bonus cap must be positive / Giới hạn thưởng tối đa phải dương")
+ .When(x => x.Referral!.MaxBonusCap.HasValue);
+ });
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueryHandlers.cs b/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueryHandlers.cs
index 8c7e5770..c9054c56 100644
--- a/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueryHandlers.cs
+++ b/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueryHandlers.cs
@@ -163,3 +163,113 @@ public class GetMissionsByCategoryQueryHandler : IRequestHandler
+/// EN: Handler for GetUserMissionProgressQuery - returns user's overall mission progress.
+/// VI: Handler cho GetUserMissionProgressQuery - trả về tiến độ tổng thể của user trên các mission.
+///
+public class GetUserMissionProgressQueryHandler : IRequestHandler
+{
+ private readonly IMissionRepository _missionRepository;
+ private readonly IUserTaskRepository _taskRepository;
+
+ public GetUserMissionProgressQueryHandler(
+ IMissionRepository missionRepository,
+ IUserTaskRepository taskRepository)
+ {
+ _missionRepository = missionRepository;
+ _taskRepository = taskRepository;
+ }
+
+ public async Task Handle(GetUserMissionProgressQuery request, CancellationToken cancellationToken)
+ {
+ // EN: Get all active missions and user's tasks / VI: Lấy tất cả missions đang hoạt động và tasks của user
+ var missions = await _missionRepository.GetActiveMissionsAsync(cancellationToken);
+ var userTasks = await _taskRepository.GetByUserIdAsync(request.UserId, cancellationToken);
+
+ // EN: Separate active and completed tasks / VI: Phân loại tasks đang hoạt động và đã hoàn thành
+ var activeTasks = userTasks
+ .Where(t => t.Status == TaskStatus.InProgress || t.Status == TaskStatus.PendingVerification)
+ .ToList();
+
+ var completedTasks = userTasks
+ .Where(t => t.Status == TaskStatus.Completed)
+ .ToList();
+
+ // EN: Calculate total points earned from claimed rewards
+ // VI: Tính tổng điểm đã nhận từ phần thưởng đã claim
+ var completedMissionIds = completedTasks.Select(t => t.MissionId).ToHashSet();
+ var totalPointsEarned = 0m;
+ foreach (var mission in missions.Where(m => completedMissionIds.Contains(m.Id)))
+ {
+ var taskCount = completedTasks.Count(t => t.MissionId == mission.Id && t.RewardClaimed);
+ totalPointsEarned += mission.Reward.Points * taskCount;
+ }
+
+ // EN: Map active missions with progress / VI: Map các mission đang hoạt động với tiến độ
+ var activeMissionSummaries = missions
+ .Where(m => activeTasks.Any(t => t.MissionId == m.Id))
+ .Select(m =>
+ {
+ var task = activeTasks.First(t => t.MissionId == m.Id);
+ return new MissionSummaryDto(
+ Id: m.Id,
+ Code: m.Code,
+ Title: m.TitleEn,
+ Description: m.DescriptionEn,
+ Type: m.Type.Name,
+ Category: m.Category.Name,
+ RewardPoints: m.Reward.Points,
+ Frequency: m.Frequency.Name,
+ MaxCompletions: m.MaxCompletions,
+ IsAvailable: m.IsAvailable(),
+ UserProgress: new UserTaskProgressDto(
+ TaskId: task.Id,
+ CurrentValue: task.Progress.CurrentValue,
+ TargetValue: task.Progress.TargetValue,
+ PercentComplete: task.Progress.PercentComplete,
+ Status: task.Status.Name,
+ StartedAt: task.StartedAt,
+ CompletedAt: task.CompletedAt,
+ RewardClaimed: task.RewardClaimed));
+ })
+ .ToList();
+
+ // EN: Map completed missions / VI: Map các mission đã hoàn thành
+ var completedMissionSummaries = missions
+ .Where(m => completedMissionIds.Contains(m.Id))
+ .Select(m =>
+ {
+ var task = completedTasks.First(t => t.MissionId == m.Id);
+ return new MissionSummaryDto(
+ Id: m.Id,
+ Code: m.Code,
+ Title: m.TitleEn,
+ Description: m.DescriptionEn,
+ Type: m.Type.Name,
+ Category: m.Category.Name,
+ RewardPoints: m.Reward.Points,
+ Frequency: m.Frequency.Name,
+ MaxCompletions: m.MaxCompletions,
+ IsAvailable: m.IsAvailable(),
+ UserProgress: new UserTaskProgressDto(
+ TaskId: task.Id,
+ CurrentValue: task.Progress.CurrentValue,
+ TargetValue: task.Progress.TargetValue,
+ PercentComplete: task.Progress.PercentComplete,
+ Status: task.Status.Name,
+ StartedAt: task.StartedAt,
+ CompletedAt: task.CompletedAt,
+ RewardClaimed: task.RewardClaimed));
+ })
+ .ToList();
+
+ return new UserMissionProgressResult(
+ TotalMissions: missions.Count,
+ CompletedMissionsCount: completedMissionIds.Count,
+ InProgressMissions: activeTasks.Count,
+ TotalPointsEarned: totalPointsEarned,
+ ActiveMissions: activeMissionSummaries,
+ CompletedMissions: completedMissionSummaries);
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.API/Application/Validations/ClaimTaskRewardCommandValidator.cs b/services/mission-service-net/src/MissionService.API/Application/Validations/ClaimTaskRewardCommandValidator.cs
new file mode 100644
index 00000000..4699d273
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Application/Validations/ClaimTaskRewardCommandValidator.cs
@@ -0,0 +1,20 @@
+using FluentValidation;
+using MissionService.API.Application.Commands;
+
+namespace MissionService.API.Application.Validations;
+
+///
+/// EN: Validator for ClaimTaskRewardCommand.
+/// VI: Validator cho ClaimTaskRewardCommand.
+///
+public class ClaimTaskRewardCommandValidator : AbstractValidator
+{
+ public ClaimTaskRewardCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.TaskId)
+ .NotEmpty().WithMessage("Task ID is required / ID task là bắt buộc");
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.API/Application/Validations/PerformCheckInCommandValidator.cs b/services/mission-service-net/src/MissionService.API/Application/Validations/PerformCheckInCommandValidator.cs
new file mode 100644
index 00000000..9f2361c8
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Application/Validations/PerformCheckInCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using MissionService.API.Application.Commands;
+
+namespace MissionService.API.Application.Validations;
+
+///
+/// EN: Validator for PerformCheckInCommand.
+/// VI: Validator cho PerformCheckInCommand.
+///
+public class PerformCheckInCommandValidator : AbstractValidator
+{
+ public PerformCheckInCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.API/Application/Validations/StartMissionTaskCommandValidator.cs b/services/mission-service-net/src/MissionService.API/Application/Validations/StartMissionTaskCommandValidator.cs
new file mode 100644
index 00000000..25db4710
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Application/Validations/StartMissionTaskCommandValidator.cs
@@ -0,0 +1,20 @@
+using FluentValidation;
+using MissionService.API.Application.Commands;
+
+namespace MissionService.API.Application.Validations;
+
+///
+/// EN: Validator for StartMissionTaskCommand.
+/// VI: Validator cho StartMissionTaskCommand.
+///
+public class StartMissionTaskCommandValidator : AbstractValidator
+{
+ public StartMissionTaskCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.MissionId)
+ .NotEmpty().WithMessage("Mission ID is required / ID nhiệm vụ là bắt buộc");
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.API/Application/Validations/UpdateTaskProgressCommandValidator.cs b/services/mission-service-net/src/MissionService.API/Application/Validations/UpdateTaskProgressCommandValidator.cs
new file mode 100644
index 00000000..46449736
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Application/Validations/UpdateTaskProgressCommandValidator.cs
@@ -0,0 +1,42 @@
+using FluentValidation;
+using MissionService.API.Application.Commands;
+
+namespace MissionService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateTaskProgressCommand.
+/// VI: Validator cho UpdateTaskProgressCommand.
+///
+public class UpdateTaskProgressCommandValidator : AbstractValidator
+{
+ public UpdateTaskProgressCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.TaskId)
+ .NotEmpty().WithMessage("Task ID is required / ID task là bắt buộc");
+
+ RuleFor(x => x.CurrentValue)
+ .GreaterThanOrEqualTo(0).WithMessage("Current value must be non-negative / Giá trị hiện tại không được âm");
+
+ When(x => x.Evidence != null, () =>
+ {
+ RuleFor(x => x.Evidence!.Type)
+ .NotEmpty().WithMessage("Evidence type is required / Loại bằng chứng là bắt buộc")
+ .MaximumLength(50).WithMessage("Evidence type max 50 chars / Loại bằng chứng tối đa 50 ký tự");
+
+ RuleFor(x => x.Evidence!.Data)
+ .NotEmpty().WithMessage("Evidence data is required / Dữ liệu bằng chứng là bắt buộc")
+ .MaximumLength(2000).WithMessage("Evidence data max 2000 chars / Dữ liệu bằng chứng tối đa 2000 ký tự");
+
+ RuleFor(x => x.Evidence!.ScreenshotUrl)
+ .MaximumLength(500).WithMessage("Screenshot URL max 500 chars / URL ảnh chụp tối đa 500 ký tự")
+ .When(x => x.Evidence!.ScreenshotUrl != null);
+
+ RuleFor(x => x.Evidence!.VideoUrl)
+ .MaximumLength(500).WithMessage("Video URL max 500 chars / URL video tối đa 500 ký tự")
+ .When(x => x.Evidence!.VideoUrl != null);
+ });
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/ConversationQueryHandlers.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/ConversationQueryHandlers.cs
index 00479345..3d762eda 100644
--- a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/ConversationQueryHandlers.cs
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/ConversationQueryHandlers.cs
@@ -1,9 +1,95 @@
using MediatR;
+using Microsoft.EntityFrameworkCore;
using FacebookService.API.Application.Dtos;
using FacebookService.Domain.AggregatesModel.ConversationAggregate;
+using FacebookService.Domain.AggregatesModel.CustomerAggregate;
+using FacebookService.Infrastructure;
namespace FacebookService.API.Application.Queries;
+///
+/// EN: Handler for GetConversationsQuery - returns paginated conversations list.
+/// VI: Handler cho GetConversationsQuery - trả về danh sách conversations phân trang.
+///
+public class GetConversationsQueryHandler : IRequestHandler
+{
+ private readonly FacebookServiceContext _context;
+
+ public GetConversationsQueryHandler(FacebookServiceContext context)
+ {
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ }
+
+ public async Task Handle(
+ GetConversationsQuery request,
+ CancellationToken cancellationToken)
+ {
+ // EN: Build base query with status inclusion / VI: Xây dựng query cơ bản với status
+ var query = _context.Conversations
+ .AsNoTracking()
+ .Include(c => c.Status)
+ .AsQueryable();
+
+ // EN: Filter by page ID (shop) if provided / VI: Lọc theo page ID (shop) nếu có
+ if (request.ShopId.HasValue)
+ {
+ var pageId = request.ShopId.Value.ToString();
+ query = query.Where(c => EF.Property(c, "_pageId") == pageId);
+ }
+
+ // EN: Filter by status if provided / VI: Lọc theo status nếu có
+ if (!string.IsNullOrEmpty(request.Status))
+ {
+ var statusId = request.Status.ToLower() switch
+ {
+ "active" => ConversationStatus.Active.Id,
+ "closed" => ConversationStatus.Closed.Id,
+ "archived" => ConversationStatus.Archived.Id,
+ _ => 0
+ };
+ if (statusId > 0)
+ query = query.Where(c => c.StatusId == statusId);
+ }
+
+ // EN: Get total count for pagination / VI: Lấy tổng số cho phân trang
+ var totalCount = await query.CountAsync(cancellationToken);
+
+ // EN: Get paginated conversations ordered by last message time
+ // VI: Lấy conversations phân trang sắp xếp theo thời gian tin nhắn cuối
+ var conversations = await query
+ .OrderByDescending(c => EF.Property(c, "_lastMessageAt"))
+ .Skip(request.Skip)
+ .Take(request.Take)
+ .ToListAsync(cancellationToken);
+
+ // EN: Load customer names for summaries / VI: Tải tên khách hàng cho tóm tắt
+ var customerIds = conversations.Select(c => c.CustomerId).Distinct().ToList();
+ var customers = await _context.Customers
+ .AsNoTracking()
+ .Where(c => customerIds.Contains(c.Id))
+ .ToDictionaryAsync(c => c.Id, c => c.Name, cancellationToken);
+
+ // EN: Map to summary DTOs / VI: Map sang DTO tóm tắt
+ var summaries = conversations.Select(c =>
+ {
+ customers.TryGetValue(c.CustomerId, out var customerName);
+ var lastMessage = c.Messages.OrderByDescending(m => m.SentAt).FirstOrDefault();
+
+ return new ConversationSummaryDto(
+ c.Id,
+ c.CustomerId,
+ customerName,
+ c.PageId,
+ c.Status.Name,
+ lastMessage?.Content,
+ c.LastMessageAt,
+ c.CreatedAt);
+ }).ToList();
+
+ return new GetConversationsQueryResult(summaries, totalCount);
+ }
+}
+
///
/// EN: Handler for GetConversationByIdQuery.
/// VI: Handler cho GetConversationByIdQuery.
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/CustomerQueryHandlers.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/CustomerQueryHandlers.cs
index c2ca5094..388956b3 100644
--- a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/CustomerQueryHandlers.cs
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/CustomerQueryHandlers.cs
@@ -1,9 +1,76 @@
using MediatR;
+using Microsoft.EntityFrameworkCore;
using FacebookService.API.Application.Dtos;
using FacebookService.Domain.AggregatesModel.CustomerAggregate;
+using FacebookService.Infrastructure;
namespace FacebookService.API.Application.Queries;
+///
+/// EN: Handler for GetCustomersQuery - returns paginated customers list.
+/// VI: Handler cho GetCustomersQuery - trả về danh sách customers phân trang.
+///
+public class GetCustomersQueryHandler : IRequestHandler
+{
+ private readonly FacebookServiceContext _context;
+
+ public GetCustomersQueryHandler(FacebookServiceContext context)
+ {
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ }
+
+ public async Task Handle(
+ GetCustomersQuery request,
+ CancellationToken cancellationToken)
+ {
+ // EN: Build base query / VI: Xây dựng query cơ bản
+ var query = _context.Customers.AsNoTracking().AsQueryable();
+
+ // EN: Filter by search term (name match) / VI: Lọc theo từ khóa tìm kiếm (tên)
+ if (!string.IsNullOrWhiteSpace(request.Search))
+ {
+ var searchTerm = request.Search.Trim();
+ query = query.Where(c =>
+ EF.Property(c, "_name") != null &&
+ EF.Property(c, "_name")!.Contains(searchTerm));
+ }
+
+ // EN: Filter by tags if provided / VI: Lọc theo tags nếu có
+ if (request.Tags != null && request.Tags.Any())
+ {
+ var tagList = request.Tags.ToList();
+ foreach (var tag in tagList)
+ {
+ // EN: Each tag must be present / VI: Mỗi tag phải có mặt
+ query = query.Where(c => c.Tags.Contains(tag));
+ }
+ }
+
+ // EN: Get total count for pagination / VI: Lấy tổng số cho phân trang
+ var totalCount = await query.CountAsync(cancellationToken);
+
+ // EN: Get paginated customers ordered by last interaction
+ // VI: Lấy customers phân trang sắp xếp theo tương tác cuối
+ var customers = await query
+ .OrderByDescending(c => EF.Property(c, "_lastInteractionAt"))
+ .Skip(request.Skip)
+ .Take(request.Take)
+ .ToListAsync(cancellationToken);
+
+ // EN: Map to summary DTOs / VI: Map sang DTO tóm tắt
+ var summaries = customers.Select(c => new CustomerSummaryDto(
+ c.Id,
+ c.FacebookUserId,
+ c.Name,
+ c.ProfilePicUrl,
+ c.Tags,
+ c.LastInteractionAt
+ )).ToList();
+
+ return new GetCustomersQueryResult(summaries, totalCount);
+ }
+}
+
///
/// EN: Handler for GetCustomerByIdQuery.
/// VI: Handler cho GetCustomerByIdQuery.
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Commands/VoucherCommandHandlers.cs b/services/promotion-service-net/src/PromotionService.API/Application/Commands/VoucherCommandHandlers.cs
index 3c0a976c..ab292d7e 100644
--- a/services/promotion-service-net/src/PromotionService.API/Application/Commands/VoucherCommandHandlers.cs
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Commands/VoucherCommandHandlers.cs
@@ -9,6 +9,163 @@ using PromotionService.Domain.Exceptions;
namespace PromotionService.API.Application.Commands;
+///
+/// EN: Handler for ExchangeVoucherCommand (exchange points for voucher).
+/// VI: Handler cho ExchangeVoucherCommand (đổi điểm lấy voucher).
+///
+public class ExchangeVoucherCommandHandler : IRequestHandler
+{
+ private readonly ICampaignRepository _campaignRepository;
+ private readonly IWalletServiceClient _walletService;
+ private readonly ILogger _logger;
+
+ public ExchangeVoucherCommandHandler(
+ ICampaignRepository campaignRepository,
+ IWalletServiceClient walletService,
+ ILogger logger)
+ {
+ _campaignRepository = campaignRepository;
+ _walletService = walletService;
+ _logger = logger;
+ }
+
+ public async Task Handle(ExchangeVoucherCommand request, CancellationToken cancellationToken)
+ {
+ // EN: Find and validate campaign / VI: Tìm và xác thực chiến dịch
+ var campaign = await _campaignRepository.GetByIdAsync(request.CampaignId)
+ ?? throw new PromotionDomainException($"Campaign {request.CampaignId} not found");
+
+ // EN: Verify it's a point-exchange campaign / VI: Xác nhận là chiến dịch đổi điểm
+ if (campaign.AcquisitionTypeId != AcquisitionType.ExchangePoints.Id)
+ throw new PromotionDomainException("This campaign does not support point exchange");
+
+ // EN: Deduct points from user's wallet / VI: Trừ điểm từ ví người dùng
+ var pointsRequired = campaign.AcquisitionPrice;
+ var holdResult = await _walletService.CreateHoldAsync(
+ request.UserWalletId,
+ pointsRequired,
+ campaign.BackingAssetCode,
+ "VOUCHER_EXCHANGE",
+ campaign.Id,
+ $"Exchange points for voucher in campaign {campaign.Name}",
+ cancellationToken);
+
+ // EN: Execute the hold immediately (deduct points) / VI: Thực thi hold ngay (trừ điểm)
+ await _walletService.ExecuteHoldAsync(
+ request.UserWalletId,
+ holdResult.HoldId,
+ pointsRequired,
+ $"EXCHANGE:{campaign.Id}:{request.UserId}",
+ cancellationToken);
+
+ // EN: Issue voucher to user / VI: Phát voucher cho người dùng
+ var voucher = campaign.IssueVoucher(request.UserId);
+ _campaignRepository.Update(campaign);
+ await _campaignRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Voucher {VoucherCode} exchanged by user {UserId} for {Points} points",
+ voucher.Code, request.UserId, pointsRequired);
+
+ return MapToDto(voucher);
+ }
+
+ private static VoucherDto MapToDto(Voucher voucher) => new(
+ voucher.Id,
+ voucher.CampaignId,
+ voucher.Code,
+ voucher.OwnerId,
+ voucher.FaceValue,
+ voucher.RemainingValue,
+ voucher.StatusId == VoucherStatus.Available.Id ? "Available"
+ : voucher.StatusId == VoucherStatus.Claimed.Id ? "Claimed"
+ : voucher.StatusId == VoucherStatus.PartiallyRedeemed.Id ? "PartiallyRedeemed"
+ : voucher.StatusId == VoucherStatus.FullyRedeemed.Id ? "FullyRedeemed"
+ : "Expired",
+ voucher.ClaimedAt,
+ voucher.ExpiresAt,
+ voucher.RedeemedAt);
+}
+
+///
+/// EN: Handler for PurchaseVoucherCommand (buy voucher with currency).
+/// VI: Handler cho PurchaseVoucherCommand (mua voucher bằng tiền).
+///
+public class PurchaseVoucherCommandHandler : IRequestHandler
+{
+ private readonly ICampaignRepository _campaignRepository;
+ private readonly IWalletServiceClient _walletService;
+ private readonly ILogger _logger;
+
+ public PurchaseVoucherCommandHandler(
+ ICampaignRepository campaignRepository,
+ IWalletServiceClient walletService,
+ ILogger logger)
+ {
+ _campaignRepository = campaignRepository;
+ _walletService = walletService;
+ _logger = logger;
+ }
+
+ public async Task Handle(PurchaseVoucherCommand request, CancellationToken cancellationToken)
+ {
+ // EN: Find and validate campaign / VI: Tìm và xác thực chiến dịch
+ var campaign = await _campaignRepository.GetByIdAsync(request.CampaignId)
+ ?? throw new PromotionDomainException($"Campaign {request.CampaignId} not found");
+
+ // EN: Verify it's a purchase campaign / VI: Xác nhận là chiến dịch mua
+ if (campaign.AcquisitionTypeId != AcquisitionType.Purchase.Id)
+ throw new PromotionDomainException("This campaign does not support purchase");
+
+ // EN: Create hold and execute payment from user's wallet
+ // VI: Tạo hold và thực thi thanh toán từ ví người dùng
+ var purchaseAmount = campaign.AcquisitionPrice;
+ var holdResult = await _walletService.CreateHoldAsync(
+ request.UserWalletId,
+ purchaseAmount,
+ campaign.BackingAssetCode,
+ "VOUCHER_PURCHASE",
+ campaign.Id,
+ $"Purchase voucher in campaign {campaign.Name}",
+ cancellationToken);
+
+ // EN: Execute the hold immediately (deduct funds) / VI: Thực thi hold ngay (trừ tiền)
+ await _walletService.ExecuteHoldAsync(
+ request.UserWalletId,
+ holdResult.HoldId,
+ purchaseAmount,
+ $"PURCHASE:{campaign.Id}:{request.UserId}",
+ cancellationToken);
+
+ // EN: Issue voucher to user / VI: Phát voucher cho người dùng
+ var voucher = campaign.IssueVoucher(request.UserId);
+ _campaignRepository.Update(campaign);
+ await _campaignRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Voucher {VoucherCode} purchased by user {UserId} for {Amount} {Currency}",
+ voucher.Code, request.UserId, purchaseAmount, campaign.BackingAssetCode);
+
+ return MapToDto(voucher);
+ }
+
+ private static VoucherDto MapToDto(Voucher voucher) => new(
+ voucher.Id,
+ voucher.CampaignId,
+ voucher.Code,
+ voucher.OwnerId,
+ voucher.FaceValue,
+ voucher.RemainingValue,
+ voucher.StatusId == VoucherStatus.Available.Id ? "Available"
+ : voucher.StatusId == VoucherStatus.Claimed.Id ? "Claimed"
+ : voucher.StatusId == VoucherStatus.PartiallyRedeemed.Id ? "PartiallyRedeemed"
+ : voucher.StatusId == VoucherStatus.FullyRedeemed.Id ? "FullyRedeemed"
+ : "Expired",
+ voucher.ClaimedAt,
+ voucher.ExpiresAt,
+ voucher.RedeemedAt);
+}
+
///
/// EN: Handler for ClaimVoucherCommand (free vouchers).
/// VI: Handler cho ClaimVoucherCommand (voucher miễn phí).
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Queries/AdminQueryHandlers.cs b/services/promotion-service-net/src/PromotionService.API/Application/Queries/AdminQueryHandlers.cs
index 81225d52..15d614db 100644
--- a/services/promotion-service-net/src/PromotionService.API/Application/Queries/AdminQueryHandlers.cs
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Queries/AdminQueryHandlers.cs
@@ -258,6 +258,126 @@ public class GetAllRedemptionsQueryHandler : IRequestHandler
+/// EN: Handler for SearchVouchersQuery - search vouchers by code pattern.
+/// VI: Handler cho SearchVouchersQuery - tìm kiếm vouchers theo mẫu mã.
+///
+public class SearchVouchersQueryHandler : IRequestHandler>
+{
+ private readonly PromotionServiceContext _context;
+
+ public SearchVouchersQueryHandler(PromotionServiceContext context)
+ {
+ _context = context;
+ }
+
+ public async Task> Handle(SearchVouchersQuery request, CancellationToken cancellationToken)
+ {
+ // EN: Search vouchers by code pattern (case-insensitive)
+ // VI: Tìm kiếm vouchers theo mẫu mã (không phân biệt hoa thường)
+ if (string.IsNullOrWhiteSpace(request.SearchTerm))
+ return Enumerable.Empty();
+
+ var searchTerm = request.SearchTerm.Trim();
+
+ var items = await _context.Vouchers
+ .AsNoTracking()
+ .Where(v => v.Code.Contains(searchTerm))
+ .OrderByDescending(v => v.CreatedAt)
+ .Take(50) // EN: Limit search results / VI: Giới hạn kết quả tìm kiếm
+ .Join(_context.Campaigns,
+ v => v.CampaignId,
+ c => c.Id,
+ (v, c) => new AdminVoucherListDto(
+ v.Id,
+ v.CampaignId,
+ c.Name,
+ v.Code,
+ v.OwnerId,
+ null,
+ v.FaceValue,
+ v.RemainingValue,
+ v.StatusId == 1 ? "Available" : v.StatusId == 2 ? "Claimed" : v.StatusId == 3 ? "PartiallyRedeemed" : v.StatusId == 4 ? "FullyRedeemed" : "Expired",
+ v.ClaimedAt,
+ v.ExpiresAt,
+ v.RedeemedAt,
+ v.CreatedAt))
+ .ToListAsync(cancellationToken);
+
+ return items;
+ }
+}
+
+///
+/// EN: Handler for GetCampaignVouchersQuery - return vouchers for a specific campaign with pagination.
+/// VI: Handler cho GetCampaignVouchersQuery - trả về vouchers của chiến dịch cụ thể với phân trang.
+///
+public class GetCampaignVouchersQueryHandler : IRequestHandler>
+{
+ private readonly PromotionServiceContext _context;
+
+ public GetCampaignVouchersQueryHandler(PromotionServiceContext context)
+ {
+ _context = context;
+ }
+
+ public async Task> Handle(GetCampaignVouchersQuery request, CancellationToken cancellationToken)
+ {
+ // EN: Build query for campaign vouchers / VI: Xây dựng query cho vouchers chiến dịch
+ var query = _context.Vouchers
+ .AsNoTracking()
+ .Where(v => v.CampaignId == request.CampaignId);
+
+ // EN: Apply status filter if provided / VI: Áp dụng filter status nếu có
+ if (!string.IsNullOrEmpty(request.Status))
+ {
+ var statusId = request.Status.ToLower() switch
+ {
+ "available" => 1,
+ "claimed" => 2,
+ "partiallyredeemed" => 3,
+ "fullyredeemed" => 4,
+ "expired" => 5,
+ _ => 0
+ };
+ if (statusId > 0)
+ query = query.Where(v => v.StatusId == statusId);
+ }
+
+ var totalCount = await query.CountAsync(cancellationToken);
+ var totalPages = (int)Math.Ceiling(totalCount / (double)request.PageSize);
+
+ // EN: Get campaign name for DTO / VI: Lấy tên chiến dịch cho DTO
+ var campaignName = await _context.Campaigns
+ .AsNoTracking()
+ .Where(c => c.Id == request.CampaignId)
+ .Select(c => c.Name)
+ .FirstOrDefaultAsync(cancellationToken) ?? "Unknown";
+
+ var items = await query
+ .OrderByDescending(v => v.CreatedAt)
+ .Skip((request.PageNumber - 1) * request.PageSize)
+ .Take(request.PageSize)
+ .Select(v => new AdminVoucherListDto(
+ v.Id,
+ v.CampaignId,
+ campaignName,
+ v.Code,
+ v.OwnerId,
+ null,
+ v.FaceValue,
+ v.RemainingValue,
+ v.StatusId == 1 ? "Available" : v.StatusId == 2 ? "Claimed" : v.StatusId == 3 ? "PartiallyRedeemed" : v.StatusId == 4 ? "FullyRedeemed" : "Expired",
+ v.ClaimedAt,
+ v.ExpiresAt,
+ v.RedeemedAt,
+ v.CreatedAt))
+ .ToListAsync(cancellationToken);
+
+ return new PaginatedResponse(items, totalCount, request.PageNumber, request.PageSize, totalPages);
+ }
+}
+
///
/// EN: Handler for GetRedemptionStatisticsQuery.
/// VI: Handler cho GetRedemptionStatisticsQuery.
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Queries/PromotionQueryHandlers.cs b/services/promotion-service-net/src/PromotionService.API/Application/Queries/PromotionQueryHandlers.cs
index 88fdfa19..039f830a 100644
--- a/services/promotion-service-net/src/PromotionService.API/Application/Queries/PromotionQueryHandlers.cs
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Queries/PromotionQueryHandlers.cs
@@ -1,6 +1,9 @@
using MediatR;
+using Microsoft.EntityFrameworkCore;
using PromotionService.API.Application.DTOs;
using PromotionService.Domain.AggregatesModel.CampaignAggregate;
+using PromotionService.Domain.AggregatesModel.RedemptionAggregate;
+using PromotionService.Infrastructure;
namespace PromotionService.API.Application.Queries;
@@ -136,3 +139,51 @@ public class GetUserVouchersQueryHandler : IRequestHandler
+/// EN: Handler for GetCampaignStatisticsQuery - returns campaign stats (total vouchers, redeemed count, revenue).
+/// VI: Handler cho GetCampaignStatisticsQuery - trả về thống kê chiến dịch (tổng voucher, số đã dùng, doanh thu).
+///
+public class GetCampaignStatisticsQueryHandler : IRequestHandler
+{
+ private readonly ICampaignRepository _campaignRepository;
+ private readonly IRedemptionRepository _redemptionRepository;
+
+ public GetCampaignStatisticsQueryHandler(
+ ICampaignRepository campaignRepository,
+ IRedemptionRepository redemptionRepository)
+ {
+ _campaignRepository = campaignRepository;
+ _redemptionRepository = redemptionRepository;
+ }
+
+ public async Task Handle(GetCampaignStatisticsQuery request, CancellationToken cancellationToken)
+ {
+ // EN: Load campaign with vouchers / VI: Tải chiến dịch với vouchers
+ var campaign = await _campaignRepository.GetByIdAsync(request.CampaignId);
+ if (campaign == null) return null;
+
+ // EN: Calculate voucher statistics / VI: Tính thống kê voucher
+ var vouchers = campaign.Vouchers;
+ var claimedCount = vouchers.Count(v => v.OwnerId.HasValue);
+ var redeemedCount = campaign.RedeemedVoucherCount;
+ var totalFaceValue = campaign.FaceValue * campaign.TotalVouchers;
+ var totalRedeemedValue = campaign.TotalRedeemedValue;
+
+ // EN: Calculate utilization rate / VI: Tính tỷ lệ sử dụng
+ var utilizationRate = campaign.TotalVouchers > 0
+ ? (decimal)redeemedCount / campaign.TotalVouchers * 100
+ : 0;
+
+ return new CampaignStatisticsDto(
+ CampaignId: campaign.Id,
+ CampaignName: campaign.Name,
+ TotalVouchers: campaign.TotalVouchers,
+ AvailableVouchers: campaign.AvailableVoucherCount,
+ ClaimedVouchers: claimedCount,
+ RedeemedVouchers: redeemedCount,
+ TotalFaceValue: totalFaceValue,
+ TotalRedeemedValue: totalRedeemedValue,
+ UtilizationRate: utilizationRate);
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/ActivateCampaignCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/ActivateCampaignCommandValidator.cs
new file mode 100644
index 00000000..bbbd5840
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/ActivateCampaignCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for ActivateCampaignCommand.
+/// VI: Validator cho ActivateCampaignCommand.
+///
+public class ActivateCampaignCommandValidator : AbstractValidator
+{
+ public ActivateCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/CancelCampaignCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/CancelCampaignCommandValidator.cs
new file mode 100644
index 00000000..2495dbeb
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/CancelCampaignCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for CancelCampaignCommand.
+/// VI: Validator cho CancelCampaignCommand.
+///
+public class CancelCampaignCommandValidator : AbstractValidator
+{
+ public CancelCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/ClaimVoucherCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/ClaimVoucherCommandValidator.cs
new file mode 100644
index 00000000..07793e7a
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/ClaimVoucherCommandValidator.cs
@@ -0,0 +1,20 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for ClaimVoucherCommand.
+/// VI: Validator cho ClaimVoucherCommand.
+///
+public class ClaimVoucherCommandValidator : AbstractValidator
+{
+ public ClaimVoucherCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/CompleteCampaignCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/CompleteCampaignCommandValidator.cs
new file mode 100644
index 00000000..0705b681
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/CompleteCampaignCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for CompleteCampaignCommand.
+/// VI: Validator cho CompleteCampaignCommand.
+///
+public class CompleteCampaignCommandValidator : AbstractValidator
+{
+ public CompleteCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/CreateCampaignCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/CreateCampaignCommandValidator.cs
new file mode 100644
index 00000000..858ee615
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/CreateCampaignCommandValidator.cs
@@ -0,0 +1,63 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateCampaignCommand.
+/// VI: Validator cho CreateCampaignCommand.
+///
+public class CreateCampaignCommandValidator : AbstractValidator
+{
+ public CreateCampaignCommandValidator()
+ {
+ RuleFor(x => x.MerchantId)
+ .NotEmpty().WithMessage("Merchant ID is required / ID merchant là bắt buộc");
+
+ RuleFor(x => x.MerchantWalletId)
+ .NotEmpty().WithMessage("Merchant wallet ID is required / ID ví merchant là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("Campaign name is required / Tên chiến dịch là bắt buộc")
+ .MaximumLength(200).WithMessage("Campaign name max 200 chars / Tên chiến dịch tối đa 200 ký tự");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000).WithMessage("Description max 1000 chars / Mô tả tối đa 1000 ký tự")
+ .When(x => x.Description != null);
+
+ RuleFor(x => x.BackingAssetType)
+ .NotEmpty().WithMessage("Backing asset type is required / Loại tài sản đảm bảo là bắt buộc")
+ .MaximumLength(50).WithMessage("Backing asset type max 50 chars / Loại tài sản đảm bảo tối đa 50 ký tự");
+
+ RuleFor(x => x.BackingAssetCode)
+ .NotEmpty().WithMessage("Backing asset code is required / Mã tài sản đảm bảo là bắt buộc")
+ .MaximumLength(50).WithMessage("Backing asset code max 50 chars / Mã tài sản đảm bảo tối đa 50 ký tự");
+
+ RuleFor(x => x.FaceValue)
+ .GreaterThan(0).WithMessage("Face value must be positive / Mệnh giá phải dương");
+
+ RuleFor(x => x.AcquisitionType)
+ .NotEmpty().WithMessage("Acquisition type is required / Loại nhận là bắt buộc")
+ .MaximumLength(50).WithMessage("Acquisition type max 50 chars / Loại nhận tối đa 50 ký tự");
+
+ RuleFor(x => x.AcquisitionPrice)
+ .GreaterThanOrEqualTo(0).WithMessage("Acquisition price must be non-negative / Giá nhận không được âm");
+
+ RuleFor(x => x.TotalVouchers)
+ .GreaterThan(0).WithMessage("Total vouchers must be positive / Tổng voucher phải dương")
+ .LessThanOrEqualTo(1000000).WithMessage("Total vouchers max 1,000,000 / Tổng voucher tối đa 1.000.000");
+
+ RuleFor(x => x.StartDate)
+ .NotEmpty().WithMessage("Start date is required / Ngày bắt đầu là bắt buộc");
+
+ RuleFor(x => x.EndDate)
+ .NotEmpty().WithMessage("End date is required / Ngày kết thúc là bắt buộc")
+ .GreaterThan(x => x.StartDate).WithMessage("End date must be after start date / Ngày kết thúc phải sau ngày bắt đầu");
+
+ RuleFor(x => x.VoucherValidityDays)
+ .InclusiveBetween(1, 365).WithMessage("Voucher validity must be 1-365 days / Hiệu lực voucher phải từ 1-365 ngày");
+
+ RuleFor(x => x.MaxPerUser)
+ .InclusiveBetween(1, 100).WithMessage("Max per user must be 1-100 / Tối đa mỗi người phải từ 1-100");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/DeleteCampaignCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/DeleteCampaignCommandValidator.cs
new file mode 100644
index 00000000..3e22ce4e
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/DeleteCampaignCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for DeleteCampaignCommand.
+/// VI: Validator cho DeleteCampaignCommand.
+///
+public class DeleteCampaignCommandValidator : AbstractValidator
+{
+ public DeleteCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/ExchangeVoucherCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/ExchangeVoucherCommandValidator.cs
new file mode 100644
index 00000000..ac4bb442
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/ExchangeVoucherCommandValidator.cs
@@ -0,0 +1,23 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for ExchangeVoucherCommand.
+/// VI: Validator cho ExchangeVoucherCommand.
+///
+public class ExchangeVoucherCommandValidator : AbstractValidator
+{
+ public ExchangeVoucherCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.UserWalletId)
+ .NotEmpty().WithMessage("User wallet ID is required / ID ví người dùng là bắt buộc");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/ExtendVoucherExpiryCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/ExtendVoucherExpiryCommandValidator.cs
new file mode 100644
index 00000000..b2ec65ed
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/ExtendVoucherExpiryCommandValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for ExtendVoucherExpiryCommand.
+/// VI: Validator cho ExtendVoucherExpiryCommand.
+///
+public class ExtendVoucherExpiryCommandValidator : AbstractValidator
+{
+ public ExtendVoucherExpiryCommandValidator()
+ {
+ RuleFor(x => x.VoucherId)
+ .NotEmpty().WithMessage("Voucher ID is required / ID voucher là bắt buộc");
+
+ RuleFor(x => x.AdditionalDays)
+ .GreaterThan(0).WithMessage("Additional days must be positive / Số ngày gia hạn phải dương")
+ .LessThanOrEqualTo(365).WithMessage("Additional days max 365 / Số ngày gia hạn tối đa 365");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/PauseCampaignCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/PauseCampaignCommandValidator.cs
new file mode 100644
index 00000000..baf224bd
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/PauseCampaignCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for PauseCampaignCommand.
+/// VI: Validator cho PauseCampaignCommand.
+///
+public class PauseCampaignCommandValidator : AbstractValidator
+{
+ public PauseCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/PurchaseVoucherCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/PurchaseVoucherCommandValidator.cs
new file mode 100644
index 00000000..7ba9bf05
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/PurchaseVoucherCommandValidator.cs
@@ -0,0 +1,23 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for PurchaseVoucherCommand.
+/// VI: Validator cho PurchaseVoucherCommand.
+///
+public class PurchaseVoucherCommandValidator : AbstractValidator
+{
+ public PurchaseVoucherCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.UserWalletId)
+ .NotEmpty().WithMessage("User wallet ID is required / ID ví người dùng là bắt buộc");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/RedeemVoucherCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/RedeemVoucherCommandValidator.cs
new file mode 100644
index 00000000..6e28ac1b
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/RedeemVoucherCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for RedeemVoucherCommand.
+/// VI: Validator cho RedeemVoucherCommand.
+///
+public class RedeemVoucherCommandValidator : AbstractValidator
+{
+ public RedeemVoucherCommandValidator()
+ {
+ RuleFor(x => x.VoucherCode)
+ .NotEmpty().WithMessage("Voucher code is required / Mã voucher là bắt buộc")
+ .MaximumLength(50).WithMessage("Voucher code max 50 chars / Mã voucher tối đa 50 ký tự");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+
+ RuleFor(x => x.OrderAmount)
+ .GreaterThan(0).WithMessage("Order amount must be positive / Số tiền đơn hàng phải dương");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/RevokeVoucherCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/RevokeVoucherCommandValidator.cs
new file mode 100644
index 00000000..26bd87f4
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/RevokeVoucherCommandValidator.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for RevokeVoucherCommand.
+/// VI: Validator cho RevokeVoucherCommand.
+///
+public class RevokeVoucherCommandValidator : AbstractValidator
+{
+ public RevokeVoucherCommandValidator()
+ {
+ RuleFor(x => x.VoucherId)
+ .NotEmpty().WithMessage("Voucher ID is required / ID voucher là bắt buộc");
+
+ RuleFor(x => x.Reason)
+ .NotEmpty().WithMessage("Reason is required / Lý do là bắt buộc")
+ .MaximumLength(500).WithMessage("Reason max 500 chars / Lý do tối đa 500 ký tự");
+ }
+}
diff --git a/services/promotion-service-net/src/PromotionService.API/Application/Validations/UpdateCampaignCommandValidator.cs b/services/promotion-service-net/src/PromotionService.API/Application/Validations/UpdateCampaignCommandValidator.cs
new file mode 100644
index 00000000..6b806a73
--- /dev/null
+++ b/services/promotion-service-net/src/PromotionService.API/Application/Validations/UpdateCampaignCommandValidator.cs
@@ -0,0 +1,34 @@
+using FluentValidation;
+using PromotionService.API.Application.Commands;
+
+namespace PromotionService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateCampaignCommand.
+/// VI: Validator cho UpdateCampaignCommand.
+///
+public class UpdateCampaignCommandValidator : AbstractValidator
+{
+ public UpdateCampaignCommandValidator()
+ {
+ RuleFor(x => x.CampaignId)
+ .NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .MaximumLength(200).WithMessage("Campaign name max 200 chars / Tên chiến dịch tối đa 200 ký tự")
+ .When(x => x.Name != null);
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000).WithMessage("Description max 1000 chars / Mô tả tối đa 1000 ký tự")
+ .When(x => x.Description != null);
+
+ RuleFor(x => x.EndDate)
+ .GreaterThan(x => x.StartDate!.Value)
+ .WithMessage("End date must be after start date / Ngày kết thúc phải sau ngày bắt đầu")
+ .When(x => x.StartDate.HasValue && x.EndDate.HasValue);
+
+ RuleFor(x => x.MaxPerUser)
+ .InclusiveBetween(1, 100).WithMessage("Max per user must be 1-100 / Tối đa mỗi người phải từ 1-100")
+ .When(x => x.MaxPerUser.HasValue);
+ }
+}
diff --git a/services/social-service-net/src/SocialService.API/Application/Validations/AdminDeleteBlockCommandValidator.cs b/services/social-service-net/src/SocialService.API/Application/Validations/AdminDeleteBlockCommandValidator.cs
new file mode 100644
index 00000000..bb2a7bdd
--- /dev/null
+++ b/services/social-service-net/src/SocialService.API/Application/Validations/AdminDeleteBlockCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using SocialService.API.Application.Commands;
+
+namespace SocialService.API.Application.Validations;
+
+///
+/// EN: Validator for AdminDeleteBlockCommand.
+/// VI: Validator cho AdminDeleteBlockCommand.
+///
+public class AdminDeleteBlockCommandValidator : AbstractValidator
+{
+ public AdminDeleteBlockCommandValidator()
+ {
+ RuleFor(x => x.BlockId)
+ .NotEmpty().WithMessage("Block ID is required / ID block là bắt buộc");
+ }
+}
diff --git a/services/social-service-net/src/SocialService.API/Application/Validations/AdminDeleteRelationshipCommandValidator.cs b/services/social-service-net/src/SocialService.API/Application/Validations/AdminDeleteRelationshipCommandValidator.cs
new file mode 100644
index 00000000..294c4bb5
--- /dev/null
+++ b/services/social-service-net/src/SocialService.API/Application/Validations/AdminDeleteRelationshipCommandValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using SocialService.API.Application.Commands;
+
+namespace SocialService.API.Application.Validations;
+
+///
+/// EN: Validator for AdminDeleteRelationshipCommand.
+/// VI: Validator cho AdminDeleteRelationshipCommand.
+///
+public class AdminDeleteRelationshipCommandValidator : AbstractValidator
+{
+ public AdminDeleteRelationshipCommandValidator()
+ {
+ RuleFor(x => x.RelationshipId)
+ .NotEmpty().WithMessage("Relationship ID is required / ID quan hệ là bắt buộc");
+ }
+}
diff --git a/services/social-service-net/src/SocialService.API/Application/Validations/BlockUserCommandValidator.cs b/services/social-service-net/src/SocialService.API/Application/Validations/BlockUserCommandValidator.cs
new file mode 100644
index 00000000..28d8bca9
--- /dev/null
+++ b/services/social-service-net/src/SocialService.API/Application/Validations/BlockUserCommandValidator.cs
@@ -0,0 +1,28 @@
+using FluentValidation;
+using SocialService.API.Application.Commands;
+
+namespace SocialService.API.Application.Validations;
+
+///
+/// EN: Validator for BlockUserCommand.
+/// VI: Validator cho BlockUserCommand.
+///
+public class BlockUserCommandValidator : AbstractValidator
+{
+ public BlockUserCommandValidator()
+ {
+ RuleFor(x => x.BlockerId)
+ .NotEmpty().WithMessage("Blocker ID is required / ID người block là bắt buộc");
+
+ RuleFor(x => x.BlockedId)
+ .NotEmpty().WithMessage("Blocked user ID is required / ID người bị block là bắt buộc");
+
+ RuleFor(x => x)
+ .Must(x => x.BlockerId != x.BlockedId)
+ .WithMessage("Cannot block yourself / Không thể block chính mình");
+
+ RuleFor(x => x.Reason)
+ .MaximumLength(500).WithMessage("Reason max 500 chars / Lý do tối đa 500 ký tự")
+ .When(x => x.Reason != null);
+ }
+}
diff --git a/services/social-service-net/src/SocialService.API/Application/Validations/FollowUserCommandValidator.cs b/services/social-service-net/src/SocialService.API/Application/Validations/FollowUserCommandValidator.cs
new file mode 100644
index 00000000..2da2efd2
--- /dev/null
+++ b/services/social-service-net/src/SocialService.API/Application/Validations/FollowUserCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using SocialService.API.Application.Commands;
+
+namespace SocialService.API.Application.Validations;
+
+///
+/// EN: Validator for FollowUserCommand.
+/// VI: Validator cho FollowUserCommand.
+///
+public class FollowUserCommandValidator : AbstractValidator
+{
+ public FollowUserCommandValidator()
+ {
+ RuleFor(x => x.FollowerId)
+ .NotEmpty().WithMessage("Follower ID is required / ID người theo dõi là bắt buộc");
+
+ RuleFor(x => x.FolloweeId)
+ .NotEmpty().WithMessage("Followee ID is required / ID người được theo dõi là bắt buộc");
+
+ RuleFor(x => x)
+ .Must(x => x.FollowerId != x.FolloweeId)
+ .WithMessage("Cannot follow yourself / Không thể theo dõi chính mình");
+ }
+}
diff --git a/services/social-service-net/src/SocialService.API/Application/Validations/RespondToFriendRequestCommandValidator.cs b/services/social-service-net/src/SocialService.API/Application/Validations/RespondToFriendRequestCommandValidator.cs
new file mode 100644
index 00000000..d1ad1966
--- /dev/null
+++ b/services/social-service-net/src/SocialService.API/Application/Validations/RespondToFriendRequestCommandValidator.cs
@@ -0,0 +1,20 @@
+using FluentValidation;
+using SocialService.API.Application.Commands;
+
+namespace SocialService.API.Application.Validations;
+
+///
+/// EN: Validator for RespondToFriendRequestCommand.
+/// VI: Validator cho RespondToFriendRequestCommand.
+///
+public class RespondToFriendRequestCommandValidator : AbstractValidator
+{
+ public RespondToFriendRequestCommandValidator()
+ {
+ RuleFor(x => x.RelationshipId)
+ .NotEmpty().WithMessage("Relationship ID is required / ID quan hệ là bắt buộc");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
+ }
+}
diff --git a/services/social-service-net/src/SocialService.API/Application/Validations/SendFriendRequestCommandValidator.cs b/services/social-service-net/src/SocialService.API/Application/Validations/SendFriendRequestCommandValidator.cs
new file mode 100644
index 00000000..d108bfc6
--- /dev/null
+++ b/services/social-service-net/src/SocialService.API/Application/Validations/SendFriendRequestCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using SocialService.API.Application.Commands;
+
+namespace SocialService.API.Application.Validations;
+
+///
+/// EN: Validator for SendFriendRequestCommand.
+/// VI: Validator cho SendFriendRequestCommand.
+///
+public class SendFriendRequestCommandValidator : AbstractValidator
+{
+ public SendFriendRequestCommandValidator()
+ {
+ RuleFor(x => x.RequesterId)
+ .NotEmpty().WithMessage("Requester ID is required / ID người gửi là bắt buộc");
+
+ RuleFor(x => x.AddresseeId)
+ .NotEmpty().WithMessage("Addressee ID is required / ID người nhận là bắt buộc");
+
+ RuleFor(x => x)
+ .Must(x => x.RequesterId != x.AddresseeId)
+ .WithMessage("Cannot send friend request to yourself / Không thể gửi yêu cầu kết bạn cho chính mình");
+ }
+}
diff --git a/services/social-service-net/src/SocialService.API/Application/Validations/UnblockUserCommandValidator.cs b/services/social-service-net/src/SocialService.API/Application/Validations/UnblockUserCommandValidator.cs
new file mode 100644
index 00000000..6d18293d
--- /dev/null
+++ b/services/social-service-net/src/SocialService.API/Application/Validations/UnblockUserCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using SocialService.API.Application.Commands;
+
+namespace SocialService.API.Application.Validations;
+
+///
+/// EN: Validator for UnblockUserCommand.
+/// VI: Validator cho UnblockUserCommand.
+///
+public class UnblockUserCommandValidator : AbstractValidator
+{
+ public UnblockUserCommandValidator()
+ {
+ RuleFor(x => x.BlockerId)
+ .NotEmpty().WithMessage("Blocker ID is required / ID người block là bắt buộc");
+
+ RuleFor(x => x.BlockedId)
+ .NotEmpty().WithMessage("Blocked user ID is required / ID người bị block là bắt buộc");
+
+ RuleFor(x => x)
+ .Must(x => x.BlockerId != x.BlockedId)
+ .WithMessage("Cannot unblock yourself / Không thể bỏ block chính mình");
+ }
+}
diff --git a/services/social-service-net/src/SocialService.API/Application/Validations/UnfollowUserCommandValidator.cs b/services/social-service-net/src/SocialService.API/Application/Validations/UnfollowUserCommandValidator.cs
new file mode 100644
index 00000000..e4511edd
--- /dev/null
+++ b/services/social-service-net/src/SocialService.API/Application/Validations/UnfollowUserCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+using SocialService.API.Application.Commands;
+
+namespace SocialService.API.Application.Validations;
+
+///
+/// EN: Validator for UnfollowUserCommand.
+/// VI: Validator cho UnfollowUserCommand.
+///
+public class UnfollowUserCommandValidator : AbstractValidator
+{
+ public UnfollowUserCommandValidator()
+ {
+ RuleFor(x => x.FollowerId)
+ .NotEmpty().WithMessage("Follower ID is required / ID người theo dõi là bắt buộc");
+
+ RuleFor(x => x.FolloweeId)
+ .NotEmpty().WithMessage("Followee ID is required / ID người được theo dõi là bắt buộc");
+
+ RuleFor(x => x)
+ .Must(x => x.FollowerId != x.FolloweeId)
+ .WithMessage("Cannot unfollow yourself / Không thể bỏ theo dõi chính mình");
+ }
+}