diff --git a/services/ads-analytics-service-net/docs/en/ARCHITECTURE.md b/services/ads-analytics-service-net/docs/en/ARCHITECTURE.md
index 9d80ba57..afd65471 100644
--- a/services/ads-analytics-service-net/docs/en/ARCHITECTURE.md
+++ b/services/ads-analytics-service-net/docs/en/ARCHITECTURE.md
@@ -1,271 +1,50 @@
-# Architecture Documentation
+# Ads Analytics Service Architecture
-> Detailed architecture documentation for the .NET 10 Microservice Template.
-
-## Architecture Overview
+## Overview
```mermaid
graph TB
- subgraph "API Layer"
- C[Controllers]
- CMD[Commands]
- Q[Queries]
- B[Behaviors]
- V[Validations]
+ subgraph "Data Sources"
+ SERV[ads-serving]
+ TRACK[ads-tracking]
end
- subgraph "Domain Layer"
- AR[Aggregate Roots]
- E[Entities]
- VO[Value Objects]
- DE[Domain Events]
- DX[Domain Exceptions]
+ subgraph "ads-analytics-service"
+ INGEST[Ingestion]
+ AGG[Aggregation]
+ STORE[(ClickHouse)]
end
- subgraph "Infrastructure Layer"
- DB[(PostgreSQL)]
- R[Repositories]
- CTX[DbContext]
- ID[Idempotency]
- end
-
- C --> CMD
- C --> Q
- CMD --> B --> V
- CMD --> AR
- Q --> R
- R --> CTX --> DB
- AR --> DE
- R --> AR
-
- style C fill:#4a90d9,stroke:#2d5986,color:#fff
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
+ SERV -->|Events| INGEST
+ TRACK -->|Conversions| INGEST
+ INGEST --> STORE --> AGG
```
-## Layer Responsibilities
+## Metrics
-### 1. Domain Layer (MyService.Domain)
+| Metric | Formula |
+|--------|---------|
+| **CTR** | Clicks / Impressions |
+| **CPC** | Spend / Clicks |
+| **CPM** | (Spend / Impressions) × 1000 |
+| **ROAS** | Revenue / Spend |
-The heart of the application containing pure business logic. This layer:
-- Has **ZERO** external dependencies (except MediatR.Contracts for events)
-- Contains only POCO classes
-- Implements DDD tactical patterns
+## Database (ClickHouse)
-#### Components
-
-| Component | Purpose |
-|-----------|---------|
-| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
-| **AggregatesModel** | Aggregate roots with their entities and value objects |
-| **Events** | Domain events for cross-aggregate communication |
-| **Exceptions** | Domain-specific exceptions for business rule violations |
-
-### 2. Infrastructure Layer (MyService.Infrastructure)
-
-Technical implementations and external concerns:
-- Database access (EF Core)
-- Repository implementations
-- External service integrations
-
-### 3. API Layer (MyService.API)
-
-Application entry point and CQRS implementation:
-- Controllers for HTTP handling
-- Commands for write operations
-- Queries for read operations
-- MediatR behaviors for cross-cutting concerns
-
-## CQRS Flow
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Controller
- participant MediatR
- participant LoggingBehavior
- participant ValidatorBehavior
- participant TransactionBehavior
- participant CommandHandler
- participant Repository
- participant DbContext
-
- Client->>Controller: HTTP Request
- Controller->>MediatR: Send(Command)
- MediatR->>LoggingBehavior: Handle
- LoggingBehavior->>ValidatorBehavior: Next()
- ValidatorBehavior->>TransactionBehavior: Next()
- TransactionBehavior->>CommandHandler: Next()
- CommandHandler->>Repository: Add/Update/Delete
- Repository->>DbContext: SaveEntitiesAsync()
- DbContext-->>Repository: Success
- Repository-->>CommandHandler: Result
- CommandHandler-->>Controller: Response
- Controller-->>Client: HTTP Response
+```sql
+CREATE TABLE ad_events (
+ event_type Enum8('impression', 'click', 'conversion'),
+ ad_id UUID,
+ campaign_id UUID,
+ cost Decimal(18, 6),
+ event_time DateTime
+) ENGINE = MergeTree()
+ORDER BY (campaign_id, event_time);
```
-## Domain Events
+## Integration
-```mermaid
-graph LR
- AR[Aggregate Root] -->|Raises| DE[Domain Event]
- DE -->|Dispatched by| CTX[DbContext]
- CTX -->|Publishes to| M[MediatR]
- M -->|Handled by| H1[Handler 1]
- M -->|Handled by| H2[Handler 2]
-
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DE fill:#f39c12,stroke:#d68910,color:#fff
- style M fill:#9b59b6,stroke:#7d3c98,color:#fff
-```
-
-## Database Schema
-
-### Sample Aggregate
-
-```mermaid
-erDiagram
- samples {
- uuid id PK
- varchar(200) name
- varchar(1000) description
- int status_id FK
- timestamp created_at
- timestamp updated_at
- }
-
- sample_statuses {
- int id PK
- varchar(50) name
- }
-
- samples ||--o{ sample_statuses : has
-```
-
-## MediatR Pipeline
-
-```
-Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
- │ │ │
- ▼ ▼ ▼
- Log start/end Validate Begin/Commit
- + timing with Transaction
- FluentValidation
-```
-
-### Behavior Order
-
-1. **LoggingBehavior** - Logs request handling with timing
-2. **ValidatorBehavior** - Validates request using FluentValidation
-3. **TransactionBehavior** - Wraps command handlers in database transactions
-
-## Error Handling
-
-### Exception Hierarchy
-
-```
-Exception
-└── DomainException
- └── SampleDomainException
-```
-
-### Problem Details (RFC 7807)
-
-All errors are returned in Problem Details format:
-
-```json
-{
- "type": "https://tools.ietf.org/html/rfc7807",
- "title": "Validation Error",
- "status": 400,
- "detail": "One or more validation errors occurred.",
- "errors": {
- "Name": ["Name is required"]
- }
-}
-```
-
-## Health Checks
-
-```mermaid
-graph TD
- HC[Health Check Endpoint]
- HC --> |/health/live| L[Liveness]
- HC --> |/health/ready| R[Readiness]
- HC --> |/health| F[Full Status]
-
- R --> PG[(PostgreSQL)]
- R --> RD[(Redis)]
-
- style HC fill:#3498db,stroke:#2980b9,color:#fff
- style L fill:#2ecc71,stroke:#27ae60,color:#fff
- style R fill:#f39c12,stroke:#d68910,color:#fff
-```
-
-## Deployment Architecture
-
-### Docker Compose (Local)
-
-```yaml
-services:
- myservice-api:
- build: .
- ports: ["5000:8080"]
- depends_on:
- - postgres
- - redis
-
- postgres:
- image: postgres:16-alpine
-
- redis:
- image: redis:7-alpine
-```
-
-### Kubernetes (Production)
-
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: myservice-api
-spec:
- replicas: 3
- template:
- spec:
- containers:
- - name: api
- image: myservice:latest
- ports:
- - containerPort: 8080
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8080
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8080
-```
-
-## Security Considerations
-
-1. **Authentication**: JWT Bearer token (configure in production)
-2. **Authorization**: Role-based access control
-3. **Input Validation**: FluentValidation on all requests
-4. **SQL Injection**: EF Core parameterized queries
-5. **Secrets**: Environment variables, never in code
-
-## Performance Optimization
-
-1. **Connection Pooling**: EF Core with Npgsql connection resilience
-2. **Async/Await**: All I/O operations are async
-3. **Response Caching**: Add caching headers for queries
-4. **Database Indexes**: Configure in EntityConfigurations
-
-## References
-
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
-- [.NET Microservices Architecture Guide](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
-- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
-- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
+| Service | Type | Purpose |
+|---------|------|---------|
+| **ads-serving** | RabbitMQ | Events |
+| **ads-tracking** | RabbitMQ | Conversions |
diff --git a/services/ads-analytics-service-net/docs/vi/ARCHITECTURE.md b/services/ads-analytics-service-net/docs/vi/ARCHITECTURE.md
index 55a5d13b..0663d0d2 100644
--- a/services/ads-analytics-service-net/docs/vi/ARCHITECTURE.md
+++ b/services/ads-analytics-service-net/docs/vi/ARCHITECTURE.md
@@ -1,271 +1,171 @@
-# Tài Liệu Kiến Trúc
+# Ads Analytics Service Architecture / Kiến Trúc Dịch Vụ Phân Tích Quảng Cáo
-> Tài liệu kiến trúc chi tiết cho Template Microservice .NET 10.
-
-## Tổng Quan Kiến Trúc
+## Tổng Quan / Overview
```mermaid
graph TB
- subgraph "Lớp API"
- C[Controllers]
- CMD[Commands]
- Q[Queries]
- B[Behaviors]
- V[Validations]
+ subgraph "Data Sources"
+ SERV[ads-serving-service]
+ TRACK[ads-tracking-service]
+ MGR[ads-manager-service]
end
- subgraph "Lớp Domain"
- AR[Aggregate Roots]
- E[Entities]
- VO[Value Objects]
- DE[Domain Events]
- DX[Domain Exceptions]
+ subgraph "ads-analytics-service"
+ API[API Layer]
+ INGEST[Ingestion Workers]
+ AGG[Aggregation Engine]
+ STORE[(ClickHouse/TimescaleDB)]
end
- subgraph "Lớp Infrastructure"
- DB[(PostgreSQL)]
- R[Repositories]
- CTX[DbContext]
- ID[Idempotency]
+ subgraph "Output"
+ DASH[Dashboard API]
+ REPORT[Report Generator]
+ EXPORT[Export Service]
end
- C --> CMD
- C --> Q
- CMD --> B --> V
- CMD --> AR
- Q --> R
- R --> CTX --> DB
- AR --> DE
- R --> AR
+ SERV -->|Impressions/Clicks| INGEST
+ TRACK -->|Conversions| INGEST
+ MGR -->|Campaign Metadata| API
- style C fill:#4a90d9,stroke:#2d5986,color:#fff
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
+ INGEST --> STORE
+ STORE --> AGG
+ AGG --> DASH
+ AGG --> REPORT
+ REPORT --> EXPORT
```
-## Trách Nhiệm Các Lớp
+## Domain Aggregates
-### 1. Lớp Domain (MyService.Domain)
+### MetricsAggregate
+- **CampaignMetrics** (Root): Impressions, Clicks, CTR, Spend
+- **AdSetMetrics**: Per-targeting performance
+- **AdMetrics**: Per-creative performance
-Trái tim của ứng dụng chứa business logic thuần túy. Lớp này:
-- Có **ZERO** phụ thuộc bên ngoài (ngoại trừ MediatR.Contracts cho events)
-- Chỉ chứa các class POCO
-- Triển khai các tactical patterns của DDD
+### ReportAggregate
+- **Report** (Root): Custom/scheduled reports
+- **ReportSchedule**: Daily, Weekly, Monthly
+- **ReportExport**: CSV, Excel, PDF formats
-#### Thành Phần
+### InsightAggregate
+- **AudienceInsight**: Demographics of reached users
+- **PerformanceInsight**: Optimization recommendations
-| Thành phần | Mục Đích |
-|------------|----------|
-| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
-| **AggregatesModel** | Aggregate roots với entities và value objects |
-| **Events** | Domain events cho giao tiếp cross-aggregate |
-| **Exceptions** | Domain exceptions cho vi phạm business rules |
+## Metrics Calculation
-### 2. Lớp Infrastructure (MyService.Infrastructure)
+| Metric | Formula | Description |
+|--------|---------|-------------|
+| **CTR** | Clicks / Impressions | Click-through rate |
+| **CPC** | Spend / Clicks | Cost per click |
+| **CPM** | (Spend / Impressions) × 1000 | Cost per mille |
+| **CPA** | Spend / Conversions | Cost per acquisition |
+| **ROAS** | Revenue / Spend | Return on ad spend |
+| **Frequency** | Impressions / Reach | Avg views per user |
-Triển khai kỹ thuật và các mối quan tâm bên ngoài:
-- Truy cập database (EF Core)
-- Triển khai repositories
-- Tích hợp external services
-
-### 3. Lớp API (MyService.API)
-
-Điểm vào ứng dụng và triển khai CQRS:
-- Controllers để xử lý HTTP
-- Commands cho các thao tác ghi
-- Queries cho các thao tác đọc
-- MediatR behaviors cho cross-cutting concerns
-
-## Luồng CQRS
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Controller
- participant MediatR
- participant LoggingBehavior
- participant ValidatorBehavior
- participant TransactionBehavior
- participant CommandHandler
- participant Repository
- participant DbContext
-
- Client->>Controller: HTTP Request
- Controller->>MediatR: Send(Command)
- MediatR->>LoggingBehavior: Handle
- LoggingBehavior->>ValidatorBehavior: Next()
- ValidatorBehavior->>TransactionBehavior: Next()
- TransactionBehavior->>CommandHandler: Next()
- CommandHandler->>Repository: Add/Update/Delete
- Repository->>DbContext: SaveEntitiesAsync()
- DbContext-->>Repository: Success
- Repository-->>CommandHandler: Result
- CommandHandler-->>Controller: Response
- Controller-->>Client: HTTP Response
-```
-
-## Domain Events
-
-```mermaid
-graph LR
- AR[Aggregate Root] -->|Phát sinh| DE[Domain Event]
- DE -->|Dispatch bởi| CTX[DbContext]
- CTX -->|Publish tới| M[MediatR]
- M -->|Xử lý bởi| H1[Handler 1]
- M -->|Xử lý bởi| H2[Handler 2]
-
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DE fill:#f39c12,stroke:#d68910,color:#fff
- style M fill:#9b59b6,stroke:#7d3c98,color:#fff
-```
-
-## Schema Database
-
-### Sample Aggregate
-
-```mermaid
-erDiagram
- samples {
- uuid id PK
- varchar(200) name
- varchar(1000) description
- int status_id FK
- timestamp created_at
- timestamp updated_at
- }
-
- sample_statuses {
- int id PK
- varchar(50) name
- }
-
- samples ||--o{ sample_statuses : has
-```
-
-## Pipeline MediatR
+## Data Flow
```
-Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
- │ │ │
- ▼ ▼ ▼
- Log start/end Validate Begin/Commit
- + timing với Transaction
- FluentValidation
+┌─────────────────────────────────────────────────────────────────┐
+│ DATA PIPELINE │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ Raw Events Aggregation Query │
+│ (RabbitMQ) (Background) (API) │
+│ │
+│ Impression ──┐ │
+│ │ ┌──────────────┐ ┌──────────────┐ │
+│ Click ───────┼───►│ Hourly │───►│ Campaign │ │
+│ │ │ Rollup │ │ Dashboard │ │
+│ Conversion ──┘ └──────────────┘ └──────────────┘ │
+│ │ │
+│ ▼ │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ Daily │───►│ Reports │ │
+│ │ Summary │ │ Export │ │
+│ └──────────────┘ └──────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
```
-### Thứ Tự Behaviors
+## Database Schema (ClickHouse)
-1. **LoggingBehavior** - Ghi log xử lý request với timing
-2. **ValidatorBehavior** - Validate request sử dụng FluentValidation
-3. **TransactionBehavior** - Bao bọc command handlers trong database transactions
+```sql
+-- Raw events (high write throughput)
+CREATE TABLE ad_events (
+ event_id UUID,
+ event_type Enum8('impression' = 1, 'click' = 2, 'conversion' = 3),
+ ad_id UUID,
+ campaign_id UUID,
+ advertiser_id UUID,
+ user_id String,
+ cost Decimal(18, 6),
+ event_time DateTime
+) ENGINE = MergeTree()
+PARTITION BY toYYYYMM(event_time)
+ORDER BY (campaign_id, event_time);
-## Xử Lý Lỗi
-
-### Phân Cấp Exceptions
-
-```
-Exception
-└── DomainException
- └── SampleDomainException
+-- Hourly aggregates (fast queries)
+CREATE MATERIALIZED VIEW campaign_metrics_hourly
+ENGINE = SummingMergeTree()
+PARTITION BY toYYYYMM(hour)
+ORDER BY (campaign_id, hour)
+AS SELECT
+ campaign_id,
+ toStartOfHour(event_time) AS hour,
+ countIf(event_type = 'impression') AS impressions,
+ countIf(event_type = 'click') AS clicks,
+ countIf(event_type = 'conversion') AS conversions,
+ sumIf(cost, event_type IN ('impression', 'click')) AS spend
+FROM ad_events
+GROUP BY campaign_id, hour;
```
-### Problem Details (RFC 7807)
+## API Endpoints
-Tất cả lỗi được trả về theo định dạng Problem Details:
+### Metrics
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/api/v1/ads-analytics/campaigns/{id}/metrics` | Campaign metrics |
+| `GET` | `/api/v1/ads-analytics/campaigns/{id}/breakdown` | Breakdown by dimension |
-```json
+### Reports
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `POST` | `/api/v1/ads-analytics/reports` | Create custom report |
+| `POST` | `/api/v1/ads-analytics/reports/schedule` | Schedule report |
+| `GET` | `/api/v1/ads-analytics/reports/{id}/export` | Export report |
+
+## Query Patterns
+
+```csharp
+///
+/// EN: Get campaign metrics with breakdown.
+/// VI: Lấy metrics chiến dịch với breakdown.
+///
+public async Task GetMetricsAsync(
+ Guid campaignId,
+ DateRange dateRange,
+ BreakdownDimension? breakdown = null)
{
- "type": "https://tools.ietf.org/html/rfc7807",
- "title": "Lỗi Validation",
- "status": 400,
- "detail": "Một hoặc nhiều lỗi validation đã xảy ra.",
- "errors": {
- "Name": ["Tên là bắt buộc"]
- }
+ var query = _clickHouse.Query(@"
+ SELECT
+ campaign_id,
+ sum(impressions) as impressions,
+ sum(clicks) as clicks,
+ sum(spend) as spend
+ FROM campaign_metrics_hourly
+ WHERE campaign_id = @campaignId
+ AND hour BETWEEN @start AND @end
+ GROUP BY campaign_id
+ ");
+
+ return await query.ExecuteAsync();
}
```
-## Health Checks
+## Integration
-```mermaid
-graph TD
- HC[Health Check Endpoint]
- HC --> |/health/live| L[Liveness]
- HC --> |/health/ready| R[Readiness]
- HC --> |/health| F[Full Status]
-
- R --> PG[(PostgreSQL)]
- R --> RD[(Redis)]
-
- style HC fill:#3498db,stroke:#2980b9,color:#fff
- style L fill:#2ecc71,stroke:#27ae60,color:#fff
- style R fill:#f39c12,stroke:#d68910,color:#fff
-```
-
-## Kiến Trúc Deployment
-
-### Docker Compose (Local)
-
-```yaml
-services:
- myservice-api:
- build: .
- ports: ["5000:8080"]
- depends_on:
- - postgres
- - redis
-
- postgres:
- image: postgres:16-alpine
-
- redis:
- image: redis:7-alpine
-```
-
-### Kubernetes (Production)
-
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: myservice-api
-spec:
- replicas: 3
- template:
- spec:
- containers:
- - name: api
- image: myservice:latest
- ports:
- - containerPort: 8080
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8080
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8080
-```
-
-## Cân Nhắc Bảo Mật
-
-1. **Authentication**: JWT Bearer token (cấu hình trong production)
-2. **Authorization**: Role-based access control
-3. **Input Validation**: FluentValidation trên tất cả requests
-4. **SQL Injection**: EF Core parameterized queries
-5. **Secrets**: Biến môi trường, không bao giờ trong code
-
-## Tối Ưu Hiệu Năng
-
-1. **Connection Pooling**: EF Core với Npgsql connection resilience
-2. **Async/Await**: Tất cả I/O operations đều async
-3. **Response Caching**: Thêm caching headers cho queries
-4. **Database Indexes**: Cấu hình trong EntityConfigurations
-
-## Tài Liệu Tham Khảo
-
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
-- [Hướng dẫn Kiến trúc .NET Microservices](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
-- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
-- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
+| Service | Integration Type | Purpose |
+|---------|-----------------|---------|
+| **ads-serving-service** | RabbitMQ Consumer | Impression/Click events |
+| **ads-tracking-service** | RabbitMQ Consumer | Conversion events |
+| **ads-manager-service** | HTTP Client | Campaign metadata |
diff --git a/services/ads-billing-service-net/docs/en/ARCHITECTURE.md b/services/ads-billing-service-net/docs/en/ARCHITECTURE.md
index 9d80ba57..dc2a8b71 100644
--- a/services/ads-billing-service-net/docs/en/ARCHITECTURE.md
+++ b/services/ads-billing-service-net/docs/en/ARCHITECTURE.md
@@ -1,271 +1,40 @@
-# Architecture Documentation
+# Ads Billing Service Architecture
-> Detailed architecture documentation for the .NET 10 Microservice Template.
-
-## Architecture Overview
+## Overview
```mermaid
graph TB
- subgraph "API Layer"
- C[Controllers]
- CMD[Commands]
- Q[Queries]
- B[Behaviors]
- V[Validations]
- end
-
- subgraph "Domain Layer"
- AR[Aggregate Roots]
- E[Entities]
- VO[Value Objects]
- DE[Domain Events]
- DX[Domain Exceptions]
- end
-
- subgraph "Infrastructure Layer"
- DB[(PostgreSQL)]
- R[Repositories]
- CTX[DbContext]
- ID[Idempotency]
- end
-
- C --> CMD
- C --> Q
- CMD --> B --> V
- CMD --> AR
- Q --> R
- R --> CTX --> DB
- AR --> DE
- R --> AR
-
- style C fill:#4a90d9,stroke:#2d5986,color:#fff
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
+ SERVE[ads-serving] -->|Charges| MQ[RabbitMQ]
+ MQ --> BILL[ads-billing]
+ BILL -->|Debit| WALLET[Wallet Service]
```
-## Layer Responsibilities
+## Domain Aggregates
-### 1. Domain Layer (MyService.Domain)
+- **BillingAccount**: Payment settings, thresholds
+- **CreditLine**: Postpaid limits
+- **Invoice**: Monthly/threshold invoices
+- **AdCharge**: Individual costs
-The heart of the application containing pure business logic. This layer:
-- Has **ZERO** external dependencies (except MediatR.Contracts for events)
-- Contains only POCO classes
-- Implements DDD tactical patterns
+## Billing Flow
-#### Components
+| Type | Flow |
+|------|------|
+| **Prepaid** | Add Funds → Run Ads → Deduct → Pause when $0 |
+| **Postpaid** | Run Ads → Accumulate → Threshold → Auto-charge |
-| Component | Purpose |
-|-----------|---------|
-| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
-| **AggregatesModel** | Aggregate roots with their entities and value objects |
-| **Events** | Domain events for cross-aggregate communication |
-| **Exceptions** | Domain-specific exceptions for business rule violations |
+## Thresholds
-### 2. Infrastructure Layer (MyService.Infrastructure)
+| Level | Condition |
+|-------|-----------|
+| $25 | New account |
+| $50 | 2 payments on-time |
+| $250 | 5 payments on-time |
+| $500 | High trust |
-Technical implementations and external concerns:
-- Database access (EF Core)
-- Repository implementations
-- External service integrations
+## Integration
-### 3. API Layer (MyService.API)
-
-Application entry point and CQRS implementation:
-- Controllers for HTTP handling
-- Commands for write operations
-- Queries for read operations
-- MediatR behaviors for cross-cutting concerns
-
-## CQRS Flow
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Controller
- participant MediatR
- participant LoggingBehavior
- participant ValidatorBehavior
- participant TransactionBehavior
- participant CommandHandler
- participant Repository
- participant DbContext
-
- Client->>Controller: HTTP Request
- Controller->>MediatR: Send(Command)
- MediatR->>LoggingBehavior: Handle
- LoggingBehavior->>ValidatorBehavior: Next()
- ValidatorBehavior->>TransactionBehavior: Next()
- TransactionBehavior->>CommandHandler: Next()
- CommandHandler->>Repository: Add/Update/Delete
- Repository->>DbContext: SaveEntitiesAsync()
- DbContext-->>Repository: Success
- Repository-->>CommandHandler: Result
- CommandHandler-->>Controller: Response
- Controller-->>Client: HTTP Response
-```
-
-## Domain Events
-
-```mermaid
-graph LR
- AR[Aggregate Root] -->|Raises| DE[Domain Event]
- DE -->|Dispatched by| CTX[DbContext]
- CTX -->|Publishes to| M[MediatR]
- M -->|Handled by| H1[Handler 1]
- M -->|Handled by| H2[Handler 2]
-
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DE fill:#f39c12,stroke:#d68910,color:#fff
- style M fill:#9b59b6,stroke:#7d3c98,color:#fff
-```
-
-## Database Schema
-
-### Sample Aggregate
-
-```mermaid
-erDiagram
- samples {
- uuid id PK
- varchar(200) name
- varchar(1000) description
- int status_id FK
- timestamp created_at
- timestamp updated_at
- }
-
- sample_statuses {
- int id PK
- varchar(50) name
- }
-
- samples ||--o{ sample_statuses : has
-```
-
-## MediatR Pipeline
-
-```
-Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
- │ │ │
- ▼ ▼ ▼
- Log start/end Validate Begin/Commit
- + timing with Transaction
- FluentValidation
-```
-
-### Behavior Order
-
-1. **LoggingBehavior** - Logs request handling with timing
-2. **ValidatorBehavior** - Validates request using FluentValidation
-3. **TransactionBehavior** - Wraps command handlers in database transactions
-
-## Error Handling
-
-### Exception Hierarchy
-
-```
-Exception
-└── DomainException
- └── SampleDomainException
-```
-
-### Problem Details (RFC 7807)
-
-All errors are returned in Problem Details format:
-
-```json
-{
- "type": "https://tools.ietf.org/html/rfc7807",
- "title": "Validation Error",
- "status": 400,
- "detail": "One or more validation errors occurred.",
- "errors": {
- "Name": ["Name is required"]
- }
-}
-```
-
-## Health Checks
-
-```mermaid
-graph TD
- HC[Health Check Endpoint]
- HC --> |/health/live| L[Liveness]
- HC --> |/health/ready| R[Readiness]
- HC --> |/health| F[Full Status]
-
- R --> PG[(PostgreSQL)]
- R --> RD[(Redis)]
-
- style HC fill:#3498db,stroke:#2980b9,color:#fff
- style L fill:#2ecc71,stroke:#27ae60,color:#fff
- style R fill:#f39c12,stroke:#d68910,color:#fff
-```
-
-## Deployment Architecture
-
-### Docker Compose (Local)
-
-```yaml
-services:
- myservice-api:
- build: .
- ports: ["5000:8080"]
- depends_on:
- - postgres
- - redis
-
- postgres:
- image: postgres:16-alpine
-
- redis:
- image: redis:7-alpine
-```
-
-### Kubernetes (Production)
-
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: myservice-api
-spec:
- replicas: 3
- template:
- spec:
- containers:
- - name: api
- image: myservice:latest
- ports:
- - containerPort: 8080
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8080
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8080
-```
-
-## Security Considerations
-
-1. **Authentication**: JWT Bearer token (configure in production)
-2. **Authorization**: Role-based access control
-3. **Input Validation**: FluentValidation on all requests
-4. **SQL Injection**: EF Core parameterized queries
-5. **Secrets**: Environment variables, never in code
-
-## Performance Optimization
-
-1. **Connection Pooling**: EF Core with Npgsql connection resilience
-2. **Async/Await**: All I/O operations are async
-3. **Response Caching**: Add caching headers for queries
-4. **Database Indexes**: Configure in EntityConfigurations
-
-## References
-
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
-- [.NET Microservices Architecture Guide](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
-- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
-- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
+| Service | Purpose |
+|---------|---------|
+| **ads-serving** | Consume charges |
+| **Wallet Service** | Process payments |
diff --git a/services/ads-billing-service-net/docs/vi/ARCHITECTURE.md b/services/ads-billing-service-net/docs/vi/ARCHITECTURE.md
index 55a5d13b..6db27ebd 100644
--- a/services/ads-billing-service-net/docs/vi/ARCHITECTURE.md
+++ b/services/ads-billing-service-net/docs/vi/ARCHITECTURE.md
@@ -1,271 +1,165 @@
-# Tài Liệu Kiến Trúc
+# Ads Billing Service Architecture / Kiến Trúc Dịch Vụ Thanh Toán Quảng Cáo
-> Tài liệu kiến trúc chi tiết cho Template Microservice .NET 10.
-
-## Tổng Quan Kiến Trúc
+## Tổng Quan / Overview
```mermaid
graph TB
- subgraph "Lớp API"
- C[Controllers]
- CMD[Commands]
- Q[Queries]
- B[Behaviors]
- V[Validations]
+ subgraph "ads-billing-service"
+ API[API Layer]
+ APP[Application Layer]
+ DOM[Domain Layer]
+ INF[Infrastructure Layer]
end
- subgraph "Lớp Domain"
- AR[Aggregate Roots]
- E[Entities]
- VO[Value Objects]
- DE[Domain Events]
- DX[Domain Exceptions]
+ subgraph "External Services"
+ SERVE[ads-serving-service]
+ WALLET[Wallet Service]
+ MQ[RabbitMQ]
end
- subgraph "Lớp Infrastructure"
- DB[(PostgreSQL)]
- R[Repositories]
- CTX[DbContext]
- ID[Idempotency]
- end
-
- C --> CMD
- C --> Q
- CMD --> B --> V
- CMD --> AR
- Q --> R
- R --> CTX --> DB
- AR --> DE
- R --> AR
-
- style C fill:#4a90d9,stroke:#2d5986,color:#fff
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
+ SERVE -->|Charge Events| MQ
+ MQ --> INF
+ INF --> DOM
+ DOM -->|Debit Request| MQ
+ MQ --> WALLET
```
-## Trách Nhiệm Các Lớp
+## Domain Aggregates
-### 1. Lớp Domain (MyService.Domain)
+### BillingAccountAggregate
+- **BillingAccount** (Root): Links to Wallet, payment settings
+- **PaymentMethod**: Credit Card, Bank, Wallet
+- **BillingThreshold**: $25, $50, $250, $500
-Trái tim của ứng dụng chứa business logic thuần túy. Lớp này:
-- Có **ZERO** phụ thuộc bên ngoài (ngoại trừ MediatR.Contracts cho events)
-- Chỉ chứa các class POCO
-- Triển khai các tactical patterns của DDD
+### CreditLineAggregate
+- **CreditLine** (Root): Postpaid limit
+- **CreditEvaluation**: Trust score calculation
+- **PaymentHistory**: Track payment reliability
-#### Thành Phần
+### InvoiceAggregate
+- **Invoice** (Root): Monthly/threshold invoice
+- **InvoiceLineItem**: Per-campaign charges
+- **TaxCalculation**: VAT by region
-| Thành phần | Mục Đích |
-|------------|----------|
-| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
-| **AggregatesModel** | Aggregate roots với entities và value objects |
-| **Events** | Domain events cho giao tiếp cross-aggregate |
-| **Exceptions** | Domain exceptions cho vi phạm business rules |
+### ChargeAggregate
+- **AdCharge**: Individual impression/click cost
+- **DailySpendSummary**: Aggregated daily charges
-### 2. Lớp Infrastructure (MyService.Infrastructure)
-
-Triển khai kỹ thuật và các mối quan tâm bên ngoài:
-- Truy cập database (EF Core)
-- Triển khai repositories
-- Tích hợp external services
-
-### 3. Lớp API (MyService.API)
-
-Điểm vào ứng dụng và triển khai CQRS:
-- Controllers để xử lý HTTP
-- Commands cho các thao tác ghi
-- Queries cho các thao tác đọc
-- MediatR behaviors cho cross-cutting concerns
-
-## Luồng CQRS
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Controller
- participant MediatR
- participant LoggingBehavior
- participant ValidatorBehavior
- participant TransactionBehavior
- participant CommandHandler
- participant Repository
- participant DbContext
-
- Client->>Controller: HTTP Request
- Controller->>MediatR: Send(Command)
- MediatR->>LoggingBehavior: Handle
- LoggingBehavior->>ValidatorBehavior: Next()
- ValidatorBehavior->>TransactionBehavior: Next()
- TransactionBehavior->>CommandHandler: Next()
- CommandHandler->>Repository: Add/Update/Delete
- Repository->>DbContext: SaveEntitiesAsync()
- DbContext-->>Repository: Success
- Repository-->>CommandHandler: Result
- CommandHandler-->>Controller: Response
- Controller-->>Client: HTTP Response
-```
-
-## Domain Events
-
-```mermaid
-graph LR
- AR[Aggregate Root] -->|Phát sinh| DE[Domain Event]
- DE -->|Dispatch bởi| CTX[DbContext]
- CTX -->|Publish tới| M[MediatR]
- M -->|Xử lý bởi| H1[Handler 1]
- M -->|Xử lý bởi| H2[Handler 2]
-
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DE fill:#f39c12,stroke:#d68910,color:#fff
- style M fill:#9b59b6,stroke:#7d3c98,color:#fff
-```
-
-## Schema Database
-
-### Sample Aggregate
-
-```mermaid
-erDiagram
- samples {
- uuid id PK
- varchar(200) name
- varchar(1000) description
- int status_id FK
- timestamp created_at
- timestamp updated_at
- }
-
- sample_statuses {
- int id PK
- varchar(50) name
- }
-
- samples ||--o{ sample_statuses : has
-```
-
-## Pipeline MediatR
+## Billing Flow
```
-Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
- │ │ │
- ▼ ▼ ▼
- Log start/end Validate Begin/Commit
- + timing với Transaction
- FluentValidation
+┌─────────────────────────────────────────────────────────────────┐
+│ BILLING FLOW │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ PREPAID POSTPAID │
+│ ──────── ──────── │
+│ │
+│ 1. Add Funds ──► Wallet 1. Run Ads ──► Accumulate │
+│ 2. Run Ads ──► Deduct 2. Reach Threshold │
+│ 3. Balance = 0 ──► Pause 3. Auto-charge ──► Invoice │
+│ │
+└─────────────────────────────────────────────────────────────────┘
```
-### Thứ Tự Behaviors
+## Database Schema
-1. **LoggingBehavior** - Ghi log xử lý request với timing
-2. **ValidatorBehavior** - Validate request sử dụng FluentValidation
-3. **TransactionBehavior** - Bao bọc command handlers trong database transactions
+```sql
+-- Billing Accounts
+CREATE TABLE billing_accounts (
+ id UUID PRIMARY KEY,
+ advertiser_id UUID NOT NULL,
+ wallet_id UUID,
+ billing_type VARCHAR(20), -- 'PREPAID' | 'POSTPAID'
+ threshold_level INT DEFAULT 25,
+ current_balance DECIMAL(18, 2),
+ credit_limit DECIMAL(18, 2),
+ created_at TIMESTAMP DEFAULT NOW()
+);
-## Xử Lý Lỗi
+-- Charges (high volume)
+CREATE TABLE ad_charges (
+ id UUID PRIMARY KEY,
+ billing_account_id UUID REFERENCES billing_accounts(id),
+ campaign_id UUID NOT NULL,
+ ad_id UUID NOT NULL,
+ charge_type VARCHAR(20), -- 'IMPRESSION' | 'CLICK'
+ amount DECIMAL(18, 6),
+ charged_at TIMESTAMP DEFAULT NOW()
+);
-### Phân Cấp Exceptions
-
-```
-Exception
-└── DomainException
- └── SampleDomainException
+-- Invoices
+CREATE TABLE invoices (
+ id UUID PRIMARY KEY,
+ billing_account_id UUID REFERENCES billing_accounts(id),
+ invoice_number VARCHAR(50) UNIQUE,
+ period_start DATE,
+ period_end DATE,
+ subtotal DECIMAL(18, 2),
+ tax_amount DECIMAL(18, 2),
+ total DECIMAL(18, 2),
+ status VARCHAR(20), -- 'DRAFT' | 'ISSUED' | 'PAID'
+ issued_at TIMESTAMP
+);
```
-### Problem Details (RFC 7807)
+## Threshold Billing Logic
-Tất cả lỗi được trả về theo định dạng Problem Details:
-
-```json
+```csharp
+///
+/// EN: Check and trigger threshold billing.
+/// VI: Kiểm tra và kích hoạt billing theo ngưỡng.
+///
+public async Task ProcessChargeAsync(AdCharge charge)
{
- "type": "https://tools.ietf.org/html/rfc7807",
- "title": "Lỗi Validation",
- "status": 400,
- "detail": "Một hoặc nhiều lỗi validation đã xảy ra.",
- "errors": {
- "Name": ["Tên là bắt buộc"]
- }
+ var account = await _accountRepository.GetAsync(charge.BillingAccountId);
+
+ // EN: Accumulate spend
+ account.AddCharge(charge.Amount);
+
+ // EN: Check threshold
+ if (account.BillingType == BillingType.Postpaid
+ && account.PendingBalance >= account.ThresholdLevel)
+ {
+ // EN: Trigger auto-charge
+ await _publisher.Publish(new WalletDebitRequestedEvent
+ {
+ WalletId = account.WalletId,
+ Amount = account.PendingBalance,
+ Description = $"Ads spend threshold reached",
+ ReferenceId = account.Id,
+ ReferenceType = "ADS_BILLING"
+ });
+
+ // EN: Generate invoice
+ var invoice = account.GenerateInvoice();
+ await _invoiceRepository.AddAsync(invoice);
+
+ account.ResetPendingBalance();
+ }
+
+ await _accountRepository.UnitOfWork.SaveChangesAsync();
}
```
-## Health Checks
+## Integration Events
-```mermaid
-graph TD
- HC[Health Check Endpoint]
- HC --> |/health/live| L[Liveness]
- HC --> |/health/ready| R[Readiness]
- HC --> |/health| F[Full Status]
-
- R --> PG[(PostgreSQL)]
- R --> RD[(Redis)]
-
- style HC fill:#3498db,stroke:#2980b9,color:#fff
- style L fill:#2ecc71,stroke:#27ae60,color:#fff
- style R fill:#f39c12,stroke:#d68910,color:#fff
+```csharp
+// Consume from ads-serving
+public record AdImpressionChargedEvent(
+ Guid AdId, Guid CampaignId, Guid AdvertiserId,
+ decimal Cost, DateTime ChargedAt);
+
+// Publish to wallet-service
+public record WalletDebitRequestedEvent(
+ Guid WalletId, decimal Amount, string Description,
+ Guid ReferenceId, string ReferenceType);
```
-## Kiến Trúc Deployment
+## API Endpoints
-### Docker Compose (Local)
-
-```yaml
-services:
- myservice-api:
- build: .
- ports: ["5000:8080"]
- depends_on:
- - postgres
- - redis
-
- postgres:
- image: postgres:16-alpine
-
- redis:
- image: redis:7-alpine
-```
-
-### Kubernetes (Production)
-
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: myservice-api
-spec:
- replicas: 3
- template:
- spec:
- containers:
- - name: api
- image: myservice:latest
- ports:
- - containerPort: 8080
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8080
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8080
-```
-
-## Cân Nhắc Bảo Mật
-
-1. **Authentication**: JWT Bearer token (cấu hình trong production)
-2. **Authorization**: Role-based access control
-3. **Input Validation**: FluentValidation trên tất cả requests
-4. **SQL Injection**: EF Core parameterized queries
-5. **Secrets**: Biến môi trường, không bao giờ trong code
-
-## Tối Ưu Hiệu Năng
-
-1. **Connection Pooling**: EF Core với Npgsql connection resilience
-2. **Async/Await**: Tất cả I/O operations đều async
-3. **Response Caching**: Thêm caching headers cho queries
-4. **Database Indexes**: Cấu hình trong EntityConfigurations
-
-## Tài Liệu Tham Khảo
-
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
-- [Hướng dẫn Kiến trúc .NET Microservices](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
-- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
-- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/api/v1/ads-billing/accounts/{id}` | Account info |
+| `POST` | `/api/v1/ads-billing/accounts/{id}/add-funds` | Add funds |
+| `GET` | `/api/v1/ads-billing/invoices` | List invoices |
diff --git a/services/ads-tracking-service-net/docs/en/ARCHITECTURE.md b/services/ads-tracking-service-net/docs/en/ARCHITECTURE.md
index 9d80ba57..ea87f6f1 100644
--- a/services/ads-tracking-service-net/docs/en/ARCHITECTURE.md
+++ b/services/ads-tracking-service-net/docs/en/ARCHITECTURE.md
@@ -1,271 +1,45 @@
-# Architecture Documentation
+# Ads Tracking Service Architecture
-> Detailed architecture documentation for the .NET 10 Microservice Template.
-
-## Architecture Overview
+## Overview
```mermaid
graph TB
- subgraph "API Layer"
- C[Controllers]
- CMD[Commands]
- Q[Queries]
- B[Behaviors]
- V[Validations]
- end
-
- subgraph "Domain Layer"
- AR[Aggregate Roots]
- E[Entities]
- VO[Value Objects]
- DE[Domain Events]
- DX[Domain Exceptions]
- end
-
- subgraph "Infrastructure Layer"
- DB[(PostgreSQL)]
- R[Repositories]
- CTX[DbContext]
- ID[Idempotency]
- end
-
- C --> CMD
- C --> Q
- CMD --> B --> V
- CMD --> AR
- Q --> R
- R --> CTX --> DB
- AR --> DE
- R --> AR
-
- style C fill:#4a90d9,stroke:#2d5986,color:#fff
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
+ PIX[Pixel] --> EVT[Event Ingestion]
+ SDK[SDK] --> EVT
+ EVT --> ATTR[Attribution]
+ ATTR --> ANAL[ads-analytics]
```
-## Layer Responsibilities
+## Domain Aggregates
-### 1. Domain Layer (MyService.Domain)
+- **TrackingPixel**: Per-advertiser pixel code
+- **PixelEvent**: PageView, Purchase, Lead
+- **Conversion**: Attributed conversion
+- **Attribution**: Links conversion to ad
-The heart of the application containing pure business logic. This layer:
-- Has **ZERO** external dependencies (except MediatR.Contracts for events)
-- Contains only POCO classes
-- Implements DDD tactical patterns
+## Attribution Models
-#### Components
+| Model | Description |
+|-------|-------------|
+| **Last-click** | Credit to last clicked ad |
+| **First-click** | Credit to first clicked ad |
+| **Linear** | Equal credit to all touchpoints |
-| Component | Purpose |
-|-----------|---------|
-| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
-| **AggregatesModel** | Aggregate roots with their entities and value objects |
-| **Events** | Domain events for cross-aggregate communication |
-| **Exceptions** | Domain-specific exceptions for business rule violations |
+## Windows
-### 2. Infrastructure Layer (MyService.Infrastructure)
+| Setting | Click | View |
+|---------|-------|------|
+| **7/1** | 7 days | 1 day |
+| **28/1** | 28 days | 1 day |
-Technical implementations and external concerns:
-- Database access (EF Core)
-- Repository implementations
-- External service integrations
+## Database (TimescaleDB)
-### 3. API Layer (MyService.API)
-
-Application entry point and CQRS implementation:
-- Controllers for HTTP handling
-- Commands for write operations
-- Queries for read operations
-- MediatR behaviors for cross-cutting concerns
-
-## CQRS Flow
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Controller
- participant MediatR
- participant LoggingBehavior
- participant ValidatorBehavior
- participant TransactionBehavior
- participant CommandHandler
- participant Repository
- participant DbContext
-
- Client->>Controller: HTTP Request
- Controller->>MediatR: Send(Command)
- MediatR->>LoggingBehavior: Handle
- LoggingBehavior->>ValidatorBehavior: Next()
- ValidatorBehavior->>TransactionBehavior: Next()
- TransactionBehavior->>CommandHandler: Next()
- CommandHandler->>Repository: Add/Update/Delete
- Repository->>DbContext: SaveEntitiesAsync()
- DbContext-->>Repository: Success
- Repository-->>CommandHandler: Result
- CommandHandler-->>Controller: Response
- Controller-->>Client: HTTP Response
+```sql
+CREATE TABLE pixel_events (
+ event_type VARCHAR(50),
+ event_data JSONB,
+ user_id VARCHAR(255),
+ event_time TIMESTAMPTZ
+);
+SELECT create_hypertable('pixel_events', 'event_time');
```
-
-## Domain Events
-
-```mermaid
-graph LR
- AR[Aggregate Root] -->|Raises| DE[Domain Event]
- DE -->|Dispatched by| CTX[DbContext]
- CTX -->|Publishes to| M[MediatR]
- M -->|Handled by| H1[Handler 1]
- M -->|Handled by| H2[Handler 2]
-
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DE fill:#f39c12,stroke:#d68910,color:#fff
- style M fill:#9b59b6,stroke:#7d3c98,color:#fff
-```
-
-## Database Schema
-
-### Sample Aggregate
-
-```mermaid
-erDiagram
- samples {
- uuid id PK
- varchar(200) name
- varchar(1000) description
- int status_id FK
- timestamp created_at
- timestamp updated_at
- }
-
- sample_statuses {
- int id PK
- varchar(50) name
- }
-
- samples ||--o{ sample_statuses : has
-```
-
-## MediatR Pipeline
-
-```
-Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
- │ │ │
- ▼ ▼ ▼
- Log start/end Validate Begin/Commit
- + timing with Transaction
- FluentValidation
-```
-
-### Behavior Order
-
-1. **LoggingBehavior** - Logs request handling with timing
-2. **ValidatorBehavior** - Validates request using FluentValidation
-3. **TransactionBehavior** - Wraps command handlers in database transactions
-
-## Error Handling
-
-### Exception Hierarchy
-
-```
-Exception
-└── DomainException
- └── SampleDomainException
-```
-
-### Problem Details (RFC 7807)
-
-All errors are returned in Problem Details format:
-
-```json
-{
- "type": "https://tools.ietf.org/html/rfc7807",
- "title": "Validation Error",
- "status": 400,
- "detail": "One or more validation errors occurred.",
- "errors": {
- "Name": ["Name is required"]
- }
-}
-```
-
-## Health Checks
-
-```mermaid
-graph TD
- HC[Health Check Endpoint]
- HC --> |/health/live| L[Liveness]
- HC --> |/health/ready| R[Readiness]
- HC --> |/health| F[Full Status]
-
- R --> PG[(PostgreSQL)]
- R --> RD[(Redis)]
-
- style HC fill:#3498db,stroke:#2980b9,color:#fff
- style L fill:#2ecc71,stroke:#27ae60,color:#fff
- style R fill:#f39c12,stroke:#d68910,color:#fff
-```
-
-## Deployment Architecture
-
-### Docker Compose (Local)
-
-```yaml
-services:
- myservice-api:
- build: .
- ports: ["5000:8080"]
- depends_on:
- - postgres
- - redis
-
- postgres:
- image: postgres:16-alpine
-
- redis:
- image: redis:7-alpine
-```
-
-### Kubernetes (Production)
-
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: myservice-api
-spec:
- replicas: 3
- template:
- spec:
- containers:
- - name: api
- image: myservice:latest
- ports:
- - containerPort: 8080
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8080
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8080
-```
-
-## Security Considerations
-
-1. **Authentication**: JWT Bearer token (configure in production)
-2. **Authorization**: Role-based access control
-3. **Input Validation**: FluentValidation on all requests
-4. **SQL Injection**: EF Core parameterized queries
-5. **Secrets**: Environment variables, never in code
-
-## Performance Optimization
-
-1. **Connection Pooling**: EF Core with Npgsql connection resilience
-2. **Async/Await**: All I/O operations are async
-3. **Response Caching**: Add caching headers for queries
-4. **Database Indexes**: Configure in EntityConfigurations
-
-## References
-
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
-- [.NET Microservices Architecture Guide](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
-- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
-- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
diff --git a/services/ads-tracking-service-net/docs/vi/ARCHITECTURE.md b/services/ads-tracking-service-net/docs/vi/ARCHITECTURE.md
index 55a5d13b..64c61fdb 100644
--- a/services/ads-tracking-service-net/docs/vi/ARCHITECTURE.md
+++ b/services/ads-tracking-service-net/docs/vi/ARCHITECTURE.md
@@ -1,271 +1,205 @@
-# Tài Liệu Kiến Trúc
+# Ads Tracking Service Architecture / Kiến Trúc Dịch Vụ Theo Dõi Quảng Cáo
-> Tài liệu kiến trúc chi tiết cho Template Microservice .NET 10.
-
-## Tổng Quan Kiến Trúc
+## Tổng Quan / Overview
```mermaid
graph TB
- subgraph "Lớp API"
- C[Controllers]
- CMD[Commands]
- Q[Queries]
- B[Behaviors]
- V[Validations]
+ subgraph "Tracking Sources"
+ PIX[Website Pixel]
+ SDK[Mobile SDK]
+ API[Server-side API]
end
- subgraph "Lớp Domain"
- AR[Aggregate Roots]
- E[Entities]
- VO[Value Objects]
- DE[Domain Events]
- DX[Domain Exceptions]
+ subgraph "ads-tracking-service"
+ EVT[Event Ingestion]
+ ATTR[Attribution Engine]
+ STORE[(TimescaleDB)]
end
- subgraph "Lớp Infrastructure"
- DB[(PostgreSQL)]
- R[Repositories]
- CTX[DbContext]
- ID[Idempotency]
+ subgraph "Output"
+ ANAL[ads-analytics]
+ BILL[ads-billing]
end
- C --> CMD
- C --> Q
- CMD --> B --> V
- CMD --> AR
- Q --> R
- R --> CTX --> DB
- AR --> DE
- R --> AR
+ PIX --> EVT
+ SDK --> EVT
+ API --> EVT
- style C fill:#4a90d9,stroke:#2d5986,color:#fff
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
+ EVT --> STORE
+ EVT --> ATTR
+ ATTR --> ANAL
+ ATTR --> BILL
```
-## Trách Nhiệm Các Lớp
+## Domain Aggregates
-### 1. Lớp Domain (MyService.Domain)
+### TrackingPixelAggregate
+- **TrackingPixel** (Root): Unique pixel per advertiser
+- **PixelEvent**: PageView, AddToCart, Purchase, Lead
-Trái tim của ứng dụng chứa business logic thuần túy. Lớp này:
-- Có **ZERO** phụ thuộc bên ngoài (ngoại trừ MediatR.Contracts cho events)
-- Chỉ chứa các class POCO
-- Triển khai các tactical patterns của DDD
+### ConversionAggregate
+- **Conversion** (Root): Actual conversion event
+- **ConversionValue**: Revenue amount
+- **ConversionWindow**: 1-day, 7-day, 28-day
-#### Thành Phần
+### AttributionAggregate
+- **Attribution** (Root): Links conversion to ad
+- **AttributionModel**: Last-click, First-click, Linear
+- **TouchPoint**: Each ad interaction in journey
-| Thành phần | Mục Đích |
-|------------|----------|
-| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
-| **AggregatesModel** | Aggregate roots với entities và value objects |
-| **Events** | Domain events cho giao tiếp cross-aggregate |
-| **Exceptions** | Domain exceptions cho vi phạm business rules |
-
-### 2. Lớp Infrastructure (MyService.Infrastructure)
-
-Triển khai kỹ thuật và các mối quan tâm bên ngoài:
-- Truy cập database (EF Core)
-- Triển khai repositories
-- Tích hợp external services
-
-### 3. Lớp API (MyService.API)
-
-Điểm vào ứng dụng và triển khai CQRS:
-- Controllers để xử lý HTTP
-- Commands cho các thao tác ghi
-- Queries cho các thao tác đọc
-- MediatR behaviors cho cross-cutting concerns
-
-## Luồng CQRS
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Controller
- participant MediatR
- participant LoggingBehavior
- participant ValidatorBehavior
- participant TransactionBehavior
- participant CommandHandler
- participant Repository
- participant DbContext
-
- Client->>Controller: HTTP Request
- Controller->>MediatR: Send(Command)
- MediatR->>LoggingBehavior: Handle
- LoggingBehavior->>ValidatorBehavior: Next()
- ValidatorBehavior->>TransactionBehavior: Next()
- TransactionBehavior->>CommandHandler: Next()
- CommandHandler->>Repository: Add/Update/Delete
- Repository->>DbContext: SaveEntitiesAsync()
- DbContext-->>Repository: Success
- Repository-->>CommandHandler: Result
- CommandHandler-->>Controller: Response
- Controller-->>Client: HTTP Response
-```
-
-## Domain Events
-
-```mermaid
-graph LR
- AR[Aggregate Root] -->|Phát sinh| DE[Domain Event]
- DE -->|Dispatch bởi| CTX[DbContext]
- CTX -->|Publish tới| M[MediatR]
- M -->|Xử lý bởi| H1[Handler 1]
- M -->|Xử lý bởi| H2[Handler 2]
-
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DE fill:#f39c12,stroke:#d68910,color:#fff
- style M fill:#9b59b6,stroke:#7d3c98,color:#fff
-```
-
-## Schema Database
-
-### Sample Aggregate
-
-```mermaid
-erDiagram
- samples {
- uuid id PK
- varchar(200) name
- varchar(1000) description
- int status_id FK
- timestamp created_at
- timestamp updated_at
- }
-
- sample_statuses {
- int id PK
- varchar(50) name
- }
-
- samples ||--o{ sample_statuses : has
-```
-
-## Pipeline MediatR
+## Attribution Logic
```
-Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
- │ │ │
- ▼ ▼ ▼
- Log start/end Validate Begin/Commit
- + timing với Transaction
- FluentValidation
+┌─────────────────────────────────────────────────────────────────┐
+│ ATTRIBUTION FLOW │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ User Journey: │
+│ ──────────── │
+│ Day 1: See Ad A (impression) │
+│ Day 3: Click Ad B │
+│ Day 5: Click Ad C │
+│ Day 6: Purchase $100 │
+│ │
+│ Attribution Models: │
+│ ────────────────── │
+│ Last-click: Ad C = 100% │
+│ First-click: Ad B = 100% │
+│ Linear: Ad B = 50%, Ad C = 50% │
+│ Time-decay: Ad B = 25%, Ad C = 75% │
+│ │
+└─────────────────────────────────────────────────────────────────┘
```
-### Thứ Tự Behaviors
+## Database Schema (TimescaleDB)
-1. **LoggingBehavior** - Ghi log xử lý request với timing
-2. **ValidatorBehavior** - Validate request sử dụng FluentValidation
-3. **TransactionBehavior** - Bao bọc command handlers trong database transactions
+```sql
+-- Pixels
+CREATE TABLE tracking_pixels (
+ id UUID PRIMARY KEY,
+ advertiser_id UUID NOT NULL,
+ pixel_code VARCHAR(50) UNIQUE,
+ domain VARCHAR(255),
+ created_at TIMESTAMP DEFAULT NOW()
+);
-## Xử Lý Lỗi
+-- Events (hypertable for time-series)
+CREATE TABLE pixel_events (
+ id UUID,
+ pixel_id UUID REFERENCES tracking_pixels(id),
+ event_type VARCHAR(50), -- 'PageView', 'AddToCart', 'Purchase'
+ event_data JSONB,
+ user_id VARCHAR(255),
+ session_id VARCHAR(255),
+ event_time TIMESTAMPTZ NOT NULL
+);
+SELECT create_hypertable('pixel_events', 'event_time');
-### Phân Cấp Exceptions
+-- Ad interactions (for attribution)
+CREATE TABLE ad_interactions (
+ id UUID PRIMARY KEY,
+ user_id VARCHAR(255),
+ ad_id UUID,
+ campaign_id UUID,
+ interaction_type VARCHAR(20), -- 'impression', 'click'
+ interaction_time TIMESTAMPTZ NOT NULL
+);
-```
-Exception
-└── DomainException
- └── SampleDomainException
+-- Conversions with attribution
+CREATE TABLE conversions (
+ id UUID PRIMARY KEY,
+ pixel_event_id UUID,
+ attributed_ad_id UUID,
+ attributed_campaign_id UUID,
+ attribution_model VARCHAR(50),
+ conversion_value DECIMAL(18, 2),
+ converted_at TIMESTAMPTZ
+);
```
-### Problem Details (RFC 7807)
+## Attribution Window Settings
-Tất cả lỗi được trả về theo định dạng Problem Details:
+| Setting | Click Window | View Window |
+|---------|--------------|-------------|
+| **7/1** (default) | 7 days | 1 day |
+| **7/0** | 7 days | None |
+| **28/1** | 28 days | 1 day |
+| **1/1** | 1 day | 1 day |
-```json
+## Pixel Integration Code
+
+```html
+
+
+
+
+
+```
+
+## Server-side Conversion API
+
+```csharp
+///
+/// EN: Server-side conversion tracking.
+/// VI: Tracking conversion phía server.
+///
+[HttpPost("events/server")]
+public async Task TrackServerEvent(
+ [FromBody] ServerEventRequest request)
{
- "type": "https://tools.ietf.org/html/rfc7807",
- "title": "Lỗi Validation",
- "status": 400,
- "detail": "Một hoặc nhiều lỗi validation đã xảy ra.",
- "errors": {
- "Name": ["Tên là bắt buộc"]
- }
+ // EN: More reliable than pixel (no ad blockers)
+ // VI: Đáng tin cậy hơn pixel (không bị ad blocker)
+ var result = await _mediator.Send(new TrackServerEventCommand
+ {
+ PixelId = request.PixelId,
+ EventType = request.EventType,
+ EventData = request.EventData,
+ UserId = request.UserId, // hashed
+ EventTime = request.EventTime
+ });
+
+ return Ok(result);
}
```
-## Health Checks
+## Integration Events
-```mermaid
-graph TD
- HC[Health Check Endpoint]
- HC --> |/health/live| L[Liveness]
- HC --> |/health/ready| R[Readiness]
- HC --> |/health| F[Full Status]
-
- R --> PG[(PostgreSQL)]
- R --> RD[(Redis)]
-
- style HC fill:#3498db,stroke:#2980b9,color:#fff
- style L fill:#2ecc71,stroke:#27ae60,color:#fff
- style R fill:#f39c12,stroke:#d68910,color:#fff
+```csharp
+// Consume from ads-serving
+public record AdImpressionTrackedEvent(
+ Guid AdId, string UserId, string Placement, DateTime TrackedAt);
+
+public record AdClickTrackedEvent(
+ Guid AdId, string UserId, string DestinationUrl, DateTime TrackedAt);
+
+// Publish to ads-analytics
+public record ConversionAttributedEvent(
+ Guid ConversionId, Guid AdId, Guid CampaignId,
+ decimal ConversionValue, string AttributionModel);
```
-## Kiến Trúc Deployment
+## API Endpoints
-### Docker Compose (Local)
-
-```yaml
-services:
- myservice-api:
- build: .
- ports: ["5000:8080"]
- depends_on:
- - postgres
- - redis
-
- postgres:
- image: postgres:16-alpine
-
- redis:
- image: redis:7-alpine
-```
-
-### Kubernetes (Production)
-
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: myservice-api
-spec:
- replicas: 3
- template:
- spec:
- containers:
- - name: api
- image: myservice:latest
- ports:
- - containerPort: 8080
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8080
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8080
-```
-
-## Cân Nhắc Bảo Mật
-
-1. **Authentication**: JWT Bearer token (cấu hình trong production)
-2. **Authorization**: Role-based access control
-3. **Input Validation**: FluentValidation trên tất cả requests
-4. **SQL Injection**: EF Core parameterized queries
-5. **Secrets**: Biến môi trường, không bao giờ trong code
-
-## Tối Ưu Hiệu Năng
-
-1. **Connection Pooling**: EF Core với Npgsql connection resilience
-2. **Async/Await**: Tất cả I/O operations đều async
-3. **Response Caching**: Thêm caching headers cho queries
-4. **Database Indexes**: Cấu hình trong EntityConfigurations
-
-## Tài Liệu Tham Khảo
-
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
-- [Hướng dẫn Kiến trúc .NET Microservices](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
-- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
-- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/api/v1/ads-tracking/pixels/{advertiserId}` | Get pixel code |
+| `POST` | `/api/v1/ads-tracking/events` | Track pixel event |
+| `POST` | `/api/v1/ads-tracking/events/server` | Server-side event |
+| `GET` | `/api/v1/ads-tracking/conversions` | List conversions |
diff --git a/services/booking-service-net/.env.example b/services/booking-service-net/.env.example
new file mode 100644
index 00000000..f9053bc3
--- /dev/null
+++ b/services/booking-service-net/.env.example
@@ -0,0 +1,40 @@
+# Environment / Môi Trường
+ASPNETCORE_ENVIRONMENT=Development
+
+# Database / Cơ Sở Dữ Liệu
+# PostgreSQL connection string (Neon or local)
+DATABASE_URL=Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres
+
+# Redis Cache
+REDIS_URL=localhost:6379
+REDIS_PASSWORD=
+
+# JWT Authentication / Xác Thực JWT
+JWT_SECRET=your-secret-key-min-32-characters-long-here
+JWT_ISSUER=goodgo-platform
+JWT_AUDIENCE=goodgo-services
+JWT_ACCESS_TOKEN_EXPIRY_MINUTES=15
+JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
+
+# API Configuration / Cấu Hình API
+API_PORT=5000
+API_BASE_PATH=/api/v1/myservice
+
+# Observability / Quan Sát
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+OTEL_SERVICE_NAME=myservice
+
+# Logging
+LOG_LEVEL=Information
+SEQ_URL=http://localhost:5341
+
+# Feature Flags
+FEATURE_SWAGGER_ENABLED=true
+FEATURE_DETAILED_ERRORS=true
+
+# Rate Limiting
+RATE_LIMIT_PERMITS_PER_MINUTE=100
+RATE_LIMIT_QUEUE_LIMIT=10
+
+# Health Checks
+HEALTHCHECK_TIMEOUT_SECONDS=5
diff --git a/services/booking-service-net/.gitignore b/services/booking-service-net/.gitignore
new file mode 100644
index 00000000..84b02a53
--- /dev/null
+++ b/services/booking-service-net/.gitignore
@@ -0,0 +1,75 @@
+# Build results
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio
+.vs/
+*.user
+*.userosscache
+*.suo
+*.userprefs
+*.sln.docstates
+
+# Rider
+.idea/
+*.sln.iml
+
+# Visual Studio Code
+.vscode/
+
+# NuGet
+*.nupkg
+*.snupkg
+.nuget/
+packages/
+project.lock.json
+project.fragment.lock.json
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# Coverage
+TestResults/
+*.coverage
+*.coveragexml
+coverage*.json
+coverage*.xml
+
+# Publish output
+publish/
+out/
+
+# Environment files
+.env
+.env.local
+.env.*.local
+*.env
+
+# Secrets
+appsettings.*.json
+!appsettings.json
+!appsettings.Development.json
+
+# macOS
+.DS_Store
+
+# Windows
+Thumbs.db
+ehthumbs.db
+
+# JetBrains
+*.resharper
+
+# dotnet tools
+.config/dotnet-tools.json
+
+# Migration scripts (only keep structure)
+Migrations/
+
+# Temp files
+*.tmp
+*.temp
+~$*
diff --git a/services/booking-service-net/BookingService.slnx b/services/booking-service-net/BookingService.slnx
new file mode 100644
index 00000000..22c2c9fe
--- /dev/null
+++ b/services/booking-service-net/BookingService.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/booking-service-net/Directory.Build.props b/services/booking-service-net/Directory.Build.props
new file mode 100644
index 00000000..c3b74373
--- /dev/null
+++ b/services/booking-service-net/Directory.Build.props
@@ -0,0 +1,22 @@
+
+
+ net10.0
+ 14.0
+ enable
+ enable
+ true
+ true
+ $(NoWarn);1591;CA2017
+
+
+
+ GoodGo Team
+ GoodGo
+ © 2026 GoodGo. All rights reserved.
+ git
+
+
+
+
+
+
diff --git a/services/booking-service-net/Dockerfile b/services/booking-service-net/Dockerfile
new file mode 100644
index 00000000..192106ab
--- /dev/null
+++ b/services/booking-service-net/Dockerfile
@@ -0,0 +1,66 @@
+# Build stage / Giai đoạn build
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+# EN: Copy project files for layer caching
+# VI: Sao chép các file project để tận dụng layer caching
+COPY ["src/MyService.API/MyService.API.csproj", "src/MyService.API/"]
+COPY ["src/MyService.Domain/MyService.Domain.csproj", "src/MyService.Domain/"]
+COPY ["src/MyService.Infrastructure/MyService.Infrastructure.csproj", "src/MyService.Infrastructure/"]
+COPY ["Directory.Build.props", "./"]
+
+# EN: Restore dependencies
+# VI: Khôi phục dependencies
+RUN dotnet restore "src/MyService.API/MyService.API.csproj"
+
+# EN: Copy all source code
+# VI: Sao chép toàn bộ source code
+COPY src/ ./src/
+
+# EN: Build the application
+# VI: Build ứng dụng
+WORKDIR "/src/src/MyService.API"
+RUN dotnet build "MyService.API.csproj" -c Release -o /app/build --no-restore
+
+# Publish stage / Giai đoạn publish
+FROM build AS publish
+RUN dotnet publish "MyService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
+
+# Runtime stage / Giai đoạn runtime
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
+WORKDIR /app
+
+# EN: Create non-root user for security
+# VI: Tạo user non-root cho bảo mật
+RUN groupadd -g 1001 dotnetuser && \
+ useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
+
+# EN: Copy published application
+# VI: Sao chép ứng dụng đã publish
+COPY --from=publish /app/publish .
+
+# EN: Change ownership to non-root user
+# VI: Thay đổi quyền sở hữu sang user non-root
+RUN chown -R dotnetuser:dotnetuser /app
+
+# EN: Switch to non-root user
+# VI: Chuyển sang user non-root
+USER dotnetuser
+
+# EN: Expose port
+# VI: Mở cổng
+EXPOSE 8080
+
+# EN: Set environment variables
+# VI: Thiết lập biến môi trường
+ENV ASPNETCORE_URLS=http://+:8080
+ENV ASPNETCORE_ENVIRONMENT=Production
+
+# EN: Health check
+# VI: Kiểm tra health
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+ CMD curl -f http://localhost:8080/health/live || exit 1
+
+# EN: Start the application
+# VI: Khởi động ứng dụng
+ENTRYPOINT ["dotnet", "MyService.API.dll"]
diff --git a/services/booking-service-net/docker-compose.yml b/services/booking-service-net/docker-compose.yml
new file mode 100644
index 00000000..254ceb12
--- /dev/null
+++ b/services/booking-service-net/docker-compose.yml
@@ -0,0 +1,72 @@
+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/docs/en/ARCHITECTURE.md b/services/booking-service-net/docs/en/ARCHITECTURE.md
new file mode 100644
index 00000000..c04c6840
--- /dev/null
+++ b/services/booking-service-net/docs/en/ARCHITECTURE.md
@@ -0,0 +1,120 @@
+# Booking Service Architecture
+
+> Architecture documentation for Booking Service.
+
+## Architecture Overview
+
+```mermaid
+graph TB
+ subgraph "API Layer"
+ SC[StaffController]
+ RC[ResourceController]
+ AC[AppointmentController]
+ end
+
+ subgraph "Domain Layer"
+ SS[StaffSchedule]
+ R[Resource]
+ A[Appointment]
+ end
+
+ subgraph "Infrastructure"
+ DB[(PostgreSQL)]
+ REPO[Repositories]
+ end
+
+ SC --> SS
+ RC --> R
+ AC --> A
+ A --> SS
+ A --> R
+ SS --> DB
+ R --> DB
+ A --> DB
+
+ style A fill:#50c878,stroke:#2d8659,color:#fff
+ style R fill:#3498db,stroke:#2980b9,color:#fff
+```
+
+## Database Schema
+
+```mermaid
+erDiagram
+ staff_schedules {
+ uuid id PK
+ uuid staff_id FK
+ uuid shop_id FK
+ int day_of_week
+ time start_time
+ time end_time
+ }
+
+ resources {
+ uuid id PK
+ uuid shop_id FK
+ varchar name
+ varchar resource_type
+ int capacity
+ }
+
+ appointments {
+ uuid id PK
+ uuid shop_id FK
+ uuid customer_id FK
+ uuid staff_id FK
+ uuid resource_id FK
+ uuid service_id FK
+ timestamp start_time
+ timestamp end_time
+ varchar status
+ }
+
+ staff_schedules ||--o{ appointments : assigned
+ resources ||--o{ appointments : booked
+```
+
+## Appointment State Machine
+
+```mermaid
+stateDiagram-v2
+ [*] --> Scheduled: Book
+ Scheduled --> Confirmed: Confirm
+ Scheduled --> Cancelled: Cancel
+ Confirmed --> InProgress: Check-in
+ InProgress --> Completed: Complete
+ Confirmed --> NoShow: No Show
+ Confirmed --> Cancelled: Cancel
+```
+
+## Slot Finding Algorithm
+
+```mermaid
+flowchart TD
+ START[Find Slots Request] --> GET_STAFF[Get Staff Schedules]
+ GET_STAFF --> GET_RESOURCE[Get Resource Availability]
+ GET_RESOURCE --> GET_APPTS[Get Existing Appointments]
+ GET_APPTS --> INTERSECT[Calculate Intersection]
+ INTERSECT --> REMOVE[Remove Booked Slots]
+ REMOVE --> SLOTS[Return Available Slots]
+```
+
+## Integration with Order Service
+
+```mermaid
+sequenceDiagram
+ participant Order as Order Service
+ participant Strategy as ServiceStrategy
+ participant Booking as Booking Service
+
+ Order->>Strategy: ValidateAsync(item)
+ Strategy->>Booking: Check Slot Available
+ Booking-->>Strategy: Available
+
+ Order->>Strategy: ExecuteAsync(item)
+ Strategy->>Booking: Create Appointment
+ Booking-->>Strategy: Appointment Created
+```
+
+## References
+
+- [Multi-vertical Architecture](../../../../docs/en/architecture/multi-vertical-architecture.md)
diff --git a/services/booking-service-net/docs/en/README.md b/services/booking-service-net/docs/en/README.md
new file mode 100644
index 00000000..fc81a583
--- /dev/null
+++ b/services/booking-service-net/docs/en/README.md
@@ -0,0 +1,144 @@
+# Booking Service
+
+> Appointment scheduling and "time inventory" management for service businesses.
+
+## Overview
+
+Booking Service manages appointments for service-oriented businesses like spas, salons, and clinics. It tracks staff availability, resource scheduling, and time slot management.
+
+### Key Features
+
+- **Staff Scheduling** - Working hours per staff member
+- **Resource Management** - Rooms, beds, equipment availability
+- **Appointment Booking** - Find and reserve time slots
+- **Availability Algorithm** - Intersection of staff + resource schedules
+
+## Architecture Context
+
+```mermaid
+graph LR
+ ORDER["📝 Order Service"] --> BOOKING["📅 Booking Service"]
+ POS["POS"] --> BOOKING
+ BOOKING --> MERCHANT["🏪 Merchant Service"]
+
+ style BOOKING fill:#9b59b6,stroke:#7d3c98,color:#fff
+```
+
+## Quick Start
+
+```bash
+cd services/booking-service-net
+cp .env.example .env
+dotnet run --project src/BookingService.API
+```
+
+## API Endpoints
+
+### Staff Schedules
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/api/v1/staff/{id}/schedule` | Get staff schedule |
+| `PUT` | `/api/v1/staff/{id}/schedule` | Update schedule |
+| `GET` | `/api/v1/staff/{id}/availability` | Get available slots |
+
+### Resources
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/api/v1/resources` | List resources by shop |
+| `POST` | `/api/v1/resources` | Create resource |
+| `PUT` | `/api/v1/resources/{id}` | Update resource |
+| `GET` | `/api/v1/resources/{id}/availability` | Get available slots |
+
+### Appointments
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/api/v1/appointments` | List appointments |
+| `POST` | `/api/v1/appointments` | Create appointment |
+| `GET` | `/api/v1/appointments/{id}` | Get appointment details |
+| `PATCH` | `/api/v1/appointments/{id}/status` | Update status |
+| `DELETE` | `/api/v1/appointments/{id}` | Cancel appointment |
+
+### Slot Finder
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `POST` | `/api/v1/slots/find` | Find available slots |
+
+## Domain Model
+
+### StaffSchedule
+
+```csharp
+public class StaffSchedule : Entity
+{
+ public Guid StaffId { get; private set; }
+ public Guid ShopId { get; private set; }
+ public DayOfWeek DayOfWeek { get; private set; }
+ public TimeOnly StartTime { get; private set; }
+ public TimeOnly EndTime { get; private set; }
+}
+```
+
+### Resource
+
+```csharp
+public class Resource : Entity, IAggregateRoot
+{
+ public Guid ShopId { get; private set; }
+ public string Name { get; private set; }
+ public ResourceType Type { get; private set; } // Room, Bed, Equipment
+ public int Capacity { get; private set; }
+}
+```
+
+### Appointment
+
+```csharp
+public class Appointment : Entity, IAggregateRoot
+{
+ public Guid ShopId { get; private set; }
+ public Guid? CustomerId { get; private set; }
+ public Guid? StaffId { get; private set; }
+ public Guid? ResourceId { get; private set; }
+ public Guid ServiceId { get; private set; }
+ public DateTime StartTime { get; private set; }
+ public DateTime EndTime { get; private set; }
+ public AppointmentStatus Status { get; private set; }
+}
+```
+
+## Slot Finding Algorithm
+
+```
+Available Slots = Staff Schedule ∩ Resource Schedule - Existing Appointments
+```
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Booking as Booking Service
+ participant DB as Database
+
+ Client->>Booking: Find Slots (Service, Date, Staff?)
+ Booking->>DB: Get Staff Schedules
+ Booking->>DB: Get Resource Schedules
+ Booking->>DB: Get Existing Appointments
+
+ Booking->>Booking: Calculate Intersection
+ Booking->>Booking: Remove Booked Slots
+
+ Booking-->>Client: Available Slots
+```
+
+## Related Services
+
+- [Order Service](../../order-service-net/docs/en/README.md) - Uses ServiceStrategy
+- [Merchant Service](../../merchant-service-net/docs/en/README.md) - Shop configuration
+- [Catalog Service](../../catalog-service-net/docs/en/README.md) - Service products
+
+## License
+
+Proprietary - GoodGo Platform
diff --git a/services/booking-service-net/docs/vi/ARCHITECTURE.md b/services/booking-service-net/docs/vi/ARCHITECTURE.md
new file mode 100644
index 00000000..64e71893
--- /dev/null
+++ b/services/booking-service-net/docs/vi/ARCHITECTURE.md
@@ -0,0 +1,120 @@
+# Kiến Trúc Booking Service
+
+> Tài liệu kiến trúc cho Booking Service.
+
+## Tổng Quan Kiến Trúc
+
+```mermaid
+graph TB
+ subgraph "Tầng API"
+ SC[StaffController]
+ RC[ResourceController]
+ AC[AppointmentController]
+ end
+
+ subgraph "Tầng Domain"
+ SS[StaffSchedule]
+ R[Resource]
+ A[Appointment]
+ end
+
+ subgraph "Infrastructure"
+ DB[(PostgreSQL)]
+ REPO[Repositories]
+ end
+
+ SC --> SS
+ RC --> R
+ AC --> A
+ A --> SS
+ A --> R
+ SS --> DB
+ R --> DB
+ A --> DB
+
+ style A fill:#50c878,stroke:#2d8659,color:#fff
+ style R fill:#3498db,stroke:#2980b9,color:#fff
+```
+
+## Database Schema
+
+```mermaid
+erDiagram
+ staff_schedules {
+ uuid id PK
+ uuid staff_id FK
+ uuid shop_id FK
+ int day_of_week
+ time start_time
+ time end_time
+ }
+
+ resources {
+ uuid id PK
+ uuid shop_id FK
+ varchar name
+ varchar resource_type
+ int capacity
+ }
+
+ appointments {
+ uuid id PK
+ uuid shop_id FK
+ uuid customer_id FK
+ uuid staff_id FK
+ uuid resource_id FK
+ uuid service_id FK
+ timestamp start_time
+ timestamp end_time
+ varchar status
+ }
+
+ staff_schedules ||--o{ appointments : assigned
+ resources ||--o{ appointments : booked
+```
+
+## Appointment State Machine
+
+```mermaid
+stateDiagram-v2
+ [*] --> Scheduled: Đặt
+ Scheduled --> Confirmed: Xác Nhận
+ Scheduled --> Cancelled: Hủy
+ Confirmed --> InProgress: Check-in
+ InProgress --> Completed: Hoàn Tất
+ Confirmed --> NoShow: Không Đến
+ Confirmed --> Cancelled: Hủy
+```
+
+## Thuật Toán Tìm Slot
+
+```mermaid
+flowchart TD
+ START[Yêu Cầu Tìm Slot] --> GET_STAFF[Lấy Lịch Nhân Viên]
+ GET_STAFF --> GET_RESOURCE[Lấy Khả Dụng Tài Nguyên]
+ GET_RESOURCE --> GET_APPTS[Lấy Cuộc Hẹn Đã Đặt]
+ GET_APPTS --> INTERSECT[Tính Giao Tập Hợp]
+ INTERSECT --> REMOVE[Loại Bỏ Slot Đã Đặt]
+ REMOVE --> SLOTS[Trả Về Slot Khả Dụng]
+```
+
+## Tích Hợp Với Order Service
+
+```mermaid
+sequenceDiagram
+ participant Order as Order Service
+ participant Strategy as ServiceStrategy
+ participant Booking as Booking Service
+
+ Order->>Strategy: ValidateAsync(item)
+ Strategy->>Booking: Kiểm Tra Slot Khả Dụng
+ Booking-->>Strategy: Khả Dụng
+
+ Order->>Strategy: ExecuteAsync(item)
+ Strategy->>Booking: Tạo Cuộc Hẹn
+ Booking-->>Strategy: Cuộc Hẹn Đã Tạo
+```
+
+## Tài Nguyên
+
+- [Kiến Trúc Đa Ngành Hàng](../../../../docs/vi/architecture/multi-vertical-architecture.md)
diff --git a/services/booking-service-net/docs/vi/README.md b/services/booking-service-net/docs/vi/README.md
new file mode 100644
index 00000000..5f121d97
--- /dev/null
+++ b/services/booking-service-net/docs/vi/README.md
@@ -0,0 +1,144 @@
+# Booking Service
+
+> Đặt lịch hẹn và quản lý "tồn kho thời gian" cho doanh nghiệp dịch vụ.
+
+## Tổng Quan
+
+Booking Service quản lý cuộc hẹn cho các doanh nghiệp dịch vụ như spa, salon và phòng khám. Theo dõi lịch nhân viên, lịch tài nguyên, và quản lý slot thời gian.
+
+### Tính Năng Chính
+
+- **Lịch Nhân viên** - Giờ làm việc theo nhân viên
+- **Quản lý Tài nguyên** - Phòng, giường, thiết bị
+- **Đặt Lịch hẹn** - Tìm và đặt slot thời gian
+- **Thuật toán Khả dụng** - Giao của lịch nhân viên + tài nguyên
+
+## Bối Cảnh Kiến Trúc
+
+```mermaid
+graph LR
+ ORDER["📝 Order Service"] --> BOOKING["📅 Booking Service"]
+ POS["POS"] --> BOOKING
+ BOOKING --> MERCHANT["🏪 Merchant Service"]
+
+ style BOOKING fill:#9b59b6,stroke:#7d3c98,color:#fff
+```
+
+## Bắt Đầu Nhanh
+
+```bash
+cd services/booking-service-net
+cp .env.example .env
+dotnet run --project src/BookingService.API
+```
+
+## API Endpoints
+
+### Lịch Nhân Viên
+
+| Method | Endpoint | Mô tả |
+|--------|----------|-------|
+| `GET` | `/api/v1/staff/{id}/schedule` | Lấy lịch nhân viên |
+| `PUT` | `/api/v1/staff/{id}/schedule` | Cập nhật lịch |
+| `GET` | `/api/v1/staff/{id}/availability` | Lấy slot khả dụng |
+
+### Tài Nguyên
+
+| Method | Endpoint | Mô tả |
+|--------|----------|-------|
+| `GET` | `/api/v1/resources` | Danh sách tài nguyên theo shop |
+| `POST` | `/api/v1/resources` | Tạo tài nguyên |
+| `PUT` | `/api/v1/resources/{id}` | Cập nhật tài nguyên |
+| `GET` | `/api/v1/resources/{id}/availability` | Lấy slot khả dụng |
+
+### Cuộc Hẹn
+
+| Method | Endpoint | Mô tả |
+|--------|----------|-------|
+| `GET` | `/api/v1/appointments` | Danh sách cuộc hẹn |
+| `POST` | `/api/v1/appointments` | Tạo cuộc hẹn |
+| `GET` | `/api/v1/appointments/{id}` | Chi tiết cuộc hẹn |
+| `PATCH` | `/api/v1/appointments/{id}/status` | Cập nhật trạng thái |
+| `DELETE` | `/api/v1/appointments/{id}` | Hủy cuộc hẹn |
+
+### Tìm Slot
+
+| Method | Endpoint | Mô tả |
+|--------|----------|-------|
+| `POST` | `/api/v1/slots/find` | Tìm slot khả dụng |
+
+## Domain Model
+
+### StaffSchedule
+
+```csharp
+public class StaffSchedule : Entity
+{
+ public Guid StaffId { get; private set; }
+ public Guid ShopId { get; private set; }
+ public DayOfWeek DayOfWeek { get; private set; }
+ public TimeOnly StartTime { get; private set; }
+ public TimeOnly EndTime { get; private set; }
+}
+```
+
+### Resource
+
+```csharp
+public class Resource : Entity, IAggregateRoot
+{
+ public Guid ShopId { get; private set; }
+ public string Name { get; private set; }
+ public ResourceType Type { get; private set; } // Room, Bed, Equipment
+ public int Capacity { get; private set; }
+}
+```
+
+### Appointment
+
+```csharp
+public class Appointment : Entity, IAggregateRoot
+{
+ public Guid ShopId { get; private set; }
+ public Guid? CustomerId { get; private set; }
+ public Guid? StaffId { get; private set; }
+ public Guid? ResourceId { get; private set; }
+ public Guid ServiceId { get; private set; }
+ public DateTime StartTime { get; private set; }
+ public DateTime EndTime { get; private set; }
+ public AppointmentStatus Status { get; private set; }
+}
+```
+
+## Thuật Toán Tìm Slot
+
+```
+Slot Khả Dụng = Lịch Nhân Viên ∩ Lịch Tài Nguyên - Cuộc Hẹn Đã Đặt
+```
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Booking as Booking Service
+ participant DB as Database
+
+ Client->>Booking: Tìm Slot (Dịch vụ, Ngày, Nhân viên?)
+ Booking->>DB: Lấy Lịch Nhân Viên
+ Booking->>DB: Lấy Lịch Tài Nguyên
+ Booking->>DB: Lấy Cuộc Hẹn Đã Đặt
+
+ Booking->>Booking: Tính Giao Tập Hợp
+ Booking->>Booking: Loại Bỏ Slot Đã Đặt
+
+ Booking-->>Client: Danh Sách Slot Khả Dụng
+```
+
+## Services Liên Quan
+
+- [Order Service](../../order-service-net/docs/vi/README.md) - Sử dụng ServiceStrategy
+- [Merchant Service](../../merchant-service-net/docs/vi/README.md) - Cấu hình Shop
+- [Catalog Service](../../catalog-service-net/docs/vi/README.md) - Sản phẩm dịch vụ
+
+## License
+
+Proprietary - GoodGo Platform
diff --git a/services/booking-service-net/global.json b/services/booking-service-net/global.json
new file mode 100644
index 00000000..f78eeaf4
--- /dev/null
+++ b/services/booking-service-net/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "10.0.101",
+ "rollForward": "latestMinor",
+ "allowPrerelease": false
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..a724424d
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Behaviors/LoggingBehavior.cs
@@ -0,0 +1,58 @@
+using System.Diagnostics;
+using MediatR;
+
+namespace MyService.API.Application.Behaviors;
+
+///
+/// EN: MediatR behavior for logging request handling.
+/// VI: MediatR behavior để logging việc xử lý request.
+///
+/// EN: Request type / VI: Loại request
+/// EN: Response type / VI: Loại response
+public class LoggingBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly ILogger> _logger;
+
+ public LoggingBehavior(ILogger> logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ _logger.LogInformation(
+ "Handling {RequestName} / Đang xử lý {RequestName}",
+ requestName);
+
+ var stopwatch = Stopwatch.StartNew();
+
+ try
+ {
+ var response = await next();
+
+ stopwatch.Stop();
+
+ _logger.LogInformation(
+ "Handled {RequestName} in {ElapsedMs}ms / Đã xử lý {RequestName} trong {ElapsedMs}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+
+ _logger.LogError(ex,
+ "Error handling {RequestName} after {ElapsedMs}ms / Lỗi xử lý {RequestName} sau {ElapsedMs}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+
+ throw;
+ }
+ }
+}
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
new file mode 100644
index 00000000..8675b649
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Behaviors/TransactionBehavior.cs
@@ -0,0 +1,84 @@
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using MyService.Infrastructure;
+
+namespace MyService.API.Application.Behaviors;
+
+///
+/// EN: MediatR behavior for handling database transactions.
+/// VI: MediatR behavior để xử lý database transactions.
+///
+/// EN: Request type / VI: Loại request
+/// EN: Response type / VI: Loại response
+public class TransactionBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly MyServiceContext _dbContext;
+ private readonly ILogger> _logger;
+
+ public TransactionBehavior(
+ MyServiceContext dbContext,
+ ILogger> logger)
+ {
+ _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ // EN: Skip transaction for queries (read operations)
+ // VI: Bỏ qua transaction cho queries (các thao tác đọc)
+ if (requestName.EndsWith("Query"))
+ {
+ return await next();
+ }
+
+ // EN: Skip if already in a transaction
+ // VI: Bỏ qua nếu đã trong transaction
+ if (_dbContext.HasActiveTransaction)
+ {
+ return await next();
+ }
+
+ var strategy = _dbContext.Database.CreateExecutionStrategy();
+
+ return await strategy.ExecuteAsync(async () =>
+ {
+ await using var transaction = await _dbContext.BeginTransactionAsync();
+
+ _logger.LogInformation(
+ "Begin transaction {TransactionId} for {RequestName} / Bắt đầu transaction {TransactionId} cho {RequestName}",
+ transaction?.TransactionId, requestName);
+
+ try
+ {
+ var response = await next();
+
+ if (transaction != null)
+ {
+ await _dbContext.CommitTransactionAsync(transaction);
+
+ _logger.LogInformation(
+ "Committed transaction {TransactionId} for {RequestName} / Đã commit transaction {TransactionId} cho {RequestName}",
+ transaction.TransactionId, requestName);
+ }
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex,
+ "Error during transaction {TransactionId} for {RequestName} / Lỗi trong transaction {TransactionId} cho {RequestName}",
+ transaction?.TransactionId, requestName);
+
+ _dbContext.RollbackTransaction();
+ throw;
+ }
+ });
+ }
+}
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
new file mode 100644
index 00000000..0062cd60
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Behaviors/ValidatorBehavior.cs
@@ -0,0 +1,63 @@
+using FluentValidation;
+using MediatR;
+
+namespace MyService.API.Application.Behaviors;
+
+///
+/// EN: MediatR behavior for FluentValidation integration.
+/// VI: MediatR behavior để tích hợp FluentValidation.
+///
+/// EN: Request type / VI: Loại request
+/// EN: Response type / VI: Loại response
+public class ValidatorBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly IEnumerable> _validators;
+ private readonly ILogger> _logger;
+
+ public ValidatorBehavior(
+ IEnumerable> validators,
+ ILogger> logger)
+ {
+ _validators = validators ?? throw new ArgumentNullException(nameof(validators));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ if (!_validators.Any())
+ {
+ return await next();
+ }
+
+ _logger.LogDebug(
+ "Validating {RequestName} / Đang validate {RequestName}",
+ requestName);
+
+ var context = new ValidationContext(request);
+
+ var validationResults = await Task.WhenAll(
+ _validators.Select(v => v.ValidateAsync(context, cancellationToken)));
+
+ var failures = validationResults
+ .SelectMany(r => r.Errors)
+ .Where(f => f != null)
+ .ToList();
+
+ if (failures.Count != 0)
+ {
+ _logger.LogWarning(
+ "Validation failed for {RequestName} with {ErrorCount} errors / Validation thất bại cho {RequestName} với {ErrorCount} lỗi",
+ requestName, failures.Count);
+
+ throw new ValidationException(failures);
+ }
+
+ return await next();
+ }
+}
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
new file mode 100644
index 00000000..49825490
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommand.cs
@@ -0,0 +1,14 @@
+using MediatR;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs
new file mode 100644
index 00000000..76e31030
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs
@@ -0,0 +1,70 @@
+using MediatR;
+using MyService.Domain.AggregatesModel.SampleAggregate;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommand.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommand.cs
new file mode 100644
index 00000000..138cc794
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommand.cs
@@ -0,0 +1,21 @@
+using MediatR;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommandHandler.cs
new file mode 100644
index 00000000..d7d0fd7c
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Commands/CreateSampleCommandHandler.cs
@@ -0,0 +1,46 @@
+using MediatR;
+using MyService.Domain.AggregatesModel.SampleAggregate;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommand.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommand.cs
new file mode 100644
index 00000000..0de392db
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommand.cs
@@ -0,0 +1,10 @@
+using MediatR;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommandHandler.cs
new file mode 100644
index 00000000..c7632189
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Commands/DeleteSampleCommandHandler.cs
@@ -0,0 +1,54 @@
+using MediatR;
+using MyService.Domain.AggregatesModel.SampleAggregate;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommand.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommand.cs
new file mode 100644
index 00000000..6fad8514
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommand.cs
@@ -0,0 +1,16 @@
+using MediatR;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommandHandler.cs
new file mode 100644
index 00000000..e904cf0a
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Commands/UpdateSampleCommandHandler.cs
@@ -0,0 +1,54 @@
+using MediatR;
+using MyService.Domain.AggregatesModel.SampleAggregate;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQuery.cs b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQuery.cs
new file mode 100644
index 00000000..8b90789c
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQuery.cs
@@ -0,0 +1,23 @@
+using MediatR;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQueryHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQueryHandler.cs
new file mode 100644
index 00000000..2da10b6d
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSampleQueryHandler.cs
@@ -0,0 +1,39 @@
+using MediatR;
+using MyService.Domain.AggregatesModel.SampleAggregate;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQuery.cs b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQuery.cs
new file mode 100644
index 00000000..d6a98e34
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQuery.cs
@@ -0,0 +1,9 @@
+using MediatR;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQueryHandler.cs
new file mode 100644
index 00000000..2185302d
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Queries/GetSamplesQueryHandler.cs
@@ -0,0 +1,34 @@
+using MediatR;
+using MyService.Domain.AggregatesModel.SampleAggregate;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/booking-service-net/src/BookingService.API/Application/Validations/CreateSampleCommandValidator.cs
new file mode 100644
index 00000000..2f339fb3
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Validations/CreateSampleCommandValidator.cs
@@ -0,0 +1,25 @@
+using FluentValidation;
+using MyService.API.Application.Commands;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/booking-service-net/src/BookingService.API/Application/Validations/UpdateSampleCommandValidator.cs
new file mode 100644
index 00000000..7030d5c8
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Application/Validations/UpdateSampleCommandValidator.cs
@@ -0,0 +1,29 @@
+using FluentValidation;
+using MyService.API.Application.Commands;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/BookingService.API.csproj b/services/booking-service-net/src/BookingService.API/BookingService.API.csproj
new file mode 100644
index 00000000..1b5bb222
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/BookingService.API.csproj
@@ -0,0 +1,43 @@
+
+
+
+ MyService.API
+ MyService.API
+ Web API layer with CQRS pattern
+ myservice-api
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/booking-service-net/src/BookingService.API/Controllers/SamplesController.cs b/services/booking-service-net/src/BookingService.API/Controllers/SamplesController.cs
new file mode 100644
index 00000000..c87e0ffa
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Controllers/SamplesController.cs
@@ -0,0 +1,200 @@
+using Asp.Versioning;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using MyService.API.Application.Commands;
+using MyService.API.Application.Queries;
+
+namespace MyService.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/booking-service-net/src/BookingService.API/Program.cs b/services/booking-service-net/src/BookingService.API/Program.cs
new file mode 100644
index 00000000..bd9b3df4
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Program.cs
@@ -0,0 +1,144 @@
+using Asp.Versioning;
+using FluentValidation;
+using Hellang.Middleware.ProblemDetails;
+using MyService.API.Application.Behaviors;
+using MyService.Infrastructure;
+using Serilog;
+
+// EN: Configure Serilog early / VI: Cấu hình Serilog sớm
+Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console()
+ .CreateBootstrapLogger();
+
+try
+{
+ Log.Information("Starting MyService API / Khởi động MyService API");
+
+ var builder = WebApplication.CreateBuilder(args);
+
+ // EN: Configure Serilog / VI: Cấu hình Serilog
+ builder.Host.UseSerilog((context, services, configuration) => configuration
+ .ReadFrom.Configuration(context.Configuration)
+ .ReadFrom.Services(services)
+ .Enrich.FromLogContext()
+ .WriteTo.Console());
+
+ // EN: Add Infrastructure services / VI: Thêm Infrastructure services
+ builder.Services.AddInfrastructure(builder.Configuration);
+
+ // EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors
+ builder.Services.AddMediatR(cfg =>
+ {
+ cfg.RegisterServicesFromAssemblyContaining();
+ cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
+ cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>));
+ cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
+ });
+
+ // EN: Add FluentValidation / VI: Thêm FluentValidation
+ builder.Services.AddValidatorsFromAssemblyContaining();
+
+ // EN: Add API versioning / VI: Thêm API versioning
+ builder.Services.AddApiVersioning(options =>
+ {
+ options.DefaultApiVersion = new ApiVersion(1, 0);
+ options.AssumeDefaultVersionWhenUnspecified = true;
+ options.ReportApiVersions = true;
+ options.ApiVersionReader = ApiVersionReader.Combine(
+ new UrlSegmentApiVersionReader(),
+ new HeaderApiVersionReader("X-Api-Version"));
+ })
+ .AddApiExplorer(options =>
+ {
+ options.GroupNameFormat = "'v'VVV";
+ options.SubstituteApiVersionInUrl = true;
+ });
+
+ // EN: Add controllers / VI: Thêm controllers
+ builder.Services.AddControllers();
+
+ // EN: Add ProblemDetails middleware (RFC 7807) / VI: Thêm ProblemDetails middleware
+ builder.Services.AddProblemDetails(options =>
+ {
+ options.IncludeExceptionDetails = (ctx, ex) =>
+ builder.Environment.IsDevelopment();
+ });
+
+ // EN: Add Swagger / VI: Thêm Swagger
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddSwaggerGen(options =>
+ {
+ options.SwaggerDoc("v1", new()
+ {
+ Title = "MyService API",
+ Version = "v1",
+ Description = "MyService microservice API / API microservice MyService"
+ });
+ });
+
+ // EN: Add health checks / VI: Thêm health checks
+ builder.Services.AddHealthChecks()
+ .AddNpgSql(
+ builder.Configuration.GetConnectionString("DefaultConnection")
+ ?? builder.Configuration["DATABASE_URL"]
+ ?? "",
+ name: "postgresql",
+ tags: ["db", "postgresql"]);
+
+ // EN: Add CORS / VI: Thêm CORS
+ builder.Services.AddCors(options =>
+ {
+ options.AddDefaultPolicy(policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+ });
+
+ var app = builder.Build();
+
+ // EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline
+ app.UseSerilogRequestLogging();
+ app.UseProblemDetails();
+
+ if (app.Environment.IsDevelopment())
+ {
+ app.UseSwagger();
+ app.UseSwaggerUI(c =>
+ {
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyService API v1");
+ c.RoutePrefix = "swagger";
+ });
+ }
+
+ app.UseCors();
+ app.UseRouting();
+
+ // EN: Map health check endpoints / VI: Map health check endpoints
+ app.MapHealthChecks("/health");
+ app.MapHealthChecks("/health/live", new()
+ {
+ Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy
+ });
+ app.MapHealthChecks("/health/ready");
+
+ // EN: Map controllers / VI: Map controllers
+ app.MapControllers();
+
+ // EN: Run the application / VI: Chạy ứng dụng
+ app.Run();
+}
+catch (Exception ex)
+{
+ Log.Fatal(ex, "Application terminated unexpectedly / Ứng dụng kết thúc bất ngờ");
+ throw;
+}
+finally
+{
+ Log.CloseAndFlush();
+}
+
+// EN: Make Program class accessible for integration tests
+// VI: Làm cho class Program có thể truy cập cho integration tests
+public partial class Program { }
diff --git a/services/booking-service-net/src/BookingService.API/Properties/launchSettings.json b/services/booking-service-net/src/BookingService.API/Properties/launchSettings.json
new file mode 100644
index 00000000..6355d40b
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/Properties/launchSettings.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/booking-service-net/src/BookingService.API/appsettings.Development.json b/services/booking-service-net/src/BookingService.API/appsettings.Development.json
new file mode 100644
index 00000000..e407ac85
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/appsettings.Development.json
@@ -0,0 +1,19 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Information",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information"
+ }
+ },
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Debug",
+ "Override": {
+ "Microsoft": "Information",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information",
+ "System": "Information"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/booking-service-net/src/BookingService.API/appsettings.json b/services/booking-service-net/src/BookingService.API/appsettings.json
new file mode 100644
index 00000000..523dc0fc
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.API/appsettings.json
@@ -0,0 +1,46 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning"
+ }
+ },
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
+ }
+ }
+ ],
+ "Enrich": [
+ "FromLogContext",
+ "WithMachineName",
+ "WithThreadId"
+ ]
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres"
+ },
+ "Redis": {
+ "ConnectionString": "localhost:6379"
+ },
+ "Jwt": {
+ "Secret": "your-super-secret-key-min-32-characters",
+ "Issuer": "goodgo-platform",
+ "Audience": "goodgo-services",
+ "AccessTokenExpiryMinutes": 15,
+ "RefreshTokenExpiryDays": 7
+ },
+ "AllowedHosts": "*"
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..40bc8c3a
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs
@@ -0,0 +1,61 @@
+using MyService.Domain.SeedWork;
+
+namespace MyService.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/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/Sample.cs
new file mode 100644
index 00000000..641bb385
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/Sample.cs
@@ -0,0 +1,158 @@
+using MyService.Domain.Events;
+using MyService.Domain.Exceptions;
+using MyService.Domain.SeedWork;
+
+namespace MyService.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/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs
new file mode 100644
index 00000000..54ce63ba
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs
@@ -0,0 +1,77 @@
+using MyService.Domain.SeedWork;
+
+namespace MyService.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/booking-service-net/src/BookingService.Domain/BookingService.Domain.csproj b/services/booking-service-net/src/BookingService.Domain/BookingService.Domain.csproj
new file mode 100644
index 00000000..3208317a
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/BookingService.Domain.csproj
@@ -0,0 +1,14 @@
+
+
+
+ MyService.Domain
+ MyService.Domain
+ Domain layer containing core business logic and entities
+
+
+
+
+
+
+
+
diff --git a/services/booking-service-net/src/BookingService.Domain/Events/SampleCreatedDomainEvent.cs b/services/booking-service-net/src/BookingService.Domain/Events/SampleCreatedDomainEvent.cs
new file mode 100644
index 00000000..7e838214
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/Events/SampleCreatedDomainEvent.cs
@@ -0,0 +1,22 @@
+using MediatR;
+using MyService.Domain.AggregatesModel.SampleAggregate;
+
+namespace MyService.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/booking-service-net/src/BookingService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/booking-service-net/src/BookingService.Domain/Events/SampleStatusChangedDomainEvent.cs
new file mode 100644
index 00000000..f6d9b422
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/Events/SampleStatusChangedDomainEvent.cs
@@ -0,0 +1,39 @@
+using MediatR;
+using MyService.Domain.AggregatesModel.SampleAggregate;
+
+namespace MyService.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/booking-service-net/src/BookingService.Domain/Exceptions/DomainException.cs b/services/booking-service-net/src/BookingService.Domain/Exceptions/DomainException.cs
new file mode 100644
index 00000000..7e737f64
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/Exceptions/DomainException.cs
@@ -0,0 +1,21 @@
+namespace MyService.Domain.Exceptions;
+
+///
+/// EN: Base exception for domain errors.
+/// VI: Exception cơ sở cho các lỗi domain.
+///
+public class DomainException : Exception
+{
+ public DomainException()
+ {
+ }
+
+ public DomainException(string message) : base(message)
+ {
+ }
+
+ public DomainException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/services/booking-service-net/src/BookingService.Domain/Exceptions/SampleDomainException.cs b/services/booking-service-net/src/BookingService.Domain/Exceptions/SampleDomainException.cs
new file mode 100644
index 00000000..c850944c
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/Exceptions/SampleDomainException.cs
@@ -0,0 +1,21 @@
+namespace MyService.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/booking-service-net/src/BookingService.Domain/SeedWork/Entity.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/Entity.cs
new file mode 100644
index 00000000..b07fdd3b
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/Entity.cs
@@ -0,0 +1,102 @@
+using MediatR;
+
+namespace MyService.Domain.SeedWork;
+
+///
+/// EN: Base class for all domain entities.
+/// VI: Lớp cơ sở cho tất cả các entity trong domain.
+///
+public abstract class Entity
+{
+ private int? _requestedHashCode;
+ private Guid _id;
+ private List _domainEvents = new();
+
+ ///
+ /// EN: Unique identifier for the entity.
+ /// VI: Định danh duy nhất cho entity.
+ ///
+ public virtual Guid Id
+ {
+ get => _id;
+ protected set => _id = value;
+ }
+
+ ///
+ /// EN: Domain events raised by this entity.
+ /// VI: Các domain event được phát ra bởi entity này.
+ ///
+ public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly();
+
+ ///
+ /// EN: Add a domain event to be dispatched.
+ /// VI: Thêm một domain event để dispatch.
+ ///
+ public void AddDomainEvent(INotification eventItem)
+ {
+ _domainEvents.Add(eventItem);
+ }
+
+ ///
+ /// EN: Remove a domain event.
+ /// VI: Xóa một domain event.
+ ///
+ public void RemoveDomainEvent(INotification eventItem)
+ {
+ _domainEvents.Remove(eventItem);
+ }
+
+ ///
+ /// EN: Clear all domain events.
+ /// VI: Xóa tất cả domain events.
+ ///
+ public void ClearDomainEvents()
+ {
+ _domainEvents.Clear();
+ }
+
+ ///
+ /// EN: Check if entity is transient (not persisted yet).
+ /// VI: Kiểm tra xem entity có phải là transient (chưa lưu) không.
+ ///
+ public bool IsTransient()
+ {
+ return Id == default;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not Entity item)
+ return false;
+
+ if (ReferenceEquals(this, item))
+ return true;
+
+ if (GetType() != item.GetType())
+ return false;
+
+ if (item.IsTransient() || IsTransient())
+ return false;
+
+ return item.Id == Id;
+ }
+
+ public override int GetHashCode()
+ {
+ if (IsTransient())
+ return base.GetHashCode();
+
+ _requestedHashCode ??= Id.GetHashCode() ^ 31;
+ return _requestedHashCode.Value;
+ }
+
+ public static bool operator ==(Entity? left, Entity? right)
+ {
+ return left?.Equals(right) ?? right is null;
+ }
+
+ public static bool operator !=(Entity? left, Entity? right)
+ {
+ return !(left == right);
+ }
+}
diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/Enumeration.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/Enumeration.cs
new file mode 100644
index 00000000..6641979c
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/Enumeration.cs
@@ -0,0 +1,95 @@
+using System.Reflection;
+
+namespace MyService.Domain.SeedWork;
+
+///
+/// EN: Base class for enumeration classes (type-safe enum pattern).
+/// VI: Lớp cơ sở cho các lớp enumeration (pattern enum an toàn kiểu).
+///
+///
+/// EN: This provides a type-safe alternative to enums with additional functionality
+/// like validation, parsing, and rich behavior.
+/// VI: Cung cấp một thay thế an toàn kiểu cho enums với các chức năng bổ sung
+/// như validation, parsing, và hành vi phong phú.
+///
+public abstract class Enumeration : IComparable
+{
+ ///
+ /// EN: The name of the enumeration value.
+ /// VI: Tên của giá trị enumeration.
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// EN: The unique identifier of the enumeration value.
+ /// VI: Định danh duy nhất của giá trị enumeration.
+ ///
+ public int Id { get; private set; }
+
+ protected Enumeration(int id, string name) => (Id, Name) = (id, name);
+
+ public override string ToString() => Name;
+
+ ///
+ /// EN: Get all enumeration values of a given type.
+ /// VI: Lấy tất cả các giá trị enumeration của một kiểu cho trước.
+ ///
+ public static IEnumerable GetAll() where T : Enumeration =>
+ typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
+ .Select(f => f.GetValue(null))
+ .Cast();
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not Enumeration otherValue)
+ return false;
+
+ var typeMatches = GetType() == obj.GetType();
+ var valueMatches = Id.Equals(otherValue.Id);
+
+ return typeMatches && valueMatches;
+ }
+
+ public override int GetHashCode() => Id.GetHashCode();
+
+ ///
+ /// EN: Get absolute difference between two enumeration values.
+ /// VI: Lấy sự khác biệt tuyệt đối giữa hai giá trị enumeration.
+ ///
+ public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
+ {
+ return Math.Abs(firstValue.Id - secondValue.Id);
+ }
+
+ ///
+ /// EN: Parse an integer ID to the corresponding enumeration value.
+ /// VI: Parse một ID integer thành giá trị enumeration tương ứng.
+ ///
+ public static T FromValue(int value) where T : Enumeration
+ {
+ var matchingItem = Parse(value, "value", item => item.Id == value);
+ return matchingItem;
+ }
+
+ ///
+ /// EN: Parse a display name to the corresponding enumeration value.
+ /// VI: Parse một tên hiển thị thành giá trị enumeration tương ứng.
+ ///
+ public static T FromDisplayName(string displayName) where T : Enumeration
+ {
+ var matchingItem = Parse(displayName, "display name", item => item.Name == displayName);
+ return matchingItem;
+ }
+
+ private static T Parse(TValue value, string description, Func predicate) where T : Enumeration
+ {
+ var matchingItem = GetAll().FirstOrDefault(predicate);
+
+ if (matchingItem is null)
+ throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
+
+ return matchingItem;
+ }
+
+ public int CompareTo(object? other) => Id.CompareTo(((Enumeration)other!).Id);
+}
diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/IAggregateRoot.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/IAggregateRoot.cs
new file mode 100644
index 00000000..d361394f
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/IAggregateRoot.cs
@@ -0,0 +1,15 @@
+namespace MyService.Domain.SeedWork;
+
+///
+/// EN: Marker interface for aggregate roots.
+/// VI: Interface đánh dấu cho aggregate roots.
+///
+///
+/// EN: Aggregate roots are the entry points to aggregates and are the only objects
+/// that outside code should hold references to.
+/// VI: Aggregate roots là điểm vào của aggregates và là đối tượng duy nhất
+/// mà code bên ngoài nên giữ tham chiếu đến.
+///
+public interface IAggregateRoot
+{
+}
diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/IRepository.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/IRepository.cs
new file mode 100644
index 00000000..2d539e44
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/IRepository.cs
@@ -0,0 +1,15 @@
+namespace MyService.Domain.SeedWork;
+
+///
+/// EN: Generic repository interface for aggregate roots.
+/// VI: Interface repository generic cho aggregate roots.
+///
+/// EN: The aggregate root type / VI: Kiểu aggregate root
+public interface IRepository where T : IAggregateRoot
+{
+ ///
+ /// EN: The unit of work for this repository.
+ /// VI: Unit of work cho repository này.
+ ///
+ IUnitOfWork UnitOfWork { get; }
+}
diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/IUnitOfWork.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/IUnitOfWork.cs
new file mode 100644
index 00000000..d37d8fa4
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/IUnitOfWork.cs
@@ -0,0 +1,30 @@
+namespace MyService.Domain.SeedWork;
+
+///
+/// EN: Unit of Work pattern interface.
+/// VI: Interface cho Unit of Work pattern.
+///
+///
+/// EN: Maintains a list of objects affected by a business transaction
+/// and coordinates the writing out of changes.
+/// VI: Duy trì danh sách các đối tượng bị ảnh hưởng bởi một transaction nghiệp vụ
+/// và điều phối việc ghi các thay đổi.
+///
+public interface IUnitOfWork : IDisposable
+{
+ ///
+ /// EN: Save all changes made in this unit of work.
+ /// VI: Lưu tất cả các thay đổi được thực hiện trong unit of work này.
+ ///
+ /// EN: Cancellation token / VI: Token hủy
+ /// EN: Number of entities written / VI: Số entity đã ghi
+ Task SaveChangesAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// EN: Save all changes and dispatch domain events.
+ /// VI: Lưu tất cả thay đổi và dispatch domain events.
+ ///
+ /// EN: Cancellation token / VI: Token hủy
+ /// EN: True if successful / VI: True nếu thành công
+ Task SaveEntitiesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/services/booking-service-net/src/BookingService.Domain/SeedWork/ValueObject.cs b/services/booking-service-net/src/BookingService.Domain/SeedWork/ValueObject.cs
new file mode 100644
index 00000000..5cf4188f
--- /dev/null
+++ b/services/booking-service-net/src/BookingService.Domain/SeedWork/ValueObject.cs
@@ -0,0 +1,53 @@
+namespace MyService.Domain.SeedWork;
+
+///
+/// EN: Base class for Value Objects following DDD patterns.
+/// VI: Lớp cơ sở cho Value Objects theo mẫu DDD.
+///
+///
+/// EN: Value objects are immutable and compared by their values, not identity.
+/// VI: Value objects là bất biến và được so sánh theo giá trị, không phải định danh.
+///
+public abstract class ValueObject
+{
+ ///
+ /// EN: Get the atomic values that make up this value object.
+ /// VI: Lấy các giá trị nguyên tử tạo nên value object này.
+ ///
+ protected abstract IEnumerable