Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/Buy2.Api/Controllers/RewardsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,31 @@ public async Task<IActionResult> GetRewardById(int id, CancellationToken cancell

return Ok(result.Value);
}

[HttpGet("{id}/analytics")]
[Authorize(Roles = "HRAdmin,Admin,SuperAdmin")]
[ProducesResponseType(typeof(RewardAnalyticsDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetRewardAnalytics(
int id,
[FromQuery] RewardTransactionFilterQueryDto filter,
CancellationToken cancellation)
{
var result = await _mediator.Send(new GetRewardAnalyticsQuery(id, filter), cancellation);

if (result.IsNotFound)
{
return NotFound(new { message = result.ErrorMessage });
}

if (!result.IsSuccess)
{
return BadRequest(new { message = result.ErrorMessage });
}

return Ok(result.Value);
}
}
39 changes: 39 additions & 0 deletions src/Buy2.Application/DTOs/Rewards/DTOs/RewardAnalyticsDtos.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Buy2.Application.DTOs.Rewards.DTOs;

public record RewardTransactionFilterQueryDto(
DateTimeOffset? DateFrom,
DateTimeOffset? DateTo,
string? TimelinePeriod = "Monthly",
string? SearchTerm = null,
int PageNumber = 1,
int PageSize = 10
);

public record RedemptionTimelinePointDto(
string PeriodLabel,
DateTimeOffset DateFrom,
DateTimeOffset DateTo,
int RedemptionCount,
int TotalPointsSpent
);

public record RewardTransactionItemDto(
int Id,
int EmployeeId,
string EmployeeName,
string EmployeeCode,
string DepartmentName,
string VoucherCode,
DateTimeOffset RedeemedAt,
TimeSpan Time,
int PointsDeducted
);

public record RewardAnalyticsDto(
IReadOnlyCollection<RedemptionTimelinePointDto> Timeline,
IReadOnlyCollection<RewardTransactionItemDto> Transactions,
int TotalCount,
int PageNumber,
int PageSize,
int TotalPages
);
15 changes: 0 additions & 15 deletions src/Buy2.Application/DTOs/Rewards/DTOs/RewardProfileDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,6 @@ public record RewardKpiStatistics(
int TopRedeemed,
int PointsValue
);
// Reward Analytics Chart:
public record RewardRedemptionAnalyticsChartDto(
DateTimeOffset StartTime,
DateTimeOffset EndTime,
int TotalRedemptions
);
// Reward Transaction History Log:
public record RewardTransactionHistoryLogDto(
int TransactionId,
string EmployeeName,
DateTimeOffset Date,
DateTime Time,
string Code
);

public record RewardProfileResponseDto(
RewardProfileListDto Profile,
RewardKpiStatistics KpiStats
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using Buy2.Application.Common.Interfaces;
using Buy2.Application.Common.Models;
using Buy2.Application.DTOs.Rewards.DTOs;
using Buy2.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Buy2.Application.Features.Rewards.Queries;

public record GetRewardAnalyticsQuery(
int Id,
RewardTransactionFilterQueryDto Filter
) : IRequest<Result<RewardAnalyticsDto>>;

public class GetRewardAnalyticsQueryHandler : IRequestHandler<GetRewardAnalyticsQuery, Result<RewardAnalyticsDto>>
{
private readonly IRepository<RewardItem> _rewardItemRepository;
private readonly IRepository<RewardRedemption> _redemptionRepository;

public GetRewardAnalyticsQueryHandler(
IRepository<RewardItem> rewardItemRepository,
IRepository<RewardRedemption> redemptionRepository)
{
_rewardItemRepository = rewardItemRepository;
_redemptionRepository = redemptionRepository;
}

public async Task<Result<RewardAnalyticsDto>> Handle(GetRewardAnalyticsQuery query, CancellationToken cancellation)
{
var rewardItem = await _rewardItemRepository
.Query(true)
.FirstOrDefaultAsync(r => r.Id == query.Id, cancellation);

if (rewardItem is null)
{
return Result<RewardAnalyticsDto>.NotFound("Reward item not found.");
}

var (dateFrom, dateTo, isWeekly) = ResolveDateBounds(query.Filter);

var timeline = await BuildTimelineAsync(query.Id, dateFrom, dateTo, isWeekly, cancellation);

var transactions = await BuildTransactionsAsync(query, dateFrom, dateTo, cancellation);

var totalPages = transactions.TotalCount == 0
? 0
: (int)Math.Ceiling((double)transactions.TotalCount / transactions.PageSize);

return Result<RewardAnalyticsDto>.Success(new RewardAnalyticsDto(
timeline,
transactions.Items.ToList(),
transactions.TotalCount,
transactions.Page,
transactions.PageSize,
totalPages
));
}

private static (DateTimeOffset From, DateTimeOffset To, bool IsWeekly) ResolveDateBounds(RewardTransactionFilterQueryDto filter)
{
var to = filter.DateTo ?? DateTimeOffset.UtcNow;
var from = filter.DateFrom ?? to.AddMonths(-6);
var isWeekly = string.Equals(filter.TimelinePeriod, "Weekly", StringComparison.OrdinalIgnoreCase);
return (from, to, isWeekly);
}

private async Task<List<RedemptionTimelinePointDto>> BuildTimelineAsync(
int rewardId, DateTimeOffset dateFrom, DateTimeOffset dateTo, bool isWeekly, CancellationToken cancellation)
{
var redemptions = await _redemptionRepository
.Query(true)
.Where(r =>
r.RewardItemId == rewardId &&
r.RedeemedAt >= dateFrom &&
r.RedeemedAt <= dateTo)
.Select(r => new
{
r.RedeemedAt,
PointsSpent = r.PointsTransaction != null ? Math.Abs(r.PointsTransaction.Amount) : 0
})
.ToListAsync(cancellation);

return isWeekly
? redemptions
.GroupBy(r => StartOfWeek(r.RedeemedAt))
.Select(g => new RedemptionTimelinePointDto(
g.Key.ToString("dd MMM yyyy"),
g.Key,
g.Key.AddDays(6),
g.Count(),
g.Sum(r => r.PointsSpent)))
.OrderBy(p => p.DateFrom)
.ToList()
: redemptions
.GroupBy(r => new DateTimeOffset(r.RedeemedAt.DateTime.Year, r.RedeemedAt.DateTime.Month, 1, 0, 0, 0, TimeSpan.Zero))
.Select(g => new RedemptionTimelinePointDto(
g.Key.ToString("MMM yyyy"),
g.Key,
g.Key.AddMonths(1).AddDays(-1),
g.Count(),
g.Sum(r => r.PointsSpent)))
.OrderBy(p => p.DateFrom)
.ToList();
}

private async Task<PageResultDto<RewardTransactionItemDto>> BuildTransactionsAsync(
GetRewardAnalyticsQuery query, DateTimeOffset dateFrom, DateTimeOffset dateTo, CancellationToken cancellation)
{
var redemptionQuery = _redemptionRepository
.Query(true)
.Where(r =>
r.RewardItemId == query.Id &&
r.RedeemedAt >= dateFrom &&
r.RedeemedAt <= dateTo);

if (!string.IsNullOrWhiteSpace(query.Filter.SearchTerm))
{
var search = query.Filter.SearchTerm.Trim();
redemptionQuery = redemptionQuery.Where(r =>
r.Employee.FirstName.Contains(search) ||
r.Employee.LastName.Contains(search) ||
r.Employee.EmployeeCode.Contains(search) ||
r.VoucherCode.Contains(search));
}

var totalCount = await redemptionQuery.CountAsync(cancellation);

var pageNumber = Math.Max(1, query.Filter.PageNumber);
var pageSize = query.Filter.PageSize < 1 ? 10 : Math.Min(query.Filter.PageSize, 100);

var items = await redemptionQuery
.OrderByDescending(r => r.RedeemedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(r => new RewardTransactionItemDto(
r.Id,
r.EmployeeId,
r.Employee.FirstName + " " + r.Employee.LastName,
r.Employee.EmployeeCode,
r.Employee.JobRole != null && r.Employee.JobRole.Department != null
? r.Employee.JobRole.Department.Name
: string.Empty,
r.VoucherCode,
r.RedeemedAt,
r.RedeemedAt.DateTime.TimeOfDay,
Math.Abs(r.PointsTransaction.Amount)
))
.ToListAsync(cancellation);

return new PageResultDto<RewardTransactionItemDto>(items, totalCount, pageNumber, pageSize);
}

private static DateTimeOffset StartOfWeek(DateTimeOffset date)
{
var day = date.DateTime.Date;
var diff = (7 + (day.DayOfWeek - DayOfWeek.Monday)) % 7;
return new DateTimeOffset(day.AddDays(-diff), TimeSpan.Zero);
}
}
Loading