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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Moq;
using Shouldly;
using SimpleResults;
using TransactionProcessor.Aggregates;
using TransactionProcessor.BusinessLogic.Services;
using TransactionProcessor.Models.Merchant;
Expand Down Expand Up @@ -48,8 +49,10 @@ public async Task GetStatementHtml_ReturnsHtmlWithReplacedTokens() {
// (Mocks already set up in constructor)

// Act
var html = await _builder.GetStatementHtml(merchantStatementAggregate, _merchant, _cancellationToken);
Result<String> htmlResult = await _builder.GetStatementHtml(merchantStatementAggregate, _merchant, _cancellationToken);

htmlResult.IsSuccess.ShouldBeTrue();
String html = htmlResult.Data;
// Assert
html.ShouldContain(this._merchant.MerchantName);
html.ShouldContain(this._merchant.Addresses.First().AddressLine1);
Expand All @@ -62,6 +65,39 @@ public async Task GetStatementHtml_ReturnsHtmlWithReplacedTokens() {
html.ShouldContain(".fa-solid{font-weight:bold;}");
}

[Fact]
public async Task GetStatementHtml_StatementNotGenerated_ErrorResult()
{
// Arrange
var merchantStatementAggregate = new MerchantStatementAggregate();
merchantStatementAggregate.RecordActivityDateOnStatement(TestData.MerchantStatementId, TestData.StatementDate, TestData.EstateId, TestData.MerchantId, TestData.MerchantStatementForDateId1, new DateTime(2025, 5, 1));
merchantStatementAggregate.RecordActivityDateOnStatement(TestData.MerchantStatementId, TestData.StatementDate, TestData.EstateId, TestData.MerchantId, TestData.MerchantStatementForDateId2, new DateTime(2025, 5, 2));
merchantStatementAggregate.AddDailySummaryRecord(new DateTime(2025, 5, 1), 100, 1000.00m, 100, 10.00m, 1, 1000, 1, 200);
merchantStatementAggregate.AddDailySummaryRecord(new DateTime(2025, 5, 2), 200, 2000.00m, 200, 20.00m, 2, 1000, 2, 200);

// Act
Result<String> htmlResult = await _builder.GetStatementHtml(merchantStatementAggregate, _merchant, _cancellationToken);

htmlResult.IsFailed.ShouldBeTrue();
}

[Fact]
public async Task GetStatementHtml_StatementAleadyBuilt_ErrorResult()
{
// Arrange
var merchantStatementAggregate = new MerchantStatementAggregate();
merchantStatementAggregate.RecordActivityDateOnStatement(TestData.MerchantStatementId, TestData.StatementDate, TestData.EstateId, TestData.MerchantId, TestData.MerchantStatementForDateId1, new DateTime(2025, 5, 1));
merchantStatementAggregate.RecordActivityDateOnStatement(TestData.MerchantStatementId, TestData.StatementDate, TestData.EstateId, TestData.MerchantId, TestData.MerchantStatementForDateId2, new DateTime(2025, 5, 2));
merchantStatementAggregate.AddDailySummaryRecord(new DateTime(2025, 5, 1), 100, 1000.00m, 100, 10.00m, 1, 1000, 1, 200);
merchantStatementAggregate.AddDailySummaryRecord(new DateTime(2025, 5, 2), 200, 2000.00m, 200, 20.00m, 2, 1000, 2, 200);
merchantStatementAggregate.GenerateStatement(TestData.GeneratedDateTime);
merchantStatementAggregate.BuildStatement(TestData.StatementBuiltDate, "<html>statement</html>");
// Act
Result<String> htmlResult = await _builder.GetStatementHtml(merchantStatementAggregate, _merchant, _cancellationToken);

htmlResult.IsFailed.ShouldBeTrue();
}

[Fact]
public async Task GetStatementHtml_ThrowsIfTemplateMissing() {
// Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,12 @@ public async Task<Result> BuildStatement(MerchantStatementCommands.BuildMerchant

Merchant merchantModel = getMerchantResult.Data.GetMerchant();

String html = await this.StatementBuilder.GetStatementHtml(merchantStatementAggregate, merchantModel, cancellationToken);
// TODO: Record the html to the statement aggregate so we can use it later if needed
Result<String> htmlResult = await this.StatementBuilder.GetStatementHtml(merchantStatementAggregate, merchantModel, cancellationToken);
if (htmlResult.IsFailed)
return ResultHelpers.CreateFailure(htmlResult);

String base64 = EncodeTo64(html);
// TODO: Record the html to the statement aggregate so we can use it later if needed
String base64 = EncodeTo64(htmlResult.Data);

Result stateResult = merchantStatementAggregate.BuildStatement(DateTime.Now, base64);
if (stateResult.IsFailed)
Expand Down
53 changes: 29 additions & 24 deletions TransactionProcessor.BusinessLogic/Services/StatementBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using SimpleResults;
using TransactionProcessor.Aggregates;
using TransactionProcessor.Database.Entities;
using TransactionProcessor.Models.Merchant;
Expand All @@ -23,7 +24,7 @@ namespace TransactionProcessor.BusinessLogic.Services
{
public interface IStatementBuilder
{
Task<String> GetStatementHtml(MerchantStatementAggregate statementAggregate,
Task<Result<String>> GetStatementHtml(MerchantStatementAggregate statementAggregate,
Merchant merchant,
CancellationToken cancellationToken);
}
Expand Down Expand Up @@ -51,16 +52,22 @@ public StatementBuilder(IFileSystem fileSystem)
#region Methods


public async Task<String> GetStatementHtml(MerchantStatementAggregate statementAggregate,
Merchant merchant,
CancellationToken cancellationToken)
public async Task<Result<String>> GetStatementHtml(MerchantStatementAggregate statementAggregate,
Merchant merchant,
CancellationToken cancellationToken)
{
// TODO: Check statement aggregate status
// TODO: Check merchant and addresses exist

IDirectoryInfo path = this.FileSystem.Directory.GetParent(Assembly.GetExecutingAssembly().Location);
MerchantStatement statementHeader = statementAggregate.GetStatement();

// This is only allowed if the statement has been generated but not yet built
if (statementHeader.IsBuilt)
return Result.Invalid("Statement has already been built");

if (statementHeader.IsGenerated == false)
return Result.Invalid("Statement has not yet been generated");

String mainHtml = await this.FileSystem.File.ReadAllTextAsync($"{path}/Templates/Email/statement.html", cancellationToken);

var anonymousHeader = new {
Expand All @@ -74,37 +81,33 @@ public async Task<String> GetStatementHtml(MerchantStatementAggregate statementA
MerchantPostcode = merchant.Addresses.First().PostalCode,
MerchantContactNumber = merchant.Contacts.First().ContactPhoneNumber,
StatementDate = statementHeader.StatementDate,

};
var statementLines = statementHeader.GetStatementLines();

List<(Int32 lineNumber, MerchantStatementLine statementLine)> statementLines = statementHeader.GetStatementLines();

var anonymousFooter = new { StatementTotal = statementLines.Sum(sl => sl.statementLine.Amount), TransactionsValue = statementLines.Where(sl => sl.statementLine.LineType == 1).Sum(sl => sl.statementLine.Amount), TransactionFeesValue = statementLines.Where(sl => sl.statementLine.LineType == 2).Sum(sl => sl.statementLine.Amount) };

// Statement header class first
PropertyInfo[] statementHeaderProperties = anonymousHeader.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

// Do the replaces for the transaction
foreach (PropertyInfo propertyInfo in statementHeaderProperties)
{
foreach (PropertyInfo propertyInfo in statementHeaderProperties) {
mainHtml = mainHtml.Replace($"[{propertyInfo.Name}]", propertyInfo.GetValue(anonymousHeader)?.ToString());
}

PropertyInfo[] statementFooterProperties = anonymousFooter.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

// Do the replaces for the transaction
foreach (PropertyInfo propertyInfo in statementFooterProperties)
{
foreach (PropertyInfo propertyInfo in statementFooterProperties) {
mainHtml = mainHtml.Replace($"[{propertyInfo.Name}]", propertyInfo.GetValue(anonymousFooter)?.ToString());
}

StringBuilder lines = new StringBuilder();
String lineHtml = await this.FileSystem.File.ReadAllTextAsync($"{path}/Templates/Email/statementline.html", cancellationToken);
foreach ((Int32 lineNumber, MerchantStatementLine statementLine) statementLineContainer in statementHeader.GetStatementLines())
{
var anonymousLine = new { StatementLineNumber = statementLineContainer.lineNumber+1, StatementLineDate = statementLineContainer.statementLine.DateTime.ToString("dd/MM/yyyy"), StatementLineDescription = statementLineContainer.statementLine.Description, StatementLineAmount = statementLineContainer.statementLine.Amount };
foreach ((Int32 lineNumber, MerchantStatementLine statementLine) statementLineContainer in statementHeader.GetStatementLines()) {
var anonymousLine = new { StatementLineNumber = statementLineContainer.lineNumber + 1, StatementLineDate = statementLineContainer.statementLine.DateTime.ToString("dd/MM/yyyy"), StatementLineDescription = statementLineContainer.statementLine.Description, StatementLineAmount = statementLineContainer.statementLine.Amount };
PropertyInfo[] statementLineProperties = anonymousLine.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo propertyInfo in statementLineProperties)
{
foreach (PropertyInfo propertyInfo in statementLineProperties) {
lineHtml = lineHtml.Replace($"[{propertyInfo.Name}]", propertyInfo.GetValue(anonymousLine)?.ToString());
}

Expand All @@ -113,17 +116,19 @@ public async Task<String> GetStatementHtml(MerchantStatementAggregate statementA

mainHtml = mainHtml.Replace("[StatementLinesData]", lines.ToString());

string basePath = Path.GetFullPath($"{AppContext.BaseDirectory}/Templates/Email/");
mainHtml = await this.AddCSSToHtml(mainHtml, "{bootstrapcss}", "bootstrap/css/bootstrap.min.css", cancellationToken);
mainHtml = await this.AddCSSToHtml(mainHtml, "{fontawesomemincss}", "fontawesome/css/fontawesome.min.css", cancellationToken);
mainHtml = await this.AddCSSToHtml(mainHtml, "{fontawesomesolidcss}", "fontawesome/css/solid.css", cancellationToken);

return Result.Success<String>(mainHtml);
}

var bootstrapmin = await this.FileSystem.File.ReadAllTextAsync($@"{basePath}bootstrap/css/bootstrap.min.css", cancellationToken);
var fontawesomemin = await this.FileSystem.File.ReadAllTextAsync($@"{basePath}fontawesome/css/fontawesome.min.css", cancellationToken);
var fontawesomesolid = await this.FileSystem.File.ReadAllTextAsync($@"{basePath}fontawesome/css/solid.css", cancellationToken);
private async Task<String> AddCSSToHtml(String html, String tag, String fileName, CancellationToken cancellationToken) {
String basePath = Path.GetFullPath($"{AppContext.BaseDirectory}/Templates/Email/");

mainHtml = mainHtml.Replace("{bootstrapcss}", bootstrapmin);
mainHtml = mainHtml.Replace("{fontawesomemincss}", fontawesomemin);
mainHtml = mainHtml.Replace("{fontawesomesolidcss}", fontawesomesolid);
String css = await this.FileSystem.File.ReadAllTextAsync($@"{basePath}{fileName}", cancellationToken);

return mainHtml;
return html.Replace(tag, css);
}

#endregion
Expand Down
Loading