# 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<InvoiceLineItem>)
- **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<INotification>), equality by Id
- **IAggregateRoot** - marker interface
- **IRepository<T>** - 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

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