Skip to content

Commit

Permalink
Added support for calculating and providing instrument rank (#26)
Browse files Browse the repository at this point in the history
* Enhance Instrument Statistics with AIM Filter Integration

* Adjusted 'Group by Day' threshold in GetDateFormatForRange

* Added support for calculating and providing instrument rank
  • Loading branch information
miris-mp authored Dec 16, 2024
1 parent b3f1b97 commit 6d86685
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 33 deletions.
2 changes: 2 additions & 0 deletions src/ApiServer/ApiServer/Controllers/StatsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ [FromRoute] ObjectId sourceId
});
}

// API to send back the data for generation and redeemed vouchers
[HttpPost("vouchers/generated-redeemed-statistics")]
[Authorize]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
Expand All @@ -104,6 +105,7 @@ public async Task<ActionResult> FetchVouchersGeneratedAndRedeemedStats([FromBody
}
}

// API to send back the data for consumed vouchers
[HttpPost("vouchers/consumed-statistics")]
[Authorize]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
Expand Down
20 changes: 20 additions & 0 deletions src/ApiServer/ApiServer/DTO/ElementRankDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using MongoDB.Bson;

namespace WomPlatform.Web.Api.DTO;

public abstract class RankDTO {
public ObjectId Id { get; set; }
public string Name { get; set; }
public int Rank { get; set; }
}

public class SourceRankDTO : RankDTO {
public int TotalGeneratedAmount { get; set; }
public int TotalRedeemedAmount { get; set; }

}

public class MerchantRankDTO : RankDTO {
public int Amount { get; set; }
}

10 changes: 0 additions & 10 deletions src/ApiServer/ApiServer/DTO/MerchantRankDTO.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public class VoucherGenerationRedemptionStatsResponse {
public List<VoucherByAimDTO> VoucherByAim { get; set; }
public int VoucherAvailable { get; set; }
public List<TotalGeneratedAndRedeemedOverTimeDto> TotalGeneratedAndRedeemedOverTime { get; set; }
public List<SourceRankDTO> SourceRank { get; set; }
}
204 changes: 181 additions & 23 deletions src/ApiServer/ApiServer/Service/GenerationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,20 @@ public async Task<VoucherGenerationRedemptionStatsResponse> FetchTotalVouchersGe
double? longitude,
int? radius
) {
var (generatedVouchers, redeemedVouchers) = await FetchTotalVouchersGeneratedAndRedeemed(startDate, endDate, sourceId, aimListFilter);
List<VoucherByAimDTO> voucherByAim = await FetchTotalVouchersGeneratedByAim(startDate, endDate, sourceId, aimListFilter);
List<TotalGeneratedAndRedeemedOverTimeDto> totalGeneratedRedeemedVouchersOverTime = await GetTotalGeneratedRedeemedVouchersOverTime(startDate, endDate, sourceId, aimListFilter);
var (generatedVouchers, redeemedVouchers) =
await FetchTotalVouchersGeneratedAndRedeemed(startDate, endDate, sourceId, aimListFilter);
List<VoucherByAimDTO> voucherByAim =
await FetchTotalVouchersGeneratedByAim(startDate, endDate, sourceId, aimListFilter);
List<TotalGeneratedAndRedeemedOverTimeDto> totalGeneratedRedeemedVouchersOverTime =
await GetTotalGeneratedRedeemedVouchersOverTime(startDate, endDate, sourceId, aimListFilter);
List<SourceRankDTO> sourceRank = await GetSourceRank(startDate, endDate, sourceId, aimListFilter);

return new VoucherGenerationRedemptionStatsResponse {
TotalGenerated = generatedVouchers,
TotalRedeemed = redeemedVouchers,
VoucherByAim = voucherByAim,
TotalGeneratedAndRedeemedOverTime = totalGeneratedRedeemedVouchersOverTime
TotalGeneratedAndRedeemedOverTime = totalGeneratedRedeemedVouchersOverTime,
SourceRank = sourceRank,
};
}

Expand All @@ -301,7 +306,7 @@ string[] aimListFilter
new BsonDocument("$and",
new BsonArray(MongoQueryHelper.DateMatchCondition(startDate, endDate, "timestamp")))));

if (aimListFilter != null && aimListFilter.Any()) {
if(aimListFilter != null && aimListFilter.Any()) {
pipeline.Add(
new BsonDocument("$match",
new BsonDocument("aimCode",
Expand Down Expand Up @@ -395,7 +400,7 @@ string[] aimListFilter
pipeline.Add(new BsonDocument("$match", new BsonDocument("$and", new BsonArray(matchConditions))));
}

if (aimListFilter != null && aimListFilter.Any()) {
if(aimListFilter != null && aimListFilter.Any()) {
pipeline.Add(
new BsonDocument("$match",
new BsonDocument("aimCode",
Expand Down Expand Up @@ -494,44 +499,40 @@ public async Task<int> FetchVouchersAvailable(double? latitude, double? longitud
);
pipeline.Add(
new BsonDocument("$lookup",
new BsonDocument
{
new BsonDocument {
{ "from", "GenerationRequests" },
{ "localField", "generationRequestId" },
{ "foreignField", "_id" },
{ "as", "generationRequest" }
})
);
);

pipeline.Add(
new BsonDocument("$unwind",
new BsonDocument
{
new BsonDocument {
{ "path", "$generationRequest" },
{ "includeArrayIndex", "string" },
{ "preserveNullAndEmptyArrays", true }
})
);
);

pipeline.Add(
new BsonDocument("$match",
new BsonDocument("generationRequest.performedAt",
new BsonDocument
{
new BsonDocument {
{ "$exists", true },
{ "$ne", BsonNull.Value }
}))
);
);
pipeline.Add(
new BsonDocument("$project",
new BsonDocument
{
new BsonDocument {
{ "source", 1 },
{ "initialCount", 1 },
{ "count", 1 },
{ "generationRequest.performedAt", 1 }
})
);
);
pipeline.Add(
new BsonDocument("$group",
new BsonDocument {
Expand Down Expand Up @@ -569,10 +570,11 @@ string[] aimListFilter
var pipeline = new List<BsonDocument>();

// set calculation on last year if period of time is not specified
if (!startDate.HasValue && !endDate.HasValue) {
if(!startDate.HasValue && !endDate.HasValue) {
endDate = DateTime.Today; // Set to today
startDate = DateTime.Today.AddYears(-1); // One year ago
}

var formatDate = DateRangeHelper.GetDateFormatForRange(startDate.Value, endDate.Value);

startDate = startDate.Value.Date; // Truncate to midnight
Expand All @@ -590,7 +592,7 @@ string[] aimListFilter
}))
);

if (aimListFilter != null && aimListFilter.Any()) {
if(aimListFilter != null && aimListFilter.Any()) {
pipeline.Add(
new BsonDocument("$match",
new BsonDocument("aimCode",
Expand Down Expand Up @@ -670,11 +672,10 @@ string[] aimListFilter
// Get the list of all dates between startDate and endDate
var allDates = new List<string>();

var currentDate = startDate.Value.Date; // Start with the initial date
var currentDate = startDate.Value.Date; // Start with the initial date

// While currentDate is less than or equal to endDate
while (currentDate <= endDate.Value.Date)
{
while(currentDate <= endDate.Value.Date) {
allDates.Add(currentDate.ToString(netFormatDate));

// Increment the date using the appropriate logic based on netFormatDate
Expand Down Expand Up @@ -707,5 +708,162 @@ string[] aimListFilter

return vouchersByAim;
}

public async Task<List<SourceRankDTO>> GetSourceRank(DateTime? startDate, DateTime? endDate,
ObjectId? sourceId, string[] aimListFilter) {
var pipeline = new List<BsonDocument>();

// Create the list to hold match conditions for the voucher collection
List<BsonDocument> matchConditions =
MongoQueryHelper.DateMatchCondition(startDate, endDate, "timestamp");


// Add the date match conditions
if(matchConditions.Count > 0) {
pipeline.Add(new BsonDocument("$match", new BsonDocument("$and", new BsonArray(matchConditions))));
}

if(aimListFilter != null && aimListFilter.Any()) {
pipeline.Add(
new BsonDocument("$match",
new BsonDocument("aimCode",
new BsonDocument("$in",
new BsonArray(aimListFilter.Select(x => x.Trim())))))
);
}

pipeline.Add(new BsonDocument("$sort",
new BsonDocument("totalRedeemedAmount", -1)));

// If instrumentName is provided, add the lookup and match conditions
if(sourceId.HasValue) {
var sourceMatchConditions = MongoQueryHelper.SourceMatchFromVouchersCondition(sourceId);
pipeline.AddRange(sourceMatchConditions);
}



// $lookup: GenerationRequests
pipeline.Add(
new BsonDocument("$lookup",
new BsonDocument
{
{ "from", "GenerationRequests" },
{ "localField", "generationRequestId" },
{ "foreignField", "_id" },
{ "as", "gen" }
}
)
);

// $unwind: gen
pipeline.Add(
new BsonDocument("$unwind",
new BsonDocument
{
{ "path", "$gen" },
{ "includeArrayIndex", "string" },
{ "preserveNullAndEmptyArrays", false }
}
)
);

// $lookup: Sources
pipeline.Add(
new BsonDocument("$lookup",
new BsonDocument
{
{ "from", "Sources" },
{ "localField", "gen.sourceId" },
{ "foreignField", "_id" },
{ "as", "source" }
}
)
);

// $unwind: source
pipeline.Add(
new BsonDocument("$unwind",
new BsonDocument
{
{ "path", "$source" },
{ "includeArrayIndex", "string" },
{ "preserveNullAndEmptyArrays", false }
}
)
);

// $group: Aggregate totals
pipeline.Add(
new BsonDocument("$group",
new BsonDocument
{
{ "_id", "$source._id" },
{ "name", new BsonDocument("$first", "$source.name") },
{ "totalGeneratedAmount", new BsonDocument("$sum", "$initialCount") },
{ "totalRedeemedAmount",
new BsonDocument("$sum",
new BsonDocument("$cond",
new BsonDocument
{
{ "if",
new BsonDocument("$gt",
new BsonArray
{
"$gen.performedAt",
BsonNull.Value
}
)
},
{ "then", "$initialCount" },
{ "else", 0 }
}
)
)
}
}
)
);

// $sort: totalRedeemedAmount descending
pipeline.Add(
new BsonDocument("$setWindowFields",
new BsonDocument {
{
"sortBy",
new BsonDocument("totalGeneratedAmount", -1)
}, {
"output",
new BsonDocument("rank",
new BsonDocument("$denseRank",
new BsonDocument()))
}
}));

try {
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var result = await VoucherCollection.AggregateAsync<BsonDocument>(pipeline);
var sourceRankList = await result.ToListAsync();
stopwatch.Stop();
var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;

Logger.LogInformation($"Rank Aggregation pipeline executed in {elapsedMilliseconds} ms");

// Map to a strongly-typed model
var sourceRank = sourceRankList.Select(doc => new SourceRankDTO() {
Id = doc["_id"].IsBsonNull ? ObjectId.Empty : doc["_id"].AsObjectId,
Name = doc["name"].AsString,
TotalGeneratedAmount = doc["totalGeneratedAmount"].AsInt32,
TotalRedeemedAmount = doc["totalRedeemedAmount"].AsInt32,
Rank = doc["rank"].AsInt32
}).ToList();

return sourceRank;
}
catch(Exception ex) {
Logger.LogError($"An error occurred: {ex.Message}");
throw;
}
}
}
}
1 change: 1 addition & 0 deletions src/ApiServer/ApiServer/Utilities/CsvFileHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static byte[] GenerateCsvContent(VoucherGenerationRedemptionStatsResponse
csvData.AddRange(consumedResponse.MerchantRanks.Select(item => new { Category = $"Merchant Rank {item.Rank} ({item.Name})", Value = item.Amount }));
csvData.AddRange(consumedResponse.VoucherByAims.Select(item => new { Category = $"Voucher Consumed ({item.AimCode})", Value = item.Amount }));
csvData.AddRange(genRedResponse.VoucherByAim.Select(item => new { Category = $"Voucher Generated ({item.AimCode})", Value = item.Amount }));
csvData.AddRange(genRedResponse.SourceRank.Select(item => new { Category = $"Source Rank {item.Rank} ({item.Name})", Value = item.TotalGeneratedAmount, TotalRedeemedAmount = item.TotalRedeemedAmount }));

using (var memoryStream = new MemoryStream())
using (var writer = new StreamWriter(memoryStream, leaveOpen: true))
Expand Down

0 comments on commit 6d86685

Please sign in to comment.