# 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`. | **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` 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), equality by ID - **IAggregateRoot**: Marker interface - **IRepository**: 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` 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.