Skip to content

Commit

Permalink
Merge pull request #23 from eurofurence/artshow-messaging-refactor
Browse files Browse the repository at this point in the history
Updating Art Show import methods & controller endpoints
  • Loading branch information
Pinselohrkater authored Jun 22, 2022
2 parents 204fc97 + 6aeb4e4 commit 8a88e5c
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;

namespace Eurofurence.App.Server.Services.Abstractions.ArtShow
{
public class AgentClosingNotificationResult
{
public int AgentBadgeNo { get; set; }
public string AgentName { get; set; }
public string ArtistName { get; set; }
public decimal TotalCashAmount { get; set; }
public int ExhibitsSold { get; set; }
public int ExhibitsUnsold { get; set; }
public int ExhibitsToAuction { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ namespace Eurofurence.App.Server.Services.Abstractions.ArtShow
{
public interface IAgentClosingResultService
{
Task ImportAgentClosingResultLogAsync(TextReader logReader);
Task<ImportResult> ImportAgentClosingResultLogAsync(TextReader logReader);

Task ExecuteNotificationRunAsync();

Task<IList<AgentClosingNotificationResult>> SimulateNotificationRunAsync();

Task DeleteUnprocessedImportRowsAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ namespace Eurofurence.App.Server.Services.Abstractions.ArtShow
{
public interface IItemActivityService
{
Task ImportActivityLogAsync(TextReader logReader);
Task<ImportResult> ImportActivityLogAsync(TextReader logReader);

Task<IList<ItemActivityNotificationResult>> SimulateNotificationRunAsync();
Task ExecuteNotificationRunAsync();
Task DeleteUnprocessedImportRowsAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Eurofurence.App.Server.Services.Abstractions.ArtShow
{
public class ImportResult
{
public int RowsImported { get; set; }
public int RowsSkippedAsDuplicate { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,30 @@ IPrivateMessageService privateMessageService
_privateMessageService = privateMessageService;
}

public async Task ImportAgentClosingResultLogAsync(TextReader logReader)
public async Task<ImportResult> ImportAgentClosingResultLogAsync(TextReader logReader)
{
var importResult = new ImportResult();

try
{
await _semaphore.WaitAsync();

var csv = new CsvReader(logReader, CultureInfo.CurrentCulture);

csv.Configuration.RegisterClassMap<AgentClosingResultImportRowClassMap>();
csv.Configuration.Delimiter = ",";
csv.Configuration.HasHeaderRecord = false;

var csvRecords = csv.GetRecords<AgentClosingResultImportRow>().ToList();
var csvRecords = await csv.GetRecordsAsync<AgentClosingResultImportRow>().ToListAsync();

foreach (var csvRecord in csvRecords)
{
var existingRecord = await _agentClosingResultRepository.FindOneAsync(a => a.ImportHash == csvRecord.Hash.Value);
if (existingRecord != null) continue;
if (existingRecord != null)
{
importResult.RowsSkippedAsDuplicate++;
continue;
}

var newRecord = new AgentClosingResultRecord()
{
Expand All @@ -69,21 +75,28 @@ public async Task ImportAgentClosingResultLogAsync(TextReader logReader)
newRecord.Touch();

await _agentClosingResultRepository.InsertOneAsync(newRecord);
importResult.RowsImported++;
}
}
finally
{
_semaphore.Release();
}

return importResult;
}

private Task<IEnumerable<AgentClosingResultRecord>> GetUnprocessedImportRows()
=> _agentClosingResultRepository.FindAllAsync(a => a.NotificationDateTimeUtc == null);


public async Task ExecuteNotificationRunAsync()
{
try
{
await _semaphore.WaitAsync();

var newNotifications = await _agentClosingResultRepository.FindAllAsync(a => a.NotificationDateTimeUtc == null);
var newNotifications = await GetUnprocessedImportRows();

var tasks = newNotifications.Select(result => SendAgentClosingResultNotificationAsync(result));

Expand Down Expand Up @@ -123,5 +136,31 @@ private async Task SendAgentClosingResultNotificationAsync(AgentClosingResultRec

await _agentClosingResultRepository.ReplaceOneAsync(result);
}

public async Task<IList<AgentClosingNotificationResult>> SimulateNotificationRunAsync()
{
var newNotifications = await GetUnprocessedImportRows();

return newNotifications
.Select(item => new AgentClosingNotificationResult()
{
AgentBadgeNo = item.AgentBadgeNo,
AgentName = item.AgentName,
ArtistName = item.ArtistName,
ExhibitsSold = item.ExhibitsSold,
ExhibitsUnsold = item.ExhibitsUnsold,
ExhibitsToAuction = item.ExhibitsToAuction
})
.ToList();

}

public async Task DeleteUnprocessedImportRowsAsync()
{
var recordsToDelete = await GetUnprocessedImportRows();

foreach (var record in recordsToDelete)
await _agentClosingResultRepository.DeleteOneAsync(record.Id);
}
}
}
24 changes: 21 additions & 3 deletions src/Eurofurence.App.Server.Services/ArtShow/ItemActivityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ IPrivateMessageService privateMessageService
_privateMessageService = privateMessageService;
}

public async Task ImportActivityLogAsync(TextReader logReader)
public async Task<ImportResult> ImportActivityLogAsync(TextReader logReader)
{
var importResult = new ImportResult();
var csv = new CsvReader(logReader, CultureInfo.CurrentCulture);

csv.Configuration.RegisterClassMap<LogImportRowClassMap>();
Expand All @@ -46,7 +47,11 @@ public async Task ImportActivityLogAsync(TextReader logReader)
foreach (var csvRecord in csvRecords)
{
var existingRecord = await _itemActivityRepository.FindOneAsync(a => a.ImportHash == csvRecord.Hash.Value);
if (existingRecord != null) continue;
if (existingRecord != null)
{
importResult.RowsSkippedAsDuplicate++;
continue;
}

var newRecord = new ItemActivityRecord()
{
Expand All @@ -63,9 +68,14 @@ public async Task ImportActivityLogAsync(TextReader logReader)
newRecord.Touch();

await _itemActivityRepository.InsertOneAsync(newRecord);
importResult.RowsImported++;
}

return importResult;
}

private Task<IEnumerable<ItemActivityRecord>> GetUnprocessedImportRows()
=> _itemActivityRepository.FindAllAsync(a => a.NotificationDateTimeUtc == null);

private class NotificationBundle
{
Expand All @@ -76,7 +86,7 @@ private class NotificationBundle

private async Task<IList<NotificationBundle>> BuildNotificationBundlesAsync()
{
var newActivities = await _itemActivityRepository.FindAllAsync(a => a.NotificationDateTimeUtc == null);
var newActivities = await GetUnprocessedImportRows();

return newActivities
.GroupBy(a => a.OwnerUid)
Expand Down Expand Up @@ -192,5 +202,13 @@ public async Task<IList<ItemActivityNotificationResult>> SimulateNotificationRun
})
.ToList();
}

public async Task DeleteUnprocessedImportRowsAsync()
{
var recordsToDelete = await GetUnprocessedImportRows();

foreach (var record in recordsToDelete)
await _itemActivityRepository.DeleteOneAsync(record.Id);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.2" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0005" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Telegram.Bot" Version="15.7.1" />
</ItemGroup>
<ItemGroup>
Expand Down
119 changes: 105 additions & 14 deletions src/Eurofurence.App.Server.Web/Controllers/ArtShowController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,50 +27,141 @@ IApiPrincipal apiPrincipal
_apiPrincipal = apiPrincipal;
}

[Authorize(Roles = "System,Developer")]
/// <summary>
/// Import Art Show results for bidders (CSV)
/// </summary>
/// <remarks>
/// Imports the Art Show result CSV for bidders (`RegNo, ASIDNO, ArtistName, ArtPieceTitle, Status ["Sold", Auction"]`).<br /><br />
/// There's a row-hash built from the mentioned import fields. If a row with the same hash is already present, it will be skipped.
/// (Importing the same data multiple times does not lead to duplicates.)
/// </remarks>
[Authorize(Roles = "System,Developer,ArtShow")]
[HttpPost("ItemActivites/Log")]
[ProducesResponseType(201)]
[BinaryPayload(Description = "Art show log contents")]
public async Task<ActionResult> ImportActivityLogAsync()
public Task<ImportResult> ImportActivityLogAsync()
{
await _itemActivityService.ImportActivityLogAsync(new StreamReader(Request.Body));
return NoContent();
return _itemActivityService.ImportActivityLogAsync(new StreamReader(Request.Body));
}

[Authorize(Roles = "System,Developer")]
/// <summary>
/// Simulate notification bundle generation for bidders.
/// </summary>
/// <remarks>
/// Simulates building notification bundles that group all item results (sold, to auction) by the bidder who holds the winning/highest bid. <br /><br />
/// The simulation data shows individual notifications for every bidder (`RecipientUid`), listing the items they won (`IdsSold`)
/// as well as items going into auction (`IdsToAuction`) with their current bid on top. <br /><br />
/// <b>Calling this endpoint does not modify any data and does not send any messages.</b>
/// </remarks>
[Authorize(Roles = "System,Developer,ArtShow")]
[HttpGet("ItemActivites/NotificationBundles/Simulation")]
[ProducesResponseType(200)]
public Task<IList<ItemActivityNotificationResult>> SimulateNotificationRunAsync()
public Task<IList<ItemActivityNotificationResult>> SimulateItemActivitiesNotificationRunAsync()
{
return _itemActivityService.SimulateNotificationRunAsync();
}

[Authorize(Roles = "System,Developer")]

/// <summary>
/// Execute/Send notification bundles for bidders.
/// </summary>
/// <remarks>
/// Builds notification bundles that group all item results (sold, to auction) by the bidder who holds the winning/highest bid. <br /><br />
/// <b><u>Calling this endpoint will produce and queue all notifications/messages for delivery. Notifications are sent asynchronously and in queue,
/// so full delivery may take a few minutes.</u></b>
/// </remarks>
[Authorize(Roles = "System,Developer,ArtShow")]
[HttpPost("ItemActivites/NotificationBundles/Send")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
public async Task<ActionResult> ExecuteNotificationRunAsync()
{
await _itemActivityService.ExecuteNotificationRunAsync();
return NoContent();
}

[Authorize(Roles = "System,Developer")]

/// <summary>
/// Delete unprocessed rows
/// </summary>
/// <remarks>
/// Deletes any imported data for which no notification has been sent yet.<br /><br />
/// <b>Use this to remove imported data if the simulation does not check out.</b>
/// </remarks>
[Authorize(Roles = "System,Developer,ArtShow")]
[HttpDelete("ItemActivites/NotificationBundles/:unprocessed")]
[ProducesResponseType(204)]
public async Task<ActionResult> DeleteUnprocessedItemActivitiesImportRowsAsync()
{
await _itemActivityService.DeleteUnprocessedImportRowsAsync();
return NoContent();
}


/// <summary>
/// Import Art Show results for agents (CSV)
/// </summary>
/// <remarks>
/// Imports the Art Show result CSV for agents (`AgentBadgeNo, AgentName, ArtistName, TotalCashAmount, ExhibitsSold, ExhibitsUnsold, ExhibitsToAuction`).<br /><br />
/// There's a row-hash built from the mentioned import fields. If a row with the same hash is already present, it will be skipped.
/// (Importing the same data multiple times does not lead to duplicates.)
/// </remarks>
[Authorize(Roles = "System,Developer,ArtShow")]
[HttpPost("AgentClosingResults/Log")]
[ProducesResponseType(201)]
[BinaryPayload(Description = "Agent closing result log contents")]
public async Task<ActionResult> ImportAgentClosingResultLogAsync()
public Task<ImportResult> ImportAgentClosingResultLogAsync()
{
await _agentClosingResultService.ImportAgentClosingResultLogAsync(new StreamReader(Request.Body));
return NoContent();
return _agentClosingResultService.ImportAgentClosingResultLogAsync(new StreamReader(Request.Body));
}

[Authorize(Roles = "System,Developer")]
[HttpPost("AgentClosingResults/NotificationBundles/Send")]
/// <summary>
/// Simulate notification bundle generation for agents.
/// </summary>
/// <remarks>
/// Simulates building notification going towards agents. <br /><br />
/// The simulation data shows individual notification data for every agent (`AgentBadgeNo`).<br /><br />
/// <b>Calling this endpoint does not modify any data and does not send any messages.</b>
/// </remarks>
[Authorize(Roles = "System,Developer,ArtShow")]
[HttpGet("AgentClosingResults/NotificationBundles/Simulation")]
[ProducesResponseType(200)]
public Task<IList<AgentClosingNotificationResult>> SimulateAgentClosingResultsNotificationRunAsync()
{
return _agentClosingResultService.SimulateNotificationRunAsync();
}


/// <summary>
/// Execute/Send notification bundles for agents.
/// </summary>
/// <remarks>
/// Builds notifications for all agents.<br /><br />
/// <b><u>Calling this endpoint will produce and queue all notifications/messages for delivery. Notifications are sent asynchronously and in queue,
/// so full delivery may take a few minutes.</u></b>
/// </remarks>
[Authorize(Roles = "System,Developer,ArtShow")]
[HttpPost("AgentClosingResults/NotificationBundles/Send")]
[ProducesResponseType(204)]
public async Task<ActionResult> ExecuteAgentNotificationRunAsync()
{
await _agentClosingResultService.ExecuteNotificationRunAsync();
return NoContent();
}

/// <summary>
/// Delete unprocessed rows
/// </summary>
/// <remarks>
/// Deletes any imported data for which no notification has been sent yet.<br /><br />
/// <b>Use this to remove imported data if the simulation does not check out.</b>
/// </remarks>
[Authorize(Roles = "System,Developer,ArtShow")]
[HttpDelete("AgentClosingResults/NotificationBundles/:unprocessed")]
[ProducesResponseType(204)]
public async Task<ActionResult> DeleteUnprocessedAgentClosingResultsImportRowsAsync()
{
await _agentClosingResultService.DeleteUnprocessedImportRowsAsync();
return NoContent();
}
}
}

0 comments on commit 8a88e5c

Please sign in to comment.