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>
23 KiB
AdsBillingService - Service Documentation
1. Overview
The AdsBillingService is a .NET 10.0 microservice responsible for managing advertising billing within the GoodGo platform. It handles billing accounts for advertisers, processes ad charges (impressions/clicks), generates invoices, manages credit lines, and provides admin analytics.
- Port: 5013 (Development), 8080 (Docker/Production)
- Database: PostgreSQL -
ads_billing_service(Neon cloud in appsettings, local Docker available) - Connection String Key:
ConnectionStrings:DefaultConnectionorDATABASE_URLenv var - Base Route:
/api/v1/ads-billing/(public),/api/v1/admin/ads-billing/(admin) - Health Checks:
/health,/health/live,/health/ready - Swagger:
/swagger(Development only) - SDK: .NET 10.0 / C# 14
- Migration:
20260117181417_InitialCreate(auto-applied on startup)
2. API Endpoints
2.1 BillingAccountsController
Route prefix: api/v1/ads-billing/accounts
| Method | Route | Action | Description |
|---|---|---|---|
| POST | / |
CreateBillingAccount |
Create a new billing account for an advertiser. Body: CreateBillingAccountCommand. Returns 201 with account GUID. |
| GET | /{id} |
GetBillingAccount |
Get billing account details by ID. Returns BillingAccountDto or 404. |
| POST | /{id}/add-funds |
AddFunds |
Add funds to a billing account. Body: AddFundsRequest { Amount }. Validates amount > 0. |
| GET | /{id}/balance |
GetBalance |
Get the current balance of a billing account. Returns { accountId, balance } or 404. |
2.2 InvoicesController
Route prefix: api/v1/ads-billing/invoices
| Method | Route | Action | Description |
|---|---|---|---|
| GET | / |
GetInvoices |
List invoices with optional filters. Query params: billingAccountId, status, pageNumber (default 1), pageSize (default 20). |
| GET | /{id} |
GetInvoiceById |
Get invoice details by ID, including line items. Returns InvoiceDto or 404. |
| GET | /{id}/download |
DownloadInvoice |
Download invoice summary as a .txt file. Returns text/plain file content. |
2.3 CreditLinesController
Route prefix: api/v1/ads-billing/credit-lines
| Method | Route | Action | Description |
|---|---|---|---|
| GET | /{advertiserId} |
GetCreditLine |
Get credit line info for an advertiser (creditLimit, balance, availableCredit, paymentMethod, status). |
| POST | /request |
RequestCreditIncrease |
Request credit limit increase. Body: CreditIncreaseRequest { AdvertiserId, RequestedAmount, Reason? }. Immediately applies the increase. Returns 202. |
2.4 AdminBillingAccountsController
Route prefix: api/v1/admin/ads-billing/accounts
| Method | Route | Action | Description |
|---|---|---|---|
| GET | / |
SearchAccounts |
Search billing accounts with filters. Query params: status, paymentMethod, pageNumber, pageSize. |
| GET | /stats |
GetStatistics |
Get aggregate statistics: totalAccounts, activeAccounts, suspendedAccounts, totalBalance, totalCreditLimit. |
| POST | /{id}/suspend |
SuspendAccount |
Suspend a billing account. Body: SuspendAccountRequest { Reason }. |
| POST | /{id}/reactivate |
ReactivateAccount |
Reactivate a suspended billing account. |
| PUT | /{id}/credit-limit |
UpdateCreditLimit |
Update credit limit for an account. Body: UpdateCreditLimitRequest { NewCreditLimit }. Validates >= 0. |
2.5 AdminChargesController
Route prefix: api/v1/admin/ads-billing/charges
| Method | Route | Action | Description |
|---|---|---|---|
| GET | / |
GetCharges |
Search charges with filters. Query params: advertiserId, campaignId, chargeType, processed, fromDate, toDate, pageNumber, pageSize (default 50). |
| GET | /stats |
GetStatistics |
Charge statistics: total/processed/unprocessed counts and amounts, breakdown by chargeType, recent 10 charges. |
| GET | /analytics/by-advertiser |
GetAdvertiserAnalytics |
Top advertisers by charge amount. Query param: top (default 10). |
2.6 AdminInvoicesController (file: Admin ChargesController.cs)
Route prefix: api/v1/admin/ads-billing/invoices
Note: This controller is defined in the file named Admin ChargesController.cs (with a space) but the class is AdminInvoicesController.
| Method | Route | Action | Description |
|---|---|---|---|
| GET | / |
SearchInvoices |
Search invoices with filters. Query params: status, fromDate, toDate, pageNumber, pageSize. |
| GET | /stats |
GetStatistics |
Invoice statistics: total/paid/overdue/pending counts, totalAmount, paidAmount, outstandingAmount. |
| POST | /{id}/mark-paid |
MarkInvoiceAsPaid |
Mark an invoice as paid. Calls invoice.MarkAsPaid(). |
| POST | /regenerate |
RegenerateInvoice |
Regenerate invoice from charge data for a billing period. Body: RegenerateInvoiceRequest { BillingAccountId, StartDate, EndDate }. Groups charges by campaign + chargeType into line items. |
3. Commands
3.1 CreateBillingAccountCommand
- File:
Application/Commands/CreateBillingAccountCommand.cs - Returns:
Guid(account ID) - Parameters:
AdvertiserId(Guid) - requiredWalletId(Guid?) - optionalPaymentMethod(string) - default "prepaid"; accepts "prepaid", "postpaid", "creditcard"
- Behavior: Checks if account already exists for the advertiser (returns existing ID if so). Creates new
BillingAccountentity with the specified payment method. Persists viaSaveEntitiesAsync. - Handler:
CreateBillingAccountCommandHandler- injectsAdsBillingServiceContextdirectly (no repository pattern).
3.2 ChargeAdvertiserCommand
- File:
Application/Commands/ChargeAdvertiserCommand.cs - Returns:
bool(success/failure) - Parameters:
AdvertiserId(Guid) - requiredCampaignId(Guid) - requiredAdId(Guid) - requiredChargeType(string) - "impression" or "click"Amount(decimal) - charge amount
- Behavior: Creates
AdChargeentity, looks up billing account by advertiser ID, callsbillingAccount.ApplyCharge(amount)(deducts for prepaid, accrues for postpaid/credit). Marks charge as processed. Returns false if account not found or charge fails (e.g., insufficient balance). - Handler:
ChargeAdvertiserCommandHandler
3.3 AddFundsCommand
- File:
Application/Commands/AddFundsCommand.cs - Returns:
bool(success/failure) - Parameters:
AccountId(Guid) - billing account IDAmount(decimal) - amount to add
- Behavior: Finds account by ID, calls
account.AddBalance(amount). Returns false if account not found. - Handler:
AddFundsCommandHandler(defined in same file)
4. Queries
4.1 GetBillingAccountQuery
- File:
Application/Queries/GetBillingAccountQuery.cs - Parameters:
AccountId(Guid) - Returns:
BillingAccountDto?- includes Id, AdvertiserId, WalletId, PaymentMethod, Status, Balance, CreditLimit, Threshold (Amount, AutoCharge), CreatedAt, UpdatedAt - Behavior: Projects from
BillingAccountsDbSet withAsNoTracking().
4.2 GetBillingAccountBalanceQuery
- File:
Application/Queries/GetBillingAccountBalanceQuery.cs - Parameters:
AccountId(Guid) - Returns:
decimal?- the balance value, or null if account not found - Behavior: Simple projection of balance from
BillingAccountswithAsNoTracking().
4.3 GetInvoicesQuery
- File:
Application/Queries/GetInvoicesQuery.cs - Parameters:
BillingAccountId(Guid?),Status(string?),PageNumber(int, default 1),PageSize(int, default 20) - Returns:
List<InvoiceDto>- ordered by IssueDate descending. Note: LineItems are returned as empty list (not loaded in list query). - Behavior: Applies optional filters on BillingAccountId and Status.
4.4 GetInvoiceByIdQuery
- File:
Application/Queries/GetInvoiceByIdQuery.cs - Parameters:
InvoiceId(Guid) - Returns:
InvoiceDto?- includes full LineItems (CampaignId, Description, Quantity, UnitPrice, TotalAmount). - Behavior: Projects from
Invoiceswith nestedLineItemsprojection.
5. Domain Model
5.1 Aggregates
BillingAccount (Aggregate Root)
- File:
Domain/AggregatesModel/BillingAccountAggregate/BillingAccount.cs - Extends:
Entity, implementsIAggregateRoot - Fields (private with public getters):
AdvertiserId(Guid)WalletId(Guid?)PaymentMethod(PaymentMethodType enum)Threshold(BillingThreshold? value object)Status(AccountStatus enum)Balance(decimal)CreditLimit(decimal)CreatedAt(DateTime)UpdatedAt(DateTime?)
- Behavior Methods:
SetThreshold(decimal amount, bool autoCharge)- sets auto-charge thresholdDeductBalance(decimal amount)- deducts from prepaid accounts only; throws if insufficientAddBalance(decimal amount)- adds funds; throws if amount <= 0Suspend()- sets status to SuspendedReactivate()- sets status to Active; throws if account is ClosedSetCreditLimit(decimal creditLimit)- updates credit limit; throws if negativeApplyCharge(decimal amount)- core billing logic:- Prepaid: deducts from balance (throws if insufficient)
- Postpaid/CreditCard: accrues to balance (throws if credit limit exceeded)
- Throws if account not active or amount <= 0
AdCharge (Aggregate Root)
- File:
Domain/AggregatesModel/ChargeAggregate/AdCharge.cs - Extends:
Entity, implementsIAggregateRoot - Fields:
AdvertiserId(Guid)CampaignId(Guid)AdId(Guid)ChargeType(ChargeType enum)Amount(decimal)Currency(string, default "VND")ChargedAt(DateTime)Processed(bool)
- Behavior Methods:
MarkAsProcessed()- flags charge as processed
- Factory Methods:
ForImpression(advertiserId, campaignId, adId, cpm)- creates impression charge (amount = cpm / 1000)ForClick(advertiserId, campaignId, adId, cpc)- creates click charge (amount = cpc)
Invoice (Aggregate Root)
- File:
Domain/AggregatesModel/InvoiceAggregate/Invoice.cs - Extends:
Entity, implementsIAggregateRoot - Fields:
BillingAccountId(Guid)InvoiceNumber(string, auto-generated:INV-yyyyMMdd-XXXXXXXX)Status(InvoiceStatus enum)IssueDate(DateTime)DueDate(DateTime)TotalAmount(decimal, auto-calculated from line items)LineItems(IReadOnlyCollection)
- Behavior Methods:
AddLineItem(campaignId, description, quantity, unitPrice)- adds line item and recalculates totalIssue()- transitions from Draft to Issued; throws if not DraftMarkAsPaid()- transitions from Issued to Paid; throws if not Issued
5.2 Child Entities
InvoiceLineItem
- File:
Domain/AggregatesModel/InvoiceAggregate/Invoice.cs(same file as Invoice) - Extends:
Entity(not an aggregate root) - Properties:
CampaignId(Guid)Description(string)Quantity(int)UnitPrice(decimal)TotalAmount(decimal, computed: Quantity * UnitPrice)
5.3 Value Objects
BillingThreshold
- File:
Domain/AggregatesModel/BillingAccountAggregate/BillingAccount.cs - Extends:
ValueObject - Properties:
Amount(decimal),AutoCharge(bool) - Validation: Amount must be positive
5.4 Enumerations (C# enums)
PaymentMethodType
Prepaid = 1(pay upfront via Wallet)Postpaid = 2(monthly invoice)CreditCard = 3(auto-charge credit card)
AccountStatus
Active = 1Suspended = 2Closed = 3
ChargeType
Impression = 1Click = 2Conversion = 3
InvoiceStatus
Draft = 1Issued = 2Paid = 3Overdue = 4Cancelled = 5
5.5 Exceptions
- AdsBillingDomainException (
Domain/Exceptions/AdsBillingDomainException.cs) - used for all business rule violations in this service - DomainException (
Domain/Exceptions/DomainException.cs) - generic base exception (not currently used by entities)
5.6 SeedWork
Standard DDD building blocks in Domain/SeedWork/:
- Entity - base class with Id (Guid), DomainEvents (List), equality by Id
- IAggregateRoot - marker interface
- IRepository - generic repo interface with
IUnitOfWork UnitOfWork - IUnitOfWork -
SaveChangesAsync()andSaveEntitiesAsync()(dispatches domain events) - ValueObject - base class with equality by components
- Enumeration - type-safe enum pattern (not used by current enums, which are plain C# enums)
6. Database Schema
Database: ads_billing_service (PostgreSQL)
Migration: 20260117181417_InitialCreate
Table: billing_accounts
| Column | Type | Nullable | Notes |
|---|---|---|---|
id |
uuid | NOT NULL | PK |
advertiser_id |
uuid | NOT NULL | Indexed |
wallet_id |
uuid | NULL | |
payment_method |
integer | NOT NULL | Enum: 1=Prepaid, 2=Postpaid, 3=CreditCard |
status |
integer | NOT NULL | Enum: 1=Active, 2=Suspended, 3=Closed |
balance |
numeric(18,2) | NOT NULL | |
credit_limit |
numeric(18,2) | NOT NULL | |
threshold_amount |
numeric(18,2) | NULL | Owned entity (BillingThreshold) |
threshold_auto_charge |
boolean | NULL | Owned entity (BillingThreshold) |
created_at |
timestamp with time zone | NOT NULL | Indexed |
updated_at |
timestamp with time zone | NULL |
Indexes:
ix_billing_accounts_advertiser_idonadvertiser_idix_billing_accounts_created_atoncreated_at
Table: ad_charges
| Column | Type | Nullable | Notes |
|---|---|---|---|
id |
uuid | NOT NULL | PK |
advertiser_id |
uuid | NOT NULL | Indexed |
campaign_id |
uuid | NOT NULL | Indexed |
ad_id |
uuid | NOT NULL | |
charge_type |
integer | NOT NULL | Enum: 1=Impression, 2=Click, 3=Conversion. Indexed |
amount |
numeric(18,6) | NOT NULL | 6 decimal places for micro-charges |
currency |
varchar(10) | NOT NULL | Default "VND" |
charged_at |
timestamp with time zone | NOT NULL | Indexed |
processed |
boolean | NOT NULL | Indexed |
Indexes:
ix_ad_charges_advertiser_idonadvertiser_idix_ad_charges_campaign_idoncampaign_idix_ad_charges_charge_typeoncharge_typeix_ad_charges_charged_atoncharged_atix_ad_charges_processedonprocessedix_ad_charges_advertiser_processed_chargedcomposite on (advertiser_id,processed,charged_at)
Table: invoices
| Column | Type | Nullable | Notes |
|---|---|---|---|
id |
uuid | NOT NULL | PK |
billing_account_id |
uuid | NOT NULL | Indexed |
invoice_number |
varchar(50) | NOT NULL | Unique index. Format: INV-yyyyMMdd-XXXXXXXX |
status |
integer | NOT NULL | Enum: 1=Draft, 2=Issued, 3=Paid, 4=Overdue, 5=Cancelled. Indexed |
issue_date |
timestamp with time zone | NOT NULL | |
due_date |
timestamp with time zone | NOT NULL | Indexed |
total_amount |
numeric(18,2) | NOT NULL |
Indexes:
ix_invoices_billing_account_idonbilling_account_idix_invoices_invoice_number(unique) oninvoice_numberix_invoices_statusonstatusix_invoices_due_dateondue_date
Table: invoice_line_items
| Column | Type | Nullable | Notes |
|---|---|---|---|
id |
uuid | NOT NULL | PK |
invoice_id |
uuid | NOT NULL | FK -> invoices.id (CASCADE). Indexed |
campaign_id |
uuid | NOT NULL | |
description |
varchar(500) | NOT NULL | |
quantity |
integer | NOT NULL | |
unit_price |
numeric(18,2) | NOT NULL | |
InvoiceId1 |
uuid | NULL | Spurious FK column from migration (likely EF config issue) |
Indexes:
ix_invoice_line_items_invoice_idoninvoice_idIX_invoice_line_items_InvoiceId1onInvoiceId1
Note: The TotalAmount property on InvoiceLineItem is computed (Quantity * UnitPrice) and ignored by EF (builder.Ignore(li => li.TotalAmount)).
Table: client_requests
| Column | Type | Nullable | Notes |
|---|---|---|---|
id |
uuid | NOT NULL | PK |
name |
varchar(200) | NOT NULL | Unique index. Stores command type name. |
time |
timestamp with time zone | NOT NULL |
Indexes:
ix_client_requests_name(unique) onname
7. Integration Events
None implemented. The service does not currently publish or consume any cross-service integration events via RabbitMQ or any other message broker. Domain events are supported via MediatR (in-process INotification) but no domain events are currently raised by any entity (no calls to AddDomainEvent in entity code). The DispatchDomainEventsAsync method exists in the DbContext but would be a no-op with the current entities.
8. Dependencies
NuGet Packages
API Layer
| 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
| 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 |
Test Projects
| Package | Version | Project |
|---|---|---|
| xunit | 2.9.2 | Both |
| xunit.runner.visualstudio | 2.8.2 | Both |
| Microsoft.NET.Test.Sdk | 17.12.0 | Both |
| FluentAssertions | 6.12.2 | Both |
| coverlet.collector | 6.0.2 | Both |
| Moq | 4.20.72 | UnitTests |
| Microsoft.AspNetCore.Mvc.Testing | 10.0.0 | FunctionalTests |
| Microsoft.EntityFrameworkCore.InMemory | 10.0.0 | FunctionalTests |
| Testcontainers.PostgreSql | 4.1.0 | FunctionalTests |
External Service Dependencies
- PostgreSQL - primary data store (Neon cloud or local Docker)
- Redis - configured in appsettings but not actively used in code (health check registered, StackExchange.Redis in Infrastructure deps)
- Dapper - referenced in Infrastructure but not actively used in current code
- Polly - referenced in Infrastructure but no resilience policies implemented yet
Inter-Service Dependencies
No direct calls to other microservices. The service references AdvertiserId, CampaignId, AdId, and WalletId as foreign keys by convention (Guid) but does not call ads-manager-service, ads-serving-service, or wallet-service APIs.
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": { "..." },
"Logging": { "..." }
}
Note: JWT settings are defined in appsettings but JWT authentication middleware is NOT configured in Program.cs. No [Authorize] attributes exist on any controller. All endpoints are currently unauthenticated.
Environment Variables
| Variable | Description | Default |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Runtime environment | Development (local), Production (Docker) |
ASPNETCORE_URLS |
Listen URL | http://+:8080 (Docker) |
DATABASE_URL |
Fallback connection string | - |
ConnectionStrings__DefaultConnection |
Primary connection string | Neon PostgreSQL URL |
.env.example Variables
Additional env vars from .env.example (template, not all used in code):
REDIS_URL,REDIS_PASSWORDJWT_SECRET,JWT_ISSUER,JWT_AUDIENCE,JWT_ACCESS_TOKEN_EXPIRY_MINUTES,JWT_REFRESH_TOKEN_EXPIRY_DAYSAPI_PORT,API_BASE_PATHOTEL_EXPORTER_OTLP_ENDPOINT,OTEL_SERVICE_NAMELOG_LEVEL,SEQ_URLRATE_LIMIT_PERMITS_PER_MINUTE,RATE_LIMIT_QUEUE_LIMITHEALTHCHECK_TIMEOUT_SECONDS
MediatR Pipeline (configured in Program.cs)
LoggingBehavior<,>- logs request name and elapsed time (Stopwatch)ValidatorBehavior<,>- runs FluentValidation validators (throwsValidationExceptionon failure)TransactionBehavior<,>- wraps commands in DB transaction (skips queries, usesExecutionStrategy)
Infrastructure DI (DependencyInjection.cs)
AdsBillingServiceContextregistered with Npgsql (retry on failure: 5 retries, 30s max delay)IRequestManager->RequestManager(scoped, for idempotency)- Repository registration commented out (
IBillingAccountRepositorynot implemented) - Sensitive data logging enabled in Development
Docker
- Multi-stage build:
sdk:10.0->aspnet:10.0 - Non-root user:
dotnetuser(UID/GID 1001) - Port: 8080
- Healthcheck:
curl -f http://localhost:8080/health/live(30s interval, 3 retries)
Tests
- FunctionalTests: 4 tests using
CustomWebApplicationFactorywith InMemory databaseBillingAccountFlow_ShouldCreateAccount_AddFunds_AndReturnBalanceCreditLineRequest_ShouldIncreaseCreditLimitRegenerateInvoice_WithoutCharges_ShouldReturnBadRequestHealthCheck_ShouldReturnHealthy
- UnitTests: Project exists but contains no test classes
10. Known Issues
- No authentication: JWT settings are in config but
UseAuthentication/UseAuthorizationmiddleware and[Authorize]attributes are not implemented. All endpoints are publicly accessible. - No repository pattern: Handlers inject
AdsBillingServiceContextdirectly instead of using repository interfaces (commented out in DI). OnlyIRequestManageris registered. - No FluentValidation validators: While
ValidatorBehavioris in the pipeline, noAbstractValidator<T>implementations exist for any command. - No domain events raised: Entities support domain events via
AddDomainEvent()but none of the aggregates actually raise any events. - Spurious FK column: The
invoice_line_itemstable has a duplicate FK columnInvoiceId1(likely EF navigation config issue). - Duplicate controller file: There is a file
Admin ChargesController.cs(with space in name) containingAdminInvoicesControllerclass, alongside the properly namedAdminChargesController.cs. - Empty unit tests project:
AdsBillingService.UnitTestshas the .csproj but no test files. - Redis/Dapper unused: Both are in NuGet dependencies but have no active usage in the codebase.