Files
pos-system/services/ads-billing-service-net/SERVICE_DOCS.md
Ho Ngoc Hai f3779c4ebe docs: add SERVICE_DOCS.md for all 24 microservices from per-service code audit
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>
2026-03-13 17:54:53 +07:00

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:DefaultConnection or DATABASE_URL env 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) - required
    • WalletId (Guid?) - optional
    • PaymentMethod (string) - default "prepaid"; accepts "prepaid", "postpaid", "creditcard"
  • Behavior: Checks if account already exists for the advertiser (returns existing ID if so). Creates new BillingAccount entity with the specified payment method. Persists via SaveEntitiesAsync.
  • Handler: CreateBillingAccountCommandHandler - injects AdsBillingServiceContext directly (no repository pattern).

3.2 ChargeAdvertiserCommand

  • File: Application/Commands/ChargeAdvertiserCommand.cs
  • Returns: bool (success/failure)
  • Parameters:
    • AdvertiserId (Guid) - required
    • CampaignId (Guid) - required
    • AdId (Guid) - required
    • ChargeType (string) - "impression" or "click"
    • Amount (decimal) - charge amount
  • Behavior: Creates AdCharge entity, looks up billing account by advertiser ID, calls billingAccount.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 ID
    • Amount (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 BillingAccounts DbSet with AsNoTracking().

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 BillingAccounts with AsNoTracking().

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 Invoices with nested LineItems projection.

5. Domain Model

5.1 Aggregates

BillingAccount (Aggregate Root)

  • File: Domain/AggregatesModel/BillingAccountAggregate/BillingAccount.cs
  • Extends: Entity, implements IAggregateRoot
  • 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 threshold
    • DeductBalance(decimal amount) - deducts from prepaid accounts only; throws if insufficient
    • AddBalance(decimal amount) - adds funds; throws if amount <= 0
    • Suspend() - sets status to Suspended
    • Reactivate() - sets status to Active; throws if account is Closed
    • SetCreditLimit(decimal creditLimit) - updates credit limit; throws if negative
    • ApplyCharge(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, implements IAggregateRoot
  • 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, implements IAggregateRoot
  • 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 total
    • Issue() - transitions from Draft to Issued; throws if not Draft
    • MarkAsPaid() - 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 = 1
  • Suspended = 2
  • Closed = 3

ChargeType

  • Impression = 1
  • Click = 2
  • Conversion = 3

InvoiceStatus

  • Draft = 1
  • Issued = 2
  • Paid = 3
  • Overdue = 4
  • Cancelled = 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() and SaveEntitiesAsync() (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_id on advertiser_id
  • ix_billing_accounts_created_at on created_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_id on advertiser_id
  • ix_ad_charges_campaign_id on campaign_id
  • ix_ad_charges_charge_type on charge_type
  • ix_ad_charges_charged_at on charged_at
  • ix_ad_charges_processed on processed
  • ix_ad_charges_advertiser_processed_charged composite 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_id on billing_account_id
  • ix_invoices_invoice_number (unique) on invoice_number
  • ix_invoices_status on status
  • ix_invoices_due_date on due_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_id on invoice_id
  • IX_invoice_line_items_InvoiceId1 on InvoiceId1

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) on name

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_PASSWORD
  • JWT_SECRET, JWT_ISSUER, JWT_AUDIENCE, JWT_ACCESS_TOKEN_EXPIRY_MINUTES, JWT_REFRESH_TOKEN_EXPIRY_DAYS
  • API_PORT, API_BASE_PATH
  • OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME
  • LOG_LEVEL, SEQ_URL
  • RATE_LIMIT_PERMITS_PER_MINUTE, RATE_LIMIT_QUEUE_LIMIT
  • HEALTHCHECK_TIMEOUT_SECONDS

MediatR Pipeline (configured in Program.cs)

  1. LoggingBehavior<,> - logs request name and elapsed time (Stopwatch)
  2. ValidatorBehavior<,> - runs FluentValidation validators (throws ValidationException on failure)
  3. TransactionBehavior<,> - wraps commands in DB transaction (skips queries, uses ExecutionStrategy)

Infrastructure DI (DependencyInjection.cs)

  • AdsBillingServiceContext registered with Npgsql (retry on failure: 5 retries, 30s max delay)
  • IRequestManager -> RequestManager (scoped, for idempotency)
  • Repository registration commented out (IBillingAccountRepository not 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 CustomWebApplicationFactory with InMemory database
    • BillingAccountFlow_ShouldCreateAccount_AddFunds_AndReturnBalance
    • CreditLineRequest_ShouldIncreaseCreditLimit
    • RegenerateInvoice_WithoutCharges_ShouldReturnBadRequest
    • HealthCheck_ShouldReturnHealthy
  • UnitTests: Project exists but contains no test classes

10. Known Issues

  1. No authentication: JWT settings are in config but UseAuthentication/UseAuthorization middleware and [Authorize] attributes are not implemented. All endpoints are publicly accessible.
  2. No repository pattern: Handlers inject AdsBillingServiceContext directly instead of using repository interfaces (commented out in DI). Only IRequestManager is registered.
  3. No FluentValidation validators: While ValidatorBehavior is in the pipeline, no AbstractValidator<T> implementations exist for any command.
  4. No domain events raised: Entities support domain events via AddDomainEvent() but none of the aggregates actually raise any events.
  5. Spurious FK column: The invoice_line_items table has a duplicate FK column InvoiceId1 (likely EF navigation config issue).
  6. Duplicate controller file: There is a file Admin ChargesController.cs (with space in name) containing AdminInvoicesController class, alongside the properly named AdminChargesController.cs.
  7. Empty unit tests project: AdsBillingService.UnitTests has the .csproj but no test files.
  8. Redis/Dapper unused: Both are in NuGet dependencies but have no active usage in the codebase.