diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/ChangeSampleStatusCommand.cs
deleted file mode 100644
index fbb0ff2b..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/ChangeSampleStatusCommand.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using MediatR;
-
-namespace AdsAnalyticsService.API.Application.Commands;
-
-///
-/// EN: Command to change status of a Sample.
-/// VI: Command để thay đổi trạng thái của Sample.
-///
-/// EN: Sample ID / VI: ID sample
-/// EN: New status (activate, complete, cancel) / VI: Trạng thái mới (activate, complete, cancel)
-public record ChangeSampleStatusCommand(
- Guid SampleId,
- string NewStatus
-) : IRequest;
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs
deleted file mode 100644
index f08db449..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using MediatR;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.API.Application.Commands;
-
-///
-/// EN: Handler for ChangeSampleStatusCommand.
-/// VI: Handler cho ChangeSampleStatusCommand.
-///
-public class ChangeSampleStatusCommandHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
- private readonly ILogger _logger;
-
- public ChangeSampleStatusCommandHandler(
- ISampleRepository sampleRepository,
- ILogger logger)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public async Task 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;
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/CreateSampleCommand.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/CreateSampleCommand.cs
deleted file mode 100644
index 72adc28d..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/CreateSampleCommand.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using MediatR;
-
-namespace AdsAnalyticsService.API.Application.Commands;
-
-///
-/// EN: Command to create a new Sample.
-/// VI: Command để tạo một Sample mới.
-///
-/// EN: Sample name / VI: Tên sample
-/// EN: Optional description / VI: Mô tả tùy chọn
-public record CreateSampleCommand(
- string Name,
- string? Description
-) : IRequest;
-
-///
-/// EN: Result of CreateSampleCommand.
-/// VI: Kết quả của CreateSampleCommand.
-///
-/// EN: Created sample ID / VI: ID sample đã tạo
-public record CreateSampleCommandResult(Guid Id);
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/CreateSampleCommandHandler.cs
deleted file mode 100644
index ef8ac79b..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/CreateSampleCommandHandler.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using MediatR;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.API.Application.Commands;
-
-///
-/// EN: Handler for CreateSampleCommand.
-/// VI: Handler cho CreateSampleCommand.
-///
-public class CreateSampleCommandHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
- private readonly ILogger _logger;
-
- public CreateSampleCommandHandler(
- ISampleRepository sampleRepository,
- ILogger logger)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public async Task 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);
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/DeleteSampleCommand.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/DeleteSampleCommand.cs
deleted file mode 100644
index b0af54cd..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/DeleteSampleCommand.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using MediatR;
-
-namespace AdsAnalyticsService.API.Application.Commands;
-
-///
-/// EN: Command to delete a Sample.
-/// VI: Command để xóa một Sample.
-///
-/// EN: Sample ID to delete / VI: ID sample cần xóa
-public record DeleteSampleCommand(Guid SampleId) : IRequest;
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/DeleteSampleCommandHandler.cs
deleted file mode 100644
index 0a98020a..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/DeleteSampleCommandHandler.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using MediatR;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.API.Application.Commands;
-
-///
-/// EN: Handler for DeleteSampleCommand.
-/// VI: Handler cho DeleteSampleCommand.
-///
-public class DeleteSampleCommandHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
- private readonly ILogger _logger;
-
- public DeleteSampleCommandHandler(
- ISampleRepository sampleRepository,
- ILogger logger)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public async Task 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;
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/UpdateSampleCommand.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/UpdateSampleCommand.cs
deleted file mode 100644
index 65e2ed4f..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/UpdateSampleCommand.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using MediatR;
-
-namespace AdsAnalyticsService.API.Application.Commands;
-
-///
-/// EN: Command to update an existing Sample.
-/// VI: Command để cập nhật một Sample đã tồn tại.
-///
-/// EN: Sample ID to update / VI: ID sample cần cập nhật
-/// EN: New name / VI: Tên mới
-/// EN: New description / VI: Mô tả mới
-public record UpdateSampleCommand(
- Guid SampleId,
- string Name,
- string? Description
-) : IRequest;
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/UpdateSampleCommandHandler.cs
deleted file mode 100644
index be602020..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Commands/UpdateSampleCommandHandler.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using MediatR;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.API.Application.Commands;
-
-///
-/// EN: Handler for UpdateSampleCommand.
-/// VI: Handler cho UpdateSampleCommand.
-///
-public class UpdateSampleCommandHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
- private readonly ILogger _logger;
-
- public UpdateSampleCommandHandler(
- ISampleRepository sampleRepository,
- ILogger logger)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public async Task 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;
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSampleQuery.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSampleQuery.cs
deleted file mode 100644
index 646a4389..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSampleQuery.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using MediatR;
-
-namespace AdsAnalyticsService.API.Application.Queries;
-
-///
-/// EN: Query to get a Sample by ID.
-/// VI: Query để lấy một Sample theo ID.
-///
-/// EN: Sample ID / VI: ID sample
-public record GetSampleQuery(Guid SampleId) : IRequest;
-
-///
-/// EN: Sample view model for API responses.
-/// VI: Sample view model cho API responses.
-///
-public record SampleViewModel(
- Guid Id,
- string Name,
- string? Description,
- string Status,
- DateTime CreatedAt,
- DateTime? UpdatedAt
-);
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSampleQueryHandler.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSampleQueryHandler.cs
deleted file mode 100644
index 5555c419..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSampleQueryHandler.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using MediatR;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.API.Application.Queries;
-
-///
-/// EN: Handler for GetSampleQuery.
-/// VI: Handler cho GetSampleQuery.
-///
-public class GetSampleQueryHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
-
- public GetSampleQueryHandler(ISampleRepository sampleRepository)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- }
-
- public async Task 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
- );
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSamplesQuery.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSamplesQuery.cs
deleted file mode 100644
index 4ee4e5d5..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSamplesQuery.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using MediatR;
-
-namespace AdsAnalyticsService.API.Application.Queries;
-
-///
-/// EN: Query to get all Samples.
-/// VI: Query để lấy tất cả Samples.
-///
-public record GetSamplesQuery : IRequest>;
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSamplesQueryHandler.cs
deleted file mode 100644
index 970dfa85..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Queries/GetSamplesQueryHandler.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using MediatR;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.API.Application.Queries;
-
-///
-/// EN: Handler for GetSamplesQuery.
-/// VI: Handler cho GetSamplesQuery.
-///
-public class GetSamplesQueryHandler : IRequestHandler>
-{
- private readonly ISampleRepository _sampleRepository;
-
- public GetSamplesQueryHandler(ISampleRepository sampleRepository)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- }
-
- public async Task> 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
- ));
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/CreateSampleCommandValidator.cs
deleted file mode 100644
index ede6c3ff..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/CreateSampleCommandValidator.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using FluentValidation;
-using AdsAnalyticsService.API.Application.Commands;
-
-namespace AdsAnalyticsService.API.Application.Validations;
-
-///
-/// EN: Validator for CreateSampleCommand.
-/// VI: Validator cho CreateSampleCommand.
-///
-public class CreateSampleCommandValidator : AbstractValidator
-{
- 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);
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/UpdateSampleCommandValidator.cs
deleted file mode 100644
index 7a65c46e..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Application/Validations/UpdateSampleCommandValidator.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using FluentValidation;
-using AdsAnalyticsService.API.Application.Commands;
-
-namespace AdsAnalyticsService.API.Application.Validations;
-
-///
-/// EN: Validator for UpdateSampleCommand.
-/// VI: Validator cho UpdateSampleCommand.
-///
-public class UpdateSampleCommandValidator : AbstractValidator
-{
- 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);
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Controllers/SamplesController.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Controllers/SamplesController.cs
deleted file mode 100644
index eec58e4b..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.API/Controllers/SamplesController.cs
+++ /dev/null
@@ -1,200 +0,0 @@
-using Asp.Versioning;
-using MediatR;
-using Microsoft.AspNetCore.Mvc;
-using AdsAnalyticsService.API.Application.Commands;
-using AdsAnalyticsService.API.Application.Queries;
-
-namespace AdsAnalyticsService.API.Controllers;
-
-///
-/// EN: Controller for Sample CRUD operations using CQRS pattern.
-/// VI: Controller cho các thao tác CRUD Sample sử dụng pattern CQRS.
-///
-[ApiController]
-[ApiVersion("1.0")]
-[Route("api/v{version:apiVersion}/[controller]")]
-[Produces("application/json")]
-public class SamplesController : ControllerBase
-{
- private readonly IMediator _mediator;
- private readonly ILogger _logger;
-
- public SamplesController(IMediator mediator, ILogger logger)
- {
- _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- ///
- /// EN: Get all samples.
- /// VI: Lấy tất cả samples.
- ///
- /// EN: List of samples / VI: Danh sách samples
- [HttpGet]
- [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
- public async Task GetSamples()
- {
- var samples = await _mediator.Send(new GetSamplesQuery());
- return Ok(new { success = true, data = samples });
- }
-
- ///
- /// EN: Get a sample by ID.
- /// VI: Lấy một sample theo ID.
- ///
- /// EN: Sample ID / VI: ID sample
- /// EN: Sample details / VI: Chi tiết sample
- [HttpGet("{id:guid}")]
- [ProducesResponseType(typeof(SampleViewModel), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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 });
- }
-
- ///
- /// EN: Create a new sample.
- /// VI: Tạo một sample mới.
- ///
- /// EN: Create request / VI: Request tạo
- /// EN: Created sample ID / VI: ID sample đã tạo
- [HttpPost]
- [ProducesResponseType(typeof(CreateSampleCommandResult), StatusCodes.Status201Created)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task 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 });
- }
-
- ///
- /// EN: Update an existing sample.
- /// VI: Cập nhật một sample đã tồn tại.
- ///
- /// EN: Sample ID / VI: ID sample
- /// EN: Update request / VI: Request cập nhật
- /// EN: Success status / VI: Trạng thái thành công
- [HttpPut("{id:guid}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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" });
- }
-
- ///
- /// EN: Delete a sample.
- /// VI: Xóa một sample.
- ///
- /// EN: Sample ID / VI: ID sample
- /// EN: Success status / VI: Trạng thái thành công
- [HttpDelete("{id:guid}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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();
- }
-
- ///
- /// EN: Change sample status.
- /// VI: Thay đổi trạng thái sample.
- ///
- /// EN: Sample ID / VI: ID sample
- /// EN: Status change request / VI: Request thay đổi trạng thái
- /// EN: Success status / VI: Trạng thái thành công
- [HttpPatch("{id:guid}/status")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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" });
- }
-}
-
-///
-/// EN: Request model for creating a sample.
-/// VI: Model request để tạo sample.
-///
-public record CreateSampleRequest(string Name, string? Description);
-
-///
-/// EN: Request model for updating a sample.
-/// VI: Model request để cập nhật sample.
-///
-public record UpdateSampleRequest(string Name, string? Description);
-
-///
-/// EN: Request model for changing sample status.
-/// VI: Model request để thay đổi trạng thái sample.
-///
-public record ChangeStatusRequest(string Status);
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/MetricsAggregate/CampaignMetrics.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/MetricsAggregate/CampaignMetrics.cs
new file mode 100644
index 00000000..6436e3f9
--- /dev/null
+++ b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/MetricsAggregate/CampaignMetrics.cs
@@ -0,0 +1,47 @@
+using AdsAnalyticsService.Domain.SeedWork;
+
+namespace AdsAnalyticsService.Domain.AggregatesModel.MetricsAggregate;
+
+///
+/// EN: Campaign metrics aggregate root - performance metrics per campaign.
+/// VI: Campaign metrics aggregate root - chỉ số hiệu suất theo campaign.
+///
+public class CampaignMetrics : Entity, IAggregateRoot
+{
+ private Guid _campaignId;
+ private DateTime _date;
+ private long _impressions;
+ private long _clicks;
+ private long _conversions;
+ private decimal _spend;
+ private decimal _revenue;
+
+ public Guid CampaignId => _campaignId;
+ public DateTime Date => _date;
+ public long Impressions => _impressions;
+ public long Clicks => _clicks;
+ public long Conversions => _conversions;
+ public decimal Spend => _spend;
+ public decimal Revenue => _revenue;
+
+ // Calculated metrics
+ public decimal CTR => _impressions > 0 ? (decimal)_clicks / _impressions * 100 : 0;
+ public decimal CPC => _clicks > 0 ? _spend / _clicks : 0;
+ public decimal CPA => _conversions > 0 ? _spend / _conversions : 0;
+ public decimal ROAS => _spend > 0 ? _revenue / _spend : 0;
+
+ protected CampaignMetrics() { }
+
+ public CampaignMetrics(Guid campaignId, DateTime date)
+ {
+ Id = Guid.NewGuid();
+ _campaignId = campaignId;
+ _date = date.Date; // Normalize to start of day
+ }
+
+ public void RecordImpression() => _impressions++;
+ public void RecordClick() => _clicks++;
+ public void RecordConversion() => _conversions++;
+ public void AddSpend(decimal amount) => _spend += amount;
+ public void AddRevenue(decimal amount) => _revenue += amount;
+}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/ReportAggregate/Report.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/ReportAggregate/Report.cs
new file mode 100644
index 00000000..bbcaf77c
--- /dev/null
+++ b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/ReportAggregate/Report.cs
@@ -0,0 +1,65 @@
+using AdsAnalyticsService.Domain.SeedWork;
+
+namespace AdsAnalyticsService.Domain.AggregatesModel.ReportAggregate;
+
+///
+/// EN: Report aggregate root - scheduled or custom reports.
+/// VI: Report aggregate root - báo cáo theo lịch hoặc tùy chỉnh.
+///
+public class Report : Entity, IAggregateRoot
+{
+ private Guid _advertiserId;
+ private string _name = null!;
+ private ReportType _reportType;
+ private DateTime _startDate;
+ private DateTime _endDate;
+ private ReportStatus _status;
+ private string? _dataJson;
+
+ public Guid AdvertiserId => _advertiserId;
+ public string Name => _name;
+ public ReportType ReportType => _reportType;
+ public DateTime StartDate => _startDate;
+ public DateTime EndDate => _endDate;
+ public ReportStatus Status => _status;
+ public string? DataJson => _dataJson;
+ public DateTime CreatedAt { get; private set; }
+
+ protected Report() { }
+
+ public Report(Guid advertiserId, string name, ReportType reportType, DateTime startDate, DateTime endDate)
+ {
+ Id = Guid.NewGuid();
+ _advertiserId = advertiserId;
+ _name = name;
+ _reportType = reportType;
+ _startDate = startDate;
+ _endDate = endDate;
+ _status = ReportStatus.Pending;
+ CreatedAt = DateTime.UtcNow;
+ }
+
+ public void MarkAsProcessing() => _status = ReportStatus.Processing;
+ public void Complete(string dataJson)
+ {
+ _dataJson = dataJson;
+ _status = ReportStatus.Completed;
+ }
+ public void Fail() => _status = ReportStatus.Failed;
+}
+
+public enum ReportType
+{
+ Campaign = 1,
+ AdSet = 2,
+ Ad = 3,
+ Audience = 4
+}
+
+public enum ReportStatus
+{
+ Pending = 1,
+ Processing = 2,
+ Completed = 3,
+ Failed = 4
+}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs
deleted file mode 100644
index 6d19f663..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using AdsAnalyticsService.Domain.SeedWork;
-
-namespace AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-///
-/// EN: Repository interface for Sample aggregate.
-/// VI: Interface repository cho Sample aggregate.
-///
-///
-/// EN: Following repository pattern, this interface defines the contract
-/// for data access operations on Sample aggregate.
-/// VI: Theo pattern repository, interface này định nghĩa contract
-/// cho các thao tác truy cập dữ liệu trên Sample aggregate.
-///
-public interface ISampleRepository : IRepository
-{
- ///
- /// EN: Get a sample by its ID.
- /// VI: Lấy một sample theo ID.
- ///
- /// EN: The sample ID / VI: ID của sample
- /// EN: The sample or null if not found / VI: Sample hoặc null nếu không tìm thấy
- Task GetAsync(Guid sampleId);
-
- ///
- /// EN: Get all samples.
- /// VI: Lấy tất cả samples.
- ///
- /// EN: List of samples / VI: Danh sách samples
- Task> GetAllAsync();
-
- ///
- /// EN: Add a new sample.
- /// VI: Thêm một sample mới.
- ///
- /// EN: The sample to add / VI: Sample cần thêm
- /// EN: The added sample / VI: Sample đã thêm
- Sample Add(Sample sample);
-
- ///
- /// EN: Update an existing sample.
- /// VI: Cập nhật một sample đã tồn tại.
- ///
- /// EN: The sample to update / VI: Sample cần cập nhật
- void Update(Sample sample);
-
- ///
- /// EN: Delete a sample.
- /// VI: Xóa một sample.
- ///
- /// EN: The sample to delete / VI: Sample cần xóa
- void Delete(Sample sample);
-
- ///
- /// EN: Get samples by status.
- /// VI: Lấy samples theo trạng thái.
- ///
- /// EN: The status ID / VI: ID trạng thái
- /// EN: List of samples with given status / VI: Danh sách samples với trạng thái cho trước
- Task> GetByStatusAsync(int statusId);
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/Sample.cs
deleted file mode 100644
index e2713c2b..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/Sample.cs
+++ /dev/null
@@ -1,158 +0,0 @@
-using AdsAnalyticsService.Domain.Events;
-using AdsAnalyticsService.Domain.Exceptions;
-using AdsAnalyticsService.Domain.SeedWork;
-
-namespace AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-///
-/// EN: Sample aggregate root demonstrating DDD patterns.
-/// VI: Sample aggregate root minh họa các pattern DDD.
-///
-public class Sample : Entity, IAggregateRoot
-{
- // EN: Private fields for encapsulation
- // VI: Fields private để đóng gói
- private string _name = null!;
- private string? _description;
- private SampleStatus _status = null!;
- private DateTime _createdAt;
- private DateTime? _updatedAt;
-
- ///
- /// EN: Sample name (required).
- /// VI: Tên sample (bắt buộc).
- ///
- public string Name => _name;
-
- ///
- /// EN: Optional description.
- /// VI: Mô tả tùy chọn.
- ///
- public string? Description => _description;
-
- ///
- /// EN: Current status.
- /// VI: Trạng thái hiện tại.
- ///
- public SampleStatus Status => _status;
-
- ///
- /// EN: Status ID for EF Core mapping.
- /// VI: ID trạng thái cho EF Core mapping.
- ///
- public int StatusId { get; private set; }
-
- ///
- /// EN: Creation timestamp.
- /// VI: Thời gian tạo.
- ///
- public DateTime CreatedAt => _createdAt;
-
- ///
- /// EN: Last update timestamp.
- /// VI: Thời gian cập nhật cuối.
- ///
- public DateTime? UpdatedAt => _updatedAt;
-
- ///
- /// EN: Private constructor for EF Core.
- /// VI: Constructor private cho EF Core.
- ///
- protected Sample()
- {
- }
-
- ///
- /// EN: Create a new Sample with required information.
- /// VI: Tạo một Sample mới với thông tin bắt buộc.
- ///
- /// EN: Sample name / VI: Tên sample
- /// EN: Optional description / VI: Mô tả tùy chọn
- public Sample(string name, string? description = null) : this()
- {
- if (string.IsNullOrWhiteSpace(name))
- throw new SampleDomainException("Sample name cannot be empty");
-
- Id = Guid.NewGuid();
- _name = name;
- _description = description;
- _status = SampleStatus.Draft;
- StatusId = SampleStatus.Draft.Id;
- _createdAt = DateTime.UtcNow;
-
- // EN: Add domain event for creation
- // VI: Thêm domain event cho việc tạo
- AddDomainEvent(new SampleCreatedDomainEvent(this));
- }
-
- ///
- /// EN: Update sample information.
- /// VI: Cập nhật thông tin sample.
- ///
- public void Update(string name, string? description)
- {
- if (string.IsNullOrWhiteSpace(name))
- throw new SampleDomainException("Sample name cannot be empty");
-
- if (_status == SampleStatus.Cancelled)
- throw new SampleDomainException("Cannot update a cancelled sample");
-
- _name = name;
- _description = description;
- _updatedAt = DateTime.UtcNow;
- }
-
- ///
- /// EN: Activate the sample.
- /// VI: Kích hoạt sample.
- ///
- public void Activate()
- {
- if (_status != SampleStatus.Draft)
- throw new SampleDomainException("Only draft samples can be activated");
-
- var previousStatus = _status;
- _status = SampleStatus.Active;
- StatusId = SampleStatus.Active.Id;
- _updatedAt = DateTime.UtcNow;
-
- AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
- }
-
- ///
- /// EN: Complete the sample.
- /// VI: Hoàn thành sample.
- ///
- public void Complete()
- {
- if (_status != SampleStatus.Active)
- throw new SampleDomainException("Only active samples can be completed");
-
- var previousStatus = _status;
- _status = SampleStatus.Completed;
- StatusId = SampleStatus.Completed.Id;
- _updatedAt = DateTime.UtcNow;
-
- AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
- }
-
- ///
- /// EN: Cancel the sample.
- /// VI: Hủy sample.
- ///
- public void Cancel()
- {
- if (_status == SampleStatus.Completed)
- throw new SampleDomainException("Cannot cancel a completed sample");
-
- if (_status == SampleStatus.Cancelled)
- throw new SampleDomainException("Sample is already cancelled");
-
- var previousStatus = _status;
- _status = SampleStatus.Cancelled;
- StatusId = SampleStatus.Cancelled.Id;
- _updatedAt = DateTime.UtcNow;
-
- AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs
deleted file mode 100644
index defbccd0..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using AdsAnalyticsService.Domain.SeedWork;
-
-namespace AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-///
-/// EN: Sample status enumeration following type-safe enum pattern.
-/// VI: Enumeration trạng thái Sample theo pattern enum an toàn kiểu.
-///
-public class SampleStatus : Enumeration
-{
- ///
- /// EN: Draft status - initial state
- /// VI: Trạng thái nháp - trạng thái ban đầu
- ///
- public static SampleStatus Draft = new(1, nameof(Draft));
-
- ///
- /// EN: Active status - ready for use
- /// VI: Trạng thái hoạt động - sẵn sàng sử dụng
- ///
- public static SampleStatus Active = new(2, nameof(Active));
-
- ///
- /// EN: Completed status - finished processing
- /// VI: Trạng thái hoàn thành - đã xử lý xong
- ///
- public static SampleStatus Completed = new(3, nameof(Completed));
-
- ///
- /// EN: Cancelled status - cancelled by user
- /// VI: Trạng thái đã hủy - bị hủy bởi người dùng
- ///
- public static SampleStatus Cancelled = new(4, nameof(Cancelled));
-
- public SampleStatus(int id, string name) : base(id, name)
- {
- }
-
- ///
- /// EN: Get all available statuses.
- /// VI: Lấy tất cả các trạng thái có sẵn.
- ///
- public static IEnumerable List() => GetAll();
-
- ///
- /// EN: Parse status from name.
- /// VI: Parse trạng thái từ tên.
- ///
- public static SampleStatus FromName(string name)
- {
- var status = List().SingleOrDefault(s =>
- string.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase));
-
- if (status is null)
- {
- throw new ArgumentException($"Possible values for SampleStatus: {string.Join(",", List().Select(s => s.Name))}");
- }
-
- return status;
- }
-
- ///
- /// EN: Parse status from ID.
- /// VI: Parse trạng thái từ ID.
- ///
- public static SampleStatus From(int id)
- {
- var status = List().SingleOrDefault(s => s.Id == id);
-
- if (status is null)
- {
- throw new ArgumentException($"Possible values for SampleStatus: {string.Join(",", List().Select(s => s.Name))}");
- }
-
- return status;
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Events/SampleCreatedDomainEvent.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Events/SampleCreatedDomainEvent.cs
deleted file mode 100644
index e543be73..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Events/SampleCreatedDomainEvent.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using MediatR;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.Domain.Events;
-
-///
-/// 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.
-///
-public class SampleCreatedDomainEvent : INotification
-{
- ///
- /// EN: The newly created sample.
- /// VI: Sample mới được tạo.
- ///
- public Sample Sample { get; }
-
- public SampleCreatedDomainEvent(Sample sample)
- {
- Sample = sample;
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Events/SampleStatusChangedDomainEvent.cs
deleted file mode 100644
index 70f70464..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Events/SampleStatusChangedDomainEvent.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using MediatR;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.Domain.Events;
-
-///
-/// EN: Domain event raised when Sample status changes.
-/// VI: Domain event được phát ra khi trạng thái Sample thay đổi.
-///
-public class SampleStatusChangedDomainEvent : INotification
-{
- ///
- /// EN: The sample ID.
- /// VI: ID của sample.
- ///
- public Guid SampleId { get; }
-
- ///
- /// EN: Previous status before the change.
- /// VI: Trạng thái trước khi thay đổi.
- ///
- public SampleStatus PreviousStatus { get; }
-
- ///
- /// EN: New status after the change.
- /// VI: Trạng thái mới sau khi thay đổi.
- ///
- public SampleStatus NewStatus { get; }
-
- public SampleStatusChangedDomainEvent(
- Guid sampleId,
- SampleStatus previousStatus,
- SampleStatus newStatus)
- {
- SampleId = sampleId;
- PreviousStatus = previousStatus;
- NewStatus = newStatus;
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Exceptions/AdsAnalyticsDomainException.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Exceptions/AdsAnalyticsDomainException.cs
new file mode 100644
index 00000000..fceb01df
--- /dev/null
+++ b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Exceptions/AdsAnalyticsDomainException.cs
@@ -0,0 +1,8 @@
+namespace AdsAnalyticsService.Domain.Exceptions;
+
+public class AdsAnalyticsDomainException : Exception
+{
+ public AdsAnalyticsDomainException() { }
+ public AdsAnalyticsDomainException(string message) : base(message) { }
+ public AdsAnalyticsDomainException(string message, Exception innerException) : base(message, innerException) { }
+}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Exceptions/SampleDomainException.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Exceptions/SampleDomainException.cs
deleted file mode 100644
index 7536168b..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Domain/Exceptions/SampleDomainException.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace AdsAnalyticsService.Domain.Exceptions;
-
-///
-/// EN: Exception for Sample aggregate domain errors.
-/// VI: Exception cho các lỗi domain của Sample aggregate.
-///
-public class SampleDomainException : DomainException
-{
- public SampleDomainException()
- {
- }
-
- public SampleDomainException(string message) : base(message)
- {
- }
-
- public SampleDomainException(string message, Exception innerException)
- : base(message, innerException)
- {
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/MyServiceContext.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/AdsAnalyticsServiceContext.cs
similarity index 100%
rename from services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/MyServiceContext.cs
rename to services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/AdsAnalyticsServiceContext.cs
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/DependencyInjection.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/DependencyInjection.cs
index 3d6f69e9..669242df 100644
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/DependencyInjection.cs
+++ b/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/DependencyInjection.cs
@@ -1,27 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
using AdsAnalyticsService.Infrastructure.Idempotency;
-using AdsAnalyticsService.Infrastructure.Repositories;
namespace AdsAnalyticsService.Infrastructure;
-///
-/// EN: Dependency injection extensions for Infrastructure layer.
-/// VI: Extensions dependency injection cho lớp Infrastructure.
-///
public static class DependencyInjection
{
- ///
- /// EN: Add infrastructure services to the DI container.
- /// VI: Thêm các services infrastructure vào DI container.
- ///
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
- // EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL
services.AddDbContext(options =>
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
@@ -31,14 +20,9 @@ public static class DependencyInjection
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.MigrationsAssembly(typeof(AdsAnalyticsServiceContext).Assembly.FullName);
- npgsqlOptions.EnableRetryOnFailure(
- maxRetryCount: 5,
- maxRetryDelay: TimeSpan.FromSeconds(30),
- errorCodesToAdd: null);
+ npgsqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(30), 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,12 +30,7 @@ public static class DependencyInjection
}
});
- // EN: Register repositories / VI: Đăng ký repositories
- services.AddScoped();
-
- // EN: Register idempotency services / VI: Đăng ký idempotency services
services.AddScoped();
-
return services;
}
}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs
deleted file mode 100644
index 2cb0457e..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.Infrastructure.EntityConfigurations;
-
-///
-/// EN: EF Core configuration for Sample entity.
-/// VI: Cấu hình EF Core cho entity Sample.
-///
-public class SampleEntityTypeConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder 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("_name")
- .HasColumnName("name")
- .HasMaxLength(200)
- .IsRequired();
-
- builder.Property("_description")
- .HasColumnName("description")
- .HasMaxLength(1000);
-
- builder.Property("_createdAt")
- .HasColumnName("created_at")
- .IsRequired();
-
- builder.Property("_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");
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs
deleted file mode 100644
index 7d3654b2..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsAnalyticsService.Infrastructure.EntityConfigurations;
-
-///
-/// EN: EF Core configuration for SampleStatus enumeration.
-/// VI: Cấu hình EF Core cho enumeration SampleStatus.
-///
-public class SampleStatusEntityTypeConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder 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
- );
- }
-}
diff --git a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/Repositories/SampleRepository.cs b/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/Repositories/SampleRepository.cs
deleted file mode 100644
index 8b3dfc29..00000000
--- a/services/ads-analytics-service-net/src/AdsAnalyticsService.Infrastructure/Repositories/SampleRepository.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-using AdsAnalyticsService.Domain.SeedWork;
-
-namespace AdsAnalyticsService.Infrastructure.Repositories;
-
-///
-/// EN: Repository implementation for Sample aggregate.
-/// VI: Triển khai repository cho Sample aggregate.
-///
-public class SampleRepository : ISampleRepository
-{
- private readonly AdsAnalyticsServiceContext _context;
-
- ///
- /// EN: Unit of work for transaction management.
- /// VI: Unit of work cho quản lý transaction.
- ///
- public IUnitOfWork UnitOfWork => _context;
-
- public SampleRepository(AdsAnalyticsServiceContext context)
- {
- _context = context ?? throw new ArgumentNullException(nameof(context));
- }
-
- ///
- public async Task GetAsync(Guid sampleId)
- {
- var sample = await _context.Samples
- .Include(s => s.Status)
- .FirstOrDefaultAsync(s => s.Id == sampleId);
-
- return sample;
- }
-
- ///
- public async Task> GetAllAsync()
- {
- return await _context.Samples
- .Include(s => s.Status)
- .OrderByDescending(s => s.CreatedAt)
- .ToListAsync();
- }
-
- ///
- public Sample Add(Sample sample)
- {
- return _context.Samples.Add(sample).Entity;
- }
-
- ///
- public void Update(Sample sample)
- {
- _context.Entry(sample).State = EntityState.Modified;
- }
-
- ///
- public void Delete(Sample sample)
- {
- _context.Samples.Remove(sample);
- }
-
- ///
- public async Task> GetByStatusAsync(int statusId)
- {
- return await _context.Samples
- .Include(s => s.Status)
- .Where(s => s.StatusId == statusId)
- .OrderByDescending(s => s.CreatedAt)
- .ToListAsync();
- }
-}
diff --git a/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Application/CreateSampleCommandHandlerTests.cs
deleted file mode 100644
index 1d9b304c..00000000
--- a/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Application/CreateSampleCommandHandlerTests.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using FluentAssertions;
-using Microsoft.Extensions.Logging;
-using Moq;
-using AdsAnalyticsService.API.Application.Commands;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-using AdsAnalyticsService.Domain.SeedWork;
-using Xunit;
-
-namespace AdsAnalyticsService.UnitTests.Application;
-
-///
-/// EN: Unit tests for CreateSampleCommandHandler.
-/// VI: Unit tests cho CreateSampleCommandHandler.
-///
-public class CreateSampleCommandHandlerTests
-{
- private readonly Mock _mockRepository;
- private readonly Mock> _mockLogger;
- private readonly CreateSampleCommandHandler _handler;
-
- public CreateSampleCommandHandlerTests()
- {
- _mockRepository = new Mock();
- _mockLogger = new Mock>();
-
- var mockUnitOfWork = new Mock();
- mockUnitOfWork.Setup(u => u.SaveEntitiesAsync(It.IsAny()))
- .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()))
- .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()), 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()), Times.Once);
- }
-}
diff --git a/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Domain/SampleAggregateTests.cs b/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Domain/SampleAggregateTests.cs
deleted file mode 100644
index 7c5508d8..00000000
--- a/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Domain/SampleAggregateTests.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-using FluentAssertions;
-using AdsAnalyticsService.Domain.AggregatesModel.SampleAggregate;
-using AdsAnalyticsService.Domain.Exceptions;
-using Xunit;
-
-namespace AdsAnalyticsService.UnitTests.Domain;
-
-///
-/// EN: Unit tests for Sample aggregate.
-/// VI: Unit tests cho Sample aggregate.
-///
-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()
- .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()
- .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()
- .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()
- .WithMessage("Cannot update a cancelled sample");
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/ChangeSampleStatusCommand.cs
deleted file mode 100644
index fbc0375c..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/ChangeSampleStatusCommand.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using MediatR;
-
-namespace AdsTrackingService.API.Application.Commands;
-
-///
-/// EN: Command to change status of a Sample.
-/// VI: Command để thay đổi trạng thái của Sample.
-///
-/// EN: Sample ID / VI: ID sample
-/// EN: New status (activate, complete, cancel) / VI: Trạng thái mới (activate, complete, cancel)
-public record ChangeSampleStatusCommand(
- Guid SampleId,
- string NewStatus
-) : IRequest;
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs
deleted file mode 100644
index 84578ef5..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using MediatR;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.API.Application.Commands;
-
-///
-/// EN: Handler for ChangeSampleStatusCommand.
-/// VI: Handler cho ChangeSampleStatusCommand.
-///
-public class ChangeSampleStatusCommandHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
- private readonly ILogger _logger;
-
- public ChangeSampleStatusCommandHandler(
- ISampleRepository sampleRepository,
- ILogger logger)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public async Task 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;
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/CreateSampleCommand.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/CreateSampleCommand.cs
deleted file mode 100644
index 1826eedd..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/CreateSampleCommand.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using MediatR;
-
-namespace AdsTrackingService.API.Application.Commands;
-
-///
-/// EN: Command to create a new Sample.
-/// VI: Command để tạo một Sample mới.
-///
-/// EN: Sample name / VI: Tên sample
-/// EN: Optional description / VI: Mô tả tùy chọn
-public record CreateSampleCommand(
- string Name,
- string? Description
-) : IRequest;
-
-///
-/// EN: Result of CreateSampleCommand.
-/// VI: Kết quả của CreateSampleCommand.
-///
-/// EN: Created sample ID / VI: ID sample đã tạo
-public record CreateSampleCommandResult(Guid Id);
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/CreateSampleCommandHandler.cs
deleted file mode 100644
index 2638f4c9..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/CreateSampleCommandHandler.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using MediatR;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.API.Application.Commands;
-
-///
-/// EN: Handler for CreateSampleCommand.
-/// VI: Handler cho CreateSampleCommand.
-///
-public class CreateSampleCommandHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
- private readonly ILogger _logger;
-
- public CreateSampleCommandHandler(
- ISampleRepository sampleRepository,
- ILogger logger)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public async Task 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);
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/DeleteSampleCommand.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/DeleteSampleCommand.cs
deleted file mode 100644
index 604b8915..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/DeleteSampleCommand.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using MediatR;
-
-namespace AdsTrackingService.API.Application.Commands;
-
-///
-/// EN: Command to delete a Sample.
-/// VI: Command để xóa một Sample.
-///
-/// EN: Sample ID to delete / VI: ID sample cần xóa
-public record DeleteSampleCommand(Guid SampleId) : IRequest;
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/DeleteSampleCommandHandler.cs
deleted file mode 100644
index 5ecc8b40..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/DeleteSampleCommandHandler.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using MediatR;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.API.Application.Commands;
-
-///
-/// EN: Handler for DeleteSampleCommand.
-/// VI: Handler cho DeleteSampleCommand.
-///
-public class DeleteSampleCommandHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
- private readonly ILogger _logger;
-
- public DeleteSampleCommandHandler(
- ISampleRepository sampleRepository,
- ILogger logger)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public async Task 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;
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/UpdateSampleCommand.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/UpdateSampleCommand.cs
deleted file mode 100644
index 02268ec4..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/UpdateSampleCommand.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using MediatR;
-
-namespace AdsTrackingService.API.Application.Commands;
-
-///
-/// EN: Command to update an existing Sample.
-/// VI: Command để cập nhật một Sample đã tồn tại.
-///
-/// EN: Sample ID to update / VI: ID sample cần cập nhật
-/// EN: New name / VI: Tên mới
-/// EN: New description / VI: Mô tả mới
-public record UpdateSampleCommand(
- Guid SampleId,
- string Name,
- string? Description
-) : IRequest;
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/UpdateSampleCommandHandler.cs
deleted file mode 100644
index a58f5b98..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Commands/UpdateSampleCommandHandler.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using MediatR;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.API.Application.Commands;
-
-///
-/// EN: Handler for UpdateSampleCommand.
-/// VI: Handler cho UpdateSampleCommand.
-///
-public class UpdateSampleCommandHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
- private readonly ILogger _logger;
-
- public UpdateSampleCommandHandler(
- ISampleRepository sampleRepository,
- ILogger logger)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public async Task 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;
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSampleQuery.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSampleQuery.cs
deleted file mode 100644
index 192600d1..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSampleQuery.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using MediatR;
-
-namespace AdsTrackingService.API.Application.Queries;
-
-///
-/// EN: Query to get a Sample by ID.
-/// VI: Query để lấy một Sample theo ID.
-///
-/// EN: Sample ID / VI: ID sample
-public record GetSampleQuery(Guid SampleId) : IRequest;
-
-///
-/// EN: Sample view model for API responses.
-/// VI: Sample view model cho API responses.
-///
-public record SampleViewModel(
- Guid Id,
- string Name,
- string? Description,
- string Status,
- DateTime CreatedAt,
- DateTime? UpdatedAt
-);
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSampleQueryHandler.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSampleQueryHandler.cs
deleted file mode 100644
index 68a5f0e3..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSampleQueryHandler.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using MediatR;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.API.Application.Queries;
-
-///
-/// EN: Handler for GetSampleQuery.
-/// VI: Handler cho GetSampleQuery.
-///
-public class GetSampleQueryHandler : IRequestHandler
-{
- private readonly ISampleRepository _sampleRepository;
-
- public GetSampleQueryHandler(ISampleRepository sampleRepository)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- }
-
- public async Task 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
- );
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSamplesQuery.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSamplesQuery.cs
deleted file mode 100644
index e5fc8e8b..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSamplesQuery.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using MediatR;
-
-namespace AdsTrackingService.API.Application.Queries;
-
-///
-/// EN: Query to get all Samples.
-/// VI: Query để lấy tất cả Samples.
-///
-public record GetSamplesQuery : IRequest>;
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSamplesQueryHandler.cs
deleted file mode 100644
index 9113dc44..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Queries/GetSamplesQueryHandler.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using MediatR;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.API.Application.Queries;
-
-///
-/// EN: Handler for GetSamplesQuery.
-/// VI: Handler cho GetSamplesQuery.
-///
-public class GetSamplesQueryHandler : IRequestHandler>
-{
- private readonly ISampleRepository _sampleRepository;
-
- public GetSamplesQueryHandler(ISampleRepository sampleRepository)
- {
- _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
- }
-
- public async Task> 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
- ));
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/CreateSampleCommandValidator.cs
deleted file mode 100644
index a5d89a5d..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/CreateSampleCommandValidator.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using FluentValidation;
-using AdsTrackingService.API.Application.Commands;
-
-namespace AdsTrackingService.API.Application.Validations;
-
-///
-/// EN: Validator for CreateSampleCommand.
-/// VI: Validator cho CreateSampleCommand.
-///
-public class CreateSampleCommandValidator : AbstractValidator
-{
- 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);
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/UpdateSampleCommandValidator.cs
deleted file mode 100644
index c6fc4380..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Application/Validations/UpdateSampleCommandValidator.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using FluentValidation;
-using AdsTrackingService.API.Application.Commands;
-
-namespace AdsTrackingService.API.Application.Validations;
-
-///
-/// EN: Validator for UpdateSampleCommand.
-/// VI: Validator cho UpdateSampleCommand.
-///
-public class UpdateSampleCommandValidator : AbstractValidator
-{
- 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);
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/SamplesController.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/SamplesController.cs
deleted file mode 100644
index 032734df..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/SamplesController.cs
+++ /dev/null
@@ -1,200 +0,0 @@
-using Asp.Versioning;
-using MediatR;
-using Microsoft.AspNetCore.Mvc;
-using AdsTrackingService.API.Application.Commands;
-using AdsTrackingService.API.Application.Queries;
-
-namespace AdsTrackingService.API.Controllers;
-
-///
-/// EN: Controller for Sample CRUD operations using CQRS pattern.
-/// VI: Controller cho các thao tác CRUD Sample sử dụng pattern CQRS.
-///
-[ApiController]
-[ApiVersion("1.0")]
-[Route("api/v{version:apiVersion}/[controller]")]
-[Produces("application/json")]
-public class SamplesController : ControllerBase
-{
- private readonly IMediator _mediator;
- private readonly ILogger _logger;
-
- public SamplesController(IMediator mediator, ILogger logger)
- {
- _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- ///
- /// EN: Get all samples.
- /// VI: Lấy tất cả samples.
- ///
- /// EN: List of samples / VI: Danh sách samples
- [HttpGet]
- [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
- public async Task GetSamples()
- {
- var samples = await _mediator.Send(new GetSamplesQuery());
- return Ok(new { success = true, data = samples });
- }
-
- ///
- /// EN: Get a sample by ID.
- /// VI: Lấy một sample theo ID.
- ///
- /// EN: Sample ID / VI: ID sample
- /// EN: Sample details / VI: Chi tiết sample
- [HttpGet("{id:guid}")]
- [ProducesResponseType(typeof(SampleViewModel), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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 });
- }
-
- ///
- /// EN: Create a new sample.
- /// VI: Tạo một sample mới.
- ///
- /// EN: Create request / VI: Request tạo
- /// EN: Created sample ID / VI: ID sample đã tạo
- [HttpPost]
- [ProducesResponseType(typeof(CreateSampleCommandResult), StatusCodes.Status201Created)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task 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 });
- }
-
- ///
- /// EN: Update an existing sample.
- /// VI: Cập nhật một sample đã tồn tại.
- ///
- /// EN: Sample ID / VI: ID sample
- /// EN: Update request / VI: Request cập nhật
- /// EN: Success status / VI: Trạng thái thành công
- [HttpPut("{id:guid}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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" });
- }
-
- ///
- /// EN: Delete a sample.
- /// VI: Xóa một sample.
- ///
- /// EN: Sample ID / VI: ID sample
- /// EN: Success status / VI: Trạng thái thành công
- [HttpDelete("{id:guid}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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();
- }
-
- ///
- /// EN: Change sample status.
- /// VI: Thay đổi trạng thái sample.
- ///
- /// EN: Sample ID / VI: ID sample
- /// EN: Status change request / VI: Request thay đổi trạng thái
- /// EN: Success status / VI: Trạng thái thành công
- [HttpPatch("{id:guid}/status")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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" });
- }
-}
-
-///
-/// EN: Request model for creating a sample.
-/// VI: Model request để tạo sample.
-///
-public record CreateSampleRequest(string Name, string? Description);
-
-///
-/// EN: Request model for updating a sample.
-/// VI: Model request để cập nhật sample.
-///
-public record UpdateSampleRequest(string Name, string? Description);
-
-///
-/// EN: Request model for changing sample status.
-/// VI: Model request để thay đổi trạng thái sample.
-///
-public record ChangeStatusRequest(string Status);
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/AttributionAggregate/Attribution.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/AttributionAggregate/Attribution.cs
new file mode 100644
index 00000000..46247538
--- /dev/null
+++ b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/AttributionAggregate/Attribution.cs
@@ -0,0 +1,49 @@
+using AdsTrackingService.Domain.SeedWork;
+
+namespace AdsTrackingService.Domain.AggregatesModel.AttributionAggregate;
+
+///
+/// EN: Attribution aggregate root - links conversions to ads.
+/// VI: Attribution aggregate root - liên kết conversion với quảng cáo.
+///
+public class Attribution : Entity, IAggregateRoot
+{
+ private Guid _conversionId;
+ private Guid _adId;
+ private Guid _campaignId;
+ private AttributionModel _model;
+ private decimal _attributedValue;
+ private DateTime _attributedAt;
+
+ public Guid ConversionId => _conversionId;
+ public Guid AdId => _adId;
+ public Guid CampaignId => _campaignId;
+ public AttributionModel Model => _model;
+ public decimal AttributedValue => _attributedValue;
+ public DateTime AttributedAt => _attributedAt;
+
+ protected Attribution() { }
+
+ public Attribution(Guid conversionId, Guid adId, Guid campaignId, AttributionModel model, decimal attributedValue)
+ {
+ Id = Guid.NewGuid();
+ _conversionId = conversionId;
+ _adId = adId;
+ _campaignId = campaignId;
+ _model = model;
+ _attributedValue = attributedValue;
+ _attributedAt = DateTime.UtcNow;
+ }
+}
+
+///
+/// EN: Attribution model enumeration.
+/// VI: Enum mô hình attribution.
+///
+public enum AttributionModel
+{
+ LastClick = 1,
+ FirstClick = 2,
+ Linear = 3,
+ TimeDecay = 4
+}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/ConversionAggregate/Conversion.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/ConversionAggregate/Conversion.cs
new file mode 100644
index 00000000..ece2308b
--- /dev/null
+++ b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/ConversionAggregate/Conversion.cs
@@ -0,0 +1,49 @@
+using AdsTrackingService.Domain.SeedWork;
+
+namespace AdsTrackingService.Domain.AggregatesModel.ConversionAggregate;
+
+///
+/// EN: Conversion aggregate root - tracks conversion events.
+/// VI: Conversion aggregate root - theo dõi sự kiện conversion.
+///
+public class Conversion : Entity, IAggregateRoot
+{
+ private Guid _advertiserId;
+ private Guid _campaignId;
+ private Guid _userId;
+ private string _conversionType = null!;
+ private decimal _conversionValue;
+ private string _currency;
+ private DateTime _conversionTime;
+
+ public Guid AdvertiserId => _advertiserId;
+ public Guid CampaignId => _campaignId;
+ public Guid UserId => _userId;
+ public string ConversionType => _conversionType;
+ public decimal ConversionValue => _conversionValue;
+ public string Currency => _currency;
+ public DateTime ConversionTime => _conversionTime;
+
+ protected Conversion()
+ {
+ _currency = "VND";
+ }
+
+ public Conversion(Guid advertiserId, Guid campaignId, Guid userId, string conversionType, decimal conversionValue, string currency = "VND")
+ {
+ Id = Guid.NewGuid();
+ _advertiserId = advertiserId;
+ _campaignId = campaignId;
+ _userId = userId;
+ _conversionType = conversionType;
+ _conversionValue = conversionValue;
+ _currency = currency;
+ _conversionTime = DateTime.UtcNow;
+ }
+
+ public static Conversion Purchase(Guid advertiserId, Guid campaignId, Guid userId, decimal amount) =>
+ new(advertiserId, campaignId, userId, "purchase", amount);
+
+ public static Conversion Lead(Guid advertiserId, Guid campaignId, Guid userId) =>
+ new(advertiserId, campaignId, userId, "lead", 0);
+}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs
deleted file mode 100644
index 106654bb..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using AdsTrackingService.Domain.SeedWork;
-
-namespace AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-///
-/// EN: Repository interface for Sample aggregate.
-/// VI: Interface repository cho Sample aggregate.
-///
-///
-/// EN: Following repository pattern, this interface defines the contract
-/// for data access operations on Sample aggregate.
-/// VI: Theo pattern repository, interface này định nghĩa contract
-/// cho các thao tác truy cập dữ liệu trên Sample aggregate.
-///
-public interface ISampleRepository : IRepository
-{
- ///
- /// EN: Get a sample by its ID.
- /// VI: Lấy một sample theo ID.
- ///
- /// EN: The sample ID / VI: ID của sample
- /// EN: The sample or null if not found / VI: Sample hoặc null nếu không tìm thấy
- Task GetAsync(Guid sampleId);
-
- ///
- /// EN: Get all samples.
- /// VI: Lấy tất cả samples.
- ///
- /// EN: List of samples / VI: Danh sách samples
- Task> GetAllAsync();
-
- ///
- /// EN: Add a new sample.
- /// VI: Thêm một sample mới.
- ///
- /// EN: The sample to add / VI: Sample cần thêm
- /// EN: The added sample / VI: Sample đã thêm
- Sample Add(Sample sample);
-
- ///
- /// EN: Update an existing sample.
- /// VI: Cập nhật một sample đã tồn tại.
- ///
- /// EN: The sample to update / VI: Sample cần cập nhật
- void Update(Sample sample);
-
- ///
- /// EN: Delete a sample.
- /// VI: Xóa một sample.
- ///
- /// EN: The sample to delete / VI: Sample cần xóa
- void Delete(Sample sample);
-
- ///
- /// EN: Get samples by status.
- /// VI: Lấy samples theo trạng thái.
- ///
- /// EN: The status ID / VI: ID trạng thái
- /// EN: List of samples with given status / VI: Danh sách samples với trạng thái cho trước
- Task> GetByStatusAsync(int statusId);
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/Sample.cs
deleted file mode 100644
index fe8efbf8..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/Sample.cs
+++ /dev/null
@@ -1,158 +0,0 @@
-using AdsTrackingService.Domain.Events;
-using AdsTrackingService.Domain.Exceptions;
-using AdsTrackingService.Domain.SeedWork;
-
-namespace AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-///
-/// EN: Sample aggregate root demonstrating DDD patterns.
-/// VI: Sample aggregate root minh họa các pattern DDD.
-///
-public class Sample : Entity, IAggregateRoot
-{
- // EN: Private fields for encapsulation
- // VI: Fields private để đóng gói
- private string _name = null!;
- private string? _description;
- private SampleStatus _status = null!;
- private DateTime _createdAt;
- private DateTime? _updatedAt;
-
- ///
- /// EN: Sample name (required).
- /// VI: Tên sample (bắt buộc).
- ///
- public string Name => _name;
-
- ///
- /// EN: Optional description.
- /// VI: Mô tả tùy chọn.
- ///
- public string? Description => _description;
-
- ///
- /// EN: Current status.
- /// VI: Trạng thái hiện tại.
- ///
- public SampleStatus Status => _status;
-
- ///
- /// EN: Status ID for EF Core mapping.
- /// VI: ID trạng thái cho EF Core mapping.
- ///
- public int StatusId { get; private set; }
-
- ///
- /// EN: Creation timestamp.
- /// VI: Thời gian tạo.
- ///
- public DateTime CreatedAt => _createdAt;
-
- ///
- /// EN: Last update timestamp.
- /// VI: Thời gian cập nhật cuối.
- ///
- public DateTime? UpdatedAt => _updatedAt;
-
- ///
- /// EN: Private constructor for EF Core.
- /// VI: Constructor private cho EF Core.
- ///
- protected Sample()
- {
- }
-
- ///
- /// EN: Create a new Sample with required information.
- /// VI: Tạo một Sample mới với thông tin bắt buộc.
- ///
- /// EN: Sample name / VI: Tên sample
- /// EN: Optional description / VI: Mô tả tùy chọn
- public Sample(string name, string? description = null) : this()
- {
- if (string.IsNullOrWhiteSpace(name))
- throw new SampleDomainException("Sample name cannot be empty");
-
- Id = Guid.NewGuid();
- _name = name;
- _description = description;
- _status = SampleStatus.Draft;
- StatusId = SampleStatus.Draft.Id;
- _createdAt = DateTime.UtcNow;
-
- // EN: Add domain event for creation
- // VI: Thêm domain event cho việc tạo
- AddDomainEvent(new SampleCreatedDomainEvent(this));
- }
-
- ///
- /// EN: Update sample information.
- /// VI: Cập nhật thông tin sample.
- ///
- public void Update(string name, string? description)
- {
- if (string.IsNullOrWhiteSpace(name))
- throw new SampleDomainException("Sample name cannot be empty");
-
- if (_status == SampleStatus.Cancelled)
- throw new SampleDomainException("Cannot update a cancelled sample");
-
- _name = name;
- _description = description;
- _updatedAt = DateTime.UtcNow;
- }
-
- ///
- /// EN: Activate the sample.
- /// VI: Kích hoạt sample.
- ///
- public void Activate()
- {
- if (_status != SampleStatus.Draft)
- throw new SampleDomainException("Only draft samples can be activated");
-
- var previousStatus = _status;
- _status = SampleStatus.Active;
- StatusId = SampleStatus.Active.Id;
- _updatedAt = DateTime.UtcNow;
-
- AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
- }
-
- ///
- /// EN: Complete the sample.
- /// VI: Hoàn thành sample.
- ///
- public void Complete()
- {
- if (_status != SampleStatus.Active)
- throw new SampleDomainException("Only active samples can be completed");
-
- var previousStatus = _status;
- _status = SampleStatus.Completed;
- StatusId = SampleStatus.Completed.Id;
- _updatedAt = DateTime.UtcNow;
-
- AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
- }
-
- ///
- /// EN: Cancel the sample.
- /// VI: Hủy sample.
- ///
- public void Cancel()
- {
- if (_status == SampleStatus.Completed)
- throw new SampleDomainException("Cannot cancel a completed sample");
-
- if (_status == SampleStatus.Cancelled)
- throw new SampleDomainException("Sample is already cancelled");
-
- var previousStatus = _status;
- _status = SampleStatus.Cancelled;
- StatusId = SampleStatus.Cancelled.Id;
- _updatedAt = DateTime.UtcNow;
-
- AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs
deleted file mode 100644
index b2aea566..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using AdsTrackingService.Domain.SeedWork;
-
-namespace AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-///
-/// EN: Sample status enumeration following type-safe enum pattern.
-/// VI: Enumeration trạng thái Sample theo pattern enum an toàn kiểu.
-///
-public class SampleStatus : Enumeration
-{
- ///
- /// EN: Draft status - initial state
- /// VI: Trạng thái nháp - trạng thái ban đầu
- ///
- public static SampleStatus Draft = new(1, nameof(Draft));
-
- ///
- /// EN: Active status - ready for use
- /// VI: Trạng thái hoạt động - sẵn sàng sử dụng
- ///
- public static SampleStatus Active = new(2, nameof(Active));
-
- ///
- /// EN: Completed status - finished processing
- /// VI: Trạng thái hoàn thành - đã xử lý xong
- ///
- public static SampleStatus Completed = new(3, nameof(Completed));
-
- ///
- /// EN: Cancelled status - cancelled by user
- /// VI: Trạng thái đã hủy - bị hủy bởi người dùng
- ///
- public static SampleStatus Cancelled = new(4, nameof(Cancelled));
-
- public SampleStatus(int id, string name) : base(id, name)
- {
- }
-
- ///
- /// EN: Get all available statuses.
- /// VI: Lấy tất cả các trạng thái có sẵn.
- ///
- public static IEnumerable List() => GetAll();
-
- ///
- /// EN: Parse status from name.
- /// VI: Parse trạng thái từ tên.
- ///
- public static SampleStatus FromName(string name)
- {
- var status = List().SingleOrDefault(s =>
- string.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase));
-
- if (status is null)
- {
- throw new ArgumentException($"Possible values for SampleStatus: {string.Join(",", List().Select(s => s.Name))}");
- }
-
- return status;
- }
-
- ///
- /// EN: Parse status from ID.
- /// VI: Parse trạng thái từ ID.
- ///
- public static SampleStatus From(int id)
- {
- var status = List().SingleOrDefault(s => s.Id == id);
-
- if (status is null)
- {
- throw new ArgumentException($"Possible values for SampleStatus: {string.Join(",", List().Select(s => s.Name))}");
- }
-
- return status;
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/TrackingPixelAggregate/TrackingPixel.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/TrackingPixelAggregate/TrackingPixel.cs
new file mode 100644
index 00000000..d4063582
--- /dev/null
+++ b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/AggregatesModel/TrackingPixelAggregate/TrackingPixel.cs
@@ -0,0 +1,79 @@
+using AdsTrackingService.Domain.SeedWork;
+
+namespace AdsTrackingService.Domain.AggregatesModel.TrackingPixelAggregate;
+
+///
+/// EN: Tracking pixel aggregate root - tracks ad impressions and clicks.
+/// VI: Tracking pixel aggregate root - theo dõi impression và click quảng cáo.
+///
+public class TrackingPixel : Entity, IAggregateRoot
+{
+ private Guid _advertiserId;
+ private string _pixelCode = null!;
+ private bool _isActive;
+
+ public Guid AdvertiserId => _advertiserId;
+ public string PixelCode => _pixelCode;
+ public bool IsActive => _isActive;
+ public DateTime CreatedAt { get; private set; }
+
+ protected TrackingPixel() { }
+
+ public TrackingPixel(Guid advertiserId)
+ {
+ Id = Guid.NewGuid();
+ _advertiserId = advertiserId;
+ _pixelCode = GeneratePixelCode();
+ _isActive = true;
+ CreatedAt = DateTime.UtcNow;
+ }
+
+ public void Deactivate() => _isActive = false;
+ public void Activate() => _isActive = true;
+
+ private static string GeneratePixelCode() => Guid.NewGuid().ToString("N")[..16].ToUpper();
+}
+
+///
+/// EN: Pixel event - individual tracking event (impression, click, pageview).
+/// VI: Pixel event - sự kiện tracking riêng lẻ.
+///
+public class PixelEvent : Entity
+{
+ private Guid _pixelId;
+ private Guid _adId;
+ private Guid _userId;
+ private PixelEventType _eventType;
+ private string? _userAgent;
+ private string? _ipAddress;
+ private DateTime _timestamp;
+
+ public Guid PixelId => _pixelId;
+ public Guid AdId => _adId;
+ public Guid UserId => _userId;
+ public PixelEventType EventType => _eventType;
+ public string? UserAgent => _userAgent;
+ public string? IpAddress => _ipAddress;
+ public DateTime Timestamp => _timestamp;
+
+ protected PixelEvent() { }
+
+ public PixelEvent(Guid pixelId, Guid adId, Guid userId, PixelEventType eventType, string? userAgent = null, string? ipAddress = null)
+ {
+ Id = Guid.NewGuid();
+ _pixelId = pixelId;
+ _adId = adId;
+ _userId = userId;
+ _eventType = eventType;
+ _userAgent = userAgent;
+ _ipAddress = ipAddress;
+ _timestamp = DateTime.UtcNow;
+ }
+}
+
+public enum PixelEventType
+{
+ Impression = 1,
+ Click = 2,
+ PageView = 3
+}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Events/SampleCreatedDomainEvent.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Events/SampleCreatedDomainEvent.cs
deleted file mode 100644
index 31bb2849..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Events/SampleCreatedDomainEvent.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using MediatR;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.Domain.Events;
-
-///
-/// 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.
-///
-public class SampleCreatedDomainEvent : INotification
-{
- ///
- /// EN: The newly created sample.
- /// VI: Sample mới được tạo.
- ///
- public Sample Sample { get; }
-
- public SampleCreatedDomainEvent(Sample sample)
- {
- Sample = sample;
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Events/SampleStatusChangedDomainEvent.cs
deleted file mode 100644
index e6b69660..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Events/SampleStatusChangedDomainEvent.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using MediatR;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.Domain.Events;
-
-///
-/// EN: Domain event raised when Sample status changes.
-/// VI: Domain event được phát ra khi trạng thái Sample thay đổi.
-///
-public class SampleStatusChangedDomainEvent : INotification
-{
- ///
- /// EN: The sample ID.
- /// VI: ID của sample.
- ///
- public Guid SampleId { get; }
-
- ///
- /// EN: Previous status before the change.
- /// VI: Trạng thái trước khi thay đổi.
- ///
- public SampleStatus PreviousStatus { get; }
-
- ///
- /// EN: New status after the change.
- /// VI: Trạng thái mới sau khi thay đổi.
- ///
- public SampleStatus NewStatus { get; }
-
- public SampleStatusChangedDomainEvent(
- Guid sampleId,
- SampleStatus previousStatus,
- SampleStatus newStatus)
- {
- SampleId = sampleId;
- PreviousStatus = previousStatus;
- NewStatus = newStatus;
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Exceptions/AdsTrackingDomainException.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Exceptions/AdsTrackingDomainException.cs
new file mode 100644
index 00000000..47d53515
--- /dev/null
+++ b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Exceptions/AdsTrackingDomainException.cs
@@ -0,0 +1,8 @@
+namespace AdsTrackingService.Domain.Exceptions;
+
+public class AdsTrackingDomainException : Exception
+{
+ public AdsTrackingDomainException() { }
+ public AdsTrackingDomainException(string message) : base(message) { }
+ public AdsTrackingDomainException(string message, Exception innerException) : base(message, innerException) { }
+}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Exceptions/SampleDomainException.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Exceptions/SampleDomainException.cs
deleted file mode 100644
index cee2b970..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Domain/Exceptions/SampleDomainException.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace AdsTrackingService.Domain.Exceptions;
-
-///
-/// EN: Exception for Sample aggregate domain errors.
-/// VI: Exception cho các lỗi domain của Sample aggregate.
-///
-public class SampleDomainException : DomainException
-{
- public SampleDomainException()
- {
- }
-
- public SampleDomainException(string message) : base(message)
- {
- }
-
- public SampleDomainException(string message, Exception innerException)
- : base(message, innerException)
- {
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/MyServiceContext.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/AdsTrackingServiceContext.cs
similarity index 100%
rename from services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/MyServiceContext.cs
rename to services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/AdsTrackingServiceContext.cs
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/DependencyInjection.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/DependencyInjection.cs
index 6d54f5d2..268f0780 100644
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/DependencyInjection.cs
+++ b/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/DependencyInjection.cs
@@ -1,27 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
using AdsTrackingService.Infrastructure.Idempotency;
-using AdsTrackingService.Infrastructure.Repositories;
namespace AdsTrackingService.Infrastructure;
-///
-/// EN: Dependency injection extensions for Infrastructure layer.
-/// VI: Extensions dependency injection cho lớp Infrastructure.
-///
public static class DependencyInjection
{
- ///
- /// EN: Add infrastructure services to the DI container.
- /// VI: Thêm các services infrastructure vào DI container.
- ///
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
- // EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL
services.AddDbContext(options =>
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
@@ -31,14 +20,9 @@ public static class DependencyInjection
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.MigrationsAssembly(typeof(AdsTrackingServiceContext).Assembly.FullName);
- npgsqlOptions.EnableRetryOnFailure(
- maxRetryCount: 5,
- maxRetryDelay: TimeSpan.FromSeconds(30),
- errorCodesToAdd: null);
+ npgsqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(30), 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,12 +30,7 @@ public static class DependencyInjection
}
});
- // EN: Register repositories / VI: Đăng ký repositories
- services.AddScoped();
-
- // EN: Register idempotency services / VI: Đăng ký idempotency services
services.AddScoped();
-
return services;
}
}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs
deleted file mode 100644
index 88ca2fdd..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.Infrastructure.EntityConfigurations;
-
-///
-/// EN: EF Core configuration for Sample entity.
-/// VI: Cấu hình EF Core cho entity Sample.
-///
-public class SampleEntityTypeConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder 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("_name")
- .HasColumnName("name")
- .HasMaxLength(200)
- .IsRequired();
-
- builder.Property("_description")
- .HasColumnName("description")
- .HasMaxLength(1000);
-
- builder.Property("_createdAt")
- .HasColumnName("created_at")
- .IsRequired();
-
- builder.Property("_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");
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs
deleted file mode 100644
index 3d0450db..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-
-namespace AdsTrackingService.Infrastructure.EntityConfigurations;
-
-///
-/// EN: EF Core configuration for SampleStatus enumeration.
-/// VI: Cấu hình EF Core cho enumeration SampleStatus.
-///
-public class SampleStatusEntityTypeConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder 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
- );
- }
-}
diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/Repositories/SampleRepository.cs b/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/Repositories/SampleRepository.cs
deleted file mode 100644
index a92f74d3..00000000
--- a/services/ads-tracking-service-net/src/AdsTrackingService.Infrastructure/Repositories/SampleRepository.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-using AdsTrackingService.Domain.SeedWork;
-
-namespace AdsTrackingService.Infrastructure.Repositories;
-
-///
-/// EN: Repository implementation for Sample aggregate.
-/// VI: Triển khai repository cho Sample aggregate.
-///
-public class SampleRepository : ISampleRepository
-{
- private readonly AdsTrackingServiceContext _context;
-
- ///
- /// EN: Unit of work for transaction management.
- /// VI: Unit of work cho quản lý transaction.
- ///
- public IUnitOfWork UnitOfWork => _context;
-
- public SampleRepository(AdsTrackingServiceContext context)
- {
- _context = context ?? throw new ArgumentNullException(nameof(context));
- }
-
- ///
- public async Task GetAsync(Guid sampleId)
- {
- var sample = await _context.Samples
- .Include(s => s.Status)
- .FirstOrDefaultAsync(s => s.Id == sampleId);
-
- return sample;
- }
-
- ///
- public async Task> GetAllAsync()
- {
- return await _context.Samples
- .Include(s => s.Status)
- .OrderByDescending(s => s.CreatedAt)
- .ToListAsync();
- }
-
- ///
- public Sample Add(Sample sample)
- {
- return _context.Samples.Add(sample).Entity;
- }
-
- ///
- public void Update(Sample sample)
- {
- _context.Entry(sample).State = EntityState.Modified;
- }
-
- ///
- public void Delete(Sample sample)
- {
- _context.Samples.Remove(sample);
- }
-
- ///
- public async Task> GetByStatusAsync(int statusId)
- {
- return await _context.Samples
- .Include(s => s.Status)
- .Where(s => s.StatusId == statusId)
- .OrderByDescending(s => s.CreatedAt)
- .ToListAsync();
- }
-}
diff --git a/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs
deleted file mode 100644
index 6967b93b..00000000
--- a/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using FluentAssertions;
-using Microsoft.Extensions.Logging;
-using Moq;
-using AdsTrackingService.API.Application.Commands;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-using AdsTrackingService.Domain.SeedWork;
-using Xunit;
-
-namespace AdsTrackingService.UnitTests.Application;
-
-///
-/// EN: Unit tests for CreateSampleCommandHandler.
-/// VI: Unit tests cho CreateSampleCommandHandler.
-///
-public class CreateSampleCommandHandlerTests
-{
- private readonly Mock _mockRepository;
- private readonly Mock> _mockLogger;
- private readonly CreateSampleCommandHandler _handler;
-
- public CreateSampleCommandHandlerTests()
- {
- _mockRepository = new Mock();
- _mockLogger = new Mock>();
-
- var mockUnitOfWork = new Mock();
- mockUnitOfWork.Setup(u => u.SaveEntitiesAsync(It.IsAny()))
- .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()))
- .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()), 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()), Times.Once);
- }
-}
diff --git a/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Domain/SampleAggregateTests.cs b/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Domain/SampleAggregateTests.cs
deleted file mode 100644
index 27e36c1f..00000000
--- a/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Domain/SampleAggregateTests.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-using FluentAssertions;
-using AdsTrackingService.Domain.AggregatesModel.SampleAggregate;
-using AdsTrackingService.Domain.Exceptions;
-using Xunit;
-
-namespace AdsTrackingService.UnitTests.Domain;
-
-///
-/// EN: Unit tests for Sample aggregate.
-/// VI: Unit tests cho Sample aggregate.
-///
-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()
- .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()
- .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()
- .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()
- .WithMessage("Cannot update a cancelled sample");
- }
-}
diff --git a/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs b/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs
index 6beeadae..09fd04d2 100644
--- a/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs
+++ b/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs
@@ -52,54 +52,61 @@ public class OrderEntityTypeConfiguration : IEntityTypeConfiguration
// EN: OrderItems collection
// VI: Collection OrderItems
- builder.OwnsMany("_orderItems", oi =>
+ builder.OwnsMany(o => o.Items, orderItems =>
{
- oi.ToTable("order_items");
+ orderItems.ToTable("order_items");
- oi.WithOwner().HasForeignKey("OrderId");
+ orderItems.WithOwner().HasForeignKey("OrderId");
- oi.Property("OrderId")
+ orderItems.Property("OrderId")
.HasColumnName("order_id");
- oi.HasKey("Id");
+ orderItems.HasKey(oi => oi.Id);
- oi.Property(x => x.Id)
+ orderItems.Property(x => x.Id)
.HasColumnName("id")
.ValueGeneratedNever();
- oi.Property("_productId")
+ orderItems.Property("_productId")
.HasColumnName("product_id")
.IsRequired();
- oi.Property("_productName")
+ orderItems.Property("_productName")
.HasColumnName("product_name")
.HasMaxLength(255)
.IsRequired();
- oi.Property(x => x.ProductTypeId)
- .HasColumnName("product_type_id")
+ orderItems.Property("_productType")
+ .HasColumnName("product_type")
+ .HasMaxLength(50)
.IsRequired();
- oi.Property("_quantity")
+ orderItems.Property("_quantity")
.HasColumnName("quantity")
.IsRequired();
- oi.Property("_unitPrice")
+ orderItems.Property("_unitPrice")
.HasColumnName("unit_price")
.HasColumnType("decimal(18,2)")
.IsRequired();
- oi.Property("_totalPrice")
- .HasColumnName("total_price")
- .HasColumnType("decimal(18,2)")
+ orderItems.Property("_status")
+ .HasColumnName("status")
+ .HasMaxLength(50)
.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);
+ orderItems.Property("_metadata")
+ .HasColumnName("metadata")
+ .HasColumnType("jsonb");
+
+ orderItems.Ignore(x => x.ProductId);
+ orderItems.Ignore(x => x.ProductName);
+ orderItems.Ignore(x => x.ProductType);
+ orderItems.Ignore(x => x.Quantity);
+ orderItems.Ignore(x => x.UnitPrice);
+ orderItems.Ignore(x => x.TotalPrice);
+ orderItems.Ignore(x => x.Status);
+ orderItems.Ignore(x => x.Metadata);
});
// EN: Indexes
@@ -114,9 +121,7 @@ public class OrderEntityTypeConfiguration : IEntityTypeConfiguration
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);
}
diff --git a/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/OrderItemEntityTypeConfiguration.cs b/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/OrderItemEntityTypeConfiguration.cs
deleted file mode 100644
index 062ed4f8..00000000
--- a/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/OrderItemEntityTypeConfiguration.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// 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;
-
-///
-/// 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).
-///
-///
-/// EN: OrderItem is configured as an owned type within Order aggregate.
-/// VI: OrderItem được cấu hình như owned type trong Order aggregate.
-///
-public class OrderItemEntityTypeConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder 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);
- }
-}
diff --git a/services/order-service-net/src/OrderService.Infrastructure/OrderContext.cs b/services/order-service-net/src/OrderService.Infrastructure/OrderContext.cs
index b972ca90..e4613264 100644
--- a/services/order-service-net/src/OrderService.Infrastructure/OrderContext.cs
+++ b/services/order-service-net/src/OrderService.Infrastructure/OrderContext.cs
@@ -46,7 +46,6 @@ public class OrderContext : DbContext, IUnitOfWork
// EN: Apply entity configurations
// VI: Áp dụng các cấu hình entity
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
- modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new OrderStatusEntityTypeConfiguration());
}