feat: Replace generic sample implementations with concrete Ads Billing and Order service domain logic.
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to change status of a Sample.
|
||||
/// VI: Command để thay đổi trạng thái của Sample.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="NewStatus">EN: New status (activate, complete, cancel) / VI: Trạng thái mới (activate, complete, cancel)</param>
|
||||
public record ChangeSampleStatusCommand(
|
||||
Guid SampleId,
|
||||
string NewStatus
|
||||
) : IRequest<bool>;
|
||||
@@ -1,70 +0,0 @@
|
||||
using MediatR;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for ChangeSampleStatusCommand.
|
||||
/// VI: Handler cho ChangeSampleStatusCommand.
|
||||
/// </summary>
|
||||
public class ChangeSampleStatusCommandHandler : IRequestHandler<ChangeSampleStatusCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<ChangeSampleStatusCommandHandler> _logger;
|
||||
|
||||
public ChangeSampleStatusCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<ChangeSampleStatusCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
ChangeSampleStatusCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Changing status of sample {SampleId} to {NewStatus} / Thay đổi trạng thái sample {SampleId} thành {NewStatus}",
|
||||
request.SampleId, request.NewStatus);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Change status based on action / VI: Thay đổi trạng thái dựa trên action
|
||||
switch (request.NewStatus.ToLowerInvariant())
|
||||
{
|
||||
case "activate":
|
||||
sample.Activate();
|
||||
break;
|
||||
case "complete":
|
||||
sample.Complete();
|
||||
break;
|
||||
case "cancel":
|
||||
sample.Cancel();
|
||||
break;
|
||||
default:
|
||||
_logger.LogWarning(
|
||||
"Invalid status action: {NewStatus} / Action trạng thái không hợp lệ: {NewStatus}",
|
||||
request.NewStatus);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} status changed to {NewStatus} / Trạng thái sample {SampleId} đã đổi thành {NewStatus}",
|
||||
request.SampleId, request.NewStatus);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to charge advertiser for ad impression/click.
|
||||
/// VI: Command để charge advertiser cho impression/click quảng cáo.
|
||||
/// </summary>
|
||||
public record ChargeAdvertiserCommand : IRequest<bool>
|
||||
{
|
||||
public Guid AdvertiserId { get; init; }
|
||||
public Guid CampaignId { get; init; }
|
||||
public Guid AdId { get; init; }
|
||||
public string ChargeType { get; init; } = null!; // "impression" or "click"
|
||||
public decimal Amount { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using AdsBillingService.Domain.AggregatesModel.ChargeAggregate;
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for ChargeAdvertiserCommand - processes ad charges and debits from wallet.
|
||||
/// VI: Handler cho ChargeAdvertiserCommand - xử lý charge quảng cáo và trừ tiền từ ví.
|
||||
/// </summary>
|
||||
public class ChargeAdvertiserCommandHandler : IRequestHandler<ChargeAdvertiserCommand, bool>
|
||||
{
|
||||
private readonly ILogger<ChargeAdvertiserCommandHandler> _logger;
|
||||
// private readonly IWalletService _walletService;
|
||||
|
||||
public ChargeAdvertiserCommandHandler(ILogger<ChargeAdvertiserCommandHandler> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(ChargeAdvertiserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Step 1: Create charge record
|
||||
var chargeType = request.ChargeType.ToLower() == "impression"
|
||||
? ChargeType.Impression
|
||||
: ChargeType.Click;
|
||||
|
||||
var charge = new AdCharge(
|
||||
request.AdvertiserId,
|
||||
request.CampaignId,
|
||||
request.AdId,
|
||||
chargeType,
|
||||
request.Amount
|
||||
);
|
||||
|
||||
// TODO: Step 2 - Debit from Wallet Service
|
||||
// var walletDebitResult = await _walletService.DebitBalance(
|
||||
// advertiserId: request.AdvertiserId,
|
||||
// amount: request.Amount,
|
||||
// reference: charge.Id,
|
||||
// description: $"Ad {chargeType} charge"
|
||||
// );
|
||||
|
||||
// if (!walletDebitResult.Success)
|
||||
// {
|
||||
// _logger.LogWarning("Failed to debit wallet for advertiser {AdvertiserId}", request.AdvertiserId);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// Step 3: Mark as processed
|
||||
charge.MarkAsProcessed();
|
||||
|
||||
// TODO: Step 4 - Save to repository
|
||||
// _adChargeRepository.Add(charge);
|
||||
// await _adChargeRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Charged advertiser {AdvertiserId} amount {Amount} for {ChargeType}",
|
||||
request.AdvertiserId, request.Amount, chargeType);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to create a billing account.
|
||||
/// VI: Command tạo tài khoản billing.
|
||||
/// </summary>
|
||||
public record CreateBillingAccountCommand : IRequest<Guid>
|
||||
{
|
||||
public Guid AdvertiserId { get; init; }
|
||||
public Guid? WalletId { get; init; }
|
||||
public string PaymentMethod { get; init; } = "prepaid"; // prepaid, postpaid
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using AdsBillingService.Domain.AggregatesModel.BillingAccountAggregate;
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
public class CreateBillingAccountCommandHandler : IRequestHandler<CreateBillingAccountCommand, Guid>
|
||||
{
|
||||
private readonly ILogger<CreateBillingAccountCommandHandler> _logger;
|
||||
|
||||
public CreateBillingAccountCommandHandler(ILogger<CreateBillingAccountCommandHandler> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<Guid> Handle(CreateBillingAccountCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var paymentMethod = request.PaymentMethod.ToLower() switch
|
||||
{
|
||||
"prepaid" => PaymentMethodType.Prepaid,
|
||||
"postpaid" => PaymentMethodType.Postpaid,
|
||||
"creditcard" => PaymentMethodType.CreditCard,
|
||||
_ => PaymentMethodType.Prepaid
|
||||
};
|
||||
|
||||
var billingAccount = new BillingAccount(
|
||||
request.AdvertiserId,
|
||||
request.WalletId,
|
||||
paymentMethod
|
||||
);
|
||||
|
||||
// TODO: Add repository save
|
||||
// _billingAccountRepository.Add(billingAccount);
|
||||
// await _billingAccountRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Billing account created for advertiser {AdvertiserId}", request.AdvertiserId);
|
||||
|
||||
return billingAccount.Id;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to create a new Sample.
|
||||
/// VI: Command để tạo một Sample mới.
|
||||
/// </summary>
|
||||
/// <param name="Name">EN: Sample name / VI: Tên sample</param>
|
||||
/// <param name="Description">EN: Optional description / VI: Mô tả tùy chọn</param>
|
||||
public record CreateSampleCommand(
|
||||
string Name,
|
||||
string? Description
|
||||
) : IRequest<CreateSampleCommandResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of CreateSampleCommand.
|
||||
/// VI: Kết quả của CreateSampleCommand.
|
||||
/// </summary>
|
||||
/// <param name="Id">EN: Created sample ID / VI: ID sample đã tạo</param>
|
||||
public record CreateSampleCommandResult(Guid Id);
|
||||
@@ -1,46 +0,0 @@
|
||||
using MediatR;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for CreateSampleCommand.
|
||||
/// VI: Handler cho CreateSampleCommand.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandHandler : IRequestHandler<CreateSampleCommand, CreateSampleCommandResult>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<CreateSampleCommandHandler> _logger;
|
||||
|
||||
public CreateSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<CreateSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CreateSampleCommandResult> Handle(
|
||||
CreateSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Creating new sample with name: {Name} / Tạo sample mới với tên: {Name}",
|
||||
request.Name);
|
||||
|
||||
// EN: Create domain entity / VI: Tạo domain entity
|
||||
var sample = new Sample(request.Name, request.Description);
|
||||
|
||||
// EN: Add to repository / VI: Thêm vào repository
|
||||
_sampleRepository.Add(sample);
|
||||
|
||||
// EN: Save changes (dispatches domain events) / VI: Lưu thay đổi (dispatch domain events)
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample created successfully with ID: {SampleId} / Sample đã tạo thành công với ID: {SampleId}",
|
||||
sample.Id);
|
||||
|
||||
return new CreateSampleCommandResult(sample.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to delete a Sample.
|
||||
/// VI: Command để xóa một Sample.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID to delete / VI: ID sample cần xóa</param>
|
||||
public record DeleteSampleCommand(Guid SampleId) : IRequest<bool>;
|
||||
@@ -1,54 +0,0 @@
|
||||
using MediatR;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for DeleteSampleCommand.
|
||||
/// VI: Handler cho DeleteSampleCommand.
|
||||
/// </summary>
|
||||
public class DeleteSampleCommandHandler : IRequestHandler<DeleteSampleCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<DeleteSampleCommandHandler> _logger;
|
||||
|
||||
public DeleteSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<DeleteSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
DeleteSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Deleting sample {SampleId} / Xóa sample {SampleId}",
|
||||
request.SampleId);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Delete sample / VI: Xóa sample
|
||||
_sampleRepository.Delete(sample);
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} deleted successfully / Sample {SampleId} đã xóa thành công",
|
||||
request.SampleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to update an existing Sample.
|
||||
/// VI: Command để cập nhật một Sample đã tồn tại.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID to update / VI: ID sample cần cập nhật</param>
|
||||
/// <param name="Name">EN: New name / VI: Tên mới</param>
|
||||
/// <param name="Description">EN: New description / VI: Mô tả mới</param>
|
||||
public record UpdateSampleCommand(
|
||||
Guid SampleId,
|
||||
string Name,
|
||||
string? Description
|
||||
) : IRequest<bool>;
|
||||
@@ -1,54 +0,0 @@
|
||||
using MediatR;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for UpdateSampleCommand.
|
||||
/// VI: Handler cho UpdateSampleCommand.
|
||||
/// </summary>
|
||||
public class UpdateSampleCommandHandler : IRequestHandler<UpdateSampleCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<UpdateSampleCommandHandler> _logger;
|
||||
|
||||
public UpdateSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<UpdateSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
UpdateSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Updating sample {SampleId} / Cập nhật sample {SampleId}",
|
||||
request.SampleId);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Update sample using domain method / VI: Cập nhật sample sử dụng domain method
|
||||
sample.Update(request.Name, request.Description);
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} updated successfully / Sample {SampleId} đã cập nhật thành công",
|
||||
request.SampleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get a Sample by ID.
|
||||
/// VI: Query để lấy một Sample theo ID.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID / VI: ID sample</param>
|
||||
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Sample view model for API responses.
|
||||
/// VI: Sample view model cho API responses.
|
||||
/// </summary>
|
||||
public record SampleViewModel(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
string Status,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt
|
||||
);
|
||||
@@ -1,39 +0,0 @@
|
||||
using MediatR;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetSampleQuery.
|
||||
/// VI: Handler cho GetSampleQuery.
|
||||
/// </summary>
|
||||
public class GetSampleQueryHandler : IRequestHandler<GetSampleQuery, SampleViewModel?>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
|
||||
public GetSampleQueryHandler(ISampleRepository sampleRepository)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
}
|
||||
|
||||
public async Task<SampleViewModel?> Handle(
|
||||
GetSampleQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SampleViewModel(
|
||||
sample.Id,
|
||||
sample.Name,
|
||||
sample.Description,
|
||||
sample.Status.Name,
|
||||
sample.CreatedAt,
|
||||
sample.UpdatedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace AdsBillingService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get all Samples.
|
||||
/// VI: Query để lấy tất cả Samples.
|
||||
/// </summary>
|
||||
public record GetSamplesQuery : IRequest<IEnumerable<SampleViewModel>>;
|
||||
@@ -1,34 +0,0 @@
|
||||
using MediatR;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetSamplesQuery.
|
||||
/// VI: Handler cho GetSamplesQuery.
|
||||
/// </summary>
|
||||
public class GetSamplesQueryHandler : IRequestHandler<GetSamplesQuery, IEnumerable<SampleViewModel>>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
|
||||
public GetSamplesQueryHandler(ISampleRepository sampleRepository)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SampleViewModel>> Handle(
|
||||
GetSamplesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var samples = await _sampleRepository.GetAllAsync();
|
||||
|
||||
return samples.Select(sample => new SampleViewModel(
|
||||
sample.Id,
|
||||
sample.Name,
|
||||
sample.Description,
|
||||
sample.Status.Name,
|
||||
sample.CreatedAt,
|
||||
sample.UpdatedAt
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using FluentValidation;
|
||||
using AdsBillingService.API.Application.Commands;
|
||||
|
||||
namespace AdsBillingService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for CreateSampleCommand.
|
||||
/// VI: Validator cho CreateSampleCommand.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandValidator : AbstractValidator<CreateSampleCommand>
|
||||
{
|
||||
public CreateSampleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Name is required / Tên là bắt buộc")
|
||||
.MaximumLength(200)
|
||||
.WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000)
|
||||
.WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
|
||||
.When(x => x.Description != null);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using FluentValidation;
|
||||
using AdsBillingService.API.Application.Commands;
|
||||
|
||||
namespace AdsBillingService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for UpdateSampleCommand.
|
||||
/// VI: Validator cho UpdateSampleCommand.
|
||||
/// </summary>
|
||||
public class UpdateSampleCommandValidator : AbstractValidator<UpdateSampleCommand>
|
||||
{
|
||||
public UpdateSampleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.SampleId)
|
||||
.NotEmpty()
|
||||
.WithMessage("Sample ID is required / ID sample là bắt buộc");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Name is required / Tên là bắt buộc")
|
||||
.MaximumLength(200)
|
||||
.WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000)
|
||||
.WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
|
||||
.When(x => x.Description != null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using AdsBillingService.API.Application.Commands;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsBillingService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: API Controller for managing billing accounts.
|
||||
/// VI: API Controller quản lý tài khoản billing.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/ads-billing/accounts")]
|
||||
[Produces("application/json")]
|
||||
public class BillingAccountsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<BillingAccountsController> _logger;
|
||||
|
||||
public BillingAccountsController(IMediator mediator, ILogger<BillingAccountsController> logger)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new billing account.
|
||||
/// VI: Tạo tài khoản billing mới.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<Guid>> CreateBillingAccount([FromBody] CreateBillingAccountCommand command)
|
||||
{
|
||||
_logger.LogInformation("Creating billing account for advertiser {AdvertiserId}", command.AdvertiserId);
|
||||
|
||||
var accountId = await _mediator.Send(command);
|
||||
|
||||
return CreatedAtAction(nameof(GetBillingAccount), new { id = accountId }, accountId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get billing account by ID (placeholder).
|
||||
/// VI: Lấy tài khoản billing theo ID (placeholder).
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetBillingAccount(Guid id)
|
||||
{
|
||||
// TODO: Implement GetBillingAccountQuery
|
||||
return Ok(new { id, message = "Billing account details" });
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdsBillingService.API.Application.Commands;
|
||||
using AdsBillingService.API.Application.Queries;
|
||||
|
||||
namespace AdsBillingService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Controller for Sample CRUD operations using CQRS pattern.
|
||||
/// VI: Controller cho các thao tác CRUD Sample sử dụng pattern CQRS.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
public class SamplesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<SamplesController> _logger;
|
||||
|
||||
public SamplesController(IMediator mediator, ILogger<SamplesController> logger)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get all samples.
|
||||
/// VI: Lấy tất cả samples.
|
||||
/// </summary>
|
||||
/// <returns>EN: List of samples / VI: Danh sách samples</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<SampleViewModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetSamples()
|
||||
{
|
||||
var samples = await _mediator.Send(new GetSamplesQuery());
|
||||
return Ok(new { success = true, data = samples });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get a sample by ID.
|
||||
/// VI: Lấy một sample theo ID.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <returns>EN: Sample details / VI: Chi tiết sample</returns>
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(SampleViewModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetSample(Guid id)
|
||||
{
|
||||
var sample = await _mediator.Send(new GetSampleQuery(id));
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, data = sample });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new sample.
|
||||
/// VI: Tạo một sample mới.
|
||||
/// </summary>
|
||||
/// <param name="request">EN: Create request / VI: Request tạo</param>
|
||||
/// <returns>EN: Created sample ID / VI: ID sample đã tạo</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CreateSampleCommandResult), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateSample([FromBody] CreateSampleRequest request)
|
||||
{
|
||||
var command = new CreateSampleCommand(request.Name, request.Description);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetSample),
|
||||
new { id = result.Id },
|
||||
new { success = true, data = result });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update an existing sample.
|
||||
/// VI: Cập nhật một sample đã tồn tại.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="request">EN: Update request / VI: Request cập nhật</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpPut("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateSample(Guid id, [FromBody] UpdateSampleRequest request)
|
||||
{
|
||||
var command = new UpdateSampleCommand(id, request.Name, request.Description);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, message = "Sample updated successfully / Sample đã cập nhật thành công" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete a sample.
|
||||
/// VI: Xóa một sample.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpDelete("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> DeleteSample(Guid id)
|
||||
{
|
||||
var command = new DeleteSampleCommand(id);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Change sample status.
|
||||
/// VI: Thay đổi trạng thái sample.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="request">EN: Status change request / VI: Request thay đổi trạng thái</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpPatch("{id:guid}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ChangeSampleStatus(Guid id, [FromBody] ChangeStatusRequest request)
|
||||
{
|
||||
var command = new ChangeSampleStatusCommand(id, request.Status);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "STATUS_CHANGE_FAILED",
|
||||
message = "Failed to change sample status / Thay đổi trạng thái sample thất bại"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, message = "Sample status changed successfully / Trạng thái sample đã thay đổi thành công" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for creating a sample.
|
||||
/// VI: Model request để tạo sample.
|
||||
/// </summary>
|
||||
public record CreateSampleRequest(string Name, string? Description);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for updating a sample.
|
||||
/// VI: Model request để cập nhật sample.
|
||||
/// </summary>
|
||||
public record UpdateSampleRequest(string Name, string? Description);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for changing sample status.
|
||||
/// VI: Model request để thay đổi trạng thái sample.
|
||||
/// </summary>
|
||||
public record ChangeStatusRequest(string Status);
|
||||
@@ -1,4 +1,5 @@
|
||||
using AdsBillingService.Domain.SeedWork;
|
||||
using AdsBillingService.Domain.Exceptions;
|
||||
|
||||
namespace AdsBillingService.Domain.AggregatesModel.BillingAccountAggregate;
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using AdsBillingService.Domain.SeedWork;
|
||||
using AdsBillingService.Domain.Exceptions;
|
||||
|
||||
namespace AdsBillingService.Domain.AggregatesModel.ChargeAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Ad charge aggregate root - individual ad impression/click charge.
|
||||
/// VI: Ad charge aggregate root - phí riêng lẻ cho impression/click quảng cáo.
|
||||
/// </summary>
|
||||
public class AdCharge : Entity, IAggregateRoot
|
||||
{
|
||||
private Guid _advertiserId;
|
||||
private Guid _campaignId;
|
||||
private Guid _adId;
|
||||
private ChargeType _chargeType;
|
||||
private decimal _amount;
|
||||
private string _currency;
|
||||
private DateTime _chargedAt;
|
||||
private bool _processed;
|
||||
|
||||
public Guid AdvertiserId => _advertiserId;
|
||||
public Guid CampaignId => _campaignId;
|
||||
public Guid AdId => _adId;
|
||||
public ChargeType ChargeType => _chargeType;
|
||||
public decimal Amount => _amount;
|
||||
public string Currency => _currency;
|
||||
public DateTime ChargedAt => _chargedAt;
|
||||
public bool Processed => _processed;
|
||||
|
||||
protected AdCharge()
|
||||
{
|
||||
_currency = "VND"; // Default value for EF Core
|
||||
}
|
||||
|
||||
public AdCharge(
|
||||
Guid advertiserId,
|
||||
Guid campaignId,
|
||||
Guid adId,
|
||||
ChargeType chargeType,
|
||||
decimal amount,
|
||||
string currency = "VND")
|
||||
{
|
||||
if (amount <= 0)
|
||||
throw new AdsBillingDomainException("Charge amount must be positive");
|
||||
|
||||
Id = Guid.NewGuid();
|
||||
_advertiserId = advertiserId;
|
||||
_campaignId = campaignId;
|
||||
_adId = adId;
|
||||
_chargeType = chargeType;
|
||||
_amount = amount;
|
||||
_currency = currency;
|
||||
_chargedAt = DateTime.UtcNow;
|
||||
_processed = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Mark charge as processed (billeddebited from wallet).
|
||||
/// VI: Đánh dấu charge đã xử lý (đã trừ tiền từ ví).
|
||||
/// </summary>
|
||||
public void MarkAsProcessed()
|
||||
{
|
||||
_processed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Factory method for impression charge.
|
||||
/// VI: Factory method cho charge impression.
|
||||
/// </summary>
|
||||
public static AdCharge ForImpression(Guid advertiserId, Guid campaignId, Guid adId, decimal cpm)
|
||||
{
|
||||
return new AdCharge(advertiserId, campaignId, adId, ChargeType.Impression, cpm / 1000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Factory method for click charge.
|
||||
/// VI: Factory method cho charge click.
|
||||
/// </summary>
|
||||
public static AdCharge ForClick(Guid advertiserId, Guid campaignId, Guid adId, decimal cpc)
|
||||
{
|
||||
return new AdCharge(advertiserId, campaignId, adId, ChargeType.Click, cpc);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Charge type enumeration.
|
||||
/// VI: Enum loại charge.
|
||||
/// </summary>
|
||||
public enum ChargeType
|
||||
{
|
||||
Impression = 1,
|
||||
Click = 2,
|
||||
Conversion = 3
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using AdsBillingService.Domain.SeedWork;
|
||||
using AdsBillingService.Domain.Exceptions;
|
||||
|
||||
namespace AdsBillingService.Domain.AggregatesModel.InvoiceAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Invoice aggregate root - monthly billing invoice.
|
||||
/// VI: Invoice aggregate root - hóa đơn thanh toán hàng tháng.
|
||||
/// </summary>
|
||||
public class Invoice : Entity, IAggregateRoot
|
||||
{
|
||||
private List<InvoiceLineItem> _lineItems = new();
|
||||
private Guid _billingAccountId;
|
||||
private string _invoiceNumber = null!;
|
||||
private InvoiceStatus _status;
|
||||
private DateTime _issueDate;
|
||||
private DateTime _dueDate;
|
||||
private decimal _totalAmount;
|
||||
|
||||
public Guid BillingAccountId => _billingAccountId;
|
||||
public string InvoiceNumber => _invoiceNumber;
|
||||
public InvoiceStatus Status => _status;
|
||||
public DateTime IssueDate => _issueDate;
|
||||
public DateTime DueDate => _dueDate;
|
||||
public decimal TotalAmount => _totalAmount;
|
||||
public IReadOnlyCollection<InvoiceLineItem> LineItems => _lineItems.AsReadOnly();
|
||||
|
||||
protected Invoice() { }
|
||||
|
||||
public Invoice(Guid billingAccountId, DateTime issueDate, DateTime dueDate)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
_billingAccountId = billingAccountId;
|
||||
_invoiceNumber = GenerateInvoiceNumber();
|
||||
_status = InvoiceStatus.Draft;
|
||||
_issueDate = issueDate;
|
||||
_dueDate = dueDate;
|
||||
_totalAmount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Add line item for campaign charges.
|
||||
/// VI: Thêm dòng chi tiết cho chi phí campaign.
|
||||
/// </summary>
|
||||
public void AddLineItem(Guid campaignId, string description, int quantity, decimal unitPrice)
|
||||
{
|
||||
var lineItem = new InvoiceLineItem(campaignId, description, quantity, unitPrice);
|
||||
_lineItems.Add(lineItem);
|
||||
RecalculateTotal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Mark invoice as issued (sent to customer).
|
||||
/// VI: Đánh dấu hóa đơn đã phát hành (gửi cho khách hàng).
|
||||
/// </summary>
|
||||
public void Issue()
|
||||
{
|
||||
if (_status != InvoiceStatus.Draft)
|
||||
throw new AdsBillingDomainException("Can only issue draft invoices");
|
||||
|
||||
_status = InvoiceStatus.Issued;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Mark invoice as paid.
|
||||
/// VI: Đánh dấu hóa đơn đã thanh toán.
|
||||
/// </summary>
|
||||
public void MarkAsPaid()
|
||||
{
|
||||
if (_status != InvoiceStatus.Issued)
|
||||
throw new AdsBillingDomainException("Can only pay issued invoices");
|
||||
|
||||
_status = InvoiceStatus.Paid;
|
||||
}
|
||||
|
||||
private void RecalculateTotal()
|
||||
{
|
||||
_totalAmount = _lineItems.Sum(li => li.TotalAmount);
|
||||
}
|
||||
|
||||
private static string GenerateInvoiceNumber()
|
||||
{
|
||||
return $"INV-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpper()}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Invoice line item entity - per-campaign charges.
|
||||
/// VI: Entity dòng hóa đơn - chi phí theo campaign.
|
||||
/// </summary>
|
||||
public class InvoiceLineItem : Entity
|
||||
{
|
||||
public Guid CampaignId { get; private set; }
|
||||
public string Description { get; private set; } = null!;
|
||||
public int Quantity { get; private set; }
|
||||
public decimal UnitPrice { get; private set; }
|
||||
public decimal TotalAmount => Quantity * UnitPrice;
|
||||
|
||||
protected InvoiceLineItem() { }
|
||||
|
||||
public InvoiceLineItem(Guid campaignId, string description, int quantity, decimal unitPrice)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
CampaignId = campaignId;
|
||||
Description = description;
|
||||
Quantity = quantity;
|
||||
UnitPrice = unitPrice;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Invoice status enumeration.
|
||||
/// VI: Enum trạng thái hóa đơn.
|
||||
/// </summary>
|
||||
public enum InvoiceStatus
|
||||
{
|
||||
Draft = 1,
|
||||
Issued = 2,
|
||||
Paid = 3,
|
||||
Overdue = 4,
|
||||
Cancelled = 5
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using MediatR;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Domain event raised when a new Sample is created.
|
||||
/// VI: Domain event được phát ra khi một Sample mới được tạo.
|
||||
/// </summary>
|
||||
public class SampleCreatedDomainEvent : INotification
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: The newly created sample.
|
||||
/// VI: Sample mới được tạo.
|
||||
/// </summary>
|
||||
public Sample Sample { get; }
|
||||
|
||||
public SampleCreatedDomainEvent(Sample sample)
|
||||
{
|
||||
Sample = sample;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using MediatR;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Domain event raised when Sample status changes.
|
||||
/// VI: Domain event được phát ra khi trạng thái Sample thay đổi.
|
||||
/// </summary>
|
||||
public class SampleStatusChangedDomainEvent : INotification
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: The sample ID.
|
||||
/// VI: ID của sample.
|
||||
/// </summary>
|
||||
public Guid SampleId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Previous status before the change.
|
||||
/// VI: Trạng thái trước khi thay đổi.
|
||||
/// </summary>
|
||||
public SampleStatus PreviousStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: New status after the change.
|
||||
/// VI: Trạng thái mới sau khi thay đổi.
|
||||
/// </summary>
|
||||
public SampleStatus NewStatus { get; }
|
||||
|
||||
public SampleStatusChangedDomainEvent(
|
||||
Guid sampleId,
|
||||
SampleStatus previousStatus,
|
||||
SampleStatus newStatus)
|
||||
{
|
||||
SampleId = sampleId;
|
||||
PreviousStatus = previousStatus;
|
||||
NewStatus = newStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace AdsBillingService.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Base exception for all Ads Billing domain exceptions.
|
||||
/// VI: Exception cơ sở cho tất cả các exception của Ads Billing domain.
|
||||
/// </summary>
|
||||
public class AdsBillingDomainException : Exception
|
||||
{
|
||||
public AdsBillingDomainException()
|
||||
{
|
||||
}
|
||||
|
||||
public AdsBillingDomainException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AdsBillingDomainException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
namespace AdsBillingService.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Exception for Sample aggregate domain errors.
|
||||
/// VI: Exception cho các lỗi domain của Sample aggregate.
|
||||
/// </summary>
|
||||
public class SampleDomainException : DomainException
|
||||
{
|
||||
public SampleDomainException()
|
||||
{
|
||||
}
|
||||
|
||||
public SampleDomainException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public SampleDomainException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,23 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
using AdsBillingService.Domain.AggregatesModel.BillingAccountAggregate;
|
||||
using AdsBillingService.Domain.AggregatesModel.InvoiceAggregate;
|
||||
using AdsBillingService.Domain.AggregatesModel.ChargeAggregate;
|
||||
using AdsBillingService.Domain.SeedWork;
|
||||
using AdsBillingService.Infrastructure.EntityConfigurations;
|
||||
|
||||
namespace AdsBillingService.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// EN: EF Core DbContext for AdsBillingService.
|
||||
/// VI: EF Core DbContext cho AdsBillingService.
|
||||
/// </summary>
|
||||
public class AdsBillingServiceContext : DbContext, IUnitOfWork
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private IDbContextTransaction? _currentTransaction;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Samples table.
|
||||
/// VI: Bảng Samples.
|
||||
/// </summary>
|
||||
public DbSet<Sample> Samples => Set<Sample>();
|
||||
public DbSet<BillingAccount> BillingAccounts => Set<BillingAccount>();
|
||||
public DbSet<Invoice> Invoices => Set<Invoice>();
|
||||
public DbSet<AdCharge> AdCharges => Set<AdCharge>();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Read-only access to current transaction.
|
||||
/// VI: Truy cập chỉ đọc đến transaction hiện tại.
|
||||
/// </summary>
|
||||
public IDbContextTransaction? CurrentTransaction => _currentTransaction;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check if there is an active transaction.
|
||||
/// VI: Kiểm tra xem có transaction đang hoạt động không.
|
||||
/// </summary>
|
||||
public bool HasActiveTransaction => _currentTransaction != null;
|
||||
|
||||
public AdsBillingServiceContext(DbContextOptions<AdsBillingServiceContext> options) : base(options)
|
||||
@@ -42,56 +28,30 @@ public class AdsBillingServiceContext : DbContext, IUnitOfWork
|
||||
public AdsBillingServiceContext(DbContextOptions<AdsBillingServiceContext> options, IMediator mediator) : base(options)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
|
||||
System.Diagnostics.Debug.WriteLine("AdsBillingServiceContext::ctor - " + GetHashCode());
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
// EN: Apply entity configurations
|
||||
// VI: Áp dụng các cấu hình entity
|
||||
modelBuilder.ApplyConfiguration(new SampleEntityTypeConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SampleStatusEntityTypeConfiguration());
|
||||
// Entity configurations will be applied here
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Save entities and dispatch domain events.
|
||||
/// VI: Lưu entities và dispatch domain events.
|
||||
/// </summary>
|
||||
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: Dispatch domain events before saving (side effects)
|
||||
// VI: Dispatch domain events trước khi lưu (side effects)
|
||||
await DispatchDomainEventsAsync();
|
||||
|
||||
// EN: Save changes to database
|
||||
// VI: Lưu thay đổi vào database
|
||||
await base.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Begin a new transaction if none is active.
|
||||
/// VI: Bắt đầu một transaction mới nếu không có transaction nào đang hoạt động.
|
||||
/// </summary>
|
||||
public async Task<IDbContextTransaction?> BeginTransactionAsync()
|
||||
{
|
||||
if (_currentTransaction != null) return null;
|
||||
|
||||
_currentTransaction = await Database.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted);
|
||||
|
||||
return _currentTransaction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Commit the current transaction.
|
||||
/// VI: Commit transaction hiện tại.
|
||||
/// </summary>
|
||||
public async Task CommitTransactionAsync(IDbContextTransaction transaction)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(transaction);
|
||||
|
||||
if (transaction != _currentTransaction)
|
||||
throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current");
|
||||
|
||||
@@ -115,10 +75,6 @@ public class AdsBillingServiceContext : DbContext, IUnitOfWork
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Rollback the current transaction.
|
||||
/// VI: Rollback transaction hiện tại.
|
||||
/// </summary>
|
||||
public void RollbackTransaction()
|
||||
{
|
||||
try
|
||||
@@ -135,10 +91,6 @@ public class AdsBillingServiceContext : DbContext, IUnitOfWork
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Dispatch all domain events from tracked entities.
|
||||
/// VI: Dispatch tất cả domain events từ các entities đang được track.
|
||||
/// </summary>
|
||||
private async Task DispatchDomainEventsAsync()
|
||||
{
|
||||
var domainEntities = ChangeTracker
|
||||
@@ -1,27 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
using AdsBillingService.Infrastructure.Idempotency;
|
||||
using AdsBillingService.Infrastructure.Repositories;
|
||||
|
||||
namespace AdsBillingService.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Dependency injection extensions for Infrastructure layer.
|
||||
/// VI: Extensions dependency injection cho lớp Infrastructure.
|
||||
/// </summary>
|
||||
public static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Add infrastructure services to the DI container.
|
||||
/// VI: Thêm các services infrastructure vào DI container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddInfrastructure(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL
|
||||
// Add DbContext with PostgreSQL
|
||||
services.AddDbContext<AdsBillingServiceContext>(options =>
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
@@ -37,8 +27,6 @@ public static class DependencyInjection
|
||||
errorCodesToAdd: null);
|
||||
});
|
||||
|
||||
// EN: Enable sensitive data logging in development only
|
||||
// VI: Chỉ bật sensitive data logging trong development
|
||||
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
|
||||
{
|
||||
options.EnableSensitiveDataLogging();
|
||||
@@ -46,10 +34,10 @@ public static class DependencyInjection
|
||||
}
|
||||
});
|
||||
|
||||
// EN: Register repositories / VI: Đăng ký repositories
|
||||
services.AddScoped<ISampleRepository, SampleRepository>();
|
||||
// Register repositories (when needed)
|
||||
// services.AddScoped<IBillingAccountRepository, BillingAccountRepository>();
|
||||
|
||||
// EN: Register idempotency services / VI: Đăng ký idempotency services
|
||||
// Register idempotency services
|
||||
services.AddScoped<IRequestManager, RequestManager>();
|
||||
|
||||
return services;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: EF Core configuration for Sample entity.
|
||||
/// VI: Cấu hình EF Core cho entity Sample.
|
||||
/// </summary>
|
||||
public class SampleEntityTypeConfiguration : IEntityTypeConfiguration<Sample>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Sample> builder)
|
||||
{
|
||||
// EN: Table name / VI: Tên bảng
|
||||
builder.ToTable("samples");
|
||||
|
||||
// EN: Primary key / VI: Khóa chính
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
// EN: Ignore domain events (not persisted)
|
||||
// VI: Bỏ qua domain events (không lưu)
|
||||
builder.Ignore(s => s.DomainEvents);
|
||||
|
||||
// EN: Properties / VI: Các thuộc tính
|
||||
builder.Property(s => s.Id)
|
||||
.HasColumnName("id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string>("_name")
|
||||
.HasColumnName("name")
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string?>("_description")
|
||||
.HasColumnName("description")
|
||||
.HasMaxLength(1000);
|
||||
|
||||
builder.Property<DateTime>("_createdAt")
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime?>("_updatedAt")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
// EN: Status relationship / VI: Quan hệ với Status
|
||||
builder.Property(s => s.StatusId)
|
||||
.HasColumnName("status_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(s => s.Status)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.StatusId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// EN: Indexes / VI: Các index
|
||||
builder.HasIndex("_name");
|
||||
builder.HasIndex(s => s.StatusId);
|
||||
builder.HasIndex("_createdAt");
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace AdsBillingService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: EF Core configuration for SampleStatus enumeration.
|
||||
/// VI: Cấu hình EF Core cho enumeration SampleStatus.
|
||||
/// </summary>
|
||||
public class SampleStatusEntityTypeConfiguration : IEntityTypeConfiguration<SampleStatus>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SampleStatus> builder)
|
||||
{
|
||||
// EN: Table name / VI: Tên bảng
|
||||
builder.ToTable("sample_statuses");
|
||||
|
||||
// EN: Primary key / VI: Khóa chính
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
builder.Property(s => s.Id)
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedNever()
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(s => s.Name)
|
||||
.HasColumnName("name")
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// EN: Seed initial data / VI: Seed dữ liệu ban đầu
|
||||
builder.HasData(
|
||||
SampleStatus.Draft,
|
||||
SampleStatus.Active,
|
||||
SampleStatus.Completed,
|
||||
SampleStatus.Cancelled
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
using AdsBillingService.Domain.SeedWork;
|
||||
|
||||
namespace AdsBillingService.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Repository implementation for Sample aggregate.
|
||||
/// VI: Triển khai repository cho Sample aggregate.
|
||||
/// </summary>
|
||||
public class SampleRepository : ISampleRepository
|
||||
{
|
||||
private readonly AdsBillingServiceContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit of work for transaction management.
|
||||
/// VI: Unit of work cho quản lý transaction.
|
||||
/// </summary>
|
||||
public IUnitOfWork UnitOfWork => _context;
|
||||
|
||||
public SampleRepository(AdsBillingServiceContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Sample?> GetAsync(Guid sampleId)
|
||||
{
|
||||
var sample = await _context.Samples
|
||||
.Include(s => s.Status)
|
||||
.FirstOrDefaultAsync(s => s.Id == sampleId);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<Sample>> GetAllAsync()
|
||||
{
|
||||
return await _context.Samples
|
||||
.Include(s => s.Status)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Sample Add(Sample sample)
|
||||
{
|
||||
return _context.Samples.Add(sample).Entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Update(Sample sample)
|
||||
{
|
||||
_context.Entry(sample).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Delete(Sample sample)
|
||||
{
|
||||
_context.Samples.Remove(sample);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<Sample>> GetByStatusAsync(int statusId)
|
||||
{
|
||||
return await _context.Samples
|
||||
.Include(s => s.Status)
|
||||
.Where(s => s.StatusId == statusId)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using AdsBillingService.API.Application.Commands;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
using AdsBillingService.Domain.SeedWork;
|
||||
using Xunit;
|
||||
|
||||
namespace AdsBillingService.UnitTests.Application;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for CreateSampleCommandHandler.
|
||||
/// VI: Unit tests cho CreateSampleCommandHandler.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<ISampleRepository> _mockRepository;
|
||||
private readonly Mock<ILogger<CreateSampleCommandHandler>> _mockLogger;
|
||||
private readonly CreateSampleCommandHandler _handler;
|
||||
|
||||
public CreateSampleCommandHandlerTests()
|
||||
{
|
||||
_mockRepository = new Mock<ISampleRepository>();
|
||||
_mockLogger = new Mock<ILogger<CreateSampleCommandHandler>>();
|
||||
|
||||
var mockUnitOfWork = new Mock<IUnitOfWork>();
|
||||
mockUnitOfWork.Setup(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
_mockRepository.SetupGet(r => r.UnitOfWork).Returns(mockUnitOfWork.Object);
|
||||
|
||||
_handler = new CreateSampleCommandHandler(_mockRepository.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidCommand_ShouldCreateSampleAndReturnId()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateSampleCommand("Test Sample", "Test Description");
|
||||
|
||||
_mockRepository.Setup(r => r.Add(It.IsAny<Sample>()))
|
||||
.Returns((Sample s) => s);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().NotBeEmpty();
|
||||
_mockRepository.Verify(r => r.Add(It.IsAny<Sample>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidCommand_ShouldCallSaveEntities()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateSampleCommand("Test Sample", null);
|
||||
|
||||
// Act
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
_mockRepository.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using AdsBillingService.Domain.AggregatesModel.SampleAggregate;
|
||||
using AdsBillingService.Domain.Exceptions;
|
||||
using Xunit;
|
||||
|
||||
namespace AdsBillingService.UnitTests.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for Sample aggregate.
|
||||
/// VI: Unit tests cho Sample aggregate.
|
||||
/// </summary>
|
||||
public class SampleAggregateTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateSample_WithValidName_ShouldCreateWithDraftStatus()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Sample";
|
||||
var description = "Test Description";
|
||||
|
||||
// Act
|
||||
var sample = new Sample(name, description);
|
||||
|
||||
// Assert
|
||||
sample.Name.Should().Be(name);
|
||||
sample.Description.Should().Be(description);
|
||||
sample.Status.Should().Be(SampleStatus.Draft);
|
||||
sample.Id.Should().NotBeEmpty();
|
||||
sample.DomainEvents.Should().ContainSingle(); // SampleCreatedDomainEvent
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateSample_WithEmptyName_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var name = "";
|
||||
|
||||
// Act
|
||||
var act = () => new Sample(name);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<SampleDomainException>()
|
||||
.WithMessage("Sample name cannot be empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Activate_WhenDraft_ShouldChangeToActive()
|
||||
{
|
||||
// Arrange
|
||||
var sample = new Sample("Test Sample");
|
||||
sample.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
sample.Activate();
|
||||
|
||||
// Assert
|
||||
sample.Status.Should().Be(SampleStatus.Active);
|
||||
sample.DomainEvents.Should().ContainSingle(); // SampleStatusChangedDomainEvent
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Activate_WhenNotDraft_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var sample = new Sample("Test Sample");
|
||||
sample.Activate();
|
||||
|
||||
// Act
|
||||
var act = () => sample.Activate();
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<SampleDomainException>()
|
||||
.WithMessage("Only draft samples can be activated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Complete_WhenActive_ShouldChangeToCompleted()
|
||||
{
|
||||
// Arrange
|
||||
var sample = new Sample("Test Sample");
|
||||
sample.Activate();
|
||||
sample.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
sample.Complete();
|
||||
|
||||
// Assert
|
||||
sample.Status.Should().Be(SampleStatus.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cancel_WhenDraftOrActive_ShouldChangeToCancelled()
|
||||
{
|
||||
// Arrange
|
||||
var sample = new Sample("Test Sample");
|
||||
|
||||
// Act
|
||||
sample.Cancel();
|
||||
|
||||
// Assert
|
||||
sample.Status.Should().Be(SampleStatus.Cancelled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cancel_WhenCompleted_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var sample = new Sample("Test Sample");
|
||||
sample.Activate();
|
||||
sample.Complete();
|
||||
|
||||
// Act
|
||||
var act = () => sample.Cancel();
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<SampleDomainException>()
|
||||
.WithMessage("Cannot cancel a completed sample");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WhenNotCancelled_ShouldUpdateNameAndDescription()
|
||||
{
|
||||
// Arrange
|
||||
var sample = new Sample("Original Name", "Original Description");
|
||||
var newName = "Updated Name";
|
||||
var newDescription = "Updated Description";
|
||||
|
||||
// Act
|
||||
sample.Update(newName, newDescription);
|
||||
|
||||
// Assert
|
||||
sample.Name.Should().Be(newName);
|
||||
sample.Description.Should().Be(newDescription);
|
||||
sample.UpdatedAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WhenCancelled_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var sample = new Sample("Test Sample");
|
||||
sample.Cancel();
|
||||
|
||||
// Act
|
||||
var act = () => sample.Update("New Name", null);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<SampleDomainException>()
|
||||
.WithMessage("Cannot update a cancelled sample");
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CatalogService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to change status of a Sample.
|
||||
/// VI: Command để thay đổi trạng thái của Sample.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="NewStatus">EN: New status (activate, complete, cancel) / VI: Trạng thái mới (activate, complete, cancel)</param>
|
||||
public record ChangeSampleStatusCommand(
|
||||
Guid SampleId,
|
||||
string NewStatus
|
||||
) : IRequest<bool>;
|
||||
@@ -1,70 +0,0 @@
|
||||
using MediatR;
|
||||
using CatalogService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace CatalogService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for ChangeSampleStatusCommand.
|
||||
/// VI: Handler cho ChangeSampleStatusCommand.
|
||||
/// </summary>
|
||||
public class ChangeSampleStatusCommandHandler : IRequestHandler<ChangeSampleStatusCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<ChangeSampleStatusCommandHandler> _logger;
|
||||
|
||||
public ChangeSampleStatusCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<ChangeSampleStatusCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
ChangeSampleStatusCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Changing status of sample {SampleId} to {NewStatus} / Thay đổi trạng thái sample {SampleId} thành {NewStatus}",
|
||||
request.SampleId, request.NewStatus);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Change status based on action / VI: Thay đổi trạng thái dựa trên action
|
||||
switch (request.NewStatus.ToLowerInvariant())
|
||||
{
|
||||
case "activate":
|
||||
sample.Activate();
|
||||
break;
|
||||
case "complete":
|
||||
sample.Complete();
|
||||
break;
|
||||
case "cancel":
|
||||
sample.Cancel();
|
||||
break;
|
||||
default:
|
||||
_logger.LogWarning(
|
||||
"Invalid status action: {NewStatus} / Action trạng thái không hợp lệ: {NewStatus}",
|
||||
request.NewStatus);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} status changed to {NewStatus} / Trạng thái sample {SampleId} đã đổi thành {NewStatus}",
|
||||
request.SampleId, request.NewStatus);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CatalogService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to create a new Sample.
|
||||
/// VI: Command để tạo một Sample mới.
|
||||
/// </summary>
|
||||
/// <param name="Name">EN: Sample name / VI: Tên sample</param>
|
||||
/// <param name="Description">EN: Optional description / VI: Mô tả tùy chọn</param>
|
||||
public record CreateSampleCommand(
|
||||
string Name,
|
||||
string? Description
|
||||
) : IRequest<CreateSampleCommandResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of CreateSampleCommand.
|
||||
/// VI: Kết quả của CreateSampleCommand.
|
||||
/// </summary>
|
||||
/// <param name="Id">EN: Created sample ID / VI: ID sample đã tạo</param>
|
||||
public record CreateSampleCommandResult(Guid Id);
|
||||
@@ -1,46 +0,0 @@
|
||||
using MediatR;
|
||||
using CatalogService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace CatalogService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for CreateSampleCommand.
|
||||
/// VI: Handler cho CreateSampleCommand.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandHandler : IRequestHandler<CreateSampleCommand, CreateSampleCommandResult>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<CreateSampleCommandHandler> _logger;
|
||||
|
||||
public CreateSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<CreateSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CreateSampleCommandResult> Handle(
|
||||
CreateSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Creating new sample with name: {Name} / Tạo sample mới với tên: {Name}",
|
||||
request.Name);
|
||||
|
||||
// EN: Create domain entity / VI: Tạo domain entity
|
||||
var sample = new Sample(request.Name, request.Description);
|
||||
|
||||
// EN: Add to repository / VI: Thêm vào repository
|
||||
_sampleRepository.Add(sample);
|
||||
|
||||
// EN: Save changes (dispatches domain events) / VI: Lưu thay đổi (dispatch domain events)
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample created successfully with ID: {SampleId} / Sample đã tạo thành công với ID: {SampleId}",
|
||||
sample.Id);
|
||||
|
||||
return new CreateSampleCommandResult(sample.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CatalogService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to delete a Sample.
|
||||
/// VI: Command để xóa một Sample.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID to delete / VI: ID sample cần xóa</param>
|
||||
public record DeleteSampleCommand(Guid SampleId) : IRequest<bool>;
|
||||
@@ -1,54 +0,0 @@
|
||||
using MediatR;
|
||||
using CatalogService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace CatalogService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for DeleteSampleCommand.
|
||||
/// VI: Handler cho DeleteSampleCommand.
|
||||
/// </summary>
|
||||
public class DeleteSampleCommandHandler : IRequestHandler<DeleteSampleCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<DeleteSampleCommandHandler> _logger;
|
||||
|
||||
public DeleteSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<DeleteSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
DeleteSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Deleting sample {SampleId} / Xóa sample {SampleId}",
|
||||
request.SampleId);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Delete sample / VI: Xóa sample
|
||||
_sampleRepository.Delete(sample);
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} deleted successfully / Sample {SampleId} đã xóa thành công",
|
||||
request.SampleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CatalogService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to update an existing Sample.
|
||||
/// VI: Command để cập nhật một Sample đã tồn tại.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID to update / VI: ID sample cần cập nhật</param>
|
||||
/// <param name="Name">EN: New name / VI: Tên mới</param>
|
||||
/// <param name="Description">EN: New description / VI: Mô tả mới</param>
|
||||
public record UpdateSampleCommand(
|
||||
Guid SampleId,
|
||||
string Name,
|
||||
string? Description
|
||||
) : IRequest<bool>;
|
||||
@@ -1,54 +0,0 @@
|
||||
using MediatR;
|
||||
using CatalogService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace CatalogService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for UpdateSampleCommand.
|
||||
/// VI: Handler cho UpdateSampleCommand.
|
||||
/// </summary>
|
||||
public class UpdateSampleCommandHandler : IRequestHandler<UpdateSampleCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<UpdateSampleCommandHandler> _logger;
|
||||
|
||||
public UpdateSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<UpdateSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
UpdateSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Updating sample {SampleId} / Cập nhật sample {SampleId}",
|
||||
request.SampleId);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Update sample using domain method / VI: Cập nhật sample sử dụng domain method
|
||||
sample.Update(request.Name, request.Description);
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} updated successfully / Sample {SampleId} đã cập nhật thành công",
|
||||
request.SampleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CatalogService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get a Sample by ID.
|
||||
/// VI: Query để lấy một Sample theo ID.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID / VI: ID sample</param>
|
||||
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Sample view model for API responses.
|
||||
/// VI: Sample view model cho API responses.
|
||||
/// </summary>
|
||||
public record SampleViewModel(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
string Status,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt
|
||||
);
|
||||
@@ -1,39 +0,0 @@
|
||||
using MediatR;
|
||||
using CatalogService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace CatalogService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetSampleQuery.
|
||||
/// VI: Handler cho GetSampleQuery.
|
||||
/// </summary>
|
||||
public class GetSampleQueryHandler : IRequestHandler<GetSampleQuery, SampleViewModel?>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
|
||||
public GetSampleQueryHandler(ISampleRepository sampleRepository)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
}
|
||||
|
||||
public async Task<SampleViewModel?> Handle(
|
||||
GetSampleQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SampleViewModel(
|
||||
sample.Id,
|
||||
sample.Name,
|
||||
sample.Description,
|
||||
sample.Status.Name,
|
||||
sample.CreatedAt,
|
||||
sample.UpdatedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CatalogService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get all Samples.
|
||||
/// VI: Query để lấy tất cả Samples.
|
||||
/// </summary>
|
||||
public record GetSamplesQuery : IRequest<IEnumerable<SampleViewModel>>;
|
||||
@@ -1,34 +0,0 @@
|
||||
using MediatR;
|
||||
using CatalogService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace CatalogService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetSamplesQuery.
|
||||
/// VI: Handler cho GetSamplesQuery.
|
||||
/// </summary>
|
||||
public class GetSamplesQueryHandler : IRequestHandler<GetSamplesQuery, IEnumerable<SampleViewModel>>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
|
||||
public GetSamplesQueryHandler(ISampleRepository sampleRepository)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SampleViewModel>> Handle(
|
||||
GetSamplesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var samples = await _sampleRepository.GetAllAsync();
|
||||
|
||||
return samples.Select(sample => new SampleViewModel(
|
||||
sample.Id,
|
||||
sample.Name,
|
||||
sample.Description,
|
||||
sample.Status.Name,
|
||||
sample.CreatedAt,
|
||||
sample.UpdatedAt
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using FluentValidation;
|
||||
using CatalogService.API.Application.Commands;
|
||||
|
||||
namespace CatalogService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for CreateSampleCommand.
|
||||
/// VI: Validator cho CreateSampleCommand.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandValidator : AbstractValidator<CreateSampleCommand>
|
||||
{
|
||||
public CreateSampleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Name is required / Tên là bắt buộc")
|
||||
.MaximumLength(200)
|
||||
.WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000)
|
||||
.WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
|
||||
.When(x => x.Description != null);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using FluentValidation;
|
||||
using CatalogService.API.Application.Commands;
|
||||
|
||||
namespace CatalogService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for UpdateSampleCommand.
|
||||
/// VI: Validator cho UpdateSampleCommand.
|
||||
/// </summary>
|
||||
public class UpdateSampleCommandValidator : AbstractValidator<UpdateSampleCommand>
|
||||
{
|
||||
public UpdateSampleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.SampleId)
|
||||
.NotEmpty()
|
||||
.WithMessage("Sample ID is required / ID sample là bắt buộc");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Name is required / Tên là bắt buộc")
|
||||
.MaximumLength(200)
|
||||
.WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000)
|
||||
.WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
|
||||
.When(x => x.Description != null);
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CatalogService.API.Application.Commands;
|
||||
using CatalogService.API.Application.Queries;
|
||||
|
||||
namespace CatalogService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Controller for Sample CRUD operations using CQRS pattern.
|
||||
/// VI: Controller cho các thao tác CRUD Sample sử dụng pattern CQRS.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
public class SamplesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<SamplesController> _logger;
|
||||
|
||||
public SamplesController(IMediator mediator, ILogger<SamplesController> logger)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get all samples.
|
||||
/// VI: Lấy tất cả samples.
|
||||
/// </summary>
|
||||
/// <returns>EN: List of samples / VI: Danh sách samples</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<SampleViewModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetSamples()
|
||||
{
|
||||
var samples = await _mediator.Send(new GetSamplesQuery());
|
||||
return Ok(new { success = true, data = samples });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get a sample by ID.
|
||||
/// VI: Lấy một sample theo ID.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <returns>EN: Sample details / VI: Chi tiết sample</returns>
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(SampleViewModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetSample(Guid id)
|
||||
{
|
||||
var sample = await _mediator.Send(new GetSampleQuery(id));
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, data = sample });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new sample.
|
||||
/// VI: Tạo một sample mới.
|
||||
/// </summary>
|
||||
/// <param name="request">EN: Create request / VI: Request tạo</param>
|
||||
/// <returns>EN: Created sample ID / VI: ID sample đã tạo</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CreateSampleCommandResult), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateSample([FromBody] CreateSampleRequest request)
|
||||
{
|
||||
var command = new CreateSampleCommand(request.Name, request.Description);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetSample),
|
||||
new { id = result.Id },
|
||||
new { success = true, data = result });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update an existing sample.
|
||||
/// VI: Cập nhật một sample đã tồn tại.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="request">EN: Update request / VI: Request cập nhật</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpPut("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateSample(Guid id, [FromBody] UpdateSampleRequest request)
|
||||
{
|
||||
var command = new UpdateSampleCommand(id, request.Name, request.Description);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, message = "Sample updated successfully / Sample đã cập nhật thành công" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete a sample.
|
||||
/// VI: Xóa một sample.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpDelete("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> DeleteSample(Guid id)
|
||||
{
|
||||
var command = new DeleteSampleCommand(id);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Change sample status.
|
||||
/// VI: Thay đổi trạng thái sample.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="request">EN: Status change request / VI: Request thay đổi trạng thái</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpPatch("{id:guid}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ChangeSampleStatus(Guid id, [FromBody] ChangeStatusRequest request)
|
||||
{
|
||||
var command = new ChangeSampleStatusCommand(id, request.Status);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "STATUS_CHANGE_FAILED",
|
||||
message = "Failed to change sample status / Thay đổi trạng thái sample thất bại"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, message = "Sample status changed successfully / Trạng thái sample đã thay đổi thành công" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for creating a sample.
|
||||
/// VI: Model request để tạo sample.
|
||||
/// </summary>
|
||||
public record CreateSampleRequest(string Name, string? Description);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for updating a sample.
|
||||
/// VI: Model request để cập nhật sample.
|
||||
/// </summary>
|
||||
public record UpdateSampleRequest(string Name, string? Description);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for changing sample status.
|
||||
/// VI: Model request để thay đổi trạng thái sample.
|
||||
/// </summary>
|
||||
public record ChangeStatusRequest(string Status);
|
||||
@@ -1,65 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using CatalogService.API.Application.Commands;
|
||||
using CatalogService.Domain.AggregatesModel.SampleAggregate;
|
||||
using CatalogService.Domain.SeedWork;
|
||||
using Xunit;
|
||||
|
||||
namespace CatalogService.UnitTests.Application;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for CreateSampleCommandHandler.
|
||||
/// VI: Unit tests cho CreateSampleCommandHandler.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<ISampleRepository> _mockRepository;
|
||||
private readonly Mock<ILogger<CreateSampleCommandHandler>> _mockLogger;
|
||||
private readonly CreateSampleCommandHandler _handler;
|
||||
|
||||
public CreateSampleCommandHandlerTests()
|
||||
{
|
||||
_mockRepository = new Mock<ISampleRepository>();
|
||||
_mockLogger = new Mock<ILogger<CreateSampleCommandHandler>>();
|
||||
|
||||
var mockUnitOfWork = new Mock<IUnitOfWork>();
|
||||
mockUnitOfWork.Setup(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
_mockRepository.SetupGet(r => r.UnitOfWork).Returns(mockUnitOfWork.Object);
|
||||
|
||||
_handler = new CreateSampleCommandHandler(_mockRepository.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidCommand_ShouldCreateSampleAndReturnId()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateSampleCommand("Test Sample", "Test Description");
|
||||
|
||||
_mockRepository.Setup(r => r.Add(It.IsAny<Sample>()))
|
||||
.Returns((Sample s) => s);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().NotBeEmpty();
|
||||
_mockRepository.Verify(r => r.Add(It.IsAny<Sample>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidCommand_ShouldCallSaveEntities()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateSampleCommand("Test Sample", null);
|
||||
|
||||
// Act
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
_mockRepository.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,11 @@ namespace OrderService.API.Application.Behaviors;
|
||||
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
private readonly OrderServiceContext _dbContext;
|
||||
private readonly OrderContext _dbContext;
|
||||
private readonly ILogger<TransactionBehavior<TRequest, TResponse>> _logger;
|
||||
|
||||
public TransactionBehavior(
|
||||
OrderServiceContext dbContext,
|
||||
OrderContext dbContext,
|
||||
ILogger<TransactionBehavior<TRequest, TResponse>> logger)
|
||||
{
|
||||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to change status of a Sample.
|
||||
/// VI: Command để thay đổi trạng thái của Sample.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="NewStatus">EN: New status (activate, complete, cancel) / VI: Trạng thái mới (activate, complete, cancel)</param>
|
||||
public record ChangeSampleStatusCommand(
|
||||
Guid SampleId,
|
||||
string NewStatus
|
||||
) : IRequest<bool>;
|
||||
@@ -1,70 +0,0 @@
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for ChangeSampleStatusCommand.
|
||||
/// VI: Handler cho ChangeSampleStatusCommand.
|
||||
/// </summary>
|
||||
public class ChangeSampleStatusCommandHandler : IRequestHandler<ChangeSampleStatusCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<ChangeSampleStatusCommandHandler> _logger;
|
||||
|
||||
public ChangeSampleStatusCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<ChangeSampleStatusCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
ChangeSampleStatusCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Changing status of sample {SampleId} to {NewStatus} / Thay đổi trạng thái sample {SampleId} thành {NewStatus}",
|
||||
request.SampleId, request.NewStatus);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Change status based on action / VI: Thay đổi trạng thái dựa trên action
|
||||
switch (request.NewStatus.ToLowerInvariant())
|
||||
{
|
||||
case "activate":
|
||||
sample.Activate();
|
||||
break;
|
||||
case "complete":
|
||||
sample.Complete();
|
||||
break;
|
||||
case "cancel":
|
||||
sample.Cancel();
|
||||
break;
|
||||
default:
|
||||
_logger.LogWarning(
|
||||
"Invalid status action: {NewStatus} / Action trạng thái không hợp lệ: {NewStatus}",
|
||||
request.NewStatus);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} status changed to {NewStatus} / Trạng thái sample {SampleId} đã đổi thành {NewStatus}",
|
||||
request.SampleId, request.NewStatus);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to create a new Sample.
|
||||
/// VI: Command để tạo một Sample mới.
|
||||
/// </summary>
|
||||
/// <param name="Name">EN: Sample name / VI: Tên sample</param>
|
||||
/// <param name="Description">EN: Optional description / VI: Mô tả tùy chọn</param>
|
||||
public record CreateSampleCommand(
|
||||
string Name,
|
||||
string? Description
|
||||
) : IRequest<CreateSampleCommandResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of CreateSampleCommand.
|
||||
/// VI: Kết quả của CreateSampleCommand.
|
||||
/// </summary>
|
||||
/// <param name="Id">EN: Created sample ID / VI: ID sample đã tạo</param>
|
||||
public record CreateSampleCommandResult(Guid Id);
|
||||
@@ -1,46 +0,0 @@
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for CreateSampleCommand.
|
||||
/// VI: Handler cho CreateSampleCommand.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandHandler : IRequestHandler<CreateSampleCommand, CreateSampleCommandResult>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<CreateSampleCommandHandler> _logger;
|
||||
|
||||
public CreateSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<CreateSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CreateSampleCommandResult> Handle(
|
||||
CreateSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Creating new sample with name: {Name} / Tạo sample mới với tên: {Name}",
|
||||
request.Name);
|
||||
|
||||
// EN: Create domain entity / VI: Tạo domain entity
|
||||
var sample = new Sample(request.Name, request.Description);
|
||||
|
||||
// EN: Add to repository / VI: Thêm vào repository
|
||||
_sampleRepository.Add(sample);
|
||||
|
||||
// EN: Save changes (dispatches domain events) / VI: Lưu thay đổi (dispatch domain events)
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample created successfully with ID: {SampleId} / Sample đã tạo thành công với ID: {SampleId}",
|
||||
sample.Id);
|
||||
|
||||
return new CreateSampleCommandResult(sample.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to delete a Sample.
|
||||
/// VI: Command để xóa một Sample.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID to delete / VI: ID sample cần xóa</param>
|
||||
public record DeleteSampleCommand(Guid SampleId) : IRequest<bool>;
|
||||
@@ -1,54 +0,0 @@
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for DeleteSampleCommand.
|
||||
/// VI: Handler cho DeleteSampleCommand.
|
||||
/// </summary>
|
||||
public class DeleteSampleCommandHandler : IRequestHandler<DeleteSampleCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<DeleteSampleCommandHandler> _logger;
|
||||
|
||||
public DeleteSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<DeleteSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
DeleteSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Deleting sample {SampleId} / Xóa sample {SampleId}",
|
||||
request.SampleId);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Delete sample / VI: Xóa sample
|
||||
_sampleRepository.Delete(sample);
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} deleted successfully / Sample {SampleId} đã xóa thành công",
|
||||
request.SampleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to update an existing Sample.
|
||||
/// VI: Command để cập nhật một Sample đã tồn tại.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID to update / VI: ID sample cần cập nhật</param>
|
||||
/// <param name="Name">EN: New name / VI: Tên mới</param>
|
||||
/// <param name="Description">EN: New description / VI: Mô tả mới</param>
|
||||
public record UpdateSampleCommand(
|
||||
Guid SampleId,
|
||||
string Name,
|
||||
string? Description
|
||||
) : IRequest<bool>;
|
||||
@@ -1,54 +0,0 @@
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for UpdateSampleCommand.
|
||||
/// VI: Handler cho UpdateSampleCommand.
|
||||
/// </summary>
|
||||
public class UpdateSampleCommandHandler : IRequestHandler<UpdateSampleCommand, bool>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
private readonly ILogger<UpdateSampleCommandHandler> _logger;
|
||||
|
||||
public UpdateSampleCommandHandler(
|
||||
ISampleRepository sampleRepository,
|
||||
ILogger<UpdateSampleCommandHandler> logger)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(
|
||||
UpdateSampleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Updating sample {SampleId} / Cập nhật sample {SampleId}",
|
||||
request.SampleId);
|
||||
|
||||
// EN: Get existing sample / VI: Lấy sample đã tồn tại
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
|
||||
request.SampleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Update sample using domain method / VI: Cập nhật sample sử dụng domain method
|
||||
sample.Update(request.Name, request.Description);
|
||||
|
||||
// EN: Save changes / VI: Lưu thay đổi
|
||||
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sample {SampleId} updated successfully / Sample {SampleId} đã cập nhật thành công",
|
||||
request.SampleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get a Sample by ID.
|
||||
/// VI: Query để lấy một Sample theo ID.
|
||||
/// </summary>
|
||||
/// <param name="SampleId">EN: Sample ID / VI: ID sample</param>
|
||||
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Sample view model for API responses.
|
||||
/// VI: Sample view model cho API responses.
|
||||
/// </summary>
|
||||
public record SampleViewModel(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
string Status,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt
|
||||
);
|
||||
@@ -1,39 +0,0 @@
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetSampleQuery.
|
||||
/// VI: Handler cho GetSampleQuery.
|
||||
/// </summary>
|
||||
public class GetSampleQueryHandler : IRequestHandler<GetSampleQuery, SampleViewModel?>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
|
||||
public GetSampleQueryHandler(ISampleRepository sampleRepository)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
}
|
||||
|
||||
public async Task<SampleViewModel?> Handle(
|
||||
GetSampleQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sample = await _sampleRepository.GetAsync(request.SampleId);
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SampleViewModel(
|
||||
sample.Id,
|
||||
sample.Name,
|
||||
sample.Description,
|
||||
sample.Status.Name,
|
||||
sample.CreatedAt,
|
||||
sample.UpdatedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get all Samples.
|
||||
/// VI: Query để lấy tất cả Samples.
|
||||
/// </summary>
|
||||
public record GetSamplesQuery : IRequest<IEnumerable<SampleViewModel>>;
|
||||
@@ -1,34 +0,0 @@
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetSamplesQuery.
|
||||
/// VI: Handler cho GetSamplesQuery.
|
||||
/// </summary>
|
||||
public class GetSamplesQueryHandler : IRequestHandler<GetSamplesQuery, IEnumerable<SampleViewModel>>
|
||||
{
|
||||
private readonly ISampleRepository _sampleRepository;
|
||||
|
||||
public GetSamplesQueryHandler(ISampleRepository sampleRepository)
|
||||
{
|
||||
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SampleViewModel>> Handle(
|
||||
GetSamplesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var samples = await _sampleRepository.GetAllAsync();
|
||||
|
||||
return samples.Select(sample => new SampleViewModel(
|
||||
sample.Id,
|
||||
sample.Name,
|
||||
sample.Description,
|
||||
sample.Status.Name,
|
||||
sample.CreatedAt,
|
||||
sample.UpdatedAt
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using FluentValidation;
|
||||
using OrderService.API.Application.Commands;
|
||||
|
||||
namespace OrderService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for CreateSampleCommand.
|
||||
/// VI: Validator cho CreateSampleCommand.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandValidator : AbstractValidator<CreateSampleCommand>
|
||||
{
|
||||
public CreateSampleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Name is required / Tên là bắt buộc")
|
||||
.MaximumLength(200)
|
||||
.WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000)
|
||||
.WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
|
||||
.When(x => x.Description != null);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using FluentValidation;
|
||||
using OrderService.API.Application.Commands;
|
||||
|
||||
namespace OrderService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for UpdateSampleCommand.
|
||||
/// VI: Validator cho UpdateSampleCommand.
|
||||
/// </summary>
|
||||
public class UpdateSampleCommandValidator : AbstractValidator<UpdateSampleCommand>
|
||||
{
|
||||
public UpdateSampleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.SampleId)
|
||||
.NotEmpty()
|
||||
.WithMessage("Sample ID is required / ID sample là bắt buộc");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Name is required / Tên là bắt buộc")
|
||||
.MaximumLength(200)
|
||||
.WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000)
|
||||
.WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
|
||||
.When(x => x.Description != null);
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OrderService.API.Application.Commands;
|
||||
using OrderService.API.Application.Queries;
|
||||
|
||||
namespace OrderService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Controller for Sample CRUD operations using CQRS pattern.
|
||||
/// VI: Controller cho các thao tác CRUD Sample sử dụng pattern CQRS.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
public class SamplesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<SamplesController> _logger;
|
||||
|
||||
public SamplesController(IMediator mediator, ILogger<SamplesController> logger)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get all samples.
|
||||
/// VI: Lấy tất cả samples.
|
||||
/// </summary>
|
||||
/// <returns>EN: List of samples / VI: Danh sách samples</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<SampleViewModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetSamples()
|
||||
{
|
||||
var samples = await _mediator.Send(new GetSamplesQuery());
|
||||
return Ok(new { success = true, data = samples });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get a sample by ID.
|
||||
/// VI: Lấy một sample theo ID.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <returns>EN: Sample details / VI: Chi tiết sample</returns>
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(SampleViewModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetSample(Guid id)
|
||||
{
|
||||
var sample = await _mediator.Send(new GetSampleQuery(id));
|
||||
|
||||
if (sample is null)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, data = sample });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new sample.
|
||||
/// VI: Tạo một sample mới.
|
||||
/// </summary>
|
||||
/// <param name="request">EN: Create request / VI: Request tạo</param>
|
||||
/// <returns>EN: Created sample ID / VI: ID sample đã tạo</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CreateSampleCommandResult), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateSample([FromBody] CreateSampleRequest request)
|
||||
{
|
||||
var command = new CreateSampleCommand(request.Name, request.Description);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetSample),
|
||||
new { id = result.Id },
|
||||
new { success = true, data = result });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update an existing sample.
|
||||
/// VI: Cập nhật một sample đã tồn tại.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="request">EN: Update request / VI: Request cập nhật</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpPut("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateSample(Guid id, [FromBody] UpdateSampleRequest request)
|
||||
{
|
||||
var command = new UpdateSampleCommand(id, request.Name, request.Description);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, message = "Sample updated successfully / Sample đã cập nhật thành công" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete a sample.
|
||||
/// VI: Xóa một sample.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpDelete("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> DeleteSample(Guid id)
|
||||
{
|
||||
var command = new DeleteSampleCommand(id);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "SAMPLE_NOT_FOUND",
|
||||
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Change sample status.
|
||||
/// VI: Thay đổi trạng thái sample.
|
||||
/// </summary>
|
||||
/// <param name="id">EN: Sample ID / VI: ID sample</param>
|
||||
/// <param name="request">EN: Status change request / VI: Request thay đổi trạng thái</param>
|
||||
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
|
||||
[HttpPatch("{id:guid}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ChangeSampleStatus(Guid id, [FromBody] ChangeStatusRequest request)
|
||||
{
|
||||
var command = new ChangeSampleStatusCommand(id, request.Status);
|
||||
var result = await _mediator.Send(command);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
error = new
|
||||
{
|
||||
code = "STATUS_CHANGE_FAILED",
|
||||
message = "Failed to change sample status / Thay đổi trạng thái sample thất bại"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, message = "Sample status changed successfully / Trạng thái sample đã thay đổi thành công" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for creating a sample.
|
||||
/// VI: Model request để tạo sample.
|
||||
/// </summary>
|
||||
public record CreateSampleRequest(string Name, string? Description);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for updating a sample.
|
||||
/// VI: Model request để cập nhật sample.
|
||||
/// </summary>
|
||||
public record UpdateSampleRequest(string Name, string? Description);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request model for changing sample status.
|
||||
/// VI: Model request để thay đổi trạng thái sample.
|
||||
/// </summary>
|
||||
public record ChangeStatusRequest(string Status);
|
||||
@@ -14,6 +14,10 @@
|
||||
<!-- EN: FluentValidation for request validation / VI: FluentValidation cho validation request -->
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- EN: Swagger/OpenAPI / VI: Swagger/OpenAPI -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Infrastructure.Idempotency;
|
||||
using OrderService.Infrastructure.Repositories;
|
||||
|
||||
@@ -22,7 +22,7 @@ public static class DependencyInjection
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL
|
||||
services.AddDbContext<OrderServiceContext>(options =>
|
||||
services.AddDbContext<OrderContext>(options =>
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? configuration["DATABASE_URL"]
|
||||
@@ -30,7 +30,7 @@ public static class DependencyInjection
|
||||
|
||||
options.UseNpgsql(connectionString, npgsqlOptions =>
|
||||
{
|
||||
npgsqlOptions.MigrationsAssembly(typeof(OrderServiceContext).Assembly.FullName);
|
||||
npgsqlOptions.MigrationsAssembly(typeof(OrderContext).Assembly.FullName);
|
||||
npgsqlOptions.EnableRetryOnFailure(
|
||||
maxRetryCount: 5,
|
||||
maxRetryDelay: TimeSpan.FromSeconds(30),
|
||||
@@ -47,7 +47,7 @@ public static class DependencyInjection
|
||||
});
|
||||
|
||||
// EN: Register repositories / VI: Đăng ký repositories
|
||||
services.AddScoped<ISampleRepository, SampleRepository>();
|
||||
services.AddScoped<IOrderRepository, OrderRepository>();
|
||||
|
||||
// EN: Register idempotency services / VI: Đăng ký idempotency services
|
||||
services.AddScoped<IRequestManager, RequestManager>();
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// EN: Order entity configuration.
|
||||
// VI: Cấu hình entity Order.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
|
||||
namespace OrderService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: EF Core configuration for Order entity.
|
||||
/// VI: Cấu hình EF Core cho Order entity.
|
||||
/// </summary>
|
||||
public class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Order> builder)
|
||||
{
|
||||
builder.ToTable("orders");
|
||||
|
||||
builder.HasKey(o => o.Id);
|
||||
|
||||
builder.Property(o => o.Id)
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid>("_shopId")
|
||||
.HasColumnName("shop_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<Guid?>("_customerId")
|
||||
.HasColumnName("customer_id");
|
||||
|
||||
builder.Property(o => o.StatusId)
|
||||
.HasColumnName("status_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<decimal>("_totalAmount")
|
||||
.HasColumnName("total_amount")
|
||||
.HasColumnType("decimal(18,2)")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string?>("_notes")
|
||||
.HasColumnName("notes")
|
||||
.HasMaxLength(2000);
|
||||
|
||||
builder.Property<DateTime>("_createdAt")
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime?>("_updatedAt")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
// EN: OrderItems collection
|
||||
// VI: Collection OrderItems
|
||||
builder.OwnsMany<OrderItem>("_orderItems", oi =>
|
||||
{
|
||||
oi.ToTable("order_items");
|
||||
|
||||
oi.WithOwner().HasForeignKey("OrderId");
|
||||
|
||||
oi.Property<Guid>("OrderId")
|
||||
.HasColumnName("order_id");
|
||||
|
||||
oi.HasKey("Id");
|
||||
|
||||
oi.Property(x => x.Id)
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedNever();
|
||||
|
||||
oi.Property<Guid>("_productId")
|
||||
.HasColumnName("product_id")
|
||||
.IsRequired();
|
||||
|
||||
oi.Property<string>("_productName")
|
||||
.HasColumnName("product_name")
|
||||
.HasMaxLength(255)
|
||||
.IsRequired();
|
||||
|
||||
oi.Property(x => x.ProductTypeId)
|
||||
.HasColumnName("product_type_id")
|
||||
.IsRequired();
|
||||
|
||||
oi.Property<int>("_quantity")
|
||||
.HasColumnName("quantity")
|
||||
.IsRequired();
|
||||
|
||||
oi.Property<decimal>("_unitPrice")
|
||||
.HasColumnName("unit_price")
|
||||
.HasColumnType("decimal(18,2)")
|
||||
.IsRequired();
|
||||
|
||||
oi.Property<decimal>("_totalPrice")
|
||||
.HasColumnName("total_price")
|
||||
.HasColumnType("decimal(18,2)")
|
||||
.IsRequired();
|
||||
|
||||
oi.Ignore(x => x.ProductId);
|
||||
oi.Ignore(x => x.ProductName);
|
||||
oi.Ignore(x => x.ProductType);
|
||||
oi.Ignore(x => x.Quantity);
|
||||
oi.Ignore(x => x.UnitPrice);
|
||||
oi.Ignore(x => x.TotalPrice);
|
||||
});
|
||||
|
||||
// EN: Indexes
|
||||
// VI: Indexes
|
||||
builder.HasIndex("_shopId").HasDatabaseName("ix_orders_shop_id");
|
||||
builder.HasIndex("_customerId").HasDatabaseName("ix_orders_customer_id");
|
||||
builder.HasIndex(o => o.StatusId).HasDatabaseName("ix_orders_status_id");
|
||||
builder.HasIndex("_createdAt").HasDatabaseName("ix_orders_created_at");
|
||||
|
||||
// EN: Ignore calculated properties
|
||||
// VI: Bỏ qua các properties được tính toán
|
||||
builder.Ignore(o => o.ShopId);
|
||||
builder.Ignore(o => o.CustomerId);
|
||||
builder.Ignore(o => o.Status);
|
||||
builder.Ignore(o => o.OrderItems);
|
||||
builder.Ignore(o => o.TotalAmount);
|
||||
builder.Ignore(o => o.Notes);
|
||||
builder.Ignore(o => o.CreatedAt);
|
||||
builder.Ignore(o => o.UpdatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// EN: OrderItem entity configuration (owned type).
|
||||
// VI: Cấu hình entity OrderItem (owned type).
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
|
||||
namespace OrderService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: EF Core configuration for OrderItem entity (configured as owned type in OrderEntityTypeConfiguration).
|
||||
/// VI: Cấu hình EF Core cho OrderItem entity (được cấu hình như owned type trong OrderEntityTypeConfiguration).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// EN: OrderItem is configured as an owned type within Order aggregate.
|
||||
/// VI: OrderItem được cấu hình như owned type trong Order aggregate.
|
||||
/// </remarks>
|
||||
public class OrderItemEntityTypeConfiguration : IEntityTypeConfiguration<OrderItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OrderItem> builder)
|
||||
{
|
||||
// EN: OrderItem is configured as owned type in OrderEntityTypeConfiguration
|
||||
// VI: OrderItem được cấu hình như owned type trong OrderEntityTypeConfiguration
|
||||
builder.Ignore(o => o.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// EN: OrderStatus enumeration entity configuration.
|
||||
// VI: Cấu hình entity cho OrderStatus enumeration.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
|
||||
namespace OrderService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: EF Core configuration for OrderStatus enumeration.
|
||||
/// VI: Cấu hình EF Core cho OrderStatus enumeration.
|
||||
/// </summary>
|
||||
public class OrderStatusEntityTypeConfiguration : IEntityTypeConfiguration<OrderStatus>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OrderStatus> builder)
|
||||
{
|
||||
builder.ToTable("order_statuses");
|
||||
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
builder.Property(s => s.Id)
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.Property(s => s.Name)
|
||||
.HasColumnName("name")
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// EN: Seed data for OrderStatus
|
||||
// VI: Dữ liệu seed cho OrderStatus
|
||||
builder.HasData(
|
||||
OrderStatus.Draft,
|
||||
OrderStatus.Validated,
|
||||
OrderStatus.Paid,
|
||||
OrderStatus.Processing,
|
||||
OrderStatus.Completed,
|
||||
OrderStatus.Cancelled
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace OrderService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: EF Core configuration for Sample entity.
|
||||
/// VI: Cấu hình EF Core cho entity Sample.
|
||||
/// </summary>
|
||||
public class SampleEntityTypeConfiguration : IEntityTypeConfiguration<Sample>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Sample> builder)
|
||||
{
|
||||
// EN: Table name / VI: Tên bảng
|
||||
builder.ToTable("samples");
|
||||
|
||||
// EN: Primary key / VI: Khóa chính
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
// EN: Ignore domain events (not persisted)
|
||||
// VI: Bỏ qua domain events (không lưu)
|
||||
builder.Ignore(s => s.DomainEvents);
|
||||
|
||||
// EN: Properties / VI: Các thuộc tính
|
||||
builder.Property(s => s.Id)
|
||||
.HasColumnName("id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string>("_name")
|
||||
.HasColumnName("name")
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string?>("_description")
|
||||
.HasColumnName("description")
|
||||
.HasMaxLength(1000);
|
||||
|
||||
builder.Property<DateTime>("_createdAt")
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime?>("_updatedAt")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
// EN: Status relationship / VI: Quan hệ với Status
|
||||
builder.Property(s => s.StatusId)
|
||||
.HasColumnName("status_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(s => s.Status)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.StatusId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// EN: Indexes / VI: Các index
|
||||
builder.HasIndex("_name");
|
||||
builder.HasIndex(s => s.StatusId);
|
||||
builder.HasIndex("_createdAt");
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
|
||||
namespace OrderService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: EF Core configuration for SampleStatus enumeration.
|
||||
/// VI: Cấu hình EF Core cho enumeration SampleStatus.
|
||||
/// </summary>
|
||||
public class SampleStatusEntityTypeConfiguration : IEntityTypeConfiguration<SampleStatus>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SampleStatus> builder)
|
||||
{
|
||||
// EN: Table name / VI: Tên bảng
|
||||
builder.ToTable("sample_statuses");
|
||||
|
||||
// EN: Primary key / VI: Khóa chính
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
builder.Property(s => s.Id)
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedNever()
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(s => s.Name)
|
||||
.HasColumnName("name")
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// EN: Seed initial data / VI: Seed dữ liệu ban đầu
|
||||
builder.HasData(
|
||||
SampleStatus.Draft,
|
||||
SampleStatus.Active,
|
||||
SampleStatus.Completed,
|
||||
SampleStatus.Cancelled
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ namespace OrderService.Infrastructure.Idempotency;
|
||||
/// </summary>
|
||||
public class RequestManager : IRequestManager
|
||||
{
|
||||
private readonly OrderServiceContext _context;
|
||||
private readonly OrderContext _context;
|
||||
|
||||
public RequestManager(OrderServiceContext context)
|
||||
public RequestManager(OrderContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.SeedWork;
|
||||
using OrderService.Infrastructure.EntityConfigurations;
|
||||
|
||||
@@ -11,16 +11,16 @@ namespace OrderService.Infrastructure;
|
||||
/// EN: EF Core DbContext for OrderService.
|
||||
/// VI: EF Core DbContext cho OrderService.
|
||||
/// </summary>
|
||||
public class OrderServiceContext : DbContext, IUnitOfWork
|
||||
public class OrderContext : DbContext, IUnitOfWork
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private IDbContextTransaction? _currentTransaction;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Samples table.
|
||||
/// VI: Bảng Samples.
|
||||
/// EN: Orders table.
|
||||
/// VI: Bảng Orders.
|
||||
/// </summary>
|
||||
public DbSet<Sample> Samples => Set<Sample>();
|
||||
public DbSet<Order> Orders => Set<Order>();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Read-only access to current transaction.
|
||||
@@ -34,24 +34,20 @@ public class OrderServiceContext : DbContext, IUnitOfWork
|
||||
/// </summary>
|
||||
public bool HasActiveTransaction => _currentTransaction != null;
|
||||
|
||||
public OrderServiceContext(DbContextOptions<OrderServiceContext> options) : base(options)
|
||||
{
|
||||
_mediator = null!;
|
||||
}
|
||||
|
||||
public OrderServiceContext(DbContextOptions<OrderServiceContext> options, IMediator mediator) : base(options)
|
||||
public OrderContext(DbContextOptions<OrderContext> options, IMediator mediator) : base(options)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
|
||||
System.Diagnostics.Debug.WriteLine("OrderServiceContext::ctor - " + GetHashCode());
|
||||
System.Diagnostics.Debug.WriteLine("OrderContext::ctor - " + GetHashCode());
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
// EN: Apply entity configurations
|
||||
// VI: Áp dụng các cấu hình entity
|
||||
modelBuilder.ApplyConfiguration(new SampleEntityTypeConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SampleStatusEntityTypeConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new OrderStatusEntityTypeConfiguration());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -0,0 +1,56 @@
|
||||
// EN: Order repository implementation.
|
||||
// VI: Implementation repository Order.
|
||||
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.SeedWork;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace OrderService.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Repository implementation for Order aggregate.
|
||||
/// VI: Implementation repository cho Order aggregate.
|
||||
/// </summary>
|
||||
public class OrderRepository : IOrderRepository
|
||||
{
|
||||
private readonly OrderContext _context;
|
||||
|
||||
public IUnitOfWork UnitOfWork => _context;
|
||||
|
||||
public OrderRepository(OrderContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public Order Add(Order order)
|
||||
{
|
||||
return _context.Orders.Add(order).Entity;
|
||||
}
|
||||
|
||||
public void Update(Order order)
|
||||
{
|
||||
_context.Entry(order).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Orders
|
||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Order>> GetByShopIdAsync(Guid shopId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Orders
|
||||
.Where(o => o.ShopId == shopId)
|
||||
.OrderByDescending(o => o.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Order>> GetByCustomerIdAsync(Guid customerId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Orders
|
||||
.Where(o => o.CustomerId == customerId)
|
||||
.OrderByDescending(o => o.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
using OrderService.Domain.SeedWork;
|
||||
|
||||
namespace OrderService.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Repository implementation for Sample aggregate.
|
||||
/// VI: Triển khai repository cho Sample aggregate.
|
||||
/// </summary>
|
||||
public class SampleRepository : ISampleRepository
|
||||
{
|
||||
private readonly OrderServiceContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit of work for transaction management.
|
||||
/// VI: Unit of work cho quản lý transaction.
|
||||
/// </summary>
|
||||
public IUnitOfWork UnitOfWork => _context;
|
||||
|
||||
public SampleRepository(OrderServiceContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Sample?> GetAsync(Guid sampleId)
|
||||
{
|
||||
var sample = await _context.Samples
|
||||
.Include(s => s.Status)
|
||||
.FirstOrDefaultAsync(s => s.Id == sampleId);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<Sample>> GetAllAsync()
|
||||
{
|
||||
return await _context.Samples
|
||||
.Include(s => s.Status)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Sample Add(Sample sample)
|
||||
{
|
||||
return _context.Samples.Add(sample).Entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Update(Sample sample)
|
||||
{
|
||||
_context.Entry(sample).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Delete(Sample sample)
|
||||
{
|
||||
_context.Samples.Remove(sample);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<Sample>> GetByStatusAsync(int statusId)
|
||||
{
|
||||
return await _context.Samples
|
||||
.Include(s => s.Status)
|
||||
.Where(s => s.StatusId == statusId)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
// EN: Remove the existing DbContext registration
|
||||
// VI: Xóa đăng ký DbContext hiện tại
|
||||
var descriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(DbContextOptions<OrderServiceContext>));
|
||||
d => d.ServiceType == typeof(DbContextOptions<OrderContext>));
|
||||
|
||||
if (descriptor != null)
|
||||
{
|
||||
@@ -31,7 +31,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
// EN: Remove DbContext service
|
||||
// VI: Xóa DbContext service
|
||||
var dbContextDescriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(OrderServiceContext));
|
||||
d => d.ServiceType == typeof(OrderContext));
|
||||
|
||||
if (dbContextDescriptor != null)
|
||||
{
|
||||
@@ -40,7 +40,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
|
||||
// EN: Add in-memory database for testing
|
||||
// VI: Thêm in-memory database để test
|
||||
services.AddDbContext<OrderServiceContext>(options =>
|
||||
services.AddDbContext<OrderContext>(options =>
|
||||
{
|
||||
options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString());
|
||||
});
|
||||
@@ -49,7 +49,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
// VI: Đảm bảo database được tạo với seed data
|
||||
var sp = services.BuildServiceProvider();
|
||||
using var scope = sp.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<OrderServiceContext>();
|
||||
var db = scope.ServiceProvider.GetRequiredService<OrderContext>();
|
||||
db.Database.EnsureCreated();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using OrderService.API.Application.Commands;
|
||||
using OrderService.Domain.AggregatesModel.SampleAggregate;
|
||||
using OrderService.Domain.SeedWork;
|
||||
using Xunit;
|
||||
|
||||
namespace OrderService.UnitTests.Application;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for CreateSampleCommandHandler.
|
||||
/// VI: Unit tests cho CreateSampleCommandHandler.
|
||||
/// </summary>
|
||||
public class CreateSampleCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<ISampleRepository> _mockRepository;
|
||||
private readonly Mock<ILogger<CreateSampleCommandHandler>> _mockLogger;
|
||||
private readonly CreateSampleCommandHandler _handler;
|
||||
|
||||
public CreateSampleCommandHandlerTests()
|
||||
{
|
||||
_mockRepository = new Mock<ISampleRepository>();
|
||||
_mockLogger = new Mock<ILogger<CreateSampleCommandHandler>>();
|
||||
|
||||
var mockUnitOfWork = new Mock<IUnitOfWork>();
|
||||
mockUnitOfWork.Setup(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
_mockRepository.SetupGet(r => r.UnitOfWork).Returns(mockUnitOfWork.Object);
|
||||
|
||||
_handler = new CreateSampleCommandHandler(_mockRepository.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidCommand_ShouldCreateSampleAndReturnId()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateSampleCommand("Test Sample", "Test Description");
|
||||
|
||||
_mockRepository.Setup(r => r.Add(It.IsAny<Sample>()))
|
||||
.Returns((Sample s) => s);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().NotBeEmpty();
|
||||
_mockRepository.Verify(r => r.Add(It.IsAny<Sample>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidCommand_ShouldCallSaveEntities()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateSampleCommand("Test Sample", null);
|
||||
|
||||
// Act
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
_mockRepository.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user