Skip to content
Closed
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
481 changes: 241 additions & 240 deletions RefactorThis.Domain.Tests/InvoicePaymentProcessorTests.cs

Large diffs are not rendered by default.

131 changes: 72 additions & 59 deletions RefactorThis.Domain.Tests/RefactorThis.Domain.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,67 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7971BDEC-EAD1-4FB8-A4F5-B1F67E4F6355}</ProjectGuid>
<ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>RefactorThis.Domain.Tests</RootNamespace>
<AssemblyName>RefactorThis.Domain.Tests</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="nunit.framework, Version=3.5.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb">
<HintPath>..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="InvoicePaymentProcessorTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RefactorThis.Domain\RefactorThis.Domain.csproj">
<Project>{5310b2fe-e26d-414e-b656-1f74c5a70368}</Project>
<Name>RefactorThis.Domain</Name>
</ProjectReference>
<ProjectReference Include="..\RefactorThis.Persistence\RefactorThis.Persistence.csproj">
<Project>{33cdc796-ff75-449c-9637-59c2efc46361}</Project>
<Name>RefactorThis.Persistence</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7971BDEC-EAD1-4FB8-A4F5-B1F67E4F6355}</ProjectGuid>
<ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>RefactorThis.Domain.Tests</RootNamespace>
<AssemblyName>RefactorThis.Domain.Tests</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Castle.Core, Version=5.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL">
<HintPath>..\packages\Castle.Core.5.1.1\lib\net462\Castle.Core.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Xml" />
<Reference Include="nunit.framework, Version=3.5.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb">
<HintPath>..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="InvoicePaymentProcessorTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RefactorThis.Domain\RefactorThis.Domain.csproj">
<Project>{5310b2fe-e26d-414e-b656-1f74c5a70368}</Project>
<Name>RefactorThis.Domain</Name>
</ProjectReference>
<ProjectReference Include="..\RefactorThis.Persistence\RefactorThis.Persistence.csproj">
<Project>{33cdc796-ff75-449c-9637-59c2efc46361}</Project>
<Name>RefactorThis.Persistence</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->

</Project>
</Project>
224 changes: 86 additions & 138 deletions RefactorThis.Domain/InvoiceService.cs
Original file line number Diff line number Diff line change
@@ -1,149 +1,97 @@
using System;
using System.Linq;
using RefactorThis.Persistence;
using RefactorThis.Domain.common;
using RefactorThis.Domain.common.enums;
using RefactorThis.Persistence.enums;
using RefactorThis.Persistence.models;
using RefactorThis.Persistence.repositories;

namespace RefactorThis.Domain
{
public class InvoiceService
{
private readonly InvoiceRepository _invoiceRepository;
public class InvoiceService
{
private readonly IInvoiceRepository _invoiceRepository;
private const decimal TaxRate = 0.14m;

public InvoiceService( InvoiceRepository invoiceRepository )
{
_invoiceRepository = invoiceRepository;
}
public InvoiceService(IInvoiceRepository invoiceRepository)
{
_invoiceRepository = invoiceRepository;
}

/// <summary>
/// Processes a payment by applying it to the matching invoice if valid.
/// </summary>
/// <param name="payment">The payment information to be applied to an invoice.</param>
/// <returns>
/// A <see cref="ProcessPaymentStatus"/> indicating the result of the operation,
/// such as full payment, partial payment, overpayment, or invoice not found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown if no invoice matches the payment reference or the invoice is in an invalid state.
/// </exception>
public ProcessPaymentStatus ProcessPayment(Payment payment)
{
var invoice = _invoiceRepository.GetByReference(payment.Reference) ??
throw new InvalidOperationException(ProcessPaymentExceptionMessage.NoInvoiceMatchingPayment);

if (invoice.Amount == 0)
{
if (invoice.Payments == null || !invoice.Payments.Any())
{
return ProcessPaymentStatus.NoPaymentNeeded;
}

public string ProcessPayment( Payment payment )
{
var inv = _invoiceRepository.GetInvoice( payment.Reference );
throw new InvalidOperationException(ProcessPaymentExceptionMessage.InvalidInvoiceState);
}

var responseMessage = string.Empty;
var totalPaid = invoice.Payments?.Sum(x => x.Amount) ?? 0;
var remaining = invoice.Amount - invoice.AmountPaid;

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();
if (totalPaid >= invoice.Amount)
{
return ProcessPaymentStatus.InvoiceAlreadyFullyPaid;
}

return responseMessage;
}
}
if (totalPaid > 0 && payment.Amount > remaining)
{
return ProcessPaymentStatus.PartialPaymentExistsAndAmountPaidExceedsAmountDue;
}

if (totalPaid == 0 && payment.Amount > invoice.Amount)
{
return ProcessPaymentStatus.NoPartialPaymentExistsAndAmountPaidExceedsInvoiceAmount;
}

var isFullPayment = payment.Amount == remaining;

ApplyPayment(invoice, payment, isFullPayment);

return totalPaid == 0
? isFullPayment
? ProcessPaymentStatus.InvoiceAlreadyFullyPaid
: ProcessPaymentStatus.PartialPaymentExistsAndAmountPaidIsLessThanAmountDue
: isFullPayment
? ProcessPaymentStatus.PartialPaymentExistsAndAmountPaidEqualsAmountDue
: ProcessPaymentStatus.PartialPaymentExistsAndAmountPaidIsLessThanAmountDue;
}

/// <summary>
/// Applies a payment to the given invoice and updates tax if applicable.
/// </summary>
/// <param name="invoice">The invoice to apply the payment to.</param>
/// <param name="payment">The payment details.</param>
/// <param name="applyTax">Indicates whether tax should be applied for this payment.</param>
private void ApplyPayment(Invoice invoice, Payment payment, bool applyTax)
{
invoice.AmountPaid += payment.Amount;
invoice.Payments.Add(payment);

if (invoice.Type == InvoiceType.Commercial || applyTax)
{
invoice.TaxAmount += payment.Amount * TaxRate;
}

_invoiceRepository.Update(invoice);
}
}
}
Loading