From 74f0689113b40d43b210fc3dca9610be90e9b450 Mon Sep 17 00:00:00 2001 From: "Jefferson L. da Silva" Date: Wed, 13 May 2026 20:27:50 -0300 Subject: [PATCH 1/2] refactor: Introduce factory pattern for barcode invoice handlers Refactors `BarcodeReaderService` to use a `BarcodeHandlerFactory` for resolving `IInvoiceBarcodeHandler` implementations based on `InvoiceType`. This change: * Replaces the manual `switch` statement for handler selection within `BarcodeReaderService` with a dedicated factory. * Leverages dependency injection to register and provide all `IInvoiceBarcodeHandler` implementations to the factory. * Enhances extensibility, allowing new invoice types and their corresponding handlers to be added without modifying the `BarcodeReaderService` directly. * Updates the DI configuration to `Append` registrations for barcode handlers, ensuring all implementations are available to the factory. --- .../DependencyInjectionConfig.cs | 2 +- .../AccountInvoiceBarcodeHandler.cs | 3 + .../BankInvoiceBarcodeHandler.cs | 3 + .../BarcodeReader/BarcodeHandlerFactory.cs | 20 ++++ .../BarcodeReader/BarcodeReaderService.cs | 20 +--- .../BarcodeReader/IBarcodeHandlerFactory.cs | 8 ++ .../BarcodeReader/IInvoiceBarcodeHandler.cs | 3 + .../BarcodeHandlerFactoryTests.cs | 92 +++++++++++++++++++ .../BarcodeReaderServiceTests.cs | 10 +- 9 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 InvoiceReminder.ExternalServices/BarcodeReader/BarcodeHandlerFactory.cs create mode 100644 InvoiceReminder.ExternalServices/BarcodeReader/IBarcodeHandlerFactory.cs create mode 100644 InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeHandlerFactoryTests.cs diff --git a/InvoiceReminder.CrossCutting.IoC/DependencyInjectionConfig.cs b/InvoiceReminder.CrossCutting.IoC/DependencyInjectionConfig.cs index 597515e..2d6582c 100644 --- a/InvoiceReminder.CrossCutting.IoC/DependencyInjectionConfig.cs +++ b/InvoiceReminder.CrossCutting.IoC/DependencyInjectionConfig.cs @@ -92,7 +92,7 @@ private static IServiceCollection AddExternalServices(this IServiceCollection se _ = services.Scan(scan => scan.FromAssembliesOf(typeof(IGmailServiceWrapper)) .AddClasses(classes => classes.InNamespaces(namespaces)) - .UsingRegistrationStrategy(RegistrationStrategy.Skip) + .UsingRegistrationStrategy(RegistrationStrategy.Append) .AsImplementedInterfaces() .WithScopedLifetime() ); diff --git a/InvoiceReminder.ExternalServices/BarcodeReader/AccountInvoiceBarcodeHandler.cs b/InvoiceReminder.ExternalServices/BarcodeReader/AccountInvoiceBarcodeHandler.cs index 0889d54..1679d1a 100644 --- a/InvoiceReminder.ExternalServices/BarcodeReader/AccountInvoiceBarcodeHandler.cs +++ b/InvoiceReminder.ExternalServices/BarcodeReader/AccountInvoiceBarcodeHandler.cs @@ -1,10 +1,13 @@ using InvoiceReminder.Domain.Entities; +using InvoiceReminder.Domain.Enums; using System.Text.RegularExpressions; namespace InvoiceReminder.ExternalServices.BarcodeReader; public class AccountInvoiceBarcodeHandler : IInvoiceBarcodeHandler { + public InvoiceType InvoiceType => InvoiceType.AccountInvoice; + public Invoice CreateInvoice(string content, string beneficiary) { ArgumentException.ThrowIfNullOrWhiteSpace(content); diff --git a/InvoiceReminder.ExternalServices/BarcodeReader/BankInvoiceBarcodeHandler.cs b/InvoiceReminder.ExternalServices/BarcodeReader/BankInvoiceBarcodeHandler.cs index 7a08993..c6c94b5 100644 --- a/InvoiceReminder.ExternalServices/BarcodeReader/BankInvoiceBarcodeHandler.cs +++ b/InvoiceReminder.ExternalServices/BarcodeReader/BankInvoiceBarcodeHandler.cs @@ -1,4 +1,5 @@ using InvoiceReminder.Domain.Entities; +using InvoiceReminder.Domain.Enums; using System.Globalization; using System.Text.RegularExpressions; @@ -8,6 +9,8 @@ public class BankInvoiceBarcodeHandler : IInvoiceBarcodeHandler { private readonly Dictionary knowBanks; + public InvoiceType InvoiceType => InvoiceType.BankInvoice; + public BankInvoiceBarcodeHandler() { knowBanks = new() diff --git a/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeHandlerFactory.cs b/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeHandlerFactory.cs new file mode 100644 index 0000000..33c6815 --- /dev/null +++ b/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeHandlerFactory.cs @@ -0,0 +1,20 @@ +using InvoiceReminder.Domain.Enums; + +namespace InvoiceReminder.ExternalServices.BarcodeReader; + +public class BarcodeHandlerFactory : IBarcodeHandlerFactory +{ + private readonly Dictionary _handlers; + + public BarcodeHandlerFactory(IEnumerable handlers) + { + _handlers = handlers.ToDictionary(h => h.InvoiceType, h => h); + } + + public IInvoiceBarcodeHandler GetHandler(InvoiceType invoiceType) + { + return _handlers.TryGetValue(invoiceType, out var handler) + ? handler + : throw new NotSupportedException($"No barcode handler found for invoice type: {invoiceType}"); + } +} diff --git a/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeReaderService.cs b/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeReaderService.cs index 46307c1..ff18212 100644 --- a/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeReaderService.cs +++ b/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeReaderService.cs @@ -11,11 +11,12 @@ namespace InvoiceReminder.ExternalServices.BarcodeReader; public class BarcodeReaderService : IBarcodeReaderService { private readonly ILogger _logger; - private IInvoiceBarcodeHandler _barcodeHandler; + private readonly IBarcodeHandlerFactory _factory; - public BarcodeReaderService(ILogger logger) + public BarcodeReaderService(ILogger logger, IBarcodeHandlerFactory factory) { _logger = logger; + _factory = factory; } public Invoice ReadTextContentFromPdf(byte[] byteStream, string beneficiary, string password, InvoiceType invoiceType) @@ -45,6 +46,7 @@ public Invoice ReadTextContentFromPdf(byte[] byteStream, string beneficiary, str StringBuilder content = new(); var numberOfPages = pdfDoc.GetNumberOfPages(); + var barcodeHandler = _factory.GetHandler(invoiceType); for (var page = 1; page <= numberOfPages; page++) { @@ -57,18 +59,6 @@ public Invoice ReadTextContentFromPdf(byte[] byteStream, string beneficiary, str _ = content.Append(currentText).Replace(" \n", "\n").Replace(" \r\n", "\r\n"); } - SetBarcodeHandler(invoiceType); - - return _barcodeHandler.CreateInvoice(content.ToString(), beneficiary); - } - - private void SetBarcodeHandler(InvoiceType invoiceType) - { - _barcodeHandler = invoiceType switch - { - InvoiceType.BankInvoice => new BankInvoiceBarcodeHandler(), - InvoiceType.AccountInvoice => new AccountInvoiceBarcodeHandler(), - _ => default - }; + return barcodeHandler.CreateInvoice(content.ToString(), beneficiary); } } diff --git a/InvoiceReminder.ExternalServices/BarcodeReader/IBarcodeHandlerFactory.cs b/InvoiceReminder.ExternalServices/BarcodeReader/IBarcodeHandlerFactory.cs new file mode 100644 index 0000000..17dc209 --- /dev/null +++ b/InvoiceReminder.ExternalServices/BarcodeReader/IBarcodeHandlerFactory.cs @@ -0,0 +1,8 @@ +using InvoiceReminder.Domain.Enums; + +namespace InvoiceReminder.ExternalServices.BarcodeReader; + +public interface IBarcodeHandlerFactory +{ + IInvoiceBarcodeHandler GetHandler(InvoiceType invoiceType); +} diff --git a/InvoiceReminder.ExternalServices/BarcodeReader/IInvoiceBarcodeHandler.cs b/InvoiceReminder.ExternalServices/BarcodeReader/IInvoiceBarcodeHandler.cs index 38d08c3..aba12e8 100644 --- a/InvoiceReminder.ExternalServices/BarcodeReader/IInvoiceBarcodeHandler.cs +++ b/InvoiceReminder.ExternalServices/BarcodeReader/IInvoiceBarcodeHandler.cs @@ -1,8 +1,11 @@ using InvoiceReminder.Domain.Entities; +using InvoiceReminder.Domain.Enums; namespace InvoiceReminder.ExternalServices.BarcodeReader; public interface IInvoiceBarcodeHandler { + InvoiceType InvoiceType { get; } + Invoice CreateInvoice(string content, string beneficiary); } diff --git a/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeHandlerFactoryTests.cs b/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeHandlerFactoryTests.cs new file mode 100644 index 0000000..31da478 --- /dev/null +++ b/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeHandlerFactoryTests.cs @@ -0,0 +1,92 @@ +using InvoiceReminder.Domain.Enums; +using InvoiceReminder.ExternalServices.BarcodeReader; +using NSubstitute; +using Shouldly; + +namespace InvoiceReminder.UnitTests.ExternalServices.BarcodeReader; + +[TestClass] +public sealed class BarcodeHandlerFactoryTests +{ + private readonly IInvoiceBarcodeHandler _accountInvoiceHandler; + private readonly IInvoiceBarcodeHandler _bankInvoiceHandler; + private BarcodeHandlerFactory _barcodeHandlerFactory; + + public BarcodeHandlerFactoryTests() + { + _accountInvoiceHandler = Substitute.For(); + _bankInvoiceHandler = Substitute.For(); + + _ = _accountInvoiceHandler.InvoiceType.Returns(InvoiceType.AccountInvoice); + _ = _bankInvoiceHandler.InvoiceType.Returns(InvoiceType.BankInvoice); + } + + [TestMethod] + public void Constructor_ShouldRegisterHandlers_WhenMultipleHandlersProvided() + { + // Arrange + var handlers = new List { _accountInvoiceHandler, _bankInvoiceHandler }; + + // Act + _barcodeHandlerFactory = new BarcodeHandlerFactory(handlers); + + // Assert + _ = _barcodeHandlerFactory.ShouldNotBeNull(); + } + + [TestMethod] + public void GetHandler_ShouldReturnAccountInvoiceHandler_WhenAccountInvoiceTypeRequested() + { + // Arrange + var handlers = new List { _accountInvoiceHandler, _bankInvoiceHandler }; + _barcodeHandlerFactory = new BarcodeHandlerFactory(handlers); + + // Act + var handler = _barcodeHandlerFactory.GetHandler(InvoiceType.AccountInvoice); + + // Assert + handler.ShouldBeSameAs(_accountInvoiceHandler); + } + + [TestMethod] + public void GetHandler_ShouldReturnBankInvoiceHandler_WhenBankInvoiceTypeRequested() + { + // Arrange + var handlers = new List { _accountInvoiceHandler, _bankInvoiceHandler }; + _barcodeHandlerFactory = new BarcodeHandlerFactory(handlers); + + // Act + var handler = _barcodeHandlerFactory.GetHandler(InvoiceType.BankInvoice); + + // Assert + handler.ShouldBeSameAs(_bankInvoiceHandler); + } + + [TestMethod] + public void GetHandler_ShouldThrowNotSupportedException_WhenInvoiceTypeNotFound() + { + // Arrange + var handlers = new List { _accountInvoiceHandler }; + _barcodeHandlerFactory = new BarcodeHandlerFactory(handlers); + + // Act && Assert + var exception = Should.Throw(() => + _barcodeHandlerFactory.GetHandler(InvoiceType.BankInvoice)); + + exception.Message.ShouldContain("No barcode handler found for invoice type:"); + } + + [TestMethod] + public void GetHandler_ShouldThrowNotSupportedException_WhenNoHandlersProvided() + { + // Arrange + var handlers = new List(); + _barcodeHandlerFactory = new BarcodeHandlerFactory(handlers); + + // Act && Assert + var exception = Should.Throw(() => + _barcodeHandlerFactory.GetHandler(InvoiceType.AccountInvoice)); + + exception.Message.ShouldContain("No barcode handler found for invoice type:"); + } +} diff --git a/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeReaderServiceTests.cs b/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeReaderServiceTests.cs index 77fbb6b..cf66491 100644 --- a/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeReaderServiceTests.cs +++ b/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeReaderServiceTests.cs @@ -16,6 +16,7 @@ public sealed class BarcodeReaderServiceTests { private readonly ILogger _logger; private readonly IInvoiceBarcodeHandler _barcodeHandler; + private readonly IBarcodeHandlerFactory _barcodeHandlerFactory; private readonly BarcodeReaderService _barcodeReaderService; private readonly Faker _invoiceFaker; private readonly Faker _faker; @@ -26,7 +27,8 @@ public BarcodeReaderServiceTests() { _logger = Substitute.For>(); _barcodeHandler = Substitute.For(); - _barcodeReaderService = new BarcodeReaderService(_logger); + _barcodeHandlerFactory = Substitute.For(); + _barcodeReaderService = new BarcodeReaderService(_logger, _barcodeHandlerFactory); _faker = new Faker(); _invoiceFaker = new Faker() @@ -83,6 +85,9 @@ public void ReadTextContentFromPdf_ShouldCreateValidAccountInvoice_WhenPdfContai var expectedInvoice = _invoiceFaker.Generate(); var password = string.Empty; + _ = _barcodeHandlerFactory.GetHandler(InvoiceType.AccountInvoice) + .Returns(_barcodeHandler); + _ = _barcodeHandler.CreateInvoice(Arg.Any(), Arg.Any()) .Returns(expectedInvoice); @@ -111,6 +116,9 @@ public void ReadTextContentFromPdf_ShouldCreateValidBankInvoice_WhenPdfContainsB var expectedInvoice = _invoiceFaker.Generate(); var password = string.Empty; + _ = _barcodeHandlerFactory.GetHandler(InvoiceType.BankInvoice) + .Returns(_barcodeHandler); + _ = _barcodeHandler.CreateInvoice(Arg.Any(), Arg.Any()) .Returns(expectedInvoice); From 9dbaf8336b45eb6097f280c2e10fa335a74edd85 Mon Sep 17 00:00:00 2001 From: "Jefferson L. da Silva" Date: Wed, 13 May 2026 20:59:37 -0300 Subject: [PATCH 2/2] refactor: Validate uniqueness of barcode handlers by invoice type Adds a check during `BarcodeHandlerFactory` initialization to ensure that no two `IInvoiceBarcodeHandler` implementations are registered for the same `InvoiceType`. This prevents potential runtime errors caused by ambiguous handler resolution and guarantees a clear one-to-one mapping between invoice types and their dedicated handlers. --- .../BarcodeReader/BarcodeHandlerFactory.cs | 12 ++++++++++ .../BarcodeHandlerFactoryTests.cs | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeHandlerFactory.cs b/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeHandlerFactory.cs index 33c6815..267d66f 100644 --- a/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeHandlerFactory.cs +++ b/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeHandlerFactory.cs @@ -8,6 +8,18 @@ public class BarcodeHandlerFactory : IBarcodeHandlerFactory public BarcodeHandlerFactory(IEnumerable handlers) { + var duplicatedTypes = handlers + .GroupBy(h => h.InvoiceType) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToArray(); + + if (duplicatedTypes.Length > 0) + { + throw new InvalidOperationException( + $"Duplicate barcode handlers registered for: {string.Join(", ", duplicatedTypes)}"); + } + _handlers = handlers.ToDictionary(h => h.InvoiceType, h => h); } diff --git a/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeHandlerFactoryTests.cs b/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeHandlerFactoryTests.cs index 31da478..3487975 100644 --- a/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeHandlerFactoryTests.cs +++ b/InvoiceReminder.UnitTests.ExternalServices/BarcodeReader/BarcodeHandlerFactoryTests.cs @@ -89,4 +89,26 @@ public void GetHandler_ShouldThrowNotSupportedException_WhenNoHandlersProvided() exception.Message.ShouldContain("No barcode handler found for invoice type:"); } + + [TestMethod] + public void Constructor_ShouldThrowInvalidOperationException_WhenDuplicateHandlersProvidedForSameType() + { + // Arrange + var duplicateAccountHandler = Substitute.For(); + _ = duplicateAccountHandler.InvoiceType.Returns(InvoiceType.AccountInvoice); + + var handlers = new List + { + _accountInvoiceHandler, + duplicateAccountHandler, + _bankInvoiceHandler + }; + + // Act && Assert + var exception = Should.Throw(() => + new BarcodeHandlerFactory(handlers)); + + exception.Message.ShouldContain("Duplicate barcode handlers registered for:"); + exception.Message.ShouldContain(InvoiceType.AccountInvoice.ToString()); + } }