diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Events/AdServingEventPublisher.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Events/AdServingEventPublisher.cs new file mode 100644 index 00000000..8aa8dca4 --- /dev/null +++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Events/AdServingEventPublisher.cs @@ -0,0 +1,102 @@ +namespace AdsServingService.API.Application.Events; + +/// +/// EN: Event payload for served-ad event. +/// VI: Payload event cho sự kiện quảng cáo đã được phục vụ. +/// +public record AdServedEvent +{ + public Guid AuctionId { get; init; } + public Guid AdId { get; init; } + public Guid CampaignId { get; init; } + public Guid UserId { get; init; } + public string PlacementType { get; init; } = string.Empty; + public decimal FinalPrice { get; init; } + public decimal WinningEcpm { get; init; } + public DateTime ServedAt { get; init; } +} + +/// +/// EN: Event payload for impression. +/// VI: Payload event cho impression. +/// +public record AdImpressionTrackedEvent +{ + public Guid AdId { get; init; } + public Guid UserId { get; init; } + public DateTime Timestamp { get; init; } +} + +/// +/// EN: Event payload for click. +/// VI: Payload event cho click. +/// +public record AdClickTrackedEvent +{ + public Guid AdId { get; init; } + public Guid UserId { get; init; } + public DateTime Timestamp { get; init; } +} + +/// +/// EN: Abstraction for publishing ads-serving events asynchronously. +/// VI: Abstraction để publish bất đồng bộ các sự kiện ads-serving. +/// +public interface IAdServingEventPublisher +{ + Task PublishAdServedAsync(AdServedEvent payload, CancellationToken cancellationToken); + Task PublishImpressionTrackedAsync(AdImpressionTrackedEvent payload, CancellationToken cancellationToken); + Task PublishClickTrackedAsync(AdClickTrackedEvent payload, CancellationToken cancellationToken); +} + +/// +/// EN: Logging-based publisher used as default implementation. +/// VI: Publisher dựa trên logging dùng làm implementation mặc định. +/// +public class LoggingAdServingEventPublisher : IAdServingEventPublisher +{ + private readonly ILogger _logger; + + public LoggingAdServingEventPublisher(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task PublishAdServedAsync(AdServedEvent payload, CancellationToken cancellationToken) + { + _logger.LogInformation( + "ad.served.v1 event published: AuctionId={AuctionId}, AdId={AdId}, CampaignId={CampaignId}, UserId={UserId}, PlacementType={PlacementType}, FinalPrice={FinalPrice}, WinningEcpm={WinningEcpm}, ServedAt={ServedAt}", + payload.AuctionId, + payload.AdId, + payload.CampaignId, + payload.UserId, + payload.PlacementType, + payload.FinalPrice, + payload.WinningEcpm, + payload.ServedAt); + + return Task.CompletedTask; + } + + public Task PublishImpressionTrackedAsync(AdImpressionTrackedEvent payload, CancellationToken cancellationToken) + { + _logger.LogInformation( + "ads.impression.tracked.v1 event published: AdId={AdId}, UserId={UserId}, Timestamp={Timestamp}", + payload.AdId, + payload.UserId, + payload.Timestamp); + + return Task.CompletedTask; + } + + public Task PublishClickTrackedAsync(AdClickTrackedEvent payload, CancellationToken cancellationToken) + { + _logger.LogInformation( + "ads.click.tracked.v1 event published: AdId={AdId}, UserId={UserId}, Timestamp={Timestamp}", + payload.AdId, + payload.UserId, + payload.Timestamp); + + return Task.CompletedTask; + } +} diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQuery.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQuery.cs new file mode 100644 index 00000000..0450142a --- /dev/null +++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQuery.cs @@ -0,0 +1,21 @@ +using MediatR; + +namespace AdsServingService.API.Application.Queries; + +/// +/// EN: Query to retrieve aggregate auction statistics. +/// VI: Query để lấy thống kê tổng hợp của auction. +/// +public record GetAuctionStatisticsQuery : IRequest; + +/// +/// EN: Aggregate auction statistics DTO. +/// VI: DTO thống kê tổng hợp auction. +/// +public record AuctionStatisticsDto +{ + public int TotalAuctions { get; init; } + public decimal AverageWinRate { get; init; } + public decimal AverageeCPM { get; init; } + public long TotalBidsPlaced { get; init; } +} diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQueryHandler.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQueryHandler.cs new file mode 100644 index 00000000..f364a800 --- /dev/null +++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQueryHandler.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using AdsServingService.Infrastructure; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace AdsServingService.API.Application.Queries; + +/// +/// EN: Handler for GetAuctionStatisticsQuery. +/// VI: Handler cho GetAuctionStatisticsQuery. +/// +public class GetAuctionStatisticsQueryHandler + : IRequestHandler +{ + private readonly AdsServingServiceContext _context; + + public GetAuctionStatisticsQueryHandler(AdsServingServiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task Handle( + GetAuctionStatisticsQuery request, + CancellationToken cancellationToken) + { + var auctionSnapshots = await _context.Auctions + .AsNoTracking() + .Select(auction => new + { + HasWinner = auction.Result != null, + WinningEcpm = auction.Result != null ? auction.Result.WinningeCPM : 0m, + BidsJson = EF.Property(auction, "_bidsJson"), + }) + .ToListAsync(cancellationToken); + + var totalAuctions = auctionSnapshots.Count; + var totalWinners = auctionSnapshots.Count(snapshot => snapshot.HasWinner); + var averageWinRate = totalAuctions == 0 + ? 0m + : Math.Round((decimal)totalWinners / totalAuctions * 100m, 2); + + var winningEcpmValues = auctionSnapshots + .Where(snapshot => snapshot.HasWinner) + .Select(snapshot => snapshot.WinningEcpm) + .ToList(); + + var averageEcpm = winningEcpmValues.Count == 0 + ? 0m + : Math.Round(winningEcpmValues.Average(), 4); + + var totalBidsPlaced = auctionSnapshots + .Sum(snapshot => CountBids(snapshot.BidsJson)); + + return new AuctionStatisticsDto + { + TotalAuctions = totalAuctions, + AverageWinRate = averageWinRate, + AverageeCPM = averageEcpm, + TotalBidsPlaced = totalBidsPlaced, + }; + } + + private static int CountBids(string? bidsJson) + { + if (string.IsNullOrWhiteSpace(bidsJson)) + { + return 0; + } + + try + { + using var document = JsonDocument.Parse(bidsJson); + return document.RootElement.ValueKind == JsonValueKind.Array + ? document.RootElement.GetArrayLength() + : 0; + } + catch (JsonException) + { + return 0; + } + } +} diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs index 1602e908..92308f7c 100644 --- a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs +++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs @@ -1,6 +1,7 @@ using AdsServingService.Infrastructure; using MediatR; using Microsoft.EntityFrameworkCore; +using System.Text.Json; namespace AdsServingService.API.Application.Queries; @@ -42,25 +43,59 @@ public class GetAuctionsQueryHandler : IRequestHandler EF.Property(a, "_auctionTime")) .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) - .Select(a => new AuctionDto + .Select(a => new { Id = a.Id, UserId = a.UserId, PlacementType = a.PlacementType, AuctionTime = EF.Property(a, "_auctionTime"), - BidCount = a.Bids.Count, + BidsJson = EF.Property(a, "_bidsJson"), WinningAdId = a.Result != null ? a.Result.WinningAdId : null, FinalPrice = a.Result != null ? a.Result.FinalPrice : null, WinningeCPM = a.Result != null ? a.Result.WinningeCPM : null }) .ToListAsync(cancellationToken); + var items = auctions + .Select(a => new AuctionDto + { + Id = a.Id, + UserId = a.UserId, + PlacementType = a.PlacementType, + AuctionTime = a.AuctionTime, + BidCount = CountBids(a.BidsJson), + WinningAdId = a.WinningAdId, + FinalPrice = a.FinalPrice, + WinningeCPM = a.WinningeCPM, + }) + .ToList(); + return new PagedResult { - Items = auctions, + Items = items, TotalCount = totalCount, Page = request.Page, PageSize = request.PageSize }; } + + private static int CountBids(string? bidsJson) + { + if (string.IsNullOrWhiteSpace(bidsJson)) + { + return 0; + } + + try + { + using var document = JsonDocument.Parse(bidsJson); + return document.RootElement.ValueKind == JsonValueKind.Array + ? document.RootElement.GetArrayLength() + : 0; + } + catch (JsonException) + { + return 0; + } + } } diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/ServeAdQueryHandler.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/ServeAdQueryHandler.cs index 04e556d3..dfd71ee1 100644 --- a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/ServeAdQueryHandler.cs +++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/ServeAdQueryHandler.cs @@ -1,4 +1,8 @@ using AdsServingService.Domain.AggregatesModel.AuctionAggregate; +using AdsServingService.API.Application.Events; +using AdsServingService.API.Application.Services; +using AdsServingService.Infrastructure; +using System.Text.Json; using MediatR; namespace AdsServingService.API.Application.Queries; @@ -9,10 +13,23 @@ namespace AdsServingService.API.Application.Queries; /// public class ServeAdQueryHandler : IRequestHandler { + private readonly AdsServingServiceContext _context; + private readonly IEligibleAdsProvider _eligibleAdsProvider; + private readonly IAuctionScoringService _auctionScoringService; + private readonly IAdServingEventPublisher _eventPublisher; private readonly ILogger _logger; - public ServeAdQueryHandler(ILogger logger) + public ServeAdQueryHandler( + AdsServingServiceContext context, + IEligibleAdsProvider eligibleAdsProvider, + IAuctionScoringService auctionScoringService, + IAdServingEventPublisher eventPublisher, + ILogger logger) { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _eligibleAdsProvider = eligibleAdsProvider ?? throw new ArgumentNullException(nameof(eligibleAdsProvider)); + _auctionScoringService = auctionScoringService ?? throw new ArgumentNullException(nameof(auctionScoringService)); + _eventPublisher = eventPublisher ?? throw new ArgumentNullException(nameof(eventPublisher)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -22,9 +39,11 @@ public class ServeAdQueryHandler : IRequestHandler try { - // TODO: Step 1 - Fetch eligible ads from cache/database - // For now, mock data for demonstration - var eligibleAds = GetMockEligibleAds(request.PlacementType); + var eligibleAds = await _eligibleAdsProvider.GetEligibleAdsAsync( + request.UserId, + request.PlacementType, + request.UserContext, + cancellationToken); if (!eligibleAds.Any()) { @@ -38,12 +57,17 @@ public class ServeAdQueryHandler : IRequestHandler // Step 3 - Add bids for each eligible ad foreach (var ad in eligibleAds) { - // TODO: Get actual bid amount, CTR prediction, quality score - var bidAmount = ad.BidAmount; - var predictedCTR = 0.02m; // 2% CTR (mock) - var qualityScore = 1.0m; // Perfect quality (mock) + var signals = _auctionScoringService.Score( + ad, + request.PlacementType, + request.UserContext); - auction.AddBid(ad.AdId, ad.CampaignId, bidAmount, predictedCTR, qualityScore); + auction.AddBid( + ad.AdId, + ad.CampaignId, + ad.BidAmount, + signals.PredictedCtr, + signals.QualityScore); } // Step 4 - Run auction @@ -58,8 +82,33 @@ public class ServeAdQueryHandler : IRequestHandler // Step 5 - Get winning ad details var winningAd = eligibleAds.First(a => a.AdId == auction.Result.WinningAdId); - // TODO: Step 6 - Fire async events (impression tracking, billing) - + _context.Auctions.Add(auction); + _context.Entry(auction).Property("_bidsJson").CurrentValue = JsonSerializer.Serialize( + auction.Bids.Select(bid => new + { + bid.AdId, + bid.CampaignId, + bid.BidAmount, + bid.PredictedCTR, + bid.QualityScore, + bid.eCPM, + })); + await _context.SaveEntitiesAsync(cancellationToken); + + await _eventPublisher.PublishAdServedAsync( + new AdServedEvent + { + AuctionId = auction.Id, + AdId = winningAd.AdId, + CampaignId = winningAd.CampaignId, + UserId = request.UserId, + PlacementType = request.PlacementType, + FinalPrice = auction.Result.FinalPrice, + WinningEcpm = auction.Result.WinningeCPM, + ServedAt = DateTime.UtcNow, + }, + cancellationToken); + var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds; _logger.LogInformation( "Ad served in {Elapsed}ms - AdId: {AdId}, eCPM: {eCPM}, Price: {Price}", @@ -85,49 +134,4 @@ public class ServeAdQueryHandler : IRequestHandler return null; } } - - // Mock data for demonstration - private List GetMockEligibleAds(string placementType) - { - return new List - { - new MockAdData - { - AdId = Guid.NewGuid(), - CampaignId = Guid.NewGuid(), - BidAmount = 5000, // 5000 VND CPC - Format = "single_image", - Headline = "Amazing Product - Limited Offer!", - PrimaryText = "Get 50% off today only", - CallToAction = "Shop Now", - CreativeUrl = "https://example.com/creative.jpg", - DestinationUrl = "https://example.com/product" - }, - new MockAdData - { - AdId = Guid.NewGuid(), - CampaignId = Guid.NewGuid(), - BidAmount = 4500, - Format = "single_image", - Headline = "Summer Sale", - PrimaryText = "Up to 70% discount", - CallToAction = "Learn More", - CreativeUrl = "https://example.com/creative2.jpg", - DestinationUrl = "https://example.com/sale" - } - }; - } -} - -internal record MockAdData -{ - public Guid AdId { get; init; } - public Guid CampaignId { get; init; } - public decimal BidAmount { get; init; } - public string Format { get; init; } = null!; - public string? Headline { get; init; } - public string? PrimaryText { get; init; } - public string? CallToAction { get; init; } - public string? CreativeUrl { get; init; } - public string? DestinationUrl { get; init; } } diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/AuctionScoringService.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/AuctionScoringService.cs new file mode 100644 index 00000000..35ab07a2 --- /dev/null +++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/AuctionScoringService.cs @@ -0,0 +1,65 @@ +namespace AdsServingService.API.Application.Services; + +/// +/// EN: Scoring signal tuple used to place bids in auction. +/// VI: Bộ tín hiệu scoring dùng để đặt bid trong auction. +/// +public record AuctionBidSignals(decimal PredictedCtr, decimal QualityScore); + +/// +/// EN: Abstraction for scoring hooks in ad serving auction. +/// VI: Abstraction cho scoring hook trong auction phục vụ quảng cáo. +/// +public interface IAuctionScoringService +{ + AuctionBidSignals Score( + EligibleAdCandidate candidate, + string placementType, + IReadOnlyDictionary userContext); +} + +/// +/// EN: Default heuristic scoring strategy. +/// VI: Chiến lược scoring heuristic mặc định. +/// +public class DefaultAuctionScoringService : IAuctionScoringService +{ + public AuctionBidSignals Score( + EligibleAdCandidate candidate, + string placementType, + IReadOnlyDictionary userContext) + { + var baseCtr = placementType.ToLowerInvariant() switch + { + "story" => 0.018m, + "banner" => 0.012m, + _ => 0.020m, + }; + + var segmentBoost = 1.0m; + if (userContext.TryGetValue("segment", out var segmentValue) && + segmentValue.Equals("high-intent", StringComparison.OrdinalIgnoreCase)) + { + segmentBoost += 0.25m; + } + + var recencyBoost = 1.0m; + if (userContext.TryGetValue("recentPurchaseDays", out var recencyRaw) && + int.TryParse(recencyRaw, out var recencyDays) && + recencyDays <= 14) + { + recencyBoost += 0.10m; + } + + var predictedCtr = Math.Round(baseCtr * segmentBoost * recencyBoost, 4); + + var qualityScore = candidate.Format.ToLowerInvariant() switch + { + "single_image" => 1.0m, + "video" => 1.1m, + _ => 0.95m, + }; + + return new AuctionBidSignals(predictedCtr, qualityScore); + } +} diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/EligibleAdsProvider.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/EligibleAdsProvider.cs new file mode 100644 index 00000000..410ace7c --- /dev/null +++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/EligibleAdsProvider.cs @@ -0,0 +1,89 @@ +namespace AdsServingService.API.Application.Services; + +/// +/// EN: Candidate ad model used in serving pipeline. +/// VI: Model quảng cáo ứng viên dùng trong pipeline phục vụ quảng cáo. +/// +public record EligibleAdCandidate +{ + public Guid AdId { get; init; } + public Guid CampaignId { get; init; } + public decimal BidAmount { get; init; } + public string Format { get; init; } = null!; + public string? Headline { get; init; } + public string? PrimaryText { get; init; } + public string? CallToAction { get; init; } + public string? CreativeUrl { get; init; } + public string? DestinationUrl { get; init; } +} + +/// +/// EN: Service abstraction to load eligible ads for a serve request. +/// VI: Abstraction service để tải danh sách quảng cáo đủ điều kiện cho request serve. +/// +public interface IEligibleAdsProvider +{ + Task> GetEligibleAdsAsync( + Guid userId, + string placementType, + IReadOnlyDictionary userContext, + CancellationToken cancellationToken); +} + +/// +/// EN: In-memory eligible ads provider used as default fallback. +/// VI: Provider quảng cáo đủ điều kiện dạng in-memory dùng như fallback mặc định. +/// +public class InMemoryEligibleAdsProvider : IEligibleAdsProvider +{ + public Task> GetEligibleAdsAsync( + Guid userId, + string placementType, + IReadOnlyDictionary userContext, + CancellationToken cancellationToken) + { + // EN: Keep deterministic IDs per request/user so placement is reproducible. + // VI: Giữ ID xác định theo request/user để kết quả placement có thể lặp lại. + var hashSeed = Math.Abs(HashCode.Combine(userId, placementType.ToLowerInvariant())); + var primaryCampaignId = DeterministicGuid($"{placementType}:{hashSeed}:campaign:primary"); + var secondaryCampaignId = DeterministicGuid($"{placementType}:{hashSeed}:campaign:secondary"); + + IReadOnlyList candidates = + [ + new EligibleAdCandidate + { + AdId = DeterministicGuid($"{placementType}:{hashSeed}:ad:primary"), + CampaignId = primaryCampaignId, + BidAmount = 5000m, + Format = "single_image", + Headline = "Amazing Product - Limited Offer!", + PrimaryText = "Get 50% off today only", + CallToAction = "Shop Now", + CreativeUrl = "https://example.com/creative.jpg", + DestinationUrl = "https://example.com/product", + }, + new EligibleAdCandidate + { + AdId = DeterministicGuid($"{placementType}:{hashSeed}:ad:secondary"), + CampaignId = secondaryCampaignId, + BidAmount = 4500m, + Format = "single_image", + Headline = "Summer Sale", + PrimaryText = "Up to 70% discount", + CallToAction = "Learn More", + CreativeUrl = "https://example.com/creative2.jpg", + DestinationUrl = "https://example.com/sale", + }, + ]; + + return Task.FromResult(candidates); + } + + private static Guid DeterministicGuid(string input) + { + var bytes = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input)); + var guidBytes = new byte[16]; + Array.Copy(bytes, guidBytes, guidBytes.Length); + return new Guid(guidBytes); + } +} diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdminAuctionsController.cs b/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdminAuctionsController.cs index 2bf41c21..acd89c58 100644 --- a/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdminAuctionsController.cs +++ b/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdminAuctionsController.cs @@ -56,25 +56,10 @@ public class AdminAuctionsController : ControllerBase /// [HttpGet("statistics")] [ProducesResponseType(typeof(AuctionStatisticsDto), StatusCodes.Status200OK)] - public ActionResult GetStatistics() + public async Task> GetStatistics() { - // TODO: Implement statistics query handler _logger.LogInformation("Auction statistics requested"); - - return Ok(new AuctionStatisticsDto - { - TotalAuctions = 0, - AverageWinRate = 0, - AverageeCPM = 0, - TotalBidsPlaced = 0 - }); + var statistics = await _mediator.Send(new GetAuctionStatisticsQuery()); + return Ok(statistics); } } - -public record AuctionStatisticsDto -{ - public int TotalAuctions { get; init; } - public decimal AverageWinRate { get; init; } - public decimal AverageeCPM { get; init; } - public long TotalBidsPlaced { get; init; } -} diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdsController.cs b/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdsController.cs index 2fb2bc31..ce7e2777 100644 --- a/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdsController.cs +++ b/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdsController.cs @@ -1,4 +1,5 @@ using AdsServingService.API.Application.Queries; +using AdsServingService.API.Application.Events; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -14,11 +15,16 @@ namespace AdsServingService.API.Controllers; public class AdsController : ControllerBase { private readonly IMediator _mediator; + private readonly IAdServingEventPublisher _eventPublisher; private readonly ILogger _logger; - public AdsController(IMediator mediator, ILogger logger) + public AdsController( + IMediator mediator, + IAdServingEventPublisher eventPublisher, + ILogger logger) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _eventPublisher = eventPublisher ?? throw new ArgumentNullException(nameof(eventPublisher)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -52,14 +58,18 @@ public class AdsController : ControllerBase /// [HttpPost("events/impression")] [ProducesResponseType(StatusCodes.Status202Accepted)] - public IActionResult TrackImpression([FromBody] ImpressionEvent impressionEvent) + public async Task TrackImpression([FromBody] ImpressionEvent impressionEvent) { _logger.LogInformation("Impression tracked for Ad {AdId}", impressionEvent.AdId); - - // TODO: Publish to RabbitMQ for async processing - // - ads-billing (charge advertiser) - // - ads-tracking (attribution) - // - ads-analytics (metrics) + + await _eventPublisher.PublishImpressionTrackedAsync( + new AdImpressionTrackedEvent + { + AdId = impressionEvent.AdId, + UserId = impressionEvent.UserId, + Timestamp = impressionEvent.Timestamp, + }, + HttpContext.RequestAborted); return Accepted(); } @@ -70,11 +80,18 @@ public class AdsController : ControllerBase /// [HttpPost("events/click")] [ProducesResponseType(StatusCodes.Status202Accepted)] - public IActionResult TrackClick([FromBody] ClickEvent clickEvent) + public async Task TrackClick([FromBody] ClickEvent clickEvent) { _logger.LogInformation("Click tracked for Ad {AdId}", clickEvent.AdId); - - // TODO: Publish to RabbitMQ for async processing + + await _eventPublisher.PublishClickTrackedAsync( + new AdClickTrackedEvent + { + AdId = clickEvent.AdId, + UserId = clickEvent.UserId, + Timestamp = clickEvent.Timestamp, + }, + HttpContext.RequestAborted); return Accepted(); } diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Program.cs b/services/ads-serving-service-net/src/AdsServingService.API/Program.cs index e6e5ed9d..f788dadb 100644 --- a/services/ads-serving-service-net/src/AdsServingService.API/Program.cs +++ b/services/ads-serving-service-net/src/AdsServingService.API/Program.cs @@ -1,4 +1,6 @@ using Asp.Versioning; +using AdsServingService.API.Application.Events; +using AdsServingService.API.Application.Services; using FluentValidation; using Hellang.Middleware.ProblemDetails; using AdsServingService.API.Application.Behaviors; @@ -25,6 +27,9 @@ try // EN: Add Infrastructure services / VI: Thêm Infrastructure services builder.Services.AddInfrastructure(builder.Configuration); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors builder.Services.AddMediatR(cfg =>