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>
26 KiB
AdsManagerService - Service Documentation
1. Overview
Purpose: Manages advertising campaigns, ad sets, and individual ads for the GoodGo platform. Follows a 3-tier ads structure: Campaign > Ad Set > Ad, similar to Meta/Facebook Ads architecture. Includes admin endpoints for ad review/moderation and reporting.
Port: 5011 (Development, via launchSettings.json), 8080 (Docker/Production)
Database: PostgreSQL - ads_manager_service (Neon cloud in appsettings.json)
Connection String Key: ConnectionStrings:DefaultConnection or DATABASE_URL environment variable
Framework: .NET 10.0, C# 14
Architecture: Clean Architecture (API / Domain / Infrastructure) + CQRS via MediatR
Health Endpoints:
GET /health- Full health check (includes PostgreSQL)GET /health/live- Liveness probe (app is running)GET /health/ready- Readiness probe (dependencies ready)
Auto-migration: EF Core migrations are applied automatically on startup.
2. API Endpoints
2.1 Campaigns Controller
Route prefix: api/v1/ads-manager/campaigns
| Method | Route | Action | Description | Response |
|---|---|---|---|---|
| POST | / |
CreateCampaign | Create a new campaign | 201 Created (Guid) |
| GET | / |
ListCampaigns | List campaigns with filtering and pagination | 200 OK (ListCampaignsResult) |
| GET | /{id} |
GetCampaignById | Get campaign by ID | 200 OK (CampaignDto) / 404 |
| PUT | /{id} |
UpdateCampaign | Update campaign name and description | 204 No Content / 404 |
| POST | /{id}/activate |
ActivateCampaign | Activate a draft/paused campaign | 204 No Content / 404 |
| POST | /{id}/pause |
PauseCampaign | Pause an active campaign | 204 No Content / 404 |
| DELETE | /{id} |
DeleteCampaign | Archive a campaign (soft delete) | 204 No Content / 404 |
ListCampaigns Query Parameters:
advertiserId(Guid?) - Filter by advertiserstatus(string?) - Filter by status (draft, active, paused, completed, archived)objective(string?) - Filter by objective (awareness, traffic, conversion, etc.)searchTerm(string?) - Search by campaign namepage(int, default: 1) - Page numberpageSize(int, default: 20) - Page size
2.2 Ad Sets Controller
Route prefix: api/v1/ads-manager/adsets
| Method | Route | Action | Description | Response |
|---|---|---|---|---|
| POST | / |
CreateAdSet | Create a new ad set | 201 Created (Guid) |
| GET | /{id} |
GetAdSetById | Get ad set by ID | 200 OK (AdSetDto) / 404 |
2.3 Ads Controller
Route prefix: api/v1/ads-manager/ads
| Method | Route | Action | Description | Response |
|---|---|---|---|---|
| POST | / |
CreateAd | Create a new ad | 201 Created (Guid) |
| GET | /{id} |
GetAdById | Get ad by ID | 200 OK (AdDto) / 404 |
| POST | /{id}/submit |
SubmitAdForReview | Submit ad for review | 204 No Content / 404 |
2.4 Audiences Controller
Route prefix: api/v1/ads-manager/audiences
| Method | Route | Action | Description | Response |
|---|---|---|---|---|
| GET | / |
ListAudiences | List audiences for an advertiser | 200 OK (List<AudienceDto>) |
| GET | /{id} |
GetAudienceById | Get audience by ID | 200 OK (AudienceDto) / 404 |
ListAudiences Query Parameters:
advertiserId(Guid) - Required. Filter by advertiser.
Note: The Audiences controller defines its own queries and DTOs inline (ListAudiencesQuery, GetAudienceByIdQuery, AudienceDto) but there are NO handler implementations for these queries in the codebase. These endpoints will fail at runtime.
2.5 Admin Ads Controller
Route prefix: api/v1/admin/ads-manager/ads
| Method | Route | Action | Description | Response |
|---|---|---|---|---|
| GET | /pending |
ListPendingAds | List ads pending review | 200 OK (List<AdDto>) |
| POST | /{id}/approve |
ApproveAd | Approve an ad | 204 No Content / 404 |
| POST | /{id}/reject |
RejectAd | Reject an ad with reason | 204 No Content / 404 |
ListPendingAds Query Parameters:
page(int, default: 1)pageSize(int, default: 20)
RejectAd Request Body: { "reason": "string" }
2.6 Admin Campaigns Controller
Route prefix: api/v1/admin/ads-manager/campaigns
| Method | Route | Action | Description | Response |
|---|---|---|---|---|
| GET | / |
ListAllCampaigns | List all campaigns across all advertisers | 200 OK (ListCampaignsResult) |
| GET | /stats |
GetCampaignStats | Get aggregate campaign statistics | 200 OK (CampaignStatsDto) |
ListAllCampaigns Query Parameters:
status(string?)page(int, default: 1)pageSize(int, default: 50)
2.7 Admin Reports Controller
Route prefix: api/v1/admin/ads-manager/reports
| Method | Route | Action | Description | Response |
|---|---|---|---|---|
| GET | /top-advertisers |
GetTopAdvertisers | Get top advertisers by spend | 200 OK (List<TopAdvertiserDto>) |
| GET | /revenue |
GetRevenueAnalytics | Get revenue analytics | 200 OK (RevenueAnalyticsDto) |
GetTopAdvertisers Query Parameters:
limit(int, default: 10)
GetRevenueAnalytics Query Parameters:
startDate(DateTime?)endDate(DateTime?)
3. Commands
CreateCampaignCommand
- Returns:
Guid(new campaign ID) - Parameters:
AdvertiserId(Guid),Name(string),Description(string?),Objective(string: "awareness"/"traffic"/"conversion"/"app_installs"/"video_views"/"lead_generation"),BudgetType(string: "daily"/"lifetime"),BudgetAmount(decimal),Currency(string, default "VND"),StartDate(DateTime?),EndDate(DateTime?) - Behavior: Creates a Campaign aggregate with Draft status, sets schedule if dates provided, raises CampaignCreatedDomainEvent.
UpdateCampaignCommand
- Returns:
bool - Parameters:
CampaignId(Guid),Name(string),Description(string?) - Behavior: Updates campaign name and description. Rejects if campaign is archived.
ActivateCampaignCommand
- Returns:
bool - Parameters:
CampaignId(Guid) - Behavior: Transitions campaign from Draft/Paused to Active. Raises CampaignActivatedDomainEvent.
PauseCampaignCommand
- Returns:
bool - Parameters:
CampaignId(Guid) - Behavior: Transitions campaign from Active to Paused. Raises CampaignStatusChangedDomainEvent. Uses injected IUnitOfWork (not repository UnitOfWork).
DeleteCampaignCommand
- Returns:
bool - Parameters:
CampaignId(Guid) - Behavior: Soft-deletes by archiving the campaign. Cannot archive active campaigns (must pause first). Raises CampaignStatusChangedDomainEvent.
CreateAdSetCommand
- Returns:
Guid(new ad set ID) - Parameters:
CampaignId(Guid),Name(string),DailyBudget(decimal),BidType(string: "cpc"/"cpm"/"ocpm"/"automatic", default "cpc"),BidAmount(decimal?),MinAge(int?),MaxAge(int?),Genders(string?),Locations(string?),Interests(string?) - Behavior: Creates an AdSet with targeting and bid strategy, in Draft status.
CreateAdCommand
- Returns:
Guid(new ad ID) - Parameters:
AdSetId(Guid),Name(string),Format(string: "single_image"/"single_video"/"carousel"/"collection"/"stories", default "single_image"),Headline(string?),PrimaryText(string?),CallToAction(string?),DestinationUrl(string?),CreativeUrl(string?) - Behavior: Creates an Ad in Draft status with ReviewStatus=NotSubmitted.
SubmitAdForReviewCommand
- Returns:
bool - Parameters:
AdId(Guid) - Behavior: Transitions ad review status from NotSubmitted/Rejected to PendingReview.
ApproveAdCommand (defined in AdminAdsController)
- Returns:
bool - Parameters:
AdId(Guid) - Behavior: Transitions ad review status from PendingReview to Approved.
RejectAdCommand (defined in AdminAdsController)
- Returns:
bool - Parameters:
AdId(Guid),Reason(string) - Behavior: Transitions ad review status from PendingReview to Rejected.
4. Queries
GetCampaignByIdQuery
- Parameters:
CampaignId(Guid) - Returns:
CampaignDto?(Id, AdvertiserId, Name, Description, Status, Objective, BudgetType, BudgetAmount, Currency, TotalSpend, StartDate, EndDate, CreatedAt, UpdatedAt) - Handler: Fetches from ICampaignRepository.
ListCampaignsQuery
- Parameters:
AdvertiserId(Guid?),Status(string?),Objective(string?),SearchTerm(string?),Page(int),PageSize(int) - Returns:
ListCampaignsResult(Items: List<CampaignDto>, TotalCount, Page, PageSize, TotalPages) - Handler: Queries AdsManagerServiceContext directly with filters, pagination, ordered by CreatedAt DESC.
GetAdByIdQuery
- Parameters:
AdId(Guid) - Returns:
AdDto?(Id, AdSetId, Name, Format, Status, ReviewStatus, Headline, PrimaryText, Description, CallToAction, DestinationUrl, CreativeUrl, CreatedAt, UpdatedAt) - Handler: Queries AdsManagerServiceContext directly.
GetAdSetByIdQuery
- Parameters:
AdSetId(Guid) - Returns:
AdSetDto?(Id, CampaignId, Name, Status, DailyBudget, BidType, BidAmount, StartDate, EndDate, Targeting: TargetingDto, CreatedAt, UpdatedAt) - Handler: Queries AdsManagerServiceContext directly. Splits targeting comma-separated strings into lists.
GetCampaignStatsQuery
- Parameters: None
- Returns:
CampaignStatsDto(TotalCampaigns, ActiveCampaigns, PausedCampaigns, DraftCampaigns, CompletedCampaigns, TotalSpend, TotalBudget) - Handler: Loads ALL campaigns into memory and counts in-memory. Warning: potential performance issue with large datasets.
ListPendingAdsQuery (defined in AdminAdsController)
- Parameters:
Page(int),PageSize(int) - Returns:
List<AdDto> - Handler: Filters ads where ReviewStatus.Name == "Pending", ordered by CreatedAt ASC.
GetTopAdvertisersQuery (defined in AdminReportsController)
- Parameters:
Limit(int, default: 10) - Returns:
List<TopAdvertiserDto>(AdvertiserId, TotalCampaigns, TotalSpend, ActiveCampaigns) - Handler: Groups campaigns by AdvertiserId, ordered by TotalSpend DESC.
GetRevenueAnalyticsQuery (defined in AdminReportsController)
- Parameters:
StartDate(DateTime?),EndDate(DateTime?) - Returns:
RevenueAnalyticsDto(TotalRevenue, AverageRevenuePerCampaign, TotalCampaigns, RevenueByObjective: Dictionary) - Handler: Loads campaigns filtered by date range, groups revenue by objective.
ListAudiencesQuery (defined in AudiencesController)
- Parameters:
AdvertiserId(Guid) - Returns:
List<AudienceDto> - Handler: NOT IMPLEMENTED - no handler exists in the codebase.
GetAudienceByIdQuery (defined in AudiencesController)
- Parameters:
AudienceId(Guid) - Returns:
AudienceDto? - Handler: NOT IMPLEMENTED - no handler exists in the codebase.
5. Domain Model
Aggregates
Campaign Aggregate (CampaignAggregate/)
-
Campaign (Entity, IAggregateRoot)
- Fields:
_name,_description,_advertiserId,_status(CampaignStatus),_objective(CampaignObjective),_budget(CampaignBudget),_startDate,_endDate,_createdAt,_updatedAt,TotalSpend - Public IDs:
StatusId,ObjectiveId - Behavior methods:
Update(),SetSchedule(),SetBudget(),Activate(),Pause(),Complete(),Archive(),RecordSpend() - State machine: Draft -> Active <-> Paused -> Completed; Draft/Paused/Completed -> Archived (Active cannot be directly archived)
- Domain events: CampaignCreatedDomainEvent (on create), CampaignActivatedDomainEvent (on activate), CampaignStatusChangedDomainEvent (on pause/complete/archive)
- Fields:
-
CampaignStatus (Enumeration)
- Values: Draft (1), Active (2), Paused (3), Completed (4), Archived (5)
- Names stored as lowercase: "draft", "active", "paused", "completed", "archived"
-
CampaignObjective (Enumeration)
- Values: Awareness (1, "awareness"), Traffic (2, "traffic"), Conversion (3, "conversion"), AppInstalls (4, "app_installs"), VideoViews (5, "video_views"), LeadGeneration (6, "lead_generation")
-
CampaignBudget (ValueObject)
- Properties:
Type(BudgetType enum),Amount(decimal),Currency(string, default "VND") - Factory methods:
Daily(),Lifetime()
- Properties:
-
BudgetType (enum): Daily (1), Lifetime (2)
-
ICampaignRepository:
Add(),Update(),GetByIdAsync(),GetByAdvertiserIdAsync(),GetActiveAsync()
AdSet Aggregate (AdSetAggregate/)
-
AdSet (Entity, IAggregateRoot)
- Fields:
_name,_campaignId,_status(AdSetStatus),_targeting(Targeting),_bidStrategy(BidStrategy),_dailyBudget,_startDate,_endDate,_createdAt,_updatedAt - Public ID:
StatusId - Behavior methods:
Update(),SetTargeting(),SetBidStrategy(),Activate(),Pause() - State machine: Draft -> Active <-> Paused
- Fields:
-
AdSetStatus (Enumeration): Draft (1), Active (2), Paused (3), Archived (4)
-
Targeting (ValueObject)
- Properties:
MinAge(int?),MaxAge(int?),Genders(string?),Locations(string?),Interests(string?),CustomAudienceIds(string?),LookalikeAudienceIds(string?) - All list-type fields stored as comma-separated strings.
- Factory methods:
Broad(),Demographic()
- Properties:
-
BidStrategy (ValueObject)
- Properties:
Type(BidType enum),BidAmount(decimal?),TargetCost(decimal?) - Factory methods:
CPC(),CPM(),OCPM(),Automatic()
- Properties:
-
BidType (enum): CPC (1), CPM (2), OCPM (3), TargetCost (4), Automatic (5)
-
IAdSetRepository:
Add(),Update(),GetByIdAsync(),GetByCampaignIdAsync()
Ad Aggregate (AdAggregate/)
-
Ad (Entity, IAggregateRoot)
- Fields:
_name,_adSetId,_format(AdFormat),_status(AdStatus),_reviewStatus(AdReviewStatus),_headline,_primaryText,_description,_callToAction,_destinationUrl,_creativeUrl,_createdAt,_updatedAt - Public IDs:
FormatId,StatusId,ReviewStatusId - Behavior methods:
Update(),SetCreative(),SubmitForReview(),Approve(),Reject(),Activate(),Pause() - Review state machine: NotSubmitted -> PendingReview -> Approved/Rejected; Approved resets to PendingReview if content updated
- Status state machine: Draft -> Active <-> Paused (activation requires Approved review status)
- Fields:
-
AdFormat (Enumeration): SingleImage (1, "single_image"), SingleVideo (2, "single_video"), Carousel (3, "carousel"), Collection (4, "collection"), Stories (5, "stories")
-
AdStatus (Enumeration): Draft (1), Active (2), Paused (3), Archived (4)
-
AdReviewStatus (Enumeration): NotSubmitted (1, "not_submitted"), PendingReview (2, "pending_review"), Approved (3, "approved"), Rejected (4, "rejected")
-
IAdRepository:
Add(),Update(),GetByIdAsync()
Audience Aggregate (AudienceAggregate/)
-
CustomAudience (Entity, IAggregateRoot)
- Fields:
_name,_advertiserId,_source(AudienceSource),_size,_createdAt,_updatedAt - Public ID:
SourceId - Behavior methods:
UpdateSize()
- Fields:
-
LookalikeAudience (Entity, IAggregateRoot)
- Fields:
_name,_advertiserId,_sourceAudienceId,_similarityPercentage(1-10),_location,_size,_createdAt - Behavior methods:
UpdateSize()
- Fields:
-
AudienceSource (Enumeration): CustomerList (1, "customer_list"), WebsiteVisitors (2, "website_visitors"), AppUsers (3, "app_users"), EngagementCustomers (4, "engagement_customers")
Note: No repository interface or implementation exists for Audience aggregates. No EF configuration exists for Audience entities. The migration creates basic CustomAudiences and LookalikeAudiences tables with minimal columns (only Id and SourceId for CustomAudience, only Id for LookalikeAudience) -- these are incomplete.
SeedWork (Base Classes)
- Entity: Base class with
Guid Id,DomainEventscollection, equality by ID - IAggregateRoot: Marker interface
- IRepository<T>: Generic interface with
IUnitOfWork UnitOfWork - IUnitOfWork:
SaveChangesAsync(),SaveEntitiesAsync() - ValueObject: Equality by component values
- Enumeration: Type-safe enum with
int Id,string Name, parsing methods
Exceptions
- DomainException: Base exception for domain errors
- AdsDomainException: Ads-specific domain exception (extends Exception, not DomainException)
Domain Events
- CampaignCreatedDomainEvent: Contains
Campaignreference. Raised when a campaign is created. - CampaignActivatedDomainEvent: Contains
CampaignId,PreviousStatus,NewStatus. Raised when a campaign is activated. - CampaignStatusChangedDomainEvent: Contains
CampaignId,PreviousStatus,NewStatus. Raised on pause, complete, or archive.
Note: No domain event handlers are implemented. Events are dispatched by DbContext but not consumed.
6. Database Schema
Database: ads_manager_service (PostgreSQL)
Migration: 20260117171043_InitialCreate
Table: campaigns
| Column | Type | Nullable | Constraints |
|---|---|---|---|
| id | uuid | NOT NULL | PK |
| name | varchar(255) | NOT NULL | |
| description | varchar(1000) | NULL | |
| advertiser_id | uuid | NOT NULL | |
| status_id | integer | NOT NULL | |
| objective_id | integer | NOT NULL | |
| budget_type | integer | NOT NULL | Owned (CampaignBudget) |
| budget_amount | numeric(18,2) | NOT NULL | Owned (CampaignBudget) |
| currency | varchar(10) | NOT NULL | Owned (CampaignBudget) |
| start_date | timestamp with time zone | NULL | |
| end_date | timestamp with time zone | NULL | |
| total_spend | numeric(18,2) | NOT NULL | Default: 0 |
| created_at | timestamp with time zone | NOT NULL | |
| updated_at | timestamp with time zone | NULL |
Indexes:
idx_campaigns_advertiser_idonadvertiser_ididx_campaigns_status_idonstatus_ididx_campaigns_created_atoncreated_at
Table: ad_sets
| Column | Type | Nullable | Constraints |
|---|---|---|---|
| id | uuid | NOT NULL | PK |
| name | varchar(255) | NOT NULL | |
| campaign_id | uuid | NOT NULL | |
| status_id | integer | NOT NULL | |
| target_min_age | integer | NULL | Owned (Targeting) |
| target_max_age | integer | NULL | Owned (Targeting) |
| target_genders | varchar(50) | NULL | Owned (Targeting) |
| target_locations | varchar(500) | NULL | Owned (Targeting) |
| target_interests | varchar(500) | NULL | Owned (Targeting) |
| custom_audience_ids | varchar(500) | NULL | Owned (Targeting) |
| lookalike_audience_ids | varchar(500) | NULL | Owned (Targeting) |
| bid_type | integer | NOT NULL | Owned (BidStrategy) |
| bid_amount | numeric(18,6) | NULL | Owned (BidStrategy) |
| target_cost | numeric(18,6) | NULL | Owned (BidStrategy) |
| daily_budget | numeric(18,2) | NOT NULL | |
| start_date | timestamp with time zone | NULL | |
| end_date | timestamp with time zone | NULL | |
| created_at | timestamp with time zone | NOT NULL | |
| updated_at | timestamp with time zone | NULL |
Indexes:
idx_ad_sets_campaign_idoncampaign_ididx_ad_sets_status_idonstatus_id
Note: No foreign key to campaigns table is defined.
Table: ads
| Column | Type | Nullable | Constraints |
|---|---|---|---|
| id | uuid | NOT NULL | PK |
| name | varchar(255) | NOT NULL | |
| ad_set_id | uuid | NOT NULL | |
| format_id | integer | NOT NULL | |
| status_id | integer | NOT NULL | |
| review_status_id | integer | NOT NULL | |
| headline | varchar(255) | NULL | |
| primary_text | varchar(1000) | NULL | |
| description | varchar(500) | NULL | |
| call_to_action | varchar(50) | NULL | |
| destination_url | varchar(2048) | NULL | |
| creative_url | varchar(2048) | NULL | |
| created_at | timestamp with time zone | NOT NULL | |
| updated_at | timestamp with time zone | NULL |
Indexes:
idx_ads_ad_set_idonad_set_ididx_ads_status_idonstatus_ididx_ads_review_status_idonreview_status_id
Note: No foreign key to ad_sets table is defined.
Table: CustomAudiences (incomplete)
| Column | Type | Nullable |
|---|---|---|
| Id | uuid | NOT NULL (PK) |
| SourceId | integer | NOT NULL |
Note: No EF configuration exists for this table. Migration only creates minimal columns. Most domain entity fields (Name, AdvertiserId, Size, CreatedAt, UpdatedAt) are not mapped.
Table: LookalikeAudiences (incomplete)
| Column | Type | Nullable |
|---|---|---|
| Id | uuid | NOT NULL (PK) |
Note: No EF configuration exists. Only Id column in migration. All other domain fields unmapped.
7. Integration Events
Published Domain Events (dispatched via MediatR before SaveChanges):
CampaignCreatedDomainEvent- When a campaign is createdCampaignActivatedDomainEvent- When a campaign is activatedCampaignStatusChangedDomainEvent- When campaign status changes (pause, complete, archive)
Cross-service Integration Events: None implemented. No RabbitMQ integration. No event publishers or consumers for cross-service communication.
Idempotency: IRequestManager / RequestManager infrastructure exists for duplicate request detection, but it is not currently used by any command handler.
8. Dependencies
NuGet Packages
API Layer (AdsManagerService.API.csproj):
- 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 (AdsManagerService.Domain.csproj):
- MediatR.Contracts 2.0.1
Infrastructure Layer (AdsManagerService.Infrastructure.csproj):
- 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:
- xunit 2.9.2
- FluentAssertions 6.12.2
- 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
External Service Dependencies
- PostgreSQL: Primary database (Neon cloud or local)
- Redis: Connection string configured but not actively used in code (health check registered)
Note: No authentication/authorization middleware is configured. No JWT validation. All endpoints are unauthenticated.
9. Configuration
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "<neon-postgresql-connection-string>"
},
"Redis": {
"ConnectionString": "localhost:6379"
},
"Jwt": {
"Secret": "your-super-secret-key-min-32-characters",
"Issuer": "goodgo-platform",
"Audience": "goodgo-services",
"AccessTokenExpiryMinutes": 15,
"RefreshTokenExpiryDays": 7
},
"Serilog": { ... }
}
Note: JWT settings are present in configuration but JWT authentication middleware is NOT registered in Program.cs. These settings are unused.
Environment Variables (.env.example)
| Variable | Description | Default |
|---|---|---|
| ASPNETCORE_ENVIRONMENT | Runtime environment | Development |
| DATABASE_URL | PostgreSQL connection string (fallback) | - |
| REDIS_URL | Redis connection | localhost:6379 |
| JWT_SECRET | JWT signing key | - |
| JWT_ISSUER | JWT issuer | goodgo-platform |
| JWT_AUDIENCE | JWT audience | goodgo-services |
| API_PORT | API port | 5000 |
| LOG_LEVEL | Minimum log level | Information |
| SEQ_URL | Seq logging endpoint | http://localhost:5341 |
Docker Configuration
- Base image: mcr.microsoft.com/dotnet/aspnet:10.0
- Build image: mcr.microsoft.com/dotnet/sdk:10.0
- Port: 8080
- User: dotnetuser (UID 1001, GID 1001) - non-root
- Health check:
curl -f http://localhost:8080/health/live(30s interval, 3 retries)
MediatR Pipeline Behaviors (registered in order)
- LoggingBehavior - Logs request name and elapsed time (with Stopwatch)
- ValidatorBehavior - Runs FluentValidation validators (throws ValidationException on failure)
- TransactionBehavior - Wraps commands in a database transaction (skips queries based on type name ending in "Query")
Database Connection Resolution Order
ConnectionStrings:DefaultConnectionfrom appsettingsDATABASE_URLenvironment variable- Throws
InvalidOperationExceptionif neither is configured
Npgsql Resilience
- Retry on failure: max 5 retries, max 30s delay
10. Known Issues and Gaps
- No Authentication: No JWT/Bearer auth middleware registered. All endpoints are publicly accessible.
- Audience handlers missing:
ListAudiencesQueryandGetAudienceByIdQueryhave no handler implementations. Endpoints will throw at runtime. - Audience tables incomplete:
CustomAudiencesandLookalikeAudiencestables in migration have minimal columns. No EF configurations exist. - No foreign keys: Campaign -> AdSet -> Ad relationships have no FK constraints in the database.
- No FluentValidation validators: No validator classes exist for any command.
- No domain event handlers: Domain events are dispatched but no handlers consume them.
- No cross-service events: No RabbitMQ/message broker integration.
- Idempotency unused:
IRequestManageris registered but never called by any handler. - Dapper unused: Dapper is referenced but no raw SQL queries exist.
- Redis unused: Redis package included and health check registered, but no caching logic implemented.
- Unit tests empty: The UnitTests project has no test files.
- Functional tests minimal: Only 2 tests (list campaigns returns 200, health check returns 200).
- GetCampaignStatsQuery: Loads ALL campaigns into memory before counting -- no SQL-level aggregation.
- AdsDomainException: Extends
Exceptiondirectly, notDomainException. Inconsistent hierarchy. - ListPendingAdsQuery handler: Filters by
ReviewStatus.Name == "Pending"but the actual enum name is"pending_review"-- potential mismatch depending on EF query translation.