feat(P1): add 57 validators + 10 missing handlers across 13 services

Wave 2 — 3 parallel agents fixing P1 issues:

Validators (57 new FluentValidation validators):
- ads-manager: 10 validators for all commands
- ads-billing: 3 validators for all commands
- ads-tracking: 2 validators for missing commands
- ads-analytics: 1 validator for CreateReport
- social: 8 validators for all commands
- mining: 16 validators for all commands
- mission: 4 validators for all commands
- promotion: 13 validators for all commands

Missing handlers (10 implemented):
- promotion: ExchangeVoucher, PurchaseVoucher, SearchVouchers,
  GetCampaignStatistics, GetCampaignVouchers
- mission: GetUserMissionProgress
- mkt-facebook: GetConversations, GetCustomers
- ads-manager: ListAudiences, GetAudienceById

All validators use bilingual messages (EN/VI) and are auto-registered
via MediatR ValidatorBehavior pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-03-13 20:24:06 +07:00
parent f8606e0447
commit 59b2cecaf2
64 changed files with 2186 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
using FluentValidation;
using AdsAnalyticsService.API.Application.Commands;
namespace AdsAnalyticsService.API.Application.Validations;
/// <summary>
/// EN: Validator for CreateReportCommand.
/// VI: Validator cho CreateReportCommand.
/// </summary>
public class CreateReportCommandValidator : AbstractValidator<CreateReportCommand>
{
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");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using AdsBillingService.API.Application.Commands;
namespace AdsBillingService.API.Application.Validations;
/// <summary>
/// EN: Validator for AddFundsCommand.
/// VI: Validator cho AddFundsCommand.
/// </summary>
public class AddFundsCommandValidator : AbstractValidator<AddFundsCommand>
{
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");
}
}

View File

@@ -0,0 +1,38 @@
using FluentValidation;
using AdsBillingService.API.Application.Commands;
namespace AdsBillingService.API.Application.Validations;
/// <summary>
/// EN: Validator for ChargeAdvertiserCommand.
/// VI: Validator cho ChargeAdvertiserCommand.
/// </summary>
public class ChargeAdvertiserCommandValidator : AbstractValidator<ChargeAdvertiserCommand>
{
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");
}
}

View File

@@ -0,0 +1,26 @@
using FluentValidation;
using AdsBillingService.API.Application.Commands;
namespace AdsBillingService.API.Application.Validations;
/// <summary>
/// EN: Validator for CreateBillingAccountCommand.
/// VI: Validator cho CreateBillingAccountCommand.
/// </summary>
public class CreateBillingAccountCommandValidator : AbstractValidator<CreateBillingAccountCommand>
{
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'");
}
}

View File

@@ -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;
/// <summary>
/// EN: Handler for ListAudiencesQuery - returns audiences for an advertiser.
/// VI: Handler cho ListAudiencesQuery - trả về audiences cho nhà quảng cáo.
/// </summary>
public class ListAudiencesQueryHandler : IRequestHandler<ListAudiencesQuery, List<AudienceDto>>
{
private readonly AdsManagerServiceContext _context;
public ListAudiencesQueryHandler(AdsManagerServiceContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<List<AudienceDto>> 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;
}
}
/// <summary>
/// EN: Handler for GetAudienceByIdQuery - returns a single audience by ID.
/// VI: Handler cho GetAudienceByIdQuery - trả về một audience theo ID.
/// </summary>
public class GetAudienceByIdQueryHandler : IRequestHandler<GetAudienceByIdQuery, AudienceDto?>
{
private readonly AdsManagerServiceContext _context;
public GetAudienceByIdQueryHandler(AdsManagerServiceContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<AudienceDto?> 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;
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using AdsManagerService.API.Application.Commands;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for ActivateCampaignCommand.
/// VI: Validator cho ActivateCampaignCommand.
/// </summary>
public class ActivateCampaignCommandValidator : AbstractValidator<ActivateCampaignCommand>
{
public ActivateCampaignCommandValidator()
{
RuleFor(x => x.CampaignId)
.NotEmpty()
.WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using AdsManagerService.API.Controllers;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for ApproveAdCommand.
/// VI: Validator cho ApproveAdCommand.
/// </summary>
public class ApproveAdCommandValidator : AbstractValidator<ApproveAdCommand>
{
public ApproveAdCommandValidator()
{
RuleFor(x => x.AdId)
.NotEmpty()
.WithMessage("Ad ID is required / Ad ID là bắt buộc");
}
}

View File

@@ -0,0 +1,57 @@
using FluentValidation;
using AdsManagerService.API.Application.Commands;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for CreateAdCommand.
/// VI: Validator cho CreateAdCommand.
/// </summary>
public class CreateAdCommandValidator : AbstractValidator<CreateAdCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,66 @@
using FluentValidation;
using AdsManagerService.API.Application.Commands;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for CreateAdSetCommand.
/// VI: Validator cho CreateAdSetCommand.
/// </summary>
public class CreateAdSetCommandValidator : AbstractValidator<CreateAdSetCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,59 @@
using FluentValidation;
using AdsManagerService.API.Application.Commands;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for CreateCampaignCommand.
/// VI: Validator cho CreateCampaignCommand.
/// </summary>
public class CreateCampaignCommandValidator : AbstractValidator<CreateCampaignCommand>
{
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");
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using AdsManagerService.API.Application.Commands;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for DeleteCampaignCommand.
/// VI: Validator cho DeleteCampaignCommand.
/// </summary>
public class DeleteCampaignCommandValidator : AbstractValidator<DeleteCampaignCommand>
{
public DeleteCampaignCommandValidator()
{
RuleFor(x => x.CampaignId)
.NotEmpty()
.WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using AdsManagerService.API.Application.Commands;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for PauseCampaignCommand.
/// VI: Validator cho PauseCampaignCommand.
/// </summary>
public class PauseCampaignCommandValidator : AbstractValidator<PauseCampaignCommand>
{
public PauseCampaignCommandValidator()
{
RuleFor(x => x.CampaignId)
.NotEmpty()
.WithMessage("Campaign ID is required / Campaign ID là bắt buộc");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using AdsManagerService.API.Controllers;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for RejectAdCommand.
/// VI: Validator cho RejectAdCommand.
/// </summary>
public class RejectAdCommandValidator : AbstractValidator<RejectAdCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using AdsManagerService.API.Application.Commands;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for SubmitAdForReviewCommand.
/// VI: Validator cho SubmitAdForReviewCommand.
/// </summary>
public class SubmitAdForReviewCommandValidator : AbstractValidator<SubmitAdForReviewCommand>
{
public SubmitAdForReviewCommandValidator()
{
RuleFor(x => x.AdId)
.NotEmpty()
.WithMessage("Ad ID is required / Ad ID là bắt buộc");
}
}

View File

@@ -0,0 +1,29 @@
using FluentValidation;
using AdsManagerService.API.Application.Commands;
namespace AdsManagerService.API.Application.Validations;
/// <summary>
/// EN: Validator for UpdateCampaignCommand.
/// VI: Validator cho UpdateCampaignCommand.
/// </summary>
public class UpdateCampaignCommandValidator : AbstractValidator<UpdateCampaignCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,42 @@
using FluentValidation;
using AdsTrackingService.API.Application.Commands;
namespace AdsTrackingService.API.Application.Validations;
/// <summary>
/// EN: Validator for RecordConversionCommand.
/// VI: Validator cho RecordConversionCommand.
/// </summary>
public class RecordConversionCommandValidator : AbstractValidator<RecordConversionCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,42 @@
using FluentValidation;
using AdsTrackingService.API.Application.Commands;
namespace AdsTrackingService.API.Application.Validations;
/// <summary>
/// EN: Validator for TrackPixelEventCommand.
/// VI: Validator cho TrackPixelEventCommand.
/// </summary>
public class TrackPixelEventCommandValidator : AbstractValidator<TrackPixelEventCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,20 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for AcceptCircleInviteCommand.
/// VI: Validator cho AcceptCircleInviteCommand.
/// </summary>
public class AcceptCircleInviteCommandValidator : AbstractValidator<AcceptCircleInviteCommand>
{
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");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for AdjustMinerPointsCommand.
/// VI: Validator cho AdjustMinerPointsCommand.
/// </summary>
public class AdjustMinerPointsCommandValidator : AbstractValidator<AdjustMinerPointsCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for ApplyReferralCodeCommand.
/// VI: Validator cho ApplyReferralCodeCommand.
/// </summary>
public class ApplyReferralCodeCommandValidator : AbstractValidator<ApplyReferralCodeCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for BanMinerCommand.
/// VI: Validator cho BanMinerCommand.
/// </summary>
public class BanMinerCommandValidator : AbstractValidator<BanMinerCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for ClaimMiningRewardCommand.
/// VI: Validator cho ClaimMiningRewardCommand.
/// </summary>
public class ClaimMiningRewardCommandValidator : AbstractValidator<ClaimMiningRewardCommand>
{
public ClaimMiningRewardCommandValidator()
{
RuleFor(x => x.UserId)
.NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for CreateCircleCommand.
/// VI: Validator cho CreateCircleCommand.
/// </summary>
public class CreateCircleCommandValidator : AbstractValidator<CreateCircleCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for InviteToCircleCommand.
/// VI: Validator cho InviteToCircleCommand.
/// </summary>
public class InviteToCircleCommandValidator : AbstractValidator<InviteToCircleCommand>
{
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");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for RemoveCircleMemberCommand.
/// VI: Validator cho RemoveCircleMemberCommand.
/// </summary>
public class RemoveCircleMemberCommandValidator : AbstractValidator<RemoveCircleMemberCommand>
{
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");
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for ResetMinerStreakCommand.
/// VI: Validator cho ResetMinerStreakCommand.
/// </summary>
public class ResetMinerStreakCommandValidator : AbstractValidator<ResetMinerStreakCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for RestoreMinerCommand.
/// VI: Validator cho RestoreMinerCommand.
/// </summary>
public class RestoreMinerCommandValidator : AbstractValidator<RestoreMinerCommand>
{
public RestoreMinerCommandValidator()
{
RuleFor(x => x.MinerId)
.NotEmpty().WithMessage("Miner ID is required / ID thợ đào là bắt buộc");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for StartMiningCommand.
/// VI: Validator cho StartMiningCommand.
/// </summary>
public class StartMiningCommandValidator : AbstractValidator<StartMiningCommand>
{
public StartMiningCommandValidator()
{
RuleFor(x => x.UserId)
.NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for SuspendMinerCommand.
/// VI: Validator cho SuspendMinerCommand.
/// </summary>
public class SuspendMinerCommandValidator : AbstractValidator<SuspendMinerCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,26 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for UpdateMiningConfigCommand.
/// VI: Validator cho UpdateMiningConfigCommand.
/// </summary>
public class UpdateMiningConfigCommandValidator : AbstractValidator<UpdateMiningConfigCommand>
{
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);
}
}

View File

@@ -0,0 +1,22 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for UpdateReferralConfigCommand.
/// VI: Validator cho UpdateReferralConfigCommand.
/// </summary>
public class UpdateReferralConfigCommandValidator : AbstractValidator<UpdateReferralConfigCommand>
{
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);
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for UpdateStreakConfigCommand.
/// VI: Validator cho UpdateStreakConfigCommand.
/// </summary>
public class UpdateStreakConfigCommandValidator : AbstractValidator<UpdateStreakConfigCommand>
{
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);
}
}

View File

@@ -0,0 +1,40 @@
using FluentValidation;
using MiningService.API.Application.Commands;
namespace MiningService.API.Application.Validations;
/// <summary>
/// EN: Validator for UpdateSystemConfigCommand.
/// VI: Validator cho UpdateSystemConfigCommand.
/// </summary>
public class UpdateSystemConfigCommandValidator : AbstractValidator<UpdateSystemConfigCommand>
{
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);
});
}
}

View File

@@ -163,3 +163,113 @@ public class GetMissionsByCategoryQueryHandler : IRequestHandler<GetMissionsByCa
return new MissionsListResult(summaries);
}
}
/// <summary>
/// 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.
/// </summary>
public class GetUserMissionProgressQueryHandler : IRequestHandler<GetUserMissionProgressQuery, UserMissionProgressResult>
{
private readonly IMissionRepository _missionRepository;
private readonly IUserTaskRepository _taskRepository;
public GetUserMissionProgressQueryHandler(
IMissionRepository missionRepository,
IUserTaskRepository taskRepository)
{
_missionRepository = missionRepository;
_taskRepository = taskRepository;
}
public async Task<UserMissionProgressResult> 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);
}
}

View File

@@ -0,0 +1,20 @@
using FluentValidation;
using MissionService.API.Application.Commands;
namespace MissionService.API.Application.Validations;
/// <summary>
/// EN: Validator for ClaimTaskRewardCommand.
/// VI: Validator cho ClaimTaskRewardCommand.
/// </summary>
public class ClaimTaskRewardCommandValidator : AbstractValidator<ClaimTaskRewardCommand>
{
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");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using MissionService.API.Application.Commands;
namespace MissionService.API.Application.Validations;
/// <summary>
/// EN: Validator for PerformCheckInCommand.
/// VI: Validator cho PerformCheckInCommand.
/// </summary>
public class PerformCheckInCommandValidator : AbstractValidator<PerformCheckInCommand>
{
public PerformCheckInCommandValidator()
{
RuleFor(x => x.UserId)
.NotEmpty().WithMessage("User ID is required / ID người dùng là bắt buộc");
}
}

View File

@@ -0,0 +1,20 @@
using FluentValidation;
using MissionService.API.Application.Commands;
namespace MissionService.API.Application.Validations;
/// <summary>
/// EN: Validator for StartMissionTaskCommand.
/// VI: Validator cho StartMissionTaskCommand.
/// </summary>
public class StartMissionTaskCommandValidator : AbstractValidator<StartMissionTaskCommand>
{
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");
}
}

View File

@@ -0,0 +1,42 @@
using FluentValidation;
using MissionService.API.Application.Commands;
namespace MissionService.API.Application.Validations;
/// <summary>
/// EN: Validator for UpdateTaskProgressCommand.
/// VI: Validator cho UpdateTaskProgressCommand.
/// </summary>
public class UpdateTaskProgressCommandValidator : AbstractValidator<UpdateTaskProgressCommand>
{
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);
});
}
}

View File

@@ -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;
/// <summary>
/// EN: Handler for GetConversationsQuery - returns paginated conversations list.
/// VI: Handler cho GetConversationsQuery - trả về danh sách conversations phân trang.
/// </summary>
public class GetConversationsQueryHandler : IRequestHandler<GetConversationsQuery, GetConversationsQueryResult>
{
private readonly FacebookServiceContext _context;
public GetConversationsQueryHandler(FacebookServiceContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<GetConversationsQueryResult> 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<string>(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<DateTime?>(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);
}
}
/// <summary>
/// EN: Handler for GetConversationByIdQuery.
/// VI: Handler cho GetConversationByIdQuery.

View File

@@ -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;
/// <summary>
/// EN: Handler for GetCustomersQuery - returns paginated customers list.
/// VI: Handler cho GetCustomersQuery - trả về danh sách customers phân trang.
/// </summary>
public class GetCustomersQueryHandler : IRequestHandler<GetCustomersQuery, GetCustomersQueryResult>
{
private readonly FacebookServiceContext _context;
public GetCustomersQueryHandler(FacebookServiceContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<GetCustomersQueryResult> 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<string?>(c, "_name") != null &&
EF.Property<string>(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<DateTime>(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);
}
}
/// <summary>
/// EN: Handler for GetCustomerByIdQuery.
/// VI: Handler cho GetCustomerByIdQuery.

View File

@@ -9,6 +9,163 @@ using PromotionService.Domain.Exceptions;
namespace PromotionService.API.Application.Commands;
/// <summary>
/// EN: Handler for ExchangeVoucherCommand (exchange points for voucher).
/// VI: Handler cho ExchangeVoucherCommand (đổi điểm lấy voucher).
/// </summary>
public class ExchangeVoucherCommandHandler : IRequestHandler<ExchangeVoucherCommand, VoucherDto>
{
private readonly ICampaignRepository _campaignRepository;
private readonly IWalletServiceClient _walletService;
private readonly ILogger<ExchangeVoucherCommandHandler> _logger;
public ExchangeVoucherCommandHandler(
ICampaignRepository campaignRepository,
IWalletServiceClient walletService,
ILogger<ExchangeVoucherCommandHandler> logger)
{
_campaignRepository = campaignRepository;
_walletService = walletService;
_logger = logger;
}
public async Task<VoucherDto> 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);
}
/// <summary>
/// EN: Handler for PurchaseVoucherCommand (buy voucher with currency).
/// VI: Handler cho PurchaseVoucherCommand (mua voucher bằng tiền).
/// </summary>
public class PurchaseVoucherCommandHandler : IRequestHandler<PurchaseVoucherCommand, VoucherDto>
{
private readonly ICampaignRepository _campaignRepository;
private readonly IWalletServiceClient _walletService;
private readonly ILogger<PurchaseVoucherCommandHandler> _logger;
public PurchaseVoucherCommandHandler(
ICampaignRepository campaignRepository,
IWalletServiceClient walletService,
ILogger<PurchaseVoucherCommandHandler> logger)
{
_campaignRepository = campaignRepository;
_walletService = walletService;
_logger = logger;
}
public async Task<VoucherDto> 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);
}
/// <summary>
/// EN: Handler for ClaimVoucherCommand (free vouchers).
/// VI: Handler cho ClaimVoucherCommand (voucher miễn phí).

View File

@@ -258,6 +258,126 @@ public class GetAllRedemptionsQueryHandler : IRequestHandler<GetAllRedemptionsQu
}
}
/// <summary>
/// EN: Handler for SearchVouchersQuery - search vouchers by code pattern.
/// VI: Handler cho SearchVouchersQuery - tìm kiếm vouchers theo mẫu mã.
/// </summary>
public class SearchVouchersQueryHandler : IRequestHandler<SearchVouchersQuery, IEnumerable<AdminVoucherListDto>>
{
private readonly PromotionServiceContext _context;
public SearchVouchersQueryHandler(PromotionServiceContext context)
{
_context = context;
}
public async Task<IEnumerable<AdminVoucherListDto>> 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<AdminVoucherListDto>();
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class GetCampaignVouchersQueryHandler : IRequestHandler<GetCampaignVouchersQuery, PaginatedResponse<AdminVoucherListDto>>
{
private readonly PromotionServiceContext _context;
public GetCampaignVouchersQueryHandler(PromotionServiceContext context)
{
_context = context;
}
public async Task<PaginatedResponse<AdminVoucherListDto>> 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<AdminVoucherListDto>(items, totalCount, request.PageNumber, request.PageSize, totalPages);
}
}
/// <summary>
/// EN: Handler for GetRedemptionStatisticsQuery.
/// VI: Handler cho GetRedemptionStatisticsQuery.

View File

@@ -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<GetUserVouchersQuery,
v.ExpiresAt));
}
}
/// <summary>
/// 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).
/// </summary>
public class GetCampaignStatisticsQueryHandler : IRequestHandler<GetCampaignStatisticsQuery, CampaignStatisticsDto?>
{
private readonly ICampaignRepository _campaignRepository;
private readonly IRedemptionRepository _redemptionRepository;
public GetCampaignStatisticsQueryHandler(
ICampaignRepository campaignRepository,
IRedemptionRepository redemptionRepository)
{
_campaignRepository = campaignRepository;
_redemptionRepository = redemptionRepository;
}
public async Task<CampaignStatisticsDto?> 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);
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for ActivateCampaignCommand.
/// VI: Validator cho ActivateCampaignCommand.
/// </summary>
public class ActivateCampaignCommandValidator : AbstractValidator<ActivateCampaignCommand>
{
public ActivateCampaignCommandValidator()
{
RuleFor(x => x.CampaignId)
.NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for CancelCampaignCommand.
/// VI: Validator cho CancelCampaignCommand.
/// </summary>
public class CancelCampaignCommandValidator : AbstractValidator<CancelCampaignCommand>
{
public CancelCampaignCommandValidator()
{
RuleFor(x => x.CampaignId)
.NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
}
}

View File

@@ -0,0 +1,20 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for ClaimVoucherCommand.
/// VI: Validator cho ClaimVoucherCommand.
/// </summary>
public class ClaimVoucherCommandValidator : AbstractValidator<ClaimVoucherCommand>
{
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");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for CompleteCampaignCommand.
/// VI: Validator cho CompleteCampaignCommand.
/// </summary>
public class CompleteCampaignCommandValidator : AbstractValidator<CompleteCampaignCommand>
{
public CompleteCampaignCommandValidator()
{
RuleFor(x => x.CampaignId)
.NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
}
}

View File

@@ -0,0 +1,63 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for CreateCampaignCommand.
/// VI: Validator cho CreateCampaignCommand.
/// </summary>
public class CreateCampaignCommandValidator : AbstractValidator<CreateCampaignCommand>
{
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");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for DeleteCampaignCommand.
/// VI: Validator cho DeleteCampaignCommand.
/// </summary>
public class DeleteCampaignCommandValidator : AbstractValidator<DeleteCampaignCommand>
{
public DeleteCampaignCommandValidator()
{
RuleFor(x => x.CampaignId)
.NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
}
}

View File

@@ -0,0 +1,23 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for ExchangeVoucherCommand.
/// VI: Validator cho ExchangeVoucherCommand.
/// </summary>
public class ExchangeVoucherCommandValidator : AbstractValidator<ExchangeVoucherCommand>
{
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");
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for ExtendVoucherExpiryCommand.
/// VI: Validator cho ExtendVoucherExpiryCommand.
/// </summary>
public class ExtendVoucherExpiryCommandValidator : AbstractValidator<ExtendVoucherExpiryCommand>
{
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");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for PauseCampaignCommand.
/// VI: Validator cho PauseCampaignCommand.
/// </summary>
public class PauseCampaignCommandValidator : AbstractValidator<PauseCampaignCommand>
{
public PauseCampaignCommandValidator()
{
RuleFor(x => x.CampaignId)
.NotEmpty().WithMessage("Campaign ID is required / ID chiến dịch là bắt buộc");
}
}

View File

@@ -0,0 +1,23 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for PurchaseVoucherCommand.
/// VI: Validator cho PurchaseVoucherCommand.
/// </summary>
public class PurchaseVoucherCommandValidator : AbstractValidator<PurchaseVoucherCommand>
{
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");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for RedeemVoucherCommand.
/// VI: Validator cho RedeemVoucherCommand.
/// </summary>
public class RedeemVoucherCommandValidator : AbstractValidator<RedeemVoucherCommand>
{
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");
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for RevokeVoucherCommand.
/// VI: Validator cho RevokeVoucherCommand.
/// </summary>
public class RevokeVoucherCommandValidator : AbstractValidator<RevokeVoucherCommand>
{
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ự");
}
}

View File

@@ -0,0 +1,34 @@
using FluentValidation;
using PromotionService.API.Application.Commands;
namespace PromotionService.API.Application.Validations;
/// <summary>
/// EN: Validator for UpdateCampaignCommand.
/// VI: Validator cho UpdateCampaignCommand.
/// </summary>
public class UpdateCampaignCommandValidator : AbstractValidator<UpdateCampaignCommand>
{
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);
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using SocialService.API.Application.Commands;
namespace SocialService.API.Application.Validations;
/// <summary>
/// EN: Validator for AdminDeleteBlockCommand.
/// VI: Validator cho AdminDeleteBlockCommand.
/// </summary>
public class AdminDeleteBlockCommandValidator : AbstractValidator<AdminDeleteBlockCommand>
{
public AdminDeleteBlockCommandValidator()
{
RuleFor(x => x.BlockId)
.NotEmpty().WithMessage("Block ID is required / ID block là bắt buộc");
}
}

View File

@@ -0,0 +1,17 @@
using FluentValidation;
using SocialService.API.Application.Commands;
namespace SocialService.API.Application.Validations;
/// <summary>
/// EN: Validator for AdminDeleteRelationshipCommand.
/// VI: Validator cho AdminDeleteRelationshipCommand.
/// </summary>
public class AdminDeleteRelationshipCommandValidator : AbstractValidator<AdminDeleteRelationshipCommand>
{
public AdminDeleteRelationshipCommandValidator()
{
RuleFor(x => x.RelationshipId)
.NotEmpty().WithMessage("Relationship ID is required / ID quan hệ là bắt buộc");
}
}

View File

@@ -0,0 +1,28 @@
using FluentValidation;
using SocialService.API.Application.Commands;
namespace SocialService.API.Application.Validations;
/// <summary>
/// EN: Validator for BlockUserCommand.
/// VI: Validator cho BlockUserCommand.
/// </summary>
public class BlockUserCommandValidator : AbstractValidator<BlockUserCommand>
{
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);
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using SocialService.API.Application.Commands;
namespace SocialService.API.Application.Validations;
/// <summary>
/// EN: Validator for FollowUserCommand.
/// VI: Validator cho FollowUserCommand.
/// </summary>
public class FollowUserCommandValidator : AbstractValidator<FollowUserCommand>
{
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");
}
}

View File

@@ -0,0 +1,20 @@
using FluentValidation;
using SocialService.API.Application.Commands;
namespace SocialService.API.Application.Validations;
/// <summary>
/// EN: Validator for RespondToFriendRequestCommand.
/// VI: Validator cho RespondToFriendRequestCommand.
/// </summary>
public class RespondToFriendRequestCommandValidator : AbstractValidator<RespondToFriendRequestCommand>
{
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");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using SocialService.API.Application.Commands;
namespace SocialService.API.Application.Validations;
/// <summary>
/// EN: Validator for SendFriendRequestCommand.
/// VI: Validator cho SendFriendRequestCommand.
/// </summary>
public class SendFriendRequestCommandValidator : AbstractValidator<SendFriendRequestCommand>
{
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");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using SocialService.API.Application.Commands;
namespace SocialService.API.Application.Validations;
/// <summary>
/// EN: Validator for UnblockUserCommand.
/// VI: Validator cho UnblockUserCommand.
/// </summary>
public class UnblockUserCommandValidator : AbstractValidator<UnblockUserCommand>
{
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");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using SocialService.API.Application.Commands;
namespace SocialService.API.Application.Validations;
/// <summary>
/// EN: Validator for UnfollowUserCommand.
/// VI: Validator cho UnfollowUserCommand.
/// </summary>
public class UnfollowUserCommandValidator : AbstractValidator<UnfollowUserCommand>
{
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");
}
}