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>
386 lines
17 KiB
Markdown
386 lines
17 KiB
Markdown
# AdsAnalyticsService - Service Documentation
|
|
|
|
## 1. Overview
|
|
|
|
**Purpose**: Ads Analytics microservice for the GoodGo Platform. Provides campaign performance metrics aggregation, custom report generation, breakdown analytics (by age/gender/device/placement), audience insights, performance recommendations, and admin-level platform-wide metrics.
|
|
|
|
**Service Port**: `5015` (Development, via launchSettings.json) / `8080` (Docker/Production)
|
|
|
|
**Database**: PostgreSQL (`ads_analytics_service` on Neon cloud)
|
|
- Connection string configured via `ConnectionStrings:DefaultConnection` or `DATABASE_URL` env var
|
|
- Current default points to Neon: `ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech`
|
|
|
|
**Framework**: .NET 10.0 / C# 14
|
|
|
|
**Architecture**: Clean Architecture + CQRS (MediatR)
|
|
|
|
**Solution Structure**:
|
|
```
|
|
src/
|
|
AdsAnalyticsService.API/ # Web API + Application layer (CQRS handlers)
|
|
AdsAnalyticsService.Domain/ # Domain entities, SeedWork
|
|
AdsAnalyticsService.Infrastructure/ # EF Core DbContext, configs, migrations
|
|
tests/
|
|
AdsAnalyticsService.FunctionalTests/ # Integration tests (WebApplicationFactory)
|
|
AdsAnalyticsService.UnitTests/ # Unit tests (empty project, no test files yet)
|
|
```
|
|
|
|
---
|
|
|
|
## 2. API Endpoints
|
|
|
|
### MetricsController
|
|
**Base Route**: `api/v{version}/ads-analytics`
|
|
|
|
| Method | Route | Description | Status |
|
|
|--------|-------|-------------|--------|
|
|
| `GET` | `campaigns/{id}/metrics?startDate&endDate` | Get aggregated campaign metrics for a date range. Defaults to last 30 days. Returns `CampaignMetricsDto` with impressions, clicks, conversions, spend, revenue, CTR, CPC, CPA, ROAS. | **Implemented** |
|
|
| `GET` | `adsets/{id}/metrics` | Get ad set metrics. | **Placeholder** (returns 501) |
|
|
| `GET` | `ads/{id}/metrics` | Get ad metrics. | **Placeholder** (returns 501) |
|
|
|
|
### ReportsController
|
|
**Base Route**: `api/v{version}/ads-analytics/reports`
|
|
|
|
| Method | Route | Description | Status |
|
|
|--------|-------|-------------|--------|
|
|
| `GET` | `/` | List reports for an advertiser. Params: `advertiserId` (required), `skip`, `take`. Returns `List<ReportListDto>`. | **Implemented** |
|
|
| `POST` | `/` | Create a new custom report. Body: `CreateReportRequest`. Query: `advertiserId`. Returns `Guid` (report ID). | **Implemented** |
|
|
| `GET` | `/{id}` | Get report by ID. | **Placeholder** (returns 501) |
|
|
| `POST` | `/schedule` | Schedule a recurring report. Body: `ScheduleReportRequest`. | **Placeholder** (returns 501) |
|
|
| `GET` | `/{id}/export?format=csv` | Export report in given format. | **Placeholder** (returns 501) |
|
|
|
|
### BreakdownController
|
|
**Base Route**: `api/v{version}/ads-analytics/campaigns`
|
|
|
|
| Method | Route | Description | Status |
|
|
|--------|-------|-------------|--------|
|
|
| `GET` | `/{id}/breakdown?by&startDate&endDate` | Get campaign breakdown by dimension. Valid `by` values: `age`, `gender`, `device`, `placement`. | **Mock data** (returns hardcoded breakdown) |
|
|
|
|
### InsightsController
|
|
**Base Route**: `api/v{version}/ads-analytics/insights`
|
|
|
|
| Method | Route | Description | Status |
|
|
|--------|-------|-------------|--------|
|
|
| `GET` | `/audience?campaignId&startDate&endDate` | Get audience insights (age, gender, location, user count, engagement rate). | **Mock data** |
|
|
| `GET` | `/performance?advertiserId` | Get performance insights and recommendations (Low CTR, High CPA, Budget Underspend). | **Mock data** |
|
|
|
|
### AdminMetricsController
|
|
**Base Route**: `api/v{version}/admin/ads-analytics/metrics`
|
|
|
|
| Method | Route | Description | Status |
|
|
|--------|-------|-------------|--------|
|
|
| `GET` | `/overview?startDate&endDate` | Platform-wide metrics overview (total impressions, clicks, spend, revenue, active campaigns/advertisers, avg CTR, avg ROAS). Defaults to last 30 days. | **Implemented** (queries real DB; active advertisers count is mocked) |
|
|
| `GET` | `/top-campaigns?metric&limit` | Top performing campaigns. Valid `metric` values: `spend`, `impressions`, `clicks`, `revenue`, `roas`. Default limit: 10. | **Implemented** (queries real DB) |
|
|
| `GET` | `/anomalies` | Anomaly detection. | **Placeholder** (returns 501, planned for ML integration) |
|
|
|
|
### AdminReportsController
|
|
**Base Route**: `api/v{version}/admin/ads-analytics/reports`
|
|
|
|
| Method | Route | Description | Status |
|
|
|--------|-------|-------------|--------|
|
|
| `GET` | `/` | List all reports across all advertisers. Params: `skip`, `take` (default 50). | **Implemented** |
|
|
| `DELETE` | `/{id}` | Delete a report by ID (admin only). Returns 204 or 404. | **Implemented** |
|
|
|
|
### Health Endpoints
|
|
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| `GET` | `/health` | Full health check (includes PostgreSQL) |
|
|
| `GET` | `/health/live` | Liveness probe (app is running, no dependency checks) |
|
|
| `GET` | `/health/ready` | Readiness probe (includes PostgreSQL check) |
|
|
|
|
---
|
|
|
|
## 3. Commands
|
|
|
|
### CreateReportCommand
|
|
- **File**: `src/AdsAnalyticsService.API/Application/Commands/CreateReportCommand.cs`
|
|
- **Handler**: `CreateReportCommandHandler.cs`
|
|
- **Parameters**:
|
|
- `AdvertiserId` (Guid) - The advertiser who owns the report
|
|
- `Name` (string) - Report name
|
|
- `ReportType` (ReportType enum) - Campaign, AdSet, Ad, or Audience
|
|
- `StartDate` (DateTime) - Report period start
|
|
- `EndDate` (DateTime) - Report period end
|
|
- **Returns**: `Guid` (the newly created report's ID)
|
|
- **Behavior**: Creates a new `Report` aggregate, persists it via `AdsAnalyticsServiceContext`, and logs the creation. The report is created with status `Pending`.
|
|
- **Note**: Handler injects `AdsAnalyticsServiceContext` directly (no repository pattern used for this command).
|
|
|
|
---
|
|
|
|
## 4. Queries
|
|
|
|
### GetCampaignMetricsQuery
|
|
- **File**: `src/AdsAnalyticsService.API/Application/Queries/GetCampaignMetricsQuery.cs`
|
|
- **Handler**: `GetCampaignMetricsQueryHandler.cs`
|
|
- **Parameters**:
|
|
- `CampaignId` (Guid)
|
|
- `StartDate` (DateTime)
|
|
- `EndDate` (DateTime)
|
|
- **Returns**: `CampaignMetricsDto?` (null if no metrics found)
|
|
- `CampaignId`, `StartDate`, `EndDate`
|
|
- `TotalImpressions` (long), `TotalClicks` (long), `TotalConversions` (long)
|
|
- `TotalSpend` (decimal), `TotalRevenue` (decimal)
|
|
- `CTR` (decimal, percentage), `CPC` (decimal), `CPA` (decimal), `ROAS` (decimal)
|
|
- **Behavior**: Queries `CampaignMetrics` table filtered by campaign ID and date range, aggregates totals and calculates KPIs (CTR, CPC, CPA, ROAS).
|
|
|
|
### GetReportsQuery
|
|
- **File**: `src/AdsAnalyticsService.API/Application/Queries/GetReportsQuery.cs`
|
|
- **Handler**: Inline in same file (`GetReportsQueryHandler`)
|
|
- **Parameters**:
|
|
- `AdvertiserId` (Guid)
|
|
- `Skip` (int, default 0)
|
|
- `Take` (int, default 20)
|
|
- **Returns**: `List<ReportListDto>` ordered by `CreatedAt` descending
|
|
- Each item: `Id`, `Name`, `ReportType`, `StartDate`, `EndDate`, `Status`, `CreatedAt`
|
|
|
|
---
|
|
|
|
## 5. Domain Model
|
|
|
|
### Aggregates
|
|
|
|
#### CampaignMetrics (Aggregate Root)
|
|
- **File**: `src/AdsAnalyticsService.Domain/AggregatesModel/MetricsAggregate/CampaignMetrics.cs`
|
|
- **Extends**: `Entity`, implements `IAggregateRoot`
|
|
- **Private Fields**: `_campaignId`, `_date`, `_impressions`, `_clicks`, `_conversions`, `_spend`, `_revenue`
|
|
- **Public Getters**: `CampaignId`, `Date`, `Impressions`, `Clicks`, `Conversions`, `Spend`, `Revenue`
|
|
- **Calculated Properties** (not persisted):
|
|
- `CTR` = clicks / impressions * 100
|
|
- `CPC` = spend / clicks
|
|
- `CPA` = spend / conversions
|
|
- `ROAS` = revenue / spend
|
|
- **Behavior Methods**:
|
|
- `RecordImpression()` - Increments impressions counter
|
|
- `RecordClick()` - Increments clicks counter
|
|
- `RecordConversion()` - Increments conversions counter
|
|
- `AddSpend(decimal amount)` - Adds to spend total
|
|
- `AddRevenue(decimal amount)` - Adds to revenue total
|
|
- **Constructor**: `CampaignMetrics(Guid campaignId, DateTime date)` - Creates new metrics record, normalizes date to start of day
|
|
|
|
#### Report (Aggregate Root)
|
|
- **File**: `src/AdsAnalyticsService.Domain/AggregatesModel/ReportAggregate/Report.cs`
|
|
- **Extends**: `Entity`, implements `IAggregateRoot`
|
|
- **Private Fields**: `_advertiserId`, `_name`, `_reportType`, `_startDate`, `_endDate`, `_status`, `_dataJson`
|
|
- **Public Getters**: `AdvertiserId`, `Name`, `ReportType`, `StartDate`, `EndDate`, `Status`, `DataJson`, `CreatedAt`
|
|
- **Behavior Methods**:
|
|
- `MarkAsProcessing()` - Sets status to Processing
|
|
- `Complete(string dataJson)` - Sets data and status to Completed
|
|
- `Fail()` - Sets status to Failed
|
|
- **Constructor**: `Report(Guid advertiserId, string name, ReportType reportType, DateTime startDate, DateTime endDate)` - Creates report with status `Pending` and `CreatedAt = DateTime.UtcNow`
|
|
|
|
### Enumerations
|
|
|
|
#### ReportType (enum)
|
|
- `Campaign = 1`
|
|
- `AdSet = 2`
|
|
- `Ad = 3`
|
|
- `Audience = 4`
|
|
|
|
#### ReportStatus (enum)
|
|
- `Pending = 1`
|
|
- `Processing = 2`
|
|
- `Completed = 3`
|
|
- `Failed = 4`
|
|
|
|
### SeedWork (Base Classes)
|
|
- **Entity**: Base class with `Id` (Guid), `DomainEvents` (IReadOnlyCollection<INotification>), equality by ID
|
|
- **IAggregateRoot**: Marker interface
|
|
- **IRepository<T>**: Generic repository interface with `UnitOfWork` property
|
|
- **IUnitOfWork**: `SaveChangesAsync()`, `SaveEntitiesAsync()` (with domain event dispatch)
|
|
- **ValueObject**: Immutable value comparison base class
|
|
- **Enumeration**: Type-safe enum pattern (not used by current aggregates)
|
|
|
|
### Exceptions
|
|
- **DomainException**: Base domain exception class
|
|
- **AdsAnalyticsDomainException**: Service-specific domain exception (extends Exception, not DomainException)
|
|
|
|
---
|
|
|
|
## 6. Database Schema
|
|
|
|
**Database**: `ads_analytics_service` (PostgreSQL)
|
|
**Migration**: `20260117181438_InitialCreate`
|
|
|
|
### Table: `campaign_metrics`
|
|
|
|
| Column | Type | Nullable | Default | Notes |
|
|
|--------|------|----------|---------|-------|
|
|
| `id` | uuid | NOT NULL | - | Primary key |
|
|
| `campaign_id` | uuid | NOT NULL | - | Reference to campaign (no FK constraint) |
|
|
| `date` | date | NOT NULL | - | Daily aggregation date |
|
|
| `impressions` | bigint | NOT NULL | 0 | Number of ad impressions |
|
|
| `clicks` | bigint | NOT NULL | 0 | Number of ad clicks |
|
|
| `conversions` | bigint | NOT NULL | 0 | Number of conversions |
|
|
| `spend` | decimal(18,2) | NOT NULL | 0 | Ad spend amount |
|
|
| `revenue` | decimal(18,2) | NOT NULL | 0 | Revenue generated |
|
|
|
|
**Indexes**:
|
|
- `PK_campaign_metrics` - Primary key on `id`
|
|
- `idx_campaign_metrics_campaign_id` - Index on `campaign_id`
|
|
- `idx_campaign_metrics_campaign_date` - **Unique** composite index on (`campaign_id`, `date`) - enforces one metrics record per campaign per day
|
|
|
|
### Table: `reports`
|
|
|
|
| Column | Type | Nullable | Default | Notes |
|
|
|--------|------|----------|---------|-------|
|
|
| `id` | uuid | NOT NULL | - | Primary key |
|
|
| `advertiser_id` | uuid | NOT NULL | - | Reference to advertiser (no FK constraint) |
|
|
| `name` | varchar(200) | NOT NULL | - | Report name |
|
|
| `report_type` | integer | NOT NULL | - | Enum: 1=Campaign, 2=AdSet, 3=Ad, 4=Audience |
|
|
| `start_date` | date | NOT NULL | - | Report period start |
|
|
| `end_date` | date | NOT NULL | - | Report period end |
|
|
| `status` | integer | NOT NULL | - | Enum: 1=Pending, 2=Processing, 3=Completed, 4=Failed |
|
|
| `data_json` | jsonb | NULL | - | Report data stored as JSON |
|
|
| `created_at` | timestamp with time zone | NOT NULL | - | Creation timestamp |
|
|
|
|
**Indexes**:
|
|
- `PK_reports` - Primary key on `id`
|
|
- `idx_reports_advertiser_id` - Index on `advertiser_id`
|
|
- `idx_reports_status` - Index on `status`
|
|
- `idx_reports_created_at` - Index on `created_at`
|
|
|
|
### Idempotency: `ClientRequest` (entity exists but no EF configuration/table migration)
|
|
- `Id` (Guid), `Name` (string), `Time` (DateTime)
|
|
- Used via `IRequestManager` / `RequestManager` but **no corresponding table is created in the migration** -- this will fail at runtime if invoked.
|
|
|
|
---
|
|
|
|
## 7. Integration Events
|
|
|
|
**No integration events are defined.** The service does not publish or consume any cross-service events via RabbitMQ or any other message broker.
|
|
|
|
**No domain events are raised** by either aggregate. The `CampaignMetrics` and `Report` entities have domain event support inherited from the `Entity` base class, but neither constructor nor behavior method calls `AddDomainEvent()`.
|
|
|
|
---
|
|
|
|
## 8. Dependencies
|
|
|
|
### NuGet Packages
|
|
|
|
**API Layer** (`AdsAnalyticsService.API`):
|
|
| Package | Version |
|
|
|---------|---------|
|
|
| MediatR | 12.4.1 |
|
|
| FluentValidation | 11.11.0 |
|
|
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
|
|
| Microsoft.EntityFrameworkCore.Design | 10.0.2 |
|
|
| 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 |
|
|
|
|
**Domain Layer** (`AdsAnalyticsService.Domain`):
|
|
| Package | Version |
|
|
|---------|---------|
|
|
| MediatR.Contracts | 2.0.1 |
|
|
|
|
**Infrastructure Layer** (`AdsAnalyticsService.Infrastructure`):
|
|
| 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 |
|
|
|
|
**Test Projects**:
|
|
| Package | Version | Project |
|
|
|---------|---------|---------|
|
|
| xunit | 2.9.2 | Both |
|
|
| FluentAssertions | 6.12.2 | Both |
|
|
| Moq | 4.20.72 | UnitTests only |
|
|
| Microsoft.AspNetCore.Mvc.Testing | 10.0.0 | FunctionalTests |
|
|
| Microsoft.EntityFrameworkCore.InMemory | 10.0.0 | FunctionalTests |
|
|
| Testcontainers.PostgreSql | 4.1.0 | FunctionalTests |
|
|
| coverlet.collector | 6.0.2 | Both |
|
|
|
|
**Global Build Properties** (`Directory.Build.props`):
|
|
- `net10.0`, `LangVersion 14.0`, `Nullable enable`, `TreatWarningsAsErrors true`
|
|
- `Microsoft.SourceLink.GitHub 8.0.0`
|
|
|
|
### External Service Dependencies
|
|
- **PostgreSQL** (Neon cloud) - Primary data store
|
|
- **Redis** - Configured in appsettings (`localhost:6379`) but **not used in any code** (StackExchange.Redis is a NuGet dependency only)
|
|
- **No other service dependencies** - This service does not call any other microservices
|
|
|
|
---
|
|
|
|
## 9. Configuration
|
|
|
|
### appsettings.json
|
|
```json
|
|
{
|
|
"ConnectionStrings": {
|
|
"DefaultConnection": "postgresql://...@neon.tech/ads_analytics_service?sslmode=require"
|
|
},
|
|
"Redis": {
|
|
"ConnectionString": "localhost:6379"
|
|
},
|
|
"Jwt": {
|
|
"Secret": "your-super-secret-key-min-32-characters",
|
|
"Issuer": "goodgo-platform",
|
|
"Audience": "goodgo-services",
|
|
"AccessTokenExpiryMinutes": 15,
|
|
"RefreshTokenExpiryDays": 7
|
|
}
|
|
}
|
|
```
|
|
|
|
### Environment Variables (from .env.example)
|
|
| Variable | Description | Default |
|
|
|----------|-------------|---------|
|
|
| `ASPNETCORE_ENVIRONMENT` | Runtime environment | Development |
|
|
| `DATABASE_URL` | PostgreSQL connection string (fallback) | localhost:5432 |
|
|
| `REDIS_URL` | Redis connection string | localhost:6379 |
|
|
| `REDIS_PASSWORD` | Redis password | (empty) |
|
|
| `JWT_SECRET` | JWT signing key | - |
|
|
| `JWT_ISSUER` | JWT issuer | goodgo-platform |
|
|
| `JWT_AUDIENCE` | JWT audience | goodgo-services |
|
|
| `API_PORT` | API port | 5000 |
|
|
| `LOG_LEVEL` | Serilog minimum level | Information |
|
|
| `SEQ_URL` | Seq logging endpoint | http://localhost:5341 |
|
|
|
|
### Connection String Resolution Order
|
|
1. `ConnectionStrings:DefaultConnection` from appsettings
|
|
2. `DATABASE_URL` environment variable (fallback)
|
|
|
|
### MediatR Pipeline (order)
|
|
1. `LoggingBehavior` - Logs request name, elapsed time, errors (with Stopwatch)
|
|
2. `ValidatorBehavior` - Runs FluentValidation validators (throws `ValidationException` on failure)
|
|
3. `TransactionBehavior` - Wraps Commands in DB transaction (skips Queries by name suffix check); uses `ExecutionStrategy` for retry
|
|
|
|
### Docker Configuration
|
|
- Multi-stage build: `sdk:10.0` (build) -> `aspnet:10.0` (runtime)
|
|
- Non-root user: `dotnetuser` (UID/GID 1001)
|
|
- Exposed 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 available at `/swagger` in Development environment
|
|
- CORS: Allow any origin/method/header (open policy)
|
|
|
|
---
|
|
|
|
## 10. Notable Gaps & Observations
|
|
|
|
1. **No authentication/authorization**: No `[Authorize]` attributes on any controller. JWT config exists in appsettings but is not wired up in `Program.cs`.
|
|
2. **No FluentValidation validators defined**: `ValidatorBehavior` is registered but no `AbstractValidator<T>` implementations exist for any command.
|
|
3. **No repository interfaces/implementations**: Both `CreateReportCommandHandler` and query handlers inject `AdsAnalyticsServiceContext` directly instead of using the repository pattern defined in SeedWork.
|
|
4. **Mock data endpoints**: BreakdownController and InsightsController return hardcoded mock data, not from the database.
|
|
5. **ClientRequest table missing**: `IRequestManager`/`RequestManager` are registered in DI but the `ClientRequest` entity has no EF configuration and no migration creates its table.
|
|
6. **Redis not wired**: Redis NuGet package is included and connection string is configured, but no Redis usage exists in the codebase.
|
|
7. **Dapper not used**: Dapper NuGet package is included but never referenced in any query handler.
|
|
8. **No unit tests**: The `AdsAnalyticsService.UnitTests` project exists but contains no test files.
|
|
9. **Domain events unused**: Neither aggregate raises domain events despite inheriting the capability.
|
|
10. **No API response wrapper**: Controllers return raw DTOs or anonymous objects, not the standard `{ success: bool, data: T }` format documented in CLAUDE.md.
|