diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/StatementBuilderTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/StatementBuilderTests.cs index c8e3636b..4294bdbc 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/StatementBuilderTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/StatementBuilderTests.cs @@ -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; @@ -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 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); @@ -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 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, "statement"); + // Act + Result htmlResult = await _builder.GetStatementHtml(merchantStatementAggregate, _merchant, _cancellationToken); + + htmlResult.IsFailed.ShouldBeTrue(); + } + [Fact] public async Task GetStatementHtml_ThrowsIfTemplateMissing() { // Arrange diff --git a/TransactionProcessor.BusinessLogic/Services/MerchantStatementDomainService.cs b/TransactionProcessor.BusinessLogic/Services/MerchantStatementDomainService.cs index 9edc2e7a..05a395c0 100644 --- a/TransactionProcessor.BusinessLogic/Services/MerchantStatementDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/MerchantStatementDomainService.cs @@ -264,10 +264,12 @@ public async Task 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 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) diff --git a/TransactionProcessor.BusinessLogic/Services/StatementBuilder.cs b/TransactionProcessor.BusinessLogic/Services/StatementBuilder.cs index c9887f9f..2e35a544 100644 --- a/TransactionProcessor.BusinessLogic/Services/StatementBuilder.cs +++ b/TransactionProcessor.BusinessLogic/Services/StatementBuilder.cs @@ -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; @@ -23,7 +24,7 @@ namespace TransactionProcessor.BusinessLogic.Services { public interface IStatementBuilder { - Task GetStatementHtml(MerchantStatementAggregate statementAggregate, + Task> GetStatementHtml(MerchantStatementAggregate statementAggregate, Merchant merchant, CancellationToken cancellationToken); } @@ -51,16 +52,22 @@ public StatementBuilder(IFileSystem fileSystem) #region Methods - public async Task GetStatementHtml(MerchantStatementAggregate statementAggregate, - Merchant merchant, - CancellationToken cancellationToken) + public async Task> 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 { @@ -74,9 +81,9 @@ public async Task 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) }; @@ -84,27 +91,23 @@ public async Task GetStatementHtml(MerchantStatementAggregate statementA 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()); } @@ -113,17 +116,19 @@ public async Task 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(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 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