---
name: ads-development
description: Ads ecosystem patterns và development workflow. Use for campaign management, RTB engine, billing integration, tracking/attribution, và analytics.
compatibility: ".NET 10+, MediatR, EF Core, Redis, RabbitMQ, MassTransit"
metadata:
  author: Velik Ho
  version: "1.0"
---

# Ads Development Skill / Phát Triển Hệ Thống Quảng Cáo

Patterns và workflows cho việc phát triển Ads Services ecosystem trong GoodGo.

## When to Use This Skill / Khi Nào Sử Dụng

Use this skill when:
- Developing campaign management features / Phát triển tính năng quản lý chiến dịch
- Implementing RTB (Real-Time Bidding) / Triển khai đấu giá thời gian thực
- Working with ads billing/payment / Làm việc với thanh toán quảng cáo
- Building tracking/attribution / Xây dựng tracking và attribution
- Creating analytics dashboards / Tạo dashboard phân tích

## Core Concepts / Khái Niệm Cốt Lõi

### Ads Services Ecosystem

```
┌─────────────────────────────────────────────────────────────────────┐
│                        ADS SERVICES ECOSYSTEM                        │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐      │
│  │  ads-manager    │  │  ads-serving    │  │  ads-billing    │      │
│  │    service      │  │    service      │  │    service      │      │
│  │                 │  │                 │  │                 │      │
│  │ • Campaigns     │  │ • RTB Engine    │  │ • Prepaid       │      │
│  │ • Ad Sets       │  │ • Ad Selection  │  │ • Credit Lines  │      │
│  │ • Ads           │  │ • Pacing        │  │ • Invoicing     │      │
│  │ • Targeting     │  │ • Frequency     │  │ • Thresholds    │      │
│  └────────┬────────┘  └────────┬────────┘  └────────┬────────┘      │
│           │                    │                    │                │
│           └──────────────┬─────┴────────────────────┘                │
│                          │                                           │
│                    ┌─────▼─────┐                                     │
│                    │  RabbitMQ │                                     │
│                    │ Event Bus │                                     │
│                    └─────┬─────┘                                     │
│                          │                                           │
│  ┌─────────────────┐     │     ┌─────────────────┐                  │
│  │  ads-tracking   │◄────┴────►│  ads-analytics  │                  │
│  │    service      │           │    service      │                  │
│  └─────────────────┘           └─────────────────┘                  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
```

### Campaign 3-Tier Structure / Cấu Trúc 3 Cấp Độ

```
Campaign (Chiến dịch)
    ├── Objective: Awareness | Traffic | Conversion
    ├── Status: Draft | Active | Paused | Completed
    └── Budget: Daily | Lifetime
        │
        ├── AdSet 1 (Nhóm quảng cáo)
        │   ├── Targeting: Core | Interest | Custom | Lookalike
        │   ├── Placement: Feed | Story | Search
        │   ├── Schedule: Start/End dates
        │   └── BidStrategy: CPC | CPM | OCPM
        │       │
        │       ├── Ad 1 (Mẫu quảng cáo)
        │       │   ├── Creative: Image | Video | Carousel
        │       │   ├── Headline: Text
        │       │   └── CTA: Learn More | Shop Now | etc.
        │       │
        │       └── Ad 2
        │
        └── AdSet 2
```

## Key Patterns / Mẫu Chính

### Campaign Aggregate

```csharp
/// <summary>
/// EN: Campaign aggregate root.
/// VI: Aggregate root cho Campaign.
/// </summary>
public class Campaign : Entity, IAggregateRoot
{
    private readonly List<AdSet> _adSets = new();
    
    public Guid AdvertiserId { get; private set; }
    public string Name { get; private set; }
    public CampaignObjective Objective { get; private set; }
    public CampaignStatus Status { get; private set; }
    public CampaignBudget Budget { get; private set; }
    
    public IReadOnlyCollection<AdSet> AdSets => _adSets.AsReadOnly();
    
    public Campaign(
        Guid advertiserId,
        string name,
        CampaignObjective objective,
        CampaignBudget budget)
    {
        AdvertiserId = advertiserId;
        Name = Guard.Against.NullOrWhiteSpace(name, nameof(name));
        Objective = objective;
        Budget = budget;
        Status = CampaignStatus.Draft;
        
        AddDomainEvent(new CampaignCreatedDomainEvent(this));
    }
    
    public void Activate()
    {
        if (Status != CampaignStatus.Draft)
            throw new AdsDomainException("Only draft campaigns can be activated");
        
        if (!_adSets.Any(a => a.Ads.Any()))
            throw new AdsDomainException("Campaign must have at least one ad");
            
        Status = CampaignStatus.Active;
        AddDomainEvent(new CampaignActivatedDomainEvent(this));
    }
    
    public void Pause() { /* ... */ }
}

/// <summary>
/// EN: Campaign objective value object.
/// VI: Value object cho mục tiêu chiến dịch.
/// </summary>
public record CampaignObjective
{
    public static CampaignObjective Awareness => new("AWARENESS");
    public static CampaignObjective Traffic => new("TRAFFIC");
    public static CampaignObjective Conversion => new("CONVERSION");
    
    public string Value { get; }
    private CampaignObjective(string value) => Value = value;
}
```

### RTB Auction Engine

```csharp
/// <summary>
/// EN: Real-time bidding auction engine.
/// VI: Engine đấu giá thời gian thực.
/// </summary>
public class AuctionEngine
{
    private readonly ICacheService _cache;
    private readonly IFrequencyCapService _frequencyCap;
    private readonly IBudgetPacer _budgetPacer;
    
    public async Task<AuctionResult?> RunAuctionAsync(
        AdRequest request,
        CancellationToken ct = default)
    {
        // EN: 1. Get candidate ads from cache
        // VI: 1. Lấy danh sách ads từ cache
        var candidateAds = await _cache.GetAsync<List<CachedAd>>(
            $"ads:active:{request.PlacementType}", ct);
        
        if (candidateAds == null || !candidateAds.Any())
            return null;
        
        // EN: 2. Filter by targeting
        // VI: 2. Lọc theo targeting
        var eligibleAds = candidateAds
            .Where(ad => MatchesTargeting(ad.Targeting, request.UserContext))
            .ToList();
        
        // EN: 3. Apply frequency cap
        // VI: 3. Áp dụng frequency cap
        eligibleAds = await _frequencyCap.FilterAsync(
            eligibleAds, request.UserId, ct);
        
        // EN: 4. Apply budget pacing
        // VI: 4. Áp dụng pacing ngân sách
        eligibleAds = await _budgetPacer.FilterAsync(eligibleAds, ct);
        
        // EN: 5. Calculate eCPM and rank
        // VI: 5. Tính eCPM và xếp hạng
        var ranked = eligibleAds
            .Select(ad => new Bid
            {
                Ad = ad,
                eCPM = CalculateEcpm(ad, request)
            })
            .OrderByDescending(b => b.eCPM)
            .ToList();
        
        if (!ranked.Any())
            return null;
        
        // EN: 6. Select winner (second-price auction)
        // VI: 6. Chọn winner (đấu giá giá thứ 2)
        var winner = ranked.First();
        var secondPrice = ranked.Count > 1 
            ? ranked[1].eCPM 
            : winner.eCPM * 0.9m;
        
        return new AuctionResult
        {
            WinningAd = winner.Ad,
            FinalPrice = secondPrice + 0.01m
        };
    }
    
    private decimal CalculateEcpm(CachedAd ad, AdRequest request)
    {
        // eCPM = (Bid × Predicted CTR × 1000) + Quality Score
        var predictedCtr = PredictCtr(ad, request);
        var qualityScore = CalculateQualityScore(ad);
        
        return (ad.BidAmount * predictedCtr * 1000) + qualityScore;
    }
}
```

### Tracking Pixel Event

```csharp
/// <summary>
/// EN: Pixel event handler.
/// VI: Handler cho pixel event.
/// </summary>
public class TrackPixelEventCommandHandler 
    : IRequestHandler<TrackPixelEventCommand, TrackingResult>
{
    private readonly IConversionWindow _conversionWindow;
    private readonly IAttributionService _attribution;
    private readonly IPublishEndpoint _publisher;
    
    public async Task<TrackingResult> Handle(
        TrackPixelEventCommand request,
        CancellationToken ct)
    {
        // EN: 1. Validate pixel belongs to advertiser
        // VI: 1. Xác thực pixel thuộc về advertiser
        var pixel = await _pixelRepository.GetAsync(request.PixelId, ct);
        if (pixel == null)
            return TrackingResult.InvalidPixel();
        
        // EN: 2. Record event
        // VI: 2. Ghi nhận event
        var pixelEvent = new PixelEvent(
            request.PixelId,
            request.EventType,
            request.EventData,
            request.UserId,
            DateTime.UtcNow);
        
        await _eventRepository.AddAsync(pixelEvent, ct);
        
        // EN: 3. Check for conversion attribution
        // VI: 3. Kiểm tra attribution cho conversion
        if (IsConversionEvent(request.EventType))
        {
            var attribution = await _attribution.FindAttributionAsync(
                request.UserId,
                _conversionWindow.GetWindow(pixel.Settings),
                ct);
            
            if (attribution != null)
            {
                await _publisher.Publish(new ConversionAttributedEvent
                {
                    ConversionId = pixelEvent.Id,
                    AdId = attribution.AdId,
                    CampaignId = attribution.CampaignId,
                    Value = request.EventData.GetValueOrDefault("value", 0)
                }, ct);
            }
        }
        
        return TrackingResult.Success(pixelEvent.Id);
    }
}
```

### Integration Events

```csharp
/// <summary>
/// EN: Ads ecosystem integration events.
/// VI: Integration events cho Ads ecosystem.
/// </summary>

// ads-manager → ads-serving
public record CampaignActivatedIntegrationEvent(
    Guid CampaignId,
    List<AdSetInfo> AdSets,
    DateTime ActivatedAt) : IIntegrationEvent;

// ads-serving → ads-billing
public record AdImpressionChargedEvent(
    Guid AdId,
    Guid CampaignId,
    Guid AdvertiserId,
    decimal CostAmount,
    string BillingModel, // CPC, CPM
    DateTime ChargedAt) : IIntegrationEvent;

// ads-tracking → ads-analytics
public record ConversionAttributedEvent(
    Guid ConversionId,
    Guid AdId,
    Guid CampaignId,
    decimal ConversionValue,
    string AttributionModel,
    DateTime AttributedAt) : IIntegrationEvent;

// ads-billing → wallet-service (existing)
public record WalletDebitRequestedEvent(
    Guid WalletId,
    decimal Amount,
    string Description,
    Guid ReferenceId,
    string ReferenceType) : IIntegrationEvent;
```

## Common Mistakes / Lỗi Thường Gặp

### 1. Not Using Redis for Frequency Cap

```csharp
// ❌ BAD: Database lookup for every request
var viewCount = await _dbContext.AdViews
    .CountAsync(v => v.UserId == userId && v.AdId == adId);

// ✅ GOOD: Redis with atomic increment
var key = $"freq:{userId}:{adId}:{DateTime.UtcNow:yyyyMMdd}";
var count = await _redis.StringIncrementAsync(key);
if (count == 1)
    await _redis.KeyExpireAsync(key, TimeSpan.FromDays(1));
```

### 2. Not Handling Budget Race Conditions

```csharp
// ❌ BAD: Read-then-write race condition
var spent = await GetTodaySpend(campaignId);
if (spent + cost <= budget)
    await RecordSpend(campaignId, cost);

// ✅ GOOD: Atomic increment with Lua script
var script = @"
    local current = redis.call('GET', KEYS[1])
    local budget = tonumber(ARGV[1])
    local cost = tonumber(ARGV[2])
    current = current and tonumber(current) or 0
    if current + cost <= budget then
        redis.call('INCRBYFLOAT', KEYS[1], cost)
        return 1
    end
    return 0
";
var result = await _redis.ScriptEvaluateAsync(script, keys, args);
```

### 3. Blocking on Analytics Write

```csharp
// ❌ BAD: Sync write blocks ad serving
await _analyticsDb.InsertAsync(impression);
return adResponse;

// ✅ GOOD: Async publish, return immediately
await _publisher.Publish(new ImpressionEvent(impression));
return adResponse; // < 100ms
```

## Quick Reference / Tham Chiếu Nhanh

### Service Responsibilities

| Service | Primary Responsibility |
|---------|----------------------|
| **ads-manager** | Campaign CRUD, targeting, audiences |
| **ads-serving** | RTB auction, ad selection (< 100ms) |
| **ads-billing** | Prepaid/postpaid, invoicing |
| **ads-tracking** | Pixel events, attribution |
| **ads-analytics** | Metrics, reporting, insights |

### Redis Key Patterns

| Key | Type | TTL | Purpose |
|-----|------|-----|---------|
| `ads:active:{placement}` | Sorted Set | 5 min | Active ads by eCPM |
| `freq:{userId}:{date}` | Hash | 24h | User ad frequency |
| `budget:{campaignId}:{date}` | String | 24h | Daily spend |
| `target:{criteria}` | Set | 1h | Eligible ad sets |

### Metrics

| Metric | Formula |
|--------|---------|
| **CTR** | Clicks / Impressions |
| **CPC** | Spend / Clicks |
| **CPM** | (Spend / Impressions) × 1000 |
| **CPA** | Spend / Conversions |
| **ROAS** | Revenue / Spend |

## Resources / Tài Nguyên

- [Ads Services Proposal](../../docs/ads-services-proposal.md)
- [Inter-service Communication](../inter-service-communication/SKILL.md)
- [Redis Caching](../redis-caching/SKILL.md)
- [SAGA Pattern](../saga-pattern/SKILL.md)
- [Error Handling](../error-handling-patterns/SKILL.md)
