Each SERVICE_DOCS.md documents: Overview, API Endpoints, Commands, Queries, Domain Model, Database Schema, Integration Events, Dependencies, Configuration. Generated by 23 parallel audit agents reading actual source code. Key corrections from audit: - inventory-service: 12 commands/6 queries (was listed as scaffold) - promotion-service: 12 commands/10 queries (was listed as 0) - mission-service: 4 commands/7 queries (was listed as 0) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
442 lines
20 KiB
Markdown
442 lines
20 KiB
Markdown
# AdsServingService - Service Documentation
|
|
|
|
## 1. Overview
|
|
|
|
**Purpose**: Real-time ad serving microservice that implements a second-price auction system. It receives ad serve requests, runs real-time bidding (RTB) auctions among eligible ad candidates, determines winners via eCPM scoring, and tracks impressions/clicks. It also provides admin endpoints for monitoring auctions, budget pacing, and frequency capping.
|
|
|
|
**Port**: `5012` (Development, via launchSettings.json)
|
|
**Docker Port**: `8080` (Production, via `ASPNETCORE_URLS=http://+:8080`)
|
|
**Database**: `ads_serving_service` (PostgreSQL, Neon cloud in appsettings.json)
|
|
**Framework**: .NET 10.0, C# 14
|
|
**Solution File**: `AdsServingService.slnx`
|
|
**Initial Migration**: `20260117181413_InitialCreate`
|
|
|
|
---
|
|
|
|
## 2. API Endpoints
|
|
|
|
### AdsController (`api/v1/ads`)
|
|
|
|
| Method | Route | Description | Request Body | Response |
|
|
|--------|-------|-------------|--------------|----------|
|
|
| POST | `/api/v1/ads/serve` | Serve an ad via real-time auction (target < 100ms) | `ServeAdRequest` | `200 OK` with `ServedAdDto` or `204 NoContent` if no eligible ads |
|
|
| POST | `/api/v1/ads/events/impression` | Track ad impression (fire-and-forget) | `ImpressionEvent` | `202 Accepted` |
|
|
| POST | `/api/v1/ads/events/click` | Track ad click (fire-and-forget) | `ClickEvent` | `202 Accepted` |
|
|
|
|
**Request/Response DTOs (AdsController)**:
|
|
|
|
```
|
|
ServeAdRequest {
|
|
UserId: Guid
|
|
PlacementType: string (default "feed")
|
|
UserContext: Dictionary<string, string>? (optional)
|
|
}
|
|
|
|
ServedAdDto {
|
|
AdId: Guid
|
|
CampaignId: Guid
|
|
AdFormat: string
|
|
Headline: string?
|
|
PrimaryText: string?
|
|
CallToAction: string?
|
|
CreativeUrl: string?
|
|
DestinationUrl: string?
|
|
FinalPrice: decimal
|
|
ServedAt: DateTime
|
|
}
|
|
|
|
ImpressionEvent { AdId: Guid, UserId: Guid, Timestamp: DateTime }
|
|
ClickEvent { AdId: Guid, UserId: Guid, Timestamp: DateTime }
|
|
```
|
|
|
|
### AdminAuctionsController (`api/v1/admin/auctions`)
|
|
|
|
| Method | Route | Description | Query Params | Response |
|
|
|--------|-------|-------------|--------------|----------|
|
|
| GET | `/api/v1/admin/auctions` | Paginated list of auctions with filters | `userId`, `placementType`, `startDate`, `endDate`, `page` (default 1), `pageSize` (default 20) | `200 OK` with `PagedResult<AuctionDto>` |
|
|
| GET | `/api/v1/admin/auctions/statistics` | Aggregate auction statistics | none | `200 OK` with `AuctionStatisticsDto` |
|
|
|
|
### AdminBudgetController (`api/v1/admin/budget`)
|
|
|
|
| Method | Route | Description | Query Params | Response |
|
|
|--------|-------|-------------|--------------|----------|
|
|
| GET | `/api/v1/admin/budget/pacers` | Paginated list of budget pacers | `campaignId`, `page` (default 1), `pageSize` (default 20) | `200 OK` with `PagedResult<BudgetPacerDto>` |
|
|
| GET | `/api/v1/admin/budget/campaigns/{campaignId}` | Budget status for a specific campaign | none | `200 OK` with `BudgetPacerDto` or `404` |
|
|
| PUT | `/api/v1/admin/budget/campaigns/{campaignId}/reset` | Reset daily spend for a campaign | none | `200 OK` or `404` |
|
|
| GET | `/api/v1/admin/budget/statistics` | Budget utilization statistics | none | `200 OK` with `BudgetStatisticsDto` |
|
|
|
|
### AdminFrequencyController (`api/v1/admin/frequency`)
|
|
|
|
| Method | Route | Description | Query Params | Response |
|
|
|--------|-------|-------------|--------------|----------|
|
|
| GET | `/api/v1/admin/frequency/caps` | List frequency caps | `adId` (optional filter) | `200 OK` with `List<FrequencyCapDto>` |
|
|
| GET | `/api/v1/admin/frequency/caps/{id}` | Get frequency cap by ID | none | `200 OK` with `FrequencyCapDto` or `404` |
|
|
| POST | `/api/v1/admin/frequency/caps` | Create a new frequency cap | body: `CreateFrequencyCapRequest` | `201 Created` with `FrequencyCapDto` |
|
|
| DELETE | `/api/v1/admin/frequency/caps/{id}` | Delete a frequency cap | none | `204 NoContent` or `404` |
|
|
|
|
**Admin DTOs**:
|
|
|
|
```
|
|
AuctionDto { Id, UserId, PlacementType, AuctionTime, BidCount, WinningAdId?, FinalPrice?, WinningeCPM? }
|
|
AuctionStatisticsDto { TotalAuctions, AverageWinRate, AverageeCPM, TotalBidsPlaced }
|
|
BudgetPacerDto { Id, CampaignId, DailyBudget, SpentToday, RemainingBudget, UtilizationPercent, Strategy, LastUpdated }
|
|
BudgetStatisticsDto { TotalCampaigns, TotalDailyBudget, TotalSpentToday, AverageUtilization, CampaignsExceeded }
|
|
FrequencyCapDto { Id, AdId, MaxImpressionsPerUser, Window }
|
|
CreateFrequencyCapRequest { AdId: Guid, MaxImpressionsPerUser: int, Window: string (default "Day") }
|
|
PagedResult<T> { Items, TotalCount, Page, PageSize, TotalPages (computed) }
|
|
```
|
|
|
|
### Health Check Endpoints
|
|
|
|
| Route | Description |
|
|
|-------|-------------|
|
|
| `/health` | Full health check (includes PostgreSQL) |
|
|
| `/health/live` | Liveness probe (app is running, no dependency checks) |
|
|
| `/health/ready` | Readiness probe (includes PostgreSQL check) |
|
|
|
|
---
|
|
|
|
## 3. Commands
|
|
|
|
**No MediatR Commands exist in this service.** The service is primarily read/query-oriented. Write operations (frequency cap CRUD, budget reset) are handled directly in controllers via DbContext without MediatR commands.
|
|
|
|
---
|
|
|
|
## 4. Queries
|
|
|
|
### ServeAdQuery
|
|
- **File**: `Application/Queries/ServeAdQuery.cs`
|
|
- **Parameters**: `UserId` (Guid), `PlacementType` (string), `UserContext` (Dictionary<string, string>)
|
|
- **Returns**: `ServedAdDto?` (nullable - returns null if no eligible ads or auction fails)
|
|
- **Handler**: `ServeAdQueryHandler` - orchestrates the full RTB auction pipeline:
|
|
1. Fetches eligible ads via `IEligibleAdsProvider`
|
|
2. Creates an `Auction` domain entity
|
|
3. Scores each candidate via `IAuctionScoringService` (predicted CTR + quality score)
|
|
4. Adds bids to auction
|
|
5. Runs second-price auction (winner determined by highest eCPM)
|
|
6. Persists auction to database (bids stored as JSONB)
|
|
7. Publishes `AdServedEvent` via `IAdServingEventPublisher`
|
|
8. Returns winning ad details with final price
|
|
|
|
### GetAuctionsQuery
|
|
- **File**: `Application/Queries/GetAuctionsQuery.cs`
|
|
- **Parameters**: `UserId?` (Guid), `PlacementType?` (string), `StartDate?` (DateTime), `EndDate?` (DateTime), `Page` (int, default 1), `PageSize` (int, default 20)
|
|
- **Returns**: `PagedResult<AuctionDto>` - paginated auction history with bid counts parsed from JSONB
|
|
- **Handler**: `GetAuctionsQueryHandler` - queries auctions from DB with optional filters, ordered by auction_time descending
|
|
|
|
### GetAuctionStatisticsQuery
|
|
- **File**: `Application/Queries/GetAuctionStatisticsQuery.cs`
|
|
- **Parameters**: none
|
|
- **Returns**: `AuctionStatisticsDto` with TotalAuctions, AverageWinRate (%), AverageeCPM, TotalBidsPlaced
|
|
- **Handler**: `GetAuctionStatisticsQueryHandler` - loads all auctions into memory and computes aggregates (bid counts parsed from JSONB)
|
|
|
|
### GetBudgetPacersQuery
|
|
- **File**: `Application/Queries/GetBudgetPacersQuery.cs`
|
|
- **Parameters**: `CampaignId?` (Guid), `Page` (int, default 1), `PageSize` (int, default 20)
|
|
- **Returns**: `PagedResult<BudgetPacerDto>` - paginated budget pacer list ordered by last_updated descending
|
|
- **Handler**: `GetBudgetPacersQueryHandler`
|
|
|
|
---
|
|
|
|
## 5. Domain Model
|
|
|
|
### Aggregates
|
|
|
|
#### Auction (AuctionAggregate)
|
|
- **File**: `Domain/AggregatesModel/AuctionAggregate/Auction.cs`
|
|
- **Type**: Entity + IAggregateRoot
|
|
- **Fields**: `_bids` (List<Bid>), `_result` (AuctionResult?), `_auctionTime` (DateTime)
|
|
- **Public Properties**: `UserId`, `PlacementType`, `Bids` (IReadOnlyCollection), `Result`, `AuctionTime`
|
|
- **Constructor**: `Auction(Guid userId, string placementType)` - generates new Id, sets auction time to UTC now
|
|
- **Behavior Methods**:
|
|
- `AddBid(adId, campaignId, bidAmount, predictedCTR, qualityScore)` - adds a Bid to the auction
|
|
- `RunAuction()` - sorts bids by eCPM descending, selects winner, calculates second-price (second-highest eCPM + 0.01)
|
|
|
|
#### Bid (child entity of Auction)
|
|
- **Type**: Entity (not aggregate root)
|
|
- **Properties**: `AdId`, `CampaignId`, `BidAmount`, `PredictedCTR`, `QualityScore`
|
|
- **Computed**: `eCPM = BidAmount * PredictedCTR * QualityScore`
|
|
- **Note**: Bids are stored as JSONB in the auctions table, not as a separate table. The `Bids` navigation is ignored by EF Core.
|
|
|
|
#### FrequencyCap (FrequencyAggregate)
|
|
- **File**: `Domain/AggregatesModel/FrequencyAggregate/FrequencyCap.cs`
|
|
- **Type**: Entity + IAggregateRoot
|
|
- **Fields**: `_adId` (Guid), `_maxImpressionsPerUser` (int), `_window` (FrequencyWindow)
|
|
- **Constructor**: `FrequencyCap(Guid adId, int maxImpressionsPerUser, FrequencyWindow window)` - validates maxImpressions > 0
|
|
- **Behavior Methods**:
|
|
- `IsUserCapped(int currentImpressions)` - returns true if user has reached the cap
|
|
- `Daily(Guid adId, int maxImpressions)` - static factory for daily caps
|
|
|
|
#### BudgetPacer (PacingAggregate)
|
|
- **File**: `Domain/AggregatesModel/PacingAggregate/BudgetPacer.cs`
|
|
- **Type**: Entity + IAggregateRoot
|
|
- **Fields**: `_campaignId` (Guid), `_dailyBudget` (decimal), `_spentToday` (decimal), `_strategy` (PacingStrategy), `_lastUpdated` (DateTime)
|
|
- **Computed Properties**: `RemainingBudget = _dailyBudget - _spentToday`, `UtilizationPercent = (_spentToday / _dailyBudget) * 100`
|
|
- **Constructor**: `BudgetPacer(Guid campaignId, decimal dailyBudget, PacingStrategy strategy)` - validates dailyBudget > 0
|
|
- **Behavior Methods**:
|
|
- `CanServeAd(decimal estimatedCost)` - checks budget availability; Smooth strategy enforces hourly pacing with 20% buffer
|
|
- `RecordSpend(decimal amount)` - adds spend, validates non-negative
|
|
- `ResetDailySpend()` - resets spent to 0
|
|
|
|
### Value Objects
|
|
|
|
#### AuctionResult
|
|
- **Properties**: `WinningAdId` (Guid), `WinningCampaignId` (Guid), `FinalPrice` (decimal), `WinningeCPM` (decimal)
|
|
- **Equality**: based on WinningAdId + FinalPrice
|
|
- **Mapped as**: Owned type within Auction table
|
|
|
|
#### UserAdHistory
|
|
- **Properties**: `UserId` (Guid), `AdImpressionCounts` (Dictionary<Guid, int>), `Date` (DateTime)
|
|
- **Methods**: `RecordImpression(Guid adId)`, `GetImpressionCount(Guid adId)`
|
|
- **Note**: Designed for Redis caching (key format `freq:{userId}:{date}`), not persisted in PostgreSQL
|
|
|
|
### Enumerations
|
|
|
|
#### FrequencyWindow (enum)
|
|
- `Hour = 1`, `Day = 2`, `Week = 3`, `Month = 4`, `Lifetime = 5`
|
|
|
|
#### PacingStrategy (enum)
|
|
- `Smooth = 1` (spread evenly throughout the day)
|
|
- `Accelerated = 2` (spend as fast as possible)
|
|
|
|
### Exceptions
|
|
|
|
- **AdsServingDomainException** - business rule violations specific to ads serving
|
|
- **DomainException** - generic domain exception base class
|
|
|
|
### SeedWork (Base Classes)
|
|
|
|
- `Entity` - base entity with `Id` (Guid), `DomainEvents` (IReadOnlyCollection<INotification>), equality by Id
|
|
- `ValueObject` - immutable value comparison base
|
|
- `Enumeration` - type-safe enum pattern with parse/lookup
|
|
- `IAggregateRoot` - marker interface
|
|
- `IRepository<T>` - generic repository with `IUnitOfWork`
|
|
- `IUnitOfWork` - `SaveChangesAsync()`, `SaveEntitiesAsync()` (dispatches domain events)
|
|
|
|
---
|
|
|
|
## 6. Database Schema
|
|
|
|
**Database**: PostgreSQL (Neon cloud)
|
|
**Migration**: `20260117181413_InitialCreate`
|
|
|
|
### Table: `auctions`
|
|
|
|
| Column | Type | Nullable | Description |
|
|
|--------|------|----------|-------------|
|
|
| `id` | uuid | NOT NULL (PK) | Auction ID |
|
|
| `user_id` | uuid | NOT NULL | User who triggered the ad request |
|
|
| `placement_type` | varchar(50) | NOT NULL | Ad placement type (feed, story, banner) |
|
|
| `auction_time` | timestamp with time zone | NOT NULL | When the auction occurred |
|
|
| `bids` | jsonb | NULL | All bids serialized as JSON array |
|
|
| `winning_ad_id` | uuid | NULL | Winning ad ID (owned AuctionResult) |
|
|
| `winning_campaign_id` | uuid | NULL | Winning campaign ID (owned AuctionResult) |
|
|
| `final_price` | numeric(18,4) | NULL | Second-price auction final price (owned AuctionResult) |
|
|
| `winning_ecpm` | numeric(18,4) | NULL | Winning bid eCPM (owned AuctionResult) |
|
|
|
|
**Indexes**:
|
|
- `ix_auctions_user_id` on `user_id`
|
|
- `ix_auctions_placement_type` on `placement_type`
|
|
- `ix_auctions_auction_time` on `auction_time`
|
|
- `ix_auctions_placement_user` composite on (`placement_type`, `user_id`)
|
|
|
|
### Table: `budget_pacers`
|
|
|
|
| Column | Type | Nullable | Description |
|
|
|--------|------|----------|-------------|
|
|
| `id` | uuid | NOT NULL (PK) | Pacer ID |
|
|
| `campaign_id` | uuid | NOT NULL | Associated campaign ID |
|
|
| `daily_budget` | numeric(18,4) | NOT NULL | Daily budget limit |
|
|
| `spent_today` | numeric(18,4) | NOT NULL | Amount spent today |
|
|
| `strategy` | varchar(20) | NOT NULL | Pacing strategy ("Smooth" or "Accelerated") |
|
|
| `last_updated` | timestamp with time zone | NOT NULL | Last update timestamp |
|
|
|
|
**Indexes**:
|
|
- `ix_budget_pacers_campaign_id` UNIQUE on `campaign_id`
|
|
- `ix_budget_pacers_last_updated` on `last_updated`
|
|
|
|
### Table: `frequency_caps`
|
|
|
|
| Column | Type | Nullable | Description |
|
|
|--------|------|----------|-------------|
|
|
| `id` | uuid | NOT NULL (PK) | Frequency cap ID |
|
|
| `ad_id` | uuid | NOT NULL | Associated ad ID |
|
|
| `max_impressions_per_user` | integer | NOT NULL | Max impressions per user in window |
|
|
| `window` | varchar(20) | NOT NULL | Time window ("Hour", "Day", "Week", "Month", "Lifetime") |
|
|
|
|
**Indexes**:
|
|
- `ix_frequency_caps_ad_id` on `ad_id`
|
|
|
|
---
|
|
|
|
## 7. Integration Events
|
|
|
|
The service defines an event publishing abstraction (`IAdServingEventPublisher`) with three event types. The **current implementation** (`LoggingAdServingEventPublisher`) only logs events -- no actual message broker integration exists yet.
|
|
|
|
### Published Events
|
|
|
|
| Event | Topic/Key | Payload Fields |
|
|
|-------|-----------|----------------|
|
|
| `AdServedEvent` | `ad.served.v1` | AuctionId, AdId, CampaignId, UserId, PlacementType, FinalPrice, WinningEcpm, ServedAt |
|
|
| `AdImpressionTrackedEvent` | `ads.impression.tracked.v1` | AdId, UserId, Timestamp |
|
|
| `AdClickTrackedEvent` | `ads.click.tracked.v1` | AdId, UserId, Timestamp |
|
|
|
|
### Consumed Events
|
|
|
|
None. The service does not consume any external events.
|
|
|
|
### Application Services
|
|
|
|
#### IEligibleAdsProvider / InMemoryEligibleAdsProvider
|
|
- Returns eligible ad candidates for a serve request
|
|
- **Current implementation**: Returns 2 hardcoded in-memory candidates with deterministic IDs (based on userId + placementType hash)
|
|
- Candidate fields: AdId, CampaignId, BidAmount, Format, Headline, PrimaryText, CallToAction, CreativeUrl, DestinationUrl
|
|
- **Note**: This is a stub. Production would integrate with ads-manager-service to fetch real campaigns/ads.
|
|
|
|
#### IAuctionScoringService / DefaultAuctionScoringService
|
|
- Scores ad candidates to produce `AuctionBidSignals(PredictedCtr, QualityScore)`
|
|
- **Predicted CTR** heuristic: base CTR by placement type (story=0.018, banner=0.012, default=0.020), boosted by user segment ("high-intent" +25%) and recency (purchase within 14 days +10%)
|
|
- **Quality Score** heuristic: by format (single_image=1.0, video=1.1, default=0.95)
|
|
|
|
---
|
|
|
|
## 8. Dependencies
|
|
|
|
### NuGet Packages
|
|
|
|
**API Layer**:
|
|
| Package | Version |
|
|
|---------|---------|
|
|
| MediatR | 12.4.1 |
|
|
| FluentValidation | 11.11.0 |
|
|
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
|
|
| Swashbuckle.AspNetCore | 7.2.0 |
|
|
| Asp.Versioning.Mvc | 8.1.0 |
|
|
| Asp.Versioning.Mvc.ApiExplorer | 8.1.0 |
|
|
| AspNetCore.HealthChecks.NpgSql | 8.0.2 |
|
|
| AspNetCore.HealthChecks.Redis | 8.0.1 |
|
|
| Hellang.Middleware.ProblemDetails | 6.5.1 |
|
|
| Serilog.AspNetCore | 8.0.3 |
|
|
| Serilog.Sinks.Console | 6.0.0 |
|
|
| Serilog.Sinks.Seq | 8.0.0 |
|
|
| Microsoft.EntityFrameworkCore.Design | 10.0.1 |
|
|
|
|
**Domain Layer**:
|
|
| Package | Version |
|
|
|---------|---------|
|
|
| MediatR.Contracts | 2.0.1 |
|
|
|
|
**Infrastructure Layer**:
|
|
| Package | Version |
|
|
|---------|---------|
|
|
| Microsoft.EntityFrameworkCore | 10.0.0 |
|
|
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.0 |
|
|
| Microsoft.EntityFrameworkCore.Tools | 10.0.0 |
|
|
| MediatR | 12.4.1 |
|
|
| Dapper | 2.1.35 |
|
|
| Microsoft.Extensions.Http.Polly | 9.0.0 |
|
|
| Polly | 8.5.0 |
|
|
| StackExchange.Redis | 2.8.16 |
|
|
|
|
### External Service Dependencies
|
|
|
|
- **PostgreSQL** (Neon cloud) - primary data store
|
|
- **Redis** (configured but not actively used in code) - intended for frequency cap caching (UserAdHistory)
|
|
- **ads-manager-service** (not integrated yet) - intended source for eligible ad campaigns (currently stubbed with InMemoryEligibleAdsProvider)
|
|
- **ads-tracking-service** (not integrated yet) - intended consumer of impression/click events (currently logged only)
|
|
|
|
---
|
|
|
|
## 9. Configuration
|
|
|
|
### Connection Strings (appsettings.json)
|
|
|
|
| Key | Description |
|
|
|-----|-------------|
|
|
| `ConnectionStrings:DefaultConnection` | PostgreSQL connection string (Neon cloud endpoint in default config) |
|
|
| `DATABASE_URL` | Alternative env var for connection string (fallback) |
|
|
|
|
### Redis
|
|
|
|
| Key | Default |
|
|
|-----|---------|
|
|
| `Redis:ConnectionString` | `localhost:6379` |
|
|
|
|
### JWT (configured but not enforced -- no `[Authorize]` attributes on controllers)
|
|
|
|
| Key | Default |
|
|
|-----|---------|
|
|
| `Jwt:Secret` | `your-super-secret-key-min-32-characters` |
|
|
| `Jwt:Issuer` | `goodgo-platform` |
|
|
| `Jwt:Audience` | `goodgo-services` |
|
|
| `Jwt:AccessTokenExpiryMinutes` | `15` |
|
|
| `Jwt:RefreshTokenExpiryDays` | `7` |
|
|
|
|
### Environment Variables (.env.example)
|
|
|
|
| Variable | Description |
|
|
|----------|-------------|
|
|
| `ASPNETCORE_ENVIRONMENT` | Runtime environment (Development/Production) |
|
|
| `DATABASE_URL` | PostgreSQL connection string |
|
|
| `REDIS_URL` | Redis connection string |
|
|
| `REDIS_PASSWORD` | Redis password |
|
|
| `JWT_SECRET` | JWT signing key |
|
|
| `API_PORT` | API listening port |
|
|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry exporter endpoint |
|
|
| `SEQ_URL` | Seq logging endpoint |
|
|
| `RATE_LIMIT_PERMITS_PER_MINUTE` | Rate limiting config |
|
|
|
|
### Build Configuration (Directory.Build.props)
|
|
|
|
- Target Framework: `net10.0`
|
|
- Language Version: C# 14
|
|
- Nullable: enabled
|
|
- TreatWarningsAsErrors: true
|
|
- GenerateDocumentationFile: true
|
|
|
|
### Docker
|
|
|
|
- Multi-stage build: `sdk:10.0` (build) -> `aspnet:10.0` (runtime)
|
|
- Non-root user: `dotnetuser` (UID/GID 1001)
|
|
- Port: 8080
|
|
- Health check: `curl -f http://localhost:8080/health/live` (30s interval, 3 retries)
|
|
|
|
### Startup Behavior
|
|
|
|
- Auto-applies EF Core migrations on startup (catches and logs errors without crashing)
|
|
- Swagger UI enabled in Development at `/swagger`
|
|
- CORS: allows any origin/method/header (open)
|
|
- MediatR pipeline: LoggingBehavior -> ValidatorBehavior -> TransactionBehavior (skips transactions for queries)
|
|
|
|
---
|
|
|
|
## 10. Tests
|
|
|
|
### Functional Tests (`tests/AdsServingService.FunctionalTests/`)
|
|
|
|
Uses `CustomWebApplicationFactory` with InMemoryDatabase (unique DB per factory instance).
|
|
|
|
| Test | Description |
|
|
|------|-------------|
|
|
| `ServeAd_ShouldReturnServedAdPayload` | POST /api/v1/ads/serve returns 200 with valid adId and positive finalPrice |
|
|
| `GetAuctionStatistics_AfterServe_ShouldReflectPersistedAuctions` | After serving an ad, statistics show >= 1 auction and >= 2 bids |
|
|
| `TrackImpression_ShouldReturnAccepted` | POST /api/v1/ads/events/impression returns 202 Accepted |
|
|
| `HealthCheck_ShouldReturnHealthy` | GET /health/live returns 200 |
|
|
|
|
### Unit Tests (`tests/AdsServingService.UnitTests/`)
|
|
|
|
Project exists but contains no test files yet (only generated assembly info).
|
|
|
|
---
|
|
|
|
## 11. Notable Architecture Decisions
|
|
|
|
1. **Second-Price Auction**: Winner pays second-highest eCPM + $0.01 (standard RTB model)
|
|
2. **eCPM Formula**: `BidAmount * PredictedCTR * QualityScore`
|
|
3. **Bids stored as JSONB**: Instead of a separate `bids` table, bids are serialized to JSON in the `auctions.bids` column for high-throughput performance. EF Core `OwnsMany` + `ToJson` was avoided due to EF Core 10 compatibility issues.
|
|
4. **No authentication**: Controllers have no `[Authorize]` attributes despite JWT being configured
|
|
5. **Stub eligible ads provider**: `InMemoryEligibleAdsProvider` returns hardcoded candidates -- needs real integration with ads-manager-service
|
|
6. **Logging-only event publisher**: Events are logged but not published to RabbitMQ or any message broker
|
|
7. **No repositories**: The service accesses `AdsServingServiceContext` directly in handlers and controllers (repository pattern is commented out in DependencyInjection.cs)
|
|
8. **Idempotency infrastructure**: `IRequestManager` / `RequestManager` / `ClientRequest` exist but are not used by any command or controller
|