# 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.
