From 2ca939dcba68c6c080f5424d7d004b9d94cfb44c Mon Sep 17 00:00:00 2001 From: iamafzalsafdar Date: Tue, 12 Aug 2025 19:31:53 +0500 Subject: [PATCH] refactor payment method --- .../RefactorThis.Domain.csproj.dtbcache.json | 1 + RefactorThis.Domain/InvoiceExtensions.cs | 14 + RefactorThis.Domain/InvoiceService.cs | 241 +++++++----------- RefactorThis.Domain/PaymentValidator.cs | 89 +++++++ .../RefactorThis.Domain.csproj | 104 ++++---- RefactorThis.Domain/TaxCalculator.cs | 29 +++ 6 files changed, 285 insertions(+), 193 deletions(-) create mode 100644 RefactorThis.Domain/.vs/RefactorThis.Domain.csproj.dtbcache.json create mode 100644 RefactorThis.Domain/InvoiceExtensions.cs create mode 100644 RefactorThis.Domain/PaymentValidator.cs create mode 100644 RefactorThis.Domain/TaxCalculator.cs diff --git a/RefactorThis.Domain/.vs/RefactorThis.Domain.csproj.dtbcache.json b/RefactorThis.Domain/.vs/RefactorThis.Domain.csproj.dtbcache.json new file mode 100644 index 0000000..1fd34f0 --- /dev/null +++ b/RefactorThis.Domain/.vs/RefactorThis.Domain.csproj.dtbcache.json @@ -0,0 +1 @@ +{"RootPath":"C:\\Users\\User\\Source\\Repos\\refactorthis\\RefactorThis.Domain","ProjectFileName":"RefactorThis.Domain.csproj","Configuration":"Debug|AnyCPU","FrameworkPath":"","Sources":[{"SourceFile":"InvoiceService.cs"},{"SourceFile":"Properties\\AssemblyInfo.cs"},{"SourceFile":"obj\\Debug\\.NETFramework,Version=v4.7.2.AssemblyAttributes.cs"}],"References":[{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2\\mscorlib.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Users\\User\\Source\\Repos\\refactorthis\\RefactorThis.Persistence\\bin\\Debug\\net472\\RefactorThis.Persistence.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":true,"ProjectPath":"C:\\Users\\User\\Source\\Repos\\refactorthis\\RefactorThis.Persistence\\bin\\Debug\\net472\\RefactorThis.Persistence.dll"},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2\\System.Core.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2\\System.Data.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2\\System.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2\\System.Xml.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""}],"Analyzers":[],"Outputs":[{"OutputItemFullPath":"C:\\Users\\User\\Source\\Repos\\refactorthis\\RefactorThis.Domain\\bin\\Debug\\RefactorThis.Domain.dll","OutputItemRelativePath":"RefactorThis.Domain.dll"},{"OutputItemFullPath":"","OutputItemRelativePath":""}],"CopyToOutputEntries":[]} \ No newline at end of file diff --git a/RefactorThis.Domain/InvoiceExtensions.cs b/RefactorThis.Domain/InvoiceExtensions.cs new file mode 100644 index 0000000..19f198a --- /dev/null +++ b/RefactorThis.Domain/InvoiceExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using RefactorThis.Persistence; + +namespace RefactorThis.Domain +{ + public static class InvoiceExtensions + { + public static void EnsurePaymentsCollection(this Invoice invoice) + { + if (invoice.Payments == null) + invoice.Payments = new List(); + } + } +} \ No newline at end of file diff --git a/RefactorThis.Domain/InvoiceService.cs b/RefactorThis.Domain/InvoiceService.cs index fbd674c..c75f882 100644 --- a/RefactorThis.Domain/InvoiceService.cs +++ b/RefactorThis.Domain/InvoiceService.cs @@ -4,146 +4,103 @@ namespace RefactorThis.Domain { - public class InvoiceService - { - private readonly InvoiceRepository _invoiceRepository; - - public InvoiceService( InvoiceRepository invoiceRepository ) - { - _invoiceRepository = invoiceRepository; - } - - public string ProcessPayment( Payment payment ) - { - var inv = _invoiceRepository.GetInvoice( payment.Reference ); - - var responseMessage = string.Empty; - - if ( inv == null ) - { - throw new InvalidOperationException( "There is no invoice matching this payment" ); - } - else - { - if ( inv.Amount == 0 ) - { - if ( inv.Payments == null || !inv.Payments.Any( ) ) - { - responseMessage = "no payment needed"; - } - else - { - throw new InvalidOperationException( "The invoice is in an invalid state, it has an amount of 0 and it has payments." ); - } - } - else - { - if ( inv.Payments != null && inv.Payments.Any( ) ) - { - if ( inv.Payments.Sum( x => x.Amount ) != 0 && inv.Amount == inv.Payments.Sum( x => x.Amount ) ) - { - responseMessage = "invoice was already fully paid"; - } - else if ( inv.Payments.Sum( x => x.Amount ) != 0 && payment.Amount > ( inv.Amount - inv.AmountPaid ) ) - { - responseMessage = "the payment is greater than the partial amount remaining"; - } - else - { - if ( ( inv.Amount - inv.AmountPaid ) == payment.Amount ) - { - switch ( inv.Type ) - { - case InvoiceType.Standard: - inv.AmountPaid += payment.Amount; - inv.Payments.Add( payment ); - responseMessage = "final partial payment received, invoice is now fully paid"; - break; - case InvoiceType.Commercial: - inv.AmountPaid += payment.Amount; - inv.TaxAmount += payment.Amount * 0.14m; - inv.Payments.Add( payment ); - responseMessage = "final partial payment received, invoice is now fully paid"; - break; - default: - throw new ArgumentOutOfRangeException( ); - } - - } - else - { - switch ( inv.Type ) - { - case InvoiceType.Standard: - inv.AmountPaid += payment.Amount; - inv.Payments.Add( payment ); - responseMessage = "another partial payment received, still not fully paid"; - break; - case InvoiceType.Commercial: - inv.AmountPaid += payment.Amount; - inv.TaxAmount += payment.Amount * 0.14m; - inv.Payments.Add( payment ); - responseMessage = "another partial payment received, still not fully paid"; - break; - default: - throw new ArgumentOutOfRangeException( ); - } - } - } - } - else - { - if ( payment.Amount > inv.Amount ) - { - responseMessage = "the payment is greater than the invoice amount"; - } - else if ( inv.Amount == payment.Amount ) - { - switch ( inv.Type ) - { - case InvoiceType.Standard: - inv.AmountPaid = payment.Amount; - inv.TaxAmount = payment.Amount * 0.14m; - inv.Payments.Add( payment ); - responseMessage = "invoice is now fully paid"; - break; - case InvoiceType.Commercial: - inv.AmountPaid = payment.Amount; - inv.TaxAmount = payment.Amount * 0.14m; - inv.Payments.Add( payment ); - responseMessage = "invoice is now fully paid"; - break; - default: - throw new ArgumentOutOfRangeException( ); - } - } - else - { - switch ( inv.Type ) - { - case InvoiceType.Standard: - inv.AmountPaid = payment.Amount; - inv.TaxAmount = payment.Amount * 0.14m; - inv.Payments.Add( payment ); - responseMessage = "invoice is now partially paid"; - break; - case InvoiceType.Commercial: - inv.AmountPaid = payment.Amount; - inv.TaxAmount = payment.Amount * 0.14m; - inv.Payments.Add( payment ); - responseMessage = "invoice is now partially paid"; - break; - default: - throw new ArgumentOutOfRangeException( ); - } - } - } - } - } - - inv.Save(); - - return responseMessage; - } - } + public class InvoiceService + { + private readonly InvoiceRepository _invoiceRepository; + private readonly ITaxCalculator _taxCalculator; + private readonly IPaymentValidator _paymentValidator; + + public InvoiceService(InvoiceRepository invoiceRepository) + : this(invoiceRepository, new TaxCalculator(), new PaymentValidator()) + { + } + + public InvoiceService(InvoiceRepository invoiceRepository, ITaxCalculator taxCalculator, IPaymentValidator paymentValidator) + { + _invoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); + _taxCalculator = taxCalculator ?? throw new ArgumentNullException(nameof(taxCalculator)); + _paymentValidator = paymentValidator ?? throw new ArgumentNullException(nameof(paymentValidator)); + } + + public string ProcessPayment(Payment payment) + { + if (payment == null) + throw new ArgumentNullException(nameof(payment)); + + var invoice = GetInvoice(payment.Reference); + + var validationResult = _paymentValidator.Validate(invoice, payment); + if (!validationResult.IsValid) + return validationResult.ErrorMessage; + + var result = ApplyPayment(invoice, payment); + + invoice.Save(); + + return result.Message; + } + + private Invoice GetInvoice(string reference) + { + var invoice = _invoiceRepository.GetInvoice(reference); + if (invoice == null) + throw new InvalidOperationException("There is no invoice matching this payment"); + + return invoice; + } + + private PaymentResult ApplyPayment(Invoice invoice, Payment payment) + { + var currentAmountPaid = GetCurrentAmountPaid(invoice); + var remainingAmount = invoice.Amount - currentAmountPaid; + + // Add payment to invoice + invoice.EnsurePaymentsCollection(); + invoice.Payments.Add(payment); + invoice.AmountPaid = currentAmountPaid + payment.Amount; + + // Calculate tax + var taxAmount = _taxCalculator.Calculate(payment.Amount, invoice.Type); + invoice.TaxAmount += taxAmount; + + return DeterminePaymentResult(invoice, payment, remainingAmount); + } + + private decimal GetCurrentAmountPaid(Invoice invoice) + { + if (invoice.Payments == null) + return 0; + + return invoice.Payments.Sum(p => p.Amount); + } + + private PaymentResult DeterminePaymentResult(Invoice invoice, Payment payment, decimal remainingAmount) + { + if (payment.Amount == remainingAmount) + { + return HasExistingPayments(invoice) + ? new PaymentResult("final partial payment received, invoice is now fully paid") + : new PaymentResult("invoice is now fully paid"); + } + + return HasExistingPayments(invoice) + ? new PaymentResult("another partial payment received, still not fully paid") + : new PaymentResult("invoice is now partially paid"); + } + + private bool HasExistingPayments(Invoice invoice) + { + return invoice.Payments != null && invoice.Payments.Any(); + } + } + + public class PaymentResult + { + public PaymentResult(string message) + { + Message = message; + } + + public string Message { get; } + } } \ No newline at end of file diff --git a/RefactorThis.Domain/PaymentValidator.cs b/RefactorThis.Domain/PaymentValidator.cs new file mode 100644 index 0000000..3f729e8 --- /dev/null +++ b/RefactorThis.Domain/PaymentValidator.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using RefactorThis.Persistence; + +namespace RefactorThis.Domain +{ + public interface IPaymentValidator + { + ValidationResult Validate(Invoice invoice, Payment payment); + } + + public class PaymentValidator : IPaymentValidator + { + public ValidationResult Validate(Invoice invoice, Payment payment) + { + if (invoice == null) + return ValidationResult.Invalid("Invoice not found"); + + if (payment == null) + return ValidationResult.Invalid("Payment cannot be null"); + + if (payment.Amount <= 0) + return ValidationResult.Invalid("Payment amount must be greater than zero"); + + return ValidateInvoiceState(invoice, payment); + } + + private ValidationResult ValidateInvoiceState(Invoice invoice, Payment payment) + { + if (invoice.Amount == 0) + { + if (invoice.Payments != null && invoice.Payments.Any()) + throw new InvalidOperationException("The invoice is in an invalid state, it has an amount of 0 and it has payments."); + + return ValidationResult.Invalid("no payment needed"); + } + + var currentAmountPaid = 0m; + if (invoice.Payments != null) + currentAmountPaid = invoice.Payments.Sum(p => p.Amount); + + if (IsInvoiceFullyPaid(invoice, currentAmountPaid)) + return ValidationResult.Invalid("invoice was already fully paid"); + + return ValidatePaymentAmount(invoice, payment, currentAmountPaid); + } + + private bool IsInvoiceFullyPaid(Invoice invoice, decimal currentAmountPaid) + { + return currentAmountPaid > 0 && invoice.Amount == currentAmountPaid; + } + + private ValidationResult ValidatePaymentAmount(Invoice invoice, Payment payment, decimal currentAmountPaid) + { + var remainingAmount = invoice.Amount - currentAmountPaid; + + if (payment.Amount > remainingAmount) + { + var message = HasExistingPayments(invoice) + ? "the payment is greater than the partial amount remaining" + : "the payment is greater than the invoice amount"; + + return ValidationResult.Invalid(message); + } + + return ValidationResult.Valid(); + } + + private bool HasExistingPayments(Invoice invoice) + { + return invoice.Payments != null && invoice.Payments.Any(); + } + } + + public class ValidationResult + { + public bool IsValid { get; } + public string ErrorMessage { get; } + + private ValidationResult(bool isValid, string errorMessage = null) + { + IsValid = isValid; + ErrorMessage = errorMessage; + } + + public static ValidationResult Valid() => new ValidationResult(true); + public static ValidationResult Invalid(string message) => new ValidationResult(false, message); + } +} \ No newline at end of file diff --git a/RefactorThis.Domain/RefactorThis.Domain.csproj b/RefactorThis.Domain/RefactorThis.Domain.csproj index 753e893..e6983e2 100644 --- a/RefactorThis.Domain/RefactorThis.Domain.csproj +++ b/RefactorThis.Domain/RefactorThis.Domain.csproj @@ -1,59 +1,61 @@  - - - Debug - AnyCPU - {5310B2FE-E26D-414E-B656-1F74C5A70368} - Library - Properties - RefactorThis.Domain - RefactorThis.Domain - v4.7.2 - 512 - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - {33cdc796-ff75-449c-9637-59c2efc46361} - RefactorThis.Persistence - - - - - - + \ No newline at end of file diff --git a/RefactorThis.Domain/TaxCalculator.cs b/RefactorThis.Domain/TaxCalculator.cs new file mode 100644 index 0000000..7d495fc --- /dev/null +++ b/RefactorThis.Domain/TaxCalculator.cs @@ -0,0 +1,29 @@ +using System; +using RefactorThis.Persistence; + +namespace RefactorThis.Domain +{ + public interface ITaxCalculator + { + decimal Calculate(decimal amount, InvoiceType invoiceType); + } + + public class TaxCalculator : ITaxCalculator + { + private const decimal StandardTaxRate = 0.14m; + private const decimal CommercialTaxRate = 0.14m; + + public decimal Calculate(decimal amount, InvoiceType invoiceType) + { + switch (invoiceType) + { + case InvoiceType.Standard: + return amount * StandardTaxRate; + case InvoiceType.Commercial: + return amount * CommercialTaxRate; + default: + throw new ArgumentOutOfRangeException(nameof(invoiceType), invoiceType, "Unknown invoice type"); + } + } + } +} \ No newline at end of file