Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating Art Show import methods & controller endpoints #23

Merged
merged 1 commit into from
Jun 22, 2022
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
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();
}
}
}