diff --git a/.agent/skills/development-lifecycle/SKILL.md b/.agent/skills/development-lifecycle/SKILL.md new file mode 100644 index 00000000..c804456f --- /dev/null +++ b/.agent/skills/development-lifecycle/SKILL.md @@ -0,0 +1,528 @@ +--- +name: development-lifecycle +description: Development lifecycle workflow - Code → Build → Test → Fix → Deploy. Use for understanding complete development process, debugging build errors, fixing test failures, and continuous improvement cycles in GoodGo microservices. +compatibility: ".NET 10+, Docker 24+, docker-compose 2.x" +metadata: + author: Velik Ho + version: "1.0" +--- + +# Development Lifecycle / Quy Trình Phát Triển + +Complete iterative development workflow from code to deployment with focus on troubleshooting and debugging. + +## When to Use This Skill / Khi Nào Sử Dụng + +Use this skill when: +- Starting development on a new feature / Bắt đầu phát triển feature mới +- Debugging build or test failures / Debug lỗi build hoặc test +- Understanding the complete dev cycle / Hiểu quy trình phát triển hoàn chỉnh +- Setting up local development environment / Setup môi trường local +- Troubleshooting deployment issues / Khắc phục sự cố deployment + +## Core Concepts / Khái Niệm Cốt Lõi + +### Development Cycle / Chu Kỳ Phát Triển + +```mermaid +graph LR + A[① CODE
Write Features] --> B[② BUILD
Compile & Package] + B --> C[③ TEST
Run Tests] + C --> D{Pass?} + D -->|No| E[④ FIX
Debug & Resolve] + E --> B + D -->|Yes| F[⑤ DEPLOY
Local/Staging/Prod] + F --> G{Issues?} + G -->|Yes| E + G -->|No| H[✓ Done] +``` + +### Philosophy / Triết Lý + +1. **Fail Fast** - Phát hiện lỗi sớm nhất có thể +2. **Iterative** - Cải tiến liên tục qua từng cycle +3. **Automated** - Tự động hóa các bước lặp lại +4. **Observable** - Luôn có visibility vào system state + +## Phase 1: CODE ✍️ + +**Goal**: Viết code sạch, maintainable theo architecture chuẩn + +### Workflow + +**Use existing skills** cho coding phase: +- [dotnet-microservice-workflow](../dotnet-microservice-workflow/SKILL.md) - 4-layer architecture +- [api-design](../api-design/SKILL.md) - RESTful endpoints +- [cqrs-mediatr](../cqrs-mediatr/SKILL.md) - Commands/Queries +- [domain-driven-design](../domain-driven-design/SKILL.md) - Domain modeling + +### Best Practices + +```csharp +// ✅ GOOD: Clear intention, follows patterns +public class CreateOrderCommandHandler : IRequestHandler +{ + private readonly IOrderRepository _repository; + + public async Task Handle(CreateOrderCommand cmd, CancellationToken ct) + { + var order = new Order(cmd.UserId, cmd.ShippingAddress); + foreach (var item in cmd.Items) + order.AddItem(item.ProductId, item.Quantity, item.Price); + + await _repository.AddAsync(order, ct); + await _repository.UnitOfWork.SaveChangesAsync(ct); + + return new OrderResult(order.Id); + } +} + +// ❌ BAD: Mixed concerns, no separation +[HttpPost] +public async Task CreateOrder(CreateOrderRequest request) +{ + var order = new Order { UserId = request.UserId }; + _context.Orders.Add(order); + await _context.SaveChangesAsync(); + return Ok(order.Id); +} +``` + +--- + +## Phase 2: BUILD 🔨 + +**Goal**: Compile code và tạo artifacts (DLLs, Docker images) + +### Local .NET Build + +```bash +# Restore NuGet packages +dotnet restore + +# Build solution +dotnet build + +# Build specific project +dotnet build src/MyService.API/ + +# Release build +dotnet build -c Release + +# Verbose output (for debugging) +dotnet build -v detailed +``` + +### Docker Build + +```bash +# Build service image +docker build -t my-service:latest -f services/my-service-net/Dockerfile . + +# Build via docker-compose +docker-compose -f deployments/local/docker-compose.yml build my-service-net + +# No cache (clean build) +docker-compose -f deployments/local/docker-compose.yml build --no-cache my-service-net + +# Parallel build multiple services +docker-compose -f deployments/local/docker-compose.yml build --parallel +``` + +### Common Build Errors + +| Error Code | Cause | Solution | +|------------|-------|----------| +| **CS0246** | Missing type/namespace | Run `dotnet restore`, add package reference | +| **CS0103** | Name does not exist | Add `using` statement | +| **CS1061** | Missing member | Check property/method name, NuGet version | +| **NU1101** | Unable to find package | Check package name, NuGet source | +| **Docker context** | Wrong build context | Verify `-f` and context path | + +**See detailed solutions**: [references/build-errors.md](references/build-errors.md) + +--- + +## Phase 3: TEST 🧪 + +**Goal**: Validate correctness through automated tests + +### Test Pyramid + +``` + ┌──────────────┐ + │ E2E Tests │ ← ~5% - Slow, brittle + │ (UI/API) │ + ├──────────────┤ + │ Integration │ ← ~15% - Medium speed + │ Tests │ Database, HTTP + ├──────────────┤ + │ Unit Tests │ ← ~80% - Fast, isolated + │ (Handlers, │ Pure logic + │ Domain) │ + └──────────────┘ +``` + +### Test Commands + +```bash +# Run all tests +dotnet test + +# Run specific test project +dotnet test tests/MyService.UnitTests/ + +# Run with filter +dotnet test --filter Category=Unit +dotnet test --filter FullyQualifiedName~CreateOrderHandler + +# Watch mode (TDD) +dotnet watch test --project tests/MyService.UnitTests/ + +# Coverage report +dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=opencover + +# Detailed output +dotnet test --logger "console;verbosity=detailed" +``` + +### Test Structure + +**Use testing skill**: [dotnet-senior-tester](../dotnet-senior-tester/SKILL.md) + +```csharp +// ✅ GOOD: Arrange-Act-Assert, clear mocking +public class CreateOrderCommandHandlerTests +{ + [Fact] + public async Task Handle_ValidCommand_CreatesOrder() + { + // Arrange + var repository = Substitute.For(); + var unitOfWork = Substitute.For(); + repository.UnitOfWork.Returns(unitOfWork); + + var handler = new CreateOrderCommandHandler(repository); + var command = new CreateOrderCommand(UserId: Guid.NewGuid(), /* ... */); + + // Act + var result = await handler.Handle(command, CancellationToken.None); + + // Assert + await repository.Received(1).AddAsync(Arg.Any(), Arg.Any()); + await unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); + Assert.NotEqual(Guid.Empty, result.OrderId); + } +} +``` + +**See test failure patterns**: [references/test-failures.md](references/test-failures.md) + +--- + +## Phase 4: FIX 🔧 + +**Goal**: Debug và resolve issues nhanh chóng + +### Debugging Workflow + +```mermaid +graph TD + A[Error Detected] --> B{Error Type?} + B -->|Build| C[Check Build Logs] + B -->|Test| D[Check Test Output] + B -->|Runtime| E[Check App Logs] + + C --> F[Identify Root Cause] + D --> F + E --> F + + F --> G[Apply Fix] + G --> H[Verify Fix] + H --> I{Fixed?} + I -->|No| F + I -->|Yes| J[Document Solution] +``` + +### Debugging Tools + +**Build Errors:** +```bash +# Verbose build output +dotnet build -v detailed + +# Clean and rebuild +dotnet clean && dotnet build + +# Check references +dotnet list package +``` + +**Test Failures:** +```bash +# Run single test with details +dotnet test --filter TestMethodName --logger "console;verbosity=detailed" + +# Debug test in VS Code +# Set breakpoint, F5 to debug +``` + +**Runtime Issues:** +```bash +# Container logs +docker logs -f my-service-net +docker logs --tail 100 my-service-net + +# Follow logs +docker-compose -f deployments/local/docker-compose.yml logs -f my-service-net + +# Exec into container +docker exec -it my-service-net sh + +# Check health endpoint +curl http://localhost/api/v1/my-service/health +``` + +### Common Fix Patterns + +| Issue | Pattern | Example | +|-------|---------|---------| +| **NullReferenceException** | Check mock setup | `repository.Setup(x => x.Get()).Returns(entity)` | +| **Async deadlock** | Use `ConfigureAwait(false)` | `await task.ConfigureAwait(false);` | +| **Database timeout** | Check connection string, indexes | Add index on frequently queried columns | +| **Container won't start** | Check environment vars, ports | Verify `docker-compose.yml` config | + +**See detailed guide**: [references/debugging-guide.md](references/debugging-guide.md) + +--- + +## Phase 5: DEPLOY 🚀 + +**Goal**: Deploy và verify application in target environment + +### Local Deployment + +```bash +# Start service with docker-compose +docker-compose -f deployments/local/docker-compose.yml up -d my-service-net + +# Check status +docker-compose -f deployments/local/docker-compose.yml ps + +# Health check +curl http://localhost/api/v1/my-service/health + +# Swagger UI +open http://localhost/api/v1/my-service/swagger +``` + +### Health Checks + +Every service MUST have `/health` endpoint: + +```csharp +// Program.cs +builder.Services.AddHealthChecks() + .AddNpgSql(connectionString, name: "database") + .AddRedis(redisConnection, name: "redis"); + +app.MapHealthChecks("/health"); +``` + +### Verification Checklist + +- [ ] Service starts without errors +- [ ] `/health` returns 200 OK +- [ ] Database migrations applied +- [ ] Swagger UI accessible +- [ ] Sample API call works +- [ ] Logs show no errors + +### Staging/Production + +**Use deployment skill**: [deployment-kubernetes](../deployment-kubernetes/SKILL.md) + +```bash +# kubectl commands for K8s +kubectl apply -f deployments/kubernetes/my-service.yaml +kubectl rollout status deployment/my-service +kubectl get pods -l app=my-service +``` + +--- + +## Development Patterns / Các Mẫu Phát Triển + +### Pattern 1: Test-Driven Development (TDD) + +``` +1. ❌ RED - Write failing test +2. ✅ GREEN - Write minimal code to pass +3. 🔧 REFACTOR - Improve code quality +4. Repeat +``` + +**Benefits:** +- Better design (testable code) +- High confidence in changes +- Living documentation + +### Pattern 2: Incremental Development + +```bash +# Small commits with working state +git commit -m "feat: add CreateOrder command" +git commit -m "test: add CreateOrderHandler tests" +git commit -m "fix: validate order items quantity" +``` + +### Pattern 3: Hot Reload Development + +```bash +# .NET hot reload (faster iteration) +dotnet watch run --project src/MyService.API + +# Docker with volume mount (for config changes) +docker-compose up --build +``` + +--- + +## Helper Scripts / Scripts Hỗ Trợ + +### Check Environment + +```bash +# Verify development environment +./scripts/check-env.sh +``` + +### Quick Build + +```bash +# Build single service quickly +./scripts/quick-build.sh my-service-net +``` + +### Debug Build + +```bash +# Debug build errors with verbose output +./scripts/debug-build.sh my-service-net +``` + +### Health Check + +```bash +# Check all services health +./scripts/health-check.sh +``` + +--- + +## Common Mistakes / Lỗi Thường Gặp + +### 1. Skip Tests + +**Problem**: Bugs go to production +**Solution**: Write tests first (TDD) or immediately after feature + +```bash +# ❌ BAD: Push without testing +git push origin main + +# ✅ GOOD: Always test first +dotnet test && git push origin main +``` + +### 2. Ignore Build Warnings + +**Problem**: Tech debt accumulates, future breaks +**Solution**: Treat warnings as errors + +```xml + + + true + +``` + +### 3. No Logging + +**Problem**: Hard to debug production issues +**Solution**: Add structured logging + +```csharp +// ✅ GOOD: Structured logging +_logger.LogInformation("Creating order for user {UserId} with {ItemCount} items", + command.UserId, command.Items.Count); +``` + +### 4. Skip Health Checks + +**Problem**: Can't verify deployment success +**Solution**: Always add `/health` endpoint + +```csharp +app.MapHealthChecks("/health", new HealthCheckOptions +{ + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse +}); +``` + +--- + +## Quick Reference / Tham Chiếu Nhanh + +### Development Commands + +| Task | Command | +|------|---------| +| **Restore packages** | `dotnet restore` | +| **Build** | `dotnet build` | +| **Run tests** | `dotnet test` | +| **Run locally** | `dotnet run --project src/MyService.API` | +| **Watch mode** | `dotnet watch run` | +| **Hot reload tests** | `dotnet watch test` | + +### Docker Commands + +| Task | Command | +|------|---------| +| **Build image** | `docker-compose build my-service-net` | +| **Start service** | `docker-compose up -d my-service-net` | +| **View logs** | `docker-compose logs -f my-service-net` | +| **Restart** | `docker-compose restart my-service-net` | +| **Stop** | `docker-compose down` | +| **Rebuild & start** | `docker-compose up -d --build my-service-net` | + +### Debugging Commands + +| Task | Command | +|------|---------| +| **Verbose build** | `dotnet build -v detailed` | +| **Clean build** | `dotnet clean && dotnet build` | +| **Test with logs** | `dotnet test --logger console` | +| **Container logs** | `docker logs -f my-service-net` | +| **Exec into container** | `docker exec -it my-service-net sh` | + +--- + +## Resources / Tài Nguyên + +### Related Skills +- [dotnet-microservice-workflow](../dotnet-microservice-workflow/SKILL.md) - 4-phase architecture workflow +- [dotnet-senior-tester](../dotnet-senior-tester/SKILL.md) - Comprehensive testing +- [docker-traefik](../docker-traefik/SKILL.md) - Docker patterns +- [error-handling-patterns](../error-handling-patterns/SKILL.md) - Exception handling +- [project-rules](../project-rules/SKILL.md) - Coding standards + +### Helper Resources +- [Build Errors Catalog](references/build-errors.md) - Common build errors and solutions +- [Test Failures Guide](references/test-failures.md) - Test debugging patterns +- [Debugging Guide](references/debugging-guide.md) - Advanced debugging techniques + +### External Resources +- [.NET CLI Reference](https://learn.microsoft.com/en-us/dotnet/core/tools/) +- [Docker Compose Reference](https://docs.docker.com/compose/compose-file/) +- [xUnit Documentation](https://xunit.net/) diff --git a/.agent/skills/development-lifecycle/references/build-errors.md b/.agent/skills/development-lifecycle/references/build-errors.md new file mode 100644 index 00000000..09427416 --- /dev/null +++ b/.agent/skills/development-lifecycle/references/build-errors.md @@ -0,0 +1,298 @@ +# Common Build Errors Catalog + +Quick reference for troubleshooting .NET and Docker build errors in GoodGo microservices. + +## .NET Build Errors + +### CS0246: Type or namespace not found + +**Error:** +``` +error CS0246: The type or namespace name 'X' could not be found +``` + +**Common Causes:** +1. Missing package reference +2. Incorrect namespace +3. Package not restored + +**Solutions:** +```bash +# 1. Restore packages +dotnet restore + +# 2. Add missing package +dotnet add package + +# 3. Check using statements +# Add: using YourNamespace; + +# 4. Clean and rebuild +dotnet clean && dotnet build +``` + +--- + +### CS0103: Name does not exist in current context + +**Error:** +``` +error CS0103: The name 'X' does not exist in the current context +``` + +**Common Causes:** +1. Missing `using` directive +2. Typo in variable/method name +3. Wrong scope + +**Solutions:** +```csharp +// Add using directive +using System.Linq; +using Microsoft.EntityFrameworkCore; + +// Check variable is in scope +public void Method() +{ + var item = new Item(); + // item is only available within Method() +} +``` + +--- + +### CS1061: Does not contain a definition + +**Error:** +``` +error CS1061: 'Type' does not contain a definition for 'Member' +``` + +**Common Causes:** +1. Typo in property/method name +2. Wrong NuGet package version +3. Extension method not imported + +**Solutions:** +```csharp +// 1. Check spelling +order.Items // not order.Item + +// 2. Import extension methods +using System.Linq; // for .Where(), .Select(), etc. + +// 3. Update package version +dotnet list package --outdated +dotnet add package Microsoft.EntityFrameworkCore --version 10.0.0 +``` + +--- + +### NU1101: Unable to find package + +**Error:** +``` +error NU1101: Unable to find package 'X'. No packages exist with this id +``` + +**Solutions:** +```bash +# 1. Check package name (spelling) +dotnet add package Swashbuckle.AspNetCore # not Swashbuckle.AspNet + +# 2. Check NuGet sources +dotnet nuget list source + +# 3. Clear NuGet cache +dotnet nuget locals all --clear + +# 4. Restore with verbose +dotnet restore -v detailed +``` + +--- + +### MSB3277: Version conflicts + +**Error:** +``` +warning MSB3277: Found conflicts between different versions of "X" +``` + +**Solutions:** +```xml + + + + + +``` + +```bash +# Or update all references +dotnet add package Microsoft.EntityFrameworkCore --version 10.0.2 +``` + +--- + +## Docker Build Errors + +### Cannot connect to Docker daemon + +**Error:** +``` +Cannot connect to the Docker daemon at unix:///var/run/docker.sock +``` + +**Solutions:** +```bash +# 1. Start Docker Desktop (macOS/Windows) +open -a Docker + +# 2. Check Docker status +docker info + +# 3. Restart Docker daemon (Linux) +sudo systemctl restart docker +``` + +--- + +### Context error: Dockerfile not found + +**Error:** +``` +ERROR: failed to solve: failed to read dockerfile: failed to resolve path +``` + +**Common Causes:** +- Wrong build context +- Dockerfile in wrong location + +**Solutions:** +```bash +# Correct: Build from repo root with context +docker build -t my-service -f services/my-service-net/Dockerfile . + +# ❌ Wrong: Building from service directory +cd services/my-service-net +docker build -t my-service . + +# ✅ Correct: Specify context +docker build -t my-service -f Dockerfile ../.. +``` + +--- + +### COPY failed: file not found + +**Error:** +``` +ERROR: failed to copy files: file not found +``` + +**Common Cause:** Wrong COPY path in Dockerfile + +**Solutions:** +```dockerfile +# ❌ Wrong: Path relative to Dockerfile location +COPY src/ /app/ + +# ✅ Correct: Path relative to build context (repo root) +COPY services/my-service-net/src/ /app/ +``` + +--- + +### Docker build cache issues + +**Problem:** Old files being used despite changes + +**Solutions:** +```bash +# Build without cache +docker build --no-cache -t my-service . + +# Or via docker-compose +docker-compose build --no-cache my-service-net + +# Clear all build cache +docker builder prune -a +``` + +--- + +## Entity Framework Errors + +### No migrations found + +**Error:** +``` +No migrations were found +``` + +**Solutions:** +```bash +# Create initial migration +dotnet ef migrations add InitialCreate --project src/MyService.Infrastructure + +# Verify migrations folder exists +ls src/MyService.Infrastructure/Migrations/ +``` + +--- + +### Connection string error + +**Error:** +``` +A connection string could not be constructed +``` + +**Solutions:** +```bash +# 1. Add connection string to appsettings.json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Database=mydb;..." + } +} + +# 2. Set environment variable +export ConnectionStrings__DefaultConnection="Host=..." + +# 3. Verify in Program.cs +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); +``` + +--- + +## Quick Troubleshooting Checklist + +When build fails: + +- [ ] Run `dotnet restore` +- [ ] Check for typos +- [ ] Verify `using` statements +- [ ] Check package versions (`dotnet list package`) +- [ ] Clean build: `dotnet clean && dotnet build` +- [ ] Check build logs: `dotnet build -v detailed` +- [ ] Clear NuGet cache: `dotnet nuget locals all --clear` + +When Docker build fails: + +- [ ] Check Docker daemon is running +- [ ] Verify build context path +- [ ] Check Dockerfile COPY paths +- [ ] Try `--no-cache` build +- [ ] Check `.dockerignore` file +- [ ] Verify base image exists + +--- + +## Related Resources + +- [Test Failures Guide](test-failures.md) +- [Debugging Guide](debugging-guide.md) +- Main Skill: [development-lifecycle](../SKILL.md) diff --git a/.agent/skills/development-lifecycle/references/debugging-guide.md b/.agent/skills/development-lifecycle/references/debugging-guide.md new file mode 100644 index 00000000..b9dc26b2 --- /dev/null +++ b/.agent/skills/development-lifecycle/references/debugging-guide.md @@ -0,0 +1,462 @@ +# Debugging Guide + +Advanced debugging techniques for .NET microservices development. + +## Visual Studio Code Debugging + +### Setup launch.json + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Debug", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/MyService.API/bin/Debug/net10.0/MyService.API.dll", + "args": [], + "cwd": "${workspaceFolder}/src/MyService.API", + "console": "internalConsole", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + { + "name": ".NET Attach", + "type": "coreclr", + "request": "attach" + } + ] +} +``` + +### Debug Tests + +```json +{ + "name": ".NET Test Debug", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "dotnet", + "args": [ + "test", + "--filter", + "FullyQualifiedName~MyTestMethod" + ], + "cwd": "${workspaceFolder}", + "console": "internalConsole" +} +``` + +**Usage:** +1. Set breakpoint in test or code +2. F5 to start debugging +3. Step through with F10 (over) / F11 (into) + +--- + +## Remote Debugging Docker Containers + +### Enable remote debugging in Dockerfile + +```dockerfile +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS debug +WORKDIR /src +COPY . . +RUN dotnet restore +RUN dotnet build --no-restore + +# Install debugger +RUN apt-get update && apt-get install -y procps + +EXPOSE 8080 +EXPOSE 4024 + +ENTRYPOINT ["dotnet", "run", "--no-build", "--project", "src/MyService.API"] +``` + +### Docker Compose debug config + +```yaml +services: + my-service-debug: + build: + context: ../.. + dockerfile: services/my-service-net/Dockerfile + target: debug + environment: + - ASPNETCORE_ENVIRONMENT=Development + - DOTNET_USE_POLLING_FILE_WATCHER=true + ports: + - "8080:8080" + - "4024:4024" # Debugger port + volumes: + - ./src:/src:ro +``` + +### Attach to running container + +```bash +# 1. Start container with debug enabled +docker-compose up my-service-debug + +# 2. Find process ID +docker exec my-service-debug ps aux | grep dotnet + +# 3. In VS Code, use "Attach to Process" configuration +# Select the dotnet process from Docker container +``` + +--- + +## Logging Strategies + +### Structured Logging with Serilog + +```csharp +// Program.cs +builder.Host.UseSerilog((context, config) => +{ + config + .ReadFrom.Configuration(context.Configuration) + .Enrich.FromLogContext() + .Enrich.WithProperty("Service", "MyService") + .WriteTo.Console(new JsonFormatter()) + .WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day); +}); + +// Usage in code +_logger.LogInformation( + "Creating order for {UserId} with {ItemCount} items and total {Total:C}", + command.UserId, + command.Items.Count, + order.Total +); +``` + +**Benefits:** +- Searchable structured data +- Context preservation +- Easy filtering + +--- + +### Log Scopes + +```csharp +using (_logger.BeginScope("OrderId: {OrderId}", orderId)) +{ + _logger.LogInformation("Processing order"); + _logger.LogInformation("Validating items"); + _logger.LogInformation("Calculating total"); +} + +// All logs include OrderId automatically +``` + +--- + +## Debugging Tools + +### dotnet-dump (Memory dumps) + +```bash +# Install tool +dotnet tool install -g dotnet-dump + +# Create dump of running process +dotnet-dump collect -p + +# Analyze dump +dotnet-dump analyze + +# Inside analyzer +> clrstack # View call stack +> dumpheap -stat # Heap statistics +> eeheap -gc # GC heap info +``` + +--- + +### dotnet-trace (Performance tracing) + +```bash +# Install tool +dotnet tool install -g dotnet-trace + +# Collect trace +dotnet-trace collect -p --duration 00:00:30 + +# Convert to speedscope format +dotnet-trace convert trace.nettrace --format speedscope + +# View in browser +open https://www.speedscope.app/ +``` + +--- + +### dotnet-counters (Live metrics) + +```bash +# Install tool +dotnet tool install -g dotnet-counters + +# Monitor live +dotnet-counters monitor -p + +# Custom metrics +dotnet-counters monitor \ + -p \ + --counters System.Runtime,Microsoft.AspNetCore.Hosting +``` + +--- + +## Database Debugging + +### EF Core Query Logging + +```csharp +// Enable sensitive data logging (Development only!) +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(connectionString); + + if (builder.Environment.IsDevelopment()) + { + options + .EnableSensitiveDataLogging() + .EnableDetailedErrors(); + } +}); +``` + +**Output:** +``` +Executed DbCommand (23ms) [Parameters=[@p0='123'], CommandType='Text'] +SELECT * FROM orders WHERE id = @p0 +``` + +--- + +### SQL Profiling + +```bash +# PostgreSQL: Enable query logging +# In postgresql.conf or docker-compose +POSTGRES_EXTRA_FLAGS="-c log_statement=all" + +# View logs +docker-compose logs -f postgres | grep "statement:" +``` + +--- + +## HTTP Debugging + +### Fiddler / Postman for API testing + +**Setup authentication:** +```bash +# Get JWT token +curl -X POST http://localhost/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"test","password":"test123"}' + +# Use token in requests +curl http://localhost/api/v1/orders \ + -H "Authorization: Bearer " +``` + +--- + +### Request/Response logging middleware + +```csharp +// Custom middleware +public class RequestResponseLoggingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public async Task InvokeAsync(HttpContext context) + { + // Log request + context.Request.EnableBuffering(); + var requestBody = await ReadBodyAsync(context.Request.Body); + + _logger.LogInformation( + "HTTP {Method} {Path} Body: {Body}", + context.Request.Method, + context.Request.Path, + requestBody + ); + + // Capture response + var originalBody = context.Response.Body; + using var responseBody = new MemoryStream(); + context.Response.Body = responseBody; + + await _next(context); + + // Log response + responseBody.Seek(0, SeekOrigin.Begin); + var response = await new StreamReader(responseBody).ReadToEndAsync(); + + _logger.LogInformation( + "HTTP {StatusCode} Response: {Response}", + context.Response.StatusCode, + response + ); + + // Copy to original stream + responseBody.Seek(0, SeekOrigin.Begin); + await responseBody.CopyToAsync(originalBody); + } +} +``` + +--- + +## Common Debugging Scenarios + +### Find source of exception + +```bash +# Enable first-chance exceptions in VS Code +# .vscode/launch.json +"exceptionOptions": { + "breakpoints": { + "raised": true, + "userUnhandled": false + } +} +``` + +--- + +### Memory leak investigation + +```bash +# 1. Take baseline dump +dotnet-dump collect -p -o baseline.dump + +# 2. Exercise application +# ... use the app normally ... + +# 3. Take second dump +dotnet-dump collect -p -o leaked.dump + +# 4. Compare +dotnet-dump analyze leaked.dump +> dumpheap -stat +# Look for growing object counts +``` + +--- + +### Slow request diagnosis + +**Add request timing:** +```csharp +app.Use(async (context, next) => +{ + var sw = Stopwatch.StartNew(); + await next(); + sw.Stop(); + + if (sw.ElapsedMilliseconds > 1000) + { + _logger.LogWarning( + "Slow request: {Method} {Path} took {Elapsed}ms", + context.Request.Method, + context.Request.Path, + sw.ElapsedMilliseconds + ); + } +}); +``` + +--- + +## Performance Profiling + +### BenchmarkDotNet + +```csharp +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; + +[MemoryDiagnoser] +public class OrderBenchmarks +{ + private Order _order; + + [GlobalSetup] + public void Setup() + { + _order = new Order(/* ... */); + } + + [Benchmark] + public void AddItem() + { + _order.AddItem(Guid.NewGuid(), 1, 10.0m); + } +} + +// Run benchmarks +BenchmarkRunner.Run(); +``` + +--- + +## Quick Reference + +### VS Code Shortcuts + +| Action | Shortcut | +|--------|----------| +| Start debugging | F5 | +| Step over | F10 | +| Step into | F11 | +| Step out | Shift+F11 | +| Continue | F5 | +| Toggle breakpoint | F9 | +| Add watch | Right-click → Add to Watch | + +### Useful Commands + +```bash +# View running processes +ps aux | grep dotnet + +# Find process by port +lsof -i :8080 + +# Kill process +kill -9 + +# View environment variables +docker exec my-service env + +# Execute command in container +docker exec -it my-service sh + +# Follow logs with filter +docker logs -f my-service | grep ERROR +``` + +--- + +## Related Resources + +- [Build Errors Catalog](build-errors.md) +- [Test Failures Guide](test-failures.md) +- Main Skill: [development-lifecycle](../SKILL.md) +- [.NET Debugging Documentation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/) diff --git a/.agent/skills/development-lifecycle/references/test-failures.md b/.agent/skills/development-lifecycle/references/test-failures.md new file mode 100644 index 00000000..f0b04ecf --- /dev/null +++ b/.agent/skills/development-lifecycle/references/test-failures.md @@ -0,0 +1,354 @@ +# Test Failures Guide + +Common test failure patterns and how to fix them in .NET microservices. + +## Mock Setup Issues + +### NullReferenceException in tests + +**Symptom:** +``` +System.NullReferenceException: Object reference not set to an instance of an object +``` + +**Common Cause:** Mock not properly configured + +**Solution:** +```csharp +// ❌ BAD: Mock returns null by default +var repository = Substitute.For(); +var order = await repository.GetByIdAsync(orderId); // returns null! +order.Status; // NullReferenceException + +// ✅ GOOD: Configure mock to return data +var repository = Substitute.For(); +var expectedOrder = new Order(/* ... */); +repository.GetByIdAsync(orderId).Returns(expectedOrder); + +var order = await repository.GetByIdAsync(orderId); +Assert.NotNull(order); +``` + +--- + +### UnitOfWork mock setup + +**Problem:** `SaveChangesAsync` not properly mocked + +**Solution:** +```csharp +// ✅ Complete mock setup +var repository = Substitute.For(); +var unitOfWork = Substitute.For(); + +// Important: Link repository to unitOfWork +repository.UnitOfWork.Returns(unitOfWork); + +// Configure SaveChangesAsync behavior +unitOfWork.SaveChangesAsync(Arg.Any()) + .Returns(Task.FromResult(1)); + +// Now handler can use it +var handler = new CreateOrderHandler(repository); +await handler.Handle(command, CancellationToken.None); + +// Verify SaveChangesAsync was called +await unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); +``` + +--- + +## Async/Await Issues + +### Test hangs or times out + +**Common Causes:** +1. Missing `await` +2. Deadlock from `.Result` or `.Wait()` +3. Infinite loop + +**Solutions:** +```csharp +// ❌ BAD: Forgetting await +[Fact] +public async Task TestMethod() +{ + var result = handler.Handle(command, ct); // Missing await! + Assert.NotNull(result); // Wrong - this is Task, not result +} + +// ✅ GOOD: Proper await +[Fact] +public async Task TestMethod() +{ + var result = await handler.Handle(command, ct); + Assert.NotNull(result); +} + +// ❌ BAD: Blocking async code +var result = repository.GetByIdAsync(id).Result; // Deadlock risk! + +// ✅ GOOD: Await properly +var result = await repository.GetByIdAsync(id); +``` + +--- + +## Assertion Failures + +### Expected vs Actual mismatch + +**Symptom:** +``` +Assert.Equal() Failure +Expected: 5 +Actual: 0 +``` + +**Debugging:** +```csharp +// ❌ Unclear what went wrong +Assert.Equal(5, order.Items.Count); + +// ✅ Better: Add message +Assert.Equal(5, order.Items.Count, + $"Expected 5 items but got {order.Items.Count}"); + +// ✅ Best: Use specific assertions +Assert.NotEmpty(order.Items); +Assert.Equal(5, order.Items.Count); +Assert.All(order.Items, item => Assert.NotNull(item.ProductId)); +``` + +--- + +### Collection comparison failures + +**Problem:** Comparing collections incorrectly + +**Solutions:** +```csharp +// ❌ BAD: Reference comparison +var expected = new List { 1, 2, 3 }; +var actual = service.GetNumbers(); +Assert.Equal(expected, actual); // May fail even if content same + +// ✅ GOOD: Value comparison +Assert.Equal(expected.Count, actual.Count); +Assert.All(expected, item => Assert.Contains(item, actual)); + +// ✅ Or use FluentAssertions +actual.Should().BeEquivalentTo(expected); +``` + +--- + +## Database Test Issues + +### Test fails due to database state + +**Problem:** Tests interfere with each other + +**Solutions:** +```csharp +// ✅ Use in-memory database per test +public class OrderRepositoryTests : IDisposable +{ + private readonly DbContextOptions _options; + private readonly OrderContext _context; + + public OrderRepositoryTests() + { + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + _context = new OrderContext(_options); + } + + public void Dispose() + { + _context.Database.EnsureDeleted(); + _context.Dispose(); + } +} + +// ✅ Or use Testcontainers for real database +public class OrderRepositoryIntegrationTests : IAsyncLifetime +{ + private PostgreSqlContainer _container; + + public async Task InitializeAsync() + { + _container = new PostgreSqlBuilder().Build(); + await _container.StartAsync(); + } + + public async Task DisposeAsync() + { + await _container.DisposeAsync(); + } +} +``` + +--- + +## Integration Test Issues + +### TestServer authentication failures + +**Problem:** Requests return 401 Unauthorized + +**Solutions:** +```csharp +// ✅ Setup test authentication +public class CustomWebApplicationFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(services => + { + // Remove real auth + services.RemoveAll(); + + // Add test auth + services.AddAuthentication("Test") + .AddScheme( + "Test", options => { }); + }); + } +} + +// Usage in test +var client = _factory.CreateClient(); +client.DefaultRequestHeaders.Add("X-Test-User-Id", userId.ToString()); +``` + +--- + +### Port already in use + +**Error:** +``` +Address already in use: bind +``` + +**Solutions:** +```csharp +// ✅ Let TestServer choose random port +var factory = new WebApplicationFactory() + .WithWebHostBuilder(builder => + { + builder.UseUrls(); // Empty = random port + }); + +// ✅ Or use different port per test class +builder.UseUrls("http://localhost:0"); // 0 = random port +``` + +--- + +## Flaky Tests + +### Tests pass/fail intermittently + +**Common Causes:** +1. Race conditions +2. Time-dependent logic +3. Shared state +4. External dependencies + +**Solutions:** +```csharp +// ❌ BAD: Time-dependent test +[Fact] +public void TestCreatedDate() +{ + var order = new Order(); + Assert.Equal(DateTime.UtcNow, order.CreatedAt); // Flaky! +} + +// ✅ GOOD: Test with tolerance +[Fact] +public void TestCreatedDate() +{ + var before = DateTime.UtcNow; + var order = new Order(); + var after = DateTime.UtcNow; + + Assert.InRange(order.CreatedAt, before, after); +} + +// ✅ Or inject time provider +public class Order +{ + public DateTime CreatedAt { get; } + + public Order(ITimeProvider timeProvider) + { + CreatedAt = timeProvider.UtcNow; + } +} + +// In test: mock time +var timeProvider = Substitute.For(); +timeProvider.UtcNow.Returns(new DateTime(2024, 1, 1)); +``` + +--- + +## Test Coverage Issues + +### Low coverage on critical code + +**Problem:** Important logic not tested + +**Solutions:** +```bash +# Generate coverage report +dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura + +# View HTML report +reportgenerator -reports:coverage.cobertura.xml -targetdir:coveragereport +open coveragereport/index.html + +# Or use coverlet +dotnet add package coverlet.collector +dotnet test --collect:"XPlat Code Coverage" +``` + +**Focus on:** +- Domain logic (business rules) +- Command/Query handlers +- Critical paths (checkout, payment, etc.) + +--- + +## Quick Troubleshooting Checklist + +When tests fail: + +- [ ] Check mock setup (returns correct values?) +- [ ] Verify async/await (no missing `await`?) +- [ ] Check assertions (expected vs actual clear?) +- [ ] Isolate test (run alone, not in suite) +- [ ] Check test output logs +- [ ] Add debug logging +- [ ] Use debugger with breakpoints + +For integration tests: + +- [ ] Check TestServer configuration +- [ ] Verify authentication setup +- [ ] Check database state (clean between tests?) +- [ ] Review test ordering (independent?) +- [ ] Check external dependencies (mocked?) + +--- + +## Related Resources + +- [Build Errors Catalog](build-errors.md) +- [Debugging Guide](debugging-guide.md) +- [dotnet-senior-tester skill](../../dotnet-senior-tester/SKILL.md) +- Main Skill: [development-lifecycle](../SKILL.md) diff --git a/.agent/skills/development-lifecycle/scripts/check-env.sh b/.agent/skills/development-lifecycle/scripts/check-env.sh new file mode 100755 index 00000000..68b4f143 --- /dev/null +++ b/.agent/skills/development-lifecycle/scripts/check-env.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# EN: Check development environment for GoodGo microservices +# VI: Kiểm tra môi trường phát triển cho GoodGo microservices + +set -e + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Development Environment Check" +echo " Kiểm tra Môi Trường Phát Triển" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +check_command() { + local cmd=$1 + local required_version=$2 + + if command -v $cmd &> /dev/null; then + local version=$($cmd --version 2>&1 | head -n 1) + echo -e "${GREEN}✓${NC} $cmd: $version" + return 0 + else + echo -e "${RED}✗${NC} $cmd: Not installed" + if [ ! -z "$required_version" ]; then + echo -e " ${YELLOW}Required: $required_version${NC}" + fi + return 1 + fi +} + +check_dotnet() { + if command -v dotnet &> /dev/null; then + local version=$(dotnet --version) + echo -e "${GREEN}✓${NC} .NET SDK: $version" + + # Check if version >= 8.0 + local major=$(echo $version | cut -d. -f1) + if [ "$major" -lt 8 ]; then + echo -e " ${YELLOW}Warning: .NET 8.0+ recommended${NC}" + fi + return 0 + else + echo -e "${RED}✗${NC} .NET SDK: Not installed" + echo -e " ${YELLOW}Required: .NET 8.0+${NC}" + return 1 + fi +} + +check_docker() { + if command -v docker &> /dev/null; then + local version=$(docker --version | cut -d' ' -f3 | tr -d ',') + echo -e "${GREEN}✓${NC} Docker: $version" + + # Check if Docker daemon is running + if docker ps &> /dev/null; then + echo -e " ${GREEN}Docker daemon is running${NC}" + else + echo -e " ${RED}Docker daemon is not running${NC}" + return 1 + fi + return 0 + else + echo -e "${RED}✗${NC} Docker: Not installed" + return 1 + fi +} + +check_docker_compose() { + if command -v docker-compose &> /dev/null; then + local version=$(docker-compose --version | cut -d' ' -f4 | tr -d ',') + echo -e "${GREEN}✓${NC} docker-compose: $version" + return 0 + elif docker compose version &> /dev/null; then + local version=$(docker compose version | cut -d' ' -f4 | tr -d 'v') + echo -e "${GREEN}✓${NC} docker compose: $version" + return 0 + else + echo -e "${RED}✗${NC} docker-compose: Not installed" + return 1 + fi +} + +# Main checks +echo "Core Tools:" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +ERRORS=0 + +check_dotnet || ((ERRORS++)) +check_docker || ((ERRORS++)) +check_docker_compose || ((ERRORS++)) + +echo "" +echo "Optional Tools:" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +check_command "git" || true +check_command "curl" || true +check_command "jq" || true + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}✓ All core tools are installed!${NC}" + echo "" + exit 0 +else + echo -e "${RED}✗ $ERRORS core tool(s) missing or not working${NC}" + echo "" + echo "Installation guides:" + echo " .NET SDK: https://dotnet.microsoft.com/download" + echo " Docker: https://docs.docker.com/get-docker/" + echo "" + exit 1 +fi diff --git a/.agent/skills/development-lifecycle/scripts/debug-build.sh b/.agent/skills/development-lifecycle/scripts/debug-build.sh new file mode 100755 index 00000000..d95c0bcb --- /dev/null +++ b/.agent/skills/development-lifecycle/scripts/debug-build.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# EN: Debug build errors with verbose output +# VI: Debug lỗi build với output chi tiết + +set -e + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +if [ $# -eq 0 ]; then + echo -e "${RED}Error: Service name required${NC}" + echo "Usage: $0 " + echo "Example: $0 my-service-net" + exit 1 +fi + +SERVICE_NAME=$1 +SERVICE_DIR="services/${SERVICE_NAME}" +LOG_FILE="build-debug-$(date +%Y%m%d-%H%M%S).log" + +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE} Debug Build: ${SERVICE_NAME}${NC}" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" + +# Check if service exists +if [ ! -d "$SERVICE_DIR" ]; then + echo -e "${RED}✗ Service not found: $SERVICE_DIR${NC}" + exit 1 +fi + +cd "$SERVICE_DIR" + +# Find solution file +SOLUTION_FILE=$(find . -maxdepth 1 -name "*.slnx" -o -name "*.sln" | head -n 1) + +if [ -z "$SOLUTION_FILE" ]; then + echo -e "${RED}✗ No solution file found${NC}" + exit 1 +fi + +echo -e "${YELLOW}Step 1: Cleaning previous build artifacts...${NC}" +dotnet clean "$SOLUTION_FILE" + +echo "" +echo -e "${YELLOW}Step 2: Restoring packages with detailed output...${NC}" +dotnet restore "$SOLUTION_FILE" -v detailed 2>&1 | tee "$LOG_FILE" + +echo "" +echo -e "${YELLOW}Step 3: Building with detailed verbosity...${NC}" +echo -e "${YELLOW}(This will take longer but show all information)${NC}" +echo "" + +# Build with detailed output +dotnet build "$SOLUTION_FILE" --no-restore -v detailed 2>&1 | tee -a "$LOG_FILE" + +BUILD_STATUS=$? + +echo "" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + +if [ $BUILD_STATUS -eq 0 ]; then + echo -e "${GREEN}✓ Build completed successfully!${NC}" + echo "" + echo "Log saved to: $LOG_FILE" +else + echo -e "${RED}✗ Build failed${NC}" + echo "" + echo "Full debug log saved to: $LOG_FILE" + echo "" + echo -e "${YELLOW}Common issues to check:${NC}" + echo " 1. Missing package references (dotnet add package)" + echo " 2. Using statements (add 'using' directive)" + echo " 3. NuGet package version conflicts" + echo " 4. Target framework mismatch" + echo "" + echo "Grep for errors:" + echo " cat $LOG_FILE | grep -i error" + echo " cat $LOG_FILE | grep -i CS[0-9]" + exit 1 +fi diff --git a/.agent/skills/development-lifecycle/scripts/health-check.sh b/.agent/skills/development-lifecycle/scripts/health-check.sh new file mode 100755 index 00000000..0e5dc689 --- /dev/null +++ b/.agent/skills/development-lifecycle/scripts/health-check.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# EN: Check health of all running services +# VI: Kiểm tra health của tất cả services đang chạy + +set -e + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE} Service Health Check${NC}" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" + +# Default base URL +BASE_URL="${BASE_URL:-http://localhost}" +TIMEOUT=5 + +check_health() { + local service=$1 + local path=$2 + local url="${BASE_URL}${path}" + + echo -n "Checking $service... " + + if response=$(curl -s -o /dev/null -w "%{http_code}" --max-time $TIMEOUT "$url" 2>/dev/null); then + if [ "$response" = "200" ]; then + echo -e "${GREEN}✓ Healthy (200)${NC}" + return 0 + else + echo -e "${YELLOW}⚠ Response: $response${NC}" + return 1 + fi + else + echo -e "${RED}✗ Not reachable${NC}" + return 1 + fi +} + +# Track results +TOTAL=0 +HEALTHY=0 +UNHEALTHY=0 + +# Service list (customize based on your services) +SERVICES=( + "IAM Service:/api/v1/iam/health" + "Storage Service:/api/v1/storage/health" + "Catalog Service:/api/v1/catalog/health" + "Order Service:/api/v1/order/health" + "Inventory Service:/api/v1/inventory/health" + "F&B Engine:/api/v1/fnb-engine/health" + "Booking Service:/api/v1/booking/health" +) + +for SERVICE_INFO in "${SERVICES[@]}"; do + IFS=':' read -r NAME PATH <<< "$SERVICE_INFO" + ((TOTAL++)) + + if check_health "$NAME" "$PATH"; then + ((HEALTHY++)) + else + ((UNHEALTHY++)) + fi +done + +echo "" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "Summary:" +echo " Total services: $TOTAL" +echo -e " Healthy: ${GREEN}$HEALTHY${NC}" +echo -e " Unhealthy: ${RED}$UNHEALTHY${NC}" +echo "" + +if [ $UNHEALTHY -eq 0 ]; then + echo -e "${GREEN}✓ All services are healthy!${NC}" + exit 0 +else + echo -e "${YELLOW}⚠ Some services are not healthy${NC}" + echo "" + echo "Troubleshooting:" + echo " • Check if services are running: docker-compose ps" + echo " • View logs: docker-compose logs -f " + echo " • Restart service: docker-compose restart " + exit 1 +fi diff --git a/.agent/skills/development-lifecycle/scripts/quick-build.sh b/.agent/skills/development-lifecycle/scripts/quick-build.sh new file mode 100755 index 00000000..0dc3fcb9 --- /dev/null +++ b/.agent/skills/development-lifecycle/scripts/quick-build.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# EN: Quick build script for a single service +# VI: Script build nhanh cho một service + +set -e + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +if [ $# -eq 0 ]; then + echo -e "${RED}Error: Service name required${NC}" + echo "Usage: $0 " + echo "Example: $0 my-service-net" + exit 1 +fi + +SERVICE_NAME=$1 +SERVICE_DIR="services/${SERVICE_NAME}" + +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE} Quick Build: ${SERVICE_NAME}${NC}" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" + +# Check if service exists +if [ ! -d "$SERVICE_DIR" ]; then + echo -e "${RED}✗ Service not found: $SERVICE_DIR${NC}" + exit 1 +fi + +cd "$SERVICE_DIR" + +# Find solution file +SOLUTION_FILE=$(find . -maxdepth 1 -name "*.slnx" -o -name "*.sln" | head -n 1) + +if [ -z "$SOLUTION_FILE" ]; then + echo -e "${RED}✗ No solution file found${NC}" + exit 1 +fi + +echo -e "${YELLOW}📦 Restoring packages...${NC}" +dotnet restore "$SOLUTION_FILE" + +echo "" +echo -e "${YELLOW}🔨 Building solution...${NC}" +dotnet build "$SOLUTION_FILE" --no-restore + +echo "" +echo -e "${GREEN}✓ Build completed successfully!${NC}" +echo "" +echo "Next steps:" +echo " • Run tests: dotnet test" +echo " • Run locally: dotnet run --project src/*/API/" +echo " • Build Docker: docker-compose -f deployments/local/docker-compose.yml build ${SERVICE_NAME}" diff --git a/deployments/local/docker-compose.yml b/deployments/local/docker-compose.yml index b76e755d..f37ad155 100644 --- a/deployments/local/docker-compose.yml +++ b/deployments/local/docker-compose.yml @@ -580,6 +580,271 @@ services: - "traefik.http.services.promotion-service.loadbalancer.healthcheck.path=/health/live" - "traefik.http.services.promotion-service.loadbalancer.healthcheck.interval=10s" + # =========================================================================== + # MULTI-VERTICAL SERVICES - Product, Order, Inventory Management + # =========================================================================== + + # Catalog Service .NET - Polymorphic Product Management + catalog-service-net: + build: + context: ../../services/catalog-service-net + dockerfile: Dockerfile + image: goodgo/catalog-service-net:latest + container_name: catalog-service-net-local + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + # EN: Database - Neon PostgreSQL + # VI: Cơ sở dữ liệu - Neon PostgreSQL + - ConnectionStrings__DefaultConnection=Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Database=catalog_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require + # EN: IAM Service Communication + # VI: Giao tiếp IAM Service + - IamService__BaseUrl=http://iam-service-net:8080 + - IamService__ServiceName=catalog-service + # EN: JWT Configuration + # VI: Cấu hình JWT + - Jwt__Authority=http://iam-service-net:8080 + - Jwt__Audience=goodgo-api + - Jwt__RequireHttpsMetadata=false + ports: + - "5016:8080" + depends_on: + iam-service-net: + condition: service_healthy + merchant-service-net: + condition: service_healthy + traefik: + condition: service_started + networks: + - microservices-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - "traefik.enable=true" + - "traefik.http.routers.catalog-service.rule=PathPrefix(`/api/v1/products`) || PathPrefix(`/api/v1/categories`)" + - "traefik.http.routers.catalog-service.entrypoints=web" + - "traefik.http.services.catalog-service.loadbalancer.server.port=8080" + - "traefik.http.services.catalog-service.loadbalancer.healthcheck.path=/health/live" + - "traefik.http.services.catalog-service.loadbalancer.healthcheck.interval=10s" + + # Order Service .NET - Order Orchestration with Strategy Pattern + order-service-net: + build: + context: ../../services/order-service-net + dockerfile: Dockerfile + image: goodgo/order-service-net:latest + container_name: order-service-net-local + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + # EN: Database - Neon PostgreSQL + # VI: Cơ sở dữ liệu - Neon PostgreSQL + - ConnectionStrings__DefaultConnection=Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Database=order_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require + # EN: IAM Service Communication + # VI: Giao tiếp IAM Service + - IamService__BaseUrl=http://iam-service-net:8080 + - IamService__ServiceName=order-service + # EN: Service Communication + # VI: Giao tiếp Service + - CatalogService__BaseUrl=http://catalog-service-net:8080 + - InventoryService__BaseUrl=http://inventory-service-net:8080 + - WalletService__BaseUrl=http://wallet-service-net:8080 + # EN: JWT Configuration + # VI: Cấu hình JWT + - Jwt__Authority=http://iam-service-net:8080 + - Jwt__Audience=goodgo-api + - Jwt__RequireHttpsMetadata=false + ports: + - "5017:8080" + depends_on: + iam-service-net: + condition: service_healthy + catalog-service-net: + condition: service_healthy + traefik: + condition: service_started + networks: + - microservices-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - "traefik.enable=true" + - "traefik.http.routers.order-service.rule=PathPrefix(`/api/v1/orders`)" + - "traefik.http.routers.order-service.entrypoints=web" + - "traefik.http.services.order-service.loadbalancer.server.port=8080" + - "traefik.http.services.order-service.loadbalancer.healthcheck.path=/health/live" + - "traefik.http.services.order-service.loadbalancer.healthcheck.interval=10s" + + # Inventory Service .NET - Stock Management (Retail + FnB) + inventory-service-net: + build: + context: ../../services/inventory-service-net + dockerfile: Dockerfile + image: goodgo/inventory-service-net:latest + container_name: inventory-service-net-local + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + # EN: Database - Neon PostgreSQL + # VI: Cơ sở dữ liệu - Neon PostgreSQL + - ConnectionStrings__DefaultConnection=Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Database=inventory_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require + # EN: IAM Service Communication + # VI: Giao tiếp IAM Service + - IamService__BaseUrl=http://iam-service-net:8080 + - IamService__ServiceName=inventory-service + # EN: JWT Configuration + # VI: Cấu hình JWT + - Jwt__Authority=http://iam-service-net:8080 + - Jwt__Audience=goodgo-api + - Jwt__RequireHttpsMetadata=false + ports: + - "5018:8080" + depends_on: + iam-service-net: + condition: service_healthy + catalog-service-net: + condition: service_healthy + traefik: + condition: service_started + networks: + - microservices-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - "traefik.enable=true" + - "traefik.http.routers.inventory-service.rule=PathPrefix(`/api/v1/inventory`) || PathPrefix(`/api/v1/stock`)" + - "traefik.http.routers.inventory-service.entrypoints=web" + - "traefik.http.services.inventory-service.loadbalancer.server.port=8080" + - "traefik.http.services.inventory-service.loadbalancer.healthcheck.path=/health/live" + - "traefik.http.services.inventory-service.loadbalancer.healthcheck.interval=10s" + + # FnB Engine .NET - Table, Session & Kitchen Management + fnb-engine-net: + build: + context: ../../services/fnb-engine-net + dockerfile: Dockerfile + image: goodgo/fnb-engine-net:latest + container_name: fnb-engine-net-local + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + # EN: Database - Neon PostgreSQL + # VI: Cơ sở dữ liệu - Neon PostgreSQL + - ConnectionStrings__DefaultConnection=Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Database=fnb_engine;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require + # EN: IAM Service Communication + # VI: Giao tiếp IAM Service + - IamService__BaseUrl=http://iam-service-net:8080 + - IamService__ServiceName=fnb-engine + # EN: JWT Configuration + # VI: Cấu hình JWT + - Jwt__Authority=http://iam-service-net:8080 + - Jwt__Audience=goodgo-api + - Jwt__RequireHttpsMetadata=false + # EN: Redis for SignalR (Kitchen Display) + # VI: Redis cho SignalR (Màn hình bếp) + - ConnectionStrings__Redis=167.114.174.113:6379,password=Velik@2026 + ports: + - "5019:8080" + depends_on: + iam-service-net: + condition: service_healthy + merchant-service-net: + condition: service_healthy + traefik: + condition: service_started + networks: + - microservices-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - "traefik.enable=true" + - "traefik.http.routers.fnb-engine.rule=PathPrefix(`/api/v1/tables`) || PathPrefix(`/api/v1/sessions`) || PathPrefix(`/api/v1/kitchen`)" + - "traefik.http.routers.fnb-engine.entrypoints=web" + - "traefik.http.services.fnb-engine.loadbalancer.server.port=8080" + - "traefik.http.services.fnb-engine.loadbalancer.healthcheck.path=/health/live" + - "traefik.http.services.fnb-engine.loadbalancer.healthcheck.interval=10s" + # EN: SignalR Hub route for Kitchen Display + # VI: Route cho SignalR Hub màn hình bếp + - "traefik.http.routers.fnb-hub.rule=PathPrefix(`/hubs/kitchen`)" + - "traefik.http.routers.fnb-hub.entrypoints=web" + - "traefik.http.routers.fnb-hub.service=fnb-engine" + - "traefik.http.services.fnb-engine.loadbalancer.sticky.cookie=true" + - "traefik.http.services.fnb-engine.loadbalancer.sticky.cookie.name=fnb_session" + + # Booking Service .NET - Appointment Scheduling (Services, Spa, Salon) + booking-service-net: + build: + context: ../../services/booking-service-net + dockerfile: Dockerfile + image: goodgo/booking-service-net:latest + container_name: booking-service-net-local + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + # EN: Database - Neon PostgreSQL + # VI: Cơ sở dữ liệu - Neon PostgreSQL + - ConnectionStrings__DefaultConnection=Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Database=booking_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require + # EN: IAM Service Communication + # VI: Giao tiếp IAM Service + - IamService__BaseUrl=http://iam-service-net:8080 + - IamService__ServiceName=booking-service + # EN: Service Communication + # VI: Giao tiếp Service + - CatalogService__BaseUrl=http://catalog-service-net:8080 + - MerchantService__BaseUrl=http://merchant-service-net:8080 + # EN: JWT Configuration + # VI: Cấu hình JWT + - Jwt__Authority=http://iam-service-net:8080 + - Jwt__Audience=goodgo-api + - Jwt__RequireHttpsMetadata=false + ports: + - "5020:8080" + depends_on: + iam-service-net: + condition: service_healthy + catalog-service-net: + condition: service_healthy + merchant-service-net: + condition: service_healthy + traefik: + condition: service_started + networks: + - microservices-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - "traefik.enable=true" + - "traefik.http.routers.booking-service.rule=PathPrefix(`/api/v1/appointments`) || PathPrefix(`/api/v1/resources`) || PathPrefix(`/api/v1/schedules`)" + - "traefik.http.routers.booking-service.entrypoints=web" + - "traefik.http.services.booking-service.loadbalancer.server.port=8080" + - "traefik.http.services.booking-service.loadbalancer.healthcheck.path=/health/live" + - "traefik.http.services.booking-service.loadbalancer.healthcheck.interval=10s" + # Jaeger - Distributed Tracing # jaeger: diff --git a/services/ads-analytics-service-net/docker-compose.yml b/services/ads-analytics-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/ads-analytics-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/ads-analytics-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj b/services/ads-analytics-service-net/tests/AdsAnalyticsService.FunctionalTests/AdsAnalyticsService.FunctionalTests.csproj similarity index 100% rename from services/ads-analytics-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj rename to services/ads-analytics-service-net/tests/AdsAnalyticsService.FunctionalTests/AdsAnalyticsService.FunctionalTests.csproj diff --git a/services/ads-analytics-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/ads-analytics-service-net/tests/AdsAnalyticsService.FunctionalTests/Controllers/SamplesControllerTests.cs similarity index 100% rename from services/ads-analytics-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs rename to services/ads-analytics-service-net/tests/AdsAnalyticsService.FunctionalTests/Controllers/SamplesControllerTests.cs diff --git a/services/ads-analytics-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs b/services/ads-analytics-service-net/tests/AdsAnalyticsService.FunctionalTests/CustomWebApplicationFactory.cs similarity index 100% rename from services/ads-analytics-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs rename to services/ads-analytics-service-net/tests/AdsAnalyticsService.FunctionalTests/CustomWebApplicationFactory.cs diff --git a/services/ads-analytics-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj b/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/AdsAnalyticsService.UnitTests.csproj similarity index 100% rename from services/ads-analytics-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj rename to services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/AdsAnalyticsService.UnitTests.csproj diff --git a/services/ads-analytics-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Application/CreateSampleCommandHandlerTests.cs similarity index 100% rename from services/ads-analytics-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs rename to services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Application/CreateSampleCommandHandlerTests.cs diff --git a/services/ads-analytics-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs b/services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Domain/SampleAggregateTests.cs similarity index 100% rename from services/ads-analytics-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs rename to services/ads-analytics-service-net/tests/AdsAnalyticsService.UnitTests/Domain/SampleAggregateTests.cs diff --git a/services/ads-billing-service-net/docker-compose.yml b/services/ads-billing-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/ads-billing-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/ads-billing-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj b/services/ads-billing-service-net/tests/AdsBillingService.FunctionalTests/AdsBillingService.FunctionalTests.csproj similarity index 100% rename from services/ads-billing-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj rename to services/ads-billing-service-net/tests/AdsBillingService.FunctionalTests/AdsBillingService.FunctionalTests.csproj diff --git a/services/ads-billing-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/ads-billing-service-net/tests/AdsBillingService.FunctionalTests/Controllers/SamplesControllerTests.cs similarity index 100% rename from services/ads-billing-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs rename to services/ads-billing-service-net/tests/AdsBillingService.FunctionalTests/Controllers/SamplesControllerTests.cs diff --git a/services/ads-billing-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs b/services/ads-billing-service-net/tests/AdsBillingService.FunctionalTests/CustomWebApplicationFactory.cs similarity index 100% rename from services/ads-billing-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs rename to services/ads-billing-service-net/tests/AdsBillingService.FunctionalTests/CustomWebApplicationFactory.cs diff --git a/services/ads-billing-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj b/services/ads-billing-service-net/tests/AdsBillingService.UnitTests/AdsBillingService.UnitTests.csproj similarity index 100% rename from services/ads-billing-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj rename to services/ads-billing-service-net/tests/AdsBillingService.UnitTests/AdsBillingService.UnitTests.csproj diff --git a/services/ads-billing-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/ads-billing-service-net/tests/AdsBillingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs similarity index 100% rename from services/ads-billing-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs rename to services/ads-billing-service-net/tests/AdsBillingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs diff --git a/services/ads-billing-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs b/services/ads-billing-service-net/tests/AdsBillingService.UnitTests/Domain/SampleAggregateTests.cs similarity index 100% rename from services/ads-billing-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs rename to services/ads-billing-service-net/tests/AdsBillingService.UnitTests/Domain/SampleAggregateTests.cs diff --git a/services/ads-manager-service-net/docker-compose.yml b/services/ads-manager-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/ads-manager-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/AdsManagerService.API.csproj b/services/ads-manager-service-net/src/AdsManagerService.API/AdsManagerService.API.csproj index c464beeb..7f54f606 100644 --- a/services/ads-manager-service-net/src/AdsManagerService.API/AdsManagerService.API.csproj +++ b/services/ads-manager-service-net/src/AdsManagerService.API/AdsManagerService.API.csproj @@ -14,6 +14,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ActivateCampaignCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ActivateCampaignCommand.cs new file mode 100644 index 00000000..7cf1c290 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ActivateCampaignCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +/// +/// EN: Command to activate a campaign. +/// VI: Command kích hoạt chiến dịch. +/// +public record ActivateCampaignCommand : IRequest +{ + public Guid CampaignId { get; init; } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ActivateCampaignCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ActivateCampaignCommandHandler.cs new file mode 100644 index 00000000..0afca9a1 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ActivateCampaignCommandHandler.cs @@ -0,0 +1,27 @@ +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +public class ActivateCampaignCommandHandler : IRequestHandler +{ + private readonly ICampaignRepository _campaignRepository; + + public ActivateCampaignCommandHandler(ICampaignRepository campaignRepository) + { + _campaignRepository = campaignRepository ?? throw new ArgumentNullException(nameof(campaignRepository)); + } + + public async Task Handle(ActivateCampaignCommand request, CancellationToken cancellationToken) + { + var campaign = await _campaignRepository.GetByIdAsync(request.CampaignId, cancellationToken); + + if (campaign == null) + return false; + + campaign.Activate(); + _campaignRepository.Update(campaign); + + return await _campaignRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ChangeSampleStatusCommand.cs deleted file mode 100644 index e78303ce..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ChangeSampleStatusCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MediatR; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs deleted file mode 100644 index 179c5f91..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs +++ /dev/null @@ -1,70 +0,0 @@ -using MediatR; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdCommand.cs new file mode 100644 index 00000000..0a623d01 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +/// +/// EN: Command to create a new ad. +/// VI: Command tạo quảng cáo mới. +/// +public record CreateAdCommand : IRequest +{ + public Guid AdSetId { get; init; } + public string Name { get; init; } = null!; + public string Format { get; init; } = "single_image"; // single_image, single_video, carousel, etc. + public string? Headline { get; init; } + public string? PrimaryText { get; init; } + public string? CallToAction { get; init; } + public string? DestinationUrl { get; init; } + public string? CreativeUrl { get; init; } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdCommandHandler.cs new file mode 100644 index 00000000..bbe0f95f --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdCommandHandler.cs @@ -0,0 +1,45 @@ +using AdsManagerService.Domain.AggregatesModel.AdAggregate; +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +public class CreateAdCommandHandler : IRequestHandler +{ + private readonly IAdRepository _adRepository; + + public CreateAdCommandHandler(IAdRepository adRepository) + { + _adRepository = adRepository ?? throw new ArgumentNullException(nameof(adRepository)); + } + + public async Task Handle(CreateAdCommand request, CancellationToken cancellationToken) + { + // Parse ad format + var format = request.Format.ToLower() switch + { + "single_image" => AdFormat.SingleImage, + "single_video" => AdFormat.SingleVideo, + "carousel" => AdFormat.Carousel, + "collection" => AdFormat.Collection, + "stories" => AdFormat.Stories, + _ => AdFormat.SingleImage + }; + + // Create ad + var ad = new Ad( + adSetId: request.AdSetId, + name: request.Name, + format: format, + headline: request.Headline, + primaryText: request.PrimaryText, + callToAction: request.CallToAction, + destinationUrl: request.DestinationUrl, + creativeUrl: request.CreativeUrl + ); + + _adRepository.Add(ad); + await _adRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); + + return ad.Id; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdSetCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdSetCommand.cs new file mode 100644 index 00000000..71a2d532 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdSetCommand.cs @@ -0,0 +1,23 @@ +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +/// +/// EN: Command to create a new ad set. +/// VI: Command tạo ad set mới. +/// +public record CreateAdSetCommand : IRequest +{ + public Guid CampaignId { get; init; } + public string Name { get; init; } = null!; + public decimal DailyBudget { get; init; } + public string BidType { get; init; } = "cpc"; // cpc, cpm, ocpm, automatic + public decimal? BidAmount { get; init; } + + // Targeting + public int? MinAge { get; init; } + public int? MaxAge { get; init; } + public string? Genders { get; init; } + public string? Locations { get; init; } + public string? Interests { get; init; } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdSetCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdSetCommandHandler.cs new file mode 100644 index 00000000..2a31b9ca --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateAdSetCommandHandler.cs @@ -0,0 +1,50 @@ +using AdsManagerService.Domain.AggregatesModel.AdSetAggregate; +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +public class CreateAdSetCommandHandler : IRequestHandler +{ + private readonly IAdSetRepository _adSetRepository; + + public CreateAdSetCommandHandler(IAdSetRepository adSetRepository) + { + _adSetRepository = adSetRepository ?? throw new ArgumentNullException(nameof(adSetRepository)); + } + + public async Task Handle(CreateAdSetCommand request, CancellationToken cancellationToken) + { + // Create targeting + var targeting = new Targeting( + minAge: request.MinAge, + maxAge: request.MaxAge, + genders: request.Genders, + locations: request.Locations, + interests: request.Interests + ); + + // Create bid strategy + var bidStrategy = request.BidType.ToLower() switch + { + "cpc" => BidStrategy.CPC(request.BidAmount ?? 1000), + "cpm" => BidStrategy.CPM(request.BidAmount ?? 10000), + "ocpm" => BidStrategy.OCPM(request.BidAmount ?? 5000), + "automatic" => BidStrategy.Automatic(), + _ => BidStrategy.CPC(1000) + }; + + // Create ad set + var adSet = new AdSet( + campaignId: request.CampaignId, + name: request.Name, + targeting: targeting, + bidStrategy: bidStrategy, + dailyBudget: request.DailyBudget + ); + + _adSetRepository.Add(adSet); + await _adSetRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); + + return adSet.Id; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateCampaignCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateCampaignCommand.cs new file mode 100644 index 00000000..80e6f9f4 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateCampaignCommand.cs @@ -0,0 +1,20 @@ +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +/// +/// EN: Command to create a new campaign. +/// VI: Command tạo chiến dịch mới. +/// +public record CreateCampaignCommand : IRequest +{ + public Guid AdvertiserId { get; init; } + public string Name { get; init; } = null!; + public string? Description { get; init; } + public string Objective { get; init; } = null!; // "awareness", "traffic", "conversion", etc. + public string BudgetType { get; init; } = null!; // "daily" or "lifetime" + public decimal BudgetAmount { get; init; } + public string Currency { get; init; } = "VND"; + public DateTime? StartDate { get; init; } + public DateTime? EndDate { get; init; } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateCampaignCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateCampaignCommandHandler.cs new file mode 100644 index 00000000..ebcb6539 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateCampaignCommandHandler.cs @@ -0,0 +1,52 @@ +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +/// +/// EN: Handler for CreateCampaignCommand. +/// VI: Handler cho CreateCampaignCommand. +/// +public class CreateCampaignCommandHandler : IRequestHandler +{ + private readonly ICampaignRepository _campaignRepository; + + public CreateCampaignCommandHandler(ICampaignRepository campaignRepository) + { + _campaignRepository = campaignRepository ?? throw new ArgumentNullException(nameof(campaignRepository)); + } + + public async Task Handle(CreateCampaignCommand request, CancellationToken cancellationToken) + { + // Parse objective + var objective = CampaignObjective.FromName(request.Objective); + + // Create budget + var budget = request.BudgetType.ToLower() == "daily" + ? CampaignBudget.Daily(request.BudgetAmount, request.Currency) + : CampaignBudget.Lifetime(request.BudgetAmount, request.Currency); + + // Create campaign + var campaign = new Campaign( + advertiserId: request.AdvertiserId, + name: request.Name, + objective: objective, + budget: budget, + description: request.Description + ); + + // Set schedule if provided + if (request.StartDate.HasValue) + { + campaign.SetSchedule(request.StartDate.Value, request.EndDate); + } + + // Add to repository + _campaignRepository.Add(campaign); + + // Save changes + await _campaignRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); + + return campaign.Id; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateSampleCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateSampleCommand.cs deleted file mode 100644 index 691a6b3b..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateSampleCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediatR; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateSampleCommandHandler.cs deleted file mode 100644 index 1dfb8c70..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/CreateSampleCommandHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MediatR; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Commands/DeleteSampleCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/DeleteSampleCommand.cs deleted file mode 100644 index a0ccc220..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/DeleteSampleCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediatR; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/DeleteSampleCommandHandler.cs deleted file mode 100644 index 20f91d16..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/DeleteSampleCommandHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MediatR; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateCampaignCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateCampaignCommand.cs new file mode 100644 index 00000000..846302d1 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateCampaignCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +/// +/// EN: Command to update campaign information. +/// VI: Command cập nhật thông tin chiến dịch. +/// +public record UpdateCampaignCommand : IRequest +{ + public Guid CampaignId { get; init; } + public string Name { get; init; } = null!; + public string? Description { get; init; } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateCampaignCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateCampaignCommandHandler.cs new file mode 100644 index 00000000..e8fb5b68 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateCampaignCommandHandler.cs @@ -0,0 +1,27 @@ +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using MediatR; + +namespace AdsManagerService.API.Application.Commands; + +public class UpdateCampaignCommandHandler : IRequestHandler +{ + private readonly ICampaignRepository _campaignRepository; + + public UpdateCampaignCommandHandler(ICampaignRepository campaignRepository) + { + _campaignRepository = campaignRepository ?? throw new ArgumentNullException(nameof(campaignRepository)); + } + + public async Task Handle(UpdateCampaignCommand request, CancellationToken cancellationToken) + { + var campaign = await _campaignRepository.GetByIdAsync(request.CampaignId, cancellationToken); + + if (campaign == null) + return false; + + campaign.Update(request.Name, request.Description); + _campaignRepository.Update(campaign); + + return await _campaignRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateSampleCommand.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateSampleCommand.cs deleted file mode 100644 index 2d872f98..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateSampleCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MediatR; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateSampleCommandHandler.cs deleted file mode 100644 index 73739045..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/UpdateSampleCommandHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MediatR; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignByIdQuery.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignByIdQuery.cs new file mode 100644 index 00000000..ae194a13 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignByIdQuery.cs @@ -0,0 +1,34 @@ +using MediatR; + +namespace AdsManagerService.API.Application.Queries; + +/// +/// EN: Query to get campaign by ID. +/// VI: Query lấy chiến dịch theo ID. +/// +public record GetCampaignByIdQuery : IRequest +{ + public Guid CampaignId { get; init; } +} + +/// +/// EN: Campaign DTO for API responses. +/// VI: Campaign DTO cho API responses. +/// +public record CampaignDto +{ + public Guid Id { get; init; } + public Guid AdvertiserId { get; init; } + public string Name { get; init; } = null!; + public string? Description { get; init; } + public string Status { get; init; } = null!; + public string Objective { get; init; } = null!; + public string BudgetType { get; init; } = null!; + public decimal BudgetAmount { get; init; } + public string Currency { get; init; } = null!; + public decimal TotalSpend { get; init; } + public DateTime? StartDate { get; init; } + public DateTime? EndDate { get; init; } + public DateTime CreatedAt { get; init; } + public DateTime? UpdatedAt { get; init; } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignByIdQueryHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignByIdQueryHandler.cs new file mode 100644 index 00000000..f8a0222e --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignByIdQueryHandler.cs @@ -0,0 +1,40 @@ +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using MediatR; + +namespace AdsManagerService.API.Application.Queries; + +public class GetCampaignByIdQueryHandler : IRequestHandler +{ + private readonly ICampaignRepository _campaignRepository; + + public GetCampaignByIdQueryHandler(ICampaignRepository campaignRepository) + { + _campaignRepository = campaignRepository ?? throw new ArgumentNullException(nameof(campaignRepository)); + } + + public async Task Handle(GetCampaignByIdQuery request, CancellationToken cancellationToken) + { + var campaign = await _campaignRepository.GetByIdAsync(request.CampaignId, cancellationToken); + + if (campaign == null) + return null; + + return new CampaignDto + { + Id = campaign.Id, + AdvertiserId = campaign.AdvertiserId, + Name = campaign.Name, + Description = campaign.Description, + Status = campaign.Status.Name, + Objective = campaign.Objective.Name, + BudgetType = campaign.Budget.Type.ToString(), + BudgetAmount = campaign.Budget.Amount, + Currency = campaign.Budget.Currency, + TotalSpend = campaign.TotalSpend, + StartDate = campaign.StartDate, + EndDate = campaign.EndDate, + CreatedAt = campaign.CreatedAt, + UpdatedAt = campaign.UpdatedAt + }; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSampleQuery.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSampleQuery.cs deleted file mode 100644 index 3b4e70b9..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSampleQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -using MediatR; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSampleQueryHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSampleQueryHandler.cs deleted file mode 100644 index 8b3c19d0..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSampleQueryHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MediatR; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSamplesQuery.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSamplesQuery.cs deleted file mode 100644 index 9e2842e0..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSamplesQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MediatR; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSamplesQueryHandler.cs deleted file mode 100644 index 32ee3f7a..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetSamplesQueryHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using MediatR; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateSampleCommandValidator.cs deleted file mode 100644 index 07303245..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/CreateSampleCommandValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using FluentValidation; -using AdsManagerService.API.Application.Commands; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/UpdateSampleCommandValidator.cs deleted file mode 100644 index f3e9f8d2..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Validations/UpdateSampleCommandValidator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FluentValidation; -using AdsManagerService.API.Application.Commands; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.API/Controllers/AdSetsController.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdSetsController.cs new file mode 100644 index 00000000..cb8267c8 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdSetsController.cs @@ -0,0 +1,53 @@ +using AdsManagerService.API.Application.Commands; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace AdsManagerService.API.Controllers; + +/// +/// EN: API Controller for managing ad sets. +/// VI: API Controller quản lý ad sets. +/// +[ApiController] +[Route("api/v1/ads-manager/adsets")] +[Produces("application/json")] +public class AdSetsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public AdSetsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Create a new ad set. + /// VI: Tạo ad set mới. + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> CreateAdSet([FromBody] CreateAdSetCommand command) + { + _logger.LogInformation("Creating ad set for campaign {CampaignId}", command.CampaignId); + + var adSetId = await _mediator.Send(command); + + return CreatedAtAction(nameof(GetAdSetById), new { id = adSetId }, adSetId); + } + + /// + /// EN: Get ad set by ID (placeholder). + /// VI: Lấy ad set theo ID (placeholder). + /// + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetAdSetById(Guid id) + { + // TODO: Implement GetAdSetByIdQuery + return Ok(new { id, message = "Ad set details" }); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdsController.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdsController.cs new file mode 100644 index 00000000..155460aa --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdsController.cs @@ -0,0 +1,67 @@ +using AdsManagerService.API.Application.Commands; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace AdsManagerService.API.Controllers; + +/// +/// EN: API Controller for managing individual ads. +/// VI: API Controller quản lý quảng cáo. +/// +[ApiController] +[Route("api/v1/ads-manager/ads")] +[Produces("application/json")] +public class AdsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public AdsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Create a new ad. + /// VI: Tạo quảng cáo mới. + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> CreateAd([FromBody] CreateAdCommand command) + { + _logger.LogInformation("Creating ad for ad set {AdSetId}", command.AdSetId); + + var adId = await _mediator.Send(command); + + return CreatedAtAction(nameof(GetAdById), new { id = adId }, adId); + } + + /// + /// EN: Get ad by ID (placeholder). + /// VI: Lấy quảng cáo theo ID (placeholder). + /// + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetAdById(Guid id) + { + // TODO: Implement GetAdByIdQuery + return Ok(new { id, message = "Ad details" }); + } + + /// + /// EN: Submit ad for review. + /// VI: Gửi quảng cáo để duyệt. + /// + [HttpPost("{id}/submit")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task SubmitAdForReview(Guid id) + { + _logger.LogInformation("Submitting ad {AdId} for review", id); + // TODO: Implement SubmitAdForReviewCommand + return NoContent(); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/CampaignsController.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/CampaignsController.cs new file mode 100644 index 00000000..f1842ccf --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/CampaignsController.cs @@ -0,0 +1,124 @@ +using AdsManagerService.API.Application.Commands; +using AdsManagerService.API.Application.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace AdsManagerService.API.Controllers; + +/// +/// EN: API Controller for managing advertising campaigns. +/// VI: API Controller quản lý chiến dịch quảng cáo. +/// +[ApiController] +[Route("api/v1/ads-manager/campaigns")] +[Produces("application/json")] +public class CampaignsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public CampaignsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Create a new campaign. + /// VI: Tạo chiến dịch mới. + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> CreateCampaign([FromBody] CreateCampaignCommand command) + { + _logger.LogInformation("Creating campaign for advertiser {AdvertiserId}", command.AdvertiserId); + + var campaignId = await _mediator.Send(command); + + return CreatedAtAction(nameof(GetCampaignById), new { id = campaignId }, campaignId); + } + + /// + /// EN: Get campaign by ID. + /// VI: Lấy chiến dịch theo ID. + /// + [HttpGet("{id}")] + [ProducesResponseType(typeof(CampaignDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetCampaignById(Guid id) + { + var campaign = await _mediator.Send(new GetCampaignByIdQuery { CampaignId = id }); + + if (campaign == null) + return NotFound(); + + return Ok(campaign); + } + + /// + /// EN: Update campaign information. + /// VI: Cập nhật thông tin chiến dịch. + /// + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateCampaign(Guid id, [FromBody] UpdateCampaignRequest request) + { + var command = new UpdateCampaignCommand + { + CampaignId = id, + Name = request.Name, + Description = request.Description + }; + + var result = await _mediator.Send(command); + + if (!result) + return NotFound(); + + return NoContent(); + } + + /// + /// EN: Activate a campaign to start serving ads. + /// VI: Kích hoạt chiến dịch để bắt đầu chạy quảng cáo. + /// + [HttpPost("{id}/activate")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ActivateCampaign(Guid id) + { + _logger.LogInformation("Activating campaign {CampaignId}", id); + + var result = await _mediator.Send(new ActivateCampaignCommand { CampaignId = id }); + + if (!result) + return NotFound(); + + return NoContent(); + } + + /// + /// EN: Pause a campaign. + /// VI: Tạm dừng chiến dịch. + /// + [HttpPost("{id}/pause")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task PauseCampaign(Guid id) + { + // TODO: Implement PauseCampaignCommand + return NoContent(); + } +} + +/// +/// EN: Request model for updating campaign. +/// VI: Request model cập nhật chiến dịch. +/// +public record UpdateCampaignRequest +{ + public string Name { get; init; } = null!; + public string? Description { get; init; } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/SamplesController.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/SamplesController.cs deleted file mode 100644 index 538f22c7..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/SamplesController.cs +++ /dev/null @@ -1,200 +0,0 @@ -using Asp.Versioning; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using AdsManagerService.API.Application.Commands; -using AdsManagerService.API.Application.Queries; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdAggregate/Ad.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdAggregate/Ad.cs new file mode 100644 index 00000000..a02c3dc8 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdAggregate/Ad.cs @@ -0,0 +1,186 @@ +using AdsManagerService.Domain.Exceptions; +using AdsManagerService.Domain.SeedWork; + +#pragma warning disable CS0649 // Field is never assigned to (EF Core managed) + +namespace AdsManagerService.Domain.AggregatesModel.AdAggregate; + +/// +/// EN: Ad aggregate root - the creative and content for an advertisement. +/// VI: Ad aggregate root - creative và nội dung cho một quảng cáo. +/// +public class Ad : Entity, IAggregateRoot +{ + private string _name = null!; + private Guid _adSetId; + private AdFormat _format = null!; + private AdStatus _status = null!; + private AdReviewStatus _reviewStatus = null!; + private string? _headline; + private string? _primaryText; + private string? _description; + private string? _callToAction; + private string? _destinationUrl; + private string? _creativeUrl; + private DateTime _createdAt; + private DateTime? _updatedAt; + + public string Name => _name; + public Guid AdSetId => _adSetId; + public AdFormat Format => _format; + public int FormatId { get; private set; } + public AdStatus Status => _status; + public int StatusId { get; private set; } + public AdReviewStatus ReviewStatus => _reviewStatus; + public int ReviewStatusId { get; private set; } + public string? Headline => _headline; + public string? PrimaryText => _primaryText; + public string? Description => _description; + public string? CallToAction => _callToAction; + public string? DestinationUrl => _destinationUrl; + public string? CreativeUrl => _creativeUrl; + public DateTime CreatedAt => _createdAt; + public DateTime? UpdatedAt => _updatedAt; + + protected Ad() { } + + public Ad( + Guid adSetId, + string name, + AdFormat format, + string? headline = null, + string? primaryText = null, + string? callToAction = null, + string? destinationUrl = null, + string? creativeUrl = null) : this() + { + if (string.IsNullOrWhiteSpace(name)) + throw new AdsDomainException("Ad name cannot be empty"); + + Id = Guid.NewGuid(); + _adSetId = adSetId; + _name = name; + _format = format; + FormatId = format.Id; + _headline = headline; + _primaryText = primaryText; + _callToAction = callToAction; + _destinationUrl = destinationUrl; + _creativeUrl = creativeUrl; + _status = AdStatus.Draft; + StatusId = AdStatus.Draft.Id; + _reviewStatus = AdReviewStatus.NotSubmitted; + ReviewStatusId = AdReviewStatus.NotSubmitted.Id; + _createdAt = DateTime.UtcNow; + } + + public void Update(string name, string? headline, string? primaryText, string? callToAction, string? destinationUrl) + { + if (string.IsNullOrWhiteSpace(name)) + throw new AdsDomainException("Ad name cannot be empty"); + + _name = name; + _headline = headline; + _primaryText = primaryText; + _callToAction = callToAction; + _destinationUrl = destinationUrl; + _updatedAt = DateTime.UtcNow; + + // Reset review if content changed + if (_reviewStatus == AdReviewStatus.Approved) + { + _reviewStatus = AdReviewStatus.PendingReview; + ReviewStatusId = AdReviewStatus.PendingReview.Id; + } + } + + public void SetCreative(string creativeUrl) + { + _creativeUrl = creativeUrl; + _updatedAt = DateTime.UtcNow; + } + + public void SubmitForReview() + { + if (_reviewStatus != AdReviewStatus.NotSubmitted && _reviewStatus != AdReviewStatus.Rejected) + throw new AdsDomainException("Ad is already submitted or approved"); + + _reviewStatus = AdReviewStatus.PendingReview; + ReviewStatusId = AdReviewStatus.PendingReview.Id; + _updatedAt = DateTime.UtcNow; + } + + public void Approve() + { + if (_reviewStatus != AdReviewStatus.PendingReview) + throw new AdsDomainException("Only pending ads can be approved"); + + _reviewStatus = AdReviewStatus.Approved; + ReviewStatusId = AdReviewStatus.Approved.Id; + _updatedAt = DateTime.UtcNow; + } + + public void Reject(string reason) + { + if (_reviewStatus != AdReviewStatus.PendingReview) + throw new AdsDomainException("Only pending ads can be rejected"); + + _reviewStatus = AdReviewStatus.Rejected; + ReviewStatusId = AdReviewStatus.Rejected.Id; + _updatedAt = DateTime.UtcNow; + } + + public void Activate() + { + if (_reviewStatus != AdReviewStatus.Approved) + throw new AdsDomainException("Only approved ads can be activated"); + + _status = AdStatus.Active; + StatusId = AdStatus.Active.Id; + _updatedAt = DateTime.UtcNow; + } + + public void Pause() + { + if (_status != AdStatus.Active) + throw new AdsDomainException("Only active ads can be paused"); + + _status = AdStatus.Paused; + StatusId = AdStatus.Paused.Id; + _updatedAt = DateTime.UtcNow; + } +} + +public class AdFormat : Enumeration +{ + public static readonly AdFormat SingleImage = new(1, "single_image"); + public static readonly AdFormat SingleVideo = new(2, "single_video"); + public static readonly AdFormat Carousel = new(3, "carousel"); + public static readonly AdFormat Collection = new(4, "collection"); + public static readonly AdFormat Stories = new(5, "stories"); + + public AdFormat(int id, string name) : base(id, name) { } + public static IEnumerable List() => new[] { SingleImage, SingleVideo, Carousel, Collection, Stories }; +} + +public class AdStatus : Enumeration +{ + public static readonly AdStatus Draft = new(1, nameof(Draft).ToLowerInvariant()); + public static readonly AdStatus Active = new(2, nameof(Active).ToLowerInvariant()); + public static readonly AdStatus Paused = new(3, nameof(Paused).ToLowerInvariant()); + public static readonly AdStatus Archived = new(4, nameof(Archived).ToLowerInvariant()); + + public AdStatus(int id, string name) : base(id, name) { } + public static IEnumerable List() => new[] { Draft, Active, Paused, Archived }; +} + +public class AdReviewStatus : Enumeration +{ + public static readonly AdReviewStatus NotSubmitted = new(1, "not_submitted"); + public static readonly AdReviewStatus PendingReview = new(2, "pending_review"); + public static readonly AdReviewStatus Approved = new(3, nameof(Approved).ToLowerInvariant()); + public static readonly AdReviewStatus Rejected = new(4, nameof(Rejected).ToLowerInvariant()); + + public AdReviewStatus(int id, string name) : base(id, name) { } + public static IEnumerable List() => new[] { NotSubmitted, PendingReview, Approved, Rejected }; +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdAggregate/IAdRepository.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdAggregate/IAdRepository.cs new file mode 100644 index 00000000..1e0d141c --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdAggregate/IAdRepository.cs @@ -0,0 +1,14 @@ +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.AdAggregate; + +/// +/// EN: Repository interface for Ad aggregate. +/// VI: Interface repository cho Ad aggregate. +/// +public interface IAdRepository : IRepository +{ + Ad Add(Ad ad); + void Update(Ad ad); + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/AdSet.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/AdSet.cs new file mode 100644 index 00000000..1583417c --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/AdSet.cs @@ -0,0 +1,150 @@ +using AdsManagerService.Domain.Exceptions; +using AdsManagerService.Domain.SeedWork; + +#pragma warning disable CS0649 // Field is never assigned to (EF Core managed) + +namespace AdsManagerService.Domain.AggregatesModel.AdSetAggregate; + +/// +/// EN: Ad Set aggregate root - targeting and budget settings for a group of ads. +/// VI: Ad Set aggregate root - cài đặt targeting và ngân sách cho một nhóm quảng cáo. +/// +public class AdSet : Entity, IAggregateRoot +{ + private string _name = null!; + private Guid _campaignId; + private AdSetStatus _status = null!; + private Targeting _targeting = null!; + private BidStrategy _bidStrategy = null!; + private decimal _dailyBudget; + private DateTime? _startDate; + private DateTime? _endDate; + private DateTime _createdAt; + private DateTime? _updatedAt; + + /// + /// EN: Ad Set name. + /// VI: Tên Ad Set. + /// + public string Name => _name; + + /// + /// EN: Parent campaign ID. + /// VI: ID chiến dịch cha. + /// + public Guid CampaignId => _campaignId; + + /// + /// EN: Current status. + /// VI: Trạng thái hiện tại. + /// + public AdSetStatus Status => _status; + public int StatusId { get; private set; } + + /// + /// EN: Targeting settings. + /// VI: Cài đặt targeting. + /// + public Targeting Targeting => _targeting; + + /// + /// EN: Bid strategy. + /// VI: Chiến lược giá thầu. + /// + public BidStrategy BidStrategy => _bidStrategy; + + /// + /// EN: Daily budget. + /// VI: Ngân sách hàng ngày. + /// + public decimal DailyBudget => _dailyBudget; + + public DateTime? StartDate => _startDate; + public DateTime? EndDate => _endDate; + public DateTime CreatedAt => _createdAt; + public DateTime? UpdatedAt => _updatedAt; + + protected AdSet() { } + + public AdSet( + Guid campaignId, + string name, + Targeting targeting, + BidStrategy bidStrategy, + decimal dailyBudget) : this() + { + if (string.IsNullOrWhiteSpace(name)) + throw new AdsDomainException("Ad Set name cannot be empty"); + if (dailyBudget <= 0) + throw new AdsDomainException("Daily budget must be greater than zero"); + + Id = Guid.NewGuid(); + _campaignId = campaignId; + _name = name; + _targeting = targeting; + _bidStrategy = bidStrategy; + _dailyBudget = dailyBudget; + _status = AdSetStatus.Draft; + StatusId = AdSetStatus.Draft.Id; + _createdAt = DateTime.UtcNow; + } + + public void Update(string name, decimal dailyBudget) + { + if (string.IsNullOrWhiteSpace(name)) + throw new AdsDomainException("Ad Set name cannot be empty"); + + _name = name; + _dailyBudget = dailyBudget; + _updatedAt = DateTime.UtcNow; + } + + public void SetTargeting(Targeting targeting) + { + _targeting = targeting ?? throw new AdsDomainException("Targeting cannot be null"); + _updatedAt = DateTime.UtcNow; + } + + public void SetBidStrategy(BidStrategy bidStrategy) + { + _bidStrategy = bidStrategy ?? throw new AdsDomainException("Bid strategy cannot be null"); + _updatedAt = DateTime.UtcNow; + } + + public void Activate() + { + if (_status != AdSetStatus.Draft && _status != AdSetStatus.Paused) + throw new AdsDomainException("Only draft or paused ad sets can be activated"); + + _status = AdSetStatus.Active; + StatusId = AdSetStatus.Active.Id; + _updatedAt = DateTime.UtcNow; + } + + public void Pause() + { + if (_status != AdSetStatus.Active) + throw new AdsDomainException("Only active ad sets can be paused"); + + _status = AdSetStatus.Paused; + StatusId = AdSetStatus.Paused.Id; + _updatedAt = DateTime.UtcNow; + } +} + +/// +/// EN: Ad Set status enumeration. +/// VI: Enum trạng thái Ad Set. +/// +public class AdSetStatus : Enumeration +{ + public static readonly AdSetStatus Draft = new(1, nameof(Draft).ToLowerInvariant()); + public static readonly AdSetStatus Active = new(2, nameof(Active).ToLowerInvariant()); + public static readonly AdSetStatus Paused = new(3, nameof(Paused).ToLowerInvariant()); + public static readonly AdSetStatus Archived = new(4, nameof(Archived).ToLowerInvariant()); + + public AdSetStatus(int id, string name) : base(id, name) { } + + public static IEnumerable List() => + new[] { Draft, Active, Paused, Archived }; +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/BidStrategy.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/BidStrategy.cs new file mode 100644 index 00000000..61e194f2 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/BidStrategy.cs @@ -0,0 +1,90 @@ +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.AdSetAggregate; + +/// +/// EN: Bid strategy for an Ad Set. +/// VI: Chiến lược giá thầu cho Ad Set. +/// +public class BidStrategy : ValueObject +{ + /// + /// EN: Bid type (CPC, CPM, OCPM, Target Cost). + /// VI: Loại giá thầu (CPC, CPM, OCPM, Target Cost). + /// + public BidType Type { get; private set; } + + /// + /// EN: Bid amount (for manual bidding). + /// VI: Số tiền giá thầu (cho trường hợp thầu thủ công). + /// + public decimal? BidAmount { get; private set; } + + /// + /// EN: Target cost per result (for OCPM/Target Cost). + /// VI: Chi phí mục tiêu mỗi kết quả (cho OCPM/Target Cost). + /// + public decimal? TargetCost { get; private set; } + + protected BidStrategy() { } + + public BidStrategy(BidType type, decimal? bidAmount = null, decimal? targetCost = null) + { + Type = type; + BidAmount = bidAmount; + TargetCost = targetCost; + } + + /// + /// EN: Create CPC (Cost Per Click) strategy. + /// VI: Tạo chiến lược CPC (Chi phí mỗi Click). + /// + public static BidStrategy CPC(decimal bidAmount) => new(BidType.CPC, bidAmount); + + /// + /// EN: Create CPM (Cost Per Mille) strategy. + /// VI: Tạo chiến lược CPM (Chi phí mỗi 1000 hiển thị). + /// + public static BidStrategy CPM(decimal bidAmount) => new(BidType.CPM, bidAmount); + + /// + /// EN: Create OCPM (Optimized CPM) strategy. + /// VI: Tạo chiến lược OCPM (CPM tối ưu). + /// + public static BidStrategy OCPM(decimal targetCost) => new(BidType.OCPM, null, targetCost); + + /// + /// EN: Create automatic bidding strategy. + /// VI: Tạo chiến lược thầu tự động. + /// + public static BidStrategy Automatic() => new(BidType.Automatic); + + protected override IEnumerable GetEqualityComponents() + { + yield return Type; + yield return BidAmount ?? 0; + yield return TargetCost ?? 0; + } +} + +/// +/// EN: Bid type enumeration. +/// VI: Enum loại giá thầu. +/// +public enum BidType +{ + /// EN: Cost Per Click / VI: Chi phí mỗi Click + CPC = 1, + + /// EN: Cost Per Mille (1000 impressions) / VI: Chi phí mỗi 1000 hiển thị + CPM = 2, + + /// EN: Optimized CPM / VI: CPM tối ưu + OCPM = 3, + + /// EN: Target Cost per result / VI: Chi phí mục tiêu mỗi kết quả + TargetCost = 4, + + /// EN: Automatic bidding / VI: Thầu tự động + Automatic = 5 +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/IAdSetRepository.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/IAdSetRepository.cs new file mode 100644 index 00000000..b744296e --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/IAdSetRepository.cs @@ -0,0 +1,15 @@ +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.AdSetAggregate; + +/// +/// EN: Repository interface for AdSet aggregate. +/// VI: Interface repository cho AdSet aggregate. +/// +public interface IAdSetRepository : IRepository +{ + AdSet Add(AdSet adSet); + void Update(AdSet adSet); + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetByCampaignIdAsync(Guid campaignId, CancellationToken cancellationToken = default); +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/Targeting.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/Targeting.cs new file mode 100644 index 00000000..e2527c35 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AdSetAggregate/Targeting.cs @@ -0,0 +1,96 @@ +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.AdSetAggregate; + +/// +/// EN: Targeting settings for an Ad Set. +/// VI: Cài đặt targeting cho Ad Set. +/// +public class Targeting : ValueObject +{ + /// + /// EN: Minimum age for targeting. + /// VI: Tuổi tối thiểu để nhắm mục tiêu. + /// + public int? MinAge { get; private set; } + + /// + /// EN: Maximum age for targeting. + /// VI: Tuổi tối đa để nhắm mục tiêu. + /// + public int? MaxAge { get; private set; } + + /// + /// EN: Target genders (male, female, all). + /// VI: Giới tính nhắm mục tiêu (nam, nữ, tất cả). + /// + public string? Genders { get; private set; } + + /// + /// EN: Target locations (comma-separated). + /// VI: Vị trí nhắm mục tiêu (phân cách bằng dấu phẩy). + /// + public string? Locations { get; private set; } + + /// + /// EN: Target interests (comma-separated). + /// VI: Sở thích nhắm mục tiêu (phân cách bằng dấu phẩy). + /// + public string? Interests { get; private set; } + + /// + /// EN: Custom audience IDs (comma-separated). + /// VI: ID đối tượng tùy chỉnh (phân cách bằng dấu phẩy). + /// + public string? CustomAudienceIds { get; private set; } + + /// + /// EN: Lookalike audience IDs (comma-separated). + /// VI: ID đối tượng tương tự (phân cách bằng dấu phẩy). + /// + public string? LookalikeAudienceIds { get; private set; } + + protected Targeting() { } + + public Targeting( + int? minAge = null, + int? maxAge = null, + string? genders = null, + string? locations = null, + string? interests = null, + string? customAudienceIds = null, + string? lookalikeAudienceIds = null) + { + MinAge = minAge; + MaxAge = maxAge; + Genders = genders; + Locations = locations; + Interests = interests; + CustomAudienceIds = customAudienceIds; + LookalikeAudienceIds = lookalikeAudienceIds; + } + + /// + /// EN: Create broad targeting (everyone). + /// VI: Tạo targeting rộng (tất cả mọi người). + /// + public static Targeting Broad() => new(); + + /// + /// EN: Create demographic targeting. + /// VI: Tạo targeting theo nhân khẩu học. + /// + public static Targeting Demographic(int minAge, int maxAge, string? genders = null, string? locations = null) => + new(minAge, maxAge, genders, locations); + + protected override IEnumerable GetEqualityComponents() + { + yield return MinAge ?? 0; + yield return MaxAge ?? 0; + yield return Genders ?? ""; + yield return Locations ?? ""; + yield return Interests ?? ""; + yield return CustomAudienceIds ?? ""; + yield return LookalikeAudienceIds ?? ""; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AudienceAggregate/Audience.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AudienceAggregate/Audience.cs new file mode 100644 index 00000000..40fc9849 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/AudienceAggregate/Audience.cs @@ -0,0 +1,122 @@ +using AdsManagerService.Domain.Exceptions; +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.AudienceAggregate; + +/// +/// EN: Custom Audience aggregate root - uploaded lists or website visitors. +/// VI: Custom Audience aggregate root - danh sách upload hoặc khách truy cập website. +/// +public class CustomAudience : Entity, IAggregateRoot +{ + private string _name = null!; + private Guid _advertiserId; + private AudienceSource _source = null!; + private int _size; + private DateTime _createdAt; + private DateTime? _updatedAt; + + public string Name => _name; + public Guid AdvertiserId => _advertiserId; + public AudienceSource Source => _source; + public int SourceId { get; private set; } + public int Size => _size; + public DateTime CreatedAt => _createdAt; + public DateTime? UpdatedAt => _updatedAt; + + protected CustomAudience() { } + + public CustomAudience( + Guid advertiserId, + string name, + AudienceSource source, + int size = 0) : this() + { + if (string.IsNullOrWhiteSpace(name)) + throw new AdsDomainException("Audience name cannot be empty"); + + Id = Guid.NewGuid(); + _advertiserId = advertiserId; + _name = name; + _source = source; + SourceId = source.Id; + _size = size; + _createdAt = DateTime.UtcNow; + } + + public void UpdateSize(int size) + { + _size = size; + _updatedAt = DateTime.UtcNow; + } +} + +/// +/// EN: Lookalike Audience - similar users to a source audience. +/// VI: Lookalike Audience - người dùng tương tự với audience nguồn. +/// +public class LookalikeAudience : Entity, IAggregateRoot +{ + private string _name = null!; + private Guid _advertiserId; + private Guid _sourceAudienceId; + private int _similarityPercentage; + private string? _location; + private int _size; + private DateTime _createdAt; + + public string Name => _name; + public Guid AdvertiserId => _advertiserId; + public Guid SourceAudienceId => _sourceAudienceId; + public int SimilarityPercentage => _similarityPercentage; + public string? Location => _location; + public int Size => _size; + public DateTime CreatedAt => _createdAt; + + protected LookalikeAudience() { } + + public LookalikeAudience( + Guid advertiserId, + string name, + Guid sourceAudienceId, + int similarityPercentage, + string? location = null) : this() + { + if (string.IsNullOrWhiteSpace(name)) + throw new AdsDomainException("Lookalike audience name cannot be empty"); + + if (similarityPercentage < 1 || similarityPercentage > 10) + throw new AdsDomainException("Similarity percentage must be between 1% and 10%"); + + Id = Guid.NewGuid(); + _advertiserId = advertiserId; + _name = name; + _sourceAudienceId = sourceAudienceId; + _similarityPercentage = similarityPercentage; + _location = location; + _size = 0; + _createdAt = DateTime.UtcNow; + } + + public void UpdateSize(int size) + { + _size = size; + } +} + +/// +/// EN: Audience source type. +/// VI: Loại nguồn audience. +/// +public class AudienceSource : Enumeration +{ + public static readonly AudienceSource CustomerList = new(1, "customer_list"); + public static readonly AudienceSource WebsiteVisitors = new(2, "website_visitors"); + public static readonly AudienceSource AppUsers = new(3, "app_users"); + public static readonly AudienceSource EngagementCustomers = new(4, "engagement_customers"); + + public AudienceSource(int id, string name) : base(id, name) { } + + public static IEnumerable List() => + new[] { CustomerList, WebsiteVisitors, AppUsers, EngagementCustomers }; +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/Campaign.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/Campaign.cs new file mode 100644 index 00000000..9b5098fd --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/Campaign.cs @@ -0,0 +1,246 @@ +using AdsManagerService.Domain.Events; +using AdsManagerService.Domain.Exceptions; +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.CampaignAggregate; + +/// +/// EN: Campaign aggregate root - the top level of the 3-tier ads structure. +/// VI: Campaign aggregate root - cấp trên nhất của cấu trúc ads 3 cấp. +/// +public class Campaign : Entity, IAggregateRoot +{ + // Private fields for encapsulation + private string _name = null!; + private string? _description; + private Guid _advertiserId; + private CampaignStatus _status = null!; + private CampaignObjective _objective = null!; + private CampaignBudget _budget = null!; + private DateTime? _startDate; + private DateTime? _endDate; + private DateTime _createdAt; + private DateTime? _updatedAt; + + /// + /// EN: Campaign name. + /// VI: Tên chiến dịch. + /// + public string Name => _name; + + /// + /// EN: Campaign description. + /// VI: Mô tả chiến dịch. + /// + public string? Description => _description; + + /// + /// EN: Advertiser who owns this campaign. + /// VI: Nhà quảng cáo sở hữu chiến dịch này. + /// + public Guid AdvertiserId => _advertiserId; + + /// + /// EN: Current campaign status. + /// VI: Trạng thái hiện tại của chiến dịch. + /// + public CampaignStatus Status => _status; + public int StatusId { get; private set; } + + /// + /// EN: Campaign objective (Awareness, Traffic, Conversion). + /// VI: Mục tiêu chiến dịch (Nhận diện, Traffic, Chuyển đổi). + /// + public CampaignObjective Objective => _objective; + public int ObjectiveId { get; private set; } + + /// + /// EN: Campaign budget settings. + /// VI: Cài đặt ngân sách chiến dịch. + /// + public CampaignBudget Budget => _budget; + + /// + /// EN: Campaign start date. + /// VI: Ngày bắt đầu chiến dịch. + /// + public DateTime? StartDate => _startDate; + + /// + /// EN: Campaign end date. + /// VI: Ngày kết thúc chiến dịch. + /// + public DateTime? EndDate => _endDate; + + /// + /// 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: Total spend so far. + /// VI: Tổng chi tiêu đến hiện tại. + /// + public decimal TotalSpend { get; private set; } + + // EF Core constructor + protected Campaign() { } + + /// + /// EN: Create a new campaign. + /// VI: Tạo chiến dịch mới. + /// + public Campaign( + Guid advertiserId, + string name, + CampaignObjective objective, + CampaignBudget budget, + string? description = null) : this() + { + if (string.IsNullOrWhiteSpace(name)) + throw new AdsDomainException("Campaign name cannot be empty"); + + Id = Guid.NewGuid(); + _advertiserId = advertiserId; + _name = name; + _description = description; + _objective = objective; + ObjectiveId = objective.Id; + _budget = budget; + _status = CampaignStatus.Draft; + StatusId = CampaignStatus.Draft.Id; + _createdAt = DateTime.UtcNow; + TotalSpend = 0; + + AddDomainEvent(new CampaignCreatedDomainEvent(this)); + } + + /// + /// EN: Update campaign information. + /// VI: Cập nhật thông tin chiến dịch. + /// + public void Update(string name, string? description) + { + if (string.IsNullOrWhiteSpace(name)) + throw new AdsDomainException("Campaign name cannot be empty"); + + if (_status == CampaignStatus.Archived) + throw new AdsDomainException("Cannot update an archived campaign"); + + _name = name; + _description = description; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Set campaign schedule. + /// VI: Đặt lịch chiến dịch. + /// + public void SetSchedule(DateTime startDate, DateTime? endDate = null) + { + if (endDate.HasValue && endDate.Value <= startDate) + throw new AdsDomainException("End date must be after start date"); + + _startDate = startDate; + _endDate = endDate; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Update campaign budget. + /// VI: Cập nhật ngân sách chiến dịch. + /// + public void SetBudget(CampaignBudget budget) + { + _budget = budget ?? throw new AdsDomainException("Budget cannot be null"); + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Activate the campaign (start running ads). + /// VI: Kích hoạt chiến dịch (bắt đầu chạy quảng cáo). + /// + public void Activate() + { + if (_status != CampaignStatus.Draft && _status != CampaignStatus.Paused) + throw new AdsDomainException("Only draft or paused campaigns can be activated"); + + var previousStatus = _status; + _status = CampaignStatus.Active; + StatusId = CampaignStatus.Active.Id; + _updatedAt = DateTime.UtcNow; + + AddDomainEvent(new CampaignActivatedDomainEvent(Id, previousStatus, _status)); + } + + /// + /// EN: Pause the campaign. + /// VI: Tạm dừng chiến dịch. + /// + public void Pause() + { + if (_status != CampaignStatus.Active) + throw new AdsDomainException("Only active campaigns can be paused"); + + var previousStatus = _status; + _status = CampaignStatus.Paused; + StatusId = CampaignStatus.Paused.Id; + _updatedAt = DateTime.UtcNow; + + AddDomainEvent(new CampaignStatusChangedDomainEvent(Id, previousStatus, _status)); + } + + /// + /// EN: Complete the campaign. + /// VI: Hoàn thành chiến dịch. + /// + public void Complete() + { + if (_status != CampaignStatus.Active) + throw new AdsDomainException("Only active campaigns can be completed"); + + var previousStatus = _status; + _status = CampaignStatus.Completed; + StatusId = CampaignStatus.Completed.Id; + _updatedAt = DateTime.UtcNow; + + AddDomainEvent(new CampaignStatusChangedDomainEvent(Id, previousStatus, _status)); + } + + /// + /// EN: Archive the campaign. + /// VI: Lưu trữ chiến dịch. + /// + public void Archive() + { + if (_status == CampaignStatus.Active) + throw new AdsDomainException("Cannot archive an active campaign. Pause it first."); + + var previousStatus = _status; + _status = CampaignStatus.Archived; + StatusId = CampaignStatus.Archived.Id; + _updatedAt = DateTime.UtcNow; + + AddDomainEvent(new CampaignStatusChangedDomainEvent(Id, previousStatus, _status)); + } + + /// + /// EN: Record spend for this campaign. + /// VI: Ghi nhận chi tiêu cho chiến dịch này. + /// + public void RecordSpend(decimal amount) + { + if (amount < 0) + throw new AdsDomainException("Spend amount cannot be negative"); + + TotalSpend += amount; + _updatedAt = DateTime.UtcNow; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignBudget.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignBudget.cs new file mode 100644 index 00000000..c2819a0f --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignBudget.cs @@ -0,0 +1,80 @@ +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.CampaignAggregate; + +/// +/// EN: Campaign budget value object representing daily or lifetime budget. +/// VI: Value object ngân sách chiến dịch đại diện cho ngân sách ngày hoặc tổng. +/// +public class CampaignBudget : ValueObject +{ + /// + /// EN: Budget type (Daily or Lifetime). + /// VI: Loại ngân sách (Hàng ngày hoặc Tổng). + /// + public BudgetType Type { get; private set; } + + /// + /// EN: Budget amount in the account's currency. + /// VI: Số tiền ngân sách theo đơn vị tiền tệ của tài khoản. + /// + public decimal Amount { get; private set; } + + /// + /// EN: Currency code (e.g., VND, USD). + /// VI: Mã tiền tệ (ví dụ: VND, USD). + /// + public string Currency { get; private set; } = "VND"; + + protected CampaignBudget() { } + + public CampaignBudget(BudgetType type, decimal amount, string currency = "VND") + { + if (amount <= 0) + throw new ArgumentException("Budget amount must be greater than zero", nameof(amount)); + + Type = type; + Amount = amount; + Currency = currency; + } + + /// + /// EN: Create a daily budget. + /// VI: Tạo ngân sách hàng ngày. + /// + public static CampaignBudget Daily(decimal amount, string currency = "VND") => + new(BudgetType.Daily, amount, currency); + + /// + /// EN: Create a lifetime budget. + /// VI: Tạo ngân sách tổng. + /// + public static CampaignBudget Lifetime(decimal amount, string currency = "VND") => + new(BudgetType.Lifetime, amount, currency); + + protected override IEnumerable GetEqualityComponents() + { + yield return Type; + yield return Amount; + yield return Currency; + } +} + +/// +/// EN: Budget type enumeration. +/// VI: Enum loại ngân sách. +/// +public enum BudgetType +{ + /// + /// EN: Daily budget - resets each day. + /// VI: Ngân sách ngày - reset mỗi ngày. + /// + Daily = 1, + + /// + /// EN: Lifetime budget - total for campaign duration. + /// VI: Ngân sách tổng - tổng cho toàn bộ thời gian chiến dịch. + /// + Lifetime = 2 +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignObjective.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignObjective.cs new file mode 100644 index 00000000..39ce785d --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignObjective.cs @@ -0,0 +1,61 @@ +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.CampaignAggregate; + +/// +/// EN: Campaign objective enumeration (Awareness, Traffic, Conversion). +/// VI: Enum mục tiêu chiến dịch (Nhận diện, Traffic, Chuyển đổi). +/// +public class CampaignObjective : Enumeration +{ + /// + /// EN: Brand awareness objective - maximize reach and impressions. + /// VI: Mục tiêu nhận diện thương hiệu - tối đa hóa reach và impressions. + /// + public static readonly CampaignObjective Awareness = new(1, nameof(Awareness).ToLowerInvariant()); + + /// + /// EN: Traffic objective - drive clicks to website/app. + /// VI: Mục tiêu traffic - tăng click vào website/app. + /// + public static readonly CampaignObjective Traffic = new(2, nameof(Traffic).ToLowerInvariant()); + + /// + /// EN: Conversion objective - optimize for purchases/leads. + /// VI: Mục tiêu chuyển đổi - tối ưu cho mua hàng/đăng ký. + /// + public static readonly CampaignObjective Conversion = new(3, nameof(Conversion).ToLowerInvariant()); + + /// + /// EN: App installs objective. + /// VI: Mục tiêu cài đặt app. + /// + public static readonly CampaignObjective AppInstalls = new(4, "app_installs"); + + /// + /// EN: Video views objective. + /// VI: Mục tiêu xem video. + /// + public static readonly CampaignObjective VideoViews = new(5, "video_views"); + + /// + /// EN: Lead generation objective. + /// VI: Mục tiêu thu thập lead. + /// + public static readonly CampaignObjective LeadGeneration = new(6, "lead_generation"); + + public CampaignObjective(int id, string name) : base(id, name) + { + } + + public static IEnumerable List() => + new[] { Awareness, Traffic, Conversion, AppInstalls, VideoViews, LeadGeneration }; + + public static CampaignObjective FromName(string name) => + List().FirstOrDefault(s => string.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)) + ?? throw new ArgumentException($"Invalid campaign objective: {name}"); + + public static CampaignObjective From(int id) => + List().FirstOrDefault(s => s.Id == id) + ?? throw new ArgumentException($"Invalid campaign objective id: {id}"); +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignStatus.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignStatus.cs new file mode 100644 index 00000000..81301fdc --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/CampaignStatus.cs @@ -0,0 +1,43 @@ +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.CampaignAggregate; + +/// +/// EN: Campaign status enumeration using smart enum pattern. +/// VI: Enum trạng thái chiến dịch sử dụng smart enum pattern. +/// +public class CampaignStatus : Enumeration +{ + public static readonly CampaignStatus Draft = new(1, nameof(Draft).ToLowerInvariant()); + public static readonly CampaignStatus Active = new(2, nameof(Active).ToLowerInvariant()); + public static readonly CampaignStatus Paused = new(3, nameof(Paused).ToLowerInvariant()); + public static readonly CampaignStatus Completed = new(4, nameof(Completed).ToLowerInvariant()); + public static readonly CampaignStatus Archived = new(5, nameof(Archived).ToLowerInvariant()); + + public CampaignStatus(int id, string name) : base(id, name) + { + } + + /// + /// EN: Get all possible campaign statuses. + /// VI: Lấy tất cả trạng thái chiến dịch có thể. + /// + public static IEnumerable List() => + new[] { Draft, Active, Paused, Completed, Archived }; + + /// + /// EN: Get status by name. + /// VI: Lấy trạng thái theo tên. + /// + public static CampaignStatus FromName(string name) => + List().FirstOrDefault(s => string.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)) + ?? throw new ArgumentException($"Invalid campaign status: {name}"); + + /// + /// EN: Get status by id. + /// VI: Lấy trạng thái theo id. + /// + public static CampaignStatus From(int id) => + List().FirstOrDefault(s => s.Id == id) + ?? throw new ArgumentException($"Invalid campaign status id: {id}"); +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/ICampaignRepository.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/ICampaignRepository.cs new file mode 100644 index 00000000..d0a1d221 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/CampaignAggregate/ICampaignRepository.cs @@ -0,0 +1,40 @@ +using AdsManagerService.Domain.SeedWork; + +namespace AdsManagerService.Domain.AggregatesModel.CampaignAggregate; + +/// +/// EN: Repository interface for Campaign aggregate. +/// VI: Interface repository cho Campaign aggregate. +/// +public interface ICampaignRepository : IRepository +{ + /// + /// EN: Add a new campaign. + /// VI: Thêm chiến dịch mới. + /// + Campaign Add(Campaign campaign); + + /// + /// EN: Update an existing campaign. + /// VI: Cập nhật chiến dịch hiện có. + /// + void Update(Campaign campaign); + + /// + /// EN: Get campaign by ID. + /// VI: Lấy chiến dịch theo ID. + /// + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// EN: Get campaigns by advertiser ID. + /// VI: Lấy danh sách chiến dịch theo ID nhà quảng cáo. + /// + Task> GetByAdvertiserIdAsync(Guid advertiserId, CancellationToken cancellationToken = default); + + /// + /// EN: Get active campaigns. + /// VI: Lấy danh sách chiến dịch đang hoạt động. + /// + Task> GetActiveAsync(CancellationToken cancellationToken = default); +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs deleted file mode 100644 index 66186c12..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs +++ /dev/null @@ -1,61 +0,0 @@ -using AdsManagerService.Domain.SeedWork; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/Sample.cs deleted file mode 100644 index 0cc63ba5..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/Sample.cs +++ /dev/null @@ -1,158 +0,0 @@ -using AdsManagerService.Domain.Events; -using AdsManagerService.Domain.Exceptions; -using AdsManagerService.Domain.SeedWork; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs deleted file mode 100644 index 386f998c..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs +++ /dev/null @@ -1,77 +0,0 @@ -using AdsManagerService.Domain.SeedWork; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Domain/Events/CampaignDomainEvents.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/Events/CampaignDomainEvents.cs new file mode 100644 index 00000000..57064b74 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/Events/CampaignDomainEvents.cs @@ -0,0 +1,54 @@ +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using MediatR; + +namespace AdsManagerService.Domain.Events; + +/// +/// EN: Domain event raised when a new campaign is created. +/// VI: Domain event phát ra khi chiến dịch mới được tạo. +/// +public class CampaignCreatedDomainEvent : INotification +{ + public Campaign Campaign { get; } + + public CampaignCreatedDomainEvent(Campaign campaign) + { + Campaign = campaign; + } +} + +/// +/// EN: Domain event raised when a campaign is activated. +/// VI: Domain event phát ra khi chiến dịch được kích hoạt. +/// +public class CampaignActivatedDomainEvent : INotification +{ + public Guid CampaignId { get; } + public CampaignStatus PreviousStatus { get; } + public CampaignStatus NewStatus { get; } + + public CampaignActivatedDomainEvent(Guid campaignId, CampaignStatus previousStatus, CampaignStatus newStatus) + { + CampaignId = campaignId; + PreviousStatus = previousStatus; + NewStatus = newStatus; + } +} + +/// +/// EN: Domain event raised when campaign status changes. +/// VI: Domain event phát ra khi trạng thái chiến dịch thay đổi. +/// +public class CampaignStatusChangedDomainEvent : INotification +{ + public Guid CampaignId { get; } + public CampaignStatus PreviousStatus { get; } + public CampaignStatus NewStatus { get; } + + public CampaignStatusChangedDomainEvent(Guid campaignId, CampaignStatus previousStatus, CampaignStatus newStatus) + { + CampaignId = campaignId; + PreviousStatus = previousStatus; + NewStatus = newStatus; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/Events/SampleCreatedDomainEvent.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/Events/SampleCreatedDomainEvent.cs deleted file mode 100644 index 5b445afe..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Domain/Events/SampleCreatedDomainEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using MediatR; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/Events/SampleStatusChangedDomainEvent.cs deleted file mode 100644 index 9ea657c5..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Domain/Events/SampleStatusChangedDomainEvent.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MediatR; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Domain/Exceptions/AdsDomainException.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/Exceptions/AdsDomainException.cs new file mode 100644 index 00000000..9abadf0d --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Domain/Exceptions/AdsDomainException.cs @@ -0,0 +1,22 @@ +namespace AdsManagerService.Domain.Exceptions; + +/// +/// EN: Base exception for all Ads domain exceptions. +/// VI: Exception cơ sở cho tất cả các exception của Ads domain. +/// +public class AdsDomainException : Exception +{ + public AdsDomainException() + { + } + + public AdsDomainException(string message) + : base(message) + { + } + + public AdsDomainException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Domain/Exceptions/SampleDomainException.cs b/services/ads-manager-service-net/src/AdsManagerService.Domain/Exceptions/SampleDomainException.cs deleted file mode 100644 index edb7e2f4..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Domain/Exceptions/SampleDomainException.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Infrastructure/MyServiceContext.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/AdsManagerServiceContext.cs similarity index 84% rename from services/ads-manager-service-net/src/AdsManagerService.Infrastructure/MyServiceContext.cs rename to services/ads-manager-service-net/src/AdsManagerService.Infrastructure/AdsManagerServiceContext.cs index f69e9511..d969ae05 100644 --- a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/MyServiceContext.cs +++ b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/AdsManagerServiceContext.cs @@ -1,7 +1,10 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using AdsManagerService.Domain.AggregatesModel.AdSetAggregate; +using AdsManagerService.Domain.AggregatesModel.AdAggregate; +using AdsManagerService.Domain.AggregatesModel.AudienceAggregate; using AdsManagerService.Domain.SeedWork; using AdsManagerService.Infrastructure.EntityConfigurations; @@ -16,11 +19,11 @@ public class AdsManagerServiceContext : DbContext, IUnitOfWork private readonly IMediator _mediator; private IDbContextTransaction? _currentTransaction; - /// - /// EN: Samples table. - /// VI: Bảng Samples. - /// - public DbSet Samples => Set(); + public DbSet Campaigns => Set(); + public DbSet AdSets => Set(); + public DbSet Ads => Set(); + public DbSet CustomAudiences => Set(); + public DbSet LookalikeAudiences => Set(); /// /// EN: Read-only access to current transaction. @@ -48,10 +51,10 @@ public class AdsManagerServiceContext : DbContext, IUnitOfWork protected override void OnModelCreating(ModelBuilder modelBuilder) { - // EN: Apply entity configurations - // VI: Áp dụng các cấu hình entity - modelBuilder.ApplyConfiguration(new SampleEntityTypeConfiguration()); - modelBuilder.ApplyConfiguration(new SampleStatusEntityTypeConfiguration()); + // Apply entity configurations for ads entities + modelBuilder.ApplyConfiguration(new CampaignEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new AdSetEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new AdEntityTypeConfiguration()); } /// diff --git a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/DependencyInjection.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/DependencyInjection.cs index c34ebe5c..a433e165 100644 --- a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/DependencyInjection.cs +++ b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/DependencyInjection.cs @@ -1,7 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using AdsManagerService.Domain.AggregatesModel.AdSetAggregate; +using AdsManagerService.Domain.AggregatesModel.AdAggregate; using AdsManagerService.Infrastructure.Idempotency; using AdsManagerService.Infrastructure.Repositories; @@ -47,7 +49,9 @@ public static class DependencyInjection }); // EN: Register repositories / VI: Đăng ký repositories - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // EN: Register idempotency services / VI: Đăng ký idempotency services services.AddScoped(); diff --git a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/AdEntityTypeConfiguration.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/AdEntityTypeConfiguration.cs new file mode 100644 index 00000000..7d09db58 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/AdEntityTypeConfiguration.cs @@ -0,0 +1,42 @@ +using AdsManagerService.Domain.AggregatesModel.AdAggregate; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AdsManagerService.Infrastructure.EntityConfigurations; + +public class AdEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ads"); + + builder.HasKey(a => a.Id); + + builder.Property(a => a.Id).HasColumnName("id").IsRequired(); + builder.Property(a => a.AdSetId).HasColumnName("ad_set_id").IsRequired(); + builder.Property(a => a.Name).HasColumnName("name").HasMaxLength(255).IsRequired(); + builder.Property(a => a.FormatId).HasColumnName("format_id").IsRequired(); + builder.Property(a => a.StatusId).HasColumnName("status_id").IsRequired(); + builder.Property(a => a.ReviewStatusId).HasColumnName("review_status_id").IsRequired(); + + builder.Property(a => a.Headline).HasColumnName("headline").HasMaxLength(255); + builder.Property(a => a.PrimaryText).HasColumnName("primary_text").HasMaxLength(1000); + builder.Property(a => a.Description).HasColumnName("description").HasMaxLength(500); + builder.Property(a => a.CallToAction).HasColumnName("call_to_action").HasMaxLength(50); + builder.Property(a => a.DestinationUrl).HasColumnName("destination_url").HasMaxLength(2048); + builder.Property(a => a.CreativeUrl).HasColumnName("creative_url").HasMaxLength(2048); + + builder.Property(a => a.CreatedAt).HasColumnName("created_at").IsRequired(); + builder.Property(a => a.UpdatedAt).HasColumnName("updated_at"); + + // Indexes + builder.HasIndex(a => a.AdSetId).HasDatabaseName("idx_ads_ad_set_id"); + builder.HasIndex(a => a.StatusId).HasDatabaseName("idx_ads_status_id"); + builder.HasIndex(a => a.ReviewStatusId).HasDatabaseName("idx_ads_review_status_id"); + + builder.Ignore(a => a.DomainEvents); + builder.Ignore(a => a.Format); + builder.Ignore(a => a.Status); + builder.Ignore(a => a.ReviewStatus); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/AdSetEntityTypeConfiguration.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/AdSetEntityTypeConfiguration.cs new file mode 100644 index 00000000..2fcd8c69 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/AdSetEntityTypeConfiguration.cs @@ -0,0 +1,69 @@ +using AdsManagerService.Domain.AggregatesModel.AdSetAggregate; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AdsManagerService.Infrastructure.EntityConfigurations; + +public class AdSetEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ad_sets"); + + builder.HasKey(a => a.Id); + + builder.Property(a => a.Id) + .HasColumnName("id") + .IsRequired(); + + builder.Property(a => a.CampaignId) + .HasColumnName("campaign_id") + .IsRequired(); + + builder.Property(a => a.Name) + .HasColumnName("name") + .HasMaxLength(255) + .IsRequired(); + + builder.Property(a => a.StatusId) + .HasColumnName("status_id") + .IsRequired(); + + // Targeting as owned type + builder.OwnsOne(a => a.Targeting, targeting => + { + targeting.Property(t => t.MinAge).HasColumnName("target_min_age"); + targeting.Property(t => t.MaxAge).HasColumnName("target_max_age"); + targeting.Property(t => t.Genders).HasColumnName("target_genders").HasMaxLength(50); + targeting.Property(t => t.Locations).HasColumnName("target_locations").HasMaxLength(500); + targeting.Property(t => t.Interests).HasColumnName("target_interests").HasMaxLength(500); + targeting.Property(t => t.CustomAudienceIds).HasColumnName("custom_audience_ids").HasMaxLength(500); + targeting.Property(t => t.LookalikeAudienceIds).HasColumnName("lookalike_audience_ids").HasMaxLength(500); + }); + + // BidStrategy as owned type + builder.OwnsOne(a => a.BidStrategy, bidStrategy => + { + bidStrategy.Property(b => b.Type).HasColumnName("bid_type").IsRequired(); + bidStrategy.Property(b => b.BidAmount).HasColumnName("bid_amount").HasColumnType("decimal(18,6)"); + bidStrategy.Property(b => b.TargetCost).HasColumnName("target_cost").HasColumnType("decimal(18,6)"); + }); + + builder.Property(a => a.DailyBudget) + .HasColumnName("daily_budget") + .HasColumnType("decimal(18,2)") + .IsRequired(); + + builder.Property(a => a.StartDate).HasColumnName("start_date"); + builder.Property(a => a.EndDate).HasColumnName("end_date"); + builder.Property(a => a.CreatedAt).HasColumnName("created_at").IsRequired(); + builder.Property(a => a.UpdatedAt).HasColumnName("updated_at"); + + // Indexes + builder.HasIndex(a => a.CampaignId).HasDatabaseName("idx_ad_sets_campaign_id"); + builder.HasIndex(a => a.StatusId).HasDatabaseName("idx_ad_sets_status_id"); + + builder.Ignore(a => a.DomainEvents); + builder.Ignore(a => a.Status); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/CampaignEntityTypeConfiguration.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/CampaignEntityTypeConfiguration.cs new file mode 100644 index 00000000..a1722c36 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/CampaignEntityTypeConfiguration.cs @@ -0,0 +1,90 @@ +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AdsManagerService.Infrastructure.EntityConfigurations; + +/// +/// EN: Entity Framework Core configuration for Campaign entity. +/// VI: Cấu hình EF Core cho entity Campaign. +/// +public class CampaignEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("campaigns"); + + builder.HasKey(c => c.Id); + + builder.Property(c => c.Id) + .HasColumnName("id") + .IsRequired(); + + builder.Property(c => c.AdvertiserId) + .HasColumnName("advertiser_id") + .IsRequired(); + + builder.Property(c => c.Name) + .HasColumnName("name") + .HasMaxLength(255) + .IsRequired(); + + builder.Property(c => c.Description) + .HasColumnName("description") + .HasMaxLength(1000); + + builder.Property(c => c.StatusId) + .HasColumnName("status_id") + .IsRequired(); + + builder.Property(c => c.ObjectiveId) + .HasColumnName("objective_id") + .IsRequired(); + + // Budget as owned type + builder.OwnsOne(c => c.Budget, budget => + { + budget.Property(b => b.Type) + .HasColumnName("budget_type") + .IsRequired(); + + budget.Property(b => b.Amount) + .HasColumnName("budget_amount") + .HasColumnType("decimal(18,2)") + .IsRequired(); + + budget.Property(b => b.Currency) + .HasColumnName("currency") + .HasMaxLength(10) + .IsRequired(); + }); + + builder.Property(c => c.StartDate) + .HasColumnName("start_date"); + + builder.Property(c => c.EndDate) + .HasColumnName("end_date"); + + builder.Property(c => c.TotalSpend) + .HasColumnName("total_spend") + .HasColumnType("decimal(18,2)") + .HasDefaultValue(0); + + builder.Property(c => c.CreatedAt) + .HasColumnName("created_at") + .IsRequired(); + + builder.Property(c => c.UpdatedAt) + .HasColumnName("updated_at"); + + // Indexes + builder.HasIndex(c => c.AdvertiserId).HasDatabaseName("idx_campaigns_advertiser_id"); + builder.HasIndex(c => c.StatusId).HasDatabaseName("idx_campaigns_status_id"); + builder.HasIndex(c => c.CreatedAt).HasDatabaseName("idx_campaigns_created_at"); + + // Ignore navigation properties + builder.Ignore(c => c.DomainEvents); + builder.Ignore(c => c.Status); + builder.Ignore(c => c.Objective); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs deleted file mode 100644 index 22a411d3..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs deleted file mode 100644 index 6ecee6d1..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; - -namespace AdsManagerService.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-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/AdRepository.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/AdRepository.cs new file mode 100644 index 00000000..11bdc987 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/AdRepository.cs @@ -0,0 +1,37 @@ +using AdsManagerService.Domain.AggregatesModel.AdAggregate; +using AdsManagerService.Domain.SeedWork; +using Microsoft.EntityFrameworkCore; + +namespace AdsManagerService.Infrastructure.Repositories; + +/// +/// EN: Repository implementation for Ad aggregate. +/// VI: Triển khai repository cho Ad aggregate. +/// +public class AdRepository : IAdRepository +{ + private readonly AdsManagerServiceContext _context; + + public IUnitOfWork UnitOfWork => _context; + + public AdRepository(AdsManagerServiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public Ad Add(Ad ad) + { + return _context.Ads.Add(ad).Entity; + } + + public void Update(Ad ad) + { + _context.Entry(ad).State = EntityState.Modified; + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _context.Ads + .FirstOrDefaultAsync(a => a.Id == id, cancellationToken); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/AdSetRepository.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/AdSetRepository.cs new file mode 100644 index 00000000..e8c3bd6e --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/AdSetRepository.cs @@ -0,0 +1,45 @@ +using AdsManagerService.Domain.AggregatesModel.AdSetAggregate; +using AdsManagerService.Domain.SeedWork; +using Microsoft.EntityFrameworkCore; + +namespace AdsManagerService.Infrastructure.Repositories; + +/// +/// EN: Repository implementation for AdSet aggregate. +/// VI: Triển khai repository cho AdSet aggregate. +/// +public class AdSetRepository : IAdSetRepository +{ + private readonly AdsManagerServiceContext _context; + + public IUnitOfWork UnitOfWork => _context; + + public AdSetRepository(AdsManagerServiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public AdSet Add(AdSet adSet) + { + return _context.AdSets.Add(adSet).Entity; + } + + public void Update(AdSet adSet) + { + _context.Entry(adSet).State = EntityState.Modified; + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _context.AdSets + .FirstOrDefaultAsync(a => a.Id == id, cancellationToken); + } + + public async Task> GetByCampaignIdAsync(Guid campaignId, CancellationToken cancellationToken = default) + { + return await _context.AdSets + .Where(a => a.CampaignId == campaignId) + .OrderByDescending(a => a.CreatedAt) + .ToListAsync(cancellationToken); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/CampaignRepository.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/CampaignRepository.cs new file mode 100644 index 00000000..5034c864 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/CampaignRepository.cs @@ -0,0 +1,52 @@ +using AdsManagerService.Domain.AggregatesModel.CampaignAggregate; +using AdsManagerService.Domain.SeedWork; +using Microsoft.EntityFrameworkCore; + +namespace AdsManagerService.Infrastructure.Repositories; + +/// +/// EN: Repository implementation for Campaign aggregate. +/// VI: Triển khai repository cho Campaign aggregate. +/// +public class CampaignRepository : ICampaignRepository +{ + private readonly AdsManagerServiceContext _context; + + public IUnitOfWork UnitOfWork => _context; + + public CampaignRepository(AdsManagerServiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public Campaign Add(Campaign campaign) + { + return _context.Campaigns.Add(campaign).Entity; + } + + public void Update(Campaign campaign) + { + _context.Entry(campaign).State = EntityState.Modified; + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _context.Campaigns + .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + } + + public async Task> GetByAdvertiserIdAsync(Guid advertiserId, CancellationToken cancellationToken = default) + { + return await _context.Campaigns + .Where(c => c.AdvertiserId == advertiserId) + .OrderByDescending(c => c.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetActiveAsync(CancellationToken cancellationToken = default) + { + return await _context.Campaigns + .Where(c => c.StatusId == CampaignStatus.Active.Id) + .ToListAsync(cancellationToken); + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/SampleRepository.cs b/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/SampleRepository.cs deleted file mode 100644 index 7d7b2d45..00000000 --- a/services/ads-manager-service-net/src/AdsManagerService.Infrastructure/Repositories/SampleRepository.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; -using AdsManagerService.Domain.SeedWork; - -namespace AdsManagerService.Infrastructure.Repositories; - -/// -/// EN: Repository implementation for Sample aggregate. -/// VI: Triển khai repository cho Sample aggregate. -/// -public class SampleRepository : ISampleRepository -{ - private readonly AdsManagerServiceContext _context; - - /// - /// EN: Unit of work for transaction management. - /// VI: Unit of work cho quản lý transaction. - /// - public IUnitOfWork UnitOfWork => _context; - - public SampleRepository(AdsManagerServiceContext 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-manager-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj b/services/ads-manager-service-net/tests/AdsManagerService.FunctionalTests/AdsManagerService.FunctionalTests.csproj similarity index 100% rename from services/ads-manager-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj rename to services/ads-manager-service-net/tests/AdsManagerService.FunctionalTests/AdsManagerService.FunctionalTests.csproj diff --git a/services/ads-manager-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/ads-manager-service-net/tests/AdsManagerService.FunctionalTests/Controllers/SamplesControllerTests.cs similarity index 100% rename from services/ads-manager-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs rename to services/ads-manager-service-net/tests/AdsManagerService.FunctionalTests/Controllers/SamplesControllerTests.cs diff --git a/services/ads-manager-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs b/services/ads-manager-service-net/tests/AdsManagerService.FunctionalTests/CustomWebApplicationFactory.cs similarity index 100% rename from services/ads-manager-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs rename to services/ads-manager-service-net/tests/AdsManagerService.FunctionalTests/CustomWebApplicationFactory.cs diff --git a/services/ads-manager-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj b/services/ads-manager-service-net/tests/AdsManagerService.UnitTests/AdsManagerService.UnitTests.csproj similarity index 100% rename from services/ads-manager-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj rename to services/ads-manager-service-net/tests/AdsManagerService.UnitTests/AdsManagerService.UnitTests.csproj diff --git a/services/ads-manager-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/ads-manager-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs deleted file mode 100644 index d0ca6a02..00000000 --- a/services/ads-manager-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Logging; -using Moq; -using AdsManagerService.API.Application.Commands; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; -using AdsManagerService.Domain.SeedWork; -using Xunit; - -namespace AdsManagerService.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-manager-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs b/services/ads-manager-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs deleted file mode 100644 index 3cce4ae8..00000000 --- a/services/ads-manager-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs +++ /dev/null @@ -1,151 +0,0 @@ -using FluentAssertions; -using AdsManagerService.Domain.AggregatesModel.SampleAggregate; -using AdsManagerService.Domain.Exceptions; -using Xunit; - -namespace AdsManagerService.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-serving-service-net/docker-compose.yml b/services/ads-serving-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/ads-serving-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/ads-serving-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj b/services/ads-serving-service-net/tests/AdsServingService.FunctionalTests/AdsServingService.FunctionalTests.csproj similarity index 100% rename from services/ads-serving-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj rename to services/ads-serving-service-net/tests/AdsServingService.FunctionalTests/AdsServingService.FunctionalTests.csproj diff --git a/services/ads-serving-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/ads-serving-service-net/tests/AdsServingService.FunctionalTests/Controllers/SamplesControllerTests.cs similarity index 100% rename from services/ads-serving-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs rename to services/ads-serving-service-net/tests/AdsServingService.FunctionalTests/Controllers/SamplesControllerTests.cs diff --git a/services/ads-serving-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs b/services/ads-serving-service-net/tests/AdsServingService.FunctionalTests/CustomWebApplicationFactory.cs similarity index 100% rename from services/ads-serving-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs rename to services/ads-serving-service-net/tests/AdsServingService.FunctionalTests/CustomWebApplicationFactory.cs diff --git a/services/ads-serving-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj b/services/ads-serving-service-net/tests/AdsServingService.UnitTests/AdsServingService.UnitTests.csproj similarity index 100% rename from services/ads-serving-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj rename to services/ads-serving-service-net/tests/AdsServingService.UnitTests/AdsServingService.UnitTests.csproj diff --git a/services/ads-serving-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/ads-serving-service-net/tests/AdsServingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs similarity index 100% rename from services/ads-serving-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs rename to services/ads-serving-service-net/tests/AdsServingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs diff --git a/services/ads-serving-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs b/services/ads-serving-service-net/tests/AdsServingService.UnitTests/Domain/SampleAggregateTests.cs similarity index 100% rename from services/ads-serving-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs rename to services/ads-serving-service-net/tests/AdsServingService.UnitTests/Domain/SampleAggregateTests.cs diff --git a/services/ads-tracking-service-net/docker-compose.yml b/services/ads-tracking-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/ads-tracking-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/ads-tracking-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj b/services/ads-tracking-service-net/tests/AdsTrackingService.FunctionalTests/AdsTrackingService.FunctionalTests.csproj similarity index 100% rename from services/ads-tracking-service-net/tests/MyService.FunctionalTests/MyService.FunctionalTests.csproj rename to services/ads-tracking-service-net/tests/AdsTrackingService.FunctionalTests/AdsTrackingService.FunctionalTests.csproj diff --git a/services/ads-tracking-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/ads-tracking-service-net/tests/AdsTrackingService.FunctionalTests/Controllers/SamplesControllerTests.cs similarity index 100% rename from services/ads-tracking-service-net/tests/MyService.FunctionalTests/Controllers/SamplesControllerTests.cs rename to services/ads-tracking-service-net/tests/AdsTrackingService.FunctionalTests/Controllers/SamplesControllerTests.cs diff --git a/services/ads-tracking-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs b/services/ads-tracking-service-net/tests/AdsTrackingService.FunctionalTests/CustomWebApplicationFactory.cs similarity index 100% rename from services/ads-tracking-service-net/tests/MyService.FunctionalTests/CustomWebApplicationFactory.cs rename to services/ads-tracking-service-net/tests/AdsTrackingService.FunctionalTests/CustomWebApplicationFactory.cs diff --git a/services/ads-tracking-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj b/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/AdsTrackingService.UnitTests.csproj similarity index 100% rename from services/ads-tracking-service-net/tests/MyService.UnitTests/MyService.UnitTests.csproj rename to services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/AdsTrackingService.UnitTests.csproj diff --git a/services/ads-tracking-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs similarity index 100% rename from services/ads-tracking-service-net/tests/MyService.UnitTests/Application/CreateSampleCommandHandlerTests.cs rename to services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs diff --git a/services/ads-tracking-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs b/services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Domain/SampleAggregateTests.cs similarity index 100% rename from services/ads-tracking-service-net/tests/MyService.UnitTests/Domain/SampleAggregateTests.cs rename to services/ads-tracking-service-net/tests/AdsTrackingService.UnitTests/Domain/SampleAggregateTests.cs diff --git a/services/booking-service-net/docker-compose.yml b/services/booking-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/booking-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/booking-service-net/src/BookingService.API/Application/Behaviors/LoggingBehavior.cs b/services/booking-service-net/src/BookingService.API/Application/Behaviors/LoggingBehavior.cs index a724424d..73cdc1d7 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Behaviors/LoggingBehavior.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Behaviors/LoggingBehavior.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace BookingService.API.Application.Behaviors; /// /// EN: MediatR behavior for logging request handling. diff --git a/services/booking-service-net/src/BookingService.API/Application/Behaviors/TransactionBehavior.cs b/services/booking-service-net/src/BookingService.API/Application/Behaviors/TransactionBehavior.cs index 8675b649..b9fae97f 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Behaviors/TransactionBehavior.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Behaviors/TransactionBehavior.cs @@ -1,8 +1,8 @@ using MediatR; using Microsoft.EntityFrameworkCore; -using MyService.Infrastructure; +using BookingService.Infrastructure; -namespace MyService.API.Application.Behaviors; +namespace BookingService.API.Application.Behaviors; /// /// EN: MediatR behavior for handling database transactions. @@ -13,11 +13,11 @@ namespace MyService.API.Application.Behaviors; public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { - private readonly MyServiceContext _dbContext; + private readonly BookingServiceContext _dbContext; private readonly ILogger> _logger; public TransactionBehavior( - MyServiceContext dbContext, + BookingServiceContext dbContext, ILogger> logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); diff --git a/services/booking-service-net/src/BookingService.API/Application/Behaviors/ValidatorBehavior.cs b/services/booking-service-net/src/BookingService.API/Application/Behaviors/ValidatorBehavior.cs index 0062cd60..844219e0 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Behaviors/ValidatorBehavior.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Behaviors/ValidatorBehavior.cs @@ -1,7 +1,7 @@ using FluentValidation; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace BookingService.API.Application.Behaviors; /// /// EN: MediatR behavior for FluentValidation integration. diff --git a/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommand.cs index 49825490..c2d393cc 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommand.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace BookingService.API.Application.Commands; /// /// EN: Command to change status of a Sample. diff --git a/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs index 76e31030..7c2be169 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace BookingService.API.Application.Commands; /// /// EN: Handler for ChangeSampleStatusCommand. diff --git a/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommand.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommand.cs index 138cc794..3ece960b 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommand.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace BookingService.API.Application.Commands; /// /// EN: Command to create a new Sample. diff --git a/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommandHandler.cs index d7d0fd7c..c386e30c 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommandHandler.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace BookingService.API.Application.Commands; /// /// EN: Handler for CreateSampleCommand. diff --git a/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommand.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommand.cs index 0de392db..d99b2340 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommand.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace BookingService.API.Application.Commands; /// /// EN: Command to delete a Sample. diff --git a/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommandHandler.cs index c7632189..5f4b6dd8 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommandHandler.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace BookingService.API.Application.Commands; /// /// EN: Handler for DeleteSampleCommand. diff --git a/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommand.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommand.cs index 6fad8514..bde94f72 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommand.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace BookingService.API.Application.Commands; /// /// EN: Command to update an existing Sample. diff --git a/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommandHandler.cs index e904cf0a..ebca581c 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommandHandler.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace BookingService.API.Application.Commands; /// /// EN: Handler for UpdateSampleCommand. diff --git a/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQuery.cs b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQuery.cs index 8b90789c..25f31497 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQuery.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace BookingService.API.Application.Queries; /// /// EN: Query to get a Sample by ID. diff --git a/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQueryHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQueryHandler.cs index 2da10b6d..b1a17658 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQueryHandler.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace BookingService.API.Application.Queries; /// /// EN: Handler for GetSampleQuery. diff --git a/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQuery.cs b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQuery.cs index d6a98e34..ced4e6ac 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQuery.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace BookingService.API.Application.Queries; /// /// EN: Query to get all Samples. diff --git a/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQueryHandler.cs index 2185302d..dc2515b0 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQueryHandler.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace BookingService.API.Application.Queries; /// /// EN: Handler for GetSamplesQuery. diff --git a/services/booking-service-net/src/BookingService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/booking-service-net/src/BookingService.API/Application/Validations/CreateSampleCommandValidator.cs index 2f339fb3..a11274b4 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Validations/CreateSampleCommandValidator.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Validations/CreateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using BookingService.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace BookingService.API.Application.Validations; /// /// EN: Validator for CreateSampleCommand. diff --git a/services/booking-service-net/src/BookingService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/booking-service-net/src/BookingService.API/Application/Validations/UpdateSampleCommandValidator.cs index 7030d5c8..c5be27df 100644 --- a/services/booking-service-net/src/BookingService.API/Application/Validations/UpdateSampleCommandValidator.cs +++ b/services/booking-service-net/src/BookingService.API/Application/Validations/UpdateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using BookingService.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace BookingService.API.Application.Validations; /// /// EN: Validator for UpdateSampleCommand. diff --git a/services/booking-service-net/src/BookingService.API/BookingService.API.csproj b/services/booking-service-net/src/BookingService.API/BookingService.API.csproj index 1b5bb222..b5e0f768 100644 --- a/services/booking-service-net/src/BookingService.API/BookingService.API.csproj +++ b/services/booking-service-net/src/BookingService.API/BookingService.API.csproj @@ -1,8 +1,8 @@ - MyService.API - MyService.API + BookingService.API + BookingService.API Web API layer with CQRS pattern myservice-api @@ -36,8 +36,8 @@ - - + + diff --git a/services/booking-service-net/src/BookingService.API/Controllers/SamplesController.cs b/services/booking-service-net/src/BookingService.API/Controllers/SamplesController.cs index c87e0ffa..a6c86169 100644 --- a/services/booking-service-net/src/BookingService.API/Controllers/SamplesController.cs +++ b/services/booking-service-net/src/BookingService.API/Controllers/SamplesController.cs @@ -1,10 +1,10 @@ using Asp.Versioning; using MediatR; using Microsoft.AspNetCore.Mvc; -using MyService.API.Application.Commands; -using MyService.API.Application.Queries; +using BookingService.API.Application.Commands; +using BookingService.API.Application.Queries; -namespace MyService.API.Controllers; +namespace BookingService.API.Controllers; /// /// EN: Controller for Sample CRUD operations using CQRS pattern. diff --git a/services/booking-service-net/src/BookingService.API/Program.cs b/services/booking-service-net/src/BookingService.API/Program.cs index bd9b3df4..f110f297 100644 --- a/services/booking-service-net/src/BookingService.API/Program.cs +++ b/services/booking-service-net/src/BookingService.API/Program.cs @@ -1,8 +1,8 @@ using Asp.Versioning; using FluentValidation; using Hellang.Middleware.ProblemDetails; -using MyService.API.Application.Behaviors; -using MyService.Infrastructure; +using BookingService.API.Application.Behaviors; +using BookingService.Infrastructure; using Serilog; // EN: Configure Serilog early / VI: Cấu hình Serilog sớm @@ -12,7 +12,7 @@ Log.Logger = new LoggerConfiguration() try { - Log.Information("Starting MyService API / Khởi động MyService API"); + Log.Information("Starting BookingService API / Khởi động BookingService API"); var builder = WebApplication.CreateBuilder(args); @@ -70,9 +70,9 @@ try { options.SwaggerDoc("v1", new() { - Title = "MyService API", + Title = "BookingService API", Version = "v1", - Description = "MyService microservice API / API microservice MyService" + Description = "BookingService microservice API / API microservice BookingService" }); }); @@ -107,7 +107,7 @@ try app.UseSwagger(); app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyService API v1"); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "BookingService API v1"); c.RoutePrefix = "swagger"; }); } diff --git a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/AppointmentAggregate/Appointment.cs b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/AppointmentAggregate/Appointment.cs new file mode 100644 index 00000000..1af55f70 --- /dev/null +++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/AppointmentAggregate/Appointment.cs @@ -0,0 +1,103 @@ +// EN: Appointment aggregate root for service bookings. +// VI: Aggregate root Appointment cho đặt lịch dịch vụ. + +using BookingService.Domain.Events; +using BookingService.Domain.Exceptions; +using BookingService.Domain.SeedWork; + +namespace BookingService.Domain.AggregatesModel.AppointmentAggregate; + +/// +/// EN: Appointment aggregate root - represents a service booking. +/// VI: Aggregate root Appointment - đại diện cho đặt lịch dịch vụ. +/// +public class Appointment : Entity, IAggregateRoot +{ + private Guid _shopId; + private Guid? _customerId; + private Guid? _staffId; + private Guid? _resourceId; + private Guid _serviceId; + private DateTime _startTime; + private DateTime _endTime; + private string _status = null!; // Scheduled, Confirmed, InProgress, Completed, Cancelled + private DateTime _createdAt; + + public Guid ShopId => _shopId; + public Guid? CustomerId => _customerId; + public Guid? StaffId => _staffId; + public Guid? ResourceId => _resourceId; + public Guid ServiceId => _serviceId; + public DateTime StartTime => _startTime; + public DateTime EndTime => _endTime; + public string Status => _status; + public DateTime CreatedAt => _createdAt; + + protected Appointment() + { + } + + public Appointment( + Guid shopId, + Guid serviceId, + DateTime startTime, + DateTime endTime, + Guid? customerId = null, + Guid? staffId = null, + Guid? resourceId = null) + { + if (shopId == Guid.Empty) + throw new DomainException("Shop ID cannot be empty"); + if (serviceId == Guid.Empty) + throw new DomainException("Service ID cannot be empty"); + if (endTime <= startTime) + throw new DomainException("End time must be after start time"); + + Id = Guid.NewGuid(); + _shopId = shopId; + _serviceId = serviceId; + _startTime = startTime; + _endTime = endTime; + _customerId = customerId; + _staffId = staffId; + _resourceId = resourceId; + _status = "Scheduled"; + _createdAt = DateTime.UtcNow; + + AddDomainEvent(new AppointmentCreatedDomainEvent(this)); + } + + public void Confirm() + { + if (_status != "Scheduled") + throw new DomainException($"Cannot confirm appointment with status {_status}"); + + _status = "Confirmed"; + } + + public void MarkAsInProgress() + { + if (_status != "Confirmed") + throw new DomainException($"Cannot start appointment with status {_status}"); + + _status = "InProgress"; + } + + public void Complete() + { + if (_status != "InProgress") + throw new DomainException($"Cannot complete appointment with status {_status}"); + + _status = "Completed"; + AddDomainEvent(new AppointmentCompletedDomainEvent(this)); + } + + public void Cancel(string reason) + { + if (_status == "Completed") + throw new DomainException("Cannot cancel completed appointment"); + + _status = "Cancelled"; + AddDomainEvent(new AppointmentCancelledDomainEvent(this, reason)); + } +} diff --git a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/ResourceAggregate/Resource.cs b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/ResourceAggregate/Resource.cs new file mode 100644 index 00000000..77c1b18a --- /dev/null +++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/ResourceAggregate/Resource.cs @@ -0,0 +1,53 @@ +// EN: Resource entity for bookable resources. +// VI: Entity Resource cho tài nguyên có thể đặt. + +using BookingService.Domain.Exceptions; +using BookingService.Domain.SeedWork; + +namespace BookingService.Domain.AggregatesModel.ResourceAggregate; + +/// +/// EN: Resource entity - represents a bookable resource (room, bed, equipment). +/// VI: Entity Resource - đại diện cho tài nguyên có thể đặt (phòng, giường, thiết bị). +/// +public class Resource : Entity, IAggregateRoot +{ + private Guid _shopId; + private string _name = null!; + private string _resourceType = null!; // Room, Bed, Equipment + private int _capacity; + private bool _isActive; + private DateTime _createdAt; + + public Guid ShopId => _shopId; + public string Name => _name; + public string ResourceType => _resourceType; + public int Capacity => _capacity; + public bool IsActive => _isActive; + public DateTime CreatedAt => _createdAt; + + protected Resource() + { + } + + public Resource(Guid shopId, string name, string resourceType, int capacity = 1) + { + if (shopId == Guid.Empty) + throw new DomainException("Shop ID cannot be empty"); + if (string.IsNullOrWhiteSpace(name)) + throw new DomainException("Resource name cannot be empty"); + if (string.IsNullOrWhiteSpace(resourceType)) + throw new DomainException("Resource type cannot be empty"); + + Id = Guid.NewGuid(); + _shopId = shopId; + _name = name.Trim(); + _resourceType = resourceType.Trim(); + _capacity = capacity; + _isActive = true; + _createdAt = DateTime.UtcNow; + } + + public void Activate() => _isActive = true; + public void Deactivate() => _isActive = false; +} diff --git a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs index 40bc8c3a..b9b72f64 100644 --- a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs +++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using BookingService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace BookingService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Repository interface for Sample aggregate. diff --git a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/Sample.cs index 641bb385..fe5e327f 100644 --- a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/Sample.cs +++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/Sample.cs @@ -1,8 +1,8 @@ -using MyService.Domain.Events; -using MyService.Domain.Exceptions; -using MyService.Domain.SeedWork; +using BookingService.Domain.Events; +using BookingService.Domain.Exceptions; +using BookingService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace BookingService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample aggregate root demonstrating DDD patterns. diff --git a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs index 54ce63ba..03304826 100644 --- a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs +++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using BookingService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace BookingService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample status enumeration following type-safe enum pattern. diff --git a/services/booking-service-net/src/BookingService.Domain/AggregatesModel/StaffAggregate/StaffSchedule.cs b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/StaffAggregate/StaffSchedule.cs new file mode 100644 index 00000000..0fd0472e --- /dev/null +++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/StaffAggregate/StaffSchedule.cs @@ -0,0 +1,39 @@ +// EN: Staff schedule entity for availability management. +// VI: Entity StaffSchedule cho quản lý lịch trống. + +using BookingService.Domain.SeedWork; + +namespace BookingService.Domain.AggregatesModel.StaffAggregate; + +/// +/// EN: Staff schedule entity - defines staff working hours. +/// VI: Entity StaffSchedule - định nghĩa giờ làm việc của nhân viên. +/// +public class StaffSchedule : Entity +{ + private Guid _staffId; + private Guid _shopId; + private int _dayOfWeek; // 0 = Sunday, 6 = Saturday + private TimeOnly _startTime; + private TimeOnly _endTime; + + public Guid StaffId => _staffId; + public Guid ShopId => _shopId; + public int DayOfWeek => _dayOfWeek; + public TimeOnly StartTime => _startTime; + public TimeOnly EndTime => _endTime; + + protected StaffSchedule() + { + } + + public StaffSchedule(Guid staffId, Guid shopId, int dayOfWeek, TimeOnly startTime, TimeOnly endTime) + { + Id = Guid.NewGuid(); + _staffId = staffId; + _shopId = shopId; + _dayOfWeek = dayOfWeek; + _startTime = startTime; + _endTime = endTime; + } +} diff --git a/services/booking-service-net/src/BookingService.Domain/BookingService.Domain.csproj b/services/booking-service-net/src/BookingService.Domain/BookingService.Domain.csproj index 3208317a..e67f63e7 100644 --- a/services/booking-service-net/src/BookingService.Domain/BookingService.Domain.csproj +++ b/services/booking-service-net/src/BookingService.Domain/BookingService.Domain.csproj @@ -1,8 +1,8 @@ - MyService.Domain - MyService.Domain + BookingService.Domain + BookingService.Domain Domain layer containing core business logic and entities diff --git a/services/booking-service-net/src/BookingService.Domain/Events/BookingDomainEvents.cs b/services/booking-service-net/src/BookingService.Domain/Events/BookingDomainEvents.cs new file mode 100644 index 00000000..eb8719b1 --- /dev/null +++ b/services/booking-service-net/src/BookingService.Domain/Events/BookingDomainEvents.cs @@ -0,0 +1,25 @@ +// EN: Booking domain events. +// VI: Domain events của Booking. + +using BookingService.Domain.AggregatesModel.AppointmentAggregate; +using MediatR; + +namespace BookingService.Domain.Events; + +/// +/// EN: Domain event raised when an appointment is created. +/// VI: Domain event phát ra khi lịch hẹn được tạo. +/// +public record AppointmentCreatedDomainEvent(Appointment Appointment) : INotification; + +/// +/// EN: Domain event raised when an appointment is completed. +/// VI: Domain event phát ra khi lịch hẹn hoàn thành. +/// +public record AppointmentCompletedDomainEvent(Appointment Appointment) : INotification; + +/// +/// EN: Domain event raised when an appointment is cancelled. +/// VI: Domain event phát ra khi lịch hẹn bị hủy. +/// +public record AppointmentCancelledDomainEvent(Appointment Appointment, string Reason) : INotification; diff --git a/services/booking-service-net/src/BookingService.Domain/Events/SampleCreatedDomainEvent.cs b/services/booking-service-net/src/BookingService.Domain/Events/SampleCreatedDomainEvent.cs index 7e838214..127044cf 100644 --- a/services/booking-service-net/src/BookingService.Domain/Events/SampleCreatedDomainEvent.cs +++ b/services/booking-service-net/src/BookingService.Domain/Events/SampleCreatedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace BookingService.Domain.Events; /// /// EN: Domain event raised when a new Sample is created. diff --git a/services/booking-service-net/src/BookingService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/booking-service-net/src/BookingService.Domain/Events/SampleStatusChangedDomainEvent.cs index f6d9b422..a51f62f4 100644 --- a/services/booking-service-net/src/BookingService.Domain/Events/SampleStatusChangedDomainEvent.cs +++ b/services/booking-service-net/src/BookingService.Domain/Events/SampleStatusChangedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace BookingService.Domain.Events; /// /// EN: Domain event raised when Sample status changes. diff --git a/services/booking-service-net/src/BookingService.Domain/Exceptions/DomainException.cs b/services/booking-service-net/src/BookingService.Domain/Exceptions/DomainException.cs index 7e737f64..4244881d 100644 --- a/services/booking-service-net/src/BookingService.Domain/Exceptions/DomainException.cs +++ b/services/booking-service-net/src/BookingService.Domain/Exceptions/DomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace BookingService.Domain.Exceptions; /// /// EN: Base exception for domain errors. diff --git a/services/booking-service-net/src/BookingService.Domain/Exceptions/SampleDomainException.cs b/services/booking-service-net/src/BookingService.Domain/Exceptions/SampleDomainException.cs index c850944c..5397122d 100644 --- a/services/booking-service-net/src/BookingService.Domain/Exceptions/SampleDomainException.cs +++ b/services/booking-service-net/src/BookingService.Domain/Exceptions/SampleDomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace BookingService.Domain.Exceptions; /// /// EN: Exception for Sample aggregate domain errors. diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/Entity.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/Entity.cs index b07fdd3b..496840dc 100644 --- a/services/booking-service-net/src/BookingService.Domain/SeedWork/Entity.cs +++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/Entity.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.Domain.SeedWork; +namespace BookingService.Domain.SeedWork; /// /// EN: Base class for all domain entities. diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/Enumeration.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/Enumeration.cs index 6641979c..b047100a 100644 --- a/services/booking-service-net/src/BookingService.Domain/SeedWork/Enumeration.cs +++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/Enumeration.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace MyService.Domain.SeedWork; +namespace BookingService.Domain.SeedWork; /// /// EN: Base class for enumeration classes (type-safe enum pattern). diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/IAggregateRoot.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/IAggregateRoot.cs index d361394f..2a319da0 100644 --- a/services/booking-service-net/src/BookingService.Domain/SeedWork/IAggregateRoot.cs +++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/IAggregateRoot.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace BookingService.Domain.SeedWork; /// /// EN: Marker interface for aggregate roots. diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/IRepository.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/IRepository.cs index 2d539e44..dd178967 100644 --- a/services/booking-service-net/src/BookingService.Domain/SeedWork/IRepository.cs +++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/IRepository.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace BookingService.Domain.SeedWork; /// /// EN: Generic repository interface for aggregate roots. diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/IUnitOfWork.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/IUnitOfWork.cs index d37d8fa4..3b271920 100644 --- a/services/booking-service-net/src/BookingService.Domain/SeedWork/IUnitOfWork.cs +++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/IUnitOfWork.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace BookingService.Domain.SeedWork; /// /// EN: Unit of Work pattern interface. diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/ValueObject.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/ValueObject.cs index 5cf4188f..37b9193e 100644 --- a/services/booking-service-net/src/BookingService.Domain/SeedWork/ValueObject.cs +++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/ValueObject.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace BookingService.Domain.SeedWork; /// /// EN: Base class for Value Objects following DDD patterns. diff --git a/services/booking-service-net/src/BookingService.Infrastructure/BookingService.Infrastructure.csproj b/services/booking-service-net/src/BookingService.Infrastructure/BookingService.Infrastructure.csproj index 7c81b5e9..aa84ab0b 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/BookingService.Infrastructure.csproj +++ b/services/booking-service-net/src/BookingService.Infrastructure/BookingService.Infrastructure.csproj @@ -1,8 +1,8 @@ - MyService.Infrastructure - MyService.Infrastructure + BookingService.Infrastructure + BookingService.Infrastructure Infrastructure layer for data access and external services @@ -30,7 +30,7 @@ - + diff --git a/services/booking-service-net/src/BookingService.Infrastructure/DependencyInjection.cs b/services/booking-service-net/src/BookingService.Infrastructure/DependencyInjection.cs index a8dfba0d..17c88958 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/DependencyInjection.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/DependencyInjection.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Infrastructure.Idempotency; -using MyService.Infrastructure.Repositories; +using BookingService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Infrastructure.Idempotency; +using BookingService.Infrastructure.Repositories; -namespace MyService.Infrastructure; +namespace BookingService.Infrastructure; /// /// EN: Dependency injection extensions for Infrastructure layer. @@ -22,7 +22,7 @@ public static class DependencyInjection IConfiguration configuration) { // EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL - services.AddDbContext(options => + services.AddDbContext(options => { var connectionString = configuration.GetConnectionString("DefaultConnection") ?? configuration["DATABASE_URL"] @@ -30,7 +30,7 @@ public static class DependencyInjection options.UseNpgsql(connectionString, npgsqlOptions => { - npgsqlOptions.MigrationsAssembly(typeof(MyServiceContext).Assembly.FullName); + npgsqlOptions.MigrationsAssembly(typeof(BookingServiceContext).Assembly.FullName); npgsqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), diff --git a/services/booking-service-net/src/BookingService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs b/services/booking-service-net/src/BookingService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs index 5c2fbd42..c0e0dae6 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace BookingService.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for Sample entity. diff --git a/services/booking-service-net/src/BookingService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs b/services/booking-service-net/src/BookingService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs index 8b683f56..28ed123b 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace BookingService.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for SampleStatus enumeration. diff --git a/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/ClientRequest.cs b/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/ClientRequest.cs index f65e4a56..54dfadc0 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/ClientRequest.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/ClientRequest.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace BookingService.Infrastructure.Idempotency; /// /// EN: Entity for tracking client requests to ensure idempotency. diff --git a/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/IRequestManager.cs b/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/IRequestManager.cs index 92097399..fc1bcdb7 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/IRequestManager.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/IRequestManager.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace BookingService.Infrastructure.Idempotency; /// /// EN: Interface for managing client request idempotency. diff --git a/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/RequestManager.cs b/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/RequestManager.cs index 41a5f318..302a4984 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/RequestManager.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/Idempotency/RequestManager.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace MyService.Infrastructure.Idempotency; +namespace BookingService.Infrastructure.Idempotency; /// /// EN: Implementation of request manager for idempotency. @@ -8,9 +8,9 @@ namespace MyService.Infrastructure.Idempotency; /// public class RequestManager : IRequestManager { - private readonly MyServiceContext _context; + private readonly BookingServiceContext _context; - public RequestManager(MyServiceContext context) + public RequestManager(BookingServiceContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/booking-service-net/src/BookingService.Infrastructure/MyServiceContext.cs b/services/booking-service-net/src/BookingService.Infrastructure/MyServiceContext.cs index 4486367d..df42730a 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/MyServiceContext.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/MyServiceContext.cs @@ -1,17 +1,17 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; -using MyService.Infrastructure.EntityConfigurations; +using BookingService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.SeedWork; +using BookingService.Infrastructure.EntityConfigurations; -namespace MyService.Infrastructure; +namespace BookingService.Infrastructure; /// -/// EN: EF Core DbContext for MyService. -/// VI: EF Core DbContext cho MyService. +/// EN: EF Core DbContext for BookingService. +/// VI: EF Core DbContext cho BookingService. /// -public class MyServiceContext : DbContext, IUnitOfWork +public class BookingServiceContext : DbContext, IUnitOfWork { private readonly IMediator _mediator; private IDbContextTransaction? _currentTransaction; @@ -34,16 +34,16 @@ public class MyServiceContext : DbContext, IUnitOfWork /// public bool HasActiveTransaction => _currentTransaction != null; - public MyServiceContext(DbContextOptions options) : base(options) + public BookingServiceContext(DbContextOptions options) : base(options) { _mediator = null!; } - public MyServiceContext(DbContextOptions options, IMediator mediator) : base(options) + public BookingServiceContext(DbContextOptions options, IMediator mediator) : base(options) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); - System.Diagnostics.Debug.WriteLine("MyServiceContext::ctor - " + GetHashCode()); + System.Diagnostics.Debug.WriteLine("BookingServiceContext::ctor - " + GetHashCode()); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/services/booking-service-net/src/BookingService.Infrastructure/Repositories/SampleRepository.cs b/services/booking-service-net/src/BookingService.Infrastructure/Repositories/SampleRepository.cs index f0a4b109..f880bb59 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/Repositories/SampleRepository.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/Repositories/SampleRepository.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using BookingService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.SeedWork; -namespace MyService.Infrastructure.Repositories; +namespace BookingService.Infrastructure.Repositories; /// /// EN: Repository implementation for Sample aggregate. @@ -10,7 +10,7 @@ namespace MyService.Infrastructure.Repositories; /// public class SampleRepository : ISampleRepository { - private readonly MyServiceContext _context; + private readonly BookingServiceContext _context; /// /// EN: Unit of work for transaction management. @@ -18,7 +18,7 @@ public class SampleRepository : ISampleRepository /// public IUnitOfWork UnitOfWork => _context; - public SampleRepository(MyServiceContext context) + public SampleRepository(BookingServiceContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/MyService.FunctionalTests.csproj b/services/booking-service-net/tests/BookingService.FunctionalTests/BookingService.FunctionalTests.csproj similarity index 86% rename from services/fnb-engine-net/tests/FnbEngine.FunctionalTests/MyService.FunctionalTests.csproj rename to services/booking-service-net/tests/BookingService.FunctionalTests/BookingService.FunctionalTests.csproj index 4cc894f8..f8f5d43b 100644 --- a/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/MyService.FunctionalTests.csproj +++ b/services/booking-service-net/tests/BookingService.FunctionalTests/BookingService.FunctionalTests.csproj @@ -1,8 +1,8 @@ - MyService.FunctionalTests - MyService.FunctionalTests + BookingService.FunctionalTests + BookingService.FunctionalTests false true @@ -32,7 +32,7 @@ - + diff --git a/services/booking-service-net/tests/BookingService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/booking-service-net/tests/BookingService.FunctionalTests/Controllers/SamplesControllerTests.cs index e6e99ac5..e5a2bfe6 100644 --- a/services/booking-service-net/tests/BookingService.FunctionalTests/Controllers/SamplesControllerTests.cs +++ b/services/booking-service-net/tests/BookingService.FunctionalTests/Controllers/SamplesControllerTests.cs @@ -4,7 +4,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; -namespace MyService.FunctionalTests.Controllers; +namespace BookingService.FunctionalTests.Controllers; /// /// EN: Functional tests for Samples API endpoints. diff --git a/services/booking-service-net/tests/BookingService.FunctionalTests/CustomWebApplicationFactory.cs b/services/booking-service-net/tests/BookingService.FunctionalTests/CustomWebApplicationFactory.cs index d74d8338..0515c6d7 100644 --- a/services/booking-service-net/tests/BookingService.FunctionalTests/CustomWebApplicationFactory.cs +++ b/services/booking-service-net/tests/BookingService.FunctionalTests/CustomWebApplicationFactory.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using MyService.Infrastructure; +using BookingService.Infrastructure; -namespace MyService.FunctionalTests; +namespace BookingService.FunctionalTests; /// /// EN: Custom WebApplicationFactory for functional tests. @@ -21,7 +21,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove the existing DbContext registration // VI: Xóa đăng ký DbContext hiện tại var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); + d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) { @@ -31,7 +31,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove DbContext service // VI: Xóa DbContext service var dbContextDescriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(MyServiceContext)); + d => d.ServiceType == typeof(BookingServiceContext)); if (dbContextDescriptor != null) { @@ -40,7 +40,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Add in-memory database for testing // VI: Thêm in-memory database để test - services.AddDbContext(options => + services.AddDbContext(options => { options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString()); }); @@ -49,7 +49,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // VI: Đảm bảo database được tạo với seed data var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); }); } diff --git a/services/booking-service-net/tests/BookingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/booking-service-net/tests/BookingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs index 75cdd0e8..0e10b430 100644 --- a/services/booking-service-net/tests/BookingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs +++ b/services/booking-service-net/tests/BookingService.UnitTests/Application/CreateSampleCommandHandlerTests.cs @@ -1,12 +1,12 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using MyService.API.Application.Commands; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using BookingService.API.Application.Commands; +using BookingService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.SeedWork; using Xunit; -namespace MyService.UnitTests.Application; +namespace BookingService.UnitTests.Application; /// /// EN: Unit tests for CreateSampleCommandHandler. diff --git a/services/inventory-service-net/tests/InventoryService.UnitTests/MyService.UnitTests.csproj b/services/booking-service-net/tests/BookingService.UnitTests/BookingService.UnitTests.csproj similarity index 78% rename from services/inventory-service-net/tests/InventoryService.UnitTests/MyService.UnitTests.csproj rename to services/booking-service-net/tests/BookingService.UnitTests/BookingService.UnitTests.csproj index b40272dc..5a73b7f3 100644 --- a/services/inventory-service-net/tests/InventoryService.UnitTests/MyService.UnitTests.csproj +++ b/services/booking-service-net/tests/BookingService.UnitTests/BookingService.UnitTests.csproj @@ -1,8 +1,8 @@ - MyService.UnitTests - MyService.UnitTests + BookingService.UnitTests + BookingService.UnitTests false true @@ -28,8 +28,8 @@ - - + + diff --git a/services/booking-service-net/tests/BookingService.UnitTests/Domain/SampleAggregateTests.cs b/services/booking-service-net/tests/BookingService.UnitTests/Domain/SampleAggregateTests.cs index dcf48767..3b3081d4 100644 --- a/services/booking-service-net/tests/BookingService.UnitTests/Domain/SampleAggregateTests.cs +++ b/services/booking-service-net/tests/BookingService.UnitTests/Domain/SampleAggregateTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.Exceptions; +using BookingService.Domain.AggregatesModel.SampleAggregate; +using BookingService.Domain.Exceptions; using Xunit; -namespace MyService.UnitTests.Domain; +namespace BookingService.UnitTests.Domain; /// /// EN: Unit tests for Sample aggregate. diff --git a/services/catalog-service-net/docker-compose.yml b/services/catalog-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/catalog-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/LoggingBehavior.cs b/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/LoggingBehavior.cs index a724424d..a8fb06b4 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/LoggingBehavior.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/LoggingBehavior.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace CatalogService.API.Application.Behaviors; /// /// EN: MediatR behavior for logging request handling. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/TransactionBehavior.cs b/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/TransactionBehavior.cs index 8675b649..dea12f05 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/TransactionBehavior.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/TransactionBehavior.cs @@ -1,8 +1,8 @@ using MediatR; using Microsoft.EntityFrameworkCore; -using MyService.Infrastructure; +using CatalogService.Infrastructure; -namespace MyService.API.Application.Behaviors; +namespace CatalogService.API.Application.Behaviors; /// /// EN: MediatR behavior for handling database transactions. @@ -13,11 +13,11 @@ namespace MyService.API.Application.Behaviors; public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { - private readonly MyServiceContext _dbContext; + private readonly CatalogServiceContext _dbContext; private readonly ILogger> _logger; public TransactionBehavior( - MyServiceContext dbContext, + CatalogServiceContext dbContext, ILogger> logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/ValidatorBehavior.cs b/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/ValidatorBehavior.cs index 0062cd60..f68022ba 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/ValidatorBehavior.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Behaviors/ValidatorBehavior.cs @@ -1,7 +1,7 @@ using FluentValidation; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace CatalogService.API.Application.Behaviors; /// /// EN: MediatR behavior for FluentValidation integration. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/ChangeSampleStatusCommand.cs index 49825490..9877c18d 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/ChangeSampleStatusCommand.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/ChangeSampleStatusCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace CatalogService.API.Application.Commands; /// /// EN: Command to change status of a Sample. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs index 76e31030..45723e24 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace CatalogService.API.Application.Commands; /// /// EN: Handler for ChangeSampleStatusCommand. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateSampleCommand.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateSampleCommand.cs index 138cc794..4a3a55fa 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateSampleCommand.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace CatalogService.API.Application.Commands; /// /// EN: Command to create a new Sample. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateSampleCommandHandler.cs index d7d0fd7c..38159eac 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateSampleCommandHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/CreateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace CatalogService.API.Application.Commands; /// /// EN: Handler for CreateSampleCommand. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteSampleCommand.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteSampleCommand.cs index 0de392db..5efdacbb 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteSampleCommand.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace CatalogService.API.Application.Commands; /// /// EN: Command to delete a Sample. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteSampleCommandHandler.cs index c7632189..10236b54 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteSampleCommandHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/DeleteSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace CatalogService.API.Application.Commands; /// /// EN: Handler for DeleteSampleCommand. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateSampleCommand.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateSampleCommand.cs index 6fad8514..b41fc95a 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateSampleCommand.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace CatalogService.API.Application.Commands; /// /// EN: Command to update an existing Sample. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateSampleCommandHandler.cs index e904cf0a..d6356ee8 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateSampleCommandHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Commands/UpdateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace CatalogService.API.Application.Commands; /// /// EN: Handler for UpdateSampleCommand. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSampleQuery.cs b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSampleQuery.cs index 8b90789c..ded09d79 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSampleQuery.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSampleQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace CatalogService.API.Application.Queries; /// /// EN: Query to get a Sample by ID. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSampleQueryHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSampleQueryHandler.cs index 2da10b6d..7377b77c 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSampleQueryHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSampleQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace CatalogService.API.Application.Queries; /// /// EN: Handler for GetSampleQuery. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSamplesQuery.cs b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSamplesQuery.cs index d6a98e34..cd14eb74 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSamplesQuery.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSamplesQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace CatalogService.API.Application.Queries; /// /// EN: Query to get all Samples. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSamplesQueryHandler.cs index 2185302d..864f6f7e 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSamplesQueryHandler.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Queries/GetSamplesQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace CatalogService.API.Application.Queries; /// /// EN: Handler for GetSamplesQuery. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/catalog-service-net/src/CatalogService.API/Application/Validations/CreateSampleCommandValidator.cs index 2f339fb3..64e8697f 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Validations/CreateSampleCommandValidator.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Validations/CreateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using CatalogService.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace CatalogService.API.Application.Validations; /// /// EN: Validator for CreateSampleCommand. diff --git a/services/catalog-service-net/src/CatalogService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/catalog-service-net/src/CatalogService.API/Application/Validations/UpdateSampleCommandValidator.cs index 7030d5c8..c078e179 100644 --- a/services/catalog-service-net/src/CatalogService.API/Application/Validations/UpdateSampleCommandValidator.cs +++ b/services/catalog-service-net/src/CatalogService.API/Application/Validations/UpdateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using CatalogService.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace CatalogService.API.Application.Validations; /// /// EN: Validator for UpdateSampleCommand. diff --git a/services/catalog-service-net/src/CatalogService.API/CatalogService.API.csproj b/services/catalog-service-net/src/CatalogService.API/CatalogService.API.csproj index 1b5bb222..6cbf3bbb 100644 --- a/services/catalog-service-net/src/CatalogService.API/CatalogService.API.csproj +++ b/services/catalog-service-net/src/CatalogService.API/CatalogService.API.csproj @@ -1,8 +1,8 @@ - MyService.API - MyService.API + CatalogService.API + CatalogService.API Web API layer with CQRS pattern myservice-api @@ -36,8 +36,8 @@ - - + + diff --git a/services/catalog-service-net/src/CatalogService.API/Controllers/SamplesController.cs b/services/catalog-service-net/src/CatalogService.API/Controllers/SamplesController.cs index c87e0ffa..0002da87 100644 --- a/services/catalog-service-net/src/CatalogService.API/Controllers/SamplesController.cs +++ b/services/catalog-service-net/src/CatalogService.API/Controllers/SamplesController.cs @@ -1,10 +1,10 @@ using Asp.Versioning; using MediatR; using Microsoft.AspNetCore.Mvc; -using MyService.API.Application.Commands; -using MyService.API.Application.Queries; +using CatalogService.API.Application.Commands; +using CatalogService.API.Application.Queries; -namespace MyService.API.Controllers; +namespace CatalogService.API.Controllers; /// /// EN: Controller for Sample CRUD operations using CQRS pattern. diff --git a/services/catalog-service-net/src/CatalogService.API/Program.cs b/services/catalog-service-net/src/CatalogService.API/Program.cs index bd9b3df4..623e3a24 100644 --- a/services/catalog-service-net/src/CatalogService.API/Program.cs +++ b/services/catalog-service-net/src/CatalogService.API/Program.cs @@ -1,8 +1,8 @@ using Asp.Versioning; using FluentValidation; using Hellang.Middleware.ProblemDetails; -using MyService.API.Application.Behaviors; -using MyService.Infrastructure; +using CatalogService.API.Application.Behaviors; +using CatalogService.Infrastructure; using Serilog; // EN: Configure Serilog early / VI: Cấu hình Serilog sớm @@ -12,7 +12,7 @@ Log.Logger = new LoggerConfiguration() try { - Log.Information("Starting MyService API / Khởi động MyService API"); + Log.Information("Starting CatalogService API / Khởi động CatalogService API"); var builder = WebApplication.CreateBuilder(args); @@ -70,9 +70,9 @@ try { options.SwaggerDoc("v1", new() { - Title = "MyService API", + Title = "CatalogService API", Version = "v1", - Description = "MyService microservice API / API microservice MyService" + Description = "CatalogService microservice API / API microservice CatalogService" }); }); @@ -107,7 +107,7 @@ try app.UseSwagger(); app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyService API v1"); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "CatalogService API v1"); c.RoutePrefix = "swagger"; }); } diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Category.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Category.cs new file mode 100644 index 00000000..6ae47d39 --- /dev/null +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Category.cs @@ -0,0 +1,153 @@ +// EN: Category entity for product categorization. +// VI: Entity Category cho phân loại sản phẩm. + +using CatalogService.Domain.Exceptions; +using CatalogService.Domain.SeedWork; + +namespace CatalogService.Domain.AggregatesModel.ProductAggregate; + +/// +/// EN: Category entity for hierarchical product categorization. +/// VI: Entity Category cho phân loại sản phẩm phân cấp. +/// +public class Category : Entity +{ + private Guid _shopId; + private string _name = null!; + private string? _description; + private Guid? _parentId; + private int _displayOrder; + private bool _isActive; + private DateTime _createdAt; + private DateTime? _updatedAt; + + /// + /// EN: Shop ID that owns this category. + /// VI: ID shop sở hữu danh mục này. + /// + public Guid ShopId => _shopId; + + /// + /// EN: Category name. + /// VI: Tên danh mục. + /// + public string Name => _name; + + /// + /// EN: Category description. + /// VI: Mô tả danh mục. + /// + public string? Description => _description; + + /// + /// EN: Parent category ID (for hierarchical categories). + /// VI: ID danh mục cha (cho danh mục phân cấp). + /// + public Guid? ParentId => _parentId; + + /// + /// EN: Display order. + /// VI: Thứ tự hiển thị. + /// + public int DisplayOrder => _displayOrder; + + /// + /// EN: Is category active. + /// VI: Danh mục có đang hoạt động không. + /// + public bool IsActive => _isActive; + + /// + /// 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 Category() + { + } + + /// + /// EN: Create a new category. + /// VI: Tạo danh mục mới. + /// + public Category( + Guid shopId, + string name, + string? description = null, + Guid? parentId = null, + int displayOrder = 0) + { + if (shopId == Guid.Empty) + throw new DomainException("Shop ID cannot be empty"); + if (string.IsNullOrWhiteSpace(name)) + throw new DomainException("Category name cannot be empty"); + + Id = Guid.NewGuid(); + _shopId = shopId; + _name = name.Trim(); + _description = description?.Trim(); + _parentId = parentId; + _displayOrder = displayOrder; + _isActive = true; + _createdAt = DateTime.UtcNow; + } + + /// + /// EN: Update category information. + /// VI: Cập nhật thông tin danh mục. + /// + public void UpdateInfo(string name, string? description, int displayOrder) + { + if (string.IsNullOrWhiteSpace(name)) + throw new DomainException("Category name cannot be empty"); + + _name = name.Trim(); + _description = description?.Trim(); + _displayOrder = displayOrder; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Update parent category. + /// VI: Cập nhật danh mục cha. + /// + public void UpdateParent(Guid? parentId) + { + if (parentId == Id) + throw new DomainException("Category cannot be its own parent"); + + _parentId = parentId; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Activate category. + /// VI: Kích hoạt danh mục. + /// + public void Activate() + { + _isActive = true; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Deactivate category. + /// VI: Vô hiệu hóa danh mục. + /// + public void Deactivate() + { + _isActive = false; + _updatedAt = DateTime.UtcNow; + } +} diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/IProductRepository.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/IProductRepository.cs new file mode 100644 index 00000000..63c14e49 --- /dev/null +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/IProductRepository.cs @@ -0,0 +1,43 @@ +// EN: Product repository interface. +// VI: Interface repository Product. + +using CatalogService.Domain.SeedWork; + +namespace CatalogService.Domain.AggregatesModel.ProductAggregate; + +/// +/// EN: Repository interface for Product aggregate. +/// VI: Interface repository cho Product aggregate. +/// +public interface IProductRepository : IRepository +{ + /// + /// EN: Add a new product. + /// VI: Thêm sản phẩm mới. + /// + Product Add(Product product); + + /// + /// EN: Update existing product. + /// VI: Cập nhật sản phẩm hiện có. + /// + void Update(Product product); + + /// + /// EN: Get product by ID. + /// VI: Lấy sản phẩm theo ID. + /// + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// EN: Get products by shop ID. + /// VI: Lấy danh sách sản phẩm theo shop ID. + /// + Task> GetByShopIdAsync(Guid shopId, CancellationToken cancellationToken = default); + + /// + /// EN: Get products by type. + /// VI: Lấy danh sách sản phẩm theo loại. + /// + Task> GetByTypeAsync(Guid shopId, ProductType type, CancellationToken cancellationToken = default); +} diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Product.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Product.cs new file mode 100644 index 00000000..b39b26d4 --- /dev/null +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/Product.cs @@ -0,0 +1,207 @@ +// EN: Product aggregate root for polymorphic products. +// VI: Aggregate root Product cho sản phẩm đa hình. + +using System.Text.Json; +using CatalogService.Domain.Events; +using CatalogService.Domain.Exceptions; +using CatalogService.Domain.SeedWork; + +namespace CatalogService.Domain.AggregatesModel.ProductAggregate; + +/// +/// EN: Product aggregate root - polymorphic product supporting multi-vertical. +/// VI: Aggregate root Product - sản phẩm đa hình hỗ trợ đa ngành hàng. +/// +public class Product : Entity, IAggregateRoot +{ + private Guid _shopId; + private string _name = null!; + private string? _description; + private decimal _price; + private ProductType _type = null!; + private JsonDocument? _attributes; // Type-specific attributes in JSONB + private string? _imageUrl; + private string? _sku; + private bool _isActive; + private DateTime _createdAt; + private DateTime? _updatedAt; + + /// + /// EN: Shop ID that owns this product. + /// VI: ID shop sở hữu sản phẩm này. + /// + public Guid ShopId => _shopId; + + /// + /// EN: Product name. + /// VI: Tên sản phẩm. + /// + public string Name => _name; + + /// + /// EN: Product description. + /// VI: Mô tả sản phẩm. + /// + public string? Description => _description; + + /// + /// EN: Product price. + /// VI: Giá sản phẩm. + /// + public decimal Price => _price; + + /// + /// EN: Product type. + /// VI: Loại sản phẩm. + /// + public ProductType Type => _type; + + /// + /// EN: Type ID for EF Core mapping. + /// VI: Type ID cho EF Core mapping. + /// + public int TypeId { get; private set; } + + /// + /// EN: Type-specific attributes stored as JSONB. + /// VI: Thuộc tính theo loại lưu dưới dạng JSONB. + /// + public JsonDocument? Attributes => _attributes; + + /// + /// EN: Product image URL. + /// VI: URL hình ảnh sản phẩm. + /// + public string? ImageUrl => _imageUrl; + + /// + /// EN: Stock Keeping Unit (for Physical products). + /// VI: Mã SKU (cho sản phẩm vật lý). + /// + public string? Sku => _sku; + + /// + /// EN: Is product active and available for sale. + /// VI: Sản phẩm có đang hoạt động và sẵn sàng bán không. + /// + public bool IsActive => _isActive; + + /// + /// 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 Product() + { + } + + /// + /// EN: Create a new product. + /// VI: Tạo sản phẩm mới. + /// + public Product( + Guid shopId, + string name, + decimal price, + ProductType type, + string? description = null, + JsonDocument? attributes = null, + string? sku = null) + { + if (shopId == Guid.Empty) + throw new DomainException("Shop ID cannot be empty"); + if (string.IsNullOrWhiteSpace(name)) + throw new DomainException("Product name cannot be empty"); + if (price < 0) + throw new DomainException("Price cannot be negative"); + ArgumentNullException.ThrowIfNull(type, nameof(type)); + + Id = Guid.NewGuid(); + _shopId = shopId; + _name = name.Trim(); + _description = description?.Trim(); + _price = price; + _type = type; + TypeId = type.Id; + _attributes = attributes; + _sku = sku?.Trim(); + _isActive = true; + _createdAt = DateTime.UtcNow; + + AddDomainEvent(new ProductCreatedDomainEvent(this)); + } + + /// + /// EN: Update product basic information. + /// VI: Cập nhật thông tin cơ bản sản phẩm. + /// + public void UpdateInfo(string name, string? description, decimal price) + { + if (string.IsNullOrWhiteSpace(name)) + throw new DomainException("Product name cannot be empty"); + if (price < 0) + throw new DomainException("Price cannot be negative"); + + _name = name.Trim(); + _description = description?.Trim(); + _price = price; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Update type-specific attributes. + /// VI: Cập nhật thuộc tính theo loại. + /// + public void UpdateAttributes(JsonDocument? attributes) + { + _attributes = attributes; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Update product image. + /// VI: Cập nhật hình ảnh sản phẩm. + /// + public void UpdateImage(string? imageUrl) + { + _imageUrl = imageUrl; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Activate product. + /// VI: Kích hoạt sản phẩm. + /// + public void Activate() + { + if (_isActive) + throw new DomainException("Product is already active"); + + _isActive = true; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Deactivate product. + /// VI: Vô hiệu hóa sản phẩm. + /// + public void Deactivate() + { + if (!_isActive) + throw new DomainException("Product is already inactive"); + + _isActive = false; + _updatedAt = DateTime.UtcNow; + } +} diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/ProductType.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/ProductType.cs new file mode 100644 index 00000000..bb3f9037 --- /dev/null +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/ProductAggregate/ProductType.cs @@ -0,0 +1,35 @@ +// EN: Product type enumeration for polymorphic products. +// VI: Enumeration loại sản phẩm cho sản phẩm đa hình. + +using CatalogService.Domain.SeedWork; + +namespace CatalogService.Domain.AggregatesModel.ProductAggregate; + +/// +/// EN: Product type enumeration - supports multi-vertical products. +/// VI: Enumeration loại sản phẩm - hỗ trợ sản phẩm đa ngành hàng. +/// +public class ProductType : Enumeration +{ + /// + /// EN: Physical product (Retail) - requires inventory. + /// VI: Sản phẩm vật lý (Bán lẻ) - cần quản lý tồn kho. + /// + public static readonly ProductType Physical = new(1, nameof(Physical)); + + /// + /// EN: Service product (Spa, Salon) - requires booking. + /// VI: Sản phẩm dịch vụ (Spa, Salon) - cần đặt lịch. + /// + public static readonly ProductType Service = new(2, nameof(Service)); + + /// + /// EN: Prepared food product (FnB) - requires kitchen. + /// VI: Sản phẩm thực phẩm chế biến (FnB) - cần bếp. + /// + public static readonly ProductType PreparedFood = new(3, nameof(PreparedFood)); + + public ProductType(int id, string name) : base(id, name) + { + } +} diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs index 40bc8c3a..e7ed0d4b 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using CatalogService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace CatalogService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Repository interface for Sample aggregate. diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/Sample.cs index 641bb385..1ead7773 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/Sample.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/Sample.cs @@ -1,8 +1,8 @@ -using MyService.Domain.Events; -using MyService.Domain.Exceptions; -using MyService.Domain.SeedWork; +using CatalogService.Domain.Events; +using CatalogService.Domain.Exceptions; +using CatalogService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace CatalogService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample aggregate root demonstrating DDD patterns. diff --git a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs index 54ce63ba..200bc8b3 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using CatalogService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace CatalogService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample status enumeration following type-safe enum pattern. diff --git a/services/catalog-service-net/src/CatalogService.Domain/CatalogService.Domain.csproj b/services/catalog-service-net/src/CatalogService.Domain/CatalogService.Domain.csproj index 3208317a..91f9df22 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/CatalogService.Domain.csproj +++ b/services/catalog-service-net/src/CatalogService.Domain/CatalogService.Domain.csproj @@ -1,8 +1,8 @@ - MyService.Domain - MyService.Domain + CatalogService.Domain + CatalogService.Domain Domain layer containing core business logic and entities diff --git a/services/catalog-service-net/src/CatalogService.Domain/Events/ProductCreatedDomainEvent.cs b/services/catalog-service-net/src/CatalogService.Domain/Events/ProductCreatedDomainEvent.cs new file mode 100644 index 00000000..c8f3e3a2 --- /dev/null +++ b/services/catalog-service-net/src/CatalogService.Domain/Events/ProductCreatedDomainEvent.cs @@ -0,0 +1,13 @@ +// EN: Product created domain event. +// VI: Domain event sản phẩm được tạo. + +using CatalogService.Domain.AggregatesModel.ProductAggregate; +using MediatR; + +namespace CatalogService.Domain.Events; + +/// +/// EN: Domain event raised when a product is created. +/// VI: Domain event phát ra khi sản phẩm được tạo. +/// +public record ProductCreatedDomainEvent(Product Product) : INotification; diff --git a/services/catalog-service-net/src/CatalogService.Domain/Events/SampleCreatedDomainEvent.cs b/services/catalog-service-net/src/CatalogService.Domain/Events/SampleCreatedDomainEvent.cs index 7e838214..9485ad61 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/Events/SampleCreatedDomainEvent.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/Events/SampleCreatedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace CatalogService.Domain.Events; /// /// EN: Domain event raised when a new Sample is created. diff --git a/services/catalog-service-net/src/CatalogService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/catalog-service-net/src/CatalogService.Domain/Events/SampleStatusChangedDomainEvent.cs index f6d9b422..42f1fd03 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/Events/SampleStatusChangedDomainEvent.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/Events/SampleStatusChangedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace CatalogService.Domain.Events; /// /// EN: Domain event raised when Sample status changes. diff --git a/services/catalog-service-net/src/CatalogService.Domain/Exceptions/DomainException.cs b/services/catalog-service-net/src/CatalogService.Domain/Exceptions/DomainException.cs index 7e737f64..6c6da865 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/Exceptions/DomainException.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/Exceptions/DomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace CatalogService.Domain.Exceptions; /// /// EN: Base exception for domain errors. diff --git a/services/catalog-service-net/src/CatalogService.Domain/Exceptions/SampleDomainException.cs b/services/catalog-service-net/src/CatalogService.Domain/Exceptions/SampleDomainException.cs index c850944c..dca7d868 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/Exceptions/SampleDomainException.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/Exceptions/SampleDomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace CatalogService.Domain.Exceptions; /// /// EN: Exception for Sample aggregate domain errors. diff --git a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/Entity.cs b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/Entity.cs index b07fdd3b..1eb13242 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/Entity.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/Entity.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.Domain.SeedWork; +namespace CatalogService.Domain.SeedWork; /// /// EN: Base class for all domain entities. diff --git a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/Enumeration.cs b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/Enumeration.cs index 6641979c..d1f6d420 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/Enumeration.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/Enumeration.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace MyService.Domain.SeedWork; +namespace CatalogService.Domain.SeedWork; /// /// EN: Base class for enumeration classes (type-safe enum pattern). diff --git a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IAggregateRoot.cs b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IAggregateRoot.cs index d361394f..afc43192 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IAggregateRoot.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IAggregateRoot.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace CatalogService.Domain.SeedWork; /// /// EN: Marker interface for aggregate roots. diff --git a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IRepository.cs b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IRepository.cs index 2d539e44..e471dc1e 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IRepository.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IRepository.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace CatalogService.Domain.SeedWork; /// /// EN: Generic repository interface for aggregate roots. diff --git a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IUnitOfWork.cs b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IUnitOfWork.cs index d37d8fa4..69ce618c 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IUnitOfWork.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/IUnitOfWork.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace CatalogService.Domain.SeedWork; /// /// EN: Unit of Work pattern interface. diff --git a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/ValueObject.cs b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/ValueObject.cs index 5cf4188f..58dc70c2 100644 --- a/services/catalog-service-net/src/CatalogService.Domain/SeedWork/ValueObject.cs +++ b/services/catalog-service-net/src/CatalogService.Domain/SeedWork/ValueObject.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace CatalogService.Domain.SeedWork; /// /// EN: Base class for Value Objects following DDD patterns. diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/CatalogService.Infrastructure.csproj b/services/catalog-service-net/src/CatalogService.Infrastructure/CatalogService.Infrastructure.csproj index 7c81b5e9..70e19254 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/CatalogService.Infrastructure.csproj +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/CatalogService.Infrastructure.csproj @@ -1,8 +1,8 @@ - MyService.Infrastructure - MyService.Infrastructure + CatalogService.Infrastructure + CatalogService.Infrastructure Infrastructure layer for data access and external services @@ -30,7 +30,7 @@ - + diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/DependencyInjection.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/DependencyInjection.cs index a8dfba0d..e39b860d 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/DependencyInjection.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/DependencyInjection.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Infrastructure.Idempotency; -using MyService.Infrastructure.Repositories; +using CatalogService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Infrastructure.Idempotency; +using CatalogService.Infrastructure.Repositories; -namespace MyService.Infrastructure; +namespace CatalogService.Infrastructure; /// /// EN: Dependency injection extensions for Infrastructure layer. @@ -22,7 +22,7 @@ public static class DependencyInjection IConfiguration configuration) { // EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL - services.AddDbContext(options => + services.AddDbContext(options => { var connectionString = configuration.GetConnectionString("DefaultConnection") ?? configuration["DATABASE_URL"] @@ -30,7 +30,7 @@ public static class DependencyInjection options.UseNpgsql(connectionString, npgsqlOptions => { - npgsqlOptions.MigrationsAssembly(typeof(MyServiceContext).Assembly.FullName); + npgsqlOptions.MigrationsAssembly(typeof(CatalogServiceContext).Assembly.FullName); npgsqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs index 5c2fbd42..68ea385b 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace CatalogService.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for Sample entity. diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs index 8b683f56..a057e4f1 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace CatalogService.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for SampleStatus enumeration. diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/ClientRequest.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/ClientRequest.cs index f65e4a56..a340ca92 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/ClientRequest.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/ClientRequest.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace CatalogService.Infrastructure.Idempotency; /// /// EN: Entity for tracking client requests to ensure idempotency. diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/IRequestManager.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/IRequestManager.cs index 92097399..0ce54bf1 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/IRequestManager.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/IRequestManager.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace CatalogService.Infrastructure.Idempotency; /// /// EN: Interface for managing client request idempotency. diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/RequestManager.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/RequestManager.cs index 41a5f318..93126bdd 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/RequestManager.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/Idempotency/RequestManager.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace MyService.Infrastructure.Idempotency; +namespace CatalogService.Infrastructure.Idempotency; /// /// EN: Implementation of request manager for idempotency. @@ -8,9 +8,9 @@ namespace MyService.Infrastructure.Idempotency; /// public class RequestManager : IRequestManager { - private readonly MyServiceContext _context; + private readonly CatalogServiceContext _context; - public RequestManager(MyServiceContext context) + public RequestManager(CatalogServiceContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/MyServiceContext.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/MyServiceContext.cs index 4486367d..5d493807 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/MyServiceContext.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/MyServiceContext.cs @@ -1,17 +1,17 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; -using MyService.Infrastructure.EntityConfigurations; +using CatalogService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.SeedWork; +using CatalogService.Infrastructure.EntityConfigurations; -namespace MyService.Infrastructure; +namespace CatalogService.Infrastructure; /// -/// EN: EF Core DbContext for MyService. -/// VI: EF Core DbContext cho MyService. +/// EN: EF Core DbContext for CatalogService. +/// VI: EF Core DbContext cho CatalogService. /// -public class MyServiceContext : DbContext, IUnitOfWork +public class CatalogServiceContext : DbContext, IUnitOfWork { private readonly IMediator _mediator; private IDbContextTransaction? _currentTransaction; @@ -34,16 +34,16 @@ public class MyServiceContext : DbContext, IUnitOfWork /// public bool HasActiveTransaction => _currentTransaction != null; - public MyServiceContext(DbContextOptions options) : base(options) + public CatalogServiceContext(DbContextOptions options) : base(options) { _mediator = null!; } - public MyServiceContext(DbContextOptions options, IMediator mediator) : base(options) + public CatalogServiceContext(DbContextOptions options, IMediator mediator) : base(options) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); - System.Diagnostics.Debug.WriteLine("MyServiceContext::ctor - " + GetHashCode()); + System.Diagnostics.Debug.WriteLine("CatalogServiceContext::ctor - " + GetHashCode()); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/services/catalog-service-net/src/CatalogService.Infrastructure/Repositories/SampleRepository.cs b/services/catalog-service-net/src/CatalogService.Infrastructure/Repositories/SampleRepository.cs index f0a4b109..872662b5 100644 --- a/services/catalog-service-net/src/CatalogService.Infrastructure/Repositories/SampleRepository.cs +++ b/services/catalog-service-net/src/CatalogService.Infrastructure/Repositories/SampleRepository.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using CatalogService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.SeedWork; -namespace MyService.Infrastructure.Repositories; +namespace CatalogService.Infrastructure.Repositories; /// /// EN: Repository implementation for Sample aggregate. @@ -10,7 +10,7 @@ namespace MyService.Infrastructure.Repositories; /// public class SampleRepository : ISampleRepository { - private readonly MyServiceContext _context; + private readonly CatalogServiceContext _context; /// /// EN: Unit of work for transaction management. @@ -18,7 +18,7 @@ public class SampleRepository : ISampleRepository /// public IUnitOfWork UnitOfWork => _context; - public SampleRepository(MyServiceContext context) + public SampleRepository(CatalogServiceContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/inventory-service-net/tests/InventoryService.FunctionalTests/MyService.FunctionalTests.csproj b/services/catalog-service-net/tests/CatalogService.FunctionalTests/CatalogService.FunctionalTests.csproj similarity index 86% rename from services/inventory-service-net/tests/InventoryService.FunctionalTests/MyService.FunctionalTests.csproj rename to services/catalog-service-net/tests/CatalogService.FunctionalTests/CatalogService.FunctionalTests.csproj index 4cc894f8..b9c393f8 100644 --- a/services/inventory-service-net/tests/InventoryService.FunctionalTests/MyService.FunctionalTests.csproj +++ b/services/catalog-service-net/tests/CatalogService.FunctionalTests/CatalogService.FunctionalTests.csproj @@ -1,8 +1,8 @@ - MyService.FunctionalTests - MyService.FunctionalTests + CatalogService.FunctionalTests + CatalogService.FunctionalTests false true @@ -32,7 +32,7 @@ - + diff --git a/services/catalog-service-net/tests/CatalogService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/catalog-service-net/tests/CatalogService.FunctionalTests/Controllers/SamplesControllerTests.cs index e6e99ac5..b701e25b 100644 --- a/services/catalog-service-net/tests/CatalogService.FunctionalTests/Controllers/SamplesControllerTests.cs +++ b/services/catalog-service-net/tests/CatalogService.FunctionalTests/Controllers/SamplesControllerTests.cs @@ -4,7 +4,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; -namespace MyService.FunctionalTests.Controllers; +namespace CatalogService.FunctionalTests.Controllers; /// /// EN: Functional tests for Samples API endpoints. diff --git a/services/catalog-service-net/tests/CatalogService.FunctionalTests/CustomWebApplicationFactory.cs b/services/catalog-service-net/tests/CatalogService.FunctionalTests/CustomWebApplicationFactory.cs index d74d8338..2d01c732 100644 --- a/services/catalog-service-net/tests/CatalogService.FunctionalTests/CustomWebApplicationFactory.cs +++ b/services/catalog-service-net/tests/CatalogService.FunctionalTests/CustomWebApplicationFactory.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using MyService.Infrastructure; +using CatalogService.Infrastructure; -namespace MyService.FunctionalTests; +namespace CatalogService.FunctionalTests; /// /// EN: Custom WebApplicationFactory for functional tests. @@ -21,7 +21,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove the existing DbContext registration // VI: Xóa đăng ký DbContext hiện tại var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); + d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) { @@ -31,7 +31,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove DbContext service // VI: Xóa DbContext service var dbContextDescriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(MyServiceContext)); + d => d.ServiceType == typeof(CatalogServiceContext)); if (dbContextDescriptor != null) { @@ -40,7 +40,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Add in-memory database for testing // VI: Thêm in-memory database để test - services.AddDbContext(options => + services.AddDbContext(options => { options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString()); }); @@ -49,7 +49,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // VI: Đảm bảo database được tạo với seed data var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); }); } diff --git a/services/catalog-service-net/tests/CatalogService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/catalog-service-net/tests/CatalogService.UnitTests/Application/CreateSampleCommandHandlerTests.cs index 75cdd0e8..657d76c2 100644 --- a/services/catalog-service-net/tests/CatalogService.UnitTests/Application/CreateSampleCommandHandlerTests.cs +++ b/services/catalog-service-net/tests/CatalogService.UnitTests/Application/CreateSampleCommandHandlerTests.cs @@ -1,12 +1,12 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using MyService.API.Application.Commands; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using CatalogService.API.Application.Commands; +using CatalogService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.SeedWork; using Xunit; -namespace MyService.UnitTests.Application; +namespace CatalogService.UnitTests.Application; /// /// EN: Unit tests for CreateSampleCommandHandler. diff --git a/services/catalog-service-net/tests/CatalogService.UnitTests/MyService.UnitTests.csproj b/services/catalog-service-net/tests/CatalogService.UnitTests/CatalogService.UnitTests.csproj similarity index 78% rename from services/catalog-service-net/tests/CatalogService.UnitTests/MyService.UnitTests.csproj rename to services/catalog-service-net/tests/CatalogService.UnitTests/CatalogService.UnitTests.csproj index b40272dc..711b54ac 100644 --- a/services/catalog-service-net/tests/CatalogService.UnitTests/MyService.UnitTests.csproj +++ b/services/catalog-service-net/tests/CatalogService.UnitTests/CatalogService.UnitTests.csproj @@ -1,8 +1,8 @@ - MyService.UnitTests - MyService.UnitTests + CatalogService.UnitTests + CatalogService.UnitTests false true @@ -28,8 +28,8 @@ - - + + diff --git a/services/catalog-service-net/tests/CatalogService.UnitTests/Domain/SampleAggregateTests.cs b/services/catalog-service-net/tests/CatalogService.UnitTests/Domain/SampleAggregateTests.cs index dcf48767..26218e25 100644 --- a/services/catalog-service-net/tests/CatalogService.UnitTests/Domain/SampleAggregateTests.cs +++ b/services/catalog-service-net/tests/CatalogService.UnitTests/Domain/SampleAggregateTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.Exceptions; +using CatalogService.Domain.AggregatesModel.SampleAggregate; +using CatalogService.Domain.Exceptions; using Xunit; -namespace MyService.UnitTests.Domain; +namespace CatalogService.UnitTests.Domain; /// /// EN: Unit tests for Sample aggregate. diff --git a/services/fnb-engine-net/docker-compose.yml b/services/fnb-engine-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/fnb-engine-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/LoggingBehavior.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/LoggingBehavior.cs index a724424d..4a199a0d 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/LoggingBehavior.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/LoggingBehavior.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace FnbEngine.API.Application.Behaviors; /// /// EN: MediatR behavior for logging request handling. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/TransactionBehavior.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/TransactionBehavior.cs index 8675b649..f644b3fd 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/TransactionBehavior.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/TransactionBehavior.cs @@ -1,8 +1,8 @@ using MediatR; using Microsoft.EntityFrameworkCore; -using MyService.Infrastructure; +using FnbEngine.Infrastructure; -namespace MyService.API.Application.Behaviors; +namespace FnbEngine.API.Application.Behaviors; /// /// EN: MediatR behavior for handling database transactions. @@ -13,11 +13,11 @@ namespace MyService.API.Application.Behaviors; public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { - private readonly MyServiceContext _dbContext; + private readonly FnbEngineContext _dbContext; private readonly ILogger> _logger; public TransactionBehavior( - MyServiceContext dbContext, + FnbEngineContext dbContext, ILogger> logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/ValidatorBehavior.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/ValidatorBehavior.cs index 0062cd60..21888d71 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/ValidatorBehavior.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Behaviors/ValidatorBehavior.cs @@ -1,7 +1,7 @@ using FluentValidation; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace FnbEngine.API.Application.Behaviors; /// /// EN: MediatR behavior for FluentValidation integration. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/ChangeSampleStatusCommand.cs index 49825490..c70629ee 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/ChangeSampleStatusCommand.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/ChangeSampleStatusCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace FnbEngine.API.Application.Commands; /// /// EN: Command to change status of a Sample. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/ChangeSampleStatusCommandHandler.cs index 76e31030..aeb626db 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/ChangeSampleStatusCommandHandler.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/ChangeSampleStatusCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace FnbEngine.API.Application.Commands; /// /// EN: Handler for ChangeSampleStatusCommand. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateSampleCommand.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateSampleCommand.cs index 138cc794..e8dadccb 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateSampleCommand.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace FnbEngine.API.Application.Commands; /// /// EN: Command to create a new Sample. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateSampleCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateSampleCommandHandler.cs index d7d0fd7c..28ace011 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateSampleCommandHandler.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace FnbEngine.API.Application.Commands; /// /// EN: Handler for CreateSampleCommand. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/DeleteSampleCommand.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/DeleteSampleCommand.cs index 0de392db..d532b9dd 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/DeleteSampleCommand.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/DeleteSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace FnbEngine.API.Application.Commands; /// /// EN: Command to delete a Sample. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/DeleteSampleCommandHandler.cs index c7632189..bfb3b237 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/DeleteSampleCommandHandler.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/DeleteSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace FnbEngine.API.Application.Commands; /// /// EN: Handler for DeleteSampleCommand. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateSampleCommand.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateSampleCommand.cs index 6fad8514..2213b81a 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateSampleCommand.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace FnbEngine.API.Application.Commands; /// /// EN: Command to update an existing Sample. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateSampleCommandHandler.cs index e904cf0a..dcf72e54 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateSampleCommandHandler.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace FnbEngine.API.Application.Commands; /// /// EN: Handler for UpdateSampleCommand. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSampleQuery.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSampleQuery.cs index 8b90789c..4555d605 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSampleQuery.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSampleQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace FnbEngine.API.Application.Queries; /// /// EN: Query to get a Sample by ID. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSampleQueryHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSampleQueryHandler.cs index 2da10b6d..d4be2933 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSampleQueryHandler.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSampleQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace FnbEngine.API.Application.Queries; /// /// EN: Handler for GetSampleQuery. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSamplesQuery.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSamplesQuery.cs index d6a98e34..33c85dee 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSamplesQuery.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSamplesQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace FnbEngine.API.Application.Queries; /// /// EN: Query to get all Samples. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSamplesQueryHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSamplesQueryHandler.cs index 2185302d..724a0e76 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSamplesQueryHandler.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSamplesQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace FnbEngine.API.Application.Queries; /// /// EN: Handler for GetSamplesQuery. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Validations/CreateSampleCommandValidator.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Validations/CreateSampleCommandValidator.cs index 2f339fb3..b0cf6e93 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Validations/CreateSampleCommandValidator.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Validations/CreateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using FnbEngine.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace FnbEngine.API.Application.Validations; /// /// EN: Validator for CreateSampleCommand. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Validations/UpdateSampleCommandValidator.cs index 7030d5c8..cb336c44 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Validations/UpdateSampleCommandValidator.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Validations/UpdateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using FnbEngine.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace FnbEngine.API.Application.Validations; /// /// EN: Validator for UpdateSampleCommand. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Controllers/SamplesController.cs b/services/fnb-engine-net/src/FnbEngine.API/Controllers/SamplesController.cs index c87e0ffa..f03ec326 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Controllers/SamplesController.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Controllers/SamplesController.cs @@ -1,10 +1,10 @@ using Asp.Versioning; using MediatR; using Microsoft.AspNetCore.Mvc; -using MyService.API.Application.Commands; -using MyService.API.Application.Queries; +using FnbEngine.API.Application.Commands; +using FnbEngine.API.Application.Queries; -namespace MyService.API.Controllers; +namespace FnbEngine.API.Controllers; /// /// EN: Controller for Sample CRUD operations using CQRS pattern. diff --git a/services/fnb-engine-net/src/FnbEngine.API/FnbEngine.API.csproj b/services/fnb-engine-net/src/FnbEngine.API/FnbEngine.API.csproj index 1b5bb222..59aa4934 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/FnbEngine.API.csproj +++ b/services/fnb-engine-net/src/FnbEngine.API/FnbEngine.API.csproj @@ -1,8 +1,8 @@ - MyService.API - MyService.API + FnbEngine.API + FnbEngine.API Web API layer with CQRS pattern myservice-api @@ -36,8 +36,8 @@ - - + + diff --git a/services/fnb-engine-net/src/FnbEngine.API/Program.cs b/services/fnb-engine-net/src/FnbEngine.API/Program.cs index bd9b3df4..3c4ba316 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Program.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Program.cs @@ -1,8 +1,8 @@ using Asp.Versioning; using FluentValidation; using Hellang.Middleware.ProblemDetails; -using MyService.API.Application.Behaviors; -using MyService.Infrastructure; +using FnbEngine.API.Application.Behaviors; +using FnbEngine.Infrastructure; using Serilog; // EN: Configure Serilog early / VI: Cấu hình Serilog sớm @@ -12,7 +12,7 @@ Log.Logger = new LoggerConfiguration() try { - Log.Information("Starting MyService API / Khởi động MyService API"); + Log.Information("Starting FnbEngine API / Khởi động FnbEngine API"); var builder = WebApplication.CreateBuilder(args); @@ -70,9 +70,9 @@ try { options.SwaggerDoc("v1", new() { - Title = "MyService API", + Title = "FnbEngine API", Version = "v1", - Description = "MyService microservice API / API microservice MyService" + Description = "FnbEngine microservice API / API microservice FnbEngine" }); }); @@ -107,7 +107,7 @@ try app.UseSwagger(); app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyService API v1"); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "FnbEngine API v1"); c.RoutePrefix = "swagger"; }); } diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/KitchenAggregate/KitchenTicket.cs b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/KitchenAggregate/KitchenTicket.cs new file mode 100644 index 00000000..2db0528b --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/KitchenAggregate/KitchenTicket.cs @@ -0,0 +1,63 @@ +// EN: Kitchen ticket entity for kitchen display. +// VI: Entity KitchenTicket cho hiển thị bếp. + +using FnbEngine.Domain.SeedWork; + +namespace FnbEngine.Domain.AggregatesModel.KitchenAggregate; + +/// +/// EN: Kitchen ticket entity - represents an order item in the kitchen. +/// VI: Entity KitchenTicket - đại diện cho item đơn hàng trong bếp. +/// +public class KitchenTicket : Entity, IAggregateRoot +{ + private Guid _sessionId; + private Guid _orderItemId; + private string _itemName = null!; + private string? _station; // Bar, Kitchen, Grill, etc. + private int _priority; + private string _status = null!; // Pending, InProgress, Ready, Served + private DateTime _createdAt; + private DateTime? _completedAt; + + public Guid SessionId => _sessionId; + public Guid OrderItemId => _orderItemId; + public string ItemName => _itemName; + public string? Station => _station; + public int Priority => _priority; + public string Status => _status; + public DateTime CreatedAt => _createdAt; + public DateTime? CompletedAt => _completedAt; + + protected KitchenTicket() + { + } + + public KitchenTicket(Guid sessionId, Guid orderItemId, string itemName, string? station = null, int priority = 0) + { + Id = Guid.NewGuid(); + _sessionId = sessionId; + _orderItemId = orderItemId; + _itemName = itemName; + _station = station; + _priority = priority; + _status = "Pending"; + _createdAt = DateTime.UtcNow; + } + + public void MarkAsInProgress() + { + _status = "InProgress"; + } + + public void MarkAsReady() + { + _status = "Ready"; + _completedAt = DateTime.UtcNow; + } + + public void MarkAsServed() + { + _status = "Served"; + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs index 40bc8c3a..10e7d359 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using FnbEngine.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace FnbEngine.Domain.AggregatesModel.SampleAggregate; /// /// EN: Repository interface for Sample aggregate. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/Sample.cs index 641bb385..bbca3285 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/Sample.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/Sample.cs @@ -1,8 +1,8 @@ -using MyService.Domain.Events; -using MyService.Domain.Exceptions; -using MyService.Domain.SeedWork; +using FnbEngine.Domain.Events; +using FnbEngine.Domain.Exceptions; +using FnbEngine.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace FnbEngine.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample aggregate root demonstrating DDD patterns. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs index 54ce63ba..fcde41cf 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using FnbEngine.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace FnbEngine.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample status enumeration following type-safe enum pattern. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SessionAggregate/Session.cs b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SessionAggregate/Session.cs new file mode 100644 index 00000000..a875d557 --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/SessionAggregate/Session.cs @@ -0,0 +1,58 @@ +// EN: Session entity for dining sessions. +// VI: Entity Session cho phiên ăn uống. + +using FnbEngine.Domain.Exceptions; +using FnbEngine.Domain.SeedWork; + +namespace FnbEngine.Domain.AggregatesModel.SessionAggregate; + +/// +/// EN: Session entity - represents a dining session at a table. +/// VI: Entity Session - đại diện cho phiên ăn tại bàn. +/// +public class Session : Entity, IAggregateRoot +{ + private Guid _tableId; + private Guid _shopId; + private int _guestCount; + private DateTime _startedAt; + private DateTime? _closedAt; + private string _status = null!; // Active, Closed + + public Guid TableId => _tableId; + public Guid ShopId => _shopId; + public int GuestCount => _guestCount; + public DateTime StartedAt => _startedAt; + public DateTime? ClosedAt => _closedAt; + public string Status => _status; + + protected Session() + { + } + + public Session(Guid tableId, Guid shopId, int guestCount = 1) + { + if (tableId == Guid.Empty) + throw new DomainException("Table ID cannot be empty"); + if (shopId == Guid.Empty) + throw new DomainException("Shop ID cannot be empty"); + if (guestCount <= 0) + throw new DomainException("Guest count must be greater than zero"); + + Id = Guid.NewGuid(); + _tableId = tableId; + _shopId = shopId; + _guestCount = guestCount; + _startedAt = DateTime.UtcNow; + _status = "Active"; + } + + public void Close() + { + if (_status == "Closed") + throw new DomainException("Session is already closed"); + + _closedAt = DateTime.UtcNow; + _status = "Closed"; + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/Table.cs b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/Table.cs new file mode 100644 index 00000000..8a413805 --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/Table.cs @@ -0,0 +1,78 @@ +// EN: Table aggregate root for FnB. +// VI: Aggregate root Table cho FnB. + +using FnbEngine.Domain.Exceptions; +using FnbEngine.Domain.SeedWork; + +namespace FnbEngine.Domain.AggregatesModel.TableAggregate; + +/// +/// EN: Table aggregate root - represents a dining table. +/// VI: Aggregate root Table - đại diện cho bàn ăn. +/// +public class Table : Entity, IAggregateRoot +{ + private Guid _shopId; + private string _tableNumber = null!; + private int _capacity; + private string? _zone; + private TableStatus _status = null!; + private DateTime _createdAt; + private DateTime? _updatedAt; + + public Guid ShopId => _shopId; + public string TableNumber => _tableNumber; + public int Capacity => _capacity; + public string? Zone => _zone; + public TableStatus Status => _status; + public int StatusId { get; private set; } + public DateTime CreatedAt => _createdAt; + public DateTime? UpdatedAt => _updatedAt; + + protected Table() + { + } + + public Table(Guid shopId, string tableNumber, int capacity, string? zone = null) + { + if (shopId == Guid.Empty) + throw new DomainException("Shop ID cannot be empty"); + if (string.IsNullOrWhiteSpace(tableNumber)) + throw new DomainException("Table number cannot be empty"); + if (capacity <= 0) + throw new DomainException("Capacity must be greater than zero"); + + Id = Guid.NewGuid(); + _shopId = shopId; + _tableNumber = tableNumber.Trim(); + _capacity = capacity; + _zone = zone?.Trim(); + _status = TableStatus.Available; + StatusId = TableStatus.Available.Id; + _createdAt = DateTime.UtcNow; + } + + public void MarkAsOccupied() + { + if (_status != TableStatus.Available && _status != TableStatus.Reserved) + throw new DomainException($"Cannot occupy table with status {_status.Name}"); + + _status = TableStatus.Occupied; + StatusId = TableStatus.Occupied.Id; + _updatedAt = DateTime.UtcNow; + } + + public void MarkAsAvailable() + { + _status = TableStatus.Available; + StatusId = TableStatus.Available.Id; + _updatedAt = DateTime.UtcNow; + } + + public void MarkAsCleaning() + { + _status = TableStatus.Cleaning; + StatusId = TableStatus.Cleaning.Id; + _updatedAt = DateTime.UtcNow; + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/TableStatus.cs b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/TableStatus.cs new file mode 100644 index 00000000..1c5921ff --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/TableStatus.cs @@ -0,0 +1,22 @@ +// EN: Table status enumeration for FnB. +// VI: Enumeration trạng thái bàn cho FnB. + +using FnbEngine.Domain.SeedWork; + +namespace FnbEngine.Domain.AggregatesModel.TableAggregate; + +/// +/// EN: Table status enumeration. +/// VI: Enumeration trạng thái bàn. +/// +public class TableStatus : Enumeration +{ + public static readonly TableStatus Available = new(1, nameof(Available)); + public static readonly TableStatus Occupied = new(2, nameof(Occupied)); + public static readonly TableStatus Reserved = new(3, nameof(Reserved)); + public static readonly TableStatus Cleaning = new(4, nameof(Cleaning)); + + public TableStatus(int id, string name) : base(id, name) + { + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/Events/SampleCreatedDomainEvent.cs b/services/fnb-engine-net/src/FnbEngine.Domain/Events/SampleCreatedDomainEvent.cs index 7e838214..6502fafb 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/Events/SampleCreatedDomainEvent.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/Events/SampleCreatedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace FnbEngine.Domain.Events; /// /// EN: Domain event raised when a new Sample is created. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/fnb-engine-net/src/FnbEngine.Domain/Events/SampleStatusChangedDomainEvent.cs index f6d9b422..c84ea1b2 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/Events/SampleStatusChangedDomainEvent.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/Events/SampleStatusChangedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace FnbEngine.Domain.Events; /// /// EN: Domain event raised when Sample status changes. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/Exceptions/DomainException.cs b/services/fnb-engine-net/src/FnbEngine.Domain/Exceptions/DomainException.cs index 7e737f64..63266dee 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/Exceptions/DomainException.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/Exceptions/DomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace FnbEngine.Domain.Exceptions; /// /// EN: Base exception for domain errors. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/Exceptions/SampleDomainException.cs b/services/fnb-engine-net/src/FnbEngine.Domain/Exceptions/SampleDomainException.cs index c850944c..b028afb7 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/Exceptions/SampleDomainException.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/Exceptions/SampleDomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace FnbEngine.Domain.Exceptions; /// /// EN: Exception for Sample aggregate domain errors. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/FnbEngine.Domain.csproj b/services/fnb-engine-net/src/FnbEngine.Domain/FnbEngine.Domain.csproj index 3208317a..6ae7c932 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/FnbEngine.Domain.csproj +++ b/services/fnb-engine-net/src/FnbEngine.Domain/FnbEngine.Domain.csproj @@ -1,8 +1,8 @@ - MyService.Domain - MyService.Domain + FnbEngine.Domain + FnbEngine.Domain Domain layer containing core business logic and entities diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/Entity.cs b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/Entity.cs index b07fdd3b..d5a53277 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/Entity.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/Entity.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.Domain.SeedWork; +namespace FnbEngine.Domain.SeedWork; /// /// EN: Base class for all domain entities. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/Enumeration.cs b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/Enumeration.cs index 6641979c..0459dd35 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/Enumeration.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/Enumeration.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace MyService.Domain.SeedWork; +namespace FnbEngine.Domain.SeedWork; /// /// EN: Base class for enumeration classes (type-safe enum pattern). diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IAggregateRoot.cs b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IAggregateRoot.cs index d361394f..880b353c 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IAggregateRoot.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IAggregateRoot.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace FnbEngine.Domain.SeedWork; /// /// EN: Marker interface for aggregate roots. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IRepository.cs b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IRepository.cs index 2d539e44..9775a365 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IRepository.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IRepository.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace FnbEngine.Domain.SeedWork; /// /// EN: Generic repository interface for aggregate roots. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IUnitOfWork.cs b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IUnitOfWork.cs index d37d8fa4..e16b25ae 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IUnitOfWork.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/IUnitOfWork.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace FnbEngine.Domain.SeedWork; /// /// EN: Unit of Work pattern interface. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/ValueObject.cs b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/ValueObject.cs index 5cf4188f..96b8ec6d 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/ValueObject.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/SeedWork/ValueObject.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace FnbEngine.Domain.SeedWork; /// /// EN: Base class for Value Objects following DDD patterns. diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/DependencyInjection.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/DependencyInjection.cs index a8dfba0d..0758eed6 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/DependencyInjection.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/DependencyInjection.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Infrastructure.Idempotency; -using MyService.Infrastructure.Repositories; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Infrastructure.Idempotency; +using FnbEngine.Infrastructure.Repositories; -namespace MyService.Infrastructure; +namespace FnbEngine.Infrastructure; /// /// EN: Dependency injection extensions for Infrastructure layer. @@ -22,7 +22,7 @@ public static class DependencyInjection IConfiguration configuration) { // EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL - services.AddDbContext(options => + services.AddDbContext(options => { var connectionString = configuration.GetConnectionString("DefaultConnection") ?? configuration["DATABASE_URL"] @@ -30,7 +30,7 @@ public static class DependencyInjection options.UseNpgsql(connectionString, npgsqlOptions => { - npgsqlOptions.MigrationsAssembly(typeof(MyServiceContext).Assembly.FullName); + npgsqlOptions.MigrationsAssembly(typeof(FnbEngineContext).Assembly.FullName); npgsqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs index 5c2fbd42..135425cd 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace FnbEngine.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for Sample entity. diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs index 8b683f56..73bdd15d 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace FnbEngine.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for SampleStatus enumeration. diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/FnbEngine.Infrastructure.csproj b/services/fnb-engine-net/src/FnbEngine.Infrastructure/FnbEngine.Infrastructure.csproj index 7c81b5e9..c4ab35de 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/FnbEngine.Infrastructure.csproj +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/FnbEngine.Infrastructure.csproj @@ -1,8 +1,8 @@ - MyService.Infrastructure - MyService.Infrastructure + FnbEngine.Infrastructure + FnbEngine.Infrastructure Infrastructure layer for data access and external services @@ -30,7 +30,7 @@ - + diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/ClientRequest.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/ClientRequest.cs index f65e4a56..e2453d4b 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/ClientRequest.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/ClientRequest.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace FnbEngine.Infrastructure.Idempotency; /// /// EN: Entity for tracking client requests to ensure idempotency. diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/IRequestManager.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/IRequestManager.cs index 92097399..ab5cfec0 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/IRequestManager.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/IRequestManager.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace FnbEngine.Infrastructure.Idempotency; /// /// EN: Interface for managing client request idempotency. diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/RequestManager.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/RequestManager.cs index 41a5f318..7fcf140e 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/RequestManager.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/Idempotency/RequestManager.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace MyService.Infrastructure.Idempotency; +namespace FnbEngine.Infrastructure.Idempotency; /// /// EN: Implementation of request manager for idempotency. @@ -8,9 +8,9 @@ namespace MyService.Infrastructure.Idempotency; /// public class RequestManager : IRequestManager { - private readonly MyServiceContext _context; + private readonly FnbEngineContext _context; - public RequestManager(MyServiceContext context) + public RequestManager(FnbEngineContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/MyServiceContext.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/MyServiceContext.cs index 4486367d..c130434f 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/MyServiceContext.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/MyServiceContext.cs @@ -1,17 +1,17 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; -using MyService.Infrastructure.EntityConfigurations; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.SeedWork; +using FnbEngine.Infrastructure.EntityConfigurations; -namespace MyService.Infrastructure; +namespace FnbEngine.Infrastructure; /// -/// EN: EF Core DbContext for MyService. -/// VI: EF Core DbContext cho MyService. +/// EN: EF Core DbContext for FnbEngine. +/// VI: EF Core DbContext cho FnbEngine. /// -public class MyServiceContext : DbContext, IUnitOfWork +public class FnbEngineContext : DbContext, IUnitOfWork { private readonly IMediator _mediator; private IDbContextTransaction? _currentTransaction; @@ -34,16 +34,16 @@ public class MyServiceContext : DbContext, IUnitOfWork /// public bool HasActiveTransaction => _currentTransaction != null; - public MyServiceContext(DbContextOptions options) : base(options) + public FnbEngineContext(DbContextOptions options) : base(options) { _mediator = null!; } - public MyServiceContext(DbContextOptions options, IMediator mediator) : base(options) + public FnbEngineContext(DbContextOptions options, IMediator mediator) : base(options) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); - System.Diagnostics.Debug.WriteLine("MyServiceContext::ctor - " + GetHashCode()); + System.Diagnostics.Debug.WriteLine("FnbEngineContext::ctor - " + GetHashCode()); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/Repositories/SampleRepository.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/Repositories/SampleRepository.cs index f0a4b109..57b09f76 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/Repositories/SampleRepository.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/Repositories/SampleRepository.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.SeedWork; -namespace MyService.Infrastructure.Repositories; +namespace FnbEngine.Infrastructure.Repositories; /// /// EN: Repository implementation for Sample aggregate. @@ -10,7 +10,7 @@ namespace MyService.Infrastructure.Repositories; /// public class SampleRepository : ISampleRepository { - private readonly MyServiceContext _context; + private readonly FnbEngineContext _context; /// /// EN: Unit of work for transaction management. @@ -18,7 +18,7 @@ public class SampleRepository : ISampleRepository /// public IUnitOfWork UnitOfWork => _context; - public SampleRepository(MyServiceContext context) + public SampleRepository(FnbEngineContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/Controllers/SamplesControllerTests.cs index e6e99ac5..cee049f7 100644 --- a/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/Controllers/SamplesControllerTests.cs +++ b/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/Controllers/SamplesControllerTests.cs @@ -4,7 +4,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; -namespace MyService.FunctionalTests.Controllers; +namespace FnbEngine.FunctionalTests.Controllers; /// /// EN: Functional tests for Samples API endpoints. diff --git a/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/CustomWebApplicationFactory.cs b/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/CustomWebApplicationFactory.cs index d74d8338..b5337473 100644 --- a/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/CustomWebApplicationFactory.cs +++ b/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/CustomWebApplicationFactory.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using MyService.Infrastructure; +using FnbEngine.Infrastructure; -namespace MyService.FunctionalTests; +namespace FnbEngine.FunctionalTests; /// /// EN: Custom WebApplicationFactory for functional tests. @@ -21,7 +21,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove the existing DbContext registration // VI: Xóa đăng ký DbContext hiện tại var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); + d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) { @@ -31,7 +31,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove DbContext service // VI: Xóa DbContext service var dbContextDescriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(MyServiceContext)); + d => d.ServiceType == typeof(FnbEngineContext)); if (dbContextDescriptor != null) { @@ -40,7 +40,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Add in-memory database for testing // VI: Thêm in-memory database để test - services.AddDbContext(options => + services.AddDbContext(options => { options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString()); }); @@ -49,7 +49,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // VI: Đảm bảo database được tạo với seed data var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); }); } diff --git a/services/booking-service-net/tests/BookingService.FunctionalTests/MyService.FunctionalTests.csproj b/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/FnbEngine.FunctionalTests.csproj similarity index 88% rename from services/booking-service-net/tests/BookingService.FunctionalTests/MyService.FunctionalTests.csproj rename to services/fnb-engine-net/tests/FnbEngine.FunctionalTests/FnbEngine.FunctionalTests.csproj index 4cc894f8..ab3958f0 100644 --- a/services/booking-service-net/tests/BookingService.FunctionalTests/MyService.FunctionalTests.csproj +++ b/services/fnb-engine-net/tests/FnbEngine.FunctionalTests/FnbEngine.FunctionalTests.csproj @@ -1,8 +1,8 @@ - MyService.FunctionalTests - MyService.FunctionalTests + FnbEngine.FunctionalTests + FnbEngine.FunctionalTests false true @@ -32,7 +32,7 @@ - + diff --git a/services/fnb-engine-net/tests/FnbEngine.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/fnb-engine-net/tests/FnbEngine.UnitTests/Application/CreateSampleCommandHandlerTests.cs index 75cdd0e8..78373e3a 100644 --- a/services/fnb-engine-net/tests/FnbEngine.UnitTests/Application/CreateSampleCommandHandlerTests.cs +++ b/services/fnb-engine-net/tests/FnbEngine.UnitTests/Application/CreateSampleCommandHandlerTests.cs @@ -1,12 +1,12 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using MyService.API.Application.Commands; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using FnbEngine.API.Application.Commands; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.SeedWork; using Xunit; -namespace MyService.UnitTests.Application; +namespace FnbEngine.UnitTests.Application; /// /// EN: Unit tests for CreateSampleCommandHandler. diff --git a/services/fnb-engine-net/tests/FnbEngine.UnitTests/Domain/SampleAggregateTests.cs b/services/fnb-engine-net/tests/FnbEngine.UnitTests/Domain/SampleAggregateTests.cs index dcf48767..4860add7 100644 --- a/services/fnb-engine-net/tests/FnbEngine.UnitTests/Domain/SampleAggregateTests.cs +++ b/services/fnb-engine-net/tests/FnbEngine.UnitTests/Domain/SampleAggregateTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.Exceptions; +using FnbEngine.Domain.AggregatesModel.SampleAggregate; +using FnbEngine.Domain.Exceptions; using Xunit; -namespace MyService.UnitTests.Domain; +namespace FnbEngine.UnitTests.Domain; /// /// EN: Unit tests for Sample aggregate. diff --git a/services/fnb-engine-net/tests/FnbEngine.UnitTests/MyService.UnitTests.csproj b/services/fnb-engine-net/tests/FnbEngine.UnitTests/FnbEngine.UnitTests.csproj similarity index 81% rename from services/fnb-engine-net/tests/FnbEngine.UnitTests/MyService.UnitTests.csproj rename to services/fnb-engine-net/tests/FnbEngine.UnitTests/FnbEngine.UnitTests.csproj index b40272dc..8b1a6d2b 100644 --- a/services/fnb-engine-net/tests/FnbEngine.UnitTests/MyService.UnitTests.csproj +++ b/services/fnb-engine-net/tests/FnbEngine.UnitTests/FnbEngine.UnitTests.csproj @@ -1,8 +1,8 @@ - MyService.UnitTests - MyService.UnitTests + FnbEngine.UnitTests + FnbEngine.UnitTests false true @@ -28,8 +28,8 @@ - - + + diff --git a/services/inventory-service-net/docker-compose.yml b/services/inventory-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/inventory-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/LoggingBehavior.cs b/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/LoggingBehavior.cs index a724424d..ec0c1459 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/LoggingBehavior.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/LoggingBehavior.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace InventoryService.API.Application.Behaviors; /// /// EN: MediatR behavior for logging request handling. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/TransactionBehavior.cs b/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/TransactionBehavior.cs index 8675b649..3608dbf8 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/TransactionBehavior.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/TransactionBehavior.cs @@ -1,8 +1,8 @@ using MediatR; using Microsoft.EntityFrameworkCore; -using MyService.Infrastructure; +using InventoryService.Infrastructure; -namespace MyService.API.Application.Behaviors; +namespace InventoryService.API.Application.Behaviors; /// /// EN: MediatR behavior for handling database transactions. @@ -13,11 +13,11 @@ namespace MyService.API.Application.Behaviors; public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { - private readonly MyServiceContext _dbContext; + private readonly InventoryServiceContext _dbContext; private readonly ILogger> _logger; public TransactionBehavior( - MyServiceContext dbContext, + InventoryServiceContext dbContext, ILogger> logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/ValidatorBehavior.cs b/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/ValidatorBehavior.cs index 0062cd60..e24dd08d 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/ValidatorBehavior.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/ValidatorBehavior.cs @@ -1,7 +1,7 @@ using FluentValidation; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace InventoryService.API.Application.Behaviors; /// /// EN: MediatR behavior for FluentValidation integration. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/inventory-service-net/src/InventoryService.API/Application/Commands/ChangeSampleStatusCommand.cs index 49825490..1a470343 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Commands/ChangeSampleStatusCommand.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Commands/ChangeSampleStatusCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace InventoryService.API.Application.Commands; /// /// EN: Command to change status of a Sample. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/inventory-service-net/src/InventoryService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs index 76e31030..59ee0a01 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace InventoryService.API.Application.Commands; /// /// EN: Handler for ChangeSampleStatusCommand. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Commands/CreateSampleCommand.cs b/services/inventory-service-net/src/InventoryService.API/Application/Commands/CreateSampleCommand.cs index 138cc794..4a1f38be 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Commands/CreateSampleCommand.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Commands/CreateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace InventoryService.API.Application.Commands; /// /// EN: Command to create a new Sample. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/inventory-service-net/src/InventoryService.API/Application/Commands/CreateSampleCommandHandler.cs index d7d0fd7c..6913e7db 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Commands/CreateSampleCommandHandler.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Commands/CreateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace InventoryService.API.Application.Commands; /// /// EN: Handler for CreateSampleCommand. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Commands/DeleteSampleCommand.cs b/services/inventory-service-net/src/InventoryService.API/Application/Commands/DeleteSampleCommand.cs index 0de392db..4a052be9 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Commands/DeleteSampleCommand.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Commands/DeleteSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace InventoryService.API.Application.Commands; /// /// EN: Command to delete a Sample. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/inventory-service-net/src/InventoryService.API/Application/Commands/DeleteSampleCommandHandler.cs index c7632189..2e3ffcb1 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Commands/DeleteSampleCommandHandler.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Commands/DeleteSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace InventoryService.API.Application.Commands; /// /// EN: Handler for DeleteSampleCommand. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Commands/UpdateSampleCommand.cs b/services/inventory-service-net/src/InventoryService.API/Application/Commands/UpdateSampleCommand.cs index 6fad8514..845f6ce8 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Commands/UpdateSampleCommand.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Commands/UpdateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace InventoryService.API.Application.Commands; /// /// EN: Command to update an existing Sample. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/inventory-service-net/src/InventoryService.API/Application/Commands/UpdateSampleCommandHandler.cs index e904cf0a..b608a4a1 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Commands/UpdateSampleCommandHandler.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Commands/UpdateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace InventoryService.API.Application.Commands; /// /// EN: Handler for UpdateSampleCommand. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSampleQuery.cs b/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSampleQuery.cs index 8b90789c..4b7bff5e 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSampleQuery.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSampleQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace InventoryService.API.Application.Queries; /// /// EN: Query to get a Sample by ID. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSampleQueryHandler.cs b/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSampleQueryHandler.cs index 2da10b6d..fede4af4 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSampleQueryHandler.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSampleQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace InventoryService.API.Application.Queries; /// /// EN: Handler for GetSampleQuery. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSamplesQuery.cs b/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSamplesQuery.cs index d6a98e34..957760c5 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSamplesQuery.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSamplesQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace InventoryService.API.Application.Queries; /// /// EN: Query to get all Samples. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSamplesQueryHandler.cs index 2185302d..89a422d7 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSamplesQueryHandler.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Queries/GetSamplesQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace InventoryService.API.Application.Queries; /// /// EN: Handler for GetSamplesQuery. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/inventory-service-net/src/InventoryService.API/Application/Validations/CreateSampleCommandValidator.cs index 2f339fb3..ad240de4 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Validations/CreateSampleCommandValidator.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Validations/CreateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using InventoryService.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace InventoryService.API.Application.Validations; /// /// EN: Validator for CreateSampleCommand. diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/inventory-service-net/src/InventoryService.API/Application/Validations/UpdateSampleCommandValidator.cs index 7030d5c8..7abb282d 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Validations/UpdateSampleCommandValidator.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Validations/UpdateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using InventoryService.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace InventoryService.API.Application.Validations; /// /// EN: Validator for UpdateSampleCommand. diff --git a/services/inventory-service-net/src/InventoryService.API/Controllers/SamplesController.cs b/services/inventory-service-net/src/InventoryService.API/Controllers/SamplesController.cs index c87e0ffa..b6390550 100644 --- a/services/inventory-service-net/src/InventoryService.API/Controllers/SamplesController.cs +++ b/services/inventory-service-net/src/InventoryService.API/Controllers/SamplesController.cs @@ -1,10 +1,10 @@ using Asp.Versioning; using MediatR; using Microsoft.AspNetCore.Mvc; -using MyService.API.Application.Commands; -using MyService.API.Application.Queries; +using InventoryService.API.Application.Commands; +using InventoryService.API.Application.Queries; -namespace MyService.API.Controllers; +namespace InventoryService.API.Controllers; /// /// EN: Controller for Sample CRUD operations using CQRS pattern. diff --git a/services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj b/services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj index 1b5bb222..c6a8247a 100644 --- a/services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj +++ b/services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj @@ -1,8 +1,8 @@ - MyService.API - MyService.API + InventoryService.API + InventoryService.API Web API layer with CQRS pattern myservice-api @@ -36,8 +36,8 @@ - - + + diff --git a/services/inventory-service-net/src/InventoryService.API/Program.cs b/services/inventory-service-net/src/InventoryService.API/Program.cs index bd9b3df4..f67ab510 100644 --- a/services/inventory-service-net/src/InventoryService.API/Program.cs +++ b/services/inventory-service-net/src/InventoryService.API/Program.cs @@ -1,8 +1,8 @@ using Asp.Versioning; using FluentValidation; using Hellang.Middleware.ProblemDetails; -using MyService.API.Application.Behaviors; -using MyService.Infrastructure; +using InventoryService.API.Application.Behaviors; +using InventoryService.Infrastructure; using Serilog; // EN: Configure Serilog early / VI: Cấu hình Serilog sớm @@ -12,7 +12,7 @@ Log.Logger = new LoggerConfiguration() try { - Log.Information("Starting MyService API / Khởi động MyService API"); + Log.Information("Starting InventoryService API / Khởi động InventoryService API"); var builder = WebApplication.CreateBuilder(args); @@ -70,9 +70,9 @@ try { options.SwaggerDoc("v1", new() { - Title = "MyService API", + Title = "InventoryService API", Version = "v1", - Description = "MyService microservice API / API microservice MyService" + Description = "InventoryService microservice API / API microservice InventoryService" }); }); @@ -107,7 +107,7 @@ try app.UseSwagger(); app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyService API v1"); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "InventoryService API v1"); c.RoutePrefix = "swagger"; }); } diff --git a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/InventoryItem.cs b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/InventoryItem.cs new file mode 100644 index 00000000..70fd01c4 --- /dev/null +++ b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/InventoryItem.cs @@ -0,0 +1,197 @@ +// EN: Inventory item aggregate root. +// VI: Aggregate root InventoryItem. + +using InventoryService.Domain.Events; +using InventoryService.Domain.Exceptions; +using InventoryService.Domain.SeedWork; + +namespace InventoryService.Domain.AggregatesModel.InventoryAggregate; + +/// +/// EN: Inventory item aggregate root - manages stock levels. +/// VI: Aggregate root InventoryItem - quản lý mức tồn kho. +/// +public class InventoryItem : Entity, IAggregateRoot +{ + private Guid _productId; + private Guid _shopId; + private int _quantity; + private int _reservedQuantity; + private int _reorderLevel; + private DateTime _createdAt; + private DateTime? _updatedAt; + + private readonly List _transactions = new(); + + /// + /// EN: Product ID from Catalog Service. + /// VI: ID sản phẩm từ Catalog Service. + /// + public Guid ProductId => _productId; + + /// + /// EN: Shop ID that owns this inventory. + /// VI: ID shop sở hữu inventory này. + /// + public Guid ShopId => _shopId; + + /// + /// EN: Total quantity in stock. + /// VI: Tổng số lượng trong kho. + /// + public int Quantity => _quantity; + + /// + /// EN: Reserved quantity for orders. + /// VI: Số lượng đặt trước cho đơn hàng. + /// + public int ReservedQuantity => _reservedQuantity; + + /// + /// EN: Available quantity (total - reserved). + /// VI: Số lượng khả dụng (tổng - đặt trước). + /// + public int AvailableQuantity => _quantity - _reservedQuantity; + + /// + /// EN: Reorder level - alert when stock goes below this. + /// VI: Mức đặt hàng lại - cảnh báo khi tồn kho dưới mức này. + /// + public int ReorderLevel => _reorderLevel; + + /// + /// EN: Transaction history. + /// VI: Lịch sử giao dịch. + /// + public IReadOnlyCollection Transactions => _transactions.AsReadOnly(); + + /// + /// 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 InventoryItem() + { + } + + /// + /// EN: Create a new inventory item. + /// VI: Tạo inventory item mới. + /// + public InventoryItem(Guid productId, Guid shopId, int reorderLevel = 10) + { + if (productId == Guid.Empty) + throw new DomainException("Product ID cannot be empty"); + if (shopId == Guid.Empty) + throw new DomainException("Shop ID cannot be empty"); + + Id = Guid.NewGuid(); + _productId = productId; + _shopId = shopId; + _quantity = 0; + _reservedQuantity = 0; + _reorderLevel = reorderLevel; + _createdAt = DateTime.UtcNow; + } + + /// + /// EN: Stock in operation. + /// VI: Thao tác nhập kho. + /// + public void StockIn(int amount, string? notes = null, Guid? referenceId = null) + { + if (amount <= 0) + throw new DomainException("Stock in amount must be positive"); + + _quantity += amount; + _updatedAt = DateTime.UtcNow; + + var transaction = new InventoryTransaction(Id, TransactionType.In, amount, referenceId, notes); + _transactions.Add(transaction); + + AddDomainEvent(new StockChangedDomainEvent(this)); + } + + /// + /// EN: Stock out operation. + /// VI: Thao tác xuất kho. + /// + public void StockOut(int amount, string? notes = null, Guid? referenceId = null) + { + if (amount <= 0) + throw new DomainException("Stock out amount must be positive"); + if (AvailableQuantity < amount) + throw new DomainException($"Insufficient stock. Available: {AvailableQuantity}, Requested: {amount}"); + + _quantity -= amount; + _updatedAt = DateTime.UtcNow; + + var transaction = new InventoryTransaction(Id, TransactionType.Out, -amount, referenceId, notes); + _transactions.Add(transaction); + + AddDomainEvent(new StockChangedDomainEvent(this)); + } + + /// + /// EN: Reserve stock for order. + /// VI: Đặt trước tồn kho cho đơn hàng. + /// + public void Reserve(int amount, Guid orderId) + { + if (amount <= 0) + throw new DomainException("Reserve amount must be positive"); + if (AvailableQuantity < amount) + throw new DomainException($"Insufficient stock to reserve. Available: {AvailableQuantity}, Requested: {amount}"); + + _reservedQuantity += amount; + _updatedAt = DateTime.UtcNow; + + var transaction = new InventoryTransaction(Id, TransactionType.Reserve, amount, orderId); + _transactions.Add(transaction); + } + + /// + /// EN: Release reservation. + /// VI: Giải phóng đặt trước. + /// + public void ReleaseReservation(int amount, Guid orderId) + { + if (amount <= 0) + throw new DomainException("Release amount must be positive"); + if (_reservedQuantity < amount) + throw new DomainException("Cannot release more than reserved"); + + _reservedQuantity -= amount; + _updatedAt = DateTime.UtcNow; + + var transaction = new InventoryTransaction(Id, TransactionType.Release, -amount, orderId); + _transactions.Add(transaction); + } + + /// + /// EN: Manual adjustment (stocktake). + /// VI: Điều chỉnh thủ công (kiểm kê). + /// + public void Adjust(int newQuantity, string notes) + { + var diff = newQuantity - _quantity; + _quantity = newQuantity; + _updatedAt = DateTime.UtcNow; + + var transaction = new InventoryTransaction(Id, TransactionType.Adjustment, diff, null, notes); + _transactions.Add(transaction); + + AddDomainEvent(new StockChangedDomainEvent(this)); + } +} diff --git a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/InventoryTransaction.cs b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/InventoryTransaction.cs new file mode 100644 index 00000000..40263067 --- /dev/null +++ b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/InventoryTransaction.cs @@ -0,0 +1,91 @@ +// EN: Inventory transaction entity. +// VI: Entity giao dịch tồn kho. + +using InventoryService.Domain.SeedWork; + +namespace InventoryService.Domain.AggregatesModel.InventoryAggregate; + +/// +/// EN: Inventory transaction entity - audit trail for inventory movements. +/// VI: Entity giao dịch tồn kho - lịch sử di chuyển tồn kho. +/// +public class InventoryTransaction : Entity +{ + private Guid _inventoryItemId; + private TransactionType _type = null!; + private int _quantity; + private Guid? _referenceId; // Order ID, PO ID, etc. + private string? _notes; + private DateTime _createdAt; + + /// + /// EN: Inventory item ID. + /// VI: ID inventory item. + /// + public Guid InventoryItemId => _inventoryItemId; + + /// + /// EN: Transaction type. + /// VI: Loại giao dịch. + /// + public TransactionType Type => _type; + + /// + /// EN: Type ID for EF Core mapping. + /// VI: Type ID cho EF Core mapping. + /// + public int TypeId { get; private set; } + + /// + /// EN: Quantity (positive or negative). + /// VI: Số lượng (dương hoặc âm). + /// + public int Quantity => _quantity; + + /// + /// EN: Reference ID (e.g., Order ID). + /// VI: ID tham chiếu (ví dụ: Order ID). + /// + public Guid? ReferenceId => _referenceId; + + /// + /// EN: Notes for this transaction. + /// VI: Ghi chú cho giao dịch này. + /// + public string? Notes => _notes; + + /// + /// EN: Creation timestamp. + /// VI: Thời gian tạo. + /// + public DateTime CreatedAt => _createdAt; + + /// + /// EN: Private constructor for EF Core. + /// VI: Constructor private cho EF Core. + /// + protected InventoryTransaction() + { + } + + /// + /// EN: Create a new inventory transaction. + /// VI: Tạo giao dịch tồn kho mới. + /// + public InventoryTransaction( + Guid inventoryItemId, + TransactionType type, + int quantity, + Guid? referenceId = null, + string? notes = null) + { + Id = Guid.NewGuid(); + _inventoryItemId = inventoryItemId; + _type = type; + TypeId = type.Id; + _quantity = quantity; + _referenceId = referenceId; + _notes = notes; + _createdAt = DateTime.UtcNow; + } +} diff --git a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/TransactionType.cs b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/TransactionType.cs new file mode 100644 index 00000000..fc4005d1 --- /dev/null +++ b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/InventoryAggregate/TransactionType.cs @@ -0,0 +1,47 @@ +// EN: Transaction type enumeration for inventory operations. +// VI: Enumeration loại giao dịch cho thao tác tồn kho. + +using InventoryService.Domain.SeedWork; + +namespace InventoryService.Domain.AggregatesModel.InventoryAggregate; + +/// +/// EN: Transaction type enumeration for inventory movements. +/// VI: Enumeration loại giao dịch cho di chuyển tồn kho. +/// +public class TransactionType : Enumeration +{ + /// + /// EN: Stock in - receiving inventory. + /// VI: Nhập kho - nhận hàng vào kho. + /// + public static readonly TransactionType In = new(1, nameof(In)); + + /// + /// EN: Stock out - releasing inventory. + /// VI: Xuất kho - xuất hàng ra khỏi kho. + /// + public static readonly TransactionType Out = new(2, nameof(Out)); + + /// + /// EN: Manual adjustment - stocktake corrections. + /// VI: Điều chỉnh thủ công - hiệu chỉnh kiểm kê. + /// + public static readonly TransactionType Adjustment = new(3, nameof(Adjustment)); + + /// + /// EN: Reserve for order. + /// VI: Đặt trước cho đơn hàng. + /// + public static readonly TransactionType Reserve = new(4, nameof(Reserve)); + + /// + /// EN: Release reservation. + /// VI: Giải phóng đặt trước. + /// + public static readonly TransactionType Release = new(5, nameof(Release)); + + public TransactionType(int id, string name) : base(id, name) + { + } +} diff --git a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs index 40bc8c3a..15e08ad5 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using InventoryService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace InventoryService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Repository interface for Sample aggregate. diff --git a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/Sample.cs index 641bb385..d31a53f8 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/Sample.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/Sample.cs @@ -1,8 +1,8 @@ -using MyService.Domain.Events; -using MyService.Domain.Exceptions; -using MyService.Domain.SeedWork; +using InventoryService.Domain.Events; +using InventoryService.Domain.Exceptions; +using InventoryService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace InventoryService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample aggregate root demonstrating DDD patterns. diff --git a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs index 54ce63ba..6732738f 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using InventoryService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace InventoryService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample status enumeration following type-safe enum pattern. diff --git a/services/inventory-service-net/src/InventoryService.Domain/Events/InventoryDomainEvents.cs b/services/inventory-service-net/src/InventoryService.Domain/Events/InventoryDomainEvents.cs new file mode 100644 index 00000000..1f7f1489 --- /dev/null +++ b/services/inventory-service-net/src/InventoryService.Domain/Events/InventoryDomainEvents.cs @@ -0,0 +1,13 @@ +// EN: Inventory domain events. +// VI: Domain events của Inventory. + +using InventoryService.Domain.AggregatesModel.InventoryAggregate; +using MediatR; + +namespace InventoryService.Domain.Events; + +/// +/// EN: Domain event raised when stock level changes. +/// VI: Domain event phát ra khi mức tồn kho thay đổi. +/// +public record StockChangedDomainEvent(InventoryItem Item) : INotification; diff --git a/services/inventory-service-net/src/InventoryService.Domain/Events/SampleCreatedDomainEvent.cs b/services/inventory-service-net/src/InventoryService.Domain/Events/SampleCreatedDomainEvent.cs index 7e838214..8719ba61 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/Events/SampleCreatedDomainEvent.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/Events/SampleCreatedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace InventoryService.Domain.Events; /// /// EN: Domain event raised when a new Sample is created. diff --git a/services/inventory-service-net/src/InventoryService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/inventory-service-net/src/InventoryService.Domain/Events/SampleStatusChangedDomainEvent.cs index f6d9b422..c647f968 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/Events/SampleStatusChangedDomainEvent.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/Events/SampleStatusChangedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace InventoryService.Domain.Events; /// /// EN: Domain event raised when Sample status changes. diff --git a/services/inventory-service-net/src/InventoryService.Domain/Exceptions/DomainException.cs b/services/inventory-service-net/src/InventoryService.Domain/Exceptions/DomainException.cs index 7e737f64..933a3f9e 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/Exceptions/DomainException.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/Exceptions/DomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace InventoryService.Domain.Exceptions; /// /// EN: Base exception for domain errors. diff --git a/services/inventory-service-net/src/InventoryService.Domain/Exceptions/SampleDomainException.cs b/services/inventory-service-net/src/InventoryService.Domain/Exceptions/SampleDomainException.cs index c850944c..d5185c24 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/Exceptions/SampleDomainException.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/Exceptions/SampleDomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace InventoryService.Domain.Exceptions; /// /// EN: Exception for Sample aggregate domain errors. diff --git a/services/inventory-service-net/src/InventoryService.Domain/InventoryService.Domain.csproj b/services/inventory-service-net/src/InventoryService.Domain/InventoryService.Domain.csproj index 3208317a..a305f62f 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/InventoryService.Domain.csproj +++ b/services/inventory-service-net/src/InventoryService.Domain/InventoryService.Domain.csproj @@ -1,8 +1,8 @@ - MyService.Domain - MyService.Domain + InventoryService.Domain + InventoryService.Domain Domain layer containing core business logic and entities diff --git a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/Entity.cs b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/Entity.cs index b07fdd3b..8d0812f3 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/Entity.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/Entity.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.Domain.SeedWork; +namespace InventoryService.Domain.SeedWork; /// /// EN: Base class for all domain entities. diff --git a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/Enumeration.cs b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/Enumeration.cs index 6641979c..440aae24 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/Enumeration.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/Enumeration.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace MyService.Domain.SeedWork; +namespace InventoryService.Domain.SeedWork; /// /// EN: Base class for enumeration classes (type-safe enum pattern). diff --git a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IAggregateRoot.cs b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IAggregateRoot.cs index d361394f..c64e5b21 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IAggregateRoot.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IAggregateRoot.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace InventoryService.Domain.SeedWork; /// /// EN: Marker interface for aggregate roots. diff --git a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IRepository.cs b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IRepository.cs index 2d539e44..b210f57b 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IRepository.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IRepository.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace InventoryService.Domain.SeedWork; /// /// EN: Generic repository interface for aggregate roots. diff --git a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IUnitOfWork.cs b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IUnitOfWork.cs index d37d8fa4..3588c0b0 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IUnitOfWork.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/IUnitOfWork.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace InventoryService.Domain.SeedWork; /// /// EN: Unit of Work pattern interface. diff --git a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/ValueObject.cs b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/ValueObject.cs index 5cf4188f..4f9d0874 100644 --- a/services/inventory-service-net/src/InventoryService.Domain/SeedWork/ValueObject.cs +++ b/services/inventory-service-net/src/InventoryService.Domain/SeedWork/ValueObject.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace InventoryService.Domain.SeedWork; /// /// EN: Base class for Value Objects following DDD patterns. diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/DependencyInjection.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/DependencyInjection.cs index a8dfba0d..94d2b5f8 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/DependencyInjection.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/DependencyInjection.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Infrastructure.Idempotency; -using MyService.Infrastructure.Repositories; +using InventoryService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Infrastructure.Idempotency; +using InventoryService.Infrastructure.Repositories; -namespace MyService.Infrastructure; +namespace InventoryService.Infrastructure; /// /// EN: Dependency injection extensions for Infrastructure layer. @@ -22,7 +22,7 @@ public static class DependencyInjection IConfiguration configuration) { // EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL - services.AddDbContext(options => + services.AddDbContext(options => { var connectionString = configuration.GetConnectionString("DefaultConnection") ?? configuration["DATABASE_URL"] @@ -30,7 +30,7 @@ public static class DependencyInjection options.UseNpgsql(connectionString, npgsqlOptions => { - npgsqlOptions.MigrationsAssembly(typeof(MyServiceContext).Assembly.FullName); + npgsqlOptions.MigrationsAssembly(typeof(InventoryServiceContext).Assembly.FullName); npgsqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs index 5c2fbd42..b95ebf32 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace InventoryService.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for Sample entity. diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs index 8b683f56..c3a632b2 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace InventoryService.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for SampleStatus enumeration. diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/ClientRequest.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/ClientRequest.cs index f65e4a56..b9345ebb 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/ClientRequest.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/ClientRequest.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace InventoryService.Infrastructure.Idempotency; /// /// EN: Entity for tracking client requests to ensure idempotency. diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/IRequestManager.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/IRequestManager.cs index 92097399..73f41ff2 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/IRequestManager.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/IRequestManager.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace InventoryService.Infrastructure.Idempotency; /// /// EN: Interface for managing client request idempotency. diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/RequestManager.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/RequestManager.cs index 41a5f318..fb60d4f0 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/RequestManager.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/RequestManager.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace MyService.Infrastructure.Idempotency; +namespace InventoryService.Infrastructure.Idempotency; /// /// EN: Implementation of request manager for idempotency. @@ -8,9 +8,9 @@ namespace MyService.Infrastructure.Idempotency; /// public class RequestManager : IRequestManager { - private readonly MyServiceContext _context; + private readonly InventoryServiceContext _context; - public RequestManager(MyServiceContext context) + public RequestManager(InventoryServiceContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj b/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj index 7c81b5e9..fee972e5 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj @@ -1,8 +1,8 @@ - MyService.Infrastructure - MyService.Infrastructure + InventoryService.Infrastructure + InventoryService.Infrastructure Infrastructure layer for data access and external services @@ -30,7 +30,7 @@ - + diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/MyServiceContext.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/MyServiceContext.cs index 4486367d..287d1a9e 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/MyServiceContext.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/MyServiceContext.cs @@ -1,17 +1,17 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; -using MyService.Infrastructure.EntityConfigurations; +using InventoryService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.SeedWork; +using InventoryService.Infrastructure.EntityConfigurations; -namespace MyService.Infrastructure; +namespace InventoryService.Infrastructure; /// -/// EN: EF Core DbContext for MyService. -/// VI: EF Core DbContext cho MyService. +/// EN: EF Core DbContext for InventoryService. +/// VI: EF Core DbContext cho InventoryService. /// -public class MyServiceContext : DbContext, IUnitOfWork +public class InventoryServiceContext : DbContext, IUnitOfWork { private readonly IMediator _mediator; private IDbContextTransaction? _currentTransaction; @@ -34,16 +34,16 @@ public class MyServiceContext : DbContext, IUnitOfWork /// public bool HasActiveTransaction => _currentTransaction != null; - public MyServiceContext(DbContextOptions options) : base(options) + public InventoryServiceContext(DbContextOptions options) : base(options) { _mediator = null!; } - public MyServiceContext(DbContextOptions options, IMediator mediator) : base(options) + public InventoryServiceContext(DbContextOptions options, IMediator mediator) : base(options) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); - System.Diagnostics.Debug.WriteLine("MyServiceContext::ctor - " + GetHashCode()); + System.Diagnostics.Debug.WriteLine("InventoryServiceContext::ctor - " + GetHashCode()); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/SampleRepository.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/SampleRepository.cs index f0a4b109..8d67e52e 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/SampleRepository.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/SampleRepository.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using InventoryService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.SeedWork; -namespace MyService.Infrastructure.Repositories; +namespace InventoryService.Infrastructure.Repositories; /// /// EN: Repository implementation for Sample aggregate. @@ -10,7 +10,7 @@ namespace MyService.Infrastructure.Repositories; /// public class SampleRepository : ISampleRepository { - private readonly MyServiceContext _context; + private readonly InventoryServiceContext _context; /// /// EN: Unit of work for transaction management. @@ -18,7 +18,7 @@ public class SampleRepository : ISampleRepository /// public IUnitOfWork UnitOfWork => _context; - public SampleRepository(MyServiceContext context) + public SampleRepository(InventoryServiceContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/inventory-service-net/tests/InventoryService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/inventory-service-net/tests/InventoryService.FunctionalTests/Controllers/SamplesControllerTests.cs index e6e99ac5..64f1ecfc 100644 --- a/services/inventory-service-net/tests/InventoryService.FunctionalTests/Controllers/SamplesControllerTests.cs +++ b/services/inventory-service-net/tests/InventoryService.FunctionalTests/Controllers/SamplesControllerTests.cs @@ -4,7 +4,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; -namespace MyService.FunctionalTests.Controllers; +namespace InventoryService.FunctionalTests.Controllers; /// /// EN: Functional tests for Samples API endpoints. diff --git a/services/inventory-service-net/tests/InventoryService.FunctionalTests/CustomWebApplicationFactory.cs b/services/inventory-service-net/tests/InventoryService.FunctionalTests/CustomWebApplicationFactory.cs index d74d8338..b3efcfc0 100644 --- a/services/inventory-service-net/tests/InventoryService.FunctionalTests/CustomWebApplicationFactory.cs +++ b/services/inventory-service-net/tests/InventoryService.FunctionalTests/CustomWebApplicationFactory.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using MyService.Infrastructure; +using InventoryService.Infrastructure; -namespace MyService.FunctionalTests; +namespace InventoryService.FunctionalTests; /// /// EN: Custom WebApplicationFactory for functional tests. @@ -21,7 +21,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove the existing DbContext registration // VI: Xóa đăng ký DbContext hiện tại var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); + d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) { @@ -31,7 +31,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove DbContext service // VI: Xóa DbContext service var dbContextDescriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(MyServiceContext)); + d => d.ServiceType == typeof(InventoryServiceContext)); if (dbContextDescriptor != null) { @@ -40,7 +40,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Add in-memory database for testing // VI: Thêm in-memory database để test - services.AddDbContext(options => + services.AddDbContext(options => { options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString()); }); @@ -49,7 +49,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // VI: Đảm bảo database được tạo với seed data var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); }); } diff --git a/services/inventory-service-net/tests/InventoryService.FunctionalTests/InventoryService.FunctionalTests.csproj b/services/inventory-service-net/tests/InventoryService.FunctionalTests/InventoryService.FunctionalTests.csproj new file mode 100644 index 00000000..6798268f --- /dev/null +++ b/services/inventory-service-net/tests/InventoryService.FunctionalTests/InventoryService.FunctionalTests.csproj @@ -0,0 +1,38 @@ + + + + InventoryService.FunctionalTests + InventoryService.FunctionalTests + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/services/inventory-service-net/tests/InventoryService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/inventory-service-net/tests/InventoryService.UnitTests/Application/CreateSampleCommandHandlerTests.cs index 75cdd0e8..508dcef1 100644 --- a/services/inventory-service-net/tests/InventoryService.UnitTests/Application/CreateSampleCommandHandlerTests.cs +++ b/services/inventory-service-net/tests/InventoryService.UnitTests/Application/CreateSampleCommandHandlerTests.cs @@ -1,12 +1,12 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using MyService.API.Application.Commands; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using InventoryService.API.Application.Commands; +using InventoryService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.SeedWork; using Xunit; -namespace MyService.UnitTests.Application; +namespace InventoryService.UnitTests.Application; /// /// EN: Unit tests for CreateSampleCommandHandler. diff --git a/services/inventory-service-net/tests/InventoryService.UnitTests/Domain/SampleAggregateTests.cs b/services/inventory-service-net/tests/InventoryService.UnitTests/Domain/SampleAggregateTests.cs index dcf48767..52e1da5b 100644 --- a/services/inventory-service-net/tests/InventoryService.UnitTests/Domain/SampleAggregateTests.cs +++ b/services/inventory-service-net/tests/InventoryService.UnitTests/Domain/SampleAggregateTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.Exceptions; +using InventoryService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.Exceptions; using Xunit; -namespace MyService.UnitTests.Domain; +namespace InventoryService.UnitTests.Domain; /// /// EN: Unit tests for Sample aggregate. diff --git a/services/inventory-service-net/tests/InventoryService.UnitTests/InventoryService.UnitTests.csproj b/services/inventory-service-net/tests/InventoryService.UnitTests/InventoryService.UnitTests.csproj new file mode 100644 index 00000000..66f9445e --- /dev/null +++ b/services/inventory-service-net/tests/InventoryService.UnitTests/InventoryService.UnitTests.csproj @@ -0,0 +1,35 @@ + + + + InventoryService.UnitTests + InventoryService.UnitTests + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/ShopAggregate/Shop.cs b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/ShopAggregate/Shop.cs index e5bbecd9..fa0ecdc2 100644 --- a/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/ShopAggregate/Shop.cs +++ b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/ShopAggregate/Shop.cs @@ -30,6 +30,7 @@ public partial class Shop : Entity, IAggregateRoot private DateTime _createdAt; private DateTime? _updatedAt; private bool _isDeleted; + private ShopFeatures _features = null!; private readonly List _branches = new(); @@ -117,6 +118,12 @@ public partial class Shop : Entity, IAggregateRoot /// public string? CoverImageUrl => _coverImageUrl; + /// + /// EN: Feature configuration for multi-vertical. + /// VI: Cấu hình tính năng cho đa ngành hàng. + /// + public ShopFeatures Features => _features; + /// /// EN: Physical branches of the shop. /// VI: Các chi nhánh vật lý của shop. @@ -148,6 +155,7 @@ public partial class Shop : Entity, IAggregateRoot protected Shop() { _contactInfo = ContactInfo.Empty; + _features = ShopFeatures.Empty; } /// @@ -183,6 +191,7 @@ public partial class Shop : Entity, IAggregateRoot _status = ShopStatus.Draft; StatusId = ShopStatus.Draft.Id; _contactInfo = ContactInfo.Empty; + _features = ShopFeatures.ForCategory(category); _createdAt = DateTime.UtcNow; AddDomainEvent(new ShopCreatedDomainEvent(this)); @@ -248,6 +257,16 @@ public partial class Shop : Entity, IAggregateRoot _updatedAt = DateTime.UtcNow; } + /// + /// EN: Update feature configuration. + /// VI: Cập nhật cấu hình tính năng. + /// + public void UpdateFeatures(ShopFeatures features) + { + _features = features ?? ShopFeatures.Empty; + _updatedAt = DateTime.UtcNow; + } + /// /// EN: Publish shop (make it visible to customers). /// VI: Công khai shop (hiển thị với khách hàng). diff --git a/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/ShopAggregate/ShopValueObjects.cs b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/ShopAggregate/ShopValueObjects.cs index 378c0307..3b21880a 100644 --- a/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/ShopAggregate/ShopValueObjects.cs +++ b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/ShopAggregate/ShopValueObjects.cs @@ -192,3 +192,87 @@ public record OperatingHours CloseTime = new TimeOnly(23, 59) }; } + +/// +/// EN: Shop feature configuration for multi-vertical support. +/// VI: Cấu hình tính năng shop cho hỗ trợ đa ngành hàng. +/// +public record ShopFeatures +{ + /// + /// EN: Inventory management enabled (Retail, FnB). + /// VI: Bật quản lý tồn kho (Bán lẻ, FnB). + /// + public bool HasInventory { get; init; } + + /// + /// EN: Appointment booking enabled (Services, Spa). + /// VI: Bật đặt lịch hẹn (Dịch vụ, Spa). + /// + public bool HasBooking { get; init; } + + /// + /// EN: Table management enabled (FnB). + /// VI: Bật quản lý bàn (FnB). + /// + public bool HasTables { get; init; } + + /// + /// EN: Kitchen display system enabled (FnB). + /// VI: Bật hệ thống hiển thị bếp (FnB). + /// + public bool HasKitchen { get; init; } + + /// + /// EN: Shipping enabled (Retail). + /// VI: Bật vận chuyển (Bán lẻ). + /// + public bool HasShipping { get; init; } + + /// + /// EN: Delivery enabled (FnB, Retail). + /// VI: Bật giao hàng (FnB, Bán lẻ). + /// + public bool HasDelivery { get; init; } + + /// + /// EN: Default empty features. + /// VI: Cấu hình tính năng mặc định rỗng. + /// + public static ShopFeatures Empty => new(); + + /// + /// EN: Get default features based on business category. + /// VI: Lấy cấu hình tính năng mặc định theo ngành nghề. + /// + public static ShopFeatures ForCategory(BusinessCategory category) => category.Id switch + { + 1 => new ShopFeatures // FoodBeverage + { + HasInventory = true, + HasTables = true, + HasKitchen = true, + HasDelivery = true + }, + 2 or 3 or 9 or 10 => new ShopFeatures // Fashion, Electronics, Grocery, HomeFurniture + { + HasInventory = true, + HasShipping = true + }, + 4 or 5 or 6 or 8 => new ShopFeatures // Healthcare, Beauty, Education, Services + { + HasBooking = true + }, + 7 => new ShopFeatures // Entertainment + { + HasBooking = true, + HasInventory = true + }, + _ => new ShopFeatures // Other + { + HasInventory = true, + HasBooking = true + } + }; +} + diff --git a/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/ShopEntityTypeConfiguration.cs b/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/ShopEntityTypeConfiguration.cs index 41925531..bb67f135 100644 --- a/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/ShopEntityTypeConfiguration.cs +++ b/services/merchant-service-net/src/MerchantService.Infrastructure/EntityConfigurations/ShopEntityTypeConfiguration.cs @@ -96,6 +96,13 @@ public class ShopEntityTypeConfiguration : IEntityTypeConfiguration ); }); + // EN: Configure owned entity for ShopFeatures (JSONB for Multi-vertical) + // VI: Cấu hình owned entity cho ShopFeatures (JSONB cho Đa ngành hàng) + builder.OwnsOne("_features", sf => + { + sf.ToJson("features_config"); + }); + builder.Property("_createdAt") .HasColumnName("created_at") .IsRequired(); @@ -137,6 +144,7 @@ public class ShopEntityTypeConfiguration : IEntityTypeConfiguration builder.Ignore(s => s.CreatedAt); builder.Ignore(s => s.UpdatedAt); builder.Ignore(s => s.IsDeleted); + builder.Ignore(s => s.Features); } } diff --git a/services/order-service-net/docker-compose.yml b/services/order-service-net/docker-compose.yml deleted file mode 100644 index 254ceb12..00000000 --- a/services/order-service-net/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -# EN: Docker Compose for local development -# VI: Docker Compose cho phát triển local - -services: - myservice-api: - build: - context: . - dockerfile: Dockerfile - container_name: myservice-api - ports: - - "5000:8080" - environment: - - ASPNETCORE_ENVIRONMENT=Development - - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres - - REDIS_URL=redis:6379 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - myservice-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - postgres: - image: postgres:16-alpine - container_name: myservice-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: myservice_db - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - myservice-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: myservice-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - myservice-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: - -networks: - myservice-network: - driver: bridge diff --git a/services/order-service-net/src/OrderService.API/Application/Behaviors/LoggingBehavior.cs b/services/order-service-net/src/OrderService.API/Application/Behaviors/LoggingBehavior.cs index a724424d..65a210a5 100644 --- a/services/order-service-net/src/OrderService.API/Application/Behaviors/LoggingBehavior.cs +++ b/services/order-service-net/src/OrderService.API/Application/Behaviors/LoggingBehavior.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace OrderService.API.Application.Behaviors; /// /// EN: MediatR behavior for logging request handling. diff --git a/services/order-service-net/src/OrderService.API/Application/Behaviors/TransactionBehavior.cs b/services/order-service-net/src/OrderService.API/Application/Behaviors/TransactionBehavior.cs index 8675b649..0933f6bc 100644 --- a/services/order-service-net/src/OrderService.API/Application/Behaviors/TransactionBehavior.cs +++ b/services/order-service-net/src/OrderService.API/Application/Behaviors/TransactionBehavior.cs @@ -1,8 +1,8 @@ using MediatR; using Microsoft.EntityFrameworkCore; -using MyService.Infrastructure; +using OrderService.Infrastructure; -namespace MyService.API.Application.Behaviors; +namespace OrderService.API.Application.Behaviors; /// /// EN: MediatR behavior for handling database transactions. @@ -13,11 +13,11 @@ namespace MyService.API.Application.Behaviors; public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { - private readonly MyServiceContext _dbContext; + private readonly OrderServiceContext _dbContext; private readonly ILogger> _logger; public TransactionBehavior( - MyServiceContext dbContext, + OrderServiceContext dbContext, ILogger> logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); diff --git a/services/order-service-net/src/OrderService.API/Application/Behaviors/ValidatorBehavior.cs b/services/order-service-net/src/OrderService.API/Application/Behaviors/ValidatorBehavior.cs index 0062cd60..c08dd992 100644 --- a/services/order-service-net/src/OrderService.API/Application/Behaviors/ValidatorBehavior.cs +++ b/services/order-service-net/src/OrderService.API/Application/Behaviors/ValidatorBehavior.cs @@ -1,7 +1,7 @@ using FluentValidation; using MediatR; -namespace MyService.API.Application.Behaviors; +namespace OrderService.API.Application.Behaviors; /// /// EN: MediatR behavior for FluentValidation integration. diff --git a/services/order-service-net/src/OrderService.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/order-service-net/src/OrderService.API/Application/Commands/ChangeSampleStatusCommand.cs index 49825490..d5282db4 100644 --- a/services/order-service-net/src/OrderService.API/Application/Commands/ChangeSampleStatusCommand.cs +++ b/services/order-service-net/src/OrderService.API/Application/Commands/ChangeSampleStatusCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace OrderService.API.Application.Commands; /// /// EN: Command to change status of a Sample. diff --git a/services/order-service-net/src/OrderService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/order-service-net/src/OrderService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs index 76e31030..4f8e8974 100644 --- a/services/order-service-net/src/OrderService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs +++ b/services/order-service-net/src/OrderService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace OrderService.API.Application.Commands; /// /// EN: Handler for ChangeSampleStatusCommand. diff --git a/services/order-service-net/src/OrderService.API/Application/Commands/CreateSampleCommand.cs b/services/order-service-net/src/OrderService.API/Application/Commands/CreateSampleCommand.cs index 138cc794..4bd435c2 100644 --- a/services/order-service-net/src/OrderService.API/Application/Commands/CreateSampleCommand.cs +++ b/services/order-service-net/src/OrderService.API/Application/Commands/CreateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace OrderService.API.Application.Commands; /// /// EN: Command to create a new Sample. diff --git a/services/order-service-net/src/OrderService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/order-service-net/src/OrderService.API/Application/Commands/CreateSampleCommandHandler.cs index d7d0fd7c..9f7320e4 100644 --- a/services/order-service-net/src/OrderService.API/Application/Commands/CreateSampleCommandHandler.cs +++ b/services/order-service-net/src/OrderService.API/Application/Commands/CreateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace OrderService.API.Application.Commands; /// /// EN: Handler for CreateSampleCommand. diff --git a/services/order-service-net/src/OrderService.API/Application/Commands/DeleteSampleCommand.cs b/services/order-service-net/src/OrderService.API/Application/Commands/DeleteSampleCommand.cs index 0de392db..8181c7ea 100644 --- a/services/order-service-net/src/OrderService.API/Application/Commands/DeleteSampleCommand.cs +++ b/services/order-service-net/src/OrderService.API/Application/Commands/DeleteSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace OrderService.API.Application.Commands; /// /// EN: Command to delete a Sample. diff --git a/services/order-service-net/src/OrderService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/order-service-net/src/OrderService.API/Application/Commands/DeleteSampleCommandHandler.cs index c7632189..fe84d8f6 100644 --- a/services/order-service-net/src/OrderService.API/Application/Commands/DeleteSampleCommandHandler.cs +++ b/services/order-service-net/src/OrderService.API/Application/Commands/DeleteSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace OrderService.API.Application.Commands; /// /// EN: Handler for DeleteSampleCommand. diff --git a/services/order-service-net/src/OrderService.API/Application/Commands/UpdateSampleCommand.cs b/services/order-service-net/src/OrderService.API/Application/Commands/UpdateSampleCommand.cs index 6fad8514..7120c8be 100644 --- a/services/order-service-net/src/OrderService.API/Application/Commands/UpdateSampleCommand.cs +++ b/services/order-service-net/src/OrderService.API/Application/Commands/UpdateSampleCommand.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Commands; +namespace OrderService.API.Application.Commands; /// /// EN: Command to update an existing Sample. diff --git a/services/order-service-net/src/OrderService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/order-service-net/src/OrderService.API/Application/Commands/UpdateSampleCommandHandler.cs index e904cf0a..e8bdcb85 100644 --- a/services/order-service-net/src/OrderService.API/Application/Commands/UpdateSampleCommandHandler.cs +++ b/services/order-service-net/src/OrderService.API/Application/Commands/UpdateSampleCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Commands; +namespace OrderService.API.Application.Commands; /// /// EN: Handler for UpdateSampleCommand. diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/GetSampleQuery.cs b/services/order-service-net/src/OrderService.API/Application/Queries/GetSampleQuery.cs index 8b90789c..683452eb 100644 --- a/services/order-service-net/src/OrderService.API/Application/Queries/GetSampleQuery.cs +++ b/services/order-service-net/src/OrderService.API/Application/Queries/GetSampleQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace OrderService.API.Application.Queries; /// /// EN: Query to get a Sample by ID. diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/GetSampleQueryHandler.cs b/services/order-service-net/src/OrderService.API/Application/Queries/GetSampleQueryHandler.cs index 2da10b6d..d1b164ff 100644 --- a/services/order-service-net/src/OrderService.API/Application/Queries/GetSampleQueryHandler.cs +++ b/services/order-service-net/src/OrderService.API/Application/Queries/GetSampleQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace OrderService.API.Application.Queries; /// /// EN: Handler for GetSampleQuery. diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/GetSamplesQuery.cs b/services/order-service-net/src/OrderService.API/Application/Queries/GetSamplesQuery.cs index d6a98e34..bee0ad2e 100644 --- a/services/order-service-net/src/OrderService.API/Application/Queries/GetSamplesQuery.cs +++ b/services/order-service-net/src/OrderService.API/Application/Queries/GetSamplesQuery.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.API.Application.Queries; +namespace OrderService.API.Application.Queries; /// /// EN: Query to get all Samples. diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/order-service-net/src/OrderService.API/Application/Queries/GetSamplesQueryHandler.cs index 2185302d..a58205a1 100644 --- a/services/order-service-net/src/OrderService.API/Application/Queries/GetSamplesQueryHandler.cs +++ b/services/order-service-net/src/OrderService.API/Application/Queries/GetSamplesQueryHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.API.Application.Queries; +namespace OrderService.API.Application.Queries; /// /// EN: Handler for GetSamplesQuery. diff --git a/services/order-service-net/src/OrderService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/order-service-net/src/OrderService.API/Application/Validations/CreateSampleCommandValidator.cs index 2f339fb3..e6f20d3a 100644 --- a/services/order-service-net/src/OrderService.API/Application/Validations/CreateSampleCommandValidator.cs +++ b/services/order-service-net/src/OrderService.API/Application/Validations/CreateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using OrderService.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace OrderService.API.Application.Validations; /// /// EN: Validator for CreateSampleCommand. diff --git a/services/order-service-net/src/OrderService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/order-service-net/src/OrderService.API/Application/Validations/UpdateSampleCommandValidator.cs index 7030d5c8..ca9f6bc3 100644 --- a/services/order-service-net/src/OrderService.API/Application/Validations/UpdateSampleCommandValidator.cs +++ b/services/order-service-net/src/OrderService.API/Application/Validations/UpdateSampleCommandValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; -using MyService.API.Application.Commands; +using OrderService.API.Application.Commands; -namespace MyService.API.Application.Validations; +namespace OrderService.API.Application.Validations; /// /// EN: Validator for UpdateSampleCommand. diff --git a/services/order-service-net/src/OrderService.API/Controllers/SamplesController.cs b/services/order-service-net/src/OrderService.API/Controllers/SamplesController.cs index c87e0ffa..b5085ba2 100644 --- a/services/order-service-net/src/OrderService.API/Controllers/SamplesController.cs +++ b/services/order-service-net/src/OrderService.API/Controllers/SamplesController.cs @@ -1,10 +1,10 @@ using Asp.Versioning; using MediatR; using Microsoft.AspNetCore.Mvc; -using MyService.API.Application.Commands; -using MyService.API.Application.Queries; +using OrderService.API.Application.Commands; +using OrderService.API.Application.Queries; -namespace MyService.API.Controllers; +namespace OrderService.API.Controllers; /// /// EN: Controller for Sample CRUD operations using CQRS pattern. diff --git a/services/order-service-net/src/OrderService.API/OrderService.API.csproj b/services/order-service-net/src/OrderService.API/OrderService.API.csproj index 1b5bb222..e16dda24 100644 --- a/services/order-service-net/src/OrderService.API/OrderService.API.csproj +++ b/services/order-service-net/src/OrderService.API/OrderService.API.csproj @@ -1,8 +1,8 @@ - MyService.API - MyService.API + OrderService.API + OrderService.API Web API layer with CQRS pattern myservice-api @@ -36,8 +36,8 @@ - - + + diff --git a/services/order-service-net/src/OrderService.API/Program.cs b/services/order-service-net/src/OrderService.API/Program.cs index bd9b3df4..e60bb013 100644 --- a/services/order-service-net/src/OrderService.API/Program.cs +++ b/services/order-service-net/src/OrderService.API/Program.cs @@ -1,8 +1,8 @@ using Asp.Versioning; using FluentValidation; using Hellang.Middleware.ProblemDetails; -using MyService.API.Application.Behaviors; -using MyService.Infrastructure; +using OrderService.API.Application.Behaviors; +using OrderService.Infrastructure; using Serilog; // EN: Configure Serilog early / VI: Cấu hình Serilog sớm @@ -12,7 +12,7 @@ Log.Logger = new LoggerConfiguration() try { - Log.Information("Starting MyService API / Khởi động MyService API"); + Log.Information("Starting OrderService API / Khởi động OrderService API"); var builder = WebApplication.CreateBuilder(args); @@ -70,9 +70,9 @@ try { options.SwaggerDoc("v1", new() { - Title = "MyService API", + Title = "OrderService API", Version = "v1", - Description = "MyService microservice API / API microservice MyService" + Description = "OrderService microservice API / API microservice OrderService" }); }); @@ -107,7 +107,7 @@ try app.UseSwagger(); app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyService API v1"); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "OrderService API v1"); c.RoutePrefix = "swagger"; }); } diff --git a/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs new file mode 100644 index 00000000..2cb7c694 --- /dev/null +++ b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs @@ -0,0 +1,43 @@ +// EN: Order repository interface. +// VI: Interface repository Order. + +using OrderService.Domain.SeedWork; + +namespace OrderService.Domain.AggregatesModel.OrderAggregate; + +/// +/// EN: Repository interface for Order aggregate. +/// VI: Interface repository cho Order aggregate. +/// +public interface IOrderRepository : IRepository +{ + /// + /// EN: Add a new order. + /// VI: Thêm đơn hàng mới. + /// + Order Add(Order order); + + /// + /// EN: Update existing order. + /// VI: Cập nhật đơn hàng hiện có. + /// + void Update(Order order); + + /// + /// EN: Get order by ID. + /// VI: Lấy đơn hàng theo ID. + /// + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// EN: Get orders by shop ID. + /// VI: Lấy danh sách đơn hàng theo shop ID. + /// + Task> GetByShopIdAsync(Guid shopId, CancellationToken cancellationToken = default); + + /// + /// EN: Get orders by customer ID. + /// VI: Lấy danh sách đơn hàng theo customer ID. + /// + Task> GetByCustomerIdAsync(Guid customerId, CancellationToken cancellationToken = default); +} diff --git a/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/Order.cs b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/Order.cs new file mode 100644 index 00000000..662eb5f5 --- /dev/null +++ b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -0,0 +1,199 @@ +// EN: Order aggregate root - the orchestrator. +// VI: Aggregate root Order - bộ điều phối. + +using OrderService.Domain.Events; +using OrderService.Domain.Exceptions; +using OrderService.Domain.SeedWork; + +namespace OrderService.Domain.AggregatesModel.OrderAggregate; + +/// +/// EN: Order aggregate root - orchestrates order processing across verticals. +/// VI: Aggregate root Order - điều phối xử lý đơn hàng qua các ngành dọc. +/// +public class Order : Entity, IAggregateRoot +{ + private Guid _shopId; + private Guid? _customerId; + private OrderStatus _status = null!; + private decimal _totalAmount; + private DateTime _createdAt; + private DateTime? _updatedAt; + + private readonly List _items = new(); + + /// + /// EN: Shop ID that owns this order. + /// VI: ID shop sở hữu đơn hàng này. + /// + public Guid ShopId => _shopId; + + /// + /// EN: Customer ID (optional for walk-in customers). + /// VI: ID khách hàng (tùy chọn cho khách vãng lai). + /// + public Guid? CustomerId => _customerId; + + /// + /// EN: Order status. + /// VI: Trạng thái đơn hàng. + /// + public OrderStatus Status => _status; + + /// + /// EN: Status ID for EF Core mapping. + /// VI: Status ID cho EF Core mapping. + /// + public int StatusId { get; private set; } + + /// + /// EN: Total amount of the order. + /// VI: Tổng số tiền của đơn hàng. + /// + public decimal TotalAmount => _totalAmount; + + /// + /// EN: Order items (line items). + /// VI: Các items trong đơn hàng (dòng hàng). + /// + public IReadOnlyCollection Items => _items.AsReadOnly(); + + /// + /// 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 Order() + { + } + + /// + /// EN: Create a new order. + /// VI: Tạo đơn hàng mới. + /// + public Order(Guid shopId, Guid? customerId = null) + { + if (shopId == Guid.Empty) + throw new DomainException("Shop ID cannot be empty"); + + Id = Guid.NewGuid(); + _shopId = shopId; + _customerId = customerId; + _status = OrderStatus.Draft; + StatusId = OrderStatus.Draft.Id; + _totalAmount = 0; + _createdAt = DateTime.UtcNow; + + AddDomainEvent(new OrderCreatedDomainEvent(this)); + } + + /// + /// EN: Add an item to the order. + /// VI: Thêm item vào đơn hàng. + /// + public void AddItem(OrderItem item) + { + if (_status != OrderStatus.Draft) + throw new DomainException("Cannot add items to non-draft orders"); + + _items.Add(item); + RecalculateTotal(); + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Mark order as validated. + /// VI: Đánh dấu đơn hàng đã xác thực. + /// + public void MarkAsValidated() + { + if (_status != OrderStatus.Draft) + throw new DomainException($"Cannot validate order with status {_status.Name}"); + if (!_items.Any()) + throw new DomainException("Cannot validate order with no items"); + + _status = OrderStatus.Validated; + StatusId = OrderStatus.Validated.Id; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Mark order as paid. + /// VI: Đánh dấu đơn hàng đã thanh toán. + /// + public void MarkAsPaid() + { + if (_status != OrderStatus.Validated) + throw new DomainException($"Cannot mark as paid order with status {_status.Name}"); + + _status = OrderStatus.Paid; + StatusId = OrderStatus.Paid.Id; + _updatedAt = DateTime.UtcNow; + + AddDomainEvent(new OrderPaidDomainEvent(this)); + } + + /// + /// EN: Mark order as processing. + /// VI: Đánh dấu đơn hàng đang xử lý. + /// + public void MarkAsProcessing() + { + if (_status != OrderStatus.Paid) + throw new DomainException($"Cannot process order with status {_status.Name}"); + + _status = OrderStatus.Processing; + StatusId = OrderStatus.Processing.Id; + _updatedAt = DateTime.UtcNow; + } + + /// + /// EN: Mark order as completed. + /// VI: Đánh dấu đơn hàng hoàn thành. + /// + public void MarkAsCompleted() + { + if (_status != OrderStatus.Processing) + throw new DomainException($"Cannot complete order with status {_status.Name}"); + + _status = OrderStatus.Completed; + StatusId = OrderStatus.Completed.Id; + _updatedAt = DateTime.UtcNow; + + AddDomainEvent(new OrderCompletedDomainEvent(this)); + } + + /// + /// EN: Cancel the order. + /// VI: Hủy đơn hàng. + /// + public void Cancel(string reason) + { + if (_status == OrderStatus.Completed) + throw new DomainException("Cannot cancel completed order"); + if (_status == OrderStatus.Cancelled) + throw new DomainException("Order is already cancelled"); + + _status = OrderStatus.Cancelled; + StatusId = OrderStatus.Cancelled.Id; + _updatedAt = DateTime.UtcNow; + + AddDomainEvent(new OrderCancelledDomainEvent(this, reason)); + } + + private void RecalculateTotal() + { + _totalAmount = _items.Sum(i => i.TotalPrice); + } +} diff --git a/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/OrderItem.cs b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/OrderItem.cs new file mode 100644 index 00000000..789b9295 --- /dev/null +++ b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/OrderItem.cs @@ -0,0 +1,129 @@ +// EN: Order item entity representing a line item in an order. +// VI: Entity OrderItem đại diện cho một dòng hàng trong đơn hàng. + +using OrderService.Domain.Exceptions; +using OrderService.Domain.SeedWork; + +namespace OrderService.Domain.AggregatesModel.OrderAggregate; + +/// +/// EN: Order item entity - represents a line item in the order. +/// VI: Entity OrderItem - đại diện cho một dòng hàng trong đơn hàng. +/// +public class OrderItem : Entity +{ + private Guid _productId; + private string _productName = null!; + private string _productType = null!; // Physical, Service, PreparedFood + private int _quantity; + private decimal _unitPrice; + private string _status = null!; // Pending, Completed, Failed + private string? _metadata; // Additional data as JSON + + /// + /// EN: Product ID from Catalog Service. + /// VI: ID sản phẩm từ Catalog Service. + /// + public Guid ProductId => _productId; + + /// + /// EN: Product name (snapshot at order time). + /// VI: Tên sản phẩm (snapshot tại thời điểm đặt hàng). + /// + public string ProductName => _productName; + + /// + /// EN: Product type for strategy routing. + /// VI: Loại sản phẩm để định tuyến strategy. + /// + public string ProductType => _productType; + + /// + /// EN: Quantity ordered. + /// VI: Số lượng đặt hàng. + /// + public int Quantity => _quantity; + + /// + /// EN: Unit price at order time. + /// VI: Giá đơn vị tại thời điểm đặt hàng. + /// + public decimal UnitPrice => _unitPrice; + + /// + /// EN: Total price for this line item. + /// VI: Tổng giá cho dòng hàng này. + /// + public decimal TotalPrice => _quantity * _unitPrice; + + /// + /// EN: Status of this line item. + /// VI: Trạng thái của dòng hàng này. + /// + public string Status => _status; + + /// + /// EN: Additional metadata as JSON. + /// VI: Metadata bổ sung dưới dạng JSON. + /// + public string? Metadata => _metadata; + + /// + /// EN: Private constructor for EF Core. + /// VI: Constructor private cho EF Core. + /// + protected OrderItem() + { + } + + /// + /// EN: Create a new order item. + /// VI: Tạo order item mới. + /// + public OrderItem( + Guid productId, + string productName, + string productType, + int quantity, + decimal unitPrice, + string? metadata = null) + { + if (productId == Guid.Empty) + throw new DomainException("Product ID cannot be empty"); + if (string.IsNullOrWhiteSpace(productName)) + throw new DomainException("Product name cannot be empty"); + if (string.IsNullOrWhiteSpace(productType)) + throw new DomainException("Product type cannot be empty"); + if (quantity <= 0) + throw new DomainException("Quantity must be greater than zero"); + if (unitPrice < 0) + throw new DomainException("Unit price cannot be negative"); + + Id = Guid.NewGuid(); + _productId = productId; + _productName = productName.Trim(); + _productType = productType.Trim(); + _quantity = quantity; + _unitPrice = unitPrice; + _status = "Pending"; + _metadata = metadata; + } + + /// + /// EN: Mark item as completed. + /// VI: Đánh dấu item hoàn thành. + /// + public void MarkAsCompleted() + { + _status = "Completed"; + } + + /// + /// EN: Mark item as failed. + /// VI: Đánh dấu item thất bại. + /// + public void MarkAsFailed() + { + _status = "Failed"; + } +} diff --git a/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs new file mode 100644 index 00000000..82039276 --- /dev/null +++ b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs @@ -0,0 +1,53 @@ +// EN: Order status enumeration. +// VI: Enumeration trạng thái đơn hàng. + +using OrderService.Domain.SeedWork; + +namespace OrderService.Domain.AggregatesModel.OrderAggregate; + +/// +/// EN: Order status enumeration. +/// VI: Enumeration trạng thái đơn hàng. +/// +public class OrderStatus : Enumeration +{ + /// + /// EN: Order created but not validated yet. + /// VI: Đơn hàng được tạo nhưng chưa được xác thực. + /// + public static readonly OrderStatus Draft = new(1, nameof(Draft)); + + /// + /// EN: All items validated and available. + /// VI: Tất cả items đã được xác thực và có sẵn. + /// + public static readonly OrderStatus Validated = new(2, nameof(Validated)); + + /// + /// EN: Payment processed successfully. + /// VI: Thanh toán đã được xử lý thành công. + /// + public static readonly OrderStatus Paid = new(3, nameof(Paid)); + + /// + /// EN: Order is being processed (items being executed). + /// VI: Đơn hàng đang được xử lý (items đang được thực thi). + /// + public static readonly OrderStatus Processing = new(4, nameof(Processing)); + + /// + /// EN: Order completed successfully. + /// VI: Đơn hàng hoàn thành thành công. + /// + public static readonly OrderStatus Completed = new(5, nameof(Completed)); + + /// + /// EN: Order cancelled. + /// VI: Đơn hàng đã bị hủy. + /// + public static readonly OrderStatus Cancelled = new(6, nameof(Cancelled)); + + public OrderStatus(int id, string name) : base(id, name) + { + } +} diff --git a/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs index 40bc8c3a..5660e8f6 100644 --- a/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs +++ b/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using OrderService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace OrderService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Repository interface for Sample aggregate. diff --git a/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/Sample.cs index 641bb385..5b96ad34 100644 --- a/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/Sample.cs +++ b/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/Sample.cs @@ -1,8 +1,8 @@ -using MyService.Domain.Events; -using MyService.Domain.Exceptions; -using MyService.Domain.SeedWork; +using OrderService.Domain.Events; +using OrderService.Domain.Exceptions; +using OrderService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace OrderService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample aggregate root demonstrating DDD patterns. diff --git a/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs index 54ce63ba..e9eec266 100644 --- a/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs +++ b/services/order-service-net/src/OrderService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs @@ -1,6 +1,6 @@ -using MyService.Domain.SeedWork; +using OrderService.Domain.SeedWork; -namespace MyService.Domain.AggregatesModel.SampleAggregate; +namespace OrderService.Domain.AggregatesModel.SampleAggregate; /// /// EN: Sample status enumeration following type-safe enum pattern. diff --git a/services/order-service-net/src/OrderService.Domain/Events/OrderDomainEvents.cs b/services/order-service-net/src/OrderService.Domain/Events/OrderDomainEvents.cs new file mode 100644 index 00000000..3247ccec --- /dev/null +++ b/services/order-service-net/src/OrderService.Domain/Events/OrderDomainEvents.cs @@ -0,0 +1,31 @@ +// EN: Domain events for Order aggregate. +// VI: Domain events cho Order aggregate. + +using MediatR; +using OrderService.Domain.AggregatesModel.OrderAggregate; + +namespace OrderService.Domain.Events; + +/// +/// EN: Domain event raised when an order is created. +/// VI: Domain event phát ra khi đơn hàng được tạo. +/// +public record OrderCreatedDomainEvent(Order Order) : INotification; + +/// +/// EN: Domain event raised when an order is paid. +/// VI: Domain event phát ra khi đơn hàng được thanh toán. +/// +public record OrderPaidDomainEvent(Order Order) : INotification; + +/// +/// EN: Domain event raised when an order is completed. +/// VI: Domain event phát ra khi đơn hàng hoàn thành. +/// +public record OrderCompletedDomainEvent(Order Order) : INotification; + +/// +/// EN: Domain event raised when an order is cancelled. +/// VI: Domain event phát ra khi đơn hàng bị hủy. +/// +public record OrderCancelledDomainEvent(Order Order, string Reason) : INotification; diff --git a/services/order-service-net/src/OrderService.Domain/Events/SampleCreatedDomainEvent.cs b/services/order-service-net/src/OrderService.Domain/Events/SampleCreatedDomainEvent.cs index 7e838214..4c933b5e 100644 --- a/services/order-service-net/src/OrderService.Domain/Events/SampleCreatedDomainEvent.cs +++ b/services/order-service-net/src/OrderService.Domain/Events/SampleCreatedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace OrderService.Domain.Events; /// /// EN: Domain event raised when a new Sample is created. diff --git a/services/order-service-net/src/OrderService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/order-service-net/src/OrderService.Domain/Events/SampleStatusChangedDomainEvent.cs index f6d9b422..8a5dac72 100644 --- a/services/order-service-net/src/OrderService.Domain/Events/SampleStatusChangedDomainEvent.cs +++ b/services/order-service-net/src/OrderService.Domain/Events/SampleStatusChangedDomainEvent.cs @@ -1,7 +1,7 @@ using MediatR; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Domain.Events; +namespace OrderService.Domain.Events; /// /// EN: Domain event raised when Sample status changes. diff --git a/services/order-service-net/src/OrderService.Domain/Exceptions/DomainException.cs b/services/order-service-net/src/OrderService.Domain/Exceptions/DomainException.cs index 7e737f64..e93bea8e 100644 --- a/services/order-service-net/src/OrderService.Domain/Exceptions/DomainException.cs +++ b/services/order-service-net/src/OrderService.Domain/Exceptions/DomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace OrderService.Domain.Exceptions; /// /// EN: Base exception for domain errors. diff --git a/services/order-service-net/src/OrderService.Domain/Exceptions/SampleDomainException.cs b/services/order-service-net/src/OrderService.Domain/Exceptions/SampleDomainException.cs index c850944c..c250f1bf 100644 --- a/services/order-service-net/src/OrderService.Domain/Exceptions/SampleDomainException.cs +++ b/services/order-service-net/src/OrderService.Domain/Exceptions/SampleDomainException.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.Exceptions; +namespace OrderService.Domain.Exceptions; /// /// EN: Exception for Sample aggregate domain errors. diff --git a/services/order-service-net/src/OrderService.Domain/OrderService.Domain.csproj b/services/order-service-net/src/OrderService.Domain/OrderService.Domain.csproj index 3208317a..73176de5 100644 --- a/services/order-service-net/src/OrderService.Domain/OrderService.Domain.csproj +++ b/services/order-service-net/src/OrderService.Domain/OrderService.Domain.csproj @@ -1,8 +1,8 @@ - MyService.Domain - MyService.Domain + OrderService.Domain + OrderService.Domain Domain layer containing core business logic and entities diff --git a/services/order-service-net/src/OrderService.Domain/SeedWork/Entity.cs b/services/order-service-net/src/OrderService.Domain/SeedWork/Entity.cs index b07fdd3b..f3cb5a8b 100644 --- a/services/order-service-net/src/OrderService.Domain/SeedWork/Entity.cs +++ b/services/order-service-net/src/OrderService.Domain/SeedWork/Entity.cs @@ -1,6 +1,6 @@ using MediatR; -namespace MyService.Domain.SeedWork; +namespace OrderService.Domain.SeedWork; /// /// EN: Base class for all domain entities. diff --git a/services/order-service-net/src/OrderService.Domain/SeedWork/Enumeration.cs b/services/order-service-net/src/OrderService.Domain/SeedWork/Enumeration.cs index 6641979c..e7fac2c7 100644 --- a/services/order-service-net/src/OrderService.Domain/SeedWork/Enumeration.cs +++ b/services/order-service-net/src/OrderService.Domain/SeedWork/Enumeration.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace MyService.Domain.SeedWork; +namespace OrderService.Domain.SeedWork; /// /// EN: Base class for enumeration classes (type-safe enum pattern). diff --git a/services/order-service-net/src/OrderService.Domain/SeedWork/IAggregateRoot.cs b/services/order-service-net/src/OrderService.Domain/SeedWork/IAggregateRoot.cs index d361394f..d3735527 100644 --- a/services/order-service-net/src/OrderService.Domain/SeedWork/IAggregateRoot.cs +++ b/services/order-service-net/src/OrderService.Domain/SeedWork/IAggregateRoot.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace OrderService.Domain.SeedWork; /// /// EN: Marker interface for aggregate roots. diff --git a/services/order-service-net/src/OrderService.Domain/SeedWork/IRepository.cs b/services/order-service-net/src/OrderService.Domain/SeedWork/IRepository.cs index 2d539e44..ba13656d 100644 --- a/services/order-service-net/src/OrderService.Domain/SeedWork/IRepository.cs +++ b/services/order-service-net/src/OrderService.Domain/SeedWork/IRepository.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace OrderService.Domain.SeedWork; /// /// EN: Generic repository interface for aggregate roots. diff --git a/services/order-service-net/src/OrderService.Domain/SeedWork/IUnitOfWork.cs b/services/order-service-net/src/OrderService.Domain/SeedWork/IUnitOfWork.cs index d37d8fa4..9762efdf 100644 --- a/services/order-service-net/src/OrderService.Domain/SeedWork/IUnitOfWork.cs +++ b/services/order-service-net/src/OrderService.Domain/SeedWork/IUnitOfWork.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace OrderService.Domain.SeedWork; /// /// EN: Unit of Work pattern interface. diff --git a/services/order-service-net/src/OrderService.Domain/SeedWork/ValueObject.cs b/services/order-service-net/src/OrderService.Domain/SeedWork/ValueObject.cs index 5cf4188f..3b54949a 100644 --- a/services/order-service-net/src/OrderService.Domain/SeedWork/ValueObject.cs +++ b/services/order-service-net/src/OrderService.Domain/SeedWork/ValueObject.cs @@ -1,4 +1,4 @@ -namespace MyService.Domain.SeedWork; +namespace OrderService.Domain.SeedWork; /// /// EN: Base class for Value Objects following DDD patterns. diff --git a/services/order-service-net/src/OrderService.Domain/Strategies/ILineItemStrategy.cs b/services/order-service-net/src/OrderService.Domain/Strategies/ILineItemStrategy.cs new file mode 100644 index 00000000..31eaca81 --- /dev/null +++ b/services/order-service-net/src/OrderService.Domain/Strategies/ILineItemStrategy.cs @@ -0,0 +1,38 @@ +// EN: Strategy interface for line item processing. +// VI: Interface Strategy cho xử lý dòng hàng. + +using OrderService.Domain.AggregatesModel.OrderAggregate; + +namespace OrderService.Domain.Strategies; + +/// +/// EN: Strategy interface for processing different product types. +/// VI: Interface Strategy cho xử lý các loại sản phẩm khác nhau. +/// +public interface ILineItemStrategy +{ + /// + /// EN: Product type this strategy supports. + /// VI: Loại sản phẩm mà strategy này hỗ trợ. + /// + string SupportedType { get; } + + /// + /// EN: Validate if the item can be fulfilled. + /// VI: Xác thực xem item có thể được thực hiện không. + /// + /// Order item to validate + /// Shop ID + /// Cancellation token + /// True if item can be fulfilled + Task ValidateAsync(OrderItem item, Guid shopId, CancellationToken cancellationToken = default); + + /// + /// EN: Execute the item fulfillment. + /// VI: Thực thi việc thực hiện item. + /// + /// Order item to execute + /// Shop ID + /// Cancellation token + Task ExecuteAsync(OrderItem item, Guid shopId, CancellationToken cancellationToken = default); +} diff --git a/services/order-service-net/src/OrderService.Infrastructure/DependencyInjection.cs b/services/order-service-net/src/OrderService.Infrastructure/DependencyInjection.cs index a8dfba0d..b6809de5 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/DependencyInjection.cs +++ b/services/order-service-net/src/OrderService.Infrastructure/DependencyInjection.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Infrastructure.Idempotency; -using MyService.Infrastructure.Repositories; +using OrderService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Infrastructure.Idempotency; +using OrderService.Infrastructure.Repositories; -namespace MyService.Infrastructure; +namespace OrderService.Infrastructure; /// /// EN: Dependency injection extensions for Infrastructure layer. @@ -22,7 +22,7 @@ public static class DependencyInjection IConfiguration configuration) { // EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL - services.AddDbContext(options => + services.AddDbContext(options => { var connectionString = configuration.GetConnectionString("DefaultConnection") ?? configuration["DATABASE_URL"] @@ -30,7 +30,7 @@ public static class DependencyInjection options.UseNpgsql(connectionString, npgsqlOptions => { - npgsqlOptions.MigrationsAssembly(typeof(MyServiceContext).Assembly.FullName); + npgsqlOptions.MigrationsAssembly(typeof(OrderServiceContext).Assembly.FullName); npgsqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), diff --git a/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs b/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs index 5c2fbd42..cef94859 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs +++ b/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/SampleEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace OrderService.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for Sample entity. diff --git a/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs b/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs index 8b683f56..bba3d930 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs +++ b/services/order-service-net/src/OrderService.Infrastructure/EntityConfigurations/SampleStatusEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using MyService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.AggregatesModel.SampleAggregate; -namespace MyService.Infrastructure.EntityConfigurations; +namespace OrderService.Infrastructure.EntityConfigurations; /// /// EN: EF Core configuration for SampleStatus enumeration. diff --git a/services/order-service-net/src/OrderService.Infrastructure/Idempotency/ClientRequest.cs b/services/order-service-net/src/OrderService.Infrastructure/Idempotency/ClientRequest.cs index f65e4a56..eb81f402 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/Idempotency/ClientRequest.cs +++ b/services/order-service-net/src/OrderService.Infrastructure/Idempotency/ClientRequest.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace OrderService.Infrastructure.Idempotency; /// /// EN: Entity for tracking client requests to ensure idempotency. diff --git a/services/order-service-net/src/OrderService.Infrastructure/Idempotency/IRequestManager.cs b/services/order-service-net/src/OrderService.Infrastructure/Idempotency/IRequestManager.cs index 92097399..2c986ace 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/Idempotency/IRequestManager.cs +++ b/services/order-service-net/src/OrderService.Infrastructure/Idempotency/IRequestManager.cs @@ -1,4 +1,4 @@ -namespace MyService.Infrastructure.Idempotency; +namespace OrderService.Infrastructure.Idempotency; /// /// EN: Interface for managing client request idempotency. diff --git a/services/order-service-net/src/OrderService.Infrastructure/Idempotency/RequestManager.cs b/services/order-service-net/src/OrderService.Infrastructure/Idempotency/RequestManager.cs index 41a5f318..61dcfa50 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/Idempotency/RequestManager.cs +++ b/services/order-service-net/src/OrderService.Infrastructure/Idempotency/RequestManager.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace MyService.Infrastructure.Idempotency; +namespace OrderService.Infrastructure.Idempotency; /// /// EN: Implementation of request manager for idempotency. @@ -8,9 +8,9 @@ namespace MyService.Infrastructure.Idempotency; /// public class RequestManager : IRequestManager { - private readonly MyServiceContext _context; + private readonly OrderServiceContext _context; - public RequestManager(MyServiceContext context) + public RequestManager(OrderServiceContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/order-service-net/src/OrderService.Infrastructure/MyServiceContext.cs b/services/order-service-net/src/OrderService.Infrastructure/MyServiceContext.cs index 4486367d..536bbff1 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/MyServiceContext.cs +++ b/services/order-service-net/src/OrderService.Infrastructure/MyServiceContext.cs @@ -1,17 +1,17 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; -using MyService.Infrastructure.EntityConfigurations; +using OrderService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.SeedWork; +using OrderService.Infrastructure.EntityConfigurations; -namespace MyService.Infrastructure; +namespace OrderService.Infrastructure; /// -/// EN: EF Core DbContext for MyService. -/// VI: EF Core DbContext cho MyService. +/// EN: EF Core DbContext for OrderService. +/// VI: EF Core DbContext cho OrderService. /// -public class MyServiceContext : DbContext, IUnitOfWork +public class OrderServiceContext : DbContext, IUnitOfWork { private readonly IMediator _mediator; private IDbContextTransaction? _currentTransaction; @@ -34,16 +34,16 @@ public class MyServiceContext : DbContext, IUnitOfWork /// public bool HasActiveTransaction => _currentTransaction != null; - public MyServiceContext(DbContextOptions options) : base(options) + public OrderServiceContext(DbContextOptions options) : base(options) { _mediator = null!; } - public MyServiceContext(DbContextOptions options, IMediator mediator) : base(options) + public OrderServiceContext(DbContextOptions options, IMediator mediator) : base(options) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); - System.Diagnostics.Debug.WriteLine("MyServiceContext::ctor - " + GetHashCode()); + System.Diagnostics.Debug.WriteLine("OrderServiceContext::ctor - " + GetHashCode()); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/services/order-service-net/src/OrderService.Infrastructure/OrderService.Infrastructure.csproj b/services/order-service-net/src/OrderService.Infrastructure/OrderService.Infrastructure.csproj index 7c81b5e9..768dcf2f 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/OrderService.Infrastructure.csproj +++ b/services/order-service-net/src/OrderService.Infrastructure/OrderService.Infrastructure.csproj @@ -1,8 +1,8 @@ - MyService.Infrastructure - MyService.Infrastructure + OrderService.Infrastructure + OrderService.Infrastructure Infrastructure layer for data access and external services @@ -30,7 +30,7 @@ - + diff --git a/services/order-service-net/src/OrderService.Infrastructure/Repositories/SampleRepository.cs b/services/order-service-net/src/OrderService.Infrastructure/Repositories/SampleRepository.cs index f0a4b109..8e558ea5 100644 --- a/services/order-service-net/src/OrderService.Infrastructure/Repositories/SampleRepository.cs +++ b/services/order-service-net/src/OrderService.Infrastructure/Repositories/SampleRepository.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using OrderService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.SeedWork; -namespace MyService.Infrastructure.Repositories; +namespace OrderService.Infrastructure.Repositories; /// /// EN: Repository implementation for Sample aggregate. @@ -10,7 +10,7 @@ namespace MyService.Infrastructure.Repositories; /// public class SampleRepository : ISampleRepository { - private readonly MyServiceContext _context; + private readonly OrderServiceContext _context; /// /// EN: Unit of work for transaction management. @@ -18,7 +18,7 @@ public class SampleRepository : ISampleRepository /// public IUnitOfWork UnitOfWork => _context; - public SampleRepository(MyServiceContext context) + public SampleRepository(OrderServiceContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/order-service-net/tests/OrderService.FunctionalTests/Controllers/SamplesControllerTests.cs b/services/order-service-net/tests/OrderService.FunctionalTests/Controllers/SamplesControllerTests.cs index e6e99ac5..e2427507 100644 --- a/services/order-service-net/tests/OrderService.FunctionalTests/Controllers/SamplesControllerTests.cs +++ b/services/order-service-net/tests/OrderService.FunctionalTests/Controllers/SamplesControllerTests.cs @@ -4,7 +4,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; -namespace MyService.FunctionalTests.Controllers; +namespace OrderService.FunctionalTests.Controllers; /// /// EN: Functional tests for Samples API endpoints. diff --git a/services/order-service-net/tests/OrderService.FunctionalTests/CustomWebApplicationFactory.cs b/services/order-service-net/tests/OrderService.FunctionalTests/CustomWebApplicationFactory.cs index d74d8338..e31c00a1 100644 --- a/services/order-service-net/tests/OrderService.FunctionalTests/CustomWebApplicationFactory.cs +++ b/services/order-service-net/tests/OrderService.FunctionalTests/CustomWebApplicationFactory.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using MyService.Infrastructure; +using OrderService.Infrastructure; -namespace MyService.FunctionalTests; +namespace OrderService.FunctionalTests; /// /// EN: Custom WebApplicationFactory for functional tests. @@ -21,7 +21,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove the existing DbContext registration // VI: Xóa đăng ký DbContext hiện tại var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); + d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) { @@ -31,7 +31,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove DbContext service // VI: Xóa DbContext service var dbContextDescriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(MyServiceContext)); + d => d.ServiceType == typeof(OrderServiceContext)); if (dbContextDescriptor != null) { @@ -40,7 +40,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Add in-memory database for testing // VI: Thêm in-memory database để test - services.AddDbContext(options => + services.AddDbContext(options => { options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString()); }); @@ -49,7 +49,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // VI: Đảm bảo database được tạo với seed data var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); }); } diff --git a/services/order-service-net/tests/OrderService.FunctionalTests/MyService.FunctionalTests.csproj b/services/order-service-net/tests/OrderService.FunctionalTests/MyService.FunctionalTests.csproj deleted file mode 100644 index 4cc894f8..00000000 --- a/services/order-service-net/tests/OrderService.FunctionalTests/MyService.FunctionalTests.csproj +++ /dev/null @@ -1,38 +0,0 @@ - - - - MyService.FunctionalTests - MyService.FunctionalTests - false - true - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/services/catalog-service-net/tests/CatalogService.FunctionalTests/MyService.FunctionalTests.csproj b/services/order-service-net/tests/OrderService.FunctionalTests/OrderService.FunctionalTests.csproj similarity index 86% rename from services/catalog-service-net/tests/CatalogService.FunctionalTests/MyService.FunctionalTests.csproj rename to services/order-service-net/tests/OrderService.FunctionalTests/OrderService.FunctionalTests.csproj index 4cc894f8..e705fbb6 100644 --- a/services/catalog-service-net/tests/CatalogService.FunctionalTests/MyService.FunctionalTests.csproj +++ b/services/order-service-net/tests/OrderService.FunctionalTests/OrderService.FunctionalTests.csproj @@ -1,8 +1,8 @@ - MyService.FunctionalTests - MyService.FunctionalTests + OrderService.FunctionalTests + OrderService.FunctionalTests false true @@ -32,7 +32,7 @@ - + diff --git a/services/order-service-net/tests/OrderService.UnitTests/Application/CreateSampleCommandHandlerTests.cs b/services/order-service-net/tests/OrderService.UnitTests/Application/CreateSampleCommandHandlerTests.cs index 75cdd0e8..caa846be 100644 --- a/services/order-service-net/tests/OrderService.UnitTests/Application/CreateSampleCommandHandlerTests.cs +++ b/services/order-service-net/tests/OrderService.UnitTests/Application/CreateSampleCommandHandlerTests.cs @@ -1,12 +1,12 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using MyService.API.Application.Commands; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.SeedWork; +using OrderService.API.Application.Commands; +using OrderService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.SeedWork; using Xunit; -namespace MyService.UnitTests.Application; +namespace OrderService.UnitTests.Application; /// /// EN: Unit tests for CreateSampleCommandHandler. diff --git a/services/order-service-net/tests/OrderService.UnitTests/Domain/SampleAggregateTests.cs b/services/order-service-net/tests/OrderService.UnitTests/Domain/SampleAggregateTests.cs index dcf48767..3df2bcee 100644 --- a/services/order-service-net/tests/OrderService.UnitTests/Domain/SampleAggregateTests.cs +++ b/services/order-service-net/tests/OrderService.UnitTests/Domain/SampleAggregateTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; -using MyService.Domain.AggregatesModel.SampleAggregate; -using MyService.Domain.Exceptions; +using OrderService.Domain.AggregatesModel.SampleAggregate; +using OrderService.Domain.Exceptions; using Xunit; -namespace MyService.UnitTests.Domain; +namespace OrderService.UnitTests.Domain; /// /// EN: Unit tests for Sample aggregate. diff --git a/services/order-service-net/tests/OrderService.UnitTests/MyService.UnitTests.csproj b/services/order-service-net/tests/OrderService.UnitTests/MyService.UnitTests.csproj deleted file mode 100644 index b40272dc..00000000 --- a/services/order-service-net/tests/OrderService.UnitTests/MyService.UnitTests.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - MyService.UnitTests - MyService.UnitTests - false - true - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/services/booking-service-net/tests/BookingService.UnitTests/MyService.UnitTests.csproj b/services/order-service-net/tests/OrderService.UnitTests/OrderService.UnitTests.csproj similarity index 79% rename from services/booking-service-net/tests/BookingService.UnitTests/MyService.UnitTests.csproj rename to services/order-service-net/tests/OrderService.UnitTests/OrderService.UnitTests.csproj index b40272dc..4dda20d6 100644 --- a/services/booking-service-net/tests/BookingService.UnitTests/MyService.UnitTests.csproj +++ b/services/order-service-net/tests/OrderService.UnitTests/OrderService.UnitTests.csproj @@ -1,8 +1,8 @@ - MyService.UnitTests - MyService.UnitTests + OrderService.UnitTests + OrderService.UnitTests false true @@ -28,8 +28,8 @@ - - + +