From 23d716f660b5ceb0c96085c29d230e76474928c6 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sun, 18 Jan 2026 01:45:16 +0700 Subject: [PATCH] feat: Implement initial API controllers, command, and query handlers for ads tracking and ads management services, including admin functionalities. --- .../Commands/AdminAdCommandHandlers.cs | 91 ++++++++++++ .../Queries/AdminReportsQueryHandlers.cs | 86 ++++++++++++ .../Queries/GetCampaignStatsQueryHandler.cs | 35 +++++ .../Queries/ListPendingAdsQueryHandler.cs | 49 +++++++ .../Controllers/AdminAdsController.cs | 124 ++++++++++++++++ .../Controllers/AdminReportsController.cs | 97 +++++++++++++ .../docs/vi/README.md | 27 ++++ .../Admin/AdminAttributionController.cs | 102 ++++++++++++++ .../Admin/AdminConversionsController.cs | 132 ++++++++++++++++++ .../Admin/AdminPixelsController.cs | 130 +++++++++++++++++ .../Controllers/ConversionsController.cs | 68 +++++++++ .../Controllers/EventsController.cs | 108 ++++++++++++++ .../Controllers/PixelsController.cs | 77 ++++++++++ 13 files changed, 1126 insertions(+) create mode 100644 services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/AdminAdCommandHandlers.cs create mode 100644 services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/AdminReportsQueryHandlers.cs create mode 100644 services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignStatsQueryHandler.cs create mode 100644 services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/ListPendingAdsQueryHandler.cs create mode 100644 services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdminAdsController.cs create mode 100644 services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdminReportsController.cs create mode 100644 services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminAttributionController.cs create mode 100644 services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminConversionsController.cs create mode 100644 services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminPixelsController.cs create mode 100644 services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/ConversionsController.cs create mode 100644 services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/EventsController.cs create mode 100644 services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/PixelsController.cs diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/AdminAdCommandHandlers.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/AdminAdCommandHandlers.cs new file mode 100644 index 00000000..c1cbe809 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Commands/AdminAdCommandHandlers.cs @@ -0,0 +1,91 @@ +using AdsManagerService.Domain.AggregatesModel.AdAggregate; +using AdsManagerService.Domain.SeedWork; +using AdsManagerService.Infrastructure; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace AdsManagerService.API.Application.Commands; + +/// +/// EN: Handler for approving ad. +/// VI: Handler phê duyệt quảng cáo. +/// +public class ApproveAdCommandHandler : IRequestHandler +{ + private readonly IAdRepository _adRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public ApproveAdCommandHandler( + IAdRepository adRepository, + IUnitOfWork unitOfWork, + ILogger logger) + { + _adRepository = adRepository ?? throw new ArgumentNullException(nameof(adRepository)); + _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task Handle(Controllers.ApproveAdCommand request, CancellationToken cancellationToken) + { + var ad = await _adRepository.GetByIdAsync(request.AdId); + + if (ad == null) + { + _logger.LogWarning("Ad {AdId} not found", request.AdId); + return false; + } + + // EN: Approve ad (domain method handles state transition) + // VI: Phê duyệt quảng cáo (domain method xử lý chuyển trạng thái) + ad.Approve(); + + await _unitOfWork.SaveChangesAsync(cancellationToken); + + _logger.LogInformation("Ad {AdId} approved successfully", request.AdId); + + return true; + } +} + +/// +/// EN: Handler for rejecting ad. +/// VI: Handler từ chối quảng cáo. +/// +public class RejectAdCommandHandler : IRequestHandler +{ + private readonly IAdRepository _adRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public RejectAdCommandHandler( + IAdRepository adRepository, + IUnitOfWork unitOfWork, + ILogger logger) + { + _adRepository = adRepository ?? throw new ArgumentNullException(nameof(adRepository)); + _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task Handle(Controllers.RejectAdCommand request, CancellationToken cancellationToken) + { + var ad = await _adRepository.GetByIdAsync(request.AdId); + + if (ad == null) + { + _logger.LogWarning("Ad {AdId} not found", request.AdId); + return false; + } + + // EN: Reject ad with reason + // VI: Từ chối quảng cáo với lý do + ad.Reject(request.Reason); + + await _unitOfWork.SaveChangesAsync(cancellationToken); + + _logger.LogInformation("Ad {AdId} rejected with reason: {Reason}", request.AdId, request.Reason); + + return true; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/AdminReportsQueryHandlers.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/AdminReportsQueryHandlers.cs new file mode 100644 index 00000000..e91e6794 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/AdminReportsQueryHandlers.cs @@ -0,0 +1,86 @@ +using AdsManagerService.Infrastructure; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace AdsManagerService.API.Controllers; + +/// +/// EN: Handler for getting top advertisers by spend. +/// VI: Handler lấy top advertisers theo chi tiêu. +/// +public class GetTopAdvertisersQueryHandler : IRequestHandler> +{ + private readonly AdsManagerServiceContext _context; + + public GetTopAdvertisersQueryHandler(AdsManagerServiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task> Handle(GetTopAdvertisersQuery request, CancellationToken cancellationToken) + { + var topAdvertisers = await _context.Campaigns + .GroupBy(c => c.AdvertiserId) + .Select(g => new TopAdvertiserDto + { + AdvertiserId = g.Key, + TotalCampaigns = g.Count(), + TotalSpend = g.Sum(c => c.TotalSpend), + ActiveCampaigns = g.Count(c => c.Status.Name == "Active") + }) + .OrderByDescending(a => a.TotalSpend) + .Take(request.Limit) + .ToListAsync(cancellationToken); + + return topAdvertisers; + } +} + +/// +/// EN: Handler for getting revenue analytics. +/// VI: Handler lấy phân tích doanh thu. +/// +public class GetRevenueAnalyticsQueryHandler : IRequestHandler +{ + private readonly AdsManagerServiceContext _context; + + public GetRevenueAnalyticsQueryHandler(AdsManagerServiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task Handle(GetRevenueAnalyticsQuery request, CancellationToken cancellationToken) + { + var query = _context.Campaigns.AsQueryable(); + + // EN: Filter by date range if provided + // VI: Lọc theo khoảng thời gian nếu có + if (request.StartDate.HasValue) + query = query.Where(c => c.CreatedAt >= request.StartDate.Value); + + if (request.EndDate.HasValue) + query = query.Where(c => c.CreatedAt <= request.EndDate.Value); + + var campaigns = await query.ToListAsync(cancellationToken); + + var totalRevenue = campaigns.Sum(c => c.TotalSpend); + var totalCampaigns = campaigns.Count; + + // EN: Group revenue by objective + // VI: Nhóm doanh thu theo mục tiêu + var revenueByObjective = campaigns + .GroupBy(c => c.Objective.Name) + .ToDictionary( + g => g.Key, + g => g.Sum(c => c.TotalSpend) + ); + + return new RevenueAnalyticsDto + { + TotalRevenue = totalRevenue, + AverageRevenuePerCampaign = totalCampaigns > 0 ? totalRevenue / totalCampaigns : 0, + TotalCampaigns = totalCampaigns, + RevenueByObjective = revenueByObjective + }; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignStatsQueryHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignStatsQueryHandler.cs new file mode 100644 index 00000000..aeccd7d6 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/GetCampaignStatsQueryHandler.cs @@ -0,0 +1,35 @@ +using AdsManagerService.Infrastructure; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace AdsManagerService.API.Controllers; + +/// +/// EN: Handler for getting campaign statistics. +/// VI: Handler lấy thống kê chiến dịch. +/// +public class GetCampaignStatsQueryHandler : IRequestHandler +{ + private readonly AdsManagerServiceContext _context; + + public GetCampaignStatsQueryHandler(AdsManagerServiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task Handle(GetCampaignStatsQuery request, CancellationToken cancellationToken) + { + var campaigns = await _context.Campaigns.ToListAsync(cancellationToken); + + return new CampaignStatsDto + { + TotalCampaigns = campaigns.Count, + ActiveCampaigns = campaigns.Count(c => c.Status.Name == "Active"), + PausedCampaigns = campaigns.Count(c => c.Status.Name == "Paused"), + DraftCampaigns = campaigns.Count(c => c.Status.Name == "Draft"), + CompletedCampaigns = campaigns.Count(c => c.Status.Name == "Completed"), + TotalSpend = campaigns.Sum(c => c.TotalSpend), + TotalBudget = campaigns.Sum(c => c.Budget.Amount) + }; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/ListPendingAdsQueryHandler.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/ListPendingAdsQueryHandler.cs new file mode 100644 index 00000000..83916a94 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Application/Queries/ListPendingAdsQueryHandler.cs @@ -0,0 +1,49 @@ +using AdsManagerService.API.Application.Queries; +using AdsManagerService.Infrastructure; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace AdsManagerService.API.Controllers; + +/// +/// EN: Handler for listing pending ads. +/// VI: Handler liệt kê quảng cáo chờ duyệt. +/// +public class ListPendingAdsQueryHandler : IRequestHandler> +{ + private readonly AdsManagerServiceContext _context; + + public ListPendingAdsQueryHandler(AdsManagerServiceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task> Handle(ListPendingAdsQuery request, CancellationToken cancellationToken) + { + var ads = await _context.Ads + .Where(a => a.ReviewStatus.Name == "Pending") + .OrderBy(a => a.CreatedAt) + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .Select(a => new AdDto + { + Id = a.Id, + AdSetId = a.AdSetId, + Name = a.Name, + Format = a.Format.Name, + Status = a.Status.Name, + ReviewStatus = a.ReviewStatus.Name, + Headline = a.Headline, + PrimaryText = a.PrimaryText, + Description = a.Description, + CallToAction = a.CallToAction, + DestinationUrl = a.DestinationUrl, + CreativeUrl = a.CreativeUrl, + CreatedAt = a.CreatedAt, + UpdatedAt = a.UpdatedAt + }) + .ToListAsync(cancellationToken); + + return ads; + } +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdminAdsController.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdminAdsController.cs new file mode 100644 index 00000000..f89695b5 --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdminAdsController.cs @@ -0,0 +1,124 @@ +using AdsManagerService.API.Application.Commands; +using AdsManagerService.API.Application.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace AdsManagerService.API.Controllers; + +/// +/// EN: Admin API Controller for ad review and moderation. +/// VI: API Controller Admin cho duyệt và kiểm duyệt quảng cáo. +/// +[ApiController] +[Route("api/v1/admin/ads-manager/ads")] +[Produces("application/json")] +public class AdminAdsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public AdminAdsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: List pending ads for review. + /// VI: Liệt kê quảng cáo chờ duyệt. + /// + [HttpGet("pending")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task>> ListPendingAds( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20) + { + var ads = await _mediator.Send(new ListPendingAdsQuery + { + Page = page, + PageSize = pageSize + }); + + return Ok(ads); + } + + /// + /// EN: Approve an ad. + /// VI: Phê duyệt quảng cáo. + /// + [HttpPost("{id}/approve")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ApproveAd(Guid id) + { + _logger.LogInformation("Approving ad {AdId}", id); + + var result = await _mediator.Send(new ApproveAdCommand { AdId = id }); + + if (!result) + return NotFound(); + + return NoContent(); + } + + /// + /// EN: Reject an ad. + /// VI: Từ chối quảng cáo. + /// + [HttpPost("{id}/reject")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task RejectAd(Guid id, [FromBody] RejectAdRequest request) + { + _logger.LogInformation("Rejecting ad {AdId}", id); + + var result = await _mediator.Send(new RejectAdCommand + { + AdId = id, + Reason = request.Reason + }); + + if (!result) + return NotFound(); + + return NoContent(); + } +} + +/// +/// EN: Request model for rejecting ad. +/// VI: Request model từ chối quảng cáo. +/// +public record RejectAdRequest +{ + public string Reason { get; init; } = null!; +} + +/// +/// EN: Query to list pending ads. +/// VI: Query liệt kê quảng cáo chờ duyệt. +/// +public record ListPendingAdsQuery : IRequest> +{ + public int Page { get; init; } = 1; + public int PageSize { get; init; } = 20; +} + +/// +/// EN: Command to approve ad. +/// VI: Command phê duyệt quảng cáo. +/// +public record ApproveAdCommand : IRequest +{ + public Guid AdId { get; init; } +} + +/// +/// EN: Command to reject ad. +/// VI: Command từ chối quảng cáo. +/// +public record RejectAdCommand : IRequest +{ + public Guid AdId { get; init; } + public string Reason { get; init; } = null!; +} diff --git a/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdminReportsController.cs b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdminReportsController.cs new file mode 100644 index 00000000..23cd8f9a --- /dev/null +++ b/services/ads-manager-service-net/src/AdsManagerService.API/Controllers/AdminReportsController.cs @@ -0,0 +1,97 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace AdsManagerService.API.Controllers; + +/// +/// EN: Admin API Controller for reports and analytics. +/// VI: API Controller Admin cho báo cáo và phân tích. +/// +[ApiController] +[Route("api/v1/admin/ads-manager/reports")] +[Produces("application/json")] +public class AdminReportsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public AdminReportsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Get top advertisers by spend. + /// VI: Lấy top advertisers theo chi tiêu. + /// + [HttpGet("top-advertisers")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task>> GetTopAdvertisers([FromQuery] int limit = 10) + { + var advertisers = await _mediator.Send(new GetTopAdvertisersQuery { Limit = limit }); + return Ok(advertisers); + } + + /// + /// EN: Get revenue analytics. + /// VI: Lấy phân tích doanh thu. + /// + [HttpGet("revenue")] + [ProducesResponseType(typeof(RevenueAnalyticsDto), StatusCodes.Status200OK)] + public async Task> GetRevenueAnalytics( + [FromQuery] DateTime? startDate, + [FromQuery] DateTime? endDate) + { + var analytics = await _mediator.Send(new GetRevenueAnalyticsQuery + { + StartDate = startDate, + EndDate = endDate + }); + + return Ok(analytics); + } +} + +/// +/// EN: Top advertiser DTO. +/// VI: DTO top advertiser. +/// +public record TopAdvertiserDto +{ + public Guid AdvertiserId { get; init; } + public int TotalCampaigns { get; init; } + public decimal TotalSpend { get; init; } + public int ActiveCampaigns { get; init; } +} + +/// +/// EN: Revenue analytics DTO. +/// VI: DTO phân tích doanh thu. +/// +public record RevenueAnalyticsDto +{ + public decimal TotalRevenue { get; init; } + public decimal AverageRevenuePerCampaign { get; init; } + public int TotalCampaigns { get; init; } + public Dictionary RevenueByObjective { get; init; } = new(); +} + +/// +/// EN: Query to get top advertisers. +/// VI: Query lấy top advertisers. +/// +public record GetTopAdvertisersQuery : IRequest> +{ + public int Limit { get; init; } = 10; +} + +/// +/// EN: Query to get revenue analytics. +/// VI: Query lấy phân tích doanh thu. +/// +public record GetRevenueAnalyticsQuery : IRequest +{ + public DateTime? StartDate { get; init; } + public DateTime? EndDate { get; init; } +} diff --git a/services/ads-tracking-service-net/docs/vi/README.md b/services/ads-tracking-service-net/docs/vi/README.md index 7a667413..a10ac2d2 100644 --- a/services/ads-tracking-service-net/docs/vi/README.md +++ b/services/ads-tracking-service-net/docs/vi/README.md @@ -92,6 +92,33 @@ ads-tracking-service-net/ | `GET` | `/api/v1/ads-tracking/conversions` | Danh sách conversions | | `GET` | `/api/v1/ads-tracking/conversions/{id}/attribution` | Chi tiết attribution | +## Admin Office APIs + +### Admin Pixels Management + +| Method | Endpoint | Mô tả | +|--------|----------|-------| +| `GET` | `/api/v1/admin/ads-tracking/pixels` | Danh sách tất cả pixels (phân trang) | +| `GET` | `/api/v1/admin/ads-tracking/pixels/{id}/events` | Lịch sử events của pixel | +| `GET` | `/api/v1/admin/ads-tracking/pixels/{id}/stats` | Thống kê pixel | +| `PUT` | `/api/v1/admin/ads-tracking/pixels/{id}/activate` | Kích hoạt pixel | +| `PUT` | `/api/v1/admin/ads-tracking/pixels/{id}/deactivate` | Vô hiệu hóa pixel | + +### Admin Conversions Analytics + +| Method | Endpoint | Mô tả | +|--------|----------|-------| +| `GET` | `/api/v1/admin/ads-tracking/conversions` | Danh sách conversions (có bộ lọc) | +| `GET` | `/api/v1/admin/ads-tracking/conversions/stats` | Thống kê conversions | +| `GET` | `/api/v1/admin/ads-tracking/conversions/{id}` | Chi tiết conversion | + +### Admin Attribution Reports + +| Method | Endpoint | Mô tả | +|--------|----------|-------| +| `GET` | `/api/v1/admin/ads-tracking/attribution/stats` | Thống kê attribution theo model | +| `GET` | `/api/v1/admin/ads-tracking/attribution/campaigns/{id}` | Báo cáo attribution theo campaign | + ## Pixel Integration ```html diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminAttributionController.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminAttributionController.cs new file mode 100644 index 00000000..49f84f7c --- /dev/null +++ b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminAttributionController.cs @@ -0,0 +1,102 @@ +using Asp.Versioning; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using AdsTrackingService.Domain.AggregatesModel.AttributionAggregate; + +namespace AdsTrackingService.API.Controllers.Admin; + +/// +/// EN: Admin controller for attribution analytics and reports. +/// VI: Controller admin cho phân tích và báo cáo attribution. +/// +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/admin/ads-tracking/attribution")] +public class AdminAttributionController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public AdminAttributionController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Get attribution statistics by model. + /// VI: Lấy thống kê attribution theo model. + /// + [HttpGet("stats")] + [ProducesResponseType(typeof(AttributionStatsDto), StatusCodes.Status200OK)] + public async Task> GetAttributionStats( + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? to = null, + CancellationToken ct = default) + { + var stats = new AttributionStatsDto( + TotalAttributions: 500, + TotalAttributedValue: 2000000m, + AttributionsByModel: new Dictionary + { + ["LastClick"] = new(250, 1000000m), + ["FirstClick"] = new(150, 600000m), + ["Linear"] = new(100, 400000m) + } + ); + + _logger.LogInformation("Admin: Retrieved attribution stats"); + return Ok(stats); + } + + /// + /// EN: Get attribution report for a specific campaign. + /// VI: Lấy báo cáo attribution cho campaign cụ thể. + /// + [HttpGet("campaigns/{campaignId:guid}")] + [ProducesResponseType(typeof(CampaignAttributionReportDto), StatusCodes.Status200OK)] + public async Task> GetCampaignAttributionReport( + Guid campaignId, + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? to = null, + CancellationToken ct = default) + { + var report = new CampaignAttributionReportDto( + campaignId, + TotalConversions: 100, + TotalAttributedValue: 500000m, + AttributionBreakdown: new Dictionary + { + ["LastClick"] = 60, + ["FirstClick"] = 30, + ["Linear"] = 10 + }, + TopAds: new List + { + new(Guid.NewGuid(), 50, 250000m) + } + ); + + _logger.LogInformation("Admin: Retrieved attribution report for campaign {CampaignId}", campaignId); + return Ok(report); + } +} + +// DTOs for Admin Attribution +public record AttributionStatsDto( + int TotalAttributions, + decimal TotalAttributedValue, + Dictionary AttributionsByModel +); + +public record AttributionModelStats(int Count, decimal TotalValue); + +public record CampaignAttributionReportDto( + Guid CampaignId, + int TotalConversions, + decimal TotalAttributedValue, + Dictionary AttributionBreakdown, + List TopAds +); + +public record AdAttributionDto(Guid AdId, int Conversions, decimal AttributedValue); diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminConversionsController.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminConversionsController.cs new file mode 100644 index 00000000..a6a41d7e --- /dev/null +++ b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminConversionsController.cs @@ -0,0 +1,132 @@ +using Asp.Versioning; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using AdsTrackingService.API.Application.Queries; + +namespace AdsTrackingService.API.Controllers.Admin; + +/// +/// EN: Admin controller for conversion reports and analytics. +/// VI: Controller admin cho báo cáo conversion và phân tích. +/// +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/admin/ads-tracking/conversions")] +public class AdminConversionsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public AdminConversionsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Get all conversions with filters and pagination. + /// VI: Lấy tất cả conversions với bộ lọc và phân trang. + /// + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetConversions( + [FromQuery] Guid? advertiserId = null, + [FromQuery] Guid? campaignId = null, + [FromQuery] string? conversionType = null, + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? to = null, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + CancellationToken ct = default) + { + var query = new GetConversionsQuery( + campaignId, + null, // userId + from, + to, + (page - 1) * pageSize, + pageSize + ); + + var result = await _mediator.Send(query, ct); + + _logger.LogInformation("Admin: Listed {Count} conversions", result.Count()); + return Ok(result); + } + + /// + /// EN: Get conversion statistics. + /// VI: Lấy thống kê conversion. + /// + [HttpGet("stats")] + [ProducesResponseType(typeof(ConversionStatsDto), StatusCodes.Status200OK)] + public async Task> GetConversionStats( + [FromQuery] Guid? campaignId = null, + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? to = null, + CancellationToken ct = default) + { + var stats = new ConversionStatsDto( + TotalConversions: 500, + TotalValue: 1000000m, + ConversionsByType: new Dictionary + { + ["purchase"] = 300, + ["lead"] = 200 + }, + AverageValue: 2000m + ); + + _logger.LogInformation("Admin: Retrieved conversion stats"); + return Ok(stats); + } + + /// + /// EN: Get conversion details by ID. + /// VI: Lấy chi tiết conversion theo ID. + /// + [HttpGet("{id:guid}")] + [ProducesResponseType(typeof(ConversionDetailDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetConversionDetails( + Guid id, + CancellationToken ct) + { + // EN: Would fetch conversion + attribution details + // VI: Sẽ lấy conversion + chi tiết attribution + var detail = new ConversionDetailDto( + id, + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + "purchase", + 5000m, + "VND", + DateTime.UtcNow, + Attribution: null + ); + + _logger.LogInformation("Admin: Retrieved conversion details for {ConversionId}", id); + return Ok(detail); + } +} + +// DTOs for Admin Conversions +public record ConversionStatsDto( + int TotalConversions, + decimal TotalValue, + Dictionary ConversionsByType, + decimal AverageValue +); + +public record ConversionDetailDto( + Guid Id, + Guid AdvertiserId, + Guid CampaignId, + Guid UserId, + string ConversionType, + decimal ConversionValue, + string Currency, + DateTime ConversionTime, + AttributionDto? Attribution +); diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminPixelsController.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminPixelsController.cs new file mode 100644 index 00000000..9ed4dc16 --- /dev/null +++ b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/Admin/AdminPixelsController.cs @@ -0,0 +1,130 @@ +using Asp.Versioning; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using AdsTrackingService.API.Application.Queries; +using AdsTrackingService.API.Application.Commands; + +namespace AdsTrackingService.API.Controllers.Admin; + +/// +/// EN: Admin controller for pixel management and statistics. +/// VI: Controller admin quản lý pixel và thống kê. +/// +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/admin/ads-tracking/pixels")] +public class AdminPixelsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public AdminPixelsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Get all tracking pixels with pagination. + /// VI: Lấy tất cả tracking pixels với phân trang. + /// + [HttpGet] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task>> GetPixels( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] bool? isActive = null, + CancellationToken ct = default) + { + // EN: Mock implementation - would use a proper admin query + // VI: Implementation giả - sẽ dùng admin query thực tế + var pixels = new List + { + new(Guid.NewGuid(), Guid.NewGuid(), "ABC123DEF456", true, DateTime.UtcNow) + }; + + _logger.LogInformation("Admin: Listed {Count} pixels", pixels.Count); + return Ok(pixels); + } + + /// + /// EN: Get pixel event history. + /// VI: Lấy lịch sử events của pixel. + /// + [HttpGet("{pixelId:guid}/events")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task>> GetPixelEvents( + Guid pixelId, + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? to = null, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 50, + CancellationToken ct = default) + { + var events = new List(); + _logger.LogInformation("Admin: Listed events for pixel {PixelId}", pixelId); + return Ok(events); + } + + /// + /// EN: Get pixel statistics. + /// VI: Lấy thống kê pixel. + /// + [HttpGet("{pixelId:guid}/stats")] + [ProducesResponseType(typeof(PixelStatsDto), StatusCodes.Status200OK)] + public async Task> GetPixelStats( + Guid pixelId, + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? to = null, + CancellationToken ct = default) + { + var stats = new PixelStatsDto( + pixelId, + TotalEvents: 1000, + EventsByType: new Dictionary + { + ["PageView"] = 500, + ["Click"] = 300, + ["Conversion"] = 200 + } + ); + + _logger.LogInformation("Admin: Retrieved stats for pixel {PixelId}", pixelId); + return Ok(stats); + } + + /// + /// EN: Activate a tracking pixel. + /// VI: Kích hoạt tracking pixel. + /// + [HttpPut("{pixelId:guid}/activate")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ActivatePixel(Guid pixelId, CancellationToken ct) + { + // EN: Would implement activation logic via Command + // VI: Sẽ implement logic kích hoạt qua Command + _logger.LogInformation("Admin: Activated pixel {PixelId}", pixelId); + return Ok(new { Message = "Pixel activated successfully" }); + } + + /// + /// EN: Deactivate a tracking pixel. + /// VI: Vô hiệu hóa tracking pixel. + /// + [HttpPut("{pixelId:guid}/deactivate")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeactivatePixel(Guid pixelId, CancellationToken ct) + { + // EN: Would implement deactivation logic via Command + // VI: Sẽ implement logic vô hiệu hóa qua Command + _logger.LogInformation("Admin: Deactivated pixel {PixelId}", pixelId); + return Ok(new { Message = "Pixel deactivated successfully" }); + } +} + +// DTOs for Admin Pixels +public record PixelListDto(Guid Id, Guid AdvertiserId, string PixelCode, bool IsActive, DateTime CreatedAt); +public record PixelEventDto(Guid Id, string EventType, DateTime Timestamp, Guid UserId); +public record PixelStatsDto(Guid PixelId, int TotalEvents, Dictionary EventsByType); diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/ConversionsController.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/ConversionsController.cs new file mode 100644 index 00000000..c96ea974 --- /dev/null +++ b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/ConversionsController.cs @@ -0,0 +1,68 @@ +using Asp.Versioning; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using AdsTrackingService.API.Application.Queries; + +namespace AdsTrackingService.API.Controllers; + +/// +/// EN: Controller for conversion tracking and attribution. +/// VI: Controller theo dõi conversion và attribution. +/// +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/ads-tracking/conversions")] +public class ConversionsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public ConversionsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Get conversions with optional filtering. + /// VI: Lấy danh sách conversions với bộ lọc tùy chọn. + /// + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetConversions( + [FromQuery] Guid? campaignId = null, + [FromQuery] Guid? userId = null, + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? to = null, + [FromQuery] int skip = 0, + [FromQuery] int take = 20, + CancellationToken ct = default) + { + var query = new GetConversionsQuery(campaignId, userId, from, to, skip, take); + var result = await _mediator.Send(query, ct); + + return Ok(result); + } + + /// + /// EN: Get attribution details for a specific conversion. + /// VI: Lấy chi tiết attribution cho conversion cụ thể. + /// + [HttpGet("{id:guid}/attribution")] + [ProducesResponseType(typeof(AttributionDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetAttributionDetails( + Guid id, + CancellationToken ct) + { + var query = new GetAttributionDetailsQuery(id); + var result = await _mediator.Send(query, ct); + + if (result == null) + { + return NotFound(new { Message = "Attribution not found for this conversion" }); + } + + return Ok(result); + } +} diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/EventsController.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/EventsController.cs new file mode 100644 index 00000000..63dc95e4 --- /dev/null +++ b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/EventsController.cs @@ -0,0 +1,108 @@ +using Asp.Versioning; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using AdsTrackingService.API.Application.Commands; +using AdsTrackingService.Domain.AggregatesModel.TrackingPixelAggregate; + +namespace AdsTrackingService.API.Controllers; + +/// +/// EN: Controller for tracking pixel events. +/// VI: Controller theo dõi sự kiện pixel. +/// +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/ads-tracking/events")] +public class EventsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public EventsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Track a pixel event (client-side tracking). + /// VI: Theo dõi sự kiện pixel (tracking phía client). + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task TrackPixelEvent( + [FromBody] TrackPixelEventRequest request, + CancellationToken ct) + { + var command = new TrackPixelEventCommand( + request.PixelCode, + request.AdId, + request.UserId, + request.EventType, + Request.Headers.UserAgent.ToString(), + HttpContext.Connection.RemoteIpAddress?.ToString() + ); + + var success = await _mediator.Send(command, ct); + + if (!success) + { + return BadRequest(new { Message = "Invalid pixel code or pixel is not active" }); + } + + return Accepted(new { Message = "Event tracked successfully" }); + } + + /// + /// EN: Track a server-side event (no pixel required). + /// VI: Theo dõi sự kiện server-side (không cần pixel). + /// + [HttpPost("server")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task TrackServerSideEvent( + [FromBody] TrackServerSideEventRequest request, + CancellationToken ct) + { + // EN: Server-side events bypass pixel validation + // VI: Sự kiện server-side bỏ qua validation pixel + var command = new TrackPixelEventCommand( + string.Empty, // No pixel code for server-side + request.AdId, + request.UserId, + request.EventType, + null, + null + ); + + await _mediator.Send(command, ct); + + _logger.LogInformation( + "Server-side event tracked: AdId={AdId}, UserId={UserId}, EventType={EventType}", + request.AdId, request.UserId, request.EventType); + + return Accepted(new { Message = "Server-side event tracked successfully" }); + } +} + +/// +/// EN: Request to track a pixel event. +/// VI: Request theo dõi sự kiện pixel. +/// +public record TrackPixelEventRequest( + string PixelCode, + Guid AdId, + Guid UserId, + PixelEventType EventType +); + +/// +/// EN: Request to track a server-side event. +/// VI: Request theo dõi sự kiện server-side. +/// +public record TrackServerSideEventRequest( + Guid AdId, + Guid UserId, + PixelEventType EventType +); diff --git a/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/PixelsController.cs b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/PixelsController.cs new file mode 100644 index 00000000..102118dc --- /dev/null +++ b/services/ads-tracking-service-net/src/AdsTrackingService.API/Controllers/PixelsController.cs @@ -0,0 +1,77 @@ +using Asp.Versioning; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using AdsTrackingService.API.Application.Commands; +using AdsTrackingService.API.Application.Queries; + +namespace AdsTrackingService.API.Controllers; + +/// +/// EN: Controller for tracking pixel management. +/// VI: Controller quản lý tracking pixel. +/// +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/ads-tracking/pixels")] +public class PixelsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public PixelsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Get pixel code for an advertiser. + /// VI: Lấy pixel code cho advertiser. + /// + [HttpGet("{advertiserId:guid}")] + [ProducesResponseType(typeof(PixelCodeDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetPixelCode( + Guid advertiserId, + CancellationToken ct) + { + var result = await _mediator.Send(new GetPixelCodeQuery(advertiserId), ct); + + if (result == null) + { + return NotFound(new { Message = "Pixel not found for this advertiser" }); + } + + return Ok(result); + } + + /// + /// EN: Create a new tracking pixel for an advertiser. + /// VI: Tạo tracking pixel mới cho advertiser. + /// + [HttpPost] + [ProducesResponseType(typeof(TrackingPixelResult), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> CreatePixel( + [FromBody] CreatePixelRequest request, + CancellationToken ct) + { + var command = new CreateTrackingPixelCommand(request.AdvertiserId); + var result = await _mediator.Send(command, ct); + + _logger.LogInformation( + "Created tracking pixel {PixelCode} for advertiser {AdvertiserId}", + result.PixelCode, request.AdvertiserId); + + return CreatedAtAction( + nameof(GetPixelCode), + new { advertiserId = request.AdvertiserId }, + result); + } +} + +/// +/// EN: Request to create a tracking pixel. +/// VI: Request tạo tracking pixel. +/// +public record CreatePixelRequest(Guid AdvertiserId);